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
bogaCLI — 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/metadatacode 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).
- Add Robinhood Chain to your wallet — network values in Robinhood Chain setup.
- 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.)
- 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:
- 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).
- Initial buy — optionally buy the first 1–2% of supply in the same transaction, with the ETH cap shown up front. Zero is fine.
- Preview — review everything. Launches are public and irreversible: the metadata is committed on-chain and cannot be edited after launch.
- 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
- 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.
- On the token page, use the buy/sell panel: enter an amount, press Review buy (or sell), and confirm the swap in your wallet.
- 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:
- 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).
- 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.
- 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).- contracts —
BogaTokenstores immutablemetadataURI+metadataHashand exposestokenURI/contractURI;BogaFactorytakes the structuredLaunchParamsand emitsTokenLaunched. @boga/sdk—BogaClient+ 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-onlyviemLaunchReader.
@boga/indexer— projectsTokenLaunched, 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.- DB —
launchesstores 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)
- Metadata document built + canonicalized + hashed (shared lib).
- Hash committed on-chain in
BogaToken+TokenLaunched(creator-signed tx). - Indexer reads the event, fetches the URI, re-derives the hash, verifies equality, validates schema, records trust/status + parsed fields.
- 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
- Validate —
boga metadata validate/client.metadata.validate(doc). Schema v1, NFC, plain-text description, https/ipfs/ar only. - Metadata URI + hash — either
- Self-host:
buildAndHashTokenMetadata(input)→ pin the canonical JSON on IPFS/Arweave/https yourself; the pinned bytes must hash tometadataHash. - Boga-host (optional):
POST /v1/metadata→{ metadataUri, metadataHash }. Both derive the identical hash (one shared canonicalization).
- Self-host:
- Predict address —
client.launch.predictAddress(rpc, creator, params)→ deterministic in(creator, salt); predict the pool id withclient.launch.predictPoolId(token). - Simulate (optional) —
POST /v1/launches/simulate→ predicted addresses, estimated tokens/ETH, gas, decoded revert, block used, expiry. - Build —
client.launch.buildTransaction(params)→{ to: factory, data, value: maxEthIn }. Offline; no key. - Sign + broadcast — the creator, in their wallet. Boga is never involved.
- Wait + read —
client.transactions.waitUntilIndexed(hash)thenGET /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:
- Output is UTF-8 with no insignificant whitespace.
- Object member keys are sorted by UTF-16 code units (JavaScript's default string sort — the JCS requirement).
- 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\u00xxfor other C0 controls,/not escaped, non-ASCII left literal). - 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. undefinedobject members are omitted (they are not JSON);nullis 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 pollutionvalidateTokenMetadata(metadata)→{ valid, errors[], warnings[] }with stableMetadataErrorCodesbuildTokenMetadata(input)/buildAndHashTokenMetadata(input)— construct a fixed-shape canonical document from developer input (EIP-55-checksums creator, always emits all fivelinkskeys, includesexternal_urlonly 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
httpsmetadata/images must go throughassertSafeHttpUrl(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 broadcasts — prepare/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.
preparereturns a plain transaction object; the server does not send it.- The RPC reader (
viemLaunchReader) calls only view functions,estimateGasand 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 namespacesmetadata,launch,transactions,protocol.robinhoodChain— viem chain (id 4663).- Free functions (also usable without the client):
buildLaunchTransaction,predictTokenAddressRequest,parseLaunchReceipt,initialBuyBpsToTokens,computePoolId/predictBogaPoolId, and the metadata primitivescanonicalizeTokenMetadata/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/cli → boga)
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) → immediatePRIVATE_KEY_REJECTEDerror, no work done.- No command signs or broadcasts;
launch prepareprints an UNSIGNED transaction with an explicit "Boga never signs" note. launch preparedisplays 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."