putty
API ReferencePublic

Serve a public object by key

Returns the bytes of a public object with `Cache-Control: public, max-age=31536000, immutable`. The path captures the key after the `public/` prefix (e.g. `/public/tokens/<chain>/<address>/logo.png`). A key that does not exist in storage returns 404. The route is rate-limited per IP and unauthenticated; private objects are reachable only through the signed-url route on the blob store.

GET
/public/{key}

Returns the bytes of a public object with Cache-Control: public, max-age=31536000, immutable. The path captures the key after the public/ prefix (e.g. /public/tokens/<chain>/<address>/logo.png). A key that does not exist in storage returns 404. The route is rate-limited per IP and unauthenticated; private objects are reachable only through the signed-url route on the blob store.

Path Parameters

key*string

The object key under the public/ prefix. Other prefixes return 404.

Length1 <= length <= 512

Response Body

application/octet-stream

application/json

application/json

curl -X GET "https://example.com/public/string"
"string"

Read the operator overview GET

A single read-only snapshot of the platform: dependency health, the run queue's depth, every active run with the age of its lease, and the buyback amount owed. `buyback.owedUsd` is a decimal string summing `RevenueEntry.buybackShareUsd` over the whole ledger, which is the spec's record of the share every payment marks (`prompt.md:1610-1612`). Execution of a buyback is manual and timing-discretionary per `tokenomics.md`; this route only reports the obligation. It is a string rather than a number because the column is `TEXT` and dollar amounts are never summed as floats. **Authorization.** Send the operator token as a bearer: `Authorization: Bearer $OPERATOR_TOKEN`. A request with no token is `401`; a token that is not the operator token is `403`. When `OPERATOR_TOKEN` is unset the route refuses every caller, including one holding a valid Privy session — it denies by default rather than falling back to a permissive check.

Read one token as a public entity GET

One call that answers everything a person needs **before** they buy a token: the Scan, the Verify label that ships attached to it, the Receipts document, and whether this token's creator published a site to the public showcase. It exists so Explore does not have to fan out over four endpoints that can each be down on their own. **It composes; it does not re-read.** Every sub-object is the value an existing module's service returned, served in that module's own schema: `scan` is `TokenScan` from `GET /scan/token/{chain}/{token}`, `receipts` is `TrustReceipts` from `GET /trust/receipts/{chain}/{token}`, and `site`/`siteEntries` are `PublicShowcaseEntry` objects from `GET /showcase`. A client that already renders those surfaces renders this with no new types. ## Every section says whether it was read `data.receipts` is either the document or `{ code, message }`, and `data.siteEntries` is either the matching entries or `null`. **A section is never silently omitted**, because a client cannot tell "there is nothing here" from "we did not ask" — and on a page about whether to trust a token, that difference is the whole product. A partially-readable entity is a normal 200, not an error: one upstream being down costs the caller one tab, not the page. `not_a_token` and `read_failed` are different sentences and are not interchangeable: the first is a claim the upstream made **about the address**, the second is a claim about putty's plumbing or an upstream's uptime. A read that failed is never rendered as a zero, an empty answer, or a field that is simply gone. **`siteEntries: []` is a real answer, and it is not `null`.** An empty array means this creator published nothing here. It never means "no site exists" — no public read in this codebase is entitled to make that claim, because a project's own build origin belongs to its owner. `site` is the first entry of `siteEntries`; both are served so a client does not have to know which read produced the match. ## No market figure, and that is deliberate The domain this implements says "scan + verify + receipts + market". **The market figure is left out, and there is no `market` key in the response.** The only market source this codebase has is the Dexscreener tool behind `/mcp`, and `/mcp` is behind `requireAuthOrApiKey` — so a price here would mean a bearer in an unauthenticated server fetch, i.e. a session minted on every anonymous page load. The token page reaches the same conclusion and fetches price in the browser, after the reader asks for it (`apps/website/app/token/[chain]/[address]/page.tsx` says why). A field that is not there is a field a client can see is not offered; a `null` would be a field it has to learn to distrust. An entity read without a price is the honest version of this domain. ## Cache-Control is short, and deliberately not `immutable` The response carries `Cache-Control: public, max-age=30`. A token's scan changes when its owner can mint, its receipts change when the creator ships a new module, and its published site changes the moment a creator opts in — this is a read of live onchain and opt-in state, not a static asset. Thirty seconds is short enough that a browser does not pin a stale security verdict across a session, and long enough that a burst of page views does not re-run three chain reads each. **`immutable` is wrong here and would be a bug.** `GET /public/{key}` in the Storage module uses `public, max-age=31536000, immutable` and is right to: it serves bytes at a content-addressed key. This route does not, and copying that header would pin a stale price, a stale mint verdict, and a stale holder window in every browser that ever saw the response. Do not "optimise" this into `immutable`. ## Amounts are decimal strings end to end `scan.totalSupplyRaw`, every `balanceRaw`, and every percentage in `receipts` are strings. A 1e9 supply at 18 decimals is thirty digits and a double cannot hold it. Do not parse them into a number to display a total. **`clearedForMainnet` is always `false`**, in `scan` and in `verify`. It is declared as the literal `false` so the document states what the code enforces: no putty report, of any label, is an external audit. A `verified custom` label means every check ran and passed on the evidence putty had — read `verify.explanation` for the sentence that must be shown beside it. ## Statuses - **200** — the entity. Sections that could not be read are `null` with a reason. - **400** — the chain is not in the registry, or the address is not 20 bytes. Both messages name what is supported. - **404** — there is no contract at that address, so there is no token. Byte-for-byte the same shape `GET /scan/token/{chain}/{token}` answers with, and a real 404 rather than an entity full of nulls: a page of refusals for something that is not a token reads like a broken token. - **429** — the per-IP limit. This route is an unauthenticated aggregator that fans out to several upstreams, so it is limited exactly as the other public reads are. - **500** — the scan itself could not run, so the module cannot tell "not a token" from "we could not look". A 500 is reserved for that; everything else is a 200 with an honest section.