openapi: 3.0.3 info: title: TON PRIMES read-proxy API version: 0.3.0 description: > Fastify caching proxy over the TON PRIMES contracts' get-methods (IMPLEMENTATION_PLAN.md §2.2 item 1). Every stats endpoint carries a `provenance` field naming the exact get-method and contract address the data came from — CONCEPT.md §9.1's honesty constraint: "every number the UI shows must map to a public get-method ... and the UI links each stat to its source call." This is what FRONTEND_PLAN.md's proof-link drawer renders. Consumed by the Mini App/webapp (see FRONTEND_PLAN.md) and the landing page. ## 0.3.0 — the `/bank/*` namespace is gone (CONCEPT.md §3.4) CONCEPT.md §3.4 drops the "bank" vocabulary: there is no bank, there is a **ledger** (`T`, `S`, `div_acc`, the head, the six-line split) and a **ratchet** (`pending`, `flush()`, the DeDust leg). The `/bank/*` deprecated aliases shipped in 0.2.0 have been removed outright; the routes are `/ledger/*` and `/ratchet/*`. ## Failure modes, and what each one means - **429 + `Retry-After`** — the upstream RPC provider is rate-limiting this proxy. Not a server error, and previously (wrongly) reported as a 500. Back off and retry; the proxy also serves the last good value stale-while-revalidate wherever it has one, so a 429 means it had nothing cached at all. - **503** — the relevant contract address env var is not configured. ## Provenance and freshness `provenance.source` is `live`, `cached`, or `stale`. `stale` means the revalidating chain read failed and this is the last good value; `provenance.fetchedAt` always states the true age, and the response also carries `Warning: 110`. The UI is expected to mark a stale stat visibly rather than present it as current. servers: - url: http://localhost:4000 description: Local / docker-compose default - url: https://primes.live/api description: Deployed tags: - name: ledger description: Splitter + ledger contract — T, S, div_acc, head, k, k_max (CONCEPT.md §3.4). - name: ratchet description: Ratchet contract — pending, swapped, flush status (CONCEPT.md §4.3, §12). - name: numbers - name: assets description: > The asset side's cash position (CONCEPT.md §5.4). The only routes in this document that return a **derived** number rather than a projection of chain state: §9.1 permits a rate to be displayed if the derivation from get-methods is published next to it, so every response here carries its own inputs and arithmetic. - name: lots description: > The won-lot record and the fee that funds its settlement. Winning an ascending lot MINTS the number in the transaction that closes it (D-90): nothing is held, nothing waits for the head. Prices are quoted by `/trophy/price/{n}`. - name: registrars - name: trophy description: > THE MARKET's lot lanes (CONCEPT.md §4.4; DECISIONS.md D-3…D-8, D-30…D-33) — the ONE route to a number ahead of the head, and it depends only on primality. A composite is acquired by opening an ascending lot (opening IS a paid first bid; there is no score gate and no distance gate); a prime is bought on the descending Dutch lane, openable before its turn at a flat `P0(p)`, with the clock starting at the turn and terminating at exactly 1 TON. What is priced is **the right to buy**, not the number. The flat-fee reservation lane these routes used to sit beside is DELETED (D-30), so `/trophy/*` is now the whole acquisition read surface. Every lot here is bypassable by waiting, and that is the design, not a leak — the mint is 1 TON at the head, always. - name: treasury description: > DECISIONS.md D-100 / CONCEPT.md §5.5 — the GOVERNED TREASURY (`primes_treasury.tolk`), the LP MINER and its per-owner position children (`primes_lp_position.tolk`), and the LP SINK that holds locked depth (`primes_lp_sink.tolk`). The team's money under holder control; the game's economics stay under nobody's. Nothing in this group mints (TR-4), and nothing in it moves `T`, `S`, `pending`, `owed`, `k`, `K_CEIL` or the five-way split. **Locked depth is never summed into `T` or into the floor** (D-100 point 4): a pool's TON/PRIMES composition moves with every trade, so an LP-derived term would void §5's ratchet theorem. Two guarantees, two panels. - name: audit - name: events description: event-poller-backed, not get-method-backed — see /feed's provenance note. components: schemas: Provenance: type: object description: > CONCEPT.md §9.1 honesty field. Present on every data-bearing response, at the top level or as an array on composite endpoints (GET /number/{n}, GET /solvency). required: [getMethod, contractAddress, source, fetchedAt] properties: getMethod: type: string description: > The exact get-method name(s) called, joined with "+" if more than one. Where a route is a projection of a composite read (e.g. /ledger/T over /ledger/floor) this names the specific get-method that produced the returned number, not the composite. example: getFloorNum+getFloorDen contractAddress: type: string description: The exact contract address the get-method was called against. source: type: string enum: [live, cached, stale] description: > "live" = fetched from the chain this request; "cached" = served from Redis within its TTL; "stale" = the revalidating read failed (upstream throttle) and this is the last good value, served past its TTL with its true `fetchedAt`. In all three cases `getMethod`/`contractAddress` name the real origin. fetchedAt: type: string format: date-time description: When the underlying chain read actually happened (not necessarily now). NotDeployed: type: object description: > The answer when a read resolves to an address with **no account behind it** — a `200`, not a `500`, and the same `{data: {found: false}}` shape `/trophy/lot/{n}` already returns for a number with no lot. On this protocol an uninit address is a normal, expected state rather than a fault: a number's NFT item is deployed by the message that first needs it (CONCEPT.md §3.1), so every number at or ahead of the mint head derives to a valid address with nothing deployed at it. The live providers report that as TVM `exit_code: -13`, which is NOT TVM's own 13 and is NOT the unknown-selector 11: 11 is a fact about the deployed bytecode, this is a fact about `n`. Any route that reads a per-number address can answer in this shape. The provenance envelope rides along, because §9.1 makes no exception for a negative answer: "there is no item here yet" was still read from the chain, and the reader is entitled to the get-method and the address it was read at. required: [data, code, reason, provenance] properties: data: type: object required: [found] properties: found: type: boolean enum: [false] code: type: string enum: [CONTRACT_NOT_DEPLOYED] reason: type: string description: Human-readable statement of which get-method could not run, and where. provenance: { $ref: "#/components/schemas/Provenance" } ConfigProvenance: type: object description: > The deliberate counterpart to Provenance, and a distinct schema rather than a fourth `source` value. Provenance asserts that a get-method was called and produced a number. Deployment addresses cannot assert that — they are the *input* to every such call. Serving them through the same shape would let configuration pass for chain-verified state, which is the confusion CONCEPT.md §9.1 exists to prevent. required: [source, configuredFrom, verifyWith, note, fetchedAt] properties: source: type: string enum: [config] description: Always "config". No chain read occurred. configuredFrom: type: object additionalProperties: { type: string } description: Env var or manifest key each address was read from. verifyWith: type: object additionalProperties: { type: string } description: > Per role, the get-method a reader may call to confirm the address independently. A pointer for the proof drawer — this service does not perform these reads. note: { type: string } fetchedAt: { type: string, format: date-time } Error: type: object properties: error: type: string RateLimitedBody: type: object properties: error: { type: string } code: { type: string, enum: [UPSTREAM_RATE_LIMITED] } retryAfterSeconds: { type: integer } NAboveCapBody: type: object description: > DECISIONS.md D-11 / D-30 — `n` is above `AUCTIONABLE_MAX`, so no lot can be opened for it and `getLotPrice(n)` throws exit code 1020 (`mkt_n_above_cap`) rather than quoting. The guard runs BEFORE Miller-Rabin deliberately: an unguarded 78-digit argument overflows int257 inside `modpow` and surfaces as exit 4 instead of a refusal a UI can explain. Note what is NOT in this object: any field that could be read as a price. CONCEPT.md §9.1's rule is that a number nothing will sell is not quoted at zero as if it were free, and the contract's refusal to quote is only honest if the proxy does not invent the quote back. The number is still MINTABLE — at 1 TON, at the head, like every other number — it simply cannot be acquired ahead of the head. REPLACES `AuctionLaneOnlyBody` / exit 507. D-30 deletes the flat-fee lane, so there is no second venue to be redirected to and no number that is "for sale, but not here"; 506 and 507 are both retired and neither value is reused. properties: error: { type: string } code: { type: string, enum: [N_ABOVE_CAP] } exitCode: { type: integer, enum: [1020] } errorName: { type: string, enum: [mkt_n_above_cap] } seeOther: type: object properties: cap: { type: string, example: /trophy/params } head: { type: string, example: /ledger/head } provenance: { $ref: "#/components/schemas/Provenance" } ScoreWeights: type: object description: > D-99's weight table as `getScoreWeights()` publishes it (PRICING_MODEL_PLAN.md §1.3): 33 per-bit weights in BIT ORDER, then the continuous LEN terms and the one clamp. The weights are OWNER-DECLARED, not sim-derived, and `shared/opcodes.ts` `SCORE_BITS` carries the same table off chain — publishing the chain's copy here is what makes "the two agree" a checkable assertion rather than a comment about a copy-paste. properties: bits: type: array description: > One row per category, in bit order. `bit` is a PERMANENT identifier: it keys the `flags` word, this table's position, and the orb's fixed satellite angle. `points` is the CHAIN's figure, never the local table's. items: type: object properties: bit: { type: integer } key: { type: string, example: mersenne } group: { type: string, enum: [DIGIT, LIFE, FORM, CULTURE, POSITION] } points: { type: string } lenRef: type: string description: LEN pays `lenW · (lenRef − digits)` while the number has fewer digits than this. lenW: { type: string } tzCap: type: string description: > Bit 5 (`trailing_zeros`) pays its weight PER trailing zero, counted no higher than this — the one bit whose contribution is not simply `points`. scoreMax: type: string description: > The ONE clamp on the sum. `2^(87/10) = 420.4`, so this figure IS the 420 TON ceiling on an opening price; no other ceiling exists. VestingLock: type: object description: > The treasury's instance of `primes_vesting.tolk`, as `getVesting` + `getVestingSchedule` report it: 1,000,000 PRIMES on a 4 x 25% / 270-day schedule (D-100 point 6). The only vesting lock since D-166 deleted the operator's. properties: contractAddress: { type: string } beneficiary: { type: string } startTime: { type: integer } totalAmountNanoprimes: { type: string } releasedAmountNanoprimes: { type: string } unlocked: { type: integer } tranches: { type: integer } nextUnlockAt: { type: integer } intervalSeconds: { type: integer } releasableNowNanoprimes: { type: string } YieldSample: type: object description: > One (head, owed) reading taken by this proxy. The *values* are chain state read through the get-methods named in `provenance`; the *pairing* — that these two numbers were read at the same moment — is the proxy's assertion, which is what makes the rate derived rather than proved (CONCEPT.md §9.1, §12). properties: head: { type: string, description: The ledger's head at the moment of the reading. } owedNanoton: { type: string, description: The item's accrued tribute/referral, nanoTON. } claimableDividendNanoton: type: string nullable: true description: '`div_acc − entry[p]`; null until the flat dividend get-method ships.' at: { type: string, format: date-time } headers: Warning: description: '`110` when the payload is stale.' schema: { type: string } Retry-After: description: Seconds to wait before retrying after an upstream throttle. schema: { type: integer } responses: NotConfigured: description: The relevant contract address env var is not set. content: application/json: schema: $ref: "#/components/schemas/Error" RateLimited: description: > The upstream RPC provider is throttling this proxy and no cached value (fresh or within the stale grace window) was available to serve. Honour `Retry-After`. headers: Retry-After: { $ref: "#/components/headers/Retry-After" } content: application/json: schema: $ref: "#/components/schemas/RateLimitedBody" NAboveCap: description: > `n` is above `AUCTIONABLE_MAX`, so the market quotes no price for it. Explicitly NOT a 500, NOT a 404 (the route and the number are both real, and a get-method answered, decisively) and NOT a 200 carrying a null or a zero. 409 because the request is well-formed and the refusal is about the current state of `n`. content: application/json: schema: $ref: "#/components/schemas/NAboveCapBody" parameters: NumberPath: name: n in: path required: true schema: { type: string } description: Decimal string; may exceed 2^53. paths: /health: get: summary: Liveness check, plus load telemetry (no chain data, no provenance). description: > `inflightUpstreamCalls` and `staleResponsesServed` are the rate-limit signals ops/rate-limit-budget.md's measured re-run reads. responses: "200": description: OK content: application/json: schema: type: object properties: ok: { type: boolean } service: { type: string } inflightUpstreamCalls: { type: integer } staleResponsesServed: { type: integer } /contracts: get: summary: The deployment manifest — which address plays which CONCEPT.md §3.4 role. description: > Configuration, not chain state, and therefore carrying ConfigProvenance rather than Provenance. This is the address book every transaction-building client needs: without it the webapp had no source for the market address at all, and could only obtain the ledger address by mining it out of another route's chain-read receipt. Roles that this deployment has not configured are OMITTED, never null — a client must be able to fail loudly on a missing role rather than fall through to a wrong address. Makes no upstream call, so it has no 429. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object additionalProperties: { type: string } description: > Role → address. **Fifteen roles**, and the list is enforced rather than described: `backend/read-proxy/test/server.test.ts` reads `ReadProxyAddresses`' fields out of the service's own source and fails when this response omits one (`GAUNTLET.md` E18). ledger, ratchet, registrar, market, jettonMaster, bountyVault, venueTimelock, collection, stake, sink, constellationsCollection, generatorsCollection, treasuryVesting, treasury, lpSink, voucher, securityVoucher (D-170), primeQuestsVoucher (G78 / D-171). provenance: { $ref: "#/components/schemas/ConfigProvenance" } /meta/{line}/{file}: get: tags: [numbers] summary: TEP-64 item metadata and artwork for the GENERATORS and CONSTELLATIONS collections — json, svg, png, lottie.json, tgs. description: > The PUBLIC URLs are `https://primes.live/generator/.json` and `https://primes.live/constellation/.json` — genesis stamps those two prefixes into the collections' init data as their `commonContentPrefix`, so `.json` is what every marketplace, wallet and indexer resolves for those items, forever. Nothing served either path until this route existed: both fell through nginx to the SPA and answered `200 text/html`, which a crawler reads as "no metadata", so generators and constellations rendered as blank, nameless tiles however correct their chain data was. THE `/meta/` PREFIX IS INTERNAL. `ops/deploy/primes.live.conf` rewrites the public URL onto it. It exists because `/constellation/{t}` is already a route on this service (the app's build-state read, served under /api/) and Fastify refuses a second registration of the same pattern — rather than fold a chain-reading route and a chain-free one into one handler, the metadata pair got its own name. Callers use the public URL. THE `.json` TIER MAKES NO UPSTREAM CALL, on either line, under the same rule as `/nft/{file}`'s `.json` tier: one document per index, forever, crawled a whole collection at a time, so nothing that varies with chain state may appear there. THE ARTWORK TIERS DIFFER BY LINE, and that is DECISIONS.md D-125. A GENERATOR's tab is a pure function of its index and reads nothing. A CONSTELLATION's plate is an engraved star chart of its own BUILD — the operation as a seal, a seed star per input, the contributor count and the op's tier as bounded ladders, the closer's Miller-Rabin as the ink — so `/constellation/.{png,svg,tgs,lottie.json}` reads `getConstellationData()` ONCE per target and caches it for the life of the item. That is `nftArt.ts`'s base/final split: a document is crawled a collection at a time and must stay chain-free, an image is one request about one item. It can never go stale, because §3.2.2's "one target, one constellation, forever" makes a build immutable. AN UNBUILT TARGET IS NOT AN ERROR. It — and every failure to read a build, including an unconfigured or unreachable chain — draws the GHOST: the same figure with its lines not yet drawn, which is what an unbuilt target is. A marketplace grid cannot degrade gracefully from a missing tile. A ghost is therefore the one artwork response here that is NOT `immutable`: it is cached for 60s, because it can stop being true in the next block. Every other artwork response is a year and `immutable`. THE STILL IS SERVED BOTH WAYS, AND `image:` NAMES THE `.png`. A plate and a tab are pure geometry, so neither needs `/nft/{file}.png`'s two-layer raster pipeline, and the `.svg` is the sharp form a marketplace can scale to any tile size. But `image` is the ONLY field TEP-64 defines, and a wallet grid, a social card and a Telegram preview want a raster — so `image` points at `.png` on both lines (D-115's Scope paragraph, which asks for a PNG still for generators, and the same treatment for D-125's plate). The raster costs this host no rasterizer and no native dependency: `shared/art/markRaster.ts` walks the SAME mark list the `.svg` is emitted from onto the number line's own `Canvas` primitives, so the two are one drawing in two containers rather than two drawings. There is no `.mp4` tier here — the number line's is prime-only and ffmpeg-gated, and neither distinction exists on these two lines. The `.tgs` is the `.lottie.json` gzipped — one document served twice, validated against Telegram's rules before it is compressed. Both lines draw from a shared generator (`shared/art/constellationPlate.ts`, `shared/art/generatorTab.ts`) that the webapp imports too, so a marketplace tile and the in-app figure cannot drift apart. A GENERATOR INDEX CARRIES ITS OWN OPERATION. `primes_generators_collection.tolk` packs the index as `kind << 58 | generator_hash mod 2^58` — 63 bits, so an indexer's signed-int64 model does not truncate it, and `kind` in the top five bits, so `get_nft_address_by_index` can rebuild an item's init data exactly. That makes the operation and its tier derivable HERE, from the id alone, with no chain read: DECISIONS.md D-101 point 2's `/.json` arriving as one number rather than two path segments. An index whose top five bits name no op in 1..18 is one the collection cannot mint, and both tiers answer 400 for it rather than serving a plausible name or a picture of nothing. No provenance envelope, for the same reason `/nft/{file}` has none: this is a document defined by an external standard. parameters: - name: line in: path required: true schema: { type: string, enum: [generator, constellation] } description: > Which collection. `generator` is keyed on the packed 63-bit `generator_id`; `constellation` is keyed on the built TARGET integer (one target, one constellation, forever — D-102). - name: file in: path required: true schema: { type: string, pattern: "^[0-9]+\.(json|svg|png|tgs|lottie\.json)$" } description: > `.json` for the TEP-64 document, `.png` for the still `image:` points at, `.svg` for the same still as vector, `.lottie.json` for the animation a marketplace plays, and `.tgs` for the same document as a Telegram sticker. Both lines carry all five (D-115 for generators, D-125 for constellations). responses: "200": description: > TEP-64 metadata (application/json), the still (image/png or image/svg+xml), the Lottie document (application/json) or the sticker (application/x-tgsticker). content: application/json: schema: type: object required: [name, description, image, attributes] properties: name: { type: string, example: "Constellation 23" } description: { type: string } image: { type: string, format: uri } lottie: type: string format: uri description: Getgems extension key — the vector animation. Present on both lines. tgs: type: string format: uri description: Getgems extension key — the same document as a Telegram sticker. attributes: type: array items: type: object required: [trait_type, value] properties: trait_type: { type: string } value: { oneOf: [{ type: string }, { type: number }] } display_type: { type: string, enum: [number] } buttons: type: array items: type: object properties: label: { type: string } uri: { type: string, format: uri } image/svg+xml: schema: { type: string } image/png: schema: { type: string, format: binary } application/x-tgsticker: schema: { type: string, format: binary } "400": description: Malformed index; a constellation target below 2; or a generator index whose top five bits name no operation in 1..18. /nft/{file}: get: tags: [numbers] summary: TEP-64 item metadata and generated artwork for one number — json, lottie.json, tgs, png, gif, mp4. description: > The URL `primes_collection.tolk`'s `get_nft_content` composes for every minted number (`.json`), so this is what marketplaces, wallets and indexers resolve. Without it the items render blank however correct their on-chain data is. The `.json`, `.lottie.json` and `.tgs` forms make NO upstream call, and have no 429: TEP-64 item metadata carries name, description, image and attributes but not the owner — ownership is `get_nft_data` on the item, which a marketplace already reads — so every field there is a pure function of `n`. That is what makes an unbounded-cardinality route affordable; a crawler sweeping the whole collection costs CPU and costs the TonAPI budget nothing. It is also why the artwork is SEEDED from `n` rather than from mint-time chain entropy (CONCEPT.md §9.8): numbers mint exactly once and strictly in sequence, so `n` is already 1:1 with mints, and a stored seed would have to be read from the chain to serve the document. The RASTER forms (`.png`, `.gif`) are the exception, and a bounded one. Since ART_VERSION 2 the picture prints the number's mint date, which does not exist until the mint does, so those two make exactly one chain read per number — `getMintedDay()` on item(n) — cached for a year because a mint date is written once by the item's `Populate` handler and can never change. Once a number has been rendered it costs nothing again: the response carries `X-Primes-Art-Layer: final` for a minted number's permanent artwork and `base` for the dateless pre-mint layer, and the `Cache-Control` max-age follows that (a year for `final`, five minutes for `base`). WHAT TEP-64 ACTUALLY DEFINES: `uri`, `name`, `description`, `image`, `image_data` — and nothing for motion; its own text still lists standardising non-image content as an open question. `attributes`, `lottie`, `content_url`/`content_type` and `buttons` are all the de-facto Getgems extension. Consequently `image` must stand alone: a client that knows only TEP-64 sees the PNG, and the PNG is a finished picture rather than a poster frame. No provenance envelope, deliberately: this is a document defined by an external standard, and wrapping it would break every consumer that expects TEP-64 at this URL. Nothing varying with chain state may be added here — that belongs on /number/{n}. parameters: - name: file in: path required: true schema: { type: string, pattern: "^[0-9]+\\.(lottie\\.json|json|png|gif|mp4|tgs)$" } description: > `.json` for metadata, `.png` for the still, `.lottie.json` for the vector animation, `.tgs` for the same animation as a Telegram sticker (gzipped Lottie, 512x512, loop capped at 3s), `.gif` for the animation as a plain image file (every number — the fallback for the many surfaces that never play a Lottie), `.mp4` for the video tier (primes only, and only where the host can render it). responses: "200": description: > TEP-64 metadata (application/json), the Lottie document (application/json), the Telegram sticker (application/x-tgsticker), the still (image/png), the animation (image/gif), or the video (video/mp4). content: application/json: schema: type: object required: [name, description, image, attributes] properties: name: { type: string, example: "Prime 9973" } description: { type: string } image: { type: string, format: uri } lottie: type: string format: uri description: Getgems extension — the vector animation for this item. tgs: type: string format: uri description: > Ours, not Getgems' — the same animation as a Telegram sticker. A client that does not know the field ignores it; the surfaces that do are the Mini App, the bot and every forward of a mint. content_url: type: string format: uri description: > Getgems extension — the item's primary media. A composite always names its GIF (`image/gif`, D-174). A prime names its mp4, and only when the serving host can actually render it; advertising a URL that 404s would be cached by a marketplace for an hour. content_type: { type: string, example: "video/mp4", enum: ["video/mp4", "image/gif"] } buttons: type: array items: type: object properties: label: { type: string } uri: { type: string, format: uri } attributes: type: array items: type: object properties: trait_type: { type: string } value: oneOf: [{ type: string }, { type: number }] image/png: schema: { type: string, format: binary } image/gif: schema: { type: string, format: binary } video/mp4: schema: { type: string, format: binary } "400": description: Unrecognised suffix, or n outside the number line (n < 2, n > 2^256-1). content: application/json: schema: { $ref: "#/components/schemas/Error" } "404": description: > `.mp4` for a composite (its animation is the Lottie document), or on a host with no video renderer configured. content: application/json: schema: { $ref: "#/components/schemas/Error" } /nft/{n}/tree.png: get: tags: [numbers] summary: The lineage card — n's factorization tree, a pure function of n (GAUNTLET.md L4). description: > A caterpillar tree of n's full prime factorization with multiplicity: peel the smallest remaining prime off one side at a time until nothing composite is left. Leaves are cyan (prime), internal split nodes are amber (composite) — CLAUDE.md's frontend colour rule. A prime n is the degenerate case: a single leaf, no split. Zero chain reads, same discipline as `/nft/{file}`'s `.json` tier: structure and digits are derivable from n alone, so the response never varies and is cached forever. Owner and name attribution are LIVE facts and belong on `/number/{n}/name` and the caller's own overlay (a bot caption, a webapp card) — never baked into this PNG. Factorization is real CPU (trial division, then Pollard's rho) under the same budget `/nft/{file}.json`'s attributes use — an n too large to factor within that budget answers 404 rather than a guessed shape. parameters: - name: n in: path required: true schema: { type: string, pattern: "^[0-9]+$" } description: The number line index, 2 <= n <= 2^256-1. responses: "200": description: The factorization tree. content: image/png: schema: { type: string, format: binary } "400": description: n outside the number line (n < 2, n > 2^256-1), or not a plain integer. content: application/json: schema: { $ref: "#/components/schemas/Error" } "404": description: n's factorization exceeds this service's budget — no tree to draw. content: application/json: schema: { $ref: "#/components/schemas/Error" } # --------------------------------------------------------------------------- # /ledger/* # --------------------------------------------------------------------------- /ledger/counters: get: tags: [ledger] summary: T, S, div_acc and π(n) in ONE call — the composite the rate-limit budget is built on. description: > UPGRADE_PLAN_V5.md §5 C2/C3.1: one cache key for the whole counter block, on the slow tier (5 calls per 30s). T and S are also served every 5s from their own `ledger:floor` key (`/ledger/floor`, `/ledger/T`, `/ledger/S`), so the two T/S readings may differ by up to one slow-tier window; each carries its own `fetchedAt`. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: floorNumNanoton: { type: string, description: "T, in nanoTON" } floorDenPrimes: { type: string, description: "S, in smallest PRIMES unit" } divAccNanoton: type: string description: CONCEPT.md §3.2's flat prime-dividend accumulator. primeCount: type: string description: π(n) — primes minted so far (`getPrimesCount`). maxSupplyPrimes: type: string description: D-156's `S_MAX` (`getMaxSupply`) — the ceiling `S` approaches and never reaches. provenance: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /ledger/floor: get: tags: [ledger] summary: The internal floor price p_f = T/S (CONCEPT.md §5), as its raw numerator/denominator. description: Backed by `getFloorNum` (T) and `getFloorDen` (S), on the fast `ledger:floor` key. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: floorNumNanoton: { type: string } floorDenPrimes: { type: string } provenance: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /ledger/T: get: tags: [ledger] summary: T alone — total TON backing, nanoTON. Projection of /ledger/floor. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: floorNumNanoton: { type: string } provenance: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /ledger/S: get: tags: [ledger] summary: S alone — PRIMES supply, smallest unit. Projection of /ledger/floor. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: floorDenPrimes: { type: string } provenance: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /launch: get: tags: [ledger] summary: Has the game started — the one-shot launch flag and the collection's item count. description: > Backed by `getLaunch` on the ledger (CONCEPT.md §6 point 0). `ignite` mints Unity (n = 1) to the founder and flips `launched`; until it does, every player-facing money path on chain refuses — `mint`, `mint_won_lot`, opening a trophy lot, and bidding on a genesis lot. A client that offers those controls while `launched` is false is offering transactions the contracts reject at the cost of the sender's gas. `itemsMinted` is `head − 2 + launched`, computed **on chain**. Unity is minted outside the sequential run and does not advance the head, so the item count and the head differ by exactly the flag; §9.1 requires the published count to be a get-method's answer rather than arithmetic performed in the client. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: launched: { type: boolean, description: "True once `ignite` has landed. One-way." } itemsMinted: type: string description: "NFTs in the collection, Unity included. `head − 2 + launched`." marketLaunched: type: [boolean, "null"] description: "DESIGN.md G34 — the MARKET's own `getLaunched()`, a separate flag from the ledger's. Every market entry point throws `mkt_not_launched` (1022) while false. Null = not read (no market address, or no account behind it)." marketLaunchedProvenance: oneOf: - { $ref: "#/components/schemas/Provenance" } - { type: "null" } description: Names the MARKET address; the envelope's `provenance` names the ledger. provenance: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /ledger/head: get: tags: [ledger] summary: The sequential mint head — the next number to be minted. description: Backed by `getHead`. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: head: { type: string } provenance: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /ledger/last-mint-time: get: tags: [ledger] summary: Unix seconds of the last settled mint — the origin of `t` for /ledger/k. description: > Backed by `getLastMintTime`. `getK` is a pure curve, so without this the *current* point on it cannot be reconstructed from chain state and the UI's live rebate clock would rest on an indexer's timestamp (CONCEPT.md §9.1, ceremony mode). `0` is the contract's cold-start sentinel — nothing has been minted yet — not a timestamp. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: lastMintTime: { type: string, description: "Unix seconds; \"0\" = no mint yet." } provenance: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /ledger/k: get: tags: [ledger] summary: k(t) at a given (n, t) — the rebate-share fixed-point curve (CONCEPT.md §5). description: > Backed by `getK`. NOTE for callers: each distinct `t` is its own cache key, so polling this with a `t` that advances every second makes the cache useless. Quantize `t` to the poll interval, or compute the curve client-side from `/ledger/kmax`. parameters: - name: n in: query required: true schema: { type: string } - name: t in: query required: true schema: { type: string } description: Seconds elapsed since the previous mint. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: k: { type: string, description: "Fixed-point k(t), contract's own scale." } provenance: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /ledger/kmax: get: tags: [ledger] summary: k_max(n) — the prime-density-decayed emission cap (CONCEPT.md §5). 30s tier. description: > Backed by `getKMax`. Moved to the 30s TTL tier by UPGRADE_PLAN_V5.md §5 C3.2 — it moves only as n advances, so a 5s key bought nothing and cost 0.2 req/s. parameters: - name: n in: query required: true schema: { type: string } responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: kMax: { type: string } provenance: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } # --------------------------------------------------------------------------- # Fast load: one cold-load round trip, and a push channel (FASTLOAD_PLAN.md) # --------------------------------------------------------------------------- /bootstrap: get: tags: [ledger] summary: Every above-the-fold ledger value in one round trip — head, lastMintTime, k, kMax, floor. description: > Adds no cache key and no upstream call of its own: every field is read through the SAME key the individual `/ledger/*` route serves from, so a warm cache answers this in one round trip for zero upstream cost (ops/rate-limit-budget.md, pinned by test/budget.test.ts). What it removes is a WATERFALL — `k` is `getK(n, t)`, so a client cannot ask for it until it knows the head and the last mint time, which cost two to three sequential round trips before this route existed. `n` and `t` are resolved server-side and returned in `kArgs`, so the caller can place `k` in its own cache under the key it will later look up. `t` is bucketed by the one rule in `@ton-primes/shared/readSurface`. EVERY FIELD IS NULLABLE. One throttled or failed read must not take the other four down with it; a field that could not be resolved is `null` and its reason is named in `unavailable`. Callers are expected to keep their per-route polling as the fallback and the steady-state path — this route only decides when the FIRST value arrives. responses: "200": description: OK. Nullable fields are absent data, never a fabricated zero. content: application/json: schema: type: object properties: head: nullable: true type: object properties: data: type: object properties: head: { type: string } provenance: { $ref: "#/components/schemas/Provenance" } lastMintTime: nullable: true description: Null when `getLastMintTime` could not be read. type: object properties: data: type: object properties: lastMintTime: { type: string } provenance: { $ref: "#/components/schemas/Provenance" } k: nullable: true description: Null when there is no last-mint origin, so `t` is unknown. type: object properties: data: type: object properties: k: { type: string } provenance: { $ref: "#/components/schemas/Provenance" } kMax: nullable: true type: object properties: data: type: object properties: kMax: { type: string } provenance: { $ref: "#/components/schemas/Provenance" } floor: nullable: true type: object properties: data: type: object properties: floorNumNanoton: { type: string } floorDenPrimes: { type: string } provenance: { $ref: "#/components/schemas/Provenance" } kArgs: nullable: true description: The exact (n, t) `k` was read at. type: object properties: n: { type: string } t: { type: string } unavailable: type: array items: type: object properties: field: { type: string } reason: { type: string } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /events: get: tags: [ledger] summary: Server-Sent Events — a `mint` frame the moment a mint is indexed. description: > `text/event-stream`. On connect the server writes a `retry:` hint and a `: connected` comment, then a `: keepalive` comment every 25s so no intermediary closes the stream as idle. When the event-poller reports a newly indexed mint (over the Redis channel `primes:mint`), the proxy re-reads `getHead` and `getLastMintTime`, rewrites the cache entries the polled routes serve from, and emits one frame: `event: mint` with `data: {"n":"...","at":"...","bootstrap":{...}}`, where `bootstrap` is exactly the `/bootstrap` payload above. This route documents no 429: an upstream throttle during a push is logged and the frame is skipped — connected clients fall back to their polling, which FASTLOAD_PLAN.md retains explicitly — so a throttle never becomes a status code on this stream. Deployments must not buffer it (see the `location = /api/events` block in `ops/deploy/primes.live.conf`); a buffered SSE stream arrives batched, which is indistinguishable from this route not working. responses: "200": description: The stream. Stays open until the client disconnects. content: text/event-stream: schema: { type: string } "503": description: Too many open streams (`SSE_CAPACITY`). Poll instead. # --------------------------------------------------------------------------- # /ratchet/* # --------------------------------------------------------------------------- /ledger/seed-t: get: tags: [ledger] summary: seedT — the `seeded` term of T = seeded + swapped + pending (D-26). 30s tier. description: > Backed by `getSeedT` on the ledger. DECISIONS.md D-26 / CONCEPT.md §5: `T` has THREE terms, not two. `seedT` is the founder capital genesis credits straight into `T`; it backs `S` from the DeDust LP and never passes through the ratchet's `pending`, so a two-term reading of the identity (`T == swapped + pending`) reports a permanent FALSE SOLVENCY GAP. A reader closes the identity with three get-calls across two contracts: this one, plus `/ratchet/swapped` and `/ratchet/pending`. The identity holds EXACTLY, as bigints. D-26 forbids closing the gap by any other route — do not net it, do not approximate it, and do not compute the missing term client-side. Its own key rather than a field on `/ledger/counters`, and the slow tier: `seedT` moves only at genesis and at D-91 point 4's permissionless `donate_floor` — a deliberate, rare operator action, not a per-mint quantity — while the counters move on every mint. Folding it into the 5s key would spend upstream budget forever to re-learn a number that changes a handful of times in a deployment's life. D-91 point 5 retires the earlier claim that `seedT` is written once and never again; what is one-shot is GENESIS, not the field. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: seedTNanoton: { type: string } provenance: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /ledger/era-leader: get: tags: [ledger] summary: The era trophy's live standing — leader and era-scoped recruit count (D-36). 30s tier. description: > Backed by `getEraLeader` on the ledger. DECISIONS.md D-36 / CONCEPT.md §4.7, §7: each primorial era runs a recruiting contest, and at the ceremony the ledger names the era's single highest recruiter, hands that wallet the NAMING RIGHT of the primorial itself, and resets every wallet's era-scoped count to zero. `leader` is `null` — never an empty string and never the zero address — when the chain returns `addr_none`, which is the honest "nobody has activated a recruit this era" reading. `recruits` is `"0"` beside it, and the pair means the ceremony will send no `award_era_trophy` and the boundary will be minted UNNAMED. A client must render that as its own state, not as a blank leader with a zero. D-106: a recruit counts on the RECRUIT EDGE — a §4.5 gift mint to an address the ledger has never seen — and ties go to whoever reached the count first (the running max moves on strict `>` only). Gifting the same wallet twice counts once. NOT the same contest as `recruitRank.recruits` on `/earn/{address}`, which is LIFETIME and never reset. A wallet's own ERA-SCOPED count is `eraRecruits`, also on `/earn/{address}`. A surface that shows one under the other's label shows the wrong contest. Its own route rather than a field on `/ledger/counters`: the pair moves on an activation edge, i.e. mint cadence at best, so the 30s tier is the honest cadence, and it is the era's GLOBAL standing rather than a per-wallet read. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: leader: type: string nullable: true description: > Raw-form address, or `null` for `addr_none` — no activation this era. recruits: { type: string } provenance: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /ratchet/pending: get: tags: [ratchet] summary: TON credited to the pool but not yet swapped (CONCEPT.md §4.3's `pending`). description: > Backed by `getPending` on the ratchet. Render this next to the floor WITH the reason it is there and with `T = swapped + pending` as the identity it is (UPGRADE_PLAN_V5.md §6 D3) — shown naked, the project's most auditable claim becomes its most suspicious-looking number. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: pendingNanoton: { type: string } provenance: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /ratchet/swapped: get: tags: [ratchet] summary: Cumulative TON actually swapped and burned via flush() (CONCEPT.md §4.3/§5). description: Backed by `getSwapped` on the ratchet. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: swappedNanoton: { type: string } provenance: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /ratchet/flush-status: get: tags: [ratchet] summary: Last flush outcome — never / filled / bounced — with timestamp and cooldown. description: > Backed by `getFlushStatus`. CONCEPT.md §12 makes this non-optional: under the floor guard, "the venue is gone" and "the market is above the floor" look identical from outside without it, and a dead venue can hide behind months of legitimate-looking non-execution. `never` is a real state. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: lastAttemptTime: { type: string, description: Unix seconds. } outcome: type: string enum: [never, filled, bounced] amountNanoton: { type: string } cooldownRemainingSeconds: { type: string } provenance: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /ratchet/floor-cache: get: tags: [ratchet] summary: The (T, S) the FLOOR GUARD prices against — the ratchet's mirror, not the ledger's pair. description: > Backed by `getFloorCache` on the ratchet. **Not the same numbers as `/ledger/floor`.** The ledger's pair is authoritative and moves on every mint; the ratchet's is a mirror pushed with each `PoolInject`, and `computeMinOut` — the floor guard of CONCEPT.md §4.3.1 — reads the mirror. Anything previewing a flush or sizing a tranche must read THIS pair: previewing against the ledger's is previewing a guard the chain will not apply. The contract publishes the mirror separately for exactly that reason, so its staleness against the ledger is measurable rather than assumed. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: tCacheNanoton: { type: string, description: "`T` as the RATCHET last cached it." } sCacheNanoPrimes: { type: string, description: "`S` from the same push." } provenance: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /ratchet/staking: get: tags: [ratchet] summary: The staked reserve — `stakedCost`, `exiting`, the reserve target, the harvest clock and the realized loss. description: > Backed by `getStaking` on the ratchet. CONCEPT.md §4.3.2 / `DECISIONS.md` D-151: the ratchet's `pending` above a liquid reserve is deposited into the Tonstakers pool, held as tsTON and counted in `T` **at cost**, so `T = seeded + swapped + pending + stakedCost + exiting + realizedLoss`. All eight figures come from one get-method because they are read together: `pending + stakedCost + exiting` is the ratchet's whole obligation, `reserveTarget` is derived from that sum on every read and never stored, and a split assembled from several calls is a split whose parts do not sum to the total they are drawn against. `realizedLoss` is reported NEXT TO `stakedCost` and never netted into it. It is cumulative TON that left `T` and did not come back, and a non-zero value means `p_f` overstates the backing by exactly `realizedLoss / S` — §5's one external risk, published as a number rather than promised as an absence. `lastRateNum` / `lastRateDen` are the tsTON/TON rate as the PAIR it was learned as (off each `stake()`'s mint notification), so a reader checks the ratio rather than a quotient this service rounded. `lastHarvestTime` is `0` until the first harvest. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: stakedCostNanoton: { type: string, description: TON in the pool, at cost. A term of `T`. } tstonHeldNano: { type: string, description: nano-tsTON owned, excluding a tranche in the exit machine. } exitingNanoton: { type: string, description: A tranche on its way back, at its cost basis. A term of `T`. } lastRateNum: { type: string } lastRateDen: { type: string } reserveTargetNanoton: { type: string, description: Derived on chain from `pending + stakedCost + exiting`, never stored. } lastHarvestTime: { type: string, description: Unix seconds; `0` before the first harvest. } realizedLossNanoton: { type: string, description: Cumulative TON that left `T` and did not come back. } provenance: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /supply: get: tags: [ledger] summary: S, circulating supply, burns and vault payouts — as separate numbers, never netted. description: > CONCEPT.md §3.3 / §11.21 / §11.22. A bounty payout moves tokens into circulation without touching `S`; a burn removes tokens without touching `S`. Both change circulating supply in holders' favour, neither moves `p_f`, and the two must be reported as two dashboard numbers rather than netted into one. Every number here is one get-method on the one contract that owns it — `getFloorDen` on the ledger, `get_jetton_data` on the jetton master, `getBurnStats` on the ratchet, `getSinkBurnStats` on the burn sink, `getVaultPayouts` on the bounty vault, `get_wallet_data` on the vault's own jetton wallet — and `provenance` is an array with one entry per number, so nothing is attributed to a call it did not come from. There are TWO burn authorities and they are two fields. `burnedPrimes` is the ratchet's swap-and-burn; `sinkBurnedPrimes` is `primes_sink.tolk`'s paid annotations (§5.2 family 1). They are never summed here — §11.21 Q21 closed as two separate rows — and together they are the third term of the identity §3.3 publishes as checkable in three get-calls: `S − getUnmintedHeld() == jetton master total_supply + cumulative sink burns`. The vault, sink and voucher figures degrade to `null` (with their names listed in `unavailable`) when that role's address is not configured. `circulatingPrimes` and `mintable` degrade to `null` (GAUNTLET.md E11) if `get_jetton_data` itself throttles or fails on a cold cache — `sPrimes` never does, since it is served through the shared, independently-cached ledger counters. `burnCounting` is a separate flag from a null total: `false` means the ratchet's own jetton wallet has not been wired yet, so the total is a floor rather than a measurement. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: sPrimes: type: string description: Cumulative emission incl. the genesis vault. Never decrements (§5). maxSupplyPrimes: type: string description: > D-156's `S_MAX` — the ceiling `S` approaches and never reaches, read from `getMaxSupply()` on the ledger. Emission is multiplied by `(S_MAX - S)/S_MAX`, so the rebate rate falls to zero as `S` closes on the cap. circulatingPrimes: type: [string, "null"] description: > TEP-74 `total_supply` — minted minus burned. Null (with "circulatingPrimes" in `unavailable`) on a get_jetton_data read failure (GAUNTLET.md E11). mintable: type: [boolean, "null"] description: Null under the same condition as `circulatingPrimes` — same call. burnedPrimes: type: string description: Measured on the ratchet. burnEvents: { type: integer } burnCounting: type: boolean description: False = counter not armed, so the total is a floor, not a measurement. sinkBurnedPrimes: type: [string, "null"] description: >- Cumulative nano-PRIMES burned by `primes_sink.tolk` (§5.2 family 1 — paid name annotations). The OTHER burn authority, never summed into `burnedPrimes` above. Null (with "sinkBurnedPrimes" in `unavailable`) when `PRIMES_SINK_ADDRESS` is unset — an unwired sink is not a measured zero. sinkBurnEvents: { type: [integer, "null"] } sinkRefundedPrimes: type: [string, "null"] description: >- Cumulative nano-PRIMES the sink refunded — an unrecognised or retired action tag, a malformed payload, or an amount under the price. Published beside the burn total because the sink's own audit is that everything its wallet ever received is in exactly one of the two, which is what makes "its jetton balance at rest is zero" a claim a reader can test. sinkRefundEvents: { type: [integer, "null"] } unmintedPrimes: type: string description: >- D-52. Emission counted in `S` whose `jetton_mint` bounced and has not been replayed by `retry_mint`. Reads "0" on a healthy deployment. Not a loss of backing — `S` is never decremented (concept §3.3/§5), so a non-zero value means `p_f = T/S` understates backing, the conservative direction, and the emission is recoverable by anyone calling `retry_mint` for that address. vaultPaidPrimes: { type: [string, "null"] } vaultPayoutCount: { type: [integer, "null"] } programPaidPrimes: type: [string, "null"] description: "DESIGN.md G37 — `getProgramPayouts().paidTotal` on the voucher program; reconciles against the vault's allow-list spend on the quests tag. Never summed with `vaultPaidPrimes`." programClaimCount: { type: [integer, "null"] } burnWalletAddress: type: [string, "null"] description: "DESIGN.md G40b — `getBurnWallet()` on the ratchet: the jetton wallet whose notifications `burnedPrimes` counts. Null until the wallet is set." vaultRemainingPrimes: type: [string, "null"] description: The vault's jetton-wallet balance, so `size − paid ≈ balance` is checkable. vaultDeferredCount: type: [integer, "null"] description: "`getDeferredPayouts` — bounty payouts the vault owes and has not delivered. A liability, never a supply band. Anyone may clear one with `retry_bounty_payout`." vaultJournalKey: type: [integer, "null"] description: "`getDeferredPayouts` — the next journal key; a retry walks `0 .. vaultJournalKey − 1`." stakedPrimes: type: "null" description: Staking does not ship in v1 (§5.3 / §11.18). Null is "not shipped", not zero. unavailable: type: array items: { type: string } derived: type: object description: > The one computed value, kept out of the measured block. `S − total_supply` is everything ever removed from circulation — it also carries the vault's unspent tokens and any bounced rebate mint — and is NOT the burn total: compare it against `burnedPrimes` + `sinkBurnedPrimes`, the two burn authorities, side by side rather than netted. properties: removedFromCirculationPrimes: type: [string, "null"] description: Null when `circulatingPrimes` is — the subtraction needs both halves. derivation: { type: string } note: { type: string } provenance: type: array items: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /primes/wallet/{address}: get: tags: [ledger] summary: An owner's own PRIMES jetton wallet address, and its balance. description: > A TEP-74 `transfer` leaves FROM the owner's own jetton wallet — not from their TON wallet and not from the master — and nothing on chain publishes a stranger's jetton wallet. It is derived with `get_wallet_address(owner)` on the MASTER, and the PRIMES master is a configured role on this service, so the derivation is available here. (`/lp/positions/{address}` returns `lpJettonWallet: null` with a reason for the mirror-image case: the DeDust LP master is NOT configured, so that one genuinely cannot be resolved by this service.) The balance rides along because every spend control needs both — an affordability gate is the difference between refusing locally and letting the wallet throw. `walletDeployed: false` means the owner has never held PRIMES, so their wallet is not on chain yet. The ADDRESS is still correct: `get_wallet_address` is a pure function of owner and master, so it answers for a wallet that does not exist, which is exactly the wallet the first transfer will deploy. That is a different state from a balance of zero, which is a wallet that exists and is empty. Two cache tiers, deliberately: the derivation never changes for a given owner and is cached as immutable; the balance changes on every transfer and is on the fast tier, because a spend control gating on a stale balance would refuse money the player already has, or offer money they have already spent. Unbounded cardinality, so it carries the inbound rate limit. parameters: - in: path name: address required: true schema: { type: string } description: The owner's TON address. A malformed one is a 400, not a 503. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: owner: { type: string } walletAddress: type: string description: >- `get_wallet_address(owner)` on the PRIMES master. Correct whether or not the wallet has been deployed. walletDeployed: type: boolean description: >- False = never held PRIMES, so the wallet is not on chain yet. Not the same claim as a zero balance. balancePrimes: type: [string, "null"] description: Null iff `walletDeployed` is false. Never a stand-in zero. provenance: type: array items: { $ref: "#/components/schemas/Provenance" } "400": { description: Not a TON address. } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /sink: get: tags: [ledger] summary: The burn sink's shop — what a naming costs, and how many have sold. description: > CONCEPT.md §5.2 family 1, §4.7.1, §9.1; DECISIONS.md D-2 / D-35 / D-126 / D-179. `primes_sink.tolk` sells exactly ONE thing: a name annotation on `n` — every prime's naming, its first included, and every re-engraving (D-179). Only an era trophy's first engraving is free, through the registrar. `getNamePriceTon` is what it costs in TON, `namePricePrimes` is that price in PRIMES at the ledger's live floor, `getSinkCounts` is how many have been bought, and `annotateAction` / `minForwardNanoton` are what a client needs to build the purchase. **The PRIMES amount.** `ceil(namePriceTon · S / T)`, with `T` = `getFloorNum()` and `S` = `getFloorDen()` on the ledger — the sink charges exactly this against the floor it reads at purchase. The floor only rises (§5), so an amount from any earlier read is enough; the sink refunds the change, and refunds everything if short. A surface may print it; it may never describe the sink as supporting the price (§5.2). **The burn totals are not here.** They are on `/supply`, beside the ratchet's, where §3.3's identity is closed. This route is the shop. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: namePriceTon: type: string description: >- The naming price in nanoTON, from `getNamePriceTon()` on the sink. namePricePrimes: type: [string, "null"] description: >- nano-PRIMES a naming burns now — `ceil(namePriceTon · S / T)` from the ledger's `getFloorNum`/`getFloorDen`. Null when either read failed. annotationsSold: type: integer description: Paid annotations granted, from `getSinkCounts()`. annotateAction: type: integer description: >- The `forwardPayload` action tag a purchase must carry — 0x414e4e54, four ASCII bytes ("ANNT"). Permanent: it ends up in transfers already on chain, so it is never reused. Served so a client cannot drift from the contract on the one field that decides what a burn paid for. minForwardNanoton: type: string description: >- The NOMINAL TON a transfer must forward so the sink can finish its worst path (ledger round trip, burn, registrar hop, change refund). A sender adds a margin and never sends the exact figure, or `insufficient_value` refuses it. unavailable: type: array items: { type: string } provenance: type: array items: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /vesting: get: tags: [ledger] summary: The treasury's genesis PRIMES vest (GAUNTLET.md E14, D-77/D-100/D-166). 30s tier. description: > Backed by `getVesting` and `getVestingSchedule` on the treasury's instance of `primes_vesting.tolk`: 1,000,000 PRIMES whose beneficiary is `primes_treasury.tolk` (no key), unlocking in four equal 25% tranches 90 days apart. D-166 deleted the operator's lock, so this is the only genesis PRIMES allocation there is, and §2 point 4's "team takes fees, never supply" is checkable here in one call. Not economic state — the vest moves custody of an already-counted allocation, never `S` or `T` (see the contract's own header), so it carries no ratchet or solvency implication either way. `unlocked`/`tranches` is the shape CLAUDE.md's frontend rule asks for: a bounded quantity renders as four segments lit in sequence, not a printed fraction, with `nextUnlockAt` driving the countdown on the unlit one. `releasedAmountNanoprimes` is what has been PUSHED OUT via `release()`, not what has matured. `503` names `PRIMES_TREASURY_VESTING_ADDRESS` until it is configured — never a zeroed schedule, which a UI would draw as a real lock that has vested nothing. responses: "200": description: OK content: application/json: schema: type: object properties: data: { $ref: "#/components/schemas/VestingLock" } provenance: type: array items: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } # --------------------------------------------------------------------------- # THE GOVERNED TREASURY, THE LP MINER, AND LOCKED DEPTH # DECISIONS.md D-100 / CONCEPT.md §5.5. Contracts: primes_treasury.tolk, # primes_lp_position.tolk (one child per depositing wallet), primes_lp_sink.tolk. # # THE RULE THAT GOVERNS EVERY ROUTE IN THIS GROUP (D-100 point 4): LOCKED DEPTH IS # NEVER SUMMED INTO `T`, into the floor `p_f = T/S`, or into any solvency figure. # A DeDust pool's TON/PRIMES composition moves with every trade, so an LP-derived # term is non-monotonic and would void CONCEPT.md §5's ratchet theorem. Two # guarantees, two panels: the floor (a redemption ratio, monotonic) and the # liquidity (locked forever / committed >= 30d / withdrawable). # # AND THERE IS NO APY ANYWHERE IN THIS GROUP. The mining rate floats with both the # treasury's PRIMES balance and the total locked LP (D-100 point 10c), so the # contract publishes the INPUTS and refuses to compute a yield. A surface that # shows one derives it at render and labels it an estimate. # --------------------------------------------------------------------------- /treasury: get: tags: [treasury] summary: The governed treasury's four balances, the proposal cap and the two LP ladders. 30s tier. description: > Backed by `getTreasuryBalances`, `getGov`, `getVoteMultipliers`, `getTierTerms` and `getTreasuryConfig` on `primes_treasury.tolk`. One contract's own get-methods reported verbatim, one provenance entry each — the same shape `/supply` and `/vesting` have. D-168 deleted the LP miner (and TR-1 with it); the treasury's PRIMES reach stakers through `distribute()`, which `/stake` serves. `lpHeldRaw` is the DEPOSITORS' LP and is never proposable (invariant TR-5); since D-167 it is all the LP the treasury holds. `tonBalanceNanoton` is already net of the contract's rent floor, so it is the same figure the proposal cap is enforced against on chain. **UNITS.** TON is `...Nanoton`, PRIMES is `...Nanoprimes`, and LP is `...Raw` — because LP is a DeDust jetton with ITS OWN decimals, which this service does not read and must not assume are nine. An LP figure may only ever be compared against another LP figure. **THE FOUR LIFETIME FEE COUNTERS ARE READ OFF THE LEDGER, NOT THE TREASURY** (`getOperatorTotals`, GAUNTLET.md S30). They are here because they are the only on-chain answer to "where did the balance above come from", and the treasury does not publish it. Their provenance entry names the LEDGER's address — check it, because it is the one entry in this response that does. Reported verbatim, never summed with a treasury figure. `treasuryPaidTonNanoton` is **§4.1's 5% line (D-152) plus the governed treasury's 58% share of an unsold-prime forfeit (§6 point 2, D-166)**: every ordinary mint pays this contract 5%. `operatorPaidTonNanoton` is the operator's 42% remainder of that same forfeit and nothing else, so it stays at 0 until a mint cites a prime nobody owns. A surface that adds the two and calls the sum "treasury revenue" is still wrong — one recipient is a contract with no key and the other is a wallet with one. All four are `null` when the counters could not be read — no `PRIMES_LEDGER_ADDRESS` on this service, or no account behind it. `null` is "not read" and is a different claim from the real `0` that `opsSweptTonNanoton` carries until the first `sweep_ops` runs. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: tonBalanceNanoton: { type: string } primesBalanceNanoprimes: { type: string } lpHeldRaw: { type: string, description: The depositors' LP. Never proposable (TR-5). } primesInNanoprimes: { type: string } tstonBalanceNano: type: string description: > D-151 / §4.3.2 — the FOURTH pot: tsTON harvested out of the ratchet's staked reserve, wholly proposable. Nano-tsTON. proposalCapBps: { type: integer, description: Max share of an asset one proposal may move, at EXECUTE time (TR-3). } proposeMinBps: { type: integer } voteDurationSeconds: { type: integer } eligibleWeight: { type: string, description: "The electorate — every running LP term, weighted by vote multiplier." } proposalSeq: { type: integer, description: The NEXT proposal id, so it doubles as the count ever opened. } voteMultipliers: { type: array, items: { type: integer }, description: "Four vote multipliers (30 d / 3 m / 6 m / 1 y)." } tierTermSeconds: { type: array, items: { type: integer }, description: Four clock-divided LP lock terms. } lpJettonWallet: type: string nullable: true description: > `getTreasuryConfig()`. THE ASSET DISCRIMINATOR for `/treasury/ledger` (read-index): PRIMES and LP both arrive as TEP-74 `transfer_notification`, so nothing but the forwarding wallet says which. `null` means an inflow is UNCLASSIFIABLE, not that it was PRIMES. primesJettonWallet: { type: string, nullable: true } tstonJettonWallet: type: string nullable: true description: > The third discriminator (D-151). A `harvest()` arrives through the same TEP-74 opcode as PRIMES and LP; this is what tells it apart. walletsSet: type: boolean description: The one-shot `set_wallets` has fired. False means both wallets are `addr_none`. operatorPaidTonNanoton: type: string nullable: true description: > LEDGER `getOperatorTotals`. Lifetime nanoton: the operator's 42% remainder of every §6 point 2 forfeit, and nothing else (D-152, D-166). treasuryPaidTonNanoton: type: string nullable: true description: > LEDGER `getOperatorTotals`. §4.1's 5% line (D-152) plus the governed treasury's 58% forfeit share (D-166). opsSweptTonNanoton: type: string nullable: true description: LEDGER `getOperatorTotals`. `sweep_ops` — lifetime gas & ops surplus returned to the ratchet pool (D-117), not operator income; a real 0 until the first sweep. premiumTreasuryPaidTonNanoton: type: string nullable: true description: LEDGER `getOperatorTotals`. D-114's treasury share of an auction premium. inviteSpend: type: object nullable: true description: > DESIGN.md G33 — LEDGER `getInviteSpend` (§4.1 seventh line, D-101), lifetime: slots declared, generators deployed, slots that fell to β, and the TON the line cost. Not a treasury inflow; null = not read. properties: generatorsFunded: { type: string } generatorsDeployed: { type: string } slotsToPool: { type: string } linePaidNanoton: { type: string } provenance: type: array items: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /treasury/proposals: get: tags: [treasury] summary: The proposal book with tallies AND executability, newest first. 30s tier. description: > Backed by `getGov`, and by `getProposal(seq)` + `getExecutable(seq)` per row on `primes_treasury.tolk`. **`getExecutable` rides along with every row on purpose.** Without it a client has to re-derive the close time, the strict-majority rule (`yes > no` — a tie FAILS) and the 25% cap against the balance at execute time in order to decide whether to enable an Execute button: three pieces of consensus logic reimplemented in TypeScript, which is the class of drift CONCEPT.md §9.1 exists to prevent. `reason` is the error code `execute` WOULD throw (1153 no such proposal, 1158 still open, 1159 already executed, 1160 not passed, 1161 over the cap, 1162 over the balance), so the button can be disabled with a reason instead of provoking the throw. Exact for every asset since D-168: nothing accrues inside the treasury. Capped at the 50 newest proposals; `omitted` says how many older ones were not read. A proposal's vote lasts one day and one proposer may hold one open proposal at a time (D-100 point 8), so the cap covers everything still votable many times over. A settled proposal is pruned off the chain (D-188 point 7: failed and closed, or past its one-vote-duration execute window), so the list has gaps; `proposalCount` still counts every proposal ever opened. **`?voter=
` (G31)** folds `getVotedCount(seq)` on that wallet's POSITION CHILD into every row as `hasVoted` — D-188 point 7 keeps the one-vote record on the voter's own child. The child records PRESENCE only, no side, so `hasVoted` must never be read as "voted yes" or "voted no" — only as "this wallet already cast one". A wallet with no child reads `false`. `null` on every row when `voter` is omitted. parameters: - name: voter in: query required: false schema: { type: string } description: > A TON address. When given, every row's `hasVoted` reflects `getVotedCount(seq)` on that wallet's position child. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: proposals: type: array items: type: object properties: seq: { type: integer } found: { type: boolean } proposer: { type: [string, "null"] } asset: { type: integer, description: "0 = TON, 1 = PRIMES, 2 = tsTON (D-167)." } amountRaw: type: string description: > Nanoton for asset 0, nanoprimes for asset 1, nano-tsTON for asset 2 — one field whose unit is decided by another field. dest: { type: [string, "null"] } opens: { type: integer } closes: { type: integer } yes: { type: string } no: { type: string } executed: { type: boolean } executable: { type: boolean } reason: { type: integer, description: The throw code `execute` would raise, or 0. } capRaw: { type: string } availableRaw: { type: string } hasVoted: type: [boolean, "null"] description: > `getVotedCount(seq) > 0` on the `?voter=` wallet's position child. `null` when no `voter` was given. Presence only — never which way the wallet voted. proposalCount: { type: integer } listed: { type: integer } omitted: { type: integer } capBps: { type: integer } eligibleWeight: { type: string } provenance: type: array items: { $ref: "#/components/schemas/Provenance" } "400": { description: "?voter= is not a valid TON address." } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /stake: get: tags: [treasury] summary: "PRIMES staking (D-168): the treasury's distribute() pot, the stream, ST-1/ST-2 and both ladders. 30s tier (accNow 5s)." description: > Backed by `getStakeSolvency`, `getStakeStream`, `getAccNow`, `getStakeTotals`, `getTierTerms`, `getTierWeights` and `getStakeConfig` on `primes_stake.tolk`; `get_wallet_data` on the master's PRIMES jetton wallet; and `getDistribution` + `getTreasuryBalances` on `primes_treasury.tolk` (all treasury fields are `null` when this service has no `PRIMES_TREASURY_ADDRESS`). Two fields are derived, and only these two: `nextDistributeAt` = `lastDistributeTime + distributeIntervalSeconds` (the contract's own definition of "due"), and `solvencyHolds` = ST-1, `walletBalance >= locked + streamRemaining + unclaimed` (`null` when the wallet could not be read, with `walletUnavailable` naming why). There is no APY field. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: stakeAddress: { type: string } treasuryPrimesNanoprimes: { type: string, nullable: true, description: "The treasury's PRIMES — the pot distribute() draws from." } lastDistributeTime: { type: integer, nullable: true, description: 0 until the first distribute(). } distributeIntervalSeconds: { type: integer, nullable: true, description: Clock-divided epoch. } nextDistributeAt: { type: integer, nullable: true, description: lastDistributeTime + distributeIntervalSeconds. } distributionRateBpsPerDay: { type: integer, nullable: true } nextBudgetEstimateNanoprimes: { type: string, nullable: true, description: The handler's own budget function over today's balance. } distributedTotalNanoprimes: { type: string, nullable: true } streamRateNanoprimesPerSecond: { type: string } streamRemainingNanoprimes: { type: string } streamEnd: { type: integer } lastAccrualTime: { type: integer } rewardPerWeightAcc: { type: string, description: As of the last state-touching message. } accNow: { type: string, description: getAccNow() — the accumulator run forward to now. } accScale: { type: string, description: 2^64. } totalWeight: { type: string, description: Sum of amount x tierWeight over earning positions. } lockedTotalNanoprimes: { type: string } unclaimedTotalNanoprimes: { type: string } walletBalanceNanoprimes: { type: string, nullable: true } walletUnavailable: { type: string, nullable: true } solvencyHolds: { type: boolean, nullable: true, description: ST-1. False is a broken invariant. } budgetsInNanoprimes: { type: string } claimedTotalNanoprimes: { type: string } livePositions: { type: integer } stakeEvents: { type: integer } budgetEvents: { type: integer } tierTermSeconds: { type: array, items: { type: integer }, description: Four clock-divided lock terms. } tierWeights: { type: array, items: { type: integer }, description: "Four stake weights (5/6/7/8)." } jettonWallet: { type: string, nullable: true } walletSet: { type: boolean } provenance: type: array items: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /stake/{address}: get: tags: [treasury] summary: One wallet's stake positions, each with its live claimable PRIMES, read off its own stake child. 5s tier. description: > Backed by `getStakePositionAddress(owner)` and `getAccNow()` on `primes_stake.tolk`, then `getPositions()`, `getPosition(id)` and `getClaimable(id, accNow)` on that wallet's `primes_stake_position.tolk` child. `claimableNanoprimes` is the master's settle rule (maturity proration included) run as a read, against the `accNow` this response publishes. A wallet that never staked answers `{"data":{"found":false},"code":"CONTRACT_NOT_DEPLOYED"}`. Walks ids downward from the newest and reads at most 32; `idsNotRead` is where it stopped. parameters: - name: address in: path required: true schema: { type: string } responses: "200": description: "OK, or `found: false` when the wallet has no stake child deployed." content: application/json: schema: type: object properties: data: type: object properties: found: { type: boolean } owner: { type: string } positionAddress: { type: string } positionCount: { type: integer } nextId: { type: integer } stakedTotalNanoprimes: { type: string } accNow: { type: string } idsNotRead: { type: integer } positions: type: array items: type: object properties: id: { type: integer } amountNanoprimes: { type: string } tier: { type: integer } lockedAt: { type: integer } maturesAt: { type: integer, description: Earning stops and withdraw opens here (ST-3). } lastSettleTime: { type: integer } expired: { type: boolean } bankedNanoprimes: { type: string } inFlight: { type: boolean } claimableNanoprimes: { type: string } provenance: type: array items: { $ref: "#/components/schemas/Provenance" } "400": { description: Not a valid TON address. } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /lp/positions/{address}: get: tags: [treasury] summary: One wallet's LP custody positions, read off its own position child. 5s tier. description: > Backed by `getPositionAddress(owner)` on `primes_treasury.tolk`, then `getPositions()` and `getPosition(id)` on that wallet's `primes_lp_position.tolk` child. The path predates D-168; an LP position now carries a term and a vote and earns nothing. A wallet that has never deposited has no child deployed. That answers `200` with `{"data":{"found":false},"code":"CONTRACT_NOT_DEPLOYED"}` and the provenance of the call that produced it — the same shape an unminted number gets. It is a fact about the wallet, not a failure of this service. `maturesAt` on each position is the instant `withdraw` stops throwing AND `vote` starts throwing — under D-147 the same instant, and the bounded quantity the tier ring fills toward. `expired` is bookkeeping, not a state a player acts on: it says whether the position's vote weight has been taken out of the published aggregates yet, and the live truth is `maturesAt` against the clock. Position ids are never reused, so the route walks the id range DOWNWARDS from the newest and reads at most 32; `idsNotRead` is the id below which it stopped. parameters: - name: address in: path required: true schema: { type: string } responses: "200": description: "OK, or `found: false` when the wallet has no position child deployed." content: application/json: schema: type: object properties: data: type: object properties: found: { type: boolean } owner: { type: string } positionAddress: { type: string } positionCount: { type: integer } nextId: { type: integer, description: Exclusive upper bound of the id range. } voteWeight: { type: string, description: Sum of amount x voteMultiplier over unexpired positions. } idsNotRead: { type: integer } lpJettonWallet: type: [string, "null"] description: > Always null today, with `lpJettonWalletUnavailable` naming why: a TEP-74 transfer is sent from the OWNER's own jetton wallet, whose address is `get_wallet_address(owner)` on the DeDust LP jetton master — a contract this service has no configured role for, because the pairing is manual (D-100 point 4). Resolve it client-side. lpJettonWalletUnavailable: { type: [string, "null"] } positions: type: array items: type: object properties: id: { type: integer } amountRaw: { type: string, description: Raw LP units. } tier: { type: integer } lockedAt: { type: integer } maturesAt: { type: integer } weightSince: { type: integer, description: "Since when the position has held its current weight; it votes only on proposals whose opens is strictly later (CONCEPT §5.5.5, 1202)." } expired: { type: boolean } inFlight: { type: boolean } provenance: type: array items: { $ref: "#/components/schemas/Provenance" } "400": { description: Not a valid TON address. } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /lp/locked: get: tags: [treasury] summary: Locked depth, and the TR-6 comparison shown rather than asserted. 30s tier. description: > Backed by `getLocked`, `getEvents` and `getLpSinkConfig` on `primes_lp_sink.tolk`, plus `get_wallet_data` on the LP jetton wallet that config names. `primes_lp_sink.tolk` declares NO outbound message type at all — LP goes in and nothing comes out — so `getLocked()` is monotone non-decreasing and equal to the sink's LP jetton wallet balance. **Invariant TR-6 is that one-line audit**, so this route serves BOTH numbers side by side with the comparison as a field (`declaredMatchesHeld`). A panel that printed only `getLocked()` would be asking a reader to take the equality on trust; a panel that printed only the wallet balance would lose the contract's own declared figure. `heldByWalletNano` is `null` with `heldUnavailable` naming the reason — before the genesis one-shot fires there is no wallet to compare against, and before the first lock the wallet has no deployed account. Never a fabricated zero. **Locked depth is not part of `T`** and no field here may be summed into one that is (D-100 point 4). It is a MARKET guarantee — depth nobody can withdraw — sitting beside the floor's REDEMPTION guarantee, and the two are different claims about different things. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: lockedRaw: { type: string, description: "`getLocked()` — the sink's own declared figure, in raw LP units." } events: { type: integer, description: How many separate locks make it up. } lpJettonWallet: { type: [string, "null"] } walletSet: { type: boolean } genesisScript: { type: [string, "null"] } heldByWalletRaw: type: [string, "null"] description: The wallet's actual TEP-74 balance, or null with `heldUnavailable` set. heldUnavailable: { type: [string, "null"] } declaredMatchesHeld: type: [boolean, "null"] description: TR-6, as a comparison the panel renders. Null when the wallet balance could not be read. lpTotalSupplyRaw: type: [string, "null"] description: > `get_jetton_data().total_supply` on the venue pool (the LP jetton master) — every LP token that exists. The denominator that turns `lockedRaw`'s arbitrary unit into the share of the pool nobody can withdraw. Null when the ratchet publishes no venue or the pool is not deployed. note: { type: string } provenance: type: array items: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /lp/wallet/{address}: get: tags: [treasury] summary: The owner's own DeDust LP jetton wallet, its balance and the pool's LP supply. description: > `/primes/wallet/{address}` for the OTHER jetton. A TEP-74 transfer leaves FROM the owner's own jetton wallet, so the control that locks LP into the treasury (D-100 point 5) cannot address a message until this answers. **The LP jetton master is the venue pool itself** — a DeDust v2 pool is the jetton master of its own LP token — so the derivation is `get_wallet_address(owner)` on `getVenue().dedustPool`, which this service already reads for `/market`. A ratchet that publishes no venue answers every field null with `balanceUnavailable` naming the reason, never a fabricated address. `balanceRaw` is null with `walletDeployed: false` when the owner has never held LP — a jetton wallet is deployed by its first transfer, so that is a fact and not a zero. `lpTotalSupplyRaw` is `get_jetton_data().total_supply` on the pool, published as the denominator of the constant-product mint rule a provide-liquidity form quotes with; the estimate itself is derived at render and labelled there, never here. Raw LP units throughout: the DeDust LP jetton carries its own decimals and this service does not read them. parameters: - name: address in: path required: true schema: { type: string } description: The owner's TON wallet address. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: owner: { type: string } poolAddress: type: [string, "null"] description: The LP jetton master, which is the venue pool itself. walletAddress: { type: [string, "null"] } walletDeployed: { type: boolean } balanceRaw: { type: [string, "null"] } balanceUnavailable: { type: [string, "null"] } lpTotalSupplyRaw: { type: [string, "null"] } provenance: type: array items: { $ref: "#/components/schemas/Provenance" } "400": { description: Not a TON address. } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } # --------------------------------------------------------------------------- # Numbers # --------------------------------------------------------------------------- /number/{n}/prime: get: tags: [numbers] summary: Whether n is a verified prime, per the ledger's on-chain bitmap. description: Backed by `isKnownPrime`. parameters: - $ref: "#/components/parameters/NumberPath" responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: isPrime: { type: boolean } provenance: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /numbers: get: tags: [numbers] summary: Ownership and accrued tribute for several numbers in one call. description: > The ownership-confirmation step behind the /me portfolio rail. The event-poller proposes CANDIDATES — mint-time attribution, which goes stale on any NFT transfer, unioned with an NFT holder index — and this route settles them: it reads each item's own `getNumberData` so the displayed owner is a get-method result (CONCEPT.md §9.1) rather than an inference from an indexed mint or a third party's word. Batched because a wallet screen asks for its whole portfolio on first paint, and the per-number form would serialise into one upstream call per number against the TonAPI budget that already produced live 429s. Each n is cached under the same key as `/number/{n}`, so repeat renders cost nothing upstream. An unminted number answers `minted: false` rather than failing the batch. Truncation beyond `maxPerRequest` is reported, never silent. parameters: - name: ns in: query required: true schema: { type: string } description: Comma-separated decimal numbers, e.g. `2,3,5`. Duplicates are collapsed. responses: "200": description: OK content: application/json: schema: type: object properties: numbers: type: array items: type: object properties: n: { type: string } minted: { type: boolean } owner: { type: string, nullable: true } isPrime: { type: boolean, nullable: true } owedNanoton: { type: string, nullable: true } itemAddress: { type: string } provenance: type: array items: { $ref: "#/components/schemas/Provenance" } requested: { type: integer } returned: { type: integer } truncated: { type: boolean } maxPerRequest: { type: integer } "400": { description: ns missing, unparseable, or below the start of the number line } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /item/{n}/address: get: tags: [numbers] summary: The item contract address for number n — the address-only half of /number/{n}. description: > Backed by `getItemAddress`, cached forever once resolved (the address a mint deploys to is a pure function of the collection code and n). Exists so the webapp's `resolveItemAddress` fallback (GAUNTLET.md P3.G2) does not have to reach TonCenter's public, keyless, unrate-limited `runGetMethod` directly to get it. parameters: - $ref: "#/components/parameters/NumberPath" responses: "200": description: OK content: application/json: schema: type: object properties: address: { type: string } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /wallet/{address}/seqno: get: tags: [numbers] summary: A wallet contract's seqno() — the seqno-inclusion probe's underlying read. description: > Proxies `seqno()` on an arbitrary wallet address, straight from TonCenter, so `webapp/src/chain/useTxConfirmation.ts`'s fallback probe (for send flows with no dedicated get-method to watch) does not have to call TonCenter directly (GAUNTLET.md P3.G2). `seqno: null` — never a 500 — is the answer for an uninitialised wallet, a non-zero exit code, or any transport failure; callers must treat null as unknown, never as zero. parameters: - name: address in: path required: true schema: { type: string } description: Any valid TON address form. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: seqno: { type: [integer, "null"] } provenance: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /wallet/{address}/balance: get: tags: [numbers] summary: A wallet's TON balance — whether it can pay for a mint at all. description: > The funding check behind the mint control's top-up branch. A newcomer arriving from Telegram with an empty wallet is the largest single term in whether they ever mint at all — a client that cannot tell "connected" from "connected and funded" offers a signature the wallet will refuse. This is the raw account state (`getState`), not a get-method, and the provenance envelope says so: `getMethod: account_state`. `balanceNanoton: null` — never a 500, never a zero — is the answer for any read failure; callers must treat null as unknown, because "we could not read your balance" is not "you are broke". parameters: - name: address in: path required: true schema: { type: string } description: Any valid TON address form. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: balanceNanoton: { type: [string, "null"] } provenance: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /number/{n}: get: tags: [numbers] summary: Full public state of number n — the item's owner, prime flag, and accrued tribute. description: > Chains two live lookups: the ledger's `getItemAddress` (cached) resolves n to its item contract, then `getNumberData` + `getOwed` are read from that item. Both sub-results carry their own provenance. parameters: - $ref: "#/components/parameters/NumberPath" responses: "200": description: OK content: application/json: schema: oneOf: # Every number at or ahead of the head derives to an address with no item # deployed at it yet. That is an answer, not a failure — see NotDeployed. - $ref: "#/components/schemas/NotDeployed" - type: object properties: number: type: object properties: data: type: object properties: number: { type: string } owner: { type: string } isPrime: { type: boolean } provenance: { $ref: "#/components/schemas/Provenance" } owed: type: object properties: data: type: object properties: owedNanoton: { type: string } provenance: { $ref: "#/components/schemas/Provenance" } provenance: type: array items: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /number/{n}/name: get: tags: [numbers] summary: Who holds naming rights over n, and what they called it. description: > CONCEPT.md §4.7.1 / §5.1 / §7: the finder of a prime, the buyer of a primorial and the era-trophy winner all take permanent naming rights, and §5.2 family 1 prices every annotation (D-179: a trophy's first engraving is the only free one) in burned PRIMES. Since D-188 point 6 a prime's naming right is its own ITEM's state — `getNamer()` and `getAnnotation()` on item(n) — and an era trophy's is the registrar's — `getTrophy(n)` and `getTrophyAnnotation(n)`; this route picks by n and answers in one shape, the provenance naming which pair it read. `namer: null` is `addr_none` (or an unminted number with no item): nobody holds naming rights over n, which is the ordinary state of every composite. `count: 0` with a namer set is a right granted and not yet used. Neither is an error, and neither is invented away. `count` is how many annotations have ever been written; `text` is the standing one. parameters: - $ref: "#/components/parameters/NumberPath" responses: "200": description: OK content: application/json: schema: type: object properties: namer: type: object properties: data: type: object properties: namer: { type: string, nullable: true } provenance: { $ref: "#/components/schemas/Provenance" } annotation: type: object properties: data: type: object properties: count: { type: integer } text: { type: string, nullable: true } provenance: { $ref: "#/components/schemas/Provenance" } provenance: type: array items: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /number/{n}/trophy: get: tags: [numbers] summary: n's era trophy, read as a trophy (holder + transferability) rather than as a naming right. description: > D-14 (CONCEPT.md §4.7, §7): `getTrophy(n)` on the registrar, distinct from `/number/{n}/name` even though, for a primorial, both read the same trophy row — that route answers "who names n"; this one answers "is n a movable trophy, and who holds it". GAUNTLET.md L3b: filed because the Telegram bot's `/wear` command's eligibility rule ("a wearable is minted when a number is named OR an era trophy fires", §7) had no way to see the trophy half of that OR without this route — `/number/{n}/name`'s `annotation.text` stays null for an un-annotated primorial even after its trophy has fired. `holder: null` is `addr_none`: either n's era has not closed yet, or n is not one of the eight primorials at all — both read the same as "no trophy", which is the ordinary state of nearly the whole number line. `transferable` is `true` iff n is one of §7's eight primorials (i.e. `TransferTrophy` would accept it) and `false` for every other number, a named prime included: prime naming rights are permanent (§5.1, §4.7.1) and D-14's transferability carve-out never reaches them. parameters: - $ref: "#/components/parameters/NumberPath" responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: holder: { type: string, nullable: true } transferable: { type: boolean } provenance: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /number/{n}/runway: get: tags: [numbers] summary: Seconds until n's item account freezes for unpaid storage rent. description: > CONCEPT.md §3.1 / §9.1, GAUNTLET.md PS1: `getStorageRunway()` on the item, priced against the config-18 storage schedule in force in the block that answers rather than against a baked figure — so a network fee change is reflected with no redeploy. WHY THIS EXISTS. TON charges storage rent forever, and a **composite** item is endowed once with `ITEM_DEPLOY_VALUE` at mint and never topped up again: only primes receive `credit_tribute`. Its balance is therefore a finite tank, and when it empties the account FREEZES — no transfer, no `get_nft_data`, no claim, nothing — until somebody funds it. `TopUp` is permissionless (DECISIONS.md D-62), so this reading is published to every caller and not only to the holder: anybody may defend anybody's number, which is what stops the freeze being a trap for an absent owner. TWO SENTINELS, AND NEITHER IS A DURATION. `-1` means the contract priced no finite runway for the year; `0` means the balance net of accrued-but-unsettled rent is already gone. Both travel as the contract's own integer, as a decimal string, and a caller that coerces either into "0 seconds left" or into "infinite" is wrong in one of the two directions that matter. Its own route rather than a third upstream call folded into `/number/{n}`, for the reason `/number/{n}/name` records: that route polls on the FAST tier for every number page, while a runway moves only with the wall clock and with a `TopUp`. Slow tier, one cache key, and a caller that never asks about rent never pays for the call. parameters: - $ref: "#/components/parameters/NumberPath" responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: runwaySeconds: type: string description: > Decimal integer. Seconds remaining, or `-1` (no finite runway) or `0` (already out). Read `getStorageRunway` in `contracts/contracts/primes_item.tolk` before treating it as a number. provenance: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /number/{n}/tribute: get: tags: [numbers] summary: Cumulative divisor tribute ever credited to n's item. description: > CONCEPT.md §3.2.1 / §9.1: `getTributeTotal()` on the item — every `credit_tribute` amount it has received, never decremented. A claim zeroes `owed`, not this, so this is the prime's "paid to date" and `owed` is only what is still unclaimed. The flat prime dividend is not included; it settles on the ledger. Its own route so an item deployed before the counter existed fails this read alone. parameters: - $ref: "#/components/parameters/NumberPath" responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: tributeTotalNanoton: { type: string, description: Decimal nanoTON. } provenance: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /assets/{n}/yield: get: tags: [assets] summary: Trailing cash-flow rate of number n, derived from `owed` deltas over head progress. description: > CONCEPT.md §5.4 argues that each mint's value rotates toward the numbers as `k_max(n)` decays. A holder cannot act on that without being able to price what they own — and every asset in the design exposes a **stock** (`getOwed`, `getDivAcc`, `getClaimableDividend`) while nothing exposes a **rate**. Storing trailing counters on-chain was rejected: it spends gas and state rent against the 20% line §12 calls the tightest budget in the system, for a number already implied by public state. So the rate is derived here, under §9.1's rule for derived numbers: **a rate may be displayed if the derivation is published next to it.** The response carries the two `owed` readings, the heads they were read at, the get-methods behind each, and the arithmetic, so a reader recomputes the rate rather than trusting it. `derived: true` is load-bearing. The *stocks* are chain state and carry provenance; the *samples* are this process's own memory, which is §12's "the growth backend is a trusted display layer" in its narrowest possible form. A restart drops the history and `rate` goes `null` with `unavailableReason` set — the honest outcome, never a seeded zero. Denominated in head progress rather than seconds, like every other clock in the design (§4.6, §5, §7): a quiet week is not a collapse in an asset's value. A tribute claim zeroes `owed` (§3.2.1). That is the asset being paid, not the asset earning less, so a negative delta restarts the window instead of being averaged through. The flat dividend's per-prime half (`getClaimableDividend`) rides beside `owed` as `stocks.claimableDividendNanoton`. **A number ahead of the head answers `200` with `minted: false`, `rate: null` and `stocks: null`.** Items are deployed by the mint, so every unminted number is a valid address with no account behind it. That is the answer "not minted yet", it covers the entire number line above the head, and reporting it as a 500 would render the busiest half of the line as a broken backend. parameters: - $ref: "#/components/parameters/NumberPath" responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: n: { type: string } derived: type: boolean description: Always true. This number is the proxy's arithmetic, not a get-method result. minted: type: boolean description: > Present and `false` only when n is ahead of the head. `rate` and `stocks` are then both null and `unavailableReason` names the current head. derivation: type: object properties: formula: { type: string } unit: { type: string } basis: { type: string } note: { type: string } inputs: type: object description: The two readings the rate is computed from. Operator-sampled. properties: first: { $ref: "#/components/schemas/YieldSample" } latest: { $ref: "#/components/schemas/YieldSample" } rate: nullable: true type: object properties: tonPer1000HeadProgress: { type: number } headProgress: { type: string } owedDeltaNanoton: { type: string } unavailableReason: type: string nullable: true description: Why `rate` is null — one sample only, a static head, or a claim inside the window. stocks: type: object nullable: true description: > The on-chain half. These carry provenance; the rate does not. `null` when the number has not been minted — there is no item contract to read. properties: owedNanoton: { type: string } claimableDividendNanoton: { type: string } claimableDividendGetMethod: { type: string } provenance: type: array items: { $ref: "#/components/schemas/Provenance" } "400": { description: n is not a decimal number, or is below 2. } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /lots/fee-split: get: tags: [lots] summary: RES_GAS and SETTLE_GAS, from the market (CONCEPT.md §4.4 point 1, D-30). 30s tier. description: > Backed by `getResFeeSplit`, which D-30/D-37 reduces to a PAIR. `RES_PRIORITY_BASE` is deleted with the flat-fee lane (there is no book to buy a place in) and `RES_PREMIUM` is deleted as a fee (nothing is charged as a scored fee), so `resPriorityNanoton` and the `resFeeNanoton` sum that added them are GONE from this response rather than reported as zeros — a fee the contract does not charge has no get-method behind it, and §9.1 does not admit a substitute computed server-side. `RES_GAS` is the one fee left: never waivable, never payable in PRIMES. That is the whole anti-abuse argument — a waiver able to reach it would let a wallet hold the line for free and park the head, which §12 calls the highest-severity risk. `settleGasNanoton` is the slice of `RES_GAS` the market forwards to the ledger with the winning lot's mint. It is what keeps that mint OFF §4.1's 20% gas & ops line: the winner funds their own settlement rather than the pool funding it. The router publishes the first of the two on its own as `getResGas()`. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: resGasNanoton: { type: string } settleGasNanoton: { type: string } provenance: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /number/{n}/off-head: get: tags: [numbers] summary: Whether n is off the sequential line — auctioned, so the head skips it forever (CONCEPT.md §4.4). description: Backed by `isNumberOffHead`. parameters: - $ref: "#/components/parameters/NumberPath" responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: isOffHead: { type: boolean } provenance: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } # --------------------------------------------------------------------------- # Audit and registrars # --------------------------------------------------------------------------- /solvency: get: tags: [audit] summary: Per-contract balance vs. declared liability (CONCEPT.md §3.4). 30s tier. description: > Backed by `getSolvency()` on each configured contract, which returns `(balance, liability, delta)` in ONE get-call — that is precisely the property §3.4's decomposition exists to produce, and "one of the few things a skeptic can check per block without trusting anything". The proxy reports the contract's own subtraction rather than recomputing it. One cache key covers every contract, because the block that renders this (§6 D7) shows them as a set and one key per contract would put the audit block on the rate-limit critical path. Contracts sharing an address (pre-decomposition, ledger and ratchet are one account) are deduplicated so no balance is counted twice. The bounty vault declares no `getSolvency` (it custodies PRIMES, not a TON liability), and a role with no deployed account has none either: both report `available: false` with their real account balance and a null liability, rather than a blanked row. `trophyAuction` is in this set because it holds a real, live liability, not because it is another address worth listing: every open lot's standing high bid sits on that account until the lot closes, and the outbid refund is immediate and in-transaction (CONCEPT.md §4.4 point 1b), so `balance ≈ Σ standing bids + unforwardedPremium` is exactly §3.4's checkable property, read off its own `getSolvency`. `provenance` is an ARRAY, one entry per contract. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: contracts: type: array items: type: object properties: contract: type: string enum: [ledger, ratchet, registrar, market, bountyVault] address: { type: string } balanceNanoton: { type: string, nullable: true } declaredLiabilityNanoton: { type: string, nullable: true } deltaNanoton: type: string nullable: true description: balance − liability, as the contract computes it. Negative is insolvency. liabilityBreakdown: type: object nullable: true description: > CONCEPT §9.1. `declaredLiabilityNanoton` itemized, from the contract's own `getLiabilityBreakdown`. Present on the ledger row only — the other contracts each have a single-line liability that the total already states — and `null` elsewhere, which is a legitimate state rather than an error. The three components always sum to `declaredLiabilityNanoton`. `tributeOwedNanoton` is the load-bearing one: §3.2's tribute is credited to an NFT item as a NUMBER while the TON stays on the ledger, so it is money already promised to named owners that would otherwise be indistinguishable from protocol float. A reader reconciles it against `Σ_p get_owed(p)`. properties: tributeOwedNanoton: { type: string } unroutedHeldNanoton: { type: string } divOutstandingNanoton: { type: string } feeHeldNanoton: { type: string, description: "G81: fee legs held below one hop's gas (ledger treasuryHeld + operatorHeld)" } required: [tributeOwedNanoton, unroutedHeldNanoton, divOutstandingNanoton, feeHeldNanoton] getMethod: { type: string } available: { type: boolean } provenance: type: array items: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /earn/{address}: get: tags: [registrars] summary: > The chain half of the `/earn` block for one wallet — §4.7 recruit rank, the §4.5 newcomer window, and the §5 uplift line. 30s tier. description: > EARN_PLAN E1.8/E3.4. Backed by three get-methods read under one cache key (D-191 point 1): the wallet's minter card `getStanding()` (the recruit rank, the newcomer window, the uplift legs and their sum, the era tally — a never-deployed card is zero standing), the ledger's `getKMaxFor(n, sum)` (the clamp flag) and its `getEraLeader()` (the era seq the tally must match). They are collapsed into one route because they only ever change together — on a mint — and `/earn` is a polling page, so separate routes would be several times the upstream budget for one fact (ops/rate-limit-budget.md §C3.1). D-106 REMOVED `getPatronLine` from this call and from the contract: there is no patron of record any more, so there is no per-recruit line to read. These live on the LEDGER. `/ranks/{address}` reads the registrar for the prime ladder; the numbers that decide a mint's rebate ceiling have to be readable synchronously inside a mint, so they are the ledger's. `/earn` reads the ledger's, because that is the one the split obeys. The OFF-CHAIN half of `/earn` — clicks, bot starts, wallet connects, quest scoring, voucher issuance — is NOT served here. It comes from the attribution service, and the two are deliberately different routes so that a counted off-chain tally can never inherit a proved response's provenance (CONCEPT §9.1, EARN_PLAN §6.3). parameters: - name: address in: path required: true schema: { type: string } description: Any valid TON address form. - name: head in: query required: false schema: { type: string, pattern: "^[0-9]+$" } description: > The number to evaluate `getKMaxFor` at, since k_max depends on n. Defaults to the current mint head — the number this wallet would mint next — so the uplift shown is the uplift the next mint would actually receive. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: address: { type: string } recruitRank: type: object description: > §4.7 / D-106, the recruiter's side — lifetime addresses this wallet introduced with a §4.5 gift mint. Never reset. properties: recruits: { type: string } tier: { type: string } upliftScaled: { type: string } nextTierAt: type: string description: > So the ladder shape can show the distance to the next step without the UI hardcoding the tier thresholds (§9.1). newcomerWindow: type: object description: §4.6, the recruit's raised rebate ceiling. properties: left: { type: string } total: { type: string } upliftScaled: { type: string } opened: { type: boolean, description: "G80: a gift ever opened this window; never cleared" } uplift: type: object description: > §5 — the uplift line itemised rather than totalled, so a player can see WHY their k_max is what it is. properties: primeRankScaled: { type: string } primeWindowScaled: { type: string } newcomerScaled: { type: string } recruitRankScaled: { type: string } summedScaled: type: string description: The RAW, UNCLAMPED sum of the four legs above (D-168 deleted the stake leg). clamped: type: boolean description: > The honest field: whether K_CEIL is currently eating part of what the ladders granted. A rank ladder must never hide this from the player it flattered. eraRecruits: type: string description: > D-36 — this wallet's ERA-SCOPED recruit count: its minter card's era tally when taken under the ledger's current era seq, else 0 (D-191 point 1). NOT the same number as `recruitRank.recruits`, which is lifetime and never reset: this one is reset to 0 for every wallet at each primorial ceremony and is the count the era trophy is awarded on. A UI that conflates them shows the wrong contest. provenance: { $ref: "#/components/schemas/Provenance" } "400": description: The path segment is not a valid TON address. content: application/json: schema: { $ref: "#/components/schemas/Error" } "429": description: > Upstream throttled and nothing cached was available to fall back on. Never a 500 — a 500 on this service means a genuine bug (ops/rate-limit-budget.md). headers: Retry-After: schema: { type: string } content: application/json: schema: { $ref: "#/components/schemas/Error" } /ranks/{address}: get: tags: [registrars] summary: An address's position on both rank ladders (CONCEPT.md §4.7, §4.7.1). 30s tier. description: > Backed by the wallet's minter card `getStanding()` (D-191 point 1: `primesMinted` and `recruits`, one call; a never-deployed card is zero standing), so the rank strip is one route and one cache key. D-188 point 6 deleted the registrar's `getPrimeRank` mirror and D-191 moved both ladders off the ledger onto the card. parameters: - name: address in: path required: true schema: { type: string } description: Any valid TON address form. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: address: { type: string } recruitRank: { type: string } primeRank: { type: string } provenance: { $ref: "#/components/schemas/Provenance" } "400": description: The path segment is not a valid TON address. content: application/json: schema: { $ref: "#/components/schemas/Error" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } # --------------------------------------------------------------------------- # Venue, auction, vault # --------------------------------------------------------------------------- /venue: get: tags: [ratchet] summary: The current DeDust venue addresses (native vault, pool, jetton vault). 30s tier. description: > Backed by `getVenue` — the venue timelock's entire admin surface (IMPLEMENTATION_PLAN.md §1 governance row), made publicly checkable. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: dedustNativeVault: { type: string } dedustPool: { type: string } primesJettonVault: { type: string } provenance: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /market: get: tags: [ratchet] summary: The PRIMES market price, read off the venue pool's reserves. 30s tier. description: > Backed by `get_reserves` + `get_assets` on the DeDust pool `getVenue` names. The spot price is the pool's MID price (`reserveTon / reservePrimes`) — what a zero-size swap would pay, before the pool's own `get_trade_fee` (read alongside and published as `tradeFeeNum`/`tradeFeeDen`) and slippage — published as an exact `num/den` rational so it compares with `/ledger/floor`'s `p_f` without either side being rounded first. `series` is read-proxy's own log of the reads it has made, one point per observed price move (plus a heartbeat through quiet hours); no get-method anywhere answers "what WAS the price", and this route does not pretend otherwise. Replaces the webapp's former `api.dedust.io` call, which was both unprovable (§9.1) and mainnet-only. `fresh=1` forces ONE live `get_reserves` past the 30s tier — what the app's own swap form asks for the moment its send confirms, so the card and the chart show the trade that just landed instead of the reading the form quoted from. Throttled to one forced read per 5 seconds across all callers (the fast tier's own budget), and a forced read that fails falls back to the cached answer rather than failing the card. parameters: - in: query name: fresh required: false schema: { type: string, enum: ["1"] } description: Force one live read past the 30s tier; throttled to one per 5s globally. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: status: { type: string, enum: [live, no-pool] } poolState: { type: string, enum: [live, not-deployed, not-a-pool] } poolAddress: { type: string } spotNumNanoton: { type: string } spotDenPrimes: { type: string } reserveTonNanoton: { type: string } reservePrimesNano: { type: string } tradeFeeNum: { type: string, nullable: true } tradeFeeDen: { type: string, nullable: true } series: type: array items: type: object properties: t: { type: integer } num: { type: string } den: { type: string } provenance: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } # --------------------------------------------------------------------------- # Trophy-number auction (CONCEPT.md §4.4 point 1b; DECISIONS.md D-3…D-8) # --------------------------------------------------------------------------- /trophy: get: tags: [trophy] summary: Trophy-lane aggregates — open/closed lots and total premium (DECISIONS.md D-7). 30s tier. description: > Backed by `getMarketStats()` on the market (`primes_market.tolk`); the read-proxy's `getTrophyStats` DTO keeps the old name because the `/trophy/*` namespace did. `totalPremiumNanoton` is §4.2's live demand signal: premium is injected into the ratchet at CLOSE and can never walk back out. It is `k = 0` money in §5.1's exact sense: `T` rises, `S` is untouched, `p_f = T/S` rises by the full amount. `unforwardedPremiumNanoton` is the honesty term — premium this contract booked but the ledger has not accepted, sitting on a contract with no withdraw path and retryable by anyone through the permissionless `retry_premium_forward`. It should be "0"; a persistent non-zero is a real finding, which is why it is published rather than netted out of the total. `headHint` is the market's lagging cache of the ledger head, raised only by `head_update`. It is a cheap pre-filter for `open_lot` (`n > headHint`) and gates a prime's Dutch lot (`n < headHint`, D-88 point 3); the ledger stays authoritative. backend/keeper calls the permissionless `sync_head` on a schedule to keep the gap to `GET /ledger/head` small. `forceCancels` (D-90) counts lots unwound by a ledger `mark_rejected` race loss. 30s tier, unlike `/trophy/lot/{n}`. Every field here moves only on an `open` or a `close` — events measured in tens over the lane's life, not in seconds — and none is a number anyone must act on before the next block. The number a bidder acts on is on the lot route, which is fast. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: openLots: { type: string } closedLots: { type: string } totalPremiumNanoton: { type: string } unforwardedPremiumNanoton: { type: string } headHint: { type: string } forceCancels: { type: string } provenance: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /trophy/lot/{n}: get: tags: [trophy] summary: One trophy lot's LIVE bid state, including snipe extensions. 5s tier. description: > Backed by `getLot(n)` on the trophy auction. The only fast-tier route in this block, and the tier is the whole point of the split. `highBidNanoton` and `closesAt` move on every bid, and both are numbers a bidder acts on within seconds. `closesAt` is the LIVE close time including every 67-minute `BID_EXTENSION` applied so far — decision D10 applies it as `closesAt = max(closesAt, now + BID_EXTENSION)`, so a bid more than 67 minutes out changes nothing and a bid inside that window buys a full 67 minutes of quiet. A 30s cache here would show a stale high bid to someone about to be outbid and a stale close time to someone deciding whether they still have time to respond — which is precisely the sniping failure `BID_EXTENSION` exists to remove, reintroduced by the cache. `minNextBidNanoton` is exactly what the `bid` handler measures the incoming value against: `high + max(MIN_INCREMENT_FLOOR, MIN_INCREMENT_PCT% · high)`, i.e. `max(1 TON, 5% · high)` at the shipped constants. Callers must still add a fee margin on top — the contract rejects an exact nominal on a busy block. There is no ceiling field and no `take` lane, unlike `/auction/lot/{prime}`: this is an ascending-only hard-close auction, and `p0Nanoton` is the OPENING BID, not a ceiling. There is also no no-bid state — opening a lot requires a bid at or above `P0(n)`, so a found lot always has a standing bidder and a closed lot always has a winner (§2: four states that do not exist cannot be got wrong). `closed: true` means the lot has settled: the number was MINTED to the winner in the closing transaction (D-90), the winner's 1 TON ran through the §4.1 split, and the premium above it went to the ratchet as `pending`. parameters: - $ref: "#/components/parameters/NumberPath" responses: "200": description: OK content: application/json: schema: oneOf: # Every number at or ahead of the head derives to an address with no item # deployed at it yet. That is an answer, not a failure — see NotDeployed. - $ref: "#/components/schemas/NotDeployed" - type: object properties: data: type: object properties: found: { type: boolean } p0Nanoton: type: string description: P0(n) — the opening bid this lot was opened at, not a ceiling. highBidNanoton: { type: string } highBidder: { type: string } openedAt: { type: string, description: Unix seconds. } closesAt: type: string description: > Unix seconds, LIVE — includes every snipe extension applied so far. minNextBidNanoton: { type: string } closed: { type: boolean } provenance: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /trophy/floor/{n}: get: tags: [trophy] summary: The floor a lot for n cannot close below (GAUNTLET.md C6.2c). 30s tier. description: > Backed by `getLotFloor(n)` on the trophy market. Returns `floorOf(cfg, mode, p0, priceFloor)` — the SAME pure function the contract itself settles a lot against (GAUNTLET.md C6.2/C6.2b, D-61 option 2), so this cannot disagree with a live lot by construction. `found: false` (with `floorNanoton: "0"`) means no lot exists yet for `n` — an ascending lane's floor is its own opening bid, which has no meaning before the lot opens. Slow tier, unlike `/trophy/lot/{n}`: the floor is fixed the instant a lot opens (mode/p0 never change afterward), so polling it fast would spend budget re-learning a number that cannot move — the same reasoning `/trophy/params` and `/trophy/score/{n}` already use. parameters: - $ref: "#/components/parameters/NumberPath" responses: "200": description: OK content: application/json: schema: oneOf: # Every number at or ahead of the head derives to an address with no item # deployed at it yet. That is an answer, not a failure — see NotDeployed. - $ref: "#/components/schemas/NotDeployed" - type: object properties: data: type: object properties: found: { type: boolean } floorNanoton: { type: string } provenance: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /trophy/score/{n}: get: tags: [trophy] summary: D(n), its category bitmask, and the sum line by line (D-99). 30s tier. description: > Backed by `getScoreBreakdown(n)` on the market. DECISIONS.md D-99 replaced the three rarity tiers with ONE additive score, so this route answers "which categories did `n` earn, and what did each contribute" rather than "which bucket is it in". `breakdown` is derived here by joining `flags` against `SCORE_BITS` in `shared/opcodes.ts` and the digits of `n`; the weights are the owner-declared table the contract compiled from, and `/trophy/params` serves the chain's own copy beside it so a drift between the two is visible. Sum every line's `points`, clamp at `scoreMax`, and you must land on `score` — that recompute is why the route exists (CONCEPT.md §9.1). A number's categories are a fact about the number, so for a given `n` this moves only at a redeploy — the same slow tier as `/trophy/params`. `?factored=1` serves the same shape from `getScoreBreakdownFrom(n, factors)` over `n`'s own factorization — the score `/trophy/price/{n}` quotes an open on (F65/F67). The plain read stays the from-`n` score, which is what the head-mint rebate pays on. parameters: - $ref: "#/components/parameters/NumberPath" - name: factored in: query required: false schema: { type: string, enum: ["1"] } responses: "200": description: OK content: application/json: schema: oneOf: # Every number at or ahead of the head derives to an address with no item # deployed at it yet. That is an answer, not a failure — see NotDeployed. - $ref: "#/components/schemas/NotDeployed" - type: object properties: data: type: object properties: score: type: string description: The CLAMPED sum — what actually prices the lot. flags: type: string description: > The uint48 category bitmask as a decimal string (JSON numbers stop being exact at 2^53). NOT clamped, unlike `score`: a number keeps every category it earned even where the clamp bought it no more points. breakdown: type: array description: > Every set category plus the continuous LEN term, in bit order. A line appears only where it contributed, so an empty array is the honest rendering of a number that earned nothing. `Σ points` can exceed `score` on a number that hit the clamp; that difference IS the clamp and is meant to be seen. items: type: object properties: bit: type: integer description: > The permanent `SCORE_BITS` identifier; `-1` for LEN, which is continuous and therefore has no bit at all. key: { type: string, example: mersenne } group: { type: string, enum: [LEN, DIGIT, LIFE, FORM, CULTURE, POSITION] } points: type: integer description: > What this line contributed, which is not always the table weight: bit 5 pays per trailing zero and LEN pays `lenW · (lenRef − digits)`. note: type: string description: > Present only where the contribution needs arithmetic to be checkable — the LEN and per-trailing-zero lines. provenance: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /trophy/price/{n}: get: tags: [trophy] summary: P0(n), RES_GAS, the D-33 bid step, D(n) and the LANE, for any number. 30s tier. description: > Backed by `getLotPriceFrom(n, factors)` + `getLotLane(n)` on the market, with `getNpv(n)` and `getPrimeFloor(n)` folded in for a prime. A composite is quoted over its own factorization (the list `open_lot` must carry), so `p0Nanoton` is what opening charges, not the from-`n` lower bound `getLotPrice(n)`; a prime passes no list (F65). Under D-30 this is the ONLY quote there is, and it answers for every `n`. `lane` is D-88's rule, and it depends on primality AND on where the head is: * `0` — MODE_ASCENDING. ANY number the head has not reached, prime or composite. `open_lot` IS the first bid (you cannot open a lot you are unwilling to buy) and a 7-day clock follows. What differs by class is the PROOF the open carries (`kind`: a factorization for a composite, nothing for a prime, which the contract checks by its own Miller-Rabin) and winning MINTS the number at close, for both kinds (D-90). * `2` — MODE_DUTCH_BUYNOW. A PRIME the head has already passed. `open_head_lot` is permissionless and gas-only, refused before the turn with 1024; the clock always runs from open. The price descends to `primeFloorNanoton` and then PARKS there until somebody buys it — no expiry, and D-88 deletes the sweep that used to delete such a lot. * `-1` — not openable at all: a §7 primorial, which mints at the head only, or a number above the cap. A UI disables the action with a reason instead of quoting a price that would be refused. **Lane 0 no longer implies "composite".** `primeFloorNanoton` is the field that says which class `n` is: the chain answers it for a prime and only for a prime, and the two flows above differ, so a caller must read it rather than infer the class from the lane. D-88 point 2 also deletes the two special modes (`3`, `4`) this route used to be able to return — there are exactly two lot modes, and under D-99 a memorable number is one on an ordinary lane whose score lifted its price. `/trophy/score/{n}` publishes that score line by line. These are deliberately the same integers `getLot(n).mode` reports, so the answer before a lot exists and the answer after it exists are the same number. The lane comes from the CONTRACT's own Miller-Rabin, never from a caller-supplied `kind`; it is also the number to check a client-side sieve against, Carmichael numbers included. `p0Nanoton` is what OPENING costs on the ascending lane — and opening is a payment, because it is the first bid — and the price a Dutch lot starts its descent from. It is the same figure on both, deliberately: a prime opened by auction the day before its turn opens at exactly the price its descent would have started from the day after (D-88 point 3). It is the exact figure the contract measures the message value against; add a fee margin on top, or a busy block rejects the nominal. `resGasNanoton` rides along because the two lanes spend it differently: it is INSIDE `p0` on the ascending lane (`p0 >= LOT_FLOOR = 1 TON + RES_GAS`) and added ON TOP of the standing price on the Dutch lane. Publishing it here makes that arithmetic checkable without a second read. `minIncrementNanoton` is D-33's bid step evaluated at `p0`: the second bid must clear `p0 + minIncrement`. Once a lot exists, `GET /trophy/lot/{n}`'s `minNextBidNanoton` is the live figure. `score` is published with the price so "why does 1000 open at 128 TON" is answerable without reading bytecode (§9.1). Cross-reference `/trophy/params`, which publishes the weight vector and the `2^(D/10)` fixed-point table the price is computed from. `npvNanoton` and `primeFloorNanoton` are fetched only for a PRIME — both branches exist only there, so a composite would pay two upstream calls to learn numbers that cannot affect its price. The gate is primality and not the lane, because a prime ahead of the head is on lane 0 and still has both. `"0"` for a prime's NPV is a real answer, not a gap: the dictionary is finite by construction and a miss means the score ladder alone governs — D-99 deletes `PRIME_P0_MIN`, so there is no flat prime floor left to fall back to — and it is exactly the case whose resting price falls back to the 1 TON clamp. **409 for `n` above `AUCTIONABLE_MAX`.** `getLotPrice` throws 1020 `mkt_n_above_cap` there, and the refusal is cached on the same 30s key as a quote — it is a stable fact about `n`, not a failure — so an out-of-range input costs the same bounded `1 call / TTL` as any other. 30s tier: `P0(n)` is a pure function of `n` over compile-time constants and a deploy-fixed NPV dictionary that no message can change (there is no setter — that would be an owner who can reprice every prime lot, which the zero-admin-surface requirement exists to deny). For a given `n` the answer changes only at a redeploy. parameters: - $ref: "#/components/parameters/NumberPath" responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: p0Nanoton: { type: string } resGasNanoton: type: string description: > Inside p0 on the ascending lane; added on top of the standing price on the Dutch lane. minIncrementNanoton: type: string description: D-33's bid step evaluated at p0. score: type: string description: > `D(n)` — the one additive score that prices every number (D-99), clamped at `scoreMax`. `/trophy/score/{n}` breaks it into lines. flags: type: string description: > The uint48 category bitmask that produced `score`, as a decimal string. Bit `b` is `SCORE_BITS[b]` in `shared/opcodes.ts`. NOT clamped, unlike `score`. lane: type: integer enum: [-1, 0, 2] description: > -1 = not openable (a §7 primorial, or above the cap); 0 = MODE_ASCENDING — ANY number ahead of the head, either class; 2 = MODE_DUTCH_BUYNOW — a PRIME the head has passed. The same integers `getLot(n).mode` reports. Read `primeFloorNanoton` for the class: lane 0 does not imply composite. npvNanoton: type: string nullable: true description: > NPV(n) as the contract will actually price it. null for a composite (the branch does not apply); "0" for a prime the table does not carry. primeFloorNanoton: type: string nullable: true description: > `getPrimeFloor(n)` — D-88 point 1: where this prime's descending lot comes to REST and stays until somebody buys it, max(NPV(n)/2, P0(n)/3, MINT_PRICE). D-99 deletes the tier lift that used to raise it. null for a composite, which has no descending lane — so this doubles as the primality bit the lane no longer carries. Draw the descent down to THIS, not to `/trophy/params`'s `primeDutch.floorNanoton`, which is the global 1 TON clamp and equals the resting price only for a prime the finite NPV table misses. provenance: { $ref: "#/components/schemas/Provenance" } "409": { $ref: "#/components/responses/NAboveCap" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /trophy/params: get: tags: [trophy] summary: Every declared constant of the trophy lane (CONCEPT.md §4.4 point 1b). 30s tier. description: > Backed by `getMarketParams()` + `getFracTable()` + `getIncrementLadder()` + `getScoreWeights()` + `getMarketConfig()` + `getPrimeDutchTerms()` on the market, collapsed into ONE cache key. §9 calls the `params.json`-vs-contract comparison "what makes the whole proposal honest, because without it the ladder drifts out of the sim's control silently". This route is where a reader performs that comparison without running a node. `SCORE_HALVING`, `SCORE_MAX`, `AUCTION_WINDOW` and `BID_EXTENSION` are DECLARED in `params.json`'s exact sense — a stated constraint, owned by the owner, not a simulation output, and §6 is explicit that this document will not dress them as such. `bidExtensionSeconds` is 4020: 67 minutes, and 67 is prime, deliberately. D-30 DELETES `dMin` — the WORTH gate — from this response, because it deletes the constant from the contract: every composite is worth an auction now, so a score gate would be a refusal to sell rather than a redirect. Nothing is substituted for it. `minAuctionDistance` IS DELETED, by D-45 — a different decision than D-30's, and later. It used to be a LIVENESS gate: an ascending lot's 7-day window had to finish before the sequential head arrived at `n` and truncated it, so `open_lot` threw 1007 unless `n > headHint + minAuctionDistance`. D-45 deletes the truncation instead of widening the margin against it: the ledger now takes an opened composite off its sequential line the moment `open_lot` runs (permanently — see `mark_off_head` in the ledger's schema), so the head can never again reach, and therefore never truncate, an open lot's window, at any distance. A lot may now open immediately next to the head. This field is removed from the response rather than zeroed, so a client cannot mistake "no gate" for "gate at distance 0". `lotFloorNanoton` (was `reserveFloorNanoton`) is an IDENTITY, not an independent constant: `MINT_PRICE + RES_GAS`, i.e. **1.06 TON** — D-30 removes the `RES_PRIORITY_BASE` term of the old 1.05, and W8.1 re-derived `RES_GAS` to 0.06. Writing it as a sum is what makes the floor a checked equality rather than a coincidence that drifts the next time `RES_GAS` is re-measured — cross-check it against `/lots/fee-split`. `incrementLadder` is D-33's bid-increment ladder, `getIncrementLadder()`: 13 basis-point rungs indexed by the band `D(n) / scoreHalving` (0..12), then the shared nanoton floor and the basis-point denominator. The contract's rule verbatim is `min_increment = max(minIncrementFloor, high · bps[D / scoreHalving] / bpsDen)`, so §9.1 holds for the bid step the way `fracTable` makes it hold for `P0`: the minimum raise on any lot is recomputable off chain from `n` alone. The flat `minIncrementPct` is DELETED — the ladder replaced it, and there is no percentage left to publish. `fracTable` is `round(2^(j/10) · 2^16)` for j = 0..9 with `fracShift = 16`, published rather than merely commented because §9 names the integer approximation as the likeliest place for silent contract/sim drift. `peers` are the two genesis-fixed destinations: the ledger (the premium's first hop) and the ratchet the ledger forwards it to. Published so the zero-admin-surface claim is checkable from outside: these are the only addresses value can reach from the market, no message changes any of them, and there is no owner field, no reprice and no withdraw path. 30s tier, and it would be slower if there were a slower tier: nothing here can change without a redeploy. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: scoreHalving: { type: string, description: P0 doubles every this many points of D(n). } scoreMaxPoints: type: string description: > SCORE_MAX — 87, the ONE clamp on the score (D-99). `2^(87/10) = 420.4`, so this figure IS the 420 TON ceiling on an opening price. It occupies the tuple position `PRIME_P0_MIN` held before D-99 deleted that constant outright: there is no flat "a prime opens at >= 5 TON" floor any more, because one additive score prices every number and a prime that earns nothing opens like anything else. primeP0MultiplePct: type: string description: Integer percent (150 = 1.5×), inherited from §6. lotFloorNanoton: type: string description: > LOT_FLOOR — an identity, MINT_PRICE + RES_GAS (1.06 TON). D-30 drops the RES_PRIORITY_BASE term; W8.1 re-derived RES_GAS to 0.06. auctionWindowSeconds: { type: string, description: D4 — 604800 (7 days). } bidExtensionSeconds: { type: string, description: D10 — 4020 (67 minutes, prime). } minIncrementFloorNanoton: type: string description: > MIN_INCREMENT_FLOOR — 0.015 TON, the same value `incrementLadder` publishes. It replaced the flat 1 TON floor, which was defensible only while D_MIN kept the cheapest lot at 8 TON. fracTable: type: array items: { type: string } fracShift: { type: string } incrementLadder: type: object description: > D-33's bid-increment ladder. min_increment = max(floor, high · bps[D / scoreHalving] / bpsDen). properties: bps: type: array items: { type: string } description: 13 rungs, indexed by the band D(n) / scoreHalving (0..12). minIncrementFloorNanoton: { type: string } bpsDen: { type: string } scoreWeights: { $ref: "#/components/schemas/ScoreWeights" } peers: type: object properties: ledger: { type: string } ratchet: { type: string } provenance: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /vault: get: tags: [audit] summary: Bounty vault configuration (CONCEPT.md §3.3). 30s tier. description: Backed by `getVaultConfig` on the bounty vault contract. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: ledger: { type: string } jettonMaster: { type: string } jettonWallet: { type: string } walletSet: { type: boolean } provenance: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /vault/rebate/{kind}/{n}: get: tags: [audit] summary: Rebate schedule and paid status for a rebate kind + n (D-99). 5s tier. description: > Backed by `getRebateSchedule(kind, n)` on the bounty vault (PRICING_MODEL_PLAN.md §1.5). There are exactly TWO kinds and they are populations, not rarities: `0` a composite minted AT THE HEAD, paid PER POINT of its score, and `1` a prime of the rebate family (`p > 100,000`, `p ≡ 1 mod 101`), paid flat by any route. An auction-won composite is paid nothing — the premium is the price of impatience. On kind `0` the two amounts are per point, so multiply them by `score` from `GET /trophy/score/{n}`; the multiplication is left to the reader precisely so it is one they can check (CONCEPT.md §9.1). `scheduled: false` means the vault's genesis-fixed `rebateSchedule` has no entry for that kind at all — a capability gap, not a claim status. `paid` flips permanently once `n`'s rebate has been sent (the vault's `paidSpecials` idempotency map), so a UI can distinguish "still claimable" from "already collected" without racing the claim transaction. parameters: - name: kind in: path required: true schema: { type: integer, enum: [0, 1] } description: 0 = composite at the head (per point), 1 = prime rebate family (flat). - $ref: "#/components/parameters/NumberPath" responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: scheduled: { type: boolean } bountyTonNanoton: type: string description: Per POINT on kind 0; flat on kind 1. maxPrimesNanoton: type: string description: The `min(bountyTon · S/T, maxPrimes)` clamp ceiling. Per point on kind 0. paid: { type: boolean } provenance: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /vault/era/{n}: get: tags: [audit] summary: The era ceremony's bounty for a primorial boundary (CONCEPT.md §7). 30s tier. description: > Backed by `getEraSchedule(n)` on the bounty vault. Whoever mints one of §7's eight boundaries takes the whole era bounty — the largest payout the vault makes to a minter, three orders of magnitude above §9.6's score rebate (0.000213 TON per point of `D(n)`, against 0.21 TON flat at the first boundary) and the only one attached to a particular number rather than to a score. It is budgeted from the genesis bounty vault (§3.3), so it is pre-counted in `S` and touches neither the split nor the ratchet. TON-denominated (DENOMINATION_PLAN P4 / D22). The vault pays `min(bountyTon · S/T, maxPrimes)` nano-PRIMES, where `S` and `T` are `GET /ledger/floor`'s two terms. The multiplication is left to the reader on purpose (CONCEPT.md §9.1), the same split `/vault/rebate/{kind}/{n}` makes for the score. `scheduled: false` is every `n` that is not one of the eight boundaries. `paid` flips permanently when the ceremony fires. parameters: - $ref: "#/components/parameters/NumberPath" responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: scheduled: { type: boolean } bountyTonNanoton: type: string description: The ceremony's fixed VALUE in nanoTON. maxPrimesNanoton: type: string description: The nominal PRIMES ceiling the payout is clamped to. paid: { type: boolean } provenance: { $ref: "#/components/schemas/Provenance" } "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } # --------------------------------------------------------------------------- # event-poller-backed (served by a different service, listed for completeness) # --------------------------------------------------------------------------- /feed: get: tags: [events] summary: Recent mint feed (event-poller-backed — NOT a get-method call). description: > Served by the event-poller service (backend/event-poller), not read-proxy itself — listed here for API-surface completeness since FRONTEND_PLAN.md treats it as part of the same "live stats" surface. This endpoint is indexed-event-backed, not a live get-method read, so it intentionally carries NO `provenance` field in the get-method sense — its honesty framing is "recomputable from indexed on-chain events," a different (weaker) claim than a direct get-method call, and is documented as such rather than given a misleading provenance object. responses: "200": description: OK content: application/json: schema: type: object properties: mints: type: array items: type: object events: description: > The same window, widened past mints: one row per indexed event, newest first, each carrying `kind` (`mint` | `flush` | `claim`), `ts`, and the fields that kind has, including the `tx_hash` of the transaction that produced it so a client can link the row to an explorer. `first_for_actor` marks a wallet's first indexed mint — a display flag over indexed history, nothing is paid on it. Pool swaps are absent on purpose: nothing here observes DeDust, and the flush trigger does not carry the swapped amount. type: array items: type: object /leaderboard/tribute: get: tags: [events] summary: Tribute leaderboard (event-poller-backed — see /feed's provenance note). description: > `around` is **D-124's band**: given a wallet, the route answers the contiguous run of rows centred on it (four above, five below) with the board's own leader prepended when rank 1 falls outside that run — so a reader outside the top ten sees the rows they can actually pass. The leader is kept because the client draws every bar against the maximum of what was served. A wallet that has claimed nothing has no seat here and the top-N is served instead, never a fabricated rank. Every row carries its true `rank` either way. parameters: - name: around in: query required: false schema: { type: string, maxLength: 128 } description: Wallet to centre the board on. Omit for the top-N; over 128 characters is a 400. responses: "200": description: OK content: application/json: schema: type: object properties: leaderboard: type: array items: type: object properties: owner: { type: string } total: { type: string } rank: type: integer description: True position on the whole board, not the index in what was served. /number/{n}/referrals: get: tags: [events] summary: Paid referral count for one referral key (event-poller-backed — see /feed's provenance note). description: > Mints that quoted `n` as their referral key AND that the ledger actually paid (`primes_ledger.tolk` referralValid: key >= 2, sold before the quoting mint, and never taken off-head by a lot). Same predicate as the read-index's /boards/referrals. parameters: - in: path name: n required: true schema: { type: integer, minimum: 0, maximum: 4294967295 } responses: "200": description: OK content: application/json: schema: type: object properties: referralKey: { type: integer } mintCount: { type: string } "400": description: n is not a decimal integer that fits 32 bits. /wallets/{address}/earn-facts: get: tags: [events] summary: Everything /earn needs from the EVENT index, for one wallet and its key (event-poller-backed — see /feed's provenance note). description: > GAUNTLET.md J8 / F50. The funnel's two PROVED tiles and six of the sixteen quest numerators are facts about TRANSACTIONS, not about current state, so no get-method answers them: "how many mints cited your key and the ledger paid" is a scan of `mints`, not a selector on the ledger. Both halves were previously rendered from a zero — the funnel from an `attributed_mints` table whose only writer was a route nothing ever called, the quests from a literal `0` in the webapp's request body. `mintCount`/`giftMints` key off `mints.payer` (who PAID), never `mints.owner` (which is the gift's recipient); `lotsOpened` counts lots this wallet OPENED, on either lane. The `referral` block is present only when `key` is supplied, and its three counts are three different questions: wallets that cited the key at all, PAID citations, and distinct wallets among those. parameters: - in: path name: address required: true schema: { type: string } - in: query name: key required: false description: The sharer's referral key `rN`. Omit it and `referral` is null. schema: { type: integer, minimum: 0, maximum: 4294967295 } responses: "200": description: OK content: application/json: schema: type: object properties: wallet: { type: string } mintCount: { type: integer } giftMints: { type: integer } tributeClaims: { type: integer } lotsOpened: { type: integer } paidReferralMints: type: integer description: Mints this wallet PAID for whose referral line the ledger paid — the §9.4 social quests' mint gate (GAUNTLET.md J9). Needs no key. ownedKeyPaidMints: type: integer description: Paid referral mints citing a number this wallet owns now (`number_owners`) — the story quest's mint gate at key granularity (DECISIONS.md D-160). Needs no key. referral: nullable: true type: object properties: referralKey: { type: integer } distinctMinters: { type: integer } paidMints: { type: integer } distinctPaidMinters: { type: integer } "400": description: key is not a decimal integer that fits 32 bits. /attrition: get: tags: [events] summary: > Claimable-state decay — how much of the collection has drifted into dead hands (CONCEPT.md §12; event-poller-backed, see /feed's provenance note). description: > CONCEPT.md §12's "Claimable state decays irreversibly" bullet, made measurable. A prime whose owner has lost their key keeps accruing tribute into `owed` and its share of `div_acc` forever and nothing can ever move it — `claim_tribute` is owner-gated and there is no admin path (§8) — so every such loss is a one-way transition and the claimable share of accrued value is monotone non-increasing. COUNTS ONLY. The TON amounts these counts refer to live on the contracts and must be read from there: `get_owed` on the item for the divisor line, and `getClaimableDividend(p)` on the ledger for the flat dividend not yet converted (§9.1). Joining the two is the dashboard's job; this endpoint deliberately does not assert a TON figure it cannot verify. `dormancyDays` is a REPORTING WINDOW and nothing more. Nothing expires, nothing is reclaimed, and no contract behaviour keys off it — changing it changes the answer, never the state. parameters: - in: query name: dormancyDays required: false schema: { type: number, default: 90, exclusiveMinimum: 0 } description: Days since a prime's last claim (or its mint, if never claimed) before it counts as dormant. responses: "200": description: OK content: application/json: schema: type: object properties: dormancyDays: { type: number } primesMinted: { type: integer } neverClaimed: type: integer description: Minted primes with no claim event at all, at any age. dormant: type: integer description: Minted primes whose last claim (or mint, if never claimed) predates the window. active: { type: integer } dormantShare: type: [number, "null"] description: > null when nothing has been minted. "No primes yet" and "no primes are dormant" are different statements and rendering the first as 0% is a lie. neverClaimedShare: { type: [number, "null"] } oldestDormantSince: { type: [string, "null"], format: date-time } dormantPrimes: type: array items: type: object properties: prime: { type: string } owner: { type: [string, "null"] } lastClaimAt: { type: [string, "null"], format: date-time } mintedAt: { type: string, format: date-time } source: type: object description: Where each half of the metric comes from, per §9.1. "400": description: dormancyDays was not a positive number of days. /portfolio/{address}: get: tags: [events] summary: Numbers attributed to one wallet, with their tribute history (event-poller-backed — see /feed's provenance note). description: > Mint-time attribution (the gift beneficiary, else the payer) unioned with the wallet's current holdings from an NFT holder index when one is configured (`attribution` says which). Transfers are not indexed, so a number sold on since its mint is still listed. NOT authority: confirm every row against the item's `getNumberData()` owner (read-proxy `POST /numbers`) before calling it owned. TON balances live on chain — `item.get_owed(p)` and `ledger.getClaimableDividend(p)`. parameters: - { name: address, in: path, required: true, schema: { type: string } } responses: "200": description: OK content: application/json: schema: type: object properties: address: { type: string } attribution: { type: string, enum: ["mint-time", "mint-time+holder-index"] } numbers: type: array items: type: object properties: number: { type: string } isPrime: { type: boolean } factors: {} mintedAt: { type: string, format: date-time } txHash: { type: string } viaAuction: { type: boolean, description: Won at auction ahead of the head (D-90). } gifted: { type: boolean } creditCount: { type: integer } creditedNanoton: { type: string, description: Replayed from mint bodies — not a get-method reading. } claimedNanoton: { type: string } lastClaimAt: { type: [string, "null"], format: date-time } source: type: object description: derivedFrom, note and tonAmountsLiveElsewhere, per §9.1. /primes/{p}/ledger: get: tags: [events] summary: One prime's tribute credit-and-claim history (event-poller-backed — see /feed's provenance note). description: > Newest first. A `claim` row's amount is read off a real `claim_tribute` body and is chain truth; a `credit` row's `shareNanoton` is replayed arithmetic over the mint body that no get-method returns. `reconciliation.impliedOutstandingNanoton` may legitimately EXCEED the item's `getOwed(p)` (a forfeited unsold lot's share, or a bounced `credit_tribute` parked in `unrouted_credits`), never fall below it. `getOwed(p)` is the authoritative balance. parameters: - { name: p, in: path, required: true, schema: { type: string, pattern: "^[0-9]+$" } } - { name: limit, in: query, required: false, schema: { type: integer, default: 100 } } responses: "200": description: OK content: application/json: schema: type: object properties: prime: { type: string } truncated: { type: boolean } lines: type: array items: type: object description: > `kind: credit` carries number, exponent, s, sumWeights, shareNanoton; `kind: claim` carries amountNanoton and owner. Both carry txHash and ts. properties: kind: { type: string, enum: [credit, claim] } number: { type: string } exponent: { type: integer } s: { type: string } sumWeights: { type: string } shareNanoton: { type: string } amountNanoton: { type: string } owner: { type: string } txHash: { type: string } ts: { type: string, format: date-time } reconciliation: type: object properties: creditCount: { type: integer } grossCreditedNanoton: { type: string } settledNanoton: { type: string } impliedOutstandingNanoton: { type: string } note: { type: string } "400": description: p is not a decimal integer, or limit is out of range. /lots/won: get: tags: [events] summary: Numbers won at auction ahead of the head (event-poller-backed — see /feed's provenance note). description: > One row per closed ascending lot with a winner, written at the lot's close. Since D-90 the close IS the mint, so every row is `settled` and the route takes no filter. Ownership is the item's `getNumberData()`, not this list. responses: "200": description: Raw `reservations` rows, ascending by number. content: application/json: schema: type: array items: type: object properties: number: { type: string } state: { type: string, enum: [settled] } reserver: { type: [string, "null"] } beneficiary: { type: [string, "null"] } referral_key: { type: integer } reserved_tx: { type: [string, "null"] } reserved_at: { type: [string, "null"], format: date-time } resolved_tx: { type: [string, "null"] } resolved_at: { type: [string, "null"], format: date-time } /invite-config: get: summary: The invite line's signer key, off the ledger (DECISIONS.md D-101/D-108) description: > `getInviteConfig()` on the LEDGER, reduced to the two facts a mint checkout has to know before it spends 1 TON: which ed25519 public key the ledger will verify an invite list against (`signerPubkey`) and whether `SetInviteConfig` has run at all (`inviteSet`). Under D-108 a signature the ledger cannot verify throws 220 and an unset config throws 222, and either ABORTS THE WHOLE MINT — so a keeper answering with a key this ledger does not verify against is a burnt 1 TON per attempt, and `SetInviteConfig` is one-shot and not rotatable. `signerPubkey` is lowercase 64-hex, zero-padded, with no `0x`: the exact spelling the keeper publishes as `signerPublicKey` on `GET /api/invitees`, so the two compare with `===` and nothing normalises anything. `found: false` means this deployment publishes no invite config at all — either no account behind the ledger address or bytecode predating the get-method — and a client must read it exactly as it reads a mismatch: do not offer the invite line. The generators collection address the get-method also returns is deliberately not republished here; this service reads the collection it is configured with. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: found: type: boolean description: > False when this deployment publishes no invite config (no account, or bytecode without the get-method). The other two fields are then null and false; a no-account ledger answers the house's bare `{found: false}`. signerPubkey: type: string nullable: true pattern: "^[0-9a-f]{64}$" description: > The ed25519 public key the ledger verifies invite signatures against. Lowercase 64-hex, zero-padded, no `0x`. Null iff `found` is false. inviteSet: type: boolean description: > `SetInviteConfig` (0xb0000046) has run. One-shot and not rotatable. provenance: { type: object } "429": { $ref: "#/components/responses/RateLimited" } "503": description: PRIMES_LEDGER_ADDRESS is not configured. /collections: get: summary: The three issued collections, as three cards (webapp /trade) description: > The numbers, generators and constellations collections in one read: what each is, how many items exist, and how it is doing on the secondary market. TWO KINDS OF NUMBER, AND THE RESPONSE SAYS WHICH IS WHICH. `issued` is a get-method return and carries a normal `Provenance` naming it — and it is a DIFFERENT method per collection. The numbers card reads `getLaunch().itemsMinted` on the LEDGER, not the collection's own `next_item_index`: D-20 makes `next_item_index` the ledger's HEAD, which walks composites only and therefore runs ahead of the true item count by π(n) and further for every number an auction minted off-head. Generators and constellations read `getCollectionData().mintedCount` on their own collection. `market` is NOT a get-method value and never can be — no contract in this system records what its items sold for on a third-party venue. Volume, floor, the live listing count and the trade count are computed from Dune's `ton.nft_events` (`tools/dune/collection-market.sql`) — EVERY TON marketplace, not one venue's view of its own order book — and ride `MarketplaceProvenance` (`source: "marketplace"`), a distinct type so a client cannot render them in a proof drawer beside a get-method citation. §9.1 is satisfied by stating the source, not by pretending the number is chain state. Volume is SECONDARY ONLY and TON-denominated. A venue's headline "total volume" blends its own primary mint revenue in; this game's primary sales are the 1 TON mints `issued` already reports from a get-method, so blending would double-count. Amounts are NANOTONS as decimal strings, because they are magnitudes a float would round. A CARD DEGRADES ALONE. An unconfigured role, an undeployed collection, or absent Dune credentials sets that card's `issuedUnavailable` / `marketUnavailable` reason; the other cards answer normally and the route does not 503. Every deployment is in that state until the genesis that deploys all three. A COLLECTION THAT HAS NEVER TRADED IS NOT AN ABSENCE: it returns `market` present with every figure zero and `floorNanoton` null. "Nothing has sold yet" and "we could not find out" are different answers and this route does not collapse them. The market read NEVER BLOCKS: it serves the last computed rows and fires a refresh behind them, so a request never waits on a ~16s query execution. responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: collections: type: array items: type: object properties: key: type: string enum: [numbers, generators, constellations] address: type: string nullable: true description: The collection contract; null when the role is unconfigured. handle: type: string description: > GetGems short-link slug (mainnet only). The client builds the URL, falling back to the /collection/{address} form on testnet. issued: type: string nullable: true description: > Items in existence, from the get-method named in `issuedProvenance`. Null is "unknown", never zero. issuedProvenance: type: object nullable: true issuedUnavailable: type: string nullable: true market: type: object nullable: true description: > Marketplace-index figures across every TON marketplace. NOT chain state, and never rendered as though they were. properties: volumeNanoton: type: string description: > All-time realised, TON-denominated, SECONDARY only. Nanotons as a decimal string. trades: type: integer description: > Sales behind `volumeNanoton`. A volume with no trade count cannot be read. floorNanoton: type: string nullable: true description: > Lowest FIXED-PRICE ask among items listed right now, in nanotons. Null means nothing is listed, which is an answer. Auction reserves are excluded — a reserve is not a price anyone can pay. onSale: type: integer description: Items on sale right now, fixed-price and auction. lastSaleAt: type: integer nullable: true description: Epoch ms of the most recent sale. marketProvenance: type: object nullable: true description: MarketplaceProvenance — `source: "marketplace"`. marketUnavailable: type: string nullable: true description: > `not-configured` = no Dune credentials on this deployment; `not-computed-yet` = the query has not been executed for these addresses (a refresh is already in flight); `unavailable` = the index could not be reached. enum: [not-configured, not-computed-yet, unavailable] "429": { $ref: "#/components/responses/RateLimited" } /constellations: get: summary: The built number line's two totals (DECISIONS.md D-102) description: > `getTotals()` on the registrar — `openBuilds` is how many builds stand open right now (up on `open_build`, down on a close or an expiry) and `builtTargets` is how many targets have ever been built, the once-forever edge, monotone. TWO values, not three: D-107 moved the accrued-PRIMES figure to the bounty vault's `getDiscoveryAccrued()`. The path is inherited from the retired §3.2.2 set surface (D-102 deleted the set registry and every other `/constellations/*` route with it); the field names are the mechanic this actually measures, and nothing here counts a "set". responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: openBuilds: { type: integer } builtTargets: type: integer description: Targets ever built. Monotone — a target is built once, forever. provenance: { type: object } "429": { $ref: "#/components/responses/RateLimited" } "503": description: PRIMES_REGISTRAR_ADDRESS is not configured. /constellations/by-owner/{address}: get: tags: [events] summary: > Every constellation one wallet built (event-poller-backed — see /feed's provenance note). description: > Served by the event-poller, not read-proxy, and for the same reason `/events/generators/by-owner/{address}` is: NO GET-METHOD ANSWERS IT. A constellation item's address is derived from its TARGET, the collection ships no owner index and cannot (the reachable target set is unbounded — CONCEPT.md §3.2.2's chaining), so the poller's `constellations` table is the only record of which targets exist at all. `owner` is MINT-TIME. Transfers of constellation items are deliberately not indexed (merging the two number lines into one ownership table is the confusion D-102 exists to prevent), so a constellation sold on since it was built is still listed here. `getConstellationData().owner`, via `/constellation/{t}`, is who holds it now. Unbounded cardinality, so it is inbound rate-limited per IP per route. parameters: - in: path name: address required: true schema: { type: string } - in: query name: limit required: false schema: { type: integer, default: 100, minimum: 1 } responses: "200": description: OK content: application/json: schema: type: object properties: owner: { type: string } constellations: type: array items: type: object properties: target: { type: string, description: "uint256 decimal — the built number." } op: { type: integer } inputCount: { type: integer } inputs: { type: array, items: { type: string } } inputIsCon: { type: integer, description: "Bitmask: input i is a constellation." } contributorCount: type: integer description: > The count only. The contributor LIST is on `/constellation/{t}`, so a rail's response size follows `limit` and not build width. classFlags: { type: string, description: "SCORE_BITS uint64; bit 16 = prime." } builtAt: { type: string, format: date-time } builtAtHead: type: string nullable: true description: > G69b: the head when the build closed, decimal (the plate's era ground). null on a row indexed before event-poller migration 025. txHash: { type: string } derivation: type: string description: Which indexed message these rows are replayed from, and where the live answer is. "400": description: limit was not an integer inside the row cap. "429": { $ref: "#/components/responses/RateLimited" } /constellations/recent: get: tags: [events] summary: > The built line, newest first, whoever built it (event-poller-backed — see /feed's provenance note). description: > The same `constellations` rows `/events/constellations/by-owner/{address}` serves, unfiltered by owner. It exists because `/trade`'s constellations card has to show the built line's own artwork, and a constellation's plate draws a RECIPE rather than an index — so an example of the line can only be a build somebody performed, and nothing on chain can enumerate which targets those are (the item index IS the target and the reachable target set is unbounded, CONCEPT.md §3.2.2). A FIXED path whose result set does not grow with caller-chosen input, so unlike the by-owner sibling it takes no per-IP limiter. `owner` is MINT-TIME here too; `getConstellationData().owner`, via `/constellation/{t}`, is who holds one now. parameters: - in: query name: limit required: false schema: { type: integer, default: 12, minimum: 1 } responses: "200": description: OK content: application/json: schema: type: object properties: constellations: type: array description: Same row shape as /constellations/by-owner/{address}. items: { type: object } derivation: { type: string } "400": description: limit was not an integer inside the row cap. /vault/discovery/{address}: get: tags: [audit] summary: The discovery bounty owed to one wallet (DECISIONS.md D-107). 30s tier. description: > `getDiscoveryOwed(addrHash)` on the BOUNTY VAULT — D-107 moved the discovery book, the amount, the per-tier window and the claim off the registrar, whose `getDiscoveryOwed` and `claim_discovery` (0xb000006a) are retired. `claim_discovery` (BOUNTY_VAULT 0x90000006) is permissionless and keyed on the SENDER, draining exactly this line, so this is also what a claim will pay out. `address` is an ordinary wallet address: the get-method's argument is that wallet's 256-bit ACCOUNT ID (`getWorkchainAndHash().1`), and this route converts. That conversion is not the caller's to make — the ledger and registrar key their address dictionaries by a different 256-bit value for the same wallet, and the lookup has a zero fallback, so the wrong spelling answers a confident `0` rather than an error. parameters: - in: path name: address required: true schema: { type: string } responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: owedNanoprimes: { type: string, description: "nanoPRIMES accrued and not yet claimed." } provenance: { $ref: "#/components/schemas/Provenance" } "400": description: address was not a valid TON address. "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /vault/discovery/tier/{tier}: get: tags: [audit] summary: One discovery tier's first-N window (DECISIONS.md D-107). 30s tier. description: > `getDiscoveryTier(tier)` on the bounty vault. D-107 bounds who gets PAID, not the line: the first `paidWindowN` payable builds of a tier accrue `bounty` nanoPRIMES each, and every build after them mints the constellation and pays zero — which is why `builds` runs ahead of `paidCount` and both are published. A BOUNDED quantity with a published floor and ceiling, so the count and the window are served separately and no ratio is computed here; a percentage is a number no get-method returned. Tiers are `1..4` (common, uncommon, rare, legendary — `shared/params.json` `generators.tiers`). The get-method is pure and answers all-zeros for any integer, so an out-of-range tier is refused here rather than served as a real tier nobody funded. Bounded cardinality — four keys for the whole world — so unlike its sibling above it is not in the inbound rate limiter's enumerable list. parameters: - in: path name: tier required: true schema: { type: integer, minimum: 1, maximum: 4 } responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: configured: type: boolean description: False for a tier the genesis schedule never funded; the rest are then zeros. paidWindowN: { type: integer, description: "The window's ceiling." } paidCount: { type: integer, description: "Its fill." } builds: { type: integer, description: "Every build of the tier, window or no window." } bountyNanoprimes: { type: string } provenance: { $ref: "#/components/schemas/Provenance" } "400": description: tier was not an integer in 1..4. "429": { $ref: "#/components/responses/RateLimited" } "503": { $ref: "#/components/responses/NotConfigured" } /generators/{addr}: get: summary: One deployed generator, read off the item contract description: > CONSTELLATIONS_PLAN.md §2 phase 3 / DECISIONS.md D-101. `addr` is a GENERATOR ITEM's contract address, not a wallet — a wallet's generator list is indexed history no get-method answers, and it is served by the event-poller. `referralKey` is a NUMBER (`rN`, uint32), the minter's number carried onto the generator, which is what makes §4.1's existing referral line pay the minter when the invitee mints. An item with no deployed account — never minted, or burned on a build close — answers `data.found: false`. parameters: - in: path name: addr required: true schema: { type: string } description: The generator item's contract address, as `/generators/by-owner/{address}` reports it (`itemAddress`). responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: generatorId: { type: string, description: "decimal — the TEP-62 index, kind << 58 | sha256(00 42 || n || slot) mod 2^58 (D-165: kind drawn at deploy)." } kind: { type: integer, description: "1..18, the operation." } tier: { type: integer, description: "1..4, common/uncommon/rare/legendary." } owner: { type: string } referralKey: { type: integer, description: "rN — a number, never an address." } originN: { type: string, description: "uint256 decimal — the mint that funded this generator." } slot: { type: integer, description: "1..5." } registrarAddr: { type: [string, "null"], description: "addr_none until the collection populates it." } mintedAt: { type: integer } found: { type: boolean, description: "false, and the only field present, when no account is deployed." } provenance: { $ref: "#/components/schemas/Provenance" } "400": description: addr was not a valid TON address. "429": { $ref: "#/components/responses/RateLimited" } "503": description: PRIMES_GENERATORS_COLLECTION_ADDRESS is not configured. /generators/by-owner/{address}: get: tags: [events] summary: > Every invite generator ever minted to one wallet (event-poller-backed — GAUNTLET.md F28, sibling of /constellations/by-owner/{address}). description: > Served by the event-poller, not read-proxy, for the same reason `/constellations/by-owner/{address}` is: NO GET-METHOD ANSWERS IT. A generator item's address is no longer derivable from `(origin_n, slot)`: since DECISIONS.md D-165 the kind half of its TEP-62 index is drawn at deploy, so the poller records the item address from the deploy transaction's populate out-message. There is no owner index on chain to walk — the poller's `generator_mints` table is the only record of which generators exist. `owner` on every row is MINT-TIME. A generator is an ordinary TEP-62 item and is transferable, so this list is "was ever sent one", not "holds one now" — `getGeneratorData().owner`, via `/generators/{addr}`, is the authority for who holds it now. Unbounded cardinality, so it is inbound rate-limited per IP per route. parameters: - in: path name: address required: true schema: { type: string } - in: query name: limit required: false schema: { type: integer, default: 100, minimum: 1 } responses: "200": description: OK content: application/json: schema: type: object properties: owner: { type: string } generators: type: array items: type: object properties: originN: { type: string, description: "uint256 decimal — the mint that funded this generator." } slot: { type: integer, description: "1..5." } itemAddress: type: string nullable: true description: > The generator item's contract address, read from the deploy transaction's populate out-message (D-165: not derivable from originN/slot). null when the upstream did not report the out-message. referralKey: { type: string, description: "uint32 rN, string-or-number at the boundary — see GeneratorMintRow." } txHash: { type: string } lt: { type: string } ts: { type: string, format: date-time } derivation: type: string description: Which indexed message these rows are replayed from, and where the live owner answer is. "400": description: limit was not an integer inside the row cap. "429": { $ref: "#/components/responses/RateLimited" } /generators/refusal/{address}: get: tags: [events] summary: > Why a generator sent into a build came back (event-poller-backed — GAUNTLET.md F27, DECISIONS.md D-111). description: > Served by the event-poller, not read-proxy, and keyed by the generator ITEM's own address — the same key `/generators/{addr}` uses, not a wallet. The registrar cannot refuse a generator by throwing (`ownership_assigned` is NoBounce and the item already saved `owner = registrar` in its own earlier transaction), so a refused generator is RETURNED carrying `GENERATOR_REFUSAL_TAG` + a reason in its `custom_payload` (`shared/opcodes.ts` `GENERATOR_REFUSAL_REASONS`), decoded from the SAME transaction's own out-messages (D-111's same-transaction resolution — no second watched account). `refusal: null` is SILENCE, not an accept: an accepted generator's transfer writes no row in migration 014's `generator_refusals` at all, so this answers `null` for both "no refusal happened" and "the poller has not caught up to the transaction yet" — a caller polls it briefly after a generator's own `generatorHeld` reads back `0` following a send, and gives up rather than polling forever. Unbounded cardinality (any generator item address), so it is inbound rate-limited per IP per route. parameters: - in: path name: address required: true schema: { type: string } description: The generator item's own contract address, the one just sent into a build. responses: "200": description: OK content: application/json: schema: type: object properties: itemAddr: { type: string } refusal: type: [object, "null"] description: "null when no refusal has been indexed for this item." properties: reason: { type: integer, description: "shared/opcodes.ts GENERATOR_REFUSAL_REASONS." } returnedTo: { type: string } txHash: { type: string } lt: { type: string } ts: { type: string, format: date-time } derivation: type: string description: Which indexed message this row is replayed from, and what null does not mean. "429": { $ref: "#/components/responses/RateLimited" } /build/{target}: get: summary: One registrant's open build on one target, and whether the target has ever been built description: > DECISIONS.md D-102's built number line. D-192 (V27.2): a target carries one open build per registrant, so `registrant` names which one (`getBuild(t, registrant)`); without it no build is read and only `built`/`builder` are live. `open` is the contract's own `op == 0` sentinel inverted — "never opened" and "closed or expired" are the same on-chain state, so both read as `open: false` rather than as a 404. `proofsIn` is the popcount of the `proved` mask the contract published. `built`/`builder` come from the constellation ITEM at t (the constellations collection's `getItemAddress(t)`, then the item's TEP-62 `get_nft_data` init flag and owner) — D-188 point 6 deleted the registrar's `isBuilt` map, and the item's refusal of a second Populate is the once-forever edge. `builder` is the item's current owner: the builder until the constellation is transferred. `closing` is 1 while a close's mint awaits the item's confirmation. Polled, and carries a row in the read-proxy's measured upstream budget — which is what put it on the 30s TTL tier, and a build lasts until the next primorial boundary, so the lag costs nothing. parameters: - in: path name: target required: true schema: { type: string, pattern: "^[0-9]+$" } - in: query name: registrant required: false description: The wallet that opened the build. A malformed address is a 400. schema: { type: string } responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: open: { type: boolean } registrant: { type: [string, "null"] } op: { type: integer, description: "The operation from the fixed table; 0 = no build." } inputCount: { type: integer } inputs: { type: array, items: { type: string }, description: "uint256 decimals, ordinal order." } inputIsCon: { type: integer, description: "Bitmask over input ordinals: input is a constellation." } proved: { type: integer, description: "Bitmask over input ordinals: ownership proved." } proofsIn: { type: integer } contributors: { type: array, items: { type: string } } contributorCount: { type: integer } generatorHeld: { type: integer } generatorId: { type: string } expiryHead: { type: string, description: "The head the build lapses at (the next primorial boundary)." } openedAt: { type: integer } closing: { type: integer, description: "1 while a close's mint awaits the constellation item's confirmation (D-188)." } headHint: { type: string, description: "The registrar's headHint (getBuild's 14th value, same as getHeadHint()) — the clock expiryHead is checked against. It only lags the ledger head, so a build has lapsed (and expire_build is not refused 908) exactly when headHint >= expiryHead (D-192). \"0\" when no build was read." } built: { type: boolean } builder: { type: [string, "null"] } provenance: { type: array, items: { type: object } } "400": description: target was not a decimal uint256. "429": { $ref: "#/components/responses/RateLimited" } "503": description: PRIMES_REGISTRAR_ADDRESS is not configured. /constellation/{t}: get: summary: One built number, off its own item description: > SINGULAR, deliberately — `/constellations` is the built line's TOTALS and this is ONE built number. A constellation is a SECOND number line: constellation 23 is not number 23, and no field here is derived from the main collection. `classFlags` is the same `SCORE_BITS` word `/trophy/score/{n}` publishes, and `classes` is that word joined against the table in `shared/opcodes.ts`; `isPrime` is BIT_PRIME (bit 16), the theorem the closer's Miller-Rabin proved. A target that has not been built has no deployed item and answers `data.found: false`. parameters: - in: path name: t required: true schema: { type: string, pattern: "^[0-9]+$" } responses: "200": description: OK content: application/json: schema: type: object properties: data: type: object properties: target: { type: string } owner: { type: string } op: { type: integer } inputCount: { type: integer } inputs: { type: array, items: { type: string } } inputIsCon: { type: integer } contributors: { type: array, items: { type: string } } contributorCount: { type: integer } classFlags: { type: string, description: "uint64 decimal." } classes: { type: array, items: { type: string } } isPrime: { type: boolean } builtAt: { type: integer } builtAtHead: { type: string, nullable: true, description: "G69 / D-125: the head when the build closed, decimal; null on bytecode that predates the stamp." } itemAddress: { type: string } found: { type: boolean, description: "false, and the only field present, when the target has not been built." } provenance: { type: array, items: { type: object } } "400": description: t was not a decimal uint256. "429": { $ref: "#/components/responses/RateLimited" } "503": description: PRIMES_CONSTELLATIONS_COLLECTION_ADDRESS is not configured.