Reading Robinhood Stock Tokens on-chain: balances, uiMultiplier and transfers
A Stock Token balance is not a share count, and the price feed already knows that. Which contract values to read over JSON-RPC, the one formula that connects them — checked against the chain to the last unit — and the transfer events that are easy to count twice.
Endpoint and plans: Robinhood Chain RPC endpoint.
If you show a Robinhood Stock Token position in a wallet, a portfolio view or an analytics table, you need two contract reads, not one: balanceOf(holder) for the raw amount and uiMultiplier() for the corporate-action ratio. The raw balance is what the token contract stores and what transfers move; it stays the same when a dividend is reinvested or a split happens. The multiplier is what changes. Share-equivalent units are raw × uiMultiplier ÷ 1e18, and the token's price feed already has the multiplier inside it.
That much is in Robinhood's developer documentation. What this post adds is a check of the formula against live contract state, done over JSON-RPC on 2026-10-13, and two details that break real trackers: integer precision when you apply the formula in JavaScript, and the fact that each transfer we sampled emitted two events, not one. Everything below is about reading public chain data. It is not a view on any asset, and the section on availability explains who these tokens are and are not offered to.
What a Stock Token is, in contract terms
Per the official Stock Tokens page, each Stock Token is a standard ERC-20 contract with 18 decimals, and each one corresponds to a specific underlying equity or ETF. There is no special SDK: balanceOf, transfer, approve and totalSupply behave like any other token, so the usual ABI encoding and an ordinary eth_call are all you need.
On top of ERC-20 the tokens implement ERC-8056, the "Scaled UI Amount Extension". The Building with Stock Tokens guide spells out the consequence: the multiplier "scales the effective amount without changing raw balances or total supply — balanceOf() and totalSupply() stay fixed. Stock tokens are not rebasing tokens." The multiplier is a fixed-point number with 18 decimals, so 1e18 means 1.0, and the guide states it starts at 1e18 at launch.
Contract addresses come from the official Token Contracts page. That page also carries the rule that matters for any token list you build: a token with a matching name or ticker but a different contract address is not a Robinhood Stock Token. Pin addresses from that page; never match on symbol() or name().
The values you read and how they fit together
This is the whole data model for a position view. The selectors in the second column are the first four bytes of keccak256 of the signature; we derived them in code and confirmed each one answers on chain.
| What you want | Read | Formula | Common mistake |
|---|---|---|---|
| Raw token amount | balanceOf(address) · 0x70a08231 | raw ÷ 10^decimals() for display; decimals() returns 18 | Showing the raw amount as a share count |
| Corporate-action ratio | uiMultiplier() · 0xa60bf13d | uiMultiplier ÷ 1e18 = shares per token | Caching it forever; it changes on dividends and splits |
| Share-equivalent units | balanceOfUI(address) · 0x437a9958, or compute | raw × uiMultiplier ÷ 1e18, then ÷ 10^18 for display | Doing the multiplication in floating point |
| Pending change | newUIMultiplier() · 0xdc767007, effectiveAt() · 0x97a4064f | Active from the effectiveAt timestamp | Ignoring a scheduled change and showing stale units |
| Token value | Chainlink feed latestRoundData() | raw ÷ 1e18 × feed price | Multiplying the feed price by uiMultiplier again |
| Price pause | oraclePaused() · 0x7706ba52 | true = treat the price as temporarily unavailable | Treating the flag as a guarantee; it is advisory |
The two computations that people get wrong are the third and fifth rows, so they get their own sections.
Reading the multiplier over RPC
A read is one eth_call with the selector as data. Pin a block number when you read several values that must agree with each other — here the multiplier and a balance — so both answers describe the same state:
curl -s https://triport.io/rpc/robinhood \
-H "x-token: $TRIPORT_API_KEY" -H "content-type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_call",
"params":[{"to":"0x03dfbbe0ac4e7bcdafd08ed41a400326b77d8c80","data":"0xa60bf13d"},"0x539e561"]}'These are the answers we received on 2026-10-13 at block 0x539e561 (87,680,353) for that contract, an ERC-8056 token taken from an on-chain transfer log. Verify any address against the official Token Contracts page before you rely on it.
| Call | Raw result (last word) | Decoded |
|---|---|---|
decimals() | 0x…12 | 18 |
uiMultiplier() | 0x…0de0ca3f404bad8a | 1,000,021,490,286,701,962 → 1.000021490286701962 |
newUIMultiplier() | 0x…0de0ca3f404bad8a | same as the active value |
effectiveAt() | 0x…6aa176e2 | 1,788,966,626 → 2026-09-09 15:10:26 UTC, already in the past |
oraclePaused() | 0x…00 | false |
The multiplier above 1.0 means at least one corporate action has already been applied to this token since launch. With newUIMultiplier() equal to the active value and effectiveAt in the past, no change was pending at that block.
Checking the formula against the chain
The guide's formula is underlying shares = raw token amount × uiMultiplier ÷ 1e18. Two values on the same contract let you test it without trusting anyone's arithmetic, because the contract computes them itself.
First, totalSupplyUI() against totalSupply(). At the same pinned block, totalSupply() × uiMultiplier() ÷ 1e18 with integer division equalled totalSupplyUI() exactly — a difference of zero units. We repeated the check on a second ERC-8056 contract with a different multiplier, with the same result.
Second, a transfer. A TransferWithScaledUI log carries both the raw value and the contract's uiValue. One log from block 0x539e54d carried value = 241,704,333,457,137,341 and uiValue = 241,709,527,752,560,441. Multiplying the first by the multiplier from the table and dividing by 1e18 with truncation gives the second, to the last unit.
Two practical rules follow:
- Use integer arithmetic. Raw amounts in the samples above are 10^17 and larger, and the multiplier is about 10^18, so the product exceeds 10^35. A JavaScript
numberkeeps only 53 bits of mantissa, soraw * m / 1e18in floating point silently loses the low digits. UseBigInt(or your language's big-integer type) and divide last. - Truncate, as the contract does. Integer division rounds toward zero. If you round to nearest, your computed units can differ by one base unit from
balanceOfUI(), which shows up as a reconciliation diff that is not a bug in the chain.
const SCALE = 10n ** 18n;
const units = (raw: bigint, multiplier: bigint) => (raw * multiplier) / SCALE; // still 18-decimal
const display = (x: bigint) => `${x / SCALE}.${(x % SCALE).toString().padStart(18, "0")}`;If the contract exposes balanceOfUI(), reading it is simpler than computing. Computing is still worth doing once as a test, because it proves your decoding of both numbers is right.
Prices: the multiplier is already inside
Robinhood's Oracles & Price Feeds page states that the feed "returns the price of one token, which is the underlying share price times the multiplier", and the guide repeats it: "don't apply the multiplier yourself." So:
- Token value = raw balance ÷ 10^18 × feed price ÷ 10^
decimals()of the feed. No multiplier. - Underlying share price, if you need it for display =
feedPrice × 1e18 ÷ uiMultiplier. - Share-equivalent units =
raw × uiMultiplier ÷ 1e18.
The economic value is the same in all three; only the unit differs. The bug to avoid is mixing them: share-equivalent units × the per-token feed price counts the corporate action twice. The same page lists the checks every feed consumer needs — read decimals() instead of hard-coding it, reject stale updatedAt values, check the L2 sequencer uptime feed, and read oraclePaused() on the token while a corporate action is processed. Feed addresses are maintained by Chainlink on its Robinhood feeds page; read them from there rather than copying them into code.
Transfers: one movement, two logs
In the transaction behind the log above, the token contract emitted a standard Transfer(address,address,uint256) at log index 1 and a TransferWithScaledUI(address,address,uint256,uint256) at log index 2 for the same movement, and the pattern repeated at indexes 5 and 6. Their topics[0] values are:
Transfer 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
TransferWithScaledUI 0x37e7f0db430edc9dd31bc66f25f8449353aa0818f503b906747dd8f286cd3802If your filter lists both topics, every movement arrives twice with different logIndex values, so de-duplication will not merge them. Pick one. Transfer keeps your code identical to any ERC-20 tracker; TransferWithScaledUI saves a multiplier lookup because it carries uiValue. One more trap: the ERC-8056 text names its optional event TransferWithUIAmount, while Robinhood's docs and the contracts we read emit TransferWithScaledUI. Derive the topic from the name the contract actually emits, and assert it against a known log at startup — a wrong topic matches nothing and looks exactly like a quiet market.
The rest is the standard log-tracking discipline, covered step by step in our Stock Token transfers guide:
- Identify a log by
(blockHash, transactionHash, logIndex); delivery over thelogssubscription is not exactly-once. - Handle
removed: trueby undoing what you applied — see reorgs — and record a cancellation even if it arrives first. - Backfill gaps with
eth_getLogs, always with anaddresslist, in windows you shrink on a range error. Range ceilings on this chain depend on the serving channel; the paging strategy is in eth_getLogs block range limits. - Subscribe to
UIMultiplierUpdated(uint256,uint256,uint256)(topic0x2205df4534432b2f60654a3fdb48737ffdaf3e9edb1a498bd985bc026b15b055) and re-readuiMultiplier()when it fires, instead of polling the multiplier on every render.
Budget the backfill against the right bucket. On Triport, eth_call is charged to robinhood_read_rpc and eth_getLogs to robinhood_read_rpc_heavy; both are 15 / 20 / 100 / 250 requests per second sustained on trial / Basic / Pro / Business, per Robinhood Chain limits. The WebSocket surface is available from Basic.
Scheduled multiplier changes
A corporate action can be scheduled before it takes effect. ERC-8056 requires the pending-multiplier extension, and the official guide documents it: newUIMultiplier() is the value that becomes active at effectiveAt(), and before anything is scheduled it tracks the current multiplier. A position view that wants to be right at the boundary reads all three values at one pinned block and switches by comparing effectiveAt with the block timestamp, not with the local clock. A transfer history that needs historic units should store uiValue from TransferWithScaledUI, or the multiplier active at that block, rather than re-deriving old rows from today's multiplier.
Availability and legal status
Robinhood's documentation describes Stock Tokens as tokenised debt securities issued by Robinhood Assets (Jersey) Limited that provide economic exposure to underlying securities but grant no legal or beneficial rights in, or against the issuer of, those securities. The same page states that they are not registered under U.S. securities laws and may not be offered, sold or delivered in the United States or to U.S. persons, and that offers and sales are restricted in other jurisdictions, including Canada, the United Kingdom and Switzerland, with the full list published at docs.robinhood.com/rhj.
This post covers reading public contract data over JSON-RPC. It is not investment, legal or tax advice, and it expresses no view on any token. Triport serves chain data only — as our Robinhood Chain ecosystem page puts it, the issuer's own APIs are separate products with their own terms and availability, and we neither proxy nor mirror them. If your product shows these tokens to end users, the issuer's eligibility rules decide who may acquire them.
When this is the wrong approach
- You need instrument data, not chain data. Trading sessions, corporate-action announcements and reference data come from the issuer's own services, not from the token contract.
- You need a price in a contract. Read the Chainlink feed on chain with its staleness and sequencer checks; an RPC read in an off-chain service is not a substitute.
- You need a full holder list. Reconstructing every balance from genesis is an indexer job. Replay transfers into a database once, then keep it current from logs.
- You only need one wallet's position. Two
eth_calls are enough; do not build a log pipeline for it.
Sources
- Robinhood, Stock Tokens, read 2026-10-13: ERC-20 with 18 decimals, one underlying per token,
uiMultiplier()via ERC-8056, raw balance static, disclaimer (issuer, U.S. and other jurisdiction restrictions). - Robinhood, Building with Stock Tokens, read 2026-10-13: multiplier conversion formula,
1e18 = 1.0, launch value1e18,balanceOfUI,totalSupplyUI,newUIMultiplier,effectiveAt,UIMultiplierUpdated,TransferWithScaledUI, "don't apply the multiplier yourself". - Robinhood, Oracles & Price Feeds, read 2026-10-13: feed price = share price × multiplier, display formulas,
oraclePaused(), sequencer uptime check. - Robinhood, Token Contracts, read 2026-10-13: canonical addresses; matching name or ticker at another address is not a Stock Token.
- ERC-8056, "Scaled UI Amount Extension for ERC-20 Tokens" — https://ercs.ethereum.org/ERCS/erc-8056, read 2026-10-13: 18-decimal multiplier, required pending-multiplier extension, optional event named
TransferWithUIAmount. - Chainlink, Robinhood tokenized equity feeds: feed addresses and parameters.
- Triport production reads, 2026-10-13, block
0x539e561and transaction0x7b9aa9f622ed9ebb8751a9761a7d4d423a4ae4100193c209697fd1682d95f9c1, viahttps://triport.io/rpc/robinhood: the values in the tables above;totalSupplyUI()anduiValuematched the integer formula with zero difference on two contracts. - Triport, eth_call, eth_getLogs and limits on Robinhood Chain:
robinhood_read_rpcandrobinhood_read_rpc_heavy15 / 20 / 100 / 250 (default, unmeasured).