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)

ContractAddressHow it was verified
USDG (6 decimals)0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168docs.robinhood.com/chain/contracts + Blockscout-verified Paxos impl
WETH90x0Bd7D308f8E1639FAb988df18A8011f41EAcAD73official docs + router.WETH9() on-chain
UniswapV3Factory0x1f7d7550B1b028f7571E69A784071F0205FD2EfASwapRouter02.factory()QuoterV2.factory()
QuoterV20x33e885eD0Ec9bF04EcfB19341582aADCb4c8A9E7same deployer as factory; factory() linkage
SwapRouter020xCaf681a66D020601342297493863E78C959E5cb2Blockscout-verified source; ecosystem routes through it
Multicall30xca11bde05977b3631167028862be2a173976ca11canonical deterministic deployment (in viem's chain def)
NOXA LaunchFactory0xD9eC2db5f3D1b236843925949fe5bd8a3836FCcBextracted from official frontend bundle; event topics matched to live logs
Odyssey BondingCurveFactory0xEb3FeeD2716cF0eEAda05B22e67424794e1f5a80extracted 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