Boga Docs

Boga is an independent, permissionless launchpad built for Robinhood Chain. It is not affiliated with, endorsed by, or operated by Robinhood Markets, Inc. Boga never signs or launches tokens for you — the Boga API prepares and simulates transactions; you sign and broadcast with your own wallet.

Overview

Boga Developer Platform

Boga is an independent, permissionless token launchpad on Robinhood Chain (chainId 4663). One transaction deploys the token, burns the launch liquidity on Uniswap V4 (making it irremovable), and commits the token's metadata on-chain. Contracts are immutable — new behavior ships as new contracts, never as mutations of what is already deployed.

Boga does not sign or launch tokens for users. Boga APIs prepare, validate and simulate transactions; the creator signs and broadcasts with their own wallet. No Boga service ever receives, stores, logs or uses a private key.

What you can do here

  • Launch programmatically from a script, bot, or backend — see Programmatic Launches.
  • Use the TypeScript SDK (@boga/sdk) with viem, ethers, Foundry, or raw RPC — see SDK.
  • Drive it from the terminal with the boga CLI — offline-first, and it refuses to ever touch a private key — see CLI.
  • Integrate the REST API for prepare/simulate/config/status — see Public API and the OpenAPI Reference.
  • Attach immutable metadata (image + description), self-hosted or Boga-hosted — see Metadata.

Where to start

If you want to… Read
Understand the system end to end Architecture
Launch a token from code Programmatic Launches
Verify what Boga can and cannot do Security Model

Guarantees

  • Non-custodial by construction. Prepare/simulate endpoints return unsigned transactions; nothing in the SDK, API, or CLI accepts a signer or key.
  • Immutable metadata. The metadata document is hashed (keccak-256 over canonical JSON) and committed on-chain with the token; there is no setter and no override path. See Metadata for the exact bytes hashed.
  • One canonicalization implementation. The same @boga/metadata code runs in the SDK, API, CLI, and indexer, so an identical document hashes identically everywhere — never reimplement it.
  • Predictable addresses. Token addresses are CREATE2-derived and can be computed before broadcast from the SDK or the API.

Robinhood Chain setup

Boga runs on Robinhood Chain, an EVM chain where ETH is the native currency. To use it from a wallet, add it as a custom network:

Setting Value
Network name Robinhood Chain
Chain ID 4663
Native currency ETH
RPC URL https://rpc.mainnet.chain.robinhood.com
Block explorer robinhoodchain.blockscout.com

Fund the wallet with ETH on Robinhood Chain and every Boga flow — launching, trading, earning creator fees — works from there. For programmatic access, point your RPC client at the same URL and chain id.

Trading

Every Boga token trades on a standard Uniswap V4 pool from the moment it launches — there is no bonding-curve phase and no migration event. Trade from the Boga web app, or through any Uniswap V4-compatible interface or router; pools pair each token with native ETH. See Uniswap V4 pools for how pools and the fee hook work.

Getting Started

Getting Started

How to use Boga from the web app — no code required. If you want to launch or trade from scripts instead, see Programmatic Launches.

Connect a wallet

Boga works with browser extension wallets (MetaMask, Rabby and similar).

  1. Add Robinhood Chain to your wallet — network values in Robinhood Chain setup.
  2. Fund the wallet with ETH on Robinhood Chain. (Network gas is always ETH; if you trade a token paired against USDG or a tokenized stock, you also hold that quote asset — see Quote Assets & Pairing.)
  3. Open the app and press Connect (top right), then approve the connection in your wallet.

On mobile, open the app inside your wallet app's built-in browser.

Create a token

Launching costs no Boga fee — you only pay network gas. From Launch a token:

  1. Details — name, symbol and description; optional website, X and Telegram links; an image (required — PNG, JPEG or WebP, at least 256×256). You can optionally set a fee recipient: the address that receives the token's creator fees (leave it empty to keep them in your wallet; the rights can be transferred later).
  2. Initial buy — optionally buy the first 1–2% of supply in the same transaction, with the ETH cap shown up front. Zero is fine.
  3. Preview — review everything. Launches are public and irreversible: the metadata is committed on-chain and cannot be edited after launch.
  4. Launch — sign the single transaction in your wallet. It deploys the token, creates the Uniswap V4 pool and burns the launch liquidity so it can never be withdrawn.

After launch: public trading opens after a short delay (15 seconds), and for the first 60 minutes a temporary max-wallet limit of 2% of supply applies.

Trade a token

  1. Find a token in Explore (trending, new, Bullseye) or search by name, ticker or contract address. Always verify the contract address — symbols are not unique.
  2. On the token page, use the buy/sell panel: enter an amount, press Review buy (or sell), and confirm the swap in your wallet.
  3. Trades run on the token's Uniswap V4 pool with a 1% trading fee, taken quote-side by the pool's hook (the pool's own LP fee is 0). See Trading for how pools work.

Tokens can lose all value. Launches are permissionless — anyone can create a token, and a token's name or image says nothing about its quality.

Creator fees

Creators earn at least 70% of the pool's trading fees, forever — paid automatically, with nothing to claim:

  1. Every trade on your token adds your share, and it reaches your wallet automatically — there is no "collect" or "claim" button, because there is nothing to harvest. On an ETH or USDG pool it pays out in the same transaction as the trade — every trade, no waiting. On a newly-listed stock pool with no prior liquidity the first trade records the fee and the next one (or a permissionless sweep anyone can trigger — you don't) settles it moments later. It always arrives in the pool's quote asset (WETH for an ETH pool, USDG for a USDG pool, the stock token for a stock pool).
  2. Open Portfolio with the wallet that holds the token's fee rights (the launching wallet, or the fee recipient set at launch) to see your payout history.
  3. Fee rights are transferable with a two-step propose/accept handshake — see Liquidity for how the split works. If a payout ever can't reach your wallet, it is held safely and released automatically once you set a wallet that can receive it.

Fees & Tokenomics

Fees and Tokenomics

Every number on this page is a fixed constant of the deployed contracts unless marked adjustable. Values are identical for every token launched on Boga.

Launch parameters

Parameter Value
Total supply 1,000,000,000 tokens — fixed, minted at launch
Launch fee None — you only pay network gas
Initial buy (optional) 1–2% of supply (10M–20M tokens), or zero
Public trading delay 15 seconds after launch
Temporary max wallet 2% of supply (20M tokens) for the first 60 minutes
Name / symbol length 1–60 / 1–12 bytes

The entire supply is seeded into the token's Uniswap V4 pool at launch; the optional initial buy is the only launch-time allocation, and it is bought from the pool at market price inside the launch transaction itself.

Trading fees

Parameter Value
Trading fee 1% per swap — the same total whether you buy or sell
Creator share of fees At least 70%, forever
Platform share of fees At most 30% — adjustable by the treasury, hard-capped on-chain

The 1% is taken on the quote side of every trade — the quote you pay in on a buy, the quote you receive on a sell — and split between the creator and the platform. There is no claim step: the fee is generated on every swap and delivered to each side's wallet automatically. For ETH- and USDG-quoted pools it is paid in the same transaction as the trade — every buy and every sell, with no waiting and no keeper. The one exception is a newly-listed stock quote with no prior liquidity: its very first trade records the fee and the next trade (or a permissionless sweep — no button; anyone can call it) settles it moments later. That short wait clears as soon as trading continues, and it never happens on an ETH or USDG pool.

Fees are always paid in the pool's quote asset, never in the launched token, so nobody has to sell the community's token to realize them: an ETH-paired pool pays in WETH, a USDG-paired pool pays in USDG, and a stock-paired pool pays in that stock token. The platform share can be lowered (or raised back) by the treasury, but never above the on-chain 30% cap, and a share already paid out is never re-split. Creator fee rights are transferable — see Liquidity.

If a payout can't reach its wallet — for example a quote asset whose issuer has restricted the receiving address — that share is held safely inside the protocol instead of blocking the trade. It is released automatically once the beneficiary points to a wallet that can receive it (the app handles this when you change your payout address); your trades never fail because a fee couldn't be paid.

Liquidity

The launch liquidity is burned: the pool's liquidity position is sent to a dead address at launch, so the principal can never be withdrawn, moved, or unlocked — by anyone, ever. There is no custodian to trust and nothing to release; liquidity is irremovable by construction.

Bullseye

A token reaches Bullseye at 4.2 ETH of cumulative net buy flow into its pool. It is a milestone, not a mechanism: reaching it never changes the pool's price curve, the 1% fee, or anything about how the token trades — see Uniswap V4 pools.

Pools paired against a non-ETH quote asset use the value equivalent of 4.2 ETH, fixed when the pool is created — see Quote Assets & Pairing.

Genesis Bounty

Genesis Bounty (points)

Genesis Bounty is Boga's single points campaign. Points are non-transferable platform rewards computed off-chain from on-chain activity under published, versioned rules. They have no monetary value and do not guarantee eligibility for any future token, airdrop or benefit. The authoritative record is the platform's points ledger; on-chain events are the evidence.

Trading points

Rule Value
Rate 1 point per USD 100 of eligible volume
Measured on the platform's 30% share of the 1% swap fee (USD 0.30 per point)
Rate curve flat — full rate for all eligible volume, no caps
Precision 1 point = 1,000,000 point units (UI shows two decimals)

Points are measured on the platform's share of the fee — not the full 1% — because it is the only component of the fee that nobody can recover: the creator's 70% flows back to whoever created the token, so measuring on the full fee would let a creator trading their own token earn points more cheaply than everyone else. Measuring on the platform share makes the metric identical for every trader by construction.

What never mints points

  • The creator trading their own token, and the launch's initial buy.
  • Wash-style round trips — a buy and a sell by the same wallet on the same token within 10 seconds.
  • Reverted transactions and swaps outside the official pools.

Held for review

If one wallet drives more than 20% of a token's eligible volume (once the token has meaningful volume in the window), its trades on that token are held pending. This resolves fully automatically, no human in the loop:

  • If other traders dilute the wallet below 20% within the 7 days following the trade, the held points confirm on the next engine cycle.
  • If the wallet is still above 20% once the trade is 7 days old — the market never showed up — those trades are disqualified for good. Not held: a sustained 20%+ share of a token means there was no real market, just one wallet.

A swap with no quote-price observation yet is valued automatically at the nearest known observation once one exists — a price is never invented.

Creator points

A token qualifies at 24 hours of age with 25+ unique eligible buyers and USD 10,000+ of eligible volume. Once qualified, its creator earns:

Component Value Cap per token
Qualification bonus 55 points once
Sustained market cap 5.5 points per USD 10,000 of the highest sustained 15-minute TWAP 275
Volume share 5% of the token's eligible trading points 690
Bullseye 140 points once
Total ceiling 1,160

Market-cap and Bullseye points start as pending and confirm after a 7-day review window anchored to the on-chain moment of the milestone; a new market-cap peak restarts the window for that component. The market-cap measurement requires 15 contiguous one-minute candles — a minute without trades breaks the window.

Creators also earn 70% of every swap fee on their token in cash, independently of points.

Statuses and rule changes

Every ledger row carries a status: pending, confirmed, under_review, adjusted or disqualified. Rules are versioned: they may change, and recorded activity may be re-scored under updated rules at any time before any payout, distribution or snapshot is announced — if any ever is. See the Terms of Use for the binding language.

Bounty Points are non-transferable platform rewards. They have no monetary value and do not guarantee eligibility for any future token or airdrop.

Architecture

Developer Platform — Architecture

                         ┌───────────────────────── @boga/metadata ─────────────────────────┐
                         │ schema v1 · RFC 8785 canonicalize · keccak hash · strict parse ·  │
                         │ validate · URI/trust · SSRF guard · builder                        │
                         └───────┬───────────────┬───────────────┬───────────────┬───────────┘
                                 │               │               │               │
                        ┌────────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐ ┌──────▼──────┐
                        │  @boga/sdk    │ │  @boga/api  │ │ @boga/indexer│ │  @boga/cli  │
                        │ BogaClient    │ │ prepare/    │ │ TokenLaunched│ │ boga …      │
                        │ buildTx/parse │ │ simulate/   │ │ → fetch +    │ │ (wraps SDK) │
                        │ predict/pool  │ │ config/     │ │ verify +     │ │             │
                        │ (no key)      │ │ status +    │ │ classify     │ │             │
                        │               │ │ uploads     │ │              │ │             │
                        └───────┬───────┘ └──────┬──────┘ └──────┬───────┘ └─────────────┘
                                │                │               │
                     ┌──────────▼────────┐  ┌────▼─────┐   ┌─────▼─────┐
                     │ contracts (Foundry)│  │ Postgres │   │  RPC (viem │
                     │ BogaFactory +      │  │ launches │   │  read-only)│
                     │ BogaToken (imm.    │  │ + metadata│   │            │
                     │ metadataURI/hash)  │  │ columns  │   │            │
                     └────────────────────┘  └──────────┘   └────────────┘

Components

  • @boga/metadata — the one canonicalization/hashing/validation implementation. Everything else depends on it; there is no second copy (developer platform §7).
  • contractsBogaToken stores immutable metadataURI + metadataHash and exposes tokenURI/contractURI; BogaFactory takes the structured LaunchParams and emits TokenLaunched.
  • @boga/sdkBogaClient + free functions. Offline: metadata + build/parse/ predict. API-backed: upload/simulate/prepare/status/config. No signer, ever.
  • @boga/api/v1/protocol/config, /v1/launches/{prepare,simulate}, /v1/transactions/:hash, /v1/launches/:token, optional /v1/metadata[/images]
    • /openapi.json. Read + prepare only; a read-only viemLaunchReader.
  • @boga/indexer — projects TokenLaunched, then fetches (SSRF-guarded), hash-verifies, schema-validates, name/symbol-checks and classifies metadata trust/status. Never blocks trading.
  • @boga/cli — thin, non-custodial wrapper over the SDK.
  • DBlaunches stores immutable-metadata columns.

Uniswap V4 pools

Every Boga token trades on a standard Uniswap V4 pool from the moment it launches — there is no internal bonding curve to migrate out of. The pool's quote asset is currency0 and the launched token currency1 — native ETH by default, or a configured ERC-20 quote such as USDG or a tokenized stock (see Quote Assets & Pairing). The pool is created and seeded in the same transaction as the launch, with the pool's own liquidity-provider fee set to zero.

A Boga hook is attached to each pool. On every swap it does two things: it takes the 1% trading fee on the quote side and it records the pool's cumulative buy flow to detect the Bullseye milestone (4.2 ETH). For ETH- and USDG-quoted pools the fee is paid out to the creator and platform in the same transaction as the trade — every swap, no keeper. Only a newly-listed stock quote with no prior liquidity is the exception: the hook records the fee and settles it on the next trade (or a permissionless sweep); see Fees & Tokenomics. It does not touch the price curve — the total 1% cost and the swap math are exactly those of the underlying Uniswap V4 pool.

Liquidity

The liquidity position backing each launch is burned at launch: the position NFT is transferred to a dead address, so nobody can ever withdraw, move, or unlock the principal. There is no custodian contract in the design — liquidity is irremovable by construction, and the only fact to verify is that the position is owned by the dead address.

Trading fees are handled entirely by the hook, not by a separate distributor. The hook takes the 1% quote-side fee on each swap and splits it between the creator and the protocol (the protocol share is bounded on-chain by a hard cap of 30%, the remainder always the creator's), pushing both shares to their wallets. For ETH- and USDG-quoted pools this happens in the same transaction as the trade; only a newly-listed stock quote with no prior liquidity records the fee first and settles it on the next trade (or a permissionless sweep) — a normal, transient state that never arises on an ETH or USDG pool. If a payout is blocked by the receiving side, that share is parked inside the hook and released later — trading is never affected. Creator fee rights are transferable via a propose/accept handshake, and a different fee recipient can be set at launch time.

Data flow (a launch)

  1. Metadata document built + canonicalized + hashed (shared lib).
  2. Hash committed on-chain in BogaToken + TokenLaunched (creator-signed tx).
  3. Indexer reads the event, fetches the URI, re-derives the hash, verifies equality, validates schema, records trust/status + parsed fields.
  4. API serves the aggregated, verified launch status.

Non-custodial boundary

The only component that touches the chain for writes is the creator's wallet. SDK/API/CLI/indexer perform read-only chain access (views, gas estimate, logs, receipts). No private key crosses any Boga boundary.

Quote Assets & Pairing

Quote Assets & Pairing

Every Boga pool has a quote asset — the currency traders use to buy and sell the launched token. By default that is native ETH, the classic pair. Creators can also launch a token paired against USDG (a stablecoin, 6 decimals) or a canonical tokenized stock / ETF on Robinhood Chain (18 decimals), when those quotes are enabled for the platform.

This page explains what changes with a non-ETH quote and what stays the same.

What stays the same

Pairing does not change the core guarantees. Regardless of quote asset:

  • Supply is fixed at 1,000,000,000 tokens — no mint, no tax, no blacklist, no pause on the launched token.
  • The entire supply is seeded into a single Uniswap V4 pool with a 1% fee.
  • Liquidity is permanently burned; the position principal can never be withdrawn.
  • Creator/platform fee split and the Bullseye milestone apply per pool.

Choosing a quote at launch

The launch wizard offers a Pair with step whenever non-ETH quotes are enabled. ETH is the default and needs no extra setup. Selecting USDG or a tokenized stock surfaces the risk disclosures below before you can continue.

What changes with an ERC-20 quote

  • Buyers trade with the quote asset. To buy a USDG-paired token you spend USDG, not ETH; to buy a stock-paired token you spend that stock token. The trade panel shows the pair and the balance you need.
  • A one-time approval. Before their first buy on an ERC-20-quoted pool, buyers approve the quote token once (a standard token approval flow). Native ETH pools need no approval.
  • Prices are shown in the quote. Market cap, volume and the Bullseye threshold are denominated in the pool's quote asset. Cross-pool rankings normalize to USD so pools with different quotes stay comparable.
  • Network gas is always ETH. Gas is paid in ETH even for a USDG- or stock-paired pool; the buy amount is in the quote. These are separate currencies and are shown separately.

Bullseye across quotes

The Bullseye milestone is a fixed 4.2 ETH of net buy flow on ETH pools. On a non-ETH pool it is the value equivalent of 4.2 ETH, fixed when the pool is created from an on-chain price reference. It remains a permanent social milestone — reaching it never changes the pool's state.

Tokenized stock quotes: extra risks

Tokenized stocks and ETFs are issued by a third party. Pairing against them adds risks that native ETH and USDG pools do not carry — read these before launching or trading a stock-paired token:

  • Issuer control. The quote asset can be upgraded, paused or restricted by its issuer. If the issuer pauses the asset, trading on the pool can freeze — a structural property of any pairing with these assets, not a Boga-specific limitation. A narrower restriction that only blocks a fee payout (e.g. a denied receiving address) does not stop trading: that share parks safely and is released once the receiver is eligible (see below).
  • Market hours. A tokenized stock tracks a security that trades only during market hours. The Boga pool trades 24/7, so its price can diverge from the underlying while the market is closed — a quiet or offset price outside market hours is expected, not stale data.
  • Creator fees are paid in the stock token. Fees earned on a stock-paired pool are paid in that stock token — the creator of a TSLA-paired pool receives TSLA, just as a USDG pool pays USDG and an ETH pool pays WETH. They reach your wallet automatically, with nothing to claim. For a newly-listed stock quote with no prior on-chain liquidity, the first trade records the fee and the next trade (or a permissionless sweep anyone can trigger) settles it moments later; once the quote has liquidity it settles in the same transaction as the trade. Boga never converts them to another asset, so as a creator you will hold the tokenized security you paired against and are responsible for managing it. If the token's transfer rules block a payout to your address, that share parks safely and is released once you provide an eligible address. Receiving fees does not depend on a price feed or on market hours.

Pairing with tokenized securities may be subject to legal and regulatory constraints in your jurisdiction. You are responsible for ensuring your launch complies with applicable law.

Programmatic Launches

Programmatic Launches — Developer Flow

The contract-first, non-custodial flow. Boga prepares, validates and simulates; the creator signs and broadcasts with their own wallet.

Developer app
  → validate metadata            (@boga/metadata / SDK / CLI — offline)
  → upload OR self-host metadata  (POST /v1/metadata  OR  ipfs/ar/https you pin)
  → predict token address         (BogaFactory.predictTokenAddress via RPC)
  → simulate launch               (POST /v1/launches/simulate — optional)
  → build transaction             (SDK buildLaunchTransaction — offline)
  → creator SIGNS (own wallet)    (viem/ethers/Safe/HW/keystore)
  → creator BROADCASTS            (their RPC)
  → indexer confirms + indexes    (TokenLaunched → metadata fetch + hash-verify)
  → read back                     (GET /v1/launches/:token)

Steps

  1. Validateboga metadata validate / client.metadata.validate(doc). Schema v1, NFC, plain-text description, https/ipfs/ar only.
  2. Metadata URI + hash — either
    • Self-host: buildAndHashTokenMetadata(input) → pin the canonical JSON on IPFS/Arweave/https yourself; the pinned bytes must hash to metadataHash.
    • Boga-host (optional): POST /v1/metadata{ metadataUri, metadataHash }. Both derive the identical hash (one shared canonicalization).
  3. Predict addressclient.launch.predictAddress(rpc, creator, params) → deterministic in (creator, salt); predict the pool id with client.launch.predictPoolId(token).
  4. Simulate (optional) — POST /v1/launches/simulate → predicted addresses, estimated tokens/ETH, gas, decoded revert, block used, expiry.
  5. Buildclient.launch.buildTransaction(params){ to: factory, data, value: maxEthIn }. Offline; no key.
  6. Sign + broadcast — the creator, in their wallet. Boga is never involved.
  7. Wait + readclient.transactions.waitUntilIndexed(hash) then GET /v1/launches/:token (metadata status/trust, pool, liquidity, bullseye, fees).

Launch rules

1B fixed supply · 1% swap fee (paid to creator + platform automatically — in-trade for ETH/USDG pools; a new stock quote settles on the next trade or a sweep) · creator ≥70% / platform ≤30% (treasury can lower its share; hard cap 3000 bps) · Bullseye 4.2 ETH net · initial buy 0 or 100–200 bps (= 10M–20M) · temporary 2% max wallet for 60 min · 15 s public-trading delay · liquidity permanently burned.

Metadata

Boga Token Metadata (v1)

Immutable launch metadata: schema, canonicalization, and the exact bytes that produce the on-chain metadataHash. The single implementation lives in packages/metadata (@boga/metadata) and is consumed by the SDK, API, CLI, indexer and tests. Do not reimplement canonicalization anywhere else.

Model

A token's launch identity is an immutable JSON document conforming to the token metadata schema (v1) ($id: https://docs.boga.fun/schemas/token-metadata-v1.json).

Required: schema, schemaVersion, name, symbol, description, image, properties.{creator,chainId,platform,version}. Optional: external_url, links.{website,x,telegram,discord,farcaster}.

Field Limit (Unicode code points, after NFC)
name 1–64
symbol 1–16
description 10–500 (single-line plain text; C0 controls + DEL rejected)
image ≤2048; scheme ∈ ipfs / ar / https
external_url ≤2048; https only
links.* ≤2048 each; https only, or null

Constraints: chainId must be 4663, platform must be Boga, properties.version must be 1, schemaVersion{"1.0"}, creator a valid EIP-55/40-hex address. Unknown fields are rejected (additionalProperties: false). Values are NFC-normalized before validation and hashing.

The contract stores only string metadataURI and bytes32 immutable metadataHash. The raw image and description are never stored on-chain.

Canonicalization — the exact bytes hashed

canonicalizeTokenMetadata(metadata) implements a subset of RFC 8785 (JCS), restricted to the metadata value space:

  1. Output is UTF-8 with no insignificant whitespace.
  2. Object member keys are sorted by UTF-16 code units (JavaScript's default string sort — the JCS requirement).
  3. Strings are NFC-normalized, then serialized with JSON string escaping (JSON.stringify — RFC 8785 §3.2.2.2-compatible: short escapes \b \t \n \f \r \" \\, lowercase \u00xx for other C0 controls, / not escaped, non-ASCII left literal).
  4. Numbers are restricted to safe integers (the schema only uses integers: chainId, version). Non-integer / non-finite / bigint values are rejected rather than risk a divergent float serialization.
  5. undefined object members are omitted (they are not JSON); null is kept.

The hash is:

metadataHash = keccak256( utf8( canonicalizeTokenMetadata(metadata) ) )

hashTokenMetadata() returns a 0x-prefixed 32-byte hex string. This value is committed on-chain and folded into the CREATE2 salt, so it must be bit-for-bit identical everywhere — hence one shared implementation.

Worked example (golden vector v1_full)

Canonical bytes (whitespace shown only for wrapping — there is none in the actual bytes):

{"description":"A community token launched on Boga.","external_url":"https://boga.fun/token/0x0000000000000000000000000000000000000000","image":"ipfs://bafybeiexampleimagecidxxxxxxxxxxxxxxxxxxxxxxxxxxxxx/image.webp","links":{"discord":null,"farcaster":null,"telegram":"https://t.me/bogadog","website":"https://bogadog.fun","x":"https://x.com/bogadog"},"name":"Boga Dog","properties":{"chainId":4663,"creator":"0x0000000000000000000000000000000000000000","platform":"Boga","version":1},"schema":"https://docs.boga.fun/schemas/token-metadata-v1.json","schemaVersion":"1.0","symbol":"BDOG"}
metadataHash = 0x301c40f3e3a07aaef969a046a441ad7f6f966c6292f97b617049e400b63dc252

The full set of frozen golden vectors (input → canonical → hash) ships with @boga/metadata. Any change to canonicalization must be a deliberate, versioned change that updates these vectors (and, since the hash changes, a new metadata schema version).

API surface (@boga/metadata)

  • canonicalizeTokenMetadata(metadata) / canonicalTokenMetadataBytes(metadata)
  • hashTokenMetadata(metadata)0x… (keccak256 of canonical bytes)
  • verifyTokenMetadata(metadata, expectedHash) → boolean (case-insensitive hex)
  • parseTokenMetadata(bytes) — strict: rejects invalid UTF-8, duplicate keys, trailing content, raw control chars; no prototype pollution
  • validateTokenMetadata(metadata){ valid, errors[], warnings[] } with stable MetadataErrorCodes
  • buildTokenMetadata(input) / buildAndHashTokenMetadata(input) — construct a fixed-shape canonical document from developer input (EIP-55-checksums creator, always emits all five links keys, includes external_url only when set)
  • classifyMetadataTrust(uri, opts)MetadataTrust, assertSafeHttpUrl(url) (SSRF guard), parseUriScheme, isSupportedMetadataUri, isContentAddressed

Trust and status

MetadataTrust: content_addressed | hash_verified | mutable_uri | hash_mismatch | unavailable | invalid — how strongly the URI binds its content. MetadataStatus (indexer): pending | valid | unavailable | invalid_schema | hash_mismatch | unsafe_image | mutable_uri.

Content-addressed (ipfs://, ar://) URIs are content_addressed. https:// metadata is mutable_uri until the downloaded bytes are verified against metadataHash, at which point it is hash_verified. Metadata failure never blocks trading — status is presentation-only.

Security notes

  • Duplicate JSON keys are rejected at parse time (a document could otherwise hash one way and be read another).
  • Server-side fetching of https metadata/images must go through assertSafeHttpUrl (blocks loopback, RFC1918, CGNAT, link-local, IPv6 loopback/ULA, cloud-metadata hosts, credentials, non-443 ports). DNS-rebind mitigation (pin the resolved IP) is applied at fetch time in the indexer/API.
  • Descriptions and all string fields reject C0 control characters; HTML is never interpreted — output rendering must escape.

Public API

Public API

Read + prepare endpoints on the existing Fastify API (apps/api). The API never signs or broadcastsprepare/simulate build calldata and read chain state through a read-only RPC reader; the creator signs and sends with their own wallet. No endpoint accepts, stores or logs a private key.

Endpoints

Method Path Auth Notes
GET /v1/protocol/config none Versioned protocol config (constants + addresses)
POST /v1/launches/simulate none Predicted addresses, estimates, decoded simulation; no tx
POST /v1/launches/prepare none simulate + unsigned { to, data, value } + calldataHash
GET /v1/transactions/:hash none pending/confirmed/reverted/replaced/indexed
GET /v1/launches/:tokenAddress none Full launch status incl. metadata, pool, LP, bullseye, fees

All are public (data is public); global rate-limiting + CORS from createApi.

Non-custodial guarantees

  • No request body accepts a key/mnemonic/signature-to-broadcast.
  • prepare returns a plain transaction object; the server does not send it.
  • The RPC reader (viemLaunchReader) calls only view functions, estimateGas and receipt reads.

Stable error envelope

{ "error": { "code": "METADATA_HASH_MISMATCH", "message": "...", "details": {}, "requestId": "..." } }

Codes used: INVALID_REQUEST, INVALID_LAUNCH_PARAMS, CHAIN_ID_MISMATCH, INVALID_TRANSACTION_HASH, INVALID_ADDRESS, LAUNCH_NOT_FOUND, SERVICE_NOT_CONFIGURED, RPC_UNAVAILABLE, RPC_ERROR. No stack traces leak.

POST /v1/launches/prepare

Request:

{ "creator":"0x…","name":"Boga Dog","symbol":"BDOG","metadataUri":"ipfs://…",
  "metadataHash":"0x…","salt":"0x…","initialBuyBps":100,"maxEthIn":"12000000000000000" }

Response (abridged):

{ "chainId":4663,"blockNumber":123456,
  "predictedTokenAddress":"0x…","predictedPoolId":"0x…",
  "transaction":{"to":"0x…(factory)","data":"0x…","value":"12000000000000000"},
  "calldataHash":"0x…",
  "metadata":{"uri":"ipfs://…","hash":"0x…","trust":"content_addressed","verified":false},
  "initialBuy":{"bps":100,"estimatedTokensOut":"…","estimatedEthIn":"…","maxEthIn":"…"},
  "approxInitialFdvWei":"…","temporaryMaxWalletWei":"…","publicTradingDelaySeconds":15,
  "simulation":{"success":true,"gasEstimate":"…","warnings":[]},"expiresAt":"…" }

SDK↔API equivalence: the transaction.data here equals @boga/sdk buildLaunchTransaction(factory, params).data for the same inputs — the two build paths are continuously asserted equivalent.

GET /v1/launches/:tokenAddress

Aggregates launches (incl. immutable metadata + trust/status), pools, token_metrics (Bullseye), and the fee-settlement history (creator/platform paid per swap). When a token's metadata is unavailable, the response returns metadataSupported:false and metadata.uri:null with placeholders — metadata is never fabricated.

GET /v1/transactions/:hash

indexed (with tokenAddress) is only reported once the launch is fully projected — never before. Falls back to live chain state for confirmed/reverted/pending when the tx isn't yet indexed.

Service availability

If the chain reader is temporarily unavailable, /v1/protocol/config keeps serving protocol constants while prepare/simulate return 503 RPC_UNAVAILABLE — clients should retry or fall back to building the transaction locally with the SDK.

SDK

TypeScript SDK (@boga/sdk)

Contract-first, non-custodial SDK for launching and reading Boga tokens on Robinhood Chain. The SDK never accepts a private key, never stores a signer, and never broadcasts — it builds and parses transactions; the creator signs and sends with their own wallet.

Exports

  • BogaClient — the entrypoint with namespaces metadata, launch, transactions, protocol.
  • robinhoodChain — viem chain (id 4663).
  • Free functions (also usable without the client): buildLaunchTransaction, predictTokenAddressRequest, parseLaunchReceipt, initialBuyBpsToTokens, computePoolId / predictBogaPoolId, and the metadata primitives canonicalizeTokenMetadata / hashTokenMetadata / verifyTokenMetadata / validateTokenMetadata / buildTokenMetadata.

Offline vs API-backed

Work with no Boga API (contract-first): metadata.validate/canonicalize/hash/verify/build, launch.buildTransaction/predictPoolId/parseReceipt/initialBuyTokens, launch.predictAddress (needs an RPC reader, not the API).

API-backed (require apiUrl; optional conveniences): metadata.upload/uploadImage, launch.simulate/prepare/getStatus, transactions.getStatus/waitUntilIndexed, protocol.getConfig. Uploading through Boga is never required — self-hosted metadata is fully supported.

The prepared transaction is a plain { to, data, value } compatible with viem, ethers, Safe, smart accounts and Foundry.

Non-custodial guarantees

  • No method takes a raw private key.
  • No global signer; nothing is broadcast automatically.
  • to / value / calldata are always explicit in the returned object.
  • No dependency on frontend code.

Example — viem (Boga-hosted metadata)

import { BogaClient, robinhoodChain } from "@boga/sdk";
import { createWalletClient, createPublicClient, custom, http } from "viem";

const boga = new BogaClient({
  chain: robinhoodChain,
  apiUrl: "https://api.boga.fun",
  contracts: { factory: FACTORY, hook: HOOK },
});
const wallet = createWalletClient({ chain: robinhoodChain, transport: custom(window.ethereum) });
const pub = createPublicClient({ chain: robinhoodChain, transport: http() });
const [account] = await wallet.getAddresses();

// 1. Upload metadata (optional — or self-host and skip to step 2).
const md = await boga.metadata.upload({
  creator: account, name: "Boga Dog", symbol: "BDOG",
  description: "A community token launched on Boga.",
  imageUri: "ipfs://bafy.../image.webp",
  links: { x: "https://x.com/bogadog" },
});

// 2. Build the launch transaction (no API needed for this step).
const tx = boga.launch.buildTransaction({
  name: "Boga Dog", symbol: "BDOG",
  metadataURI: md.metadataUri, metadataHash: md.metadataHash,
  salt: `0x${"00".repeat(31)}01`, initialBuyBps: 100, maxEthIn: 12_000_000_000_000_000n,
});

// 3. Creator signs + broadcasts with their OWN wallet.
const hash = await wallet.sendTransaction({ account, to: tx.to, data: tx.data, value: tx.value });

// 4. Wait until indexed, then read the launch.
await boga.transactions.waitUntilIndexed(hash);
const receipt = await pub.getTransactionReceipt({ hash });
const launched = boga.launch.parseReceipt(receipt); // { token, poolId, metadataURI, ... }

Example — self-hosted metadata (fully offline build)

import { buildAndHashTokenMetadata, buildLaunchTransaction } from "@boga/sdk";

// Build + hash locally, pin the canonical JSON yourself (IPFS/Arweave/https).
const { metadata, canonical, metadataHash } = buildAndHashTokenMetadata({
  creator, name: "Self Hosted", symbol: "SELF",
  description: "Pinned by the creator, no Boga API involved.",
  image: "ipfs://bafy.../img.webp",
});
const metadataURI = await pinToIpfs(canonical); // your infra; must hash to metadataHash

const tx = buildLaunchTransaction(FACTORY, {
  name: metadata.name, symbol: metadata.symbol,
  metadataURI, metadataHash, salt, initialBuyBps: 0, maxEthIn: 0n,
});
// sign + send tx with any wallet

Example — ethers v6

import { buildLaunchTransaction } from "@boga/sdk";
const tx = buildLaunchTransaction(FACTORY, params); // { to, data, value: bigint }
const sent = await signer.sendTransaction({ to: tx.to, data: tx.data, value: tx.value });
await sent.wait();

Example — Foundry (cast)

# The SDK-built calldata is chain-agnostic; broadcast it with any signer.
cast send $FACTORY "$CALLDATA" --value $MAX_ETH_IN \
  --rpc-url $ROBINHOOD_RPC_URL --account my-keystore

Example — raw JSON-RPC

const tx = boga.launch.buildTransaction(params);
await provider.request({ method: "eth_sendTransaction",
  params: [{ from: account, to: tx.to, data: tx.data, value: `0x${tx.value.toString(16)}` }] });

Example — no initial buy

boga.launch.buildTransaction({ ...common, initialBuyBps: 0, maxEthIn: 0n });

Address & pool-id prediction

predictBogaPoolId(token, hook) = keccak256(abi.encode(PoolKey)) — identical to Uniswap V4 PoolId.toId. launch.predictAddress(reader, creator, params) calls BogaFactory.predictTokenAddress(creator, salt, name, symbol, metadataURI, metadataHash) via an RPC reader; the CREATE2 address is deterministic in (creator, salt).

Tests (packages/sdk/tests)

Covered: build-without-API + calldata round-trip, ethers-compatible shape, validation, bps→tokens window, address-request shape, pool-id determinism, receipt round-trip, and client wiring (offline methods, API error envelope, waitUntilIndexed polling). The SDK↔API calldata-equivalence test lands with the /v1/launches/prepare endpoint and the local e2e.

CLI

CLI (@boga/cliboga)

Safe, non-custodial developer tooling. The CLI prepares transactions; it never signs or broadcasts, and it never accepts --private-key (passing one is a hard error). You sign the prepared { to, data, value } with your own wallet, hardware wallet, Safe, or keystore.

Commands

boga metadata validate      --file <path> | --metadata <json>
boga metadata hash          --file/--metadata | (--name --symbol --description --image --creator [--website --x --telegram --discord --farcaster])
boga metadata upload        --api <url> --name --symbol --description --image-uri --creator
boga metadata upload-image  --api <url> --file <path> --content-type <mime>

boga launch prepare         --factory <addr> --name --symbol --uri --hash --salt --bps --max-eth [--chain-id] [--api]
boga launch simulate        --api <url> --creator --name --symbol --uri --hash --salt --bps --max-eth
boga launch inspect         --api <url> --token <addr>
boga launch wait            --api <url> --tx <hash>
boga launch predict-pool    --token <addr> --hook <addr>

boga protocol config        --api <url>

Offline (no API, no key): metadata validate, metadata hash, launch prepare (builds unsigned calldata), launch predict-pool. API-backed: metadata upload/upload-image, launch simulate/inspect/wait, protocol config.

Safety guarantees

  • --private-key (or --pk) → immediate PRIVATE_KEY_REJECTED error, no work done.
  • No command signs or broadcasts; launch prepare prints an UNSIGNED transaction with an explicit "Boga never signs" note.
  • launch prepare displays destination, ETH value, calldata hash, metadata hash, and network; it refuses unknown chain-ids (UNKNOWN_NETWORK).
  • Output is machine-readable JSON; errors use { "error": { "code", "message" } }.

Running (no build step)

pnpm --filter @boga/cli boga launch prepare --factory 0x… --name … --bps 100 --max-eth 12000000000000000
# or, once published/built:  npx @boga/cli boga …

bin/boga.mjs launches the TypeScript entry via tsx (resolves the workspace's .js.ts specifiers). A published build compiles src to dist and points the boga bin at dist/bin.js.

Example — prepare, then sign yourself

# 1. hash metadata (offline)
boga metadata hash --name "Boga Dog" --symbol BDOG --description "…" --image ipfs://… --creator 0x…
# 2. build the unsigned launch tx (offline)
boga launch prepare --factory 0xFACTORY --name "Boga Dog" --symbol BDOG \
  --uri ipfs://… --hash 0x<metadataHash> --salt 0x<salt> --bps 100 --max-eth 12000000000000000
# 3. sign + send the printed { to, data, value } with YOUR wallet (cast/viem/ethers/Safe)
# 4. wait for indexing, then inspect
boga launch wait --api https://api.boga.fun --tx 0x<hash>
boga launch inspect --api https://api.boga.fun --token 0x<token>

Security Model

Developer Platform — Security Model

Maps every threat in the platform threat model to the control that addresses it. Overarching invariant: Boga never receives, stores, logs or uses a creator private key, and never signs or broadcasts. No signing dependency exists in the SDK/API/CLI, and the CLI hard-rejects --private-key.

Private-key / custody

Threat Control
Private-key handling No signing anywhere; the API prepares only; the SDK returns {to,data,value} for your wallet to sign; the CLI rejects --private-key
API becomes required for launching Contract-first design; the SDK builds calldata with no API involved

Metadata integrity

Threat Control
Metadata hash mismatch keccak of the canonical document is verified against the on-chain hash; content is not trusted on mismatch
JSON key-order / whitespace differences RFC 8785 canonicalization (sorted keys, no whitespace)
Unicode ambiguity / confusables NFC normalization before validation + hashing
Duplicate JSON keys Strict parser rejects duplicates
Oversized JSON Fetch size cap (128 KiB) + API body limits
Invalid UTF-8 Fatal UTF-8 decoding — malformed bytes are rejected
Unsupported schema versions / invalid schema Schema validation with a closed version enum and no additional properties
Name/symbol impersonation The indexer compares metadata name/symbol against the on-chain values
Unavailable metadata Never blocks trading; status is unavailable and retryable

URLs / SSRF / images

Threat Control
HTML injection / XSS Descriptions are plain text (control characters rejected); HTML is never interpreted; renderers must escape
Malicious URLs / open redirects https-only social/external links; only supported schemes for image/metadata URIs
SVG execution / polyglot images Magic-byte sniffing rejects SVG/XML; only PNG/JPEG/WebP are decoded
Incorrect MIME type Extension, filename and client MIME are never trusted; magic bytes + a real decode decide
Decompression bombs / excessive dimensions Input pixel limits + explicit dimension caps
Malicious image metadata / EXIF Orientation is applied and the image re-encoded to WebP, stripping all metadata; a raw upload is never served
Oversized payloads 5 MB body limit + size guard
SSRF / private-network fetch URL safety checks block loopback, private ranges, CGNAT, link-local, IPv6-ULA, cloud-metadata hosts, embedded credentials and non-443 ports
IPFS gateway disagreement Content-addressed CIDs bind the bytes; hashes are still verified for https URIs

Launch / transaction correctness

Threat Control
Chain ID mismatch Validated at prepare time; the SDK is bound to chain 4663
Wrong deployment addresses Configuration comes from a single validated source, served at /v1/protocol/config
Calldata mismatch (API vs SDK) Both build the transaction through the same code path; equivalence is continuously asserted
Stale simulations Simulations return blockNumber + expiresAt
Salt / CREATE2 collisions The salt is derived per-creator (keccak of creator + salt); reuse reverts
Incorrect receipt parsing Typed decoding of the TokenLaunched event
Duplicate indexing / reorgs Idempotent projection keyed by (chain, tx, log) with deterministic replay
Incorrect economics The launch enforces supply, fees, Bullseye, max-wallet, delay and the LP burn exactly
Liquidity could be withdrawn, moved, or rugged The launch position is burned to a dead address; ownerOf(position) == 0xdead is the entire custody invariant — no contract can withdraw, move, or unlock it, and there is no locker to trust
Malicious/compromised treasury raises the platform fee share The setter is treasury-gated AND hard-capped at the launch terms (3000 bps — the creator's 70% floor cannot be crossed by any governance path); every change emits an on-chain event; shares already paid out are never re-split
Creator-fee rights hijack or typo-burn on transfer Two-step propose/accept (only the current creator proposes, only the proposed address accepts); a pending proposal grants nothing; both steps emit events
A fee payout is blocked (restricted/blacklisted address, non-payable contract) Fees are paid by a push that can never revert the trade: a blocked share is parked in the hook's escrow and the trade still completes, so a hostile or restricted receiver cannot brick a market. For ETH- and USDG-quoted pools the push runs in the same transaction as every trade; for a newly-listed stock quote it runs on the next trade or a permissionless sweep. The parked balance is released once the beneficiary rotates to a receivable payout address (creator transfer / treasury rotation). Native payouts use WETH, whose transfer runs no receiver code
Fee collection could stall the first trade of a pool with no reserves The hook pays the fee inside the swap when the pool manager holds the quote — which for ETH and USDG pools it always does (deep shared reserves), so those fees settle in the same transaction as every trade. When the manager does not hold the quote — a newly-listed stock quote where Boga is the first venue — the hook instead accrues the fee as an ERC-6909 claim (a mint, which moves no real funds and cannot fail for lack of reserves), and the next trade (or a permissionless sweep) realizes it, so trading is always live regardless of pool depth. Conservation is skimmed ≡ pushed + unrealized + owed per token/currency, where unrealized is the accrued-but-not-yet-realized claim (≈0 in steady state; only transiently non-zero for a brand-new stock quote) and owed is a blocked payout (exceptional)

API abuse

Threat Control
API replay / rate-limit bypass Global per-IP rate limiting; idempotency keys are documented
Idempotency collisions Content-addressed storage — the same bytes always map to the same CID
Stack-trace / internal leakage Stable {error:{code,message,details,requestId}} envelope; stack traces are never returned

Contract immutability

Deployed Boga contracts are immutable — nothing is ever mutated in place. New behaviour ships as new contracts, and historical deployments and their on-chain records are preserved unchanged.

Residual / operational

  • Hosted metadata and images are served from persistent, content-addressed storage; fetches are SSRF-guarded at request time.
  • The on-chain launch (sign + broadcast) happens entirely in the creator's wallet; Boga's only chain interaction is read-only (views, gas estimates, receipts).

OpenAPI Reference

Machine-readable spec: https://api.boga.fun/openapi.json.

Token metadata schema (v1)

{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://docs.boga.fun/schemas/token-metadata-v1.json",
  "title": "Boga Token Metadata v1",
  "description": "Immutable launch metadata for a Boga token on Robinhood Chain (chainId 4663). The keccak256 of the RFC 8785 canonicalization of a document conforming to this schema is the on-chain metadataHash. See docs/developer-platform/METADATA.md.",
  "type": "object",
  "additionalProperties": false,
  "required": [
    "schema",
    "schemaVersion",
    "name",
    "symbol",
    "description",
    "image",
    "properties"
  ],
  "properties": {
    "schema": {
      "type": "string",
      "const": "https://docs.boga.fun/schemas/token-metadata-v1.json"
    },
    "schemaVersion": {
      "type": "string",
      "enum": ["1.0"]
    },
    "name": {
      "type": "string",
      "minLength": 1,
      "maxLength": 64
    },
    "symbol": {
      "type": "string",
      "minLength": 1,
      "maxLength": 16
    },
    "description": {
      "type": "string",
      "minLength": 10,
      "maxLength": 500
    },
    "image": {
      "type": "string",
      "minLength": 1,
      "maxLength": 2048,
      "description": "Content-addressed or https image URI. Supported schemes: ipfs://, ar://, https://."
    },
    "external_url": {
      "type": "string",
      "maxLength": 2048,
      "description": "Optional https URL for the token's canonical page."
    },
    "links": {
      "type": "object",
      "additionalProperties": false,
      "properties": {
        "website": { "type": ["string", "null"], "maxLength": 2048 },
        "x": { "type": ["string", "null"], "maxLength": 2048 },
        "telegram": { "type": ["string", "null"], "maxLength": 2048 },
        "discord": { "type": ["string", "null"], "maxLength": 2048 },
        "farcaster": { "type": ["string", "null"], "maxLength": 2048 }
      }
    },
    "properties": {
      "type": "object",
      "additionalProperties": false,
      "required": ["creator", "chainId", "platform", "version"],
      "properties": {
        "creator": {
          "type": "string",
          "pattern": "^0x[0-9a-fA-F]{40}$"
        },
        "chainId": {
          "type": "integer",
          "const": 4663
        },
        "platform": {
          "type": "string",
          "const": "Boga"
        },
        "version": {
          "type": "integer",
          "const": 1
        }
      }
    }
  }
}

boga-public-api.yaml

openapi: 3.1.0
info:
  title: Boga Public API
  version: "1.0.0"
  description: |
    Public, read-and-prepare API for the Boga launchpad on Robinhood Chain
    (chainId 4663).

    **Boga never signs or broadcasts.** The `prepare`/`simulate` endpoints build
    and validate an unsigned transaction and read chain state; the creator signs
    and broadcasts with their own wallet. No endpoint accepts, stores or logs a
    private key. Direct smart-contract interaction is always possible without
    this API.

    All monetary values are decimal strings of wei (exact integers, no floats).
  license:
    name: MIT
servers:
  - url: https://api.boga.fun
    description: Production
tags:
  - name: Protocol
  - name: Launches
  - name: Transactions
  - name: Metadata
  - name: Status
  - name: Tokens
  - name: Wallets
  - name: Discovery
  - name: Points
  - name: Auth
  - name: Watchlist
  - name: Stream
paths:
  /v1/protocol/config:
    get:
      tags: [Protocol]
      summary: Public protocol configuration
      description: Versioned protocol constants and deployed contract addresses.
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ProtocolConfig" }
  /v1/launches/simulate:
    post:
      tags: [Launches]
      summary: Simulate a launch (no signing)
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PrepareRequest" }
      responses:
        "200":
          description: Simulation result (no transaction object).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/SimulateResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /v1/launches/prepare:
    post:
      tags: [Launches]
      summary: Prepare an unsigned launch transaction
      description: |
        Returns a plain `{ to, data, value }` transaction compatible with viem,
        ethers, Safe, smart accounts and Foundry. The API does not send it. The
        `transaction.data` equals `@boga/sdk` `buildLaunchTransaction(...)` for
        the same inputs.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/PrepareRequest" }
      responses:
        "200":
          description: Prepared transaction + simulation.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/PrepareResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "502": { $ref: "#/components/responses/UpstreamError" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /v1/transactions/{transactionHash}:
    get:
      tags: [Transactions]
      summary: Transaction / indexing status
      parameters:
        - name: transactionHash
          in: path
          required: true
          schema: { type: string, pattern: "^0x[0-9a-fA-F]{64}$" }
      responses:
        "200":
          description: Status. `indexed` only once all projections are committed.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TransactionStatus" }
        "400": { $ref: "#/components/responses/BadRequest" }
  /v1/launches/{tokenAddress}:
    get:
      tags: [Launches]
      summary: Full launch status
      parameters:
        - name: tokenAddress
          in: path
          required: true
          schema: { type: string, pattern: "^0x[0-9a-fA-F]{40}$" }
      responses:
        "200":
          description: Launch record incl. immutable metadata, pool, bullseye, and settled fees.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/LaunchStatus" }
        "404": { $ref: "#/components/responses/NotFound" }
  /v1/metadata:
    post:
      tags: [Metadata]
      summary: Upload a metadata document (Boga-hosted; optional)
      description: |
        Optional convenience. Self-hosted metadata (ipfs/ar/https) is fully
        supported and never requires this endpoint. Validates + canonicalizes +
        hashes and stores content-addressed.
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/MetadataUploadRequest" }
      responses:
        "200":
          description: Stored metadata URI + hash + canonical document.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/MetadataUploadResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /v1/metadata/images:
    post:
      tags: [Metadata]
      summary: Upload + process an image (Boga-hosted; optional)
      description: |
        Validates magic bytes, decodes, strips EXIF, re-encodes, generates a
        square thumbnail and content-addresses the output. PNG/JPEG/WebP only;
        no SVG; no animation (MVP). Self-hosted images remain supported.
      requestBody:
        required: true
        content:
          image/png: { schema: { type: string, format: binary } }
          image/jpeg: { schema: { type: string, format: binary } }
          image/webp: { schema: { type: string, format: binary } }
      responses:
        "200":
          description: Processed image descriptor.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ImageUploadResponse" }
        "400": { $ref: "#/components/responses/BadRequest" }
        "413": { $ref: "#/components/responses/PayloadTooLarge" }
        "415": { $ref: "#/components/responses/UnsupportedMedia" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /v1/metadata/content/{cid}:
    get:
      tags: [Metadata]
      summary: Fetch Boga-hosted metadata/image content by CID
      description: Content-addressed retrieval for uploads hosted by Boga (optional subsystem).
      parameters:
        - { name: cid, in: path, required: true, schema: { type: string } }
      responses:
        "200": { description: The stored content (JSON metadata or image bytes). }
        "404": { $ref: "#/components/responses/NotFound" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /health:
    get:
      tags: [Status]
      summary: Service health (indexer freshness included)
      responses:
        "200":
          description: ok or degraded (still serving; on DB failure the request errors).
          content:
            application/json:
              schema:
                allOf:
                  - type: object
                    properties: { status: { type: string, enum: [ok, degraded] } }
                  - { $ref: "#/components/schemas/StatusPayload" }
  /v1/status:
    get:
      tags: [Status]
      summary: Indexer/projection status and degraded-mode notice (PRD 13.7)
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/StatusPayload" }
  /v1/stats:
    get:
      tags: [Status]
      summary: Platform-wide aggregates (all-time + rolling 24h)
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/StatsResponse" }
  /v1/quotes:
    get:
      tags: [Protocol]
      summary: Allowed pool quote assets (the multisig-governed launch allowlist)
      description: |
        The multisig-governed set a launch may pair against, scoped to the
        configured chain. Ordered for a pairing picker: native ETH first, then
        USDG, then tokenized stocks alphabetically by symbol. Empty until the
        first allow event is indexed.
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  quotes: { type: array, items: { $ref: "#/components/schemas/QuoteAsset" } }
  /v1/tokens:
    get:
      tags: [Tokens]
      summary: Token discovery list
      description: |
        `sort=new` (default) orders by launch block; `sort=bullseye` ranks by
        global peak net buy; `sort=momentum` returns only tokens with an
        available momentum score (never fabricated).
      parameters:
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 100, default: 25 } }
        - { name: sort, in: query, schema: { type: string, enum: [new, bullseye, momentum], default: new } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/TokenListItem" } }
        "400": { $ref: "#/components/responses/BadRequest" }
  /v1/tokens/{address}:
    get:
      tags: [Tokens]
      summary: Token detail (launch + pool + metrics + momentum)
      parameters: [{ $ref: "#/components/parameters/TokenAddress" }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema: { $ref: "#/components/schemas/TokenDetail" }
        "404": { $ref: "#/components/responses/NotFound" }
  /v1/tokens/{address}/trades:
    get:
      tags: [Tokens]
      summary: Trades for a token (cursor-paginated, newest first)
      parameters:
        - { $ref: "#/components/parameters/TokenAddress" }
        - { $ref: "#/components/parameters/Limit" }
        - { name: cursor, in: query, schema: { type: string, pattern: "^\\d+:\\d+$" }, description: "block:logIndex of the last seen row" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/Swap" } }
                  nextCursor: { type: [string, "null"] }
  /v1/tokens/{address}/holders:
    get:
      tags: [Tokens]
      summary: Holder list + count (via Blockscout; degrades to empty on explorer error)
      parameters: [{ $ref: "#/components/parameters/TokenAddress" }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  holdersCount: { type: [integer, "null"] }
                  holders:
                    type: array
                    items:
                      type: object
                      properties:
                        address: { $ref: "#/components/schemas/Address" }
                        value: { $ref: "#/components/schemas/WeiString" }
  /v1/tokens/{address}/fees:
    get:
      tags: [Tokens]
      summary: Fee accrual & settlement for a token (generated, unrealized, realized totals, escrow owed)
      description: |
        Swap-time fee settlement with conditional take. The 1% quote-side fee is taken and PAID in the same swap
        whenever the pool manager holds the quote — native (ETH) and USDG pools
        always do — emitting FeeAccrued + FeePaid together. For a brand-new stock
        quote with no prior liquidity the fee is minted as an unattributed claim
        (FeeAccrued → `kind=accrued`) and realized on the next qualifying trade or
        a permissionless `sweep`, emitting FeePaid/FeeParked/Swept. There is no
        claim step. The
        response separates the accrual dimensions; per currency the invariant is
        `generated ≡ (totals.creator + totals.platform) + unrealized`:
          * `generated` — gross fee accrued per currency (the honest total the
            token produced). Accrual is unattributed, so no beneficiary split here.
          * `unrealized` — the transient slice accrued but not yet realized
            (accrued − paid − parked); ≈0 in steady state (native/USDG pay
            in-swap), non-empty only for a cold stock quote's first fee.
          * `totals` — the REALIZED view: creator/platform shares per currency
            (paid + parked-to-escrow; swept not re-counted).
          * `totalsUsd` / `totalUsd1e8` / `unpriced` — the historical-USD twin
            of `totals` (additive; older deployments omit all three): the SAME
            realized rows, each valued at its own contemporaneous oracle price
            fixed at projection (fee_settlements.usd_1e8). Rows unpriced at
            projection contribute 0 and are counted in `unpriced` — the USD
            figures are partial by exactly that many settlements, never faked.
          * `owed` — escrow still pending per currency/beneficiary (parked −
            swept), non-empty only when a payout was blocked and not yet released.
          * `settlements` — the most recent REALIZED rows (paid/parked/swept, each
            carrying a beneficiary; accrual rows excluded), newest first, bounded
            by `limit`.
      parameters:
        - { $ref: "#/components/parameters/TokenAddress" }
        - { $ref: "#/components/parameters/Limit" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  generated:
                    type: array
                    description: >-
                      Gross fee accrued (generated) per currency — Σ of FeeAccrued.
                      The honest "fees this token produced"; unattributed (accrual
                      carries no beneficiary), so no creator/platform split.
                    items:
                      type: object
                      properties:
                        currency: { $ref: "#/components/schemas/Address" }
                        amount: { $ref: "#/components/schemas/WeiString" }
                  unrealized:
                    type: array
                    description: >-
                      Accrued-but-not-yet-realized per currency (accrued − paid −
                      parked). ≈0 in steady state (native/USDG pay in-swap); only
                      transiently non-empty for a brand-new stock quote, → 0 on the
                      next trade or a sweep.
                    items:
                      type: object
                      properties:
                        currency: { $ref: "#/components/schemas/Address" }
                        amount: { $ref: "#/components/schemas/WeiString" }
                  totals:
                    type: array
                    description: >-
                      REALIZED fee totals per currency — creator/platform shares
                      (paid + parked; swept is not re-counted).
                    items:
                      type: object
                      properties:
                        currency: { $ref: "#/components/schemas/Address" }
                        creator: { $ref: "#/components/schemas/WeiString" }
                        platform: { $ref: "#/components/schemas/WeiString" }
                  totalsUsd:
                    type: array
                    description: >-
                      Historical-USD twin of `totals` (additive; older
                      deployments omit it): the SAME realized rows (kind
                      paid/parked), each valued at its own contemporaneous
                      oracle price fixed at projection (fee_settlements.usd_1e8).
                      Sums are USD ×1e8 decimal strings; rows unpriced at
                      projection contribute 0 and are counted in `unpriced`.
                    items:
                      type: object
                      properties:
                        currency: { $ref: "#/components/schemas/Address" }
                        creatorUsd1e8: { type: string }
                        platformUsd1e8: { type: string }
                  totalUsd1e8:
                    type: object
                    description: >-
                      Grand historical-USD total per beneficiary across every fee
                      currency (×1e8 decimal strings) — the Σ of `totalsUsd`.
                      Additive; older deployments omit it.
                    properties:
                      creator: { type: string }
                      platform: { type: string }
                  unpriced:
                    type: integer
                    description: >-
                      Honesty counter (additive): realized rows (kind
                      paid/parked) still carrying usd_1e8 = 0 — no oracle
                      observation at projection — among the rows the USD sums
                      cover. The USD figures are partial by exactly this many
                      settlements; they are never fabricated as $0.
                  owed:
                    type: array
                    description: Escrow still owed (parked − swept), per currency/beneficiary; empty in the healthy case.
                    items:
                      type: object
                      properties:
                        currency: { $ref: "#/components/schemas/Address" }
                        beneficiary: { type: string, enum: [creator, platform] }
                        amount: { $ref: "#/components/schemas/WeiString" }
                  settlements:
                    type: array
                    description: >-
                      Most recent REALIZED settlement rows (kind one of
                      paid/parked/swept, each with a beneficiary + toAddress;
                      accrual rows excluded), newest first, bounded by `limit`.
                    items: { $ref: "#/components/schemas/ProjectionRow" }
  /v1/tokens/{address}/metrics:
    get:
      tags: [Tokens]
      summary: Token metrics + OHLC candles
      parameters:
        - { $ref: "#/components/parameters/TokenAddress" }
        - { $ref: "#/components/parameters/Limit" }
        - { name: interval, in: query, schema: { type: string, enum: [1m, 5m, 15m, 1h, 4h], default: 1h } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  metrics: { $ref: "#/components/schemas/ProjectionRow" }
                  candles: { type: array, items: { $ref: "#/components/schemas/ProjectionRow" } }
        "404": { $ref: "#/components/responses/NotFound" }
  /v1/sparklines:
    get:
      tags: [Tokens]
      summary: Batch tick series for card sparklines
      description: >-
        Last N swap spot prices per token (quote-raw per whole token — the exact
        integers candle prices use), oldest first. One window-function query
        serves a whole discovery grid; tokens without swaps are absent from
        `series`.
      parameters:
        - name: tokens
          in: query
          required: true
          schema: { type: string }
          description: Comma-separated token addresses (1-50).
        - { name: limit, in: query, schema: { type: integer, minimum: 2, maximum: 100, default: 60 } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [series]
                properties:
                  series:
                    type: object
                    additionalProperties: { type: array, items: { type: string } }
        "400": { $ref: "#/components/responses/BadRequest" }
  /v1/tokens/{address}/same-ticker:
    get:
      tags: [Tokens]
      summary: Other tokens sharing this ticker (impersonation warning)
      parameters: [{ $ref: "#/components/parameters/TokenAddress" }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/ProjectionRow" } }
                  warning: { type: [string, "null"] }
        "404": { $ref: "#/components/responses/NotFound" }
  /v1/activity/recent:
    get:
      tags: [Status]
      summary: Recent activity backfill for the live ticker
      description: >-
        Latest projected swaps, launches and bullseyes as stream-shaped frames
        (same wire format as /v1/stream events) with chain timestamps, newest
        first. Seeds the home ticker; the SSE stream appends live on top.
      parameters:
        - { name: limit, in: query, schema: { type: integer, minimum: 1, maximum: 50, default: 20 } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [events]
                properties:
                  events:
                    type: array
                    items: { $ref: "#/components/schemas/ProjectionRow" }
  /v1/analytics/daily:
    get:
      tags: [Status]
      summary: Platform analytics — rolling 24h windows, all-time rollup, and the hourly/daily series
      description: >-
        Volume is CROSS-QUOTE USD (×1e8): every swap is valued at the oracle price
        AT-OR-BEFORE its own block (never today's spot) and summed, so USDG- and
        stock-quoted pools are included. `volumeEthWei` survives on the daily
        buckets as the ETH-pool SUBSET only (structurally 0 on non-native pools).

        `last24h` and `prior24h` are ROLLING wall-clock windows, half-open
        [from, to) in chain-timestamp seconds: the lower bound is inclusive, the
        upper exclusive, so the two tile exactly and no swap is counted twice.
        The live window is not capped in practice — its `to` is the instant of
        the query, and a block stamped marginally ahead of the server clock is
        still counted rather than dropped.
        `hours` holds 24 zero-filled CLOCK-hour buckets ending on the hour that
        contains `now` — it therefore starts up to one hour after `last24h.from`,
        which makes `last24h` the authoritative 24-hour figure. `days` holds
        zero-filled UTC-day buckets, newest last, the last one still filling.
      parameters:
        - { name: days, in: query, schema: { type: integer, minimum: 7, maximum: 30, default: 14 } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                required: [days, hours, last24h, prior24h, allTime]
                properties:
                  days:
                    type: array
                    items:
                      type: object
                      required: [dayStart, volumeEthWei, volumeUsd1e8, trades, launches]
                      properties:
                        dayStart: { type: string }
                        volumeEthWei:
                          type: string
                          description: ETH principal of ETH-quoted pools only — a subset of volumeUsd1e8's coverage.
                        volumeUsd1e8: { type: string }
                        trades: { type: integer }
                        launches: { type: integer }
                  hours:
                    type: array
                    description: 24 clock-hour buckets (UTC), zero-filled, newest last.
                    items:
                      type: object
                      required: [hourStart, volumeUsd1e8, trades, launches]
                      properties:
                        hourStart: { type: string }
                        volumeUsd1e8: { type: string }
                        trades: { type: integer }
                        launches: { type: integer }
                  last24h: { $ref: "#/components/schemas/AnalyticsWindow" }
                  prior24h: { $ref: "#/components/schemas/AnalyticsWindow" }
                  allTime: { $ref: "#/components/schemas/AnalyticsWindow" }
  /v1/wallets/{address}/holdings:
    get:
      tags: [Wallets]
      summary: Token holdings of a wallet (positive balances, largest first)
      parameters:
        - { $ref: "#/components/parameters/WalletAddress" }
        - { $ref: "#/components/parameters/Limit" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/ProjectionRow" } }
  /v1/wallets/{address}/activity:
    get:
      tags: [Wallets]
      summary: Swap activity of a wallet (newest first)
      parameters:
        - { $ref: "#/components/parameters/WalletAddress" }
        - { $ref: "#/components/parameters/Limit" }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/Swap" } }
  /v1/wallets/{address}/points:
    get:
      tags: [Points]
      summary: Bounty Points balance + ledger history for a wallet
      parameters: [{ $ref: "#/components/parameters/WalletAddress" }]
      responses:
        "200":
          description: OK (includes the mandatory no-monetary-value disclaimer).
          content:
            application/json:
              schema:
                type: object
                properties:
                  wallet: { $ref: "#/components/schemas/Address" }
                  confirmedUnits: { $ref: "#/components/schemas/WeiString" }
                  pendingUnits: { $ref: "#/components/schemas/WeiString" }
                  history: { type: array, items: { $ref: "#/components/schemas/ProjectionRow" } }
                  disclaimer: { type: string }
  /v1/leaderboards/points:
    get:
      tags: [Points]
      summary: Points leaderboard (confirmed units, descending)
      parameters: [{ $ref: "#/components/parameters/Limit" }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  items: { type: array, items: { $ref: "#/components/schemas/ProjectionRow" } }
  /v1/search:
    get:
      tags: [Discovery]
      summary: Search by token address, tx hash, wallet, or name/symbol text — or browse the visible index
      description: >-
        An empty (or absent) `q` is BROWSE MODE: the text branch with no
        name/symbol predicate, so a client can page the whole visible index
        (the Cmd+K spotlight's zero-query state). `sort`, `age`, `quoteKind`
        and the pagination params apply to the text/browse branch only; the
        exact branches (tx hash, token address, creator wallet) keep their
        original behavior and shapes. Text/browse responses attach each
        token's `metrics` (token_metrics join, per-pool Bullseye threshold
        embedded) and `topHolderBps`, exactly like `GET /v1/tokens`, plus the
        additive `total`/`page`/`pageSize` trio. `sort=relevance` ranks exact
        symbol > exact name > prefix > substring (newest as tiebreak) and
        falls back to newest when `q` is empty; `mcap`/`volume` sort by the
        oracle USD columns with unpriced ("0") rows last — never fabricated.
        `sort=momentum` is a TOTAL order usable as a one-call trending feed:
        momentum-scored tokens first (score desc — the same scorer as
        `GET /v1/tokens?sort=momentum`), then the scoreless remainder by USD
        volume desc (unpriced last), launch block desc as the final tiebreak
        in both segments — in browse mode and under a query alike.
        LIKE wildcards in `q` are escaped: `%` and `_` match literally.
      parameters:
        - { name: q, in: query, required: false, schema: { type: string, maxLength: 100 } }
        - name: sort
          in: query
          required: false
          schema: { type: string, enum: [relevance, new, oldest, mcap, volume, momentum], default: relevance }
        - name: age
          in: query
          required: false
          description: Launch-age window over `launchTimestamp`.
          schema: { type: string, enum: [all, 24h, 7d], default: all }
        - name: quoteKind
          in: query
          required: false
          schema: { type: string, enum: [native_eth, usdg, stock] }
        - { name: page, in: query, required: false, schema: { type: integer, minimum: 1, default: 1 } }
        - { name: pageSize, in: query, required: false, schema: { type: integer, minimum: 1, maximum: 50, default: 24 } }
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  kind: { type: string, enum: [tx, token, wallet, text] }
                  tokens: { type: array, items: { $ref: "#/components/schemas/ProjectionRow" } }
                  wallet: { $ref: "#/components/schemas/Address" }
                  total:
                    type: integer
                    description: >-
                      Text/browse branch only (additive): rows matching the
                      predicate across ALL pages — hidden launches excluded,
                      same as the rows themselves.
                  page: { type: integer, description: "Text/browse branch only (additive)." }
                  pageSize: { type: integer, description: "Text/browse branch only (additive)." }
        "400": { $ref: "#/components/responses/BadRequest" }
  /v1/creators/{address}:
    get:
      tags: [Discovery]
      summary: Creator reputation aggregates (fees + eligible volume)
      parameters: [{ $ref: "#/components/parameters/WalletAddress" }]
      responses:
        "200":
          description: OK
          content:
            application/json:
              schema:
                type: object
                properties:
                  creatorFeesEthWei: { $ref: "#/components/schemas/WeiString" }
                  creatorFeesUsd1e8:
                    type: string
                    description: >-
                      Historical-USD creator fees across EVERY pool (native,
                      USDG, stock), USD ×1e8 decimal string (additive; older
                      deployments omit it): the realized creator-share
                      settlements (kind paid/parked) paid to this wallet, each
                      valued at its own contemporaneous oracle price fixed at
                      projection (fee_settlements.usd_1e8). Rows unpriced at
                      projection contribute 0 and are counted in
                      `creatorFeesUnpriced` — the figure is partial by exactly
                      those rows, never fabricated. Wider coverage than
                      `creatorFeesEthWei`, whose wei unit restricts it to the
                      native fee currencies.
                  creatorFeesUnpriced:
                    type: integer
                    description: >-
                      Honesty counter (additive): realized creator-share rows
                      still carrying usd_1e8 = 0 — no oracle observation at
                      projection — among the rows `creatorFeesUsd1e8` sums.
                      The USD figure is partial by exactly this many
                      settlements; it is never fabricated as $0.
                  eligibleVolumeUsd1e8:
                    type: string
                    description: >-
                      Cross-quote eligible volume in USD ×1e8 (decimal string),
                      summed across the creator's launches. Each swap was valued
                      at its contemporaneous oracle price when indexed; excludes
                      creator-self, initial-buy and round-trip trades, and swaps
                      whose quote had no USD observation yet — never fabricated.
  /v1/auth/nonce:
    get:
      tags: [Auth]
      summary: Issue a sign-in challenge for a wallet (SIWE-lite)
      description: |
        Stateless HMAC nonce (5-minute expiry) embedded in the exact message the
        wallet must sign. Free signature — no transaction, no gas. 503 until the
        deployment configures auth (clients then keep a local-only watchlist).
      parameters:
        - { name: wallet, in: query, required: true, schema: { $ref: "#/components/schemas/Address" } }
      responses:
        "200":
          description: Challenge issued.
          content:
            application/json:
              schema:
                type: object
                properties:
                  nonce: { type: string }
                  message: { type: string, description: "Sign this exact text." }
        "400": { $ref: "#/components/responses/BadRequest" }
        "503": { $ref: "#/components/responses/Unavailable" }
  /v1/auth/verify:
    post:
      tags: [Auth]
      summary: Verify the signed challenge and mint a session token
      description: |
        Locally verifies the EOA signature (no RPC; ERC-1271 smart accounts are
        not supported for this convenience feature). Returns a Bearer token
        (12 h) whose wallet gates watchlist writes.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [message, signature]
              properties:
                message: { type: string }
                signature: { type: string, pattern: "^0x[0-9a-fA-F]{130}$" }
      responses:
        "200":
          description: Session minted.
          content:
            application/json:
              schema:
                type: object
                properties:
                  token: { type: string }
                  wallet: { $ref: "#/components/schemas/Address" }
                  expiresAt: { type: integer, description: "Unix ms." }
        "401":
          description: MALFORMED_MESSAGE | BAD_NONCE | BAD_SIGNATURE.
          content: { application/json: { schema: { type: object, properties: { error: { type: string } } } } }
        "503": { $ref: "#/components/responses/Unavailable" }
  /v1/watchlist:
    post:
      tags: [Watchlist]
      summary: Add a token to the session wallet's watchlist (momentum interest input)
      description: |
        Requires a Bearer session from /v1/auth/verify — the watched wallet is
        always taken from the session, never from the request body. Idempotent —
        the first watch time is kept.
      security: [{ walletSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WatchlistRequest" }
      responses:
        "200": { description: OK, content: { application/json: { schema: { type: object, properties: { ok: { type: boolean } } } } } }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { description: Missing/expired session. }
        "503": { $ref: "#/components/responses/Unavailable" }
    delete:
      tags: [Watchlist]
      summary: Remove a token from the session wallet's watchlist
      security: [{ walletSession: [] }]
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/WatchlistRequest" }
      responses:
        "200": { description: OK, content: { application/json: { schema: { type: object, properties: { ok: { type: boolean } } } } } }
        "400": { $ref: "#/components/responses/BadRequest" }
        "401": { description: Missing/expired session. }
        "503": { $ref: "#/components/responses/Unavailable" }
  /v1/stream:
    get:
      tags: [Stream]
      summary: Live event stream (Server-Sent Events)
      description: |
        `text/event-stream` of `launch`, `swap`, `settlement` and `bullseye` events,
        after an initial `hello`. Events are produced by the API's stream
        publisher polling the indexer's projections, so delivery
        lags the chain by the projection depth plus up to ~2 s. The stream is a
        convenience feed, not an onchain truth source: clients receive events
        from subscribe-time forward (no replay), and events projected while the
        API is down are skipped.

        A `settlement` event carries one fee_settlements row. Its `kind` is one of
        `accrued` (fee GENERATED at the swap — an unattributed ERC-6909 claim, so
        `beneficiary` and `toAddress` are null), or
        `paid` / `parked` / `swept` (REALIZATION — `beneficiary` is creator/platform
        and `toAddress` the recipient).
      responses:
        "200":
          description: SSE stream.
          content:
            text/event-stream:
              schema: { type: string }
components:
  parameters:
    TokenAddress:
      name: address
      in: path
      required: true
      schema: { $ref: "#/components/schemas/Address" }
      description: Token contract address (case-insensitive).
    WalletAddress:
      name: address
      in: path
      required: true
      schema: { $ref: "#/components/schemas/Address" }
      description: Wallet address (case-insensitive).
    Limit:
      name: limit
      in: query
      schema: { type: integer, minimum: 1, maximum: 100, default: 25 }
  responses:
    BadRequest:
      description: Invalid request.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    NotFound:
      description: Not found.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    Unavailable:
      description: A required backend (RPC / storage) is not configured.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    UpstreamError:
      description: Upstream chain read failed.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    PayloadTooLarge:
      description: Image exceeds the maximum size.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
    UnsupportedMedia:
      description: Unsupported image type.
      content: { application/json: { schema: { $ref: "#/components/schemas/Error" } } }
  schemas:
    Error:
      type: object
      description: Stable error envelope. Never contains a stack trace.
      properties:
        error:
          type: object
          required: [code, message]
          properties:
            code:
              type: string
              examples: [METADATA_HASH_MISMATCH, INVALID_LAUNCH_PARAMS, CHAIN_ID_MISMATCH, RPC_UNAVAILABLE, LAUNCH_NOT_FOUND]
            message: { type: string }
            details: { type: object }
            requestId: { type: string }
    AnalyticsWindow:
      type: object
      description: >-
        One analytics window. `volumeUsd1e8` spans every quote kind (the sum of
        each swap's contemporaneous USD). `unpricedTrades` counts trades the
        oracle could not value at their block: they ARE in `trades` but
        contribute no volume, so a client can disclose the gap instead of
        silently under-reporting. `from`/`to` are present on rolling windows
        only — half-open [from, to), chain-timestamp seconds.
      required: [volumeUsd1e8, trades, launches, uniqueTraders, unpricedTrades]
      properties:
        from: { type: string }
        to: { type: string }
        volumeUsd1e8: { type: string }
        trades: { type: integer }
        launches: { type: integer }
        uniqueTraders:
          type: integer
          description: Distinct swap beneficiaries in the window.
        unpricedTrades: { type: integer }
    Address:
      type: string
      pattern: "^0x[0-9a-fA-F]{40}$"
    Bytes32:
      type: string
      pattern: "^0x[0-9a-fA-F]{64}$"
    WeiString:
      type: string
      pattern: "^[0-9]+$"
    ProtocolConfig:
      type: object
      properties:
        chainId: { type: integer, const: 4663 }
        protocolVersion: { type: string }
        deploymentBlock: { type: integer }
        contracts:
          type: object
          properties:
            factory: { $ref: "#/components/schemas/Address" }
            launchRouter: { $ref: "#/components/schemas/Address" }
            launchRegistry: { $ref: "#/components/schemas/Address" }
            hook: { $ref: "#/components/schemas/Address" }
        uniswap: { type: object, additionalProperties: { $ref: "#/components/schemas/Address" } }
        launch:
          type: object
          properties:
            totalSupply: { $ref: "#/components/schemas/WeiString" }
            poolFee:
              type: integer
              examples: [0]
              description: >-
                Uniswap V4 static LP fee of the official pool, in pips. 0 by
                design: the 1% moved out of pool math into the hook skim (see
                swapFeeBps). The pool's LP position is burned to 0xdead.
            swapFeeBps:
              type: integer
              examples: [100]
              description: >-
                Total swap fee in basis points, skimmed quote-side by the hook and
                settled to creator/platform on every swap. Equals the
                retired 1% LP fee, so the trader's total cost is unchanged.
            initialCreatorFeeShareBps: { type: integer, examples: [7000] }
            platformFeeShareMaxBps: { type: integer, examples: [3000] }
            bullseyeThresholdWei: { $ref: "#/components/schemas/WeiString" }
            initialBuyMinBps: { type: integer, examples: [100] }
            initialBuyMaxBps: { type: integer, examples: [200] }
            temporaryMaxWalletBps: { type: integer, examples: [200] }
            publicTradingDelaySeconds: { type: integer, examples: [15] }
        metadata:
          type: object
          properties:
            supportedSchemaVersions: { type: array, items: { type: string } }
            supportedUriSchemes: { type: array, items: { type: string } }
    PrepareRequest:
      type: object
      required: [creator, name, symbol, metadataUri, metadataHash, salt, initialBuyBps, maxEthIn]
      properties:
        creator: { $ref: "#/components/schemas/Address" }
        name: { type: string, minLength: 1, maxLength: 64 }
        symbol: { type: string, minLength: 1, maxLength: 16 }
        metadataUri: { type: string, maxLength: 2048 }
        metadataHash: { $ref: "#/components/schemas/Bytes32" }
        salt: { $ref: "#/components/schemas/Bytes32" }
        initialBuyBps: { type: integer, description: "0, or 100-200 bps of supply" }
        maxEthIn: { $ref: "#/components/schemas/WeiString" }
        creatorFeeRecipient:
          allOf: [{ $ref: "#/components/schemas/Address" }]
          description: "Optional recipient of the creator's share of swap fees (settled automatically per swap); omitted -> the launching wallet"
        chainId: { type: integer }
    SimulateResponse:
      type: object
      properties:
        chainId: { type: integer }
        blockNumber: { type: integer }
        predictedTokenAddress: { $ref: "#/components/schemas/Address" }
        predictedPoolId: { $ref: "#/components/schemas/Bytes32" }
        metadata:
          type: object
          properties:
            uri: { type: string }
            hash: { $ref: "#/components/schemas/Bytes32" }
            trust: { type: string }
            verified: { type: boolean }
        initialBuy:
          type: object
          properties:
            bps: { type: integer }
            estimatedTokensOut: { $ref: "#/components/schemas/WeiString" }
            estimatedEthIn: { $ref: "#/components/schemas/WeiString" }
            maxEthIn: { $ref: "#/components/schemas/WeiString" }
        creatorFeeRecipient:
          allOf: [{ $ref: "#/components/schemas/Address" }]
          description: "Optional recipient of the creator's share of swap fees (settled automatically per swap); omitted -> the launching wallet"
        approxInitialFdvWei: { $ref: "#/components/schemas/WeiString" }
        temporaryMaxWalletWei: { $ref: "#/components/schemas/WeiString" }
        publicTradingDelaySeconds: { type: integer }
        simulation:
          type: object
          properties:
            success: { type: boolean }
            gasEstimate: { type: [string, "null"] }
            warnings: { type: array, items: { type: string } }
        expiresAt: { type: string, format: date-time }
    PrepareResponse:
      allOf:
        - $ref: "#/components/schemas/SimulateResponse"
        - type: object
          properties:
            transaction:
              type: object
              properties:
                to: { $ref: "#/components/schemas/Address" }
                data: { type: string }
                value: { $ref: "#/components/schemas/WeiString" }
            calldataHash: { $ref: "#/components/schemas/Bytes32" }
    TransactionStatus:
      type: object
      properties:
        transactionHash: { $ref: "#/components/schemas/Bytes32" }
        status: { type: string, enum: [pending, confirmed, reverted, replaced, indexed] }
        blockNumber: { type: [integer, "null"] }
        confirmations: { type: integer }
        tokenAddress: { oneOf: [ { $ref: "#/components/schemas/Address" }, { type: "null" } ] }
        replacementTransactionHash: { type: [string, "null"] }
        error: { type: [string, "null"] }
        indexedAt: { type: [string, "null"] }
    LaunchStatus:
      type: object
      properties:
        tokenAddress: { $ref: "#/components/schemas/Address" }
        creator: { $ref: "#/components/schemas/Address" }
        name: { type: string }
        symbol: { type: string }
        metadataSupported: { type: boolean }
        metadata:
          type: object
          properties:
            uri: { type: [string, "null"] }
            hash: { $ref: "#/components/schemas/Bytes32" }
            status: { type: string, enum: [pending, valid, unavailable, invalid_schema, hash_mismatch, unsafe_image, mutable_uri] }
            trust: { type: [string, "null"], enum: [content_addressed, hash_verified, mutable_uri, hash_mismatch, unavailable, invalid, null] }
            schemaVersion: { type: [string, "null"] }
            image: { type: [string, "null"] }
            thumbnail: { type: [string, "null"] }
            description: { type: [string, "null"] }
            links: { type: object }
        pool: { type: [object, "null"] }
        hook: { oneOf: [ { $ref: "#/components/schemas/Address" }, { type: "null" } ] }
        launch: { type: object }
        bullseye: { type: [object, "null"] }
        creatorFees: { type: array, items: { type: object } }
        explorer: { type: [object, "null"] }
        protocolVersion: { type: string }
    MetadataUploadRequest:
      type: object
      required: [name, symbol, description, imageUri, creator]
      properties:
        name: { type: string, minLength: 1, maxLength: 64 }
        symbol: { type: string, minLength: 1, maxLength: 16 }
        description: { type: string, minLength: 10, maxLength: 500 }
        imageUri: { type: string, maxLength: 2048 }
        creator: { $ref: "#/components/schemas/Address" }
        chainId: { type: integer }
        externalUrl: { type: string, maxLength: 2048 }
        links: { type: object }
    MetadataUploadResponse:
      type: object
      properties:
        metadataUri: { type: string }
        metadataHash: { $ref: "#/components/schemas/Bytes32" }
        metadataCid: { type: string }
        canonicalMetadata: { type: object }
        warnings: { type: array, items: { type: string } }
    ImageUploadResponse:
      type: object
      properties:
        imageUri: { type: string }
        imageCid: { type: string }
        mimeType: { type: string }
        width: { type: integer }
        height: { type: integer }
        bytes: { type: integer }
        thumbnailUri: { type: string }
    ProjectionRow:
      type: object
      description: |
        An indexer read-model row served as-is (all bigint columns as decimal
        strings). Additive — new fields may appear without a version bump;
        documented fields never change meaning.
      additionalProperties: true
    StatusPayload:
      type: object
      description: |
        Indexer health. Ingestion is judged in BLOCKS behind chain head (it walks
        every block); projection is judged in SECONDS of queue age (it is
        event-driven). `degraded` is true when either crosses its threshold —
        ~300 blocks and ~30s by default.
      properties:
        chainId: { type: integer }
        chainHeadBlock:
          allOf: [{ $ref: "#/components/schemas/WeiString" }]
          description: Chain head as last observed by the indexer.
        lastIngestedBlock: { $ref: "#/components/schemas/WeiString" }
        lastProjectedBlock: { $ref: "#/components/schemas/WeiString" }
        pendingProjectionEvents:
          type: integer
          description: >-
            Canonical raw events ingested but not yet projected — the projector's
            actual queue depth. 0 means projection is fully caught up.
        projectionLagSeconds:
          allOf: [{ $ref: "#/components/schemas/WeiString" }]
          description: >-
            Seconds the OLDEST unprojected canonical event has been waiting
            (now − its block timestamp); 0 when nothing is pending. This is the
            projection half of `degraded`.
        ingestionLagBlocks:
          allOf: [{ $ref: "#/components/schemas/WeiString" }]
          description: >-
            Blocks the ingester trails chain head. This is the ingestion half of
            `degraded`.
        projectionLagBlocks:
          allOf: [{ $ref: "#/components/schemas/WeiString" }]
          description: >-
            INFORMATIONAL ONLY — not an input to `degraded`. Newest canonical event
            block minus the projector cursor. On a sparse chain the cursor rests on
            the last block that had events, so the instant a new event lands this
            reads as "blocks elapsed since the previous event" (thousands) for the
            one cycle the projector needs to drain it. It measures elapsed blocks,
            not pending work: use `pendingProjectionEvents` / `projectionLagSeconds`.
        degraded: { type: boolean }
        notice: { type: [string, "null"], description: "PRD 13.7 degraded-mode copy for the UI." }
    StatsResponse:
      type: object
      properties:
        tokensLaunched: { type: integer }
        ethUsdPrice1e8:
          type: [string, "null"]
          description: >-
            Latest interim ETH/USD oracle price ×1e8 (decimal string) — the
            same series /v1/quotes reads for native ETH. Null until the first
            oracle row is indexed (pre-launch / oracle gap); clients hide USD
            then rather than fake it.
        swaps: { type: integer }
        volumeEthWei: { $ref: "#/components/schemas/WeiString" }
        activeTraders: { type: integer }
        bullseyesReached: { type: integer }
        ethFeesCollectedWei:
          allOf: [{ $ref: "#/components/schemas/WeiString" }]
          description: >-
            Total native-ETH swap fees GENERATED at accrual (Σ FeeAccrued in
            ETH). The honest platform-wide total the fee
            subsystem produced, independent of how much has been realized yet.
        creatorFeesEthWei:
          allOf: [{ $ref: "#/components/schemas/WeiString" }]
          description: >-
            Native-ETH swap fees REALIZED to creators (paid + parked to the creator
            role). Attributed, so it stays sourced from realization — accrual is
            unattributed and never feeds this figure.
        creatorFeesUsd1e8:
          type: string
          description: >-
            Historical-USD twin of `creatorFeesEthWei` (additive; older
            deployments omit it), spanning EVERY pool (native, USDG, stock):
            the realized creator-share settlements (kind paid/parked), each
            valued at its own contemporaneous oracle price fixed at projection
            (fee_settlements.usd_1e8). USD ×1e8 decimal string; rows unpriced
            at projection contribute 0 — the figure is partial by those rows,
            never fabricated.
        nonNativeLaunches:
          type: integer
          description: >-
            Visible launches quoted in something other than native ETH
            (USDG/stock pools). Cross-quote visibility: the *EthWei figures
            above cover ETH-quoted pools only, so this count keeps non-native
            pools from vanishing out of the platform pulse.
        crossQuoteSwaps:
          type: integer
          description: >-
            All-time swaps in non-native (USDG/stock) pools — trades
            `volumeEthWei` cannot honestly include, their raw quote units not
            being wei.
        volumeUsd1e8:
          type: string
          description: >-
            All-time volume in USD ×1e8 (decimal string) spanning EVERY quote
            kind (unlike the ETH-only `volumeEthWei`): each swap was valued at
            its contemporaneous oracle price when indexed
            (token_metrics.volume_usd_1e8). "0" until the first priced swap is
            indexed — never fabricated. All-time only: a 24h USD window is not
            derivable from the cumulative rollup, so `last24h` carries no USD
            volume twin.
        last24h:
          type: object
          properties:
            launches: { type: integer }
            swaps: { type: integer }
            volumeEthWei: { $ref: "#/components/schemas/WeiString" }
            activeTraders: { type: integer }
            creatorFeesEthWei: { $ref: "#/components/schemas/WeiString" }
            creatorFeesUsd1e8:
              type: string
              description: >-
                Rolling-24h historical-USD twin of `creatorFeesEthWei`
                (additive), same split as the all-time figure: every pool's
                realized creator shares (kind paid/parked), each valued at its
                own contemporaneous price fixed at projection. Unpriced rows
                contribute 0, never a fabricated number.
            crossQuoteSwaps:
              type: integer
              description: Rolling-24h swaps in non-native (USDG/stock) pools.
    QuoteAsset:
      type: object
      description: |
        An allowed pool quote asset. `address` is the quote currency (the zero
        address is native ETH); both it and `usdFeed` are lowercased.
      properties:
        address: { $ref: "#/components/schemas/Address" }
        kind: { type: string, enum: [native_eth, usdg, stock] }
        decimals: { type: integer }
        symbol: { type: string }
        name: { type: string }
        usdFeed: { $ref: "#/components/schemas/Address" }
        usdPrice1e8:
          type: [string, "null"]
          description: >-
            Latest known USD price ×1e8 (decimal string), for a rough USD
            orientation only. Native ETH uses the interim ETH/USD series; other
            quotes use their per-asset Chainlink snapshot. Null when no snapshot
            has been indexed — never fabricated.
    TokenListItem:
      allOf:
        - { $ref: "#/components/schemas/ProjectionRow" }
        - type: object
          description: Launch projection + metrics + discovery signals.
          properties:
            tokenAddress: { $ref: "#/components/schemas/Address" }
            creatorAddress: { $ref: "#/components/schemas/Address" }
            name: { type: string }
            symbol: { type: string }
            metrics: { oneOf: [{ $ref: "#/components/schemas/ProjectionRow" }, { type: "null" }] }
            momentum: { type: [number, "null"], description: "Only present for sort=momentum; never fabricated." }
            topHolderBps:
              type: [integer, "null"]
              description: Top non-infrastructure holder's share in basis points of the fixed supply.
    TokenDetail:
      allOf:
        - { $ref: "#/components/schemas/TokenListItem" }
        - type: object
          properties:
            pool: { oneOf: [{ $ref: "#/components/schemas/ProjectionRow" }, { type: "null" }] }
            bullseyeThresholdWei:
              allOf: [{ $ref: "#/components/schemas/WeiString" }]
              description: >-
                Per-pool Bullseye threshold in RAW units of the pool's quote
                asset (pools quoted in native ETH: wei; other quotes: that
                asset's raw units at quoteAsset.decimals).
            hidden:
              type: boolean
              description: >-
                Product-layer visibility. A hidden launch is excluded from every
                discovery and aggregate surface (token lists, search, /v1/stats,
                /v1/analytics/daily, the activity feed and the live stream) and
                accrues no new points, but this direct-detail route keeps
                serving it — hiding removes DISCOVERABILITY, it does not revoke
                links people already hold, and it deletes nothing: the raw event
                ledger stays complete, a replay still rebuilds the token, and
                the hidden set survives that replay. This route is the ONLY one
                that reports the field — list and search responses never contain
                a hidden launch, so they omit it entirely.
            quoteAsset:
              type: object
              description: >-
                The pool's quote asset. Pools may be quoted in native ETH, in
                USDG or in a tokenized stock/ETF; every *EthWei field remains
                exact for native-quote pools and additive quote-denominated
                fields carry the general form.
              properties:
                address: { $ref: "#/components/schemas/Address" }
                kind: { type: string, enum: [native_eth, usdg, stock] }
                decimals: { type: integer }
    Swap:
      allOf:
        - { $ref: "#/components/schemas/ProjectionRow" }
        - type: object
          properties:
            tokenAddress: { $ref: "#/components/schemas/Address" }
            beneficiary: { $ref: "#/components/schemas/Address" }
            isBuy: { type: boolean }
            txHash: { $ref: "#/components/schemas/Bytes32" }
            blockNumber: { $ref: "#/components/schemas/WeiString" }
            logIndex: { type: integer }
    WatchlistRequest:
      type: object
      required: [token]
      properties:
        token: { $ref: "#/components/schemas/Address" }
  securitySchemes:
    walletSession:
      type: http
      scheme: bearer
      description: Session token from /v1/auth/verify (SIWE-lite wallet signature).
  headers:
    RateLimit:
      description: Per-IP request budget (default 120/min).
      schema: { type: integer }
x-rate-limits:
  default: "120 requests/minute/IP"
x-idempotency:
  metadataUploads: "Supply an Idempotency-Key header on POST /v1/metadata and /v1/metadata/images."
x-versioning:
  strategy: "URL-prefixed (/v1). Additive changes are non-breaking; breaking changes bump the prefix."