Protocol

Indexer & data

How chain events become rows, how per-token stats are computed, and how fresh the index is.

The app never fabricates data: every number is read from Robinhood Chain or projected from its events by workers/indexer.ts.

Sync loop

  1. State. sync_state.last_block is the last fully processed block; the first run starts at PINARC_START_BLOCK.
  2. Registry. Load every known token from D1: curve → token, token set, and for graduated tokens the Uniswap pair (with token0 resolved by multicall so swap direction can be decoded).
  3. Walk. Fetch logs in chunks of 2,000 blocks from the factory, every curve and token, the locker, vault, bond and floor contracts, and every known pair. Each run has a time budget (25 s from the cron / an authenticated poke, 8 s for an unauthenticated poke) and stops cleanly at a chunk boundary.
  4. Project. Decode with the contract ABIs and apply the rules below inside one D1 batch per chunk.
  5. Refresh. Recompute derived stats for every token touched in the chunk (and structurally changed ones) with a multicall + SQL pass.
  6. Metadata. Fetch and sanitise the metadata document for new or updated metadataURIs (own documents are read straight from D1).

Event → projection

EventEffect on D1
PinarcFactory.TokenCreatedInsert tokens row (status batch), snapshot the curve's parameters via multicall, record bond / dev buy / team allocation
Launchedbatch_ends_at
BatchCommittedUpsert batch_commits, batch_usdg
BatchSettledbatch_settled = 1, status batch → live, clearing price, tokens out, refund; one trades row with venue batch
BatchClaimedInsert batch_claims
TradeInsert trades (venue curve), set price_usdg, last_trade_at; set creator_sold = 1 when the trader is the creator and isBuy = false
Graduatedstatus graduated, pair, lp_lock_id (or burned), lp_usdg / lp_tokens, add floorUsdg to floor_reserve_usdg, liquidity_usdg = 2 × usdgLiquidity
Token TransferUpdate holders balances for from and to
Token MetadataUpdatedRefetch the document; update name/description/image/socials
LPLocker.Locked / Extended / Withdrawn / LockTransferredUpsert locks
VestingVault.ScheduleCreated / ReleasedUpsert vesting
CreatorBond.Posted / Released / Slashedbond_usdg, bond_status
FloorReserve.Deposited / Redeemedfloor_reserve_usdg
Uniswap pair Swap (+ Sync)Insert trades with venue dex (direction from token0), set price_usdg, liquidity_usdg = 2 × USDG reserve, creator-sold flag

Per-token refresh

For each touched token, one multicall reads price, usdgRaised, tokensSold, progressBps, creatorFeesEarned, settled, batchUsdg, FloorReserve.reserveOf and, once graduated, the pair's getReserves / token0 (DEX price = USDG reserve ÷ token reserve). Then one SQL statement derives:

ColumnDefinition
price_usdgDEX price after graduation, curve spot before
mcap_usdgprice_usdg × total_supply
liquidity_usdg2 × USDG in the pool after graduation; usdg_raised before
curve_progressprogressBps / 100; forced to 100 once graduated
holdersCount of holders rows with balance > 1e-6, excluding the curve, the pair, the vault, the locker and the dead address
volume_24h, buys_24h, sells_24hFrom trades with ts ≥ now − 86400
change_1h, change_24hprice_usdg / price of the last trade at or before (now − window) − 1
top10_pctSum of the 10 largest non-structural balances ÷ total_supply
creator_pctCreator's balance ÷ total_supply

Freshness and triggering

  • Cron: every minute.
  • Page views: when a page loads and the index is more than 45 s behind the chain, the worker starts a lock-protected background sync (one at a time), so freshness does not depend on the cron alone.
  • POST /api/v1/sync: after every confirmed transaction the app pokes it; unauthenticated pokes are throttled to one per 5 s (8 s budget); Authorization: Bearer <SYNC_SECRET> allows a 25 s pass.
  • GET /api/v1/health returns last_block and synced_at; compare with the chain head to see lag.

Reorgs and idempotency

Robinhood Chain (Arbitrum stack) gives fast finality; the indexer processes only blocks up to the head returned by the RPC and writes each chunk as one D1 batch keyed by (tx_hash, log_index), so re-running a range is idempotent.

Run your own

pinarc-indexer is the open-source reference implementation of this pipeline (Node + SQLite, no native dependencies): pinarc-indexer sync --follow replays the chain from block 65,381,613 into a local database with an event audit log, and pinarc-indexer serve exposes the same read model. Its numbers should match this app's; if they ever don't, that is a bug worth reporting.

RPC endpoints

The indexer reads through an ordered fallback list (RPC_URL holds a comma-separated list) and moves to the next endpoint when one fails or rate-limits. In production the order is rpc.ordofi.networkrpc.mainnet.chain.robinhood.comrobinhood-rpc.publicnode.com: the official RPC answers eth_getLogs from Workers with 429 under load, and dRPC's free tier refuses log ranges, so neither can be the only source. The browser uses its own ranked list (RPC_URL_PUBLIC) and prefers the fastest endpoint that answers. GET /api/v1/rpc-probe (authenticated) reports the current health of each endpoint.