Developers

App architecture

How the dapp is put together: SSR from D1, live multicall reads, the transaction pipeline, the indexer and the API.

pinarc-dapp is a React Router v7 framework-mode app rendered on Cloudflare Workers, with Hono in front of it for the JSON API, D1 as the index of chain state, wagmi/viem for wallets, and TradingView Lightweight Charts for price.

                ┌────────────────────────── Cloudflare Worker ──────────────────────────┐
browser ──HTTP──▶ Hono: /api/v1/* ─────────────────────────────────────▶ D1 (index)      │
        ◀─SSR───  React Router loaders (read D1, render)  ◀────────────┘  ▲              │
   │                                                                      │ writes        │
   │ wagmi/viem (multicall every 4 s, wallet txs)         cron * * * * * ─┤               │
   ▼                                                      POST /api/v1/sync ──▶ indexer  │
Robinhood Chain ◀──────────────────────── viem public client (RPC_URL) ◀────┘            │
                └────────────────────────────────────────────────────────────────────────┘

Rendering

Route loaders read the D1 snapshot (app/lib/db.server.ts) and render on the server; nothing in the first paint waits for the chain. On the client, useCurve() polls one Multicall3 call every 4 s: price, usdgRaised, tokensSold, progressBps, inBatch, batchEndsAt, settled, batchUsdg, batchCommitOf(me), inAntiSniperWindow, maxTx, maxWallet, maxBatchCommit, isGraduated, lastBuyAt(me), cooldownSeconds, tradeFeeBps, creatorFeesEarned, plus the user's token and USDG balances and allowances. Quotes are computed locally with the bigint curve mirror and cross-checked against quoteBuy/quoteSell reads.

Transaction pipeline

useTx().run(steps) executes an ordered list of steps, each { label, send }:

  1. Refuses to start unless the wallet is on chain id 4663.
  2. For each step: writeContractAsyncwaitForTransactionReceipt(confirmations: 1) → throw if status !== "success".
  3. approveStep(asset, spender, amount, currentAllowance) is inserted only when the allowance is short, so repeat trades are one signature.
  4. On success: toast, POST /api/v1/sync (retried once after 3.5 s if throttled), then React Router revalidate() and React Query invalidateQueries() — the D1-rendered parts and the live reads both refresh.
  5. Errors are decoded from viem's BaseError (custom errors like MaxWallet, Slippage, CommitTooLarge surface by name).

Wallet session

The connected address is mirrored into the pinarc_wallet cookie by useWalletCookieSync; loaders use it only to personalise reads (portfolio, claims, watchlist). Nothing that moves funds trusts it.

Indexer

workers/indexer.ts runs from the cron trigger and from POST /api/v1/sync. See Indexer & data for the projection rules and the stats SQL.

Data model (D1)

TableContent
sync_statelast_block, synced_at
tokensOne row per launch: addresses, metadata, snapshotted parameters, curve state, batch state, derived stats, accountability fields, graduation fields
tradesEvery curve Trade, batch fill and Uniswap Swap, with venue = curve · batch · dex
batch_commits, batch_claimsPer-wallet batch participation
holdersBalances projected from token Transfer events
locks, vestingLocker and vault state
metadataHosted metadata documents and images
watchlist, early_accessPer-wallet app data

Configuration

wrangler.jsonc vars: RPC_URL_PUBLIC (browser), EXPLORER_URL, PINARC_* contract addresses, PINARC_START_BLOCK. Secrets: RPC_URL (archive endpoint for the Worker), SYNC_SECRET. Bindings: DB (D1). Trigger: crons: ["* * * * *"].