Docs · §9ME-API-09

API

Every readout on this site comes from these endpoints. JSON, no auth (except the cron), no CORS drama. Read the capsules yourself.

Conventions: wei values are decimal strings — WETH wei (18 decimals) on legacy fields, USDG base units (6 decimals) on every field that names a quote; addresses are lowercase; timestamps are epoch milliseconds; everything is Cache-Control: no-store unless a cache time is noted. Every basket line and every asset carries a logo (a /logos/<ticker>.png path for stocks, a DexScreener URL for tokens) and a kind.

GET/api/health

The deploy check and the pons v2 readout: version, pair token, the three launch checks, operator ETH and USDG, the escrow balance. 200 when the chain answers, 503 otherwise.

curl -s https://memeetf.vercel.app/api/health
{ ok: boolean, mode: "live" | "paper", db: string,
  ponsVersion: 2, pairToken: "USDG",
  ponsLaunchEnabled: boolean | null,   // launchEnabled && canLaunch(operator) && approvedPairTokens(USDG)
  canLaunch: boolean | null, pairApproved: boolean | null, ponsFeeWei: string | null,
  operatorBalanceWei: string | null,   // ETH, for gas
  operatorUsdgWei: string | null,      // claimed fees, USDG base units (6 dec)
  escrowPendingUsdg: string | null,    // swept, unclaimed, in the pons Fee Escrow
  head: string | null }
GET/api/stats

The live readout: mode, operator, totals in both eras, last tick, ETH price, shelf counts.

curl -s https://memeetf.vercel.app/api/stats
{ mode: "live" | "paper", operator: string | null, coins: number,
  feesCollectedWei: string,            // legacy v1 coins, WETH wei
  feesCollectedUsdg?: string,          // v2 coins, USDG base units (6 dec)
  feesCollectedUsd?: number,           // both eras, dollars
  basketBoughtUsd: number, dropsSent: number, payoutsSent: number, holdersPaid: number,
  lastTickAt: number | null, ethUsd: number | null, catalog: { total: number, routable: number } }
GET/api/catalogcache 60s

The shelf: every stock with its logo, both route families and its live price. `routable` is true when either family has a pool deep enough.

curl -s https://memeetf.vercel.app/api/catalog
{ assets: Asset[] }
Asset = { symbol, name, address, decimals, logo: "/logos/nvda.png", kind: "stock" | "token",
  routable: boolean,                   // either family
  route: Route | null,                 // WETH-funded family (v1 coins): "direct" | "via-usdg"
  routeUsdg: Route | null,             // USDG-funded family (v2 coins): "direct-usdg" | "via-weth"
  priceUsd: number | null, change24h: number | null, liquidityUsd: number | null }
Route = { kind: "direct" | "via-usdg" | "direct-usdg" | "via-weth", fee: number, pool: string, hopFee?: number, depthUsd?: number }
GET/api/token/[address]

Resolve any Robinhood Chain ERC-20 for a capsule: on-chain name/symbol/decimals, DexScreener logo + price + liquidity, and a v3 route probe in both families (USDG direct or via WETH, depth ≥ $1000). Upserts the route so the engine can buy it. Rejects WETH, USDG and capsule coins.

curl -s https://memeetf.vercel.app/api/token/0xYOURTOKEN
{ address, symbol, name, decimals, logo: string | null, priceUsd: number | null,
  liquidityUsd: number | null, routable: boolean, route: Route | null, routeUsdg: Route | null, kind: "stock" | "token" }
4xx { error }   // not a token, a capsule cannot hold itself, no pool deep enough
GET/api/prices?addresses=a,b,ccache 60s

Prices for specific assets (DexScreener, cached).

curl -s "https://memeetf.vercel.app/api/prices?addresses=0xaf3d76f1834a1d425780943c99ea8a608f8a93f9"
{ prices: Record<address, { priceUsd, change24h, liquidityUsd }> }
GET/api/coins?sort=new|hot|dropped&limit=50

Every capsule. Money is in the coin's quote: `progress.quote` says whether collectedWei/targetWei are WETH wei (v1) or USDG base units (v2); the *Usd fields are the same numbers in dollars.

curl -s "https://memeetf.vercel.app/api/coins?sort=hot&limit=20"
{ coins: CoinSummary[] }
CoinSummary = { address, name, symbol, logo, curator, createdAt, pool,
  basket: [{ address, symbol, weightBps, logo, kind: "stock" | "token" }],
  feesCollectedWei,                    // legacy WETH wei (v1 coins)
  feesQuoteWei?, feesUsd?,             // v2: lifetime creator fees in USDG base units, and in dollars
  dropsSent, lastDropAt, holders: number,
  progress: { collectedWei, targetWei, pct,          // pct = the capsule's fill level
              quote?: "USDG" | "WETH", collectedUsd?, targetUsd? },
  status: "queued" | "live" | "failed", ponsUrl, dexscreenerUrl,
  curve?, pairToken?, poolId?,         // pons v2: bonding curve, USDG, Uniswap v4 pool id
  phase?: 0 | 1 | 2 | 3 }              // 0 on the curve · 1 swept · 2 in its v4 pool · 3 rescued
GET/api/coin/[address]

One capsule: detail, fee log, drop log, top holders, tray — plus the pons v2 reads: phase, live curve state while on the curve, the operator's escrow balance, and every link.

curl -s https://memeetf.vercel.app/api/coin/0xYOURCOIN
{ coin: CoinSummary & { description, socials, launchTx, salt, restrictionsEndBlock },
  fees: FeeEvent[]        // newest 50
  drops: Drop[]           // newest 20
  holders: { wallet, balance, pct }[]   // top 25
  holderCount: number, tray: { asset, symbol, amountWei }[],
  ponsVersion: 1 | 2, phase: number | null, phaseLabel: string | null,
  escrowPendingUsdg: string | null,     // operator's claimable USDG in the Fee Escrow (all capsules)
  curveState: { quoteReserve, tokenReserve, realQuoteReserve, sellableTokens,
                graduationThreshold, readyToGraduate: boolean, pct: number } | null,   // phase 0 only
  links: { pons, dexscreener, token, tokenAddress, curve: string | null, escrow: string | null } }
FeeEvent = { id, at, txHash, wethWei, coinWei, coinPolicy: "burn" | "sell" | "held",
             quoteToken?: string | null, quoteWei?: string }   // v2 rows: fees in USDG base units
Drop = { id, at, snapshotBlock, spentWei, eligibleHolders, quote?: "USDG" | "WETH",
  purchases: [{ asset, symbol, logo?, decimals?, amountWei, wethInWei, usd, txHash, status }],   // wethInWei is in the drop's quote
  payouts: { total, sent, failed, dust } }
GET/api/coin/[address]/drops?cursor= · /holders?cursor= · /payouts?dropId=

Paginated sub-lists for one capsule.

curl -s "https://memeetf.vercel.app/api/coin/0xYOURCOIN/payouts?dropId=1"
{ drops: Drop[], cursor? }  ·  { holders: HolderRow[], cursor? }  ·  { payouts: Payout[] }
GET/api/drops?limit=50

The drops feed: every capsule release, each with its quote.

curl -s "https://memeetf.vercel.app/api/drops?limit=50"
{ drops: (Drop & { coin: { address, name, symbol, logo } })[] }
GET/api/wallet/[address]

Holder view: capsules held, received drops, tray.

curl -s https://memeetf.vercel.app/api/wallet/0xYOURWALLET
{ holdings: [{ coin: CoinSummary, balance, pct }],
  received: [{ at, coin, coinSymbol, asset, symbol, logo?, kind?, decimals?, amountWei, txHash }],
  tray: [{ coin, asset, symbol, logo?, kind?, decimals?, amountWei }] }
GET/api/launch/quote

What a launch costs right now, whether launches are open, and the three pons v2 factory checks behind that answer.

curl -s https://memeetf.vercel.app/api/launch/quote
{ chainId: 4663, operator: string | null,
  launchFeeWei: string,                // what the curator pays the operator (LAUNCH_FEE_ETH)
  ponsFeeWei: string,                  // what pons charges the operator per launch, out of launchFeeWei
  ponsVersion: 2, pairToken: "USDG",
  ponsLaunchEnabled: boolean | null,   // null = factory unreadable
  ponsChecks: { launchEnabled, canLaunch, pairApproved } | null,
  mode: "live" | "paper", catalogRoutable: number,
  limits: { minAssets: 2, maxAssets: 12, nameMax: 32, symbolMax: 10 } }
POST/api/launch

Build a capsule. Requires a confirmed, unused payment tx to the operator from `creator` of at least launchFeeWei. Non-catalog assets are accepted when a route exists in either family (the resolver runs server-side on validation).

curl -s -X POST https://memeetf.vercel.app/api/launch \
  -H 'content-type: application/json' \
  -d '{ "paymentTx": "0x…", "creator": "0x…", "name": "Mag Seven", "symbol": "MAG7",
        "logo": "https://…/logo.png", "description": "…",
        "socials": { "twitter": "https://x.com/…" },
        "basket": [ { "address": "0xaf3d…93f9", "weightBps": 5000 },
                    { "address": "0x12f1…bf54", "weightBps": 5000 } ] }'
200 { id, status: "live" | "queued", token?, curve?, pool?, launchTx?, ponsUrl? }   // curve = the pons v2 bonding curve
4xx { error }   // weights must sum 10000; every asset routable;
                // paymentTx to operator, value ≥ launchFeeWei, from == creator, confirmed, unused
GET/api/launch/status?id=

Poll a queued launch.

curl -s "https://memeetf.vercel.app/api/launch/status?id=LAUNCH_ID"
{ id, status: "queued" | "launching" | "live" | "failed", token?, curve?, error? }
GET/api/events?coin=&limit=

The engine journal (newest first), plus mode, operator, last tick and operator balances in ETH, WETH and USDG. Filter by coin for one capsule.

curl -s "https://memeetf.vercel.app/api/events?limit=50"
{ mode: "live" | "paper", operator: string | null, store: string, ponsVersion: 2, pairToken: "USDG",
  lastTickAt: number | null,
  balances: { ethWei, wethWei, usdgWei, escrowUsdgWei } | null,
  events: [{ id, at, kind, label, coin: string | null, data: object }] }
GET/api/cron/tick

The engine tick. Vercel cron calls it every minute with the secret.

curl -s -H "Authorization: Bearer $CRON_SECRET" https://memeetf.vercel.app/api/cron/tick
{ ran: true, phases: { … }, ms: number }
POST/api/tick/poke

Public, rate-limited (1 per 30s per instance) fallback that runs the same tick. Poke it if the readout says the last tick is stale.

curl -s -X POST https://memeetf.vercel.app/api/tick/poke
{ ran: boolean, … }  |  429 { error }

9.1 Types

The TypeScript shapes for every response live in lib/types.ts in the repository. They are the contract the site itself is built against. Fields marked optional there are the pons v2 additions and are absent on responses for legacy v1 coins.

Rate limits

Be reasonable. The price endpoints are cached 60s upstream; hammering them only hammers the cache. The poke endpoint refuses more than one tick per 30s.