API
Every route under /api with its method, body, response and status codes. The game, the site and the share cards use nothing else.
$ curl -s -X POST https://scribblehood.vercel.app/api/summon -H 'content-type: application/json' -d '{"prompt":"golden gpu"}' { "summon": { "id": "3f1c...", "name": "GOLDEN GPU", "basket": [ { "ticker": "NVDA", "weight": 0.36, "why": "GPU design" }, ... ], "physics": { "glow": "gold", ... }, "sprite": { "base": "box", ... }, "quotes": [ ... ], "basketChangePct": 1.2, "notional": 101.2 }, "summonsLeft": 14 }
Conventions
- Every route speaks JSON. Errors are
{ "error": "a sentence" }with the status in the HTTP code. - Unexpected failures answer 500 with
{ "error": "Something broke on our side. Try again in a moment." }and no stack trace. - Rate limits are a fixed one-minute window per IP; over the limit is 429 with a sentence.
- Routes that touch the quota or the session set cookies:
sh_vid(a one-year visitor id) andsh_session(seven days, after sign in). Both are HttpOnly and SameSite=Lax. - Types for every request and response live in
src/lib/types.ts. The shapes below use those names.
Summons
POST/api/summon
| body | { prompt: string } |
| response | { summon: LiveSummon, summonsLeft: number | null }. summonsLeft is the free summons left after this one; null for holders. |
| statuses | 200. 400 empty prompt or over 80 characters. 429 rate limit (30 a minute per IP). 429 quota used up, with body { error, code: "quota", summonsLeft: 0, threshold, tokenLive }. |
| notes | Resolves through the cache, the model, then the dictionary, and attaches live quotes. Spends one summon. |
GET/api/summon?id=
| query | id |
| response | { summon: LiveSummon } |
| statuses | 200. 400 missing id. 404 unknown id. |
| notes | No quota, no cookie. Used by the share page and remakes. |
POST/api/craft
| body | { ids: string[], prompts: string[] } |
| response | Same as summon: { summon: LiveSummon, summonsLeft }. |
| statuses | 200. 400 fewer than two prompts, more than four, or any over 80 characters. 429 rate limit (20 a minute). 429 quota, same body as summon. |
| notes | The component baskets are handed to the model so it recomposes rather than invents; offline, the baskets are averaged. Spends one summon. |
Market
GET/api/prices?tickers=
| query | tickers, comma separated, at most 40 |
| response | { quotes: Quote[] } |
| statuses | 200 (cached 30 seconds). 400 no tickers, too many, or an unknown one, naming it. |
GET/api/weather
| response | { weather: Weather, quotes: Quote[] }: kind, headline, detail, movers, asOf, plus the 26 weather quotes. |
| statuses | 200 (server cache 60 seconds, browser cache 30). |
GET/api/universe
| response | { assets: Asset[] }: ticker, name, sector, emoji, Stock Token address when known, reference price. |
| statuses | 200 (cached an hour). |
GET/api/logo/{ticker}
| response | An image: the company mark as PNG when one exists, otherwise an SVG lettermark. The x-logo-source header says which. |
| statuses | 200 (a day in the browser, a week at the edge). 404 unknown ticker. |
Daily and world
GET/api/daily?date=
| query | date as YYYY-MM-DD, optional, defaults to today UTC |
| response | { daily: { date, challenges: Challenge[3] } } |
| statuses | 200 (cached five minutes). 400 malformed date. |
GET/api/world
| response | { entries: WorldEntry[] }, the newest 60, each with its remake count. |
| statuses | 200, never cached. |
POST/api/world
| body | { summon: Summon, author?: string } to post, or { remakeOf: string } to count a remake. |
| response | { entry: WorldEntry } with 201 for a post; { id, remakes } for a remake. |
| statuses | 201 posted. 200 remake counted. 400 not JSON, no summon id, a summon the server never resolved, or a bad remake id. 429 rate limit (20 a minute). |
| notes | Only summons the server itself resolved may enter the world; the client copy is not trusted. Author names are cut to 16 characters. A remake of a holder's post awards them 2 Ink. |
Share cards and NFTs
GET/api/og?id=
| query | id, optional |
| response | A 1200 by 630 PNG: paper background, the sprite, the name, the top five basket lines with weights and logos, and the top ticker as a sticker. An unknown or missing id renders the generic Scribblehood card. |
| statuses | 200. |
GET/api/nft/{id}
| response | ERC-721 metadata for a ScribbleObjects token: name, description, image (the OG card), external_url (the share page) and attributes for kind, glow, prompt, every basket line, the primary Stock Token and the mint date. |
| statuses | 200 (five minutes in the browser, an hour at the edge). 400 non-numeric id. 404 ScribbleObjects not configured, no such token, or the summon is gone. 502 the chain read failed. |
Auth and me
GET/api/me
| response | { me: Me }: address when signed in, tier, balance, threshold, tokenAddress, tokenLive, summonsLeft (null for holders), freePerDay, ink, claimedToday. |
| statuses | 200. Sets sh_vid when missing. |
GET/api/auth/nonce
| response | { nonce, message } |
| statuses | 200. 429 over 30 a minute. |
| notes | Nonces live ten minutes and burn on use. |
POST/api/auth/verify
| body | { address, signature, nonce } |
| response | { me: Me }, with the tier read fresh from chain, and the session cookie. |
| statuses | 200. 400 not JSON. 401 bad address, bad signature, stale nonce, or a signature that does not match. 429 over 10 a minute. |
POST/api/auth/logout
| response | { ok: true } |
| statuses | 200, and the session cookie is cleared. |
Ink and rewards
POST/api/ink/claim
| body | { challengeId: string } |
| response | { ink: number, awarded: number } |
| statuses | 200. 400 no challenge id, or not one of today's. 401 not signed in. 403 not a holder. 409 already claimed today. 429 over 20 a minute. |
GET/api/leaderboard
| response | { entries: LeaderboardEntry[], you?: LeaderboardEntry }: the top 20 as rank, address, ink; you is filled when signed in and on the board at all. |
| statuses | 200, never cached. |
GET/api/rewards/proof?address=
| query | address |
| response | { round, root, decimals, amount, proof, total, publishedAt } |
| statuses | 200. 400 not an address. 404 no round published yet, or, with { error, round }, this wallet is not in the latest round. |