Architecture
hoodchain is a thin, typed layer over viem. It ships data (the verified registry) and semantics (multiplier math, staleness guards, router flavors) — not a client runtime.
The multiplier, or: why generic trackers misprice Stock Tokens
Every Stock Token implements ERC-8056: uiMultiplier() returns the
shares-per-token ratio, 1e18-scaled. Corporate actions (splits, reinvested dividends)
raise the multiplier instead of rebasing balances. Two consequences the SDK encodes:
Feed prices are already adjusted
Robinhood's Chainlink feeds return the price of one token — underlying share
price × multiplier. USD value is therefore balance × feedPrice, full stop.
Applying the multiplier again double-counts every dividend. hoodchain never does.
Raw balances understate positions
Share-equivalent units are balance × uiMultiplier ÷ 1e18. hoodchain
computes this per position and its live test suite asserts the result equals the
token's own on-chain balanceOfUI() — bit-for-bit agreement with the issuer.
Staleness is a market-hours question
Stock feeds update 24/5. A Saturday read of a Friday-close answer is correct, not
stale — so getQuote defaults to a 72 h window and throws a typed
StaleFeedError (with ages attached) beyond it.
Registry pipeline
src/registry/stock-tokens.json is generated, checked in, and re-verifiable:
npm run refresh-registry re-runs the whole pipeline and refuses to write if
any step fails.
1 DISCOVER Blockscout token search — canonical "<Name> • Robinhood Token" pattern
2 VERIFY on-chain: every token's EIP-1967 beacon slot must point at the ONE
shared Stock beacon (0xe10b6f6B…1b00); symbol/name/decimals/uiMultiplier
read back via multicall
3 FEEDS Chainlink's official Robinhood directory mapped by ticker; every feed
must answer latestRoundData() > 0 at 8 decimals
4 WRITE sorted JSON + provenance (block number, counts, source URLs)
Result at generation: 95 tokens (the docs site lists 25 — the chain is
ahead of its own docs), 34 with live Chainlink feeds. Feed-less tokens
still resolve balances and multipliers; pricing them throws FeedNotFoundError.
Verified addresses (mainnet 4663)
| Contract | Address | How it was verified |
|---|---|---|
| USDG (6 decimals) | 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168 | docs.robinhood.com/chain/contracts + Blockscout-verified Paxos impl |
| WETH9 | 0x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73 | official docs + router.WETH9() on-chain |
| UniswapV3Factory | 0x1f7d7550B1b028f7571E69A784071F0205FD2EfA | SwapRouter02.factory() ∧ QuoterV2.factory() |
| QuoterV2 | 0x33e885eD0Ec9bF04EcfB19341582aADCb4c8A9E7 | same deployer as factory; factory() linkage |
| SwapRouter02 | 0xCaf681a66D020601342297493863E78C959E5cb2 | Blockscout-verified source; ecosystem routes through it |
| Multicall3 | 0xca11bde05977b3631167028862be2a173976ca11 | canonical deterministic deployment (in viem's chain def) |
| NOXA LaunchFactory | 0xD9eC2db5f3D1b236843925949fe5bd8a3836FCcB | extracted from official frontend bundle; event topics matched to live logs |
| Odyssey BondingCurveFactory | 0xEb3FeeD2716cF0eEAda05B22e67424794e1f5a80 | extracted from official frontend bundle; event topics matched to live logs |
Testnet (46630) has no official Uniswap deployment — none of the mainnet
addresses have code there. The SDK pins the one community deployment with a liquid
Stock Token pool and documents the linkage checks; its router is the classic
SwapRouter (struct-level deadline), which buildSwapTx handles
transparently.
Swap routing
quoteSwap simulates QuoterV2 across all four fee tiers for the direct pair
plus 0.05%/0.3% two-hop routes through WETH and USDG, then picks the highest output.
Pools that exist with zero liquidity (common for Stock Token pairs) revert inside the
quoter and are skipped — if nothing fills, you get a typed NoRouteError,
never a silent zero. Slippage is applied in basis points to
amountOutMinimum; deadlines use each router's native mechanism
(multicall(deadline, …) on SwapRouter02, struct field on classic).
Firehose decoding
wss://feed.mainnet.chain.robinhood.com is a standard Arbitrum Nitro
broadcast feed: JSON frames of sequenced messages whose l2Msg payload is a
base64 Nitro L2 message. hoodchain decodes kind 0x04 (one signed
transaction) and kind 0x03 (length-prefixed batch, recursively) into viem
transactions with their eventual hashes, and passes unknown kinds through raw. The
client reconnects with exponential backoff and works on the global WebSocket (Node ≥ 22,
browsers) or the optional ws package.
Error hierarchy
HoodchainError
├── UnknownSymbolError symbol not in the registry
├── FeedNotFoundError token exists, no Chainlink feed
├── StaleFeedError answer older than maxAgeSeconds (carries ages)
├── InvalidFeedAnswerError non-positive / incomplete round
├── NoRouteError no v3 route with usable liquidity
├── SlippageExceededError output below the slippage bound
├── NoAccountError write op on a read-only client
├── StockTokenEligibilityError acquiring swap without operator acknowledgement
└── FeedConnectionError firehose reconnect budget exhausted