{"openapi":"3.1.0","info":{"title":"Bonker Public API","version":"1.0.0","description":"Machine-readable contract for bonker.wtf public JSON API routes."},"servers":[{"url":"https://bonker.wtf","description":"Production"},{"url":"http://localhost:3456","description":"Local Express server"}],"externalDocs":{"description":"Human docs","url":"https://bonker.wtf/docs/api"},"tags":[{"name":"Spec"},{"name":"Tokens"},{"name":"Presales"},{"name":"Discovery"},{"name":"Genesis"},{"name":"Creator"},{"name":"System"}],"paths":{"/api/spec":{"get":{"tags":["Spec"],"summary":"OpenAPI specification","description":"Returns this OpenAPI document. Uses the global API rate limit.","responses":{"200":{"description":"OpenAPI document","content":{"application/json":{"schema":{"type":"object"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/tokens":{"get":{"tags":["Tokens"],"summary":"List indexed token launches","parameters":[{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"default":1}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"search","in":"query","schema":{"type":"string","maxLength":100}},{"name":"sort","in":"query","schema":{"type":"string","enum":["newest","oldest","mcap","volume","trending"],"default":"newest"}},{"name":"creator","in":"query","schema":{"$ref":"#/components/schemas/EvmAddress"}}],"responses":{"200":{"description":"Paginated token list","content":{"application/json":{"schema":{"type":"object","required":["tokens","total","page","pages"],"properties":{"tokens":{"type":"array","items":{"type":"object","additionalProperties":true,"properties":{"address":{"$ref":"#/components/schemas/EvmAddress"},"name":{"type":"string"},"symbol":{"type":"string"},"image":{"type":["string","null"]},"description":{"type":["string","null"]},"deployer":{"allOf":[{"$ref":"#/components/schemas/EvmAddress"}],"description":"The wallet that sent the deployToken transaction. For a launch made through the Farcaster/Telegram/MoltBook bots this is Bonker's single shared deployer wallet, not the person who asked for it — use `creator` to attribute a token to a human."},"creator":{"allOf":[{"$ref":"#/components/schemas/EvmAddress"}],"description":"The wallet that asked for the launch. Falls back to `deployer` for rows predating creator attribution. This is the address to pass to /api/creator/{address}/unclaimed."},"mcap":{"type":["number","null"]},"volume24h":{"type":["number","null"]},"priceChange24h":{"type":["number","null"]},"createdAt":{"type":["string","null"]}}}},"total":{"type":"integer"},"page":{"type":"integer"},"pages":{"type":"integer"}}}}}},"400":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/tokens/{address}":{"get":{"tags":["Tokens"],"summary":"Read one indexed token","description":"RPC-heavy route with on-chain feature enrichment. Limit: 30 requests per minute per IP, after the global /api limit.","parameters":[{"name":"address","in":"path","required":true,"schema":{"$ref":"#/components/schemas/EvmAddress"}}],"responses":{"200":{"description":"Token detail","headers":{"RateLimit-Limit":{"description":"Per-minute limit for this detail route.","schema":{"type":"integer","example":30}}},"content":{"application/json":{"schema":{"type":"object","required":["token"],"properties":{"token":{"allOf":[{"type":"object","additionalProperties":true,"properties":{"address":{"$ref":"#/components/schemas/EvmAddress"},"name":{"type":"string"},"symbol":{"type":"string"},"image":{"type":["string","null"]},"description":{"type":["string","null"]},"deployer":{"allOf":[{"$ref":"#/components/schemas/EvmAddress"}],"description":"The wallet that sent the deployToken transaction. For a launch made through the Farcaster/Telegram/MoltBook bots this is Bonker's single shared deployer wallet, not the person who asked for it — use `creator` to attribute a token to a human."},"creator":{"allOf":[{"$ref":"#/components/schemas/EvmAddress"}],"description":"The wallet that asked for the launch. Falls back to `deployer` for rows predating creator attribution. This is the address to pass to /api/creator/{address}/unclaimed."},"mcap":{"type":["number","null"]},"volume24h":{"type":["number","null"]},"priceChange24h":{"type":["number","null"]},"createdAt":{"type":["string","null"]}}},{"type":"object","properties":{"features":{"type":["object","null"],"additionalProperties":true}}}]}}}}}},"400":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"put":{"tags":["Tokens"],"summary":"Update token display metadata with a wallet signature","parameters":[{"name":"address","in":"path","required":true,"schema":{"$ref":"#/components/schemas/EvmAddress"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["timestamp","signature"],"properties":{"description":{"type":"string","maxLength":500},"image":{"type":"string","maxLength":500},"website":{"type":"string","maxLength":300},"twitter":{"type":"string","maxLength":200},"telegram":{"type":"string","maxLength":200},"farcaster":{"type":"string","maxLength":200},"timestamp":{"type":["integer","string"]},"signature":{"type":"string","pattern":"^0x[a-fA-F0-9]+$"}}}}}},"responses":{"200":{"description":"Updated token detail","content":{"application/json":{"schema":{"type":"object","properties":{"token":{"type":"object","additionalProperties":true,"properties":{"address":{"$ref":"#/components/schemas/EvmAddress"},"name":{"type":"string"},"symbol":{"type":"string"},"image":{"type":["string","null"]},"description":{"type":["string","null"]},"deployer":{"allOf":[{"$ref":"#/components/schemas/EvmAddress"}],"description":"The wallet that sent the deployToken transaction. For a launch made through the Farcaster/Telegram/MoltBook bots this is Bonker's single shared deployer wallet, not the person who asked for it — use `creator` to attribute a token to a human."},"creator":{"allOf":[{"$ref":"#/components/schemas/EvmAddress"}],"description":"The wallet that asked for the launch. Falls back to `deployer` for rows predating creator attribution. This is the address to pass to /api/creator/{address}/unclaimed."},"mcap":{"type":["number","null"]},"volume24h":{"type":["number","null"]},"priceChange24h":{"type":["number","null"]},"createdAt":{"type":["string","null"]}}}}}}}},"400":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/tokens/{address}/holders":{"get":{"tags":["Tokens"],"summary":"Read cached top holders for a token","parameters":[{"name":"address","in":"path","required":true,"schema":{"$ref":"#/components/schemas/EvmAddress"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":50,"default":20}}],"responses":{"200":{"description":"Holder list or graceful unavailable response","content":{"application/json":{"schema":{"type":"object","required":["holders","unavailable"],"properties":{"holders":{"type":"array","items":{"type":"object","properties":{"address":{"$ref":"#/components/schemas/EvmAddress"},"balance":{"type":"string"},"percent":{"type":"number"},"label":{"type":["string","null"]},"isContract":{"type":"boolean"},"creator":{"type":["object","null"],"description":"Public Bonker launch context; never contains login or linked-social data.","properties":{"tokenCount":{"type":"integer","minimum":1}}}}}},"totalHolders":{"type":"integer","minimum":1,"description":"Exact non-zero holder count when source=index."},"source":{"type":"string","enum":["index","blockscout"]},"unavailable":{"type":"boolean"}}}}}},"400":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/tokens/{address}/trades":{"get":{"tags":["Tokens"],"summary":"Read recent indexed swaps for a token","parameters":[{"name":"address","in":"path","required":true,"schema":{"$ref":"#/components/schemas/EvmAddress"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}},{"name":"before","in":"query","schema":{"type":"string","pattern":"^\\d+:\\d+$"}}],"responses":{"200":{"description":"Trade page","content":{"application/json":{"schema":{"type":"object","required":["trades","nextBefore"],"properties":{"trades":{"type":"array","items":{"type":"object","additionalProperties":true,"properties":{"maker":{"type":"string","description":"PoolManager Swap.sender; usually an execution contract, not the human trader."},"trader":{"type":["string","null"],"description":"Transaction signer resolved from tx.from; the best available wallet-level trade identity."},"creator":{"type":["object","null"],"description":"Public Bonker launch context for the resolved trader; never login or linked-social data.","properties":{"tokenCount":{"type":"integer","minimum":1}}}}}},"nextBefore":{"type":["string","null"]}}}}}},"400":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/tokens/{address}/trade-stats":{"get":{"tags":["Tokens"],"summary":"Buy/sell breakdown for a token across 4h/1d/7d windows","description":"Computed from Bonker's own token_trades index, not DexScreener. USD volume is token_amount * price_usd; a trade indexed before price enrichment caught up counts toward buyCount/sellCount/buyers/sellers but contributes 0 to volume.","parameters":[{"name":"address","in":"path","required":true,"schema":{"$ref":"#/components/schemas/EvmAddress"}}],"responses":{"200":{"description":"Buy/sell stats for the h4, d1, and d7 windows","content":{"application/json":{"schema":{"type":"object","required":["h4","d1","d7"],"properties":{"h4":{"type":"object","required":["buyCount","sellCount","buyVolumeUsd","sellVolumeUsd","buyers","sellers"],"properties":{"buyCount":{"type":"integer","minimum":0},"sellCount":{"type":"integer","minimum":0},"buyVolumeUsd":{"type":"number","minimum":0},"sellVolumeUsd":{"type":"number","minimum":0},"buyers":{"type":"integer","minimum":0},"sellers":{"type":"integer","minimum":0}}},"d1":{"type":"object","required":["buyCount","sellCount","buyVolumeUsd","sellVolumeUsd","buyers","sellers"],"properties":{"buyCount":{"type":"integer","minimum":0},"sellCount":{"type":"integer","minimum":0},"buyVolumeUsd":{"type":"number","minimum":0},"sellVolumeUsd":{"type":"number","minimum":0},"buyers":{"type":"integer","minimum":0},"sellers":{"type":"integer","minimum":0}}},"d7":{"type":"object","required":["buyCount","sellCount","buyVolumeUsd","sellVolumeUsd","buyers","sellers"],"properties":{"buyCount":{"type":"integer","minimum":0},"sellCount":{"type":"integer","minimum":0},"buyVolumeUsd":{"type":"number","minimum":0},"sellVolumeUsd":{"type":"number","minimum":0},"buyers":{"type":"integer","minimum":0},"sellers":{"type":"integer","minimum":0}}}}}}}},"400":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/tokens/{address}/candles":{"get":{"tags":["Tokens"],"summary":"OHLCV candles for a token, aggregated from indexed swaps","description":"Folded out of Bonker's own token_trades index, not a market-data vendor. Two quotes, never mixed in one series: `usd` is price_usd (USD per whole token, volume = token_amount * price_usd, the same convention as /trade-stats) and needs a WETH pair; `native` is the paired token per whole token taken straight off the two swap legs (volume = the paired-token leg), needs no oracle, and exists for EVERY token that has traded. A `usd` request against a non-WETH pair returns an empty series rather than silently substituting the native one. Empty buckets between two trades are forward-filled flat at the previous close with trades: 0; nothing is extrapolated past the newest trade, and a leading empty bucket is not back-filled. The window is the newest `limit` buckets ending at the most recent quoted trade, not at now.","parameters":[{"name":"address","in":"path","required":true,"schema":{"$ref":"#/components/schemas/EvmAddress"}},{"name":"interval","in":"query","schema":{"type":"string","enum":["1m","5m","15m","1h","4h","1d"],"default":"15m"},"description":"Bucket width. A value outside this set is a 400, never a silent fallback to the default."},{"name":"quote","in":"query","schema":{"type":"string","enum":["usd","native"]},"description":"What the price is measured in. Defaults to usd for a WETH pair and native for anything else. A value outside this set is a 400."},{"name":"limit","in":"query","schema":{"type":"integer","minimum":2,"maximum":1000,"default":240}}],"responses":{"200":{"description":"Candle series, oldest first","content":{"application/json":{"schema":{"type":"object","required":["interval","intervalSeconds","quote","quoteSymbol","nativeSymbol","quotesAvailable","candles"],"properties":{"interval":{"type":"string","enum":["1m","5m","15m","1h","4h","1d"]},"intervalSeconds":{"type":"integer","minimum":60},"quote":{"type":"string","enum":["usd","native"]},"quoteSymbol":{"type":"string","description":"Unit of THIS series: \"USD\", or the paired token's symbol."},"nativeSymbol":{"type":"string","description":"The paired token's symbol, reported on every response (including a usd one) so a client can label its quote toggle without loading the other series first."},"quotesAvailable":{"type":"array","items":{"type":"string","enum":["usd","native"]},"description":"Which quotes this token can actually be served in. A pool not paired with WETH is [\"native\"] alone."},"candles":{"type":"array","items":{"type":"object","required":["time","open","high","low","close","volume","buyVolume","sellVolume","trades"],"properties":{"time":{"type":"integer","description":"Bucket start, unix seconds."},"open":{"type":"number"},"high":{"type":"number"},"low":{"type":"number"},"close":{"type":"number"},"volume":{"type":"number","minimum":0,"description":"Denominated in the requested quote — dollars for usd, paired tokens for native."},"buyVolume":{"type":"number","minimum":0},"sellVolume":{"type":"number","minimum":0},"trades":{"type":"integer","minimum":0,"description":"Quoted swaps in this bucket. 0 marks a forward-filled hole, distinguishing it from a real doji."}}}}}}}}},"400":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/tokens/{address}/comments":{"get":{"tags":["Tokens"],"summary":"List comments for a token","parameters":[{"name":"address","in":"path","required":true,"schema":{"$ref":"#/components/schemas/EvmAddress"}},{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"default":1}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":50,"default":20}}],"responses":{"200":{"description":"Comment page","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Tokens"],"summary":"Create a wallet-signed token comment or reply","parameters":[{"name":"address","in":"path","required":true,"schema":{"$ref":"#/components/schemas/EvmAddress"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["author","body","timestamp","nonce","signature"],"properties":{"author":{"$ref":"#/components/schemas/EvmAddress"},"body":{"type":"string","maxLength":500},"parentId":{"type":["integer","null"]},"timestamp":{"type":["integer","string"]},"nonce":{"type":"string","pattern":"^0x[a-fA-F0-9]{32,128}$"},"isCall":{"type":"boolean","default":false,"description":"Marks a top-level comment as a pump.fun-style callout. Rejected with 400 when parentId is set."},"signature":{"type":"string","pattern":"^0x[a-fA-F0-9]+$"}}}}}},"responses":{"201":{"description":"Created comment","content":{"application/json":{"schema":{"type":"object","properties":{"comment":{"type":"object","additionalProperties":true}}}}}},"400":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/tokens/{address}/refresh":{"post":{"tags":["Tokens"],"summary":"Invalidate one token detail enrichment cache entry","parameters":[{"name":"address","in":"path","required":true,"schema":{"$ref":"#/components/schemas/EvmAddress"}}],"responses":{"204":{"description":"Cache invalidated"},"400":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/tokens/{address}/sync-metadata":{"post":{"tags":["Tokens"],"summary":"Mirror the token’s on-chain image/metadata into the DB","description":"Reads the token’s on-chain image and metadata pointers and copies them into the indexed row the site renders from. Permissionless: it can only ever set the DB to match on-chain truth.","parameters":[{"name":"address","in":"path","required":true,"schema":{"$ref":"#/components/schemas/EvmAddress"}}],"responses":{"200":{"description":"Refreshed token","content":{"application/json":{"schema":{"type":"object","required":["token"],"properties":{"token":{"type":"object","additionalProperties":true,"properties":{"address":{"$ref":"#/components/schemas/EvmAddress"},"name":{"type":"string"},"symbol":{"type":"string"},"image":{"type":["string","null"]},"description":{"type":["string","null"]},"deployer":{"allOf":[{"$ref":"#/components/schemas/EvmAddress"}],"description":"The wallet that sent the deployToken transaction. For a launch made through the Farcaster/Telegram/MoltBook bots this is Bonker's single shared deployer wallet, not the person who asked for it — use `creator` to attribute a token to a human."},"creator":{"allOf":[{"$ref":"#/components/schemas/EvmAddress"}],"description":"The wallet that asked for the launch. Falls back to `deployer` for rows predating creator attribution. This is the address to pass to /api/creator/{address}/unclaimed."},"mcap":{"type":["number","null"]},"volume24h":{"type":["number","null"]},"priceChange24h":{"type":["number","null"]},"createdAt":{"type":["string","null"]}}}}}}}},"400":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"422":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/presales":{"get":{"tags":["Presales"],"summary":"List presales","parameters":[{"name":"status","in":"query","schema":{"type":"string","enum":["NotCreated","Active","SuccessfulMinimumHit","SuccessfulMaximumHit","Failed","Claimable"]}}],"responses":{"200":{"description":"Presale list","content":{"application/json":{"schema":{"type":"object","required":["presales","contract"],"properties":{"presales":{"type":"array","items":{"type":"object","additionalProperties":true,"properties":{"id":{"type":"integer"},"status":{"type":"string"},"tokenName":{"type":"string"},"tokenSymbol":{"type":"string"},"tokenImage":{"type":["string","null"]},"minEthGoal":{"type":["string","null"]},"maxEthGoal":{"type":["string","null"]},"ethRaised":{"type":["string","null"]}}}},"contract":{"type":["string","null"]}}}}}},"400":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/presales/{id}":{"get":{"tags":["Presales"],"summary":"Read one presale","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1}}],"responses":{"200":{"description":"Presale detail","content":{"application/json":{"schema":{"type":"object","properties":{"presale":{"type":"object","additionalProperties":true,"properties":{"id":{"type":"integer"},"status":{"type":"string"},"tokenName":{"type":"string"},"tokenSymbol":{"type":"string"},"tokenImage":{"type":["string","null"]},"minEthGoal":{"type":["string","null"]},"maxEthGoal":{"type":["string","null"]},"ethRaised":{"type":["string","null"]}}},"contract":{"type":["string","null"]}}}}}},"400":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/random-token":{"get":{"tags":["Discovery"],"summary":"Get a launch-form token suggestion","responses":{"200":{"description":"Random token template","content":{"application/json":{"schema":{"type":"object","required":["name","symbol","description"],"properties":{"name":{"type":"string"},"symbol":{"type":"string"},"description":{"type":"string"}}}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/events":{"post":{"tags":["Discovery"],"summary":"Record a first-party product funnel event","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["event"],"properties":{"event":{"type":"string","enum":["route_viewed","login_started","login_succeeded","login_failed","genesis_viewed","genesis_join_started","genesis_join_succeeded","genesis_join_failed","wallet_connect_started","launch_form_viewed","wallet_connected","launch_submit_started","tx_submitted","tx_confirmed","tx_failed","token_viewed","outbound_clicked","genesis_status_not_found"]},"eventId":{"type":"string","maxLength":128},"occurredAt":{"type":"integer","description":"Unix seconds or milliseconds."},"sessionId":{"type":"string","maxLength":128},"anonymousId":{"type":"string","maxLength":128},"userId":{"type":"string","maxLength":128},"address":{"$ref":"#/components/schemas/EvmAddress"},"wallet":{"$ref":"#/components/schemas/EvmAddress"},"xHandle":{"type":"string","maxLength":64},"telegramHandle":{"type":"string","maxLength":64},"farcasterHandle":{"type":"string","maxLength":64},"authProvider":{"type":"string","maxLength":32},"source":{"type":"string","maxLength":128},"medium":{"type":"string","maxLength":128},"campaign":{"type":"string","maxLength":256},"referrerUrl":{"type":"string","maxLength":2048},"landingPath":{"type":"string","maxLength":2048},"pagePath":{"type":"string","maxLength":2048},"failureReason":{"type":"string","maxLength":256},"properties":{"type":"object","additionalProperties":true}}}}}},"responses":{"200":{"description":"Duplicate event already recorded"},"201":{"description":"Recorded event","content":{"application/json":{"schema":{"type":"object","properties":{"ok":{"type":"boolean"},"duplicate":{"type":"boolean"}}}}}},"400":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/upload-image":{"post":{"tags":["Discovery"],"summary":"Store a launch-form image and return its public URL","description":"Requires a logged-in session (cookie) — since 2026-08-21 this is no longer an anonymous write. The stored file is hosted permanently and content-addressed under bonker.wtf/i/, so every upload needs an owner behind it; a session from POST /api/auth/wallet is enough (no linked social identity required, unlike POST /api/generate-image). Agents with an MCP key use the upload_image tool instead, which keys its own gate. Route limit: 30 uploads per minute per IP, counted for anonymous 401s too.","requestBody":{"required":true,"content":{"multipart/form-data":{"schema":{"type":"object","required":["image"],"properties":{"image":{"type":"string","format":"binary","description":"JPEG, PNG, GIF, or WEBP, max 128KB."}}}}}},"responses":{"200":{"description":"Stored image URL","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImageResult"}}}},"400":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"No session — log in first. Body carries reason \"login_required\".","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/generate-image":{"get":{"tags":["Discovery"],"summary":"Image-generation availability and this wallet's allowance","description":"`{ enabled, cap }` for a logged-out caller; with a wallet session also `used`, `remaining`, `secondsRemaining`, `nextAvailableAt` and `queueDepth` (renders queued or rendering right now — each takes ~100 seconds and they run one at a time). `cap` is this wallet's own: 2 per rolling 24h for a wallet with no history, growing with its launches and 24h trading volume up to 10.","responses":{"200":{"description":"Availability + allowance","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImageGenerationConfig"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Discovery"],"summary":"Queue an AI token image","description":"Enqueues a render and answers 202 immediately; poll GET /api/generate-image/jobs/{jobId} until `status` is `done` for the stored URL. Renders run one at a time (~100 seconds each), so `position` is the number ahead of you; depending on server configuration the prompt is drawn either by a third-party image provider or by a self-hosted model. Requires a session with a linked social identity (401 anonymous, 403 wallet-only). Per-wallet rolling-24h cap answered with 429 `reason: daily_cap` plus the wait. Route limit: 1 request per 10 seconds per IP.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","symbol"],"properties":{"name":{"type":"string","maxLength":50},"symbol":{"type":"string","maxLength":10},"description":{"type":"string","maxLength":500}}}}}},"responses":{"202":{"description":"Queued","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImageJobQueued"}}}},"400":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/generate-image/jobs/{jobId}":{"get":{"tags":["Discovery"],"summary":"Poll a queued AI token image","description":"The job's current state. Owner-only: a job that belongs to another wallet answers 404, never 403 — ids are random UUIDs so one wallet cannot enumerate another's renders. `position` is renders ahead while queued/running, null otherwise; `url` is set once `done`; `error` is a short code (`render_failed`, `render_timeout`, `worker_timeout`, `expired`) once `failed`. A job left queued for an hour expires. Route limit: 60 per minute per IP.","parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Job state","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImageJob"}}}},"401":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"delete":{"tags":["Discovery"],"summary":"Cancel a queued AI token image","description":"Cancels the caller's own job while it is still `queued`. 409 once it is rendering or finished — the CPU is spent either way.","parameters":[{"name":"jobId","in":"path","required":true,"schema":{"type":"string","format":"uuid"}}],"responses":{"200":{"description":"Cancelled","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ImageJob"}}}},"401":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/stats":{"get":{"tags":["Discovery"],"summary":"Read aggregate token stats","responses":{"200":{"description":"Aggregate stats","content":{"application/json":{"schema":{"type":"object","properties":{"tokensLaunched":{"type":"integer"},"totalMcap":{"type":"number"},"volume24h":{"type":"number"},"totalTransfers":{"type":"integer"}}}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/leaderboard":{"get":{"tags":["Discovery"],"summary":"Read creator leaderboard","parameters":[{"name":"sort","in":"query","schema":{"type":"string","enum":["volume","tokens"],"default":"volume"}}],"responses":{"200":{"description":"Creator leaderboard","content":{"application/json":{"schema":{"type":"object","properties":{"creators":{"type":"array","items":{"type":"object","additionalProperties":true}},"sort":{"type":"string"}}}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/monitoring/envelope":{"post":{"tags":["Ops"],"summary":"Tunnel a browser error envelope to the configured ingest","description":"Same-origin tunnel: the browser posts a Sentry envelope here and the server forwards it, so the ingest only ever sees this host and the browser never makes a cross-origin request. Always answers 204 regardless of what the ingest says -- the reporter uses navigator.sendBeacon and cannot read a response, and a monitoring failure must never surface as a page error. Inert when no DSN is configured.","requestBody":{"required":true,"content":{"application/x-sentry-envelope":{"schema":{"type":"string","description":"Newline-delimited Sentry envelope, max 200KB"}}}},"responses":{"204":{"description":"Accepted. Says nothing about whether the ingest stored it."},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/profile/{address}":{"get":{"tags":["Discovery"],"summary":"Read a public creator profile","description":"One wallet's public footprint on Bonker: launch count, aggregate market cap and 24h volume, both leaderboard ranks, referral totals, and the first page of its launches. Contains no linked social identities -- that mapping is operator-only.","parameters":[{"name":"address","in":"path","required":true,"schema":{"type":"string","pattern":"^0x[a-fA-F0-9]{40}$"}},{"name":"page","in":"query","schema":{"type":"integer","minimum":1,"default":1}}],"responses":{"200":{"description":"Creator profile aggregate plus a page of that creator's tokens","content":{"application/json":{"schema":{"type":"object","properties":{"profile":{"type":"object","properties":{"address":{"type":"string"},"tokenCount":{"type":"integer"},"totalMcap":{"type":"number"},"totalVolume24h":{"type":"number"},"totalTransfers":{"type":"integer"},"totalHolders":{"type":"integer"},"volumeRank":{"type":"integer","nullable":true},"tokenRank":{"type":"integer","nullable":true},"totalCreators":{"type":"integer"},"referredCount":{"type":"integer"},"referredVolume24h":{"type":"number"},"tradingVolume24h":{"type":"number"},"tradeCount24h":{"type":"integer"},"tradingVolumeAllTime":{"type":"number"},"tradeCountAllTime":{"type":"integer"},"tradedTokenCount":{"type":"integer"},"buyCount":{"type":"integer"},"sellCount":{"type":"integer"},"pricedTradeCount":{"type":"integer"},"firstTradeAt":{"type":"integer","nullable":true},"lastTradeAt":{"type":"integer","nullable":true},"firstSeenVia":{"type":"string","nullable":true},"firstSeenAt":{"type":"integer","nullable":true},"claimedAt":{"type":"integer","nullable":true}}},"tokens":{"type":"array","items":{"type":"object","additionalProperties":true}},"total":{"type":"integer"},"page":{"type":"integer"},"pages":{"type":"integer"}}}}}},"400":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/admin/auth/logins":{"get":{"tags":["Admin"],"summary":"List recent successful logins","description":"Protected operator activity across wallet, X, Telegram, and Farcaster authentication, including social-only sessions and Genesis correlation.","security":[{"AdminApiKey":[]}],"parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}}],"responses":{"200":{"description":"Provider subjects ordered by most recent successful login."},"401":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/admin/auth/summary":{"get":{"tags":["Admin"],"summary":"Read authentication funnel and health metrics","description":"Protected 24-hour, 7-day, and 30-day auth outcomes, provider mix, wallet-link and Genesis qualification rates, daily rollups, failure reasons, and recent successful subjects.","security":[{"AdminApiKey":[]}],"responses":{"200":{"description":"Authentication analytics summary. Detailed events retain 90 days; daily rollups are permanent."},"401":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/admin/auth/export":{"get":{"tags":["Admin"],"summary":"Export detailed authentication outcomes as CSV","description":"Protected CSV export of up to 90 days of authentication attempts. No IP, user-agent, or device fingerprint is collected.","security":[{"AdminApiKey":[]}],"parameters":[{"name":"days","in":"query","schema":{"type":"integer","minimum":1,"maximum":90,"default":90}}],"responses":{"200":{"description":"CSV attachment, capped at 50,000 newest rows."},"401":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/admin/genesis/users":{"get":{"tags":["Admin"],"summary":"Look up Genesis users by linked identity","description":"Protected exact-match operator lookup across Genesis wallets and their X, Farcaster, or Telegram identity.","security":[{"AdminApiKey":[]}],"parameters":[{"name":"by","in":"query","required":true,"schema":{"type":"string","enum":["wallet","x","farcaster","telegram"]}},{"name":"q","in":"query","required":true,"schema":{"type":"string","minLength":1,"maxLength":100}}],"responses":{"200":{"description":"Exact-match Genesis users; an empty users array means no linked Genesis row."},"400":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/admin/image-generations":{"get":{"tags":["Admin"],"summary":"Read per-wallet AI image-generation usage","description":"Protected view into the image_jobs render queue. Without a wallet, returns a leaderboard of every wallet that has queued a render, most finished renders first; with one, returns that wallet's own job history and totals.","security":[{"AdminApiKey":[]}],"parameters":[{"name":"wallet","in":"query","schema":{"type":"string","pattern":"^0x[a-fA-F0-9]{40}$"}},{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":200,"default":50}}],"responses":{"200":{"description":"Leaderboard (no wallet) or one wallet's history + totals (wallet given)."},"400":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/admin/api-keys":{"post":{"tags":["Admin"],"summary":"Generate an admin API key","description":"Requires an action-specific wallet signature from the live Factory owner or an address in its admins mapping. The plaintext key is returned once; only its SHA-256 digest is stored.","requestBody":{"required":true,"content":{"application/json":{"schema":{"allOf":[{"$ref":"#/components/schemas/AdminApiKeySignedRequest"},{"type":"object","required":["label"],"properties":{"label":{"type":"string","minLength":1,"maxLength":40}}}]}}}},"responses":{"201":{"description":"Key created and returned once"},"400":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/admin/api-keys/list":{"post":{"tags":["Admin"],"summary":"List admin API key metadata","description":"Requires an action-specific Factory owner/admin wallet signature. Plaintext keys are never returned.","requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminApiKeySignedRequest"}}}},"responses":{"200":{"description":"Key metadata including revoked state"},"400":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/admin/api-keys/{id}/revoke":{"post":{"tags":["Admin"],"summary":"Revoke an admin API key","description":"Requires an action-specific Factory owner/admin wallet signature.","parameters":[{"name":"id","in":"path","required":true,"schema":{"type":"integer","minimum":1}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/AdminApiKeySignedRequest"}}}},"responses":{"200":{"description":"Key revoked"},"400":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"404":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/genesis/stats":{"get":{"tags":["Genesis"],"summary":"Read genesis campaign counts","responses":{"200":{"description":"Genesis allowlist and qualification counts","content":{"application/json":{"schema":{"type":"object","required":["allowlistCount","qualifiedCount"],"properties":{"allowlistCount":{"type":"integer"},"qualifiedCount":{"type":"integer"}}}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/genesis/leaderboard":{"get":{"tags":["Genesis"],"summary":"Read genesis referral leaderboard","parameters":[{"name":"limit","in":"query","schema":{"type":"integer","minimum":1,"maximum":100,"default":20}}],"responses":{"200":{"description":"Top genesis referrers by qualified invites","content":{"application/json":{"schema":{"type":"object","required":["leaders"],"properties":{"leaders":{"type":"array","items":{"type":"object","required":["referrer","invites"],"properties":{"referrer":{"$ref":"#/components/schemas/EvmAddress"},"invites":{"type":"integer"}}}}}}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/genesis/referrals/{address}":{"get":{"tags":["Genesis"],"summary":"Read one wallet genesis referral stats","parameters":[{"name":"address","in":"path","required":true,"schema":{"$ref":"#/components/schemas/EvmAddress"}}],"responses":{"200":{"description":"Genesis referral stats","content":{"application/json":{"schema":{"type":"object","required":["address","invites","rank"],"properties":{"address":{"$ref":"#/components/schemas/EvmAddress"},"invites":{"type":"integer"},"rank":{"type":["integer","null"]}}}}}},"400":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/genesis/status/{address}":{"get":{"tags":["Genesis"],"summary":"Read one wallet's live genesis allowlist status","description":"Live allowlist state (joined? qualified? referral count? has it launched a token?), straight from genesis_allowlist — distinct from GET /api/genesis/claim/{address}, which only answers once the operator ships the frozen airdrop manifest at genesis. onList:false for a wallet that never joined is a normal 200, not a 404.","parameters":[{"name":"address","in":"path","required":true,"schema":{"$ref":"#/components/schemas/EvmAddress"}}],"responses":{"200":{"description":"Genesis allowlist status for the wallet","content":{"application/json":{"schema":{"type":"object","required":["onList"],"properties":{"onList":{"type":"boolean"},"address":{"$ref":"#/components/schemas/EvmAddress"},"qualified":{"type":["boolean","null"],"description":"null only for a grandfathered legacy row still pending the Blockscout poller; Farcaster/X joins qualify synchronously."},"source":{"type":"string","enum":["web","farcaster","x","telegram"]},"createdAt":{"type":"integer","description":"Unix seconds."},"qualifiedReferrals":{"type":"integer"},"launched":{"type":"boolean","description":"Whether this wallet has deployed at least one token through Bonker — a separate, unchanged live tokens.deployer match kept for backward compat; the allocation formula itself now reads actions.token_launched instead."},"points":{"type":"integer","description":"Flat sum of earned actions (0-7) plus capped qualifiedReferrals — the same weight server/genesis-allocation.js uses for the airdrop. See the 2026-07-29 genesis-action-points design doc."},"referralCap":{"type":"integer","description":"Per-wallet cap on qualifiedReferrals counted toward points (server/genesis-allocation.js's DEFAULT_ALLOCATION_OPTS.referralCap)."},"actions":{"type":"object","description":"Genesis action-points breakdown — every action type, true if this wallet has earned it. Always all 7 keys, false for unearned ones. See docs/superpowers/specs/2026-07-29-genesis-action-points-design.md.","required":["account_created","identity_linked","x_quest_completed","farcaster_quest_completed","token_launched","token_traded","fees_claimed"],"properties":{"account_created":{"type":"boolean","description":"Passed this provider's own anti-sybil check — the base action every other action is gated on."},"identity_linked":{"type":"boolean","description":"Linked a 2nd social identity to this wallet via /account."},"x_quest_completed":{"type":"boolean","description":"Completed the X quest (replied to the pinned tweet tagging 2 friends, follows @bonker_wtf)."},"farcaster_quest_completed":{"type":"boolean","description":"The @bonkerwtf join cast tagged 2+ other accounts."},"token_launched":{"type":"boolean","description":"Deployed at least one token through Bonker."},"token_traded":{"type":"boolean","description":"Traded any Bonker-launched token."},"fees_claimed":{"type":"boolean","description":"Claimed creator/LP fees at least once."}}}}}}}},"400":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/genesis/claim/{address}":{"get":{"tags":["Genesis"],"summary":"Read one wallet genesis airdrop claim proof","description":"Reads the shipped frozen genesis airdrop manifest. Before the manifest ships, frozen is false and allocation/proof fields are null.","parameters":[{"name":"address","in":"path","required":true,"schema":{"$ref":"#/components/schemas/EvmAddress"}}],"responses":{"200":{"description":"Genesis airdrop claim state for the wallet","content":{"application/json":{"schema":{"type":"object","required":["frozen","token","airdrop","eligible","allocated","proof","merkleRoot"],"properties":{"frozen":{"type":"boolean"},"token":{"anyOf":[{"$ref":"#/components/schemas/EvmAddress"},{"type":"null"}]},"airdrop":{"$ref":"#/components/schemas/EvmAddress"},"eligible":{"type":"boolean"},"allocated":{"type":["string","null"],"description":"Base-unit token allocation as a decimal string."},"proof":{"type":["array","null"],"items":{"type":"string","pattern":"^0x[a-fA-F0-9]{64}$"}},"merkleRoot":{"type":["string","null"],"pattern":"^0x[a-fA-F0-9]{64}$"}}}}}},"400":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/genesis/allowlist":{"post":{"tags":["Genesis"],"summary":"Retired anonymous Genesis signup (410)","description":"Retired route. Returns 410 Gone; the legacy request shape accepted xHandle or farcaster. Use the Farcaster cast-native join or the verified X quest routes instead.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["address"],"anyOf":[{"required":["xHandle"]},{"required":["farcaster"]}],"properties":{"address":{"$ref":"#/components/schemas/EvmAddress"},"xHandle":{"type":"string","maxLength":30,"description":"X handle, sanitized to alphanumeric plus underscore and stored without a leading @."},"farcaster":{"type":"string","maxLength":40,"description":"Farcaster username, sanitized to letters, digits, underscore, dot, and hyphen."},"ref":{"type":"string"},"timestamp":{"type":["integer","string"],"description":"Only for refreshing an EXISTING entry: ms epoch, paired with `signature`, valid for 5 minutes."},"signature":{"type":"string","pattern":"^0x[a-fA-F0-9]+$","description":"Only for refreshing an EXISTING entry: wallet signature over \"Update genesis socials for {address} at {timestamp}\\n{JSON handles}\", recovered signer must equal `address`. First joins need no signature; without a valid owner signature an existing entry’s socials are never overwritten."},"website":{"type":"string","description":"Hidden honeypot field. Filled values are rejected."}}}}}},"responses":{"201":{"description":"Allowlist signup accepted","content":{"application/json":{"schema":{"type":"object","required":["ok","count","ref"],"properties":{"ok":{"type":"boolean"},"count":{"type":"integer"},"ref":{"$ref":"#/components/schemas/EvmAddress","description":"The joiner's own address — their shareable referral code, not the recorded referrer."}}}}}},"400":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/genesis/x/config":{"get":{"tags":["Genesis"],"summary":"Read X Genesis quest availability","responses":{"200":{"description":"Quest configuration status","content":{"application/json":{"schema":{"type":"object","required":["configured"],"properties":{"configured":{"type":"boolean"},"tweetUrl":{"type":["string","null"],"format":"uri","description":"Pinned quest post to reply to; null until the quest is configured"},"minAccountAgeDays":{"type":"integer","description":"Minimum X account age enforced by the quest sybil gate"},"minFollowers":{"type":"integer","description":"Minimum X follower count enforced by the quest sybil gate"}}}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/genesis/x/oauth-url":{"get":{"tags":["Genesis"],"summary":"Start the X OAuth quest connection","responses":{"200":{"description":"X OAuth authorization URL","content":{"application/json":{"schema":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri"}}}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/genesis/x/callback":{"get":{"tags":["Genesis"],"summary":"Complete X OAuth PKCE callback","responses":{"200":{"description":"HTML popup callback that posts a short-lived identity token to the opener"},"400":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/genesis/x/submit":{"post":{"tags":["Genesis"],"summary":"Submit a verified X Genesis quest","description":"Verifies OAuth identity, signed wallet, reply to the configured pinned tweet with two mentions, cached follow, account-quality gate, and optional qualified X referral.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["address","signature","timestamp","nonce","xIdentityToken","tweetUrl"],"properties":{"address":{"$ref":"#/components/schemas/EvmAddress"},"signature":{"type":"string"},"timestamp":{"type":["integer","string"]},"nonce":{"type":"string"},"xIdentityToken":{"type":"string"},"tweetUrl":{"type":"string","format":"uri"},"xref":{"type":"string"}}}}}},"responses":{"200":{"description":"Quest result","content":{"application/json":{"schema":{"type":"object","required":["qualified"],"properties":{"qualified":{"type":"boolean"},"reason":{"type":"string"}}}}}},"400":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"409":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/referrals/{address}":{"get":{"tags":["Creator"],"summary":"Read referral stats for a wallet","parameters":[{"name":"address","in":"path","required":true,"schema":{"$ref":"#/components/schemas/EvmAddress"}}],"responses":{"200":{"description":"Referral stats","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/portfolio/{address}":{"get":{"tags":["Creator"],"summary":"Read LP fee recipient portfolio for a wallet","parameters":[{"name":"address","in":"path","required":true,"schema":{"$ref":"#/components/schemas/EvmAddress"}}],"responses":{"200":{"description":"Portfolio","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/creator/{address}/unclaimed":{"get":{"tags":["Creator"],"summary":"Read creator claimable WETH summary","parameters":[{"name":"address","in":"path","required":true,"schema":{"$ref":"#/components/schemas/EvmAddress"}}],"responses":{"200":{"description":"Claimable fee summary","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}},"400":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/mcp/health":{"get":{"tags":["System"],"summary":"Read MCP server status","description":"Reports the Model Context Protocol server mounted at POST /api/mcp: its tool count, supported protocol versions, and whether server-assisted launching is enabled. POST /api/mcp speaks JSON-RPC 2.0 over the MCP Streamable HTTP transport and is not described by this REST specification; see docs/MCP-SERVER.md.","responses":{"200":{"description":"MCP server status","content":{"application/json":{"schema":{"type":"object","required":["status","transport","endpoint","toolCount"],"properties":{"status":{"type":"string","enum":["ok"]},"server":{"type":"object","additionalProperties":true},"transport":{"type":"string","example":"streamable-http"},"endpoint":{"type":"string","example":"/api/mcp"},"protocolVersion":{"type":"string","example":"2025-06-18"},"supportedProtocolVersions":{"type":"array","items":{"type":"string"}},"toolCount":{"type":"integer","example":7},"tools":{"type":"array","items":{"type":"string"}},"chain":{"type":"object","additionalProperties":true},"serverAssistedLaunch":{"type":"string","enum":["enabled","disabled"]}}}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/launch/free":{"get":{"tags":["Tokens"],"summary":"Free-launch cooldown status for the current session","description":"Whether the session wallet is inside its 24h free-launch window, and how long is left, so /launch can disable its button with a countdown instead of letting someone fill in the whole form and collect a 429. Scope is only the cooldown — the wallet-plus-two-identities half of eligibility is already derivable from the session. Always answers 200: \"logged out\" and \"on cooldown\" are ordinary page states, not failed requests. Advisory: deploy-service re-checks authoritatively on POST, and this reads the Postgres mirror of its dual-write, which can lag but never holds a row deploy-service lacks — so a stale answer can only be too permissive, never a false lockout. Never cached (private, no-store): the answer is derived from one visitor's session cookie.","responses":{"200":{"description":"The current free-launch state for this session.","content":{"application/json":{"schema":{"type":"object","required":["enabled","state","secondsRemaining","nextAvailableAt"],"properties":{"enabled":{"type":"boolean","description":"False when MCP_SERVER_ASSISTED_LAUNCH is off."},"state":{"type":"string","enum":["available","cooldown","logged_out","no_wallet","disabled"]},"secondsRemaining":{"type":"integer","description":"0 unless state is \"cooldown\"."},"nextAvailableAt":{"type":["integer","null"],"description":"Unix seconds when the next free launch unlocks; null unless on cooldown."}}}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}},"post":{"tags":["Tokens"],"summary":"Launch a token for free, paid by Bonker (session-authenticated)","description":"The website's own front door onto the same server-assisted deploy-service path POST /api/mcp's launch_token mode \"server\" uses. Requires a bonker_session cookie with a linked wallet, plus two of X/Telegram/Farcaster also linked to that session — the identical bar an MCP API key requires (see docs/MCP-SERVER.md). Limited to one launch per wallet per 24 hours by deploy-service, in addition to the 5-requests-per-minute-per-IP route limit. Fields are intentionally minimal (no description, socials, fee tier, vault, airdrop, or dev-buy) to match the fixed defaults deploy-service already applies on this path for every other caller.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","required":["name","symbol","confirm"],"properties":{"name":{"type":"string","maxLength":50},"symbol":{"type":"string","maxLength":10,"description":"A leading \"$\" is stripped automatically."},"image":{"type":"string","format":"uri","description":"Optional http(s) image URL."},"confirm":{"type":"boolean","description":"Must be true. This spends real ETH from Bonker's shared wallet and cannot be undone."}}}}}},"responses":{"200":{"description":"A mined, successful deployToken — never returned for a bare submission.","content":{"application/json":{"schema":{"type":"object","required":["txHash","tokenAddress"],"properties":{"txHash":{"type":"string"},"tokenAddress":{"$ref":"#/components/schemas/EvmAddress"}}}}}},"400":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"502":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/api/livez":{"get":{"tags":["System"],"summary":"Liveness probe — is the process answering at all?","description":"Checks nothing. A liveness failure can only cause a restart, so it must not depend on anything a restart cannot fix. Dependency state belongs in /api/readyz.","responses":{"200":{"description":"Process is alive","content":{"application/json":{"schema":{"type":"object","required":["status"],"properties":{"status":{"type":"string","enum":["alive"]},"uptime":{"type":"integer"}}}}}}}}},"/api/readyz":{"get":{"tags":["System"],"summary":"Readiness probe — can this instance serve a request?","description":"Checks Postgres reachability with a 2s bound. A failure removes the instance from load balancing without restarting it. Enricher and poller health are deliberately excluded — see /api/health for those.","responses":{"200":{"description":"Ready to serve","content":{"application/json":{"schema":{"type":"object","required":["status","checks"],"properties":{"status":{"type":"string","enum":["ready"]},"checks":{"type":"object","additionalProperties":true}}}}}},"503":{"description":"Not ready — the database check failed or timed out","content":{"application/json":{"schema":{"type":"object","required":["status","checks"],"properties":{"status":{"type":"string","enum":["not_ready"]},"checks":{"type":"object","additionalProperties":true}}}}}}}}},"/api/health":{"get":{"tags":["System"],"summary":"Read server health and poller status","responses":{"200":{"description":"Healthy or degraded health response","content":{"application/json":{"schema":{"type":"object","required":["status","checks","pollers","uptime"],"properties":{"status":{"type":"string","enum":["ok","degraded","unhealthy"]},"checks":{"type":"object","additionalProperties":true,"properties":{"tokens":{"type":"object","additionalProperties":true},"presales":{"type":"object","additionalProperties":true},"enrichers":{"type":"object","additionalProperties":true}}},"pollers":{"type":"object","additionalProperties":true,"properties":{"tokens":{"type":"object","additionalProperties":true,"properties":{"lastPoll":{"type":"integer"},"lastBlock":{"type":"integer"},"enrichers":{"type":"object","additionalProperties":true,"properties":{"dexscreener":{"type":"object","additionalProperties":true},"blockscout":{"type":"object","additionalProperties":true}}}}}}},"presaleContract":{"type":["string","null"]},"uptime":{"type":"integer"}}}}}},"429":{"description":"JSON error response","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Unhealthy required runtime dependency","content":{"application/json":{"schema":{"type":"object","additionalProperties":true}}}}}}}},"components":{"securitySchemes":{"AdminApiKey":{"type":"http","scheme":"bearer","bearerFormat":"bnk_admin_*","description":"Revocable key generated by a Factory owner/admin wallet in /admin."}},"schemas":{"EvmAddress":{"type":"string","pattern":"^0x[a-fA-F0-9]{40}$","example":"0x1111111111111111111111111111111111111111"},"Error":{"type":"object","required":["error"],"properties":{"error":{"type":"string"}}},"ImageResult":{"type":"object","required":["url"],"properties":{"url":{"type":"string","format":"uri","example":"https://bonker.wtf/i/<sha256>.png"}}},"ImageGenerationConfig":{"type":"object","required":["enabled","cap"],"properties":{"enabled":{"type":"boolean"},"cap":{"type":"integer","description":"This wallet's renders per rolling 24 hours (base for a logged-out caller)."},"used":{"type":"integer"},"remaining":{"type":"integer"},"secondsRemaining":{"type":"integer"},"nextAvailableAt":{"type":["integer","null"]},"queueDepth":{"type":["integer","null"],"description":"Renders queued or rendering right now."}}},"ImageJobQueued":{"type":"object","required":["jobId","status","cap","remaining"],"properties":{"jobId":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["queued"]},"position":{"type":["integer","null"],"description":"Renders ahead of this one (0 = next up)."},"cap":{"type":"integer"},"remaining":{"type":"integer","description":"After this one."}}},"ImageJob":{"type":"object","required":["id","status"],"properties":{"id":{"type":"string","format":"uuid"},"status":{"type":"string","enum":["queued","running","done","failed","cancelled"]},"position":{"type":["integer","null"]},"url":{"type":["string","null"],"format":"uri"},"error":{"type":["string","null"]},"createdAt":{"type":"integer"},"startedAt":{"type":["integer","null"]},"finishedAt":{"type":["integer","null"]}}},"AdminApiKeySignedRequest":{"type":"object","required":["address","timestamp","nonce","signature"],"properties":{"address":{"$ref":"#/components/schemas/EvmAddress"},"timestamp":{"type":"string","description":"Current Unix time in milliseconds."},"nonce":{"type":"string","pattern":"^0x[a-fA-F0-9]{32,128}$"},"signature":{"type":"string","pattern":"^0x[a-fA-F0-9]+$"}}}}},"x-rateLimits":{"globalApi":{"appliesTo":"/api/*","windowSeconds":60,"maxRequests":100,"headers":"standard RateLimit headers"},"tokenDetail":{"appliesTo":"GET /api/tokens/{address}","windowSeconds":60,"maxRequests":30,"reason":"Token detail performs RPC-backed enrichment and is more expensive than list routes."},"imageGeneration":{"appliesTo":"POST /api/generate-image","windowSeconds":10,"maxRequests":1,"reason":"Flood gate in front of the render queue; the real bound is the per-wallet rolling-24h cap (2-10, by activity)."},"imageJobStatus":{"appliesTo":"GET /api/generate-image/jobs/{jobId}","windowSeconds":60,"maxRequests":60,"reason":"The launch page polls a job every 3 seconds for a few minutes; this bounds a runaway poller without getting in the way of an honest one."},"imageUpload":{"appliesTo":"POST /api/upload-image","windowSeconds":60,"maxRequests":30,"reason":"Each upload writes to disk; a dedicated cap bounds per-IP disk growth on top of the global /api limit."},"mcp":{"appliesTo":"POST /api/mcp","windowSeconds":60,"maxRequests":60,"countedPer":"JSON-RPC message, not HTTP request (a batch of N messages costs N; batches are capped at 50)","reason":"MCP tool calls can touch RPC and the image store, so the endpoint gets a tighter budget than the global /api limit."},"mcpImageUpload":{"appliesTo":"POST /api/mcp (tools/call: upload_image)","windowSeconds":60,"maxRequests":30,"countedPer":"upload_image tool call, not HTTP request","reason":"Mirrors the POST /api/upload-image cap so the MCP path is not a way around it, batched or otherwise."},"mcpImageGeneration":{"appliesTo":"POST /api/mcp (tools/call: generate_image)","windowSeconds":10,"maxRequests":1,"countedPer":"generate_image tool call, not HTTP request","reason":"Mirrors the POST /api/generate-image flood gate; the per-wallet daily allowance is enforced in the tool itself, keyed on the API key owner."},"genesisSignup":{"appliesTo":"POST /api/genesis/allowlist","windowSeconds":10,"maxRequests":3},"comments":{"appliesTo":"POST /api/tokens/{address}/comments","windowSeconds":60,"maxRequests":10}}}