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 }:
- Refuses to start unless the wallet is on chain id 4663.
- For each step:
writeContractAsync→waitForTransactionReceipt(confirmations: 1)→ throw ifstatus !== "success". approveStep(asset, spender, amount, currentAllowance)is inserted only when the allowance is short, so repeat trades are one signature.- On success: toast,
POST /api/v1/sync(retried once after 3.5 s if throttled), then React Routerrevalidate()and React QueryinvalidateQueries()— the D1-rendered parts and the live reads both refresh. - Errors are decoded from viem's
BaseError(custom errors likeMaxWallet,Slippage,CommitTooLargesurface 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)
| Table | Content |
|---|---|
sync_state | last_block, synced_at |
tokens | One row per launch: addresses, metadata, snapshotted parameters, curve state, batch state, derived stats, accountability fields, graduation fields |
trades | Every curve Trade, batch fill and Uniswap Swap, with venue = curve · batch · dex |
batch_commits, batch_claims | Per-wallet batch participation |
holders | Balances projected from token Transfer events |
locks, vesting | Locker and vault state |
metadata | Hosted metadata documents and images |
watchlist, early_access | Per-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: ["* * * * *"].