Search the catalog
Returns the catalog items the caller may see: every public item (`official` or `community`), plus the caller's own `private` items. Filters — `kind`, `templateId`, `tag`, `q` — AND together. Results are ordered by install count, then by recency. There is no `status` filter, because visibility is not something a caller chooses: a private item is invisible to everyone but its owner whatever they ask for.
Returns the catalog items the caller may see: every public item (official or community), plus the caller's own private items. Filters — kind, templateId, tag, q — AND together. Results are ordered by install count, then by recency. There is no status filter, because visibility is not something a caller chooses: a private item is invisible to everyone but its owner whatever they ask for.
Query Parameters
Restrict to one kind of item.
Value in
- "template"
- "block"
- "contract"
- "hook"
- "skill"
Restrict to items whose manifest declares this template in compatibleTemplates. An item that declares nothing is not incompatible — only undeclared — so it still matches.
1 <= lengthRestrict to items carrying this tag, case-insensitively.
1 <= lengthFree-text match over the title, the description, and the tags. Not a full-text index; a substring match over those three columns.
1 <= lengthCap on returned items, 1 to 50. Defaults to 20.
1 <= value <= 50Response Body
application/json
application/json
application/json
application/json
curl -X GET "https://example.com/catalog/items"{ "items": [ { "id": "string", "kind": "template", "status": "official", "title": "string", "description": "string", "tags": [ "string" ], "installs": 0, "ownerId": "string", "version": "string", "usage": "string", "compatibleTemplates": [ "string" ], "dependencies": [ "string" ], "registryDependencies": [ "string" ], "files": [ { "path": "string", "content": "string" } ], "reviewNote": "string", "createdAt": "string", "updatedAt": "string" } ]}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.
Save a private catalog item POST
Saves a file set as a private catalog item owned by the caller — the "My snippets" path. The item is immediately usable in any of the caller's own projects with no review, and invisible to every other user: it is not merely hidden from the list, it is not readable, installable, or deletable by anyone else. The status is always `private`; publishing is a separate review flow and this route deliberately does not offer it.