# 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.

- Canonical: https://triport.io/blog/robinhood-stock-tokens-onchain-data
- Published: 2026-10-13
- Updated: 2026-10-24
- Author: Triport Engineering
- Category: Engineering
- Networks: Robinhood Chain

## TL;DR

- A Stock Token is an 18-decimal ERC-20 whose raw balance never changes on a dividend or split; the corporate action moves uiMultiplier() instead, and share-equivalent units are raw × uiMultiplier ÷ 1e18 — on 2026-10-13 that integer formula reproduced the contract's own totalSupplyUI() and a transfer's uiValue exactly.
- The issuer's Chainlink feed already includes the multiplier, so token value is balance × feed price; applying uiMultiplier to that price counts the corporate action twice.
- Every movement we sampled emitted two logs — a standard Transfer and a TransferWithScaledUI right after it — so a tracker must pick one event per movement, de-duplicate on (blockHash, transactionHash, logIndex) and page eth_getLogs by contract address.

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](https://docs.robinhood.com/chain/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](https://triport.io/learn/glossary/abi) encoding and an ordinary [`eth_call`](https://triport.io/docs/robinhood/json-rpc/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](https://docs.robinhood.com/chain/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](https://docs.robinhood.com/chain/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:

```bash
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 `number` keeps only 53 bits of mantissa, so `raw * m / 1e18` in floating point silently loses the low digits. Use `BigInt` (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.

```ts
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](https://docs.robinhood.com/chain/oracles-and-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](https://docs.chain.link/data-feeds/tokenized-equity-feeds/robinhood); 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:

```text
Transfer              0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
TransferWithScaledUI  0x37e7f0db430edc9dd31bc66f25f8449353aa0818f503b906747dd8f286cd3802
```

If 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](https://triport.io/docs/robinhood/guides/track-stock-token-transfers):

- Identify a log by `(blockHash, transactionHash, logIndex)`; delivery over the [`logs` subscription](https://triport.io/docs/robinhood/ws/logs) is not exactly-once.
- Handle `removed: true` by undoing what you applied — see [reorgs](https://triport.io/learn/glossary/reorg) — and record a cancellation even if it arrives first.
- Backfill gaps with [`eth_getLogs`](https://triport.io/docs/robinhood/json-rpc/eth_getLogs), always with an `address` list, 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](https://triport.io/blog/eth-getlogs-block-range-limits).
- Subscribe to `UIMultiplierUpdated(uint256,uint256,uint256)` (topic `0x2205df4534432b2f60654a3fdb48737ffdaf3e9edb1a498bd985bc026b15b055`) and re-read `uiMultiplier()` 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](https://triport.io/docs/robinhood/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](https://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](https://triport.io/docs/robinhood/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_call`s are enough; do not build a log pipeline for it.

## Sources

1. Robinhood, [Stock Tokens](https://docs.robinhood.com/chain/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).
2. Robinhood, [Building with Stock Tokens](https://docs.robinhood.com/chain/building-with-stock-tokens/), read 2026-10-13: multiplier conversion formula, `1e18 = 1.0`, launch value `1e18`, `balanceOfUI`, `totalSupplyUI`, `newUIMultiplier`, `effectiveAt`, `UIMultiplierUpdated`, `TransferWithScaledUI`, "don't apply the multiplier yourself".
3. Robinhood, [Oracles & Price Feeds](https://docs.robinhood.com/chain/oracles-and-price-feeds/), read 2026-10-13: feed price = share price × multiplier, display formulas, `oraclePaused()`, sequencer uptime check.
4. Robinhood, [Token Contracts](https://docs.robinhood.com/chain/contracts/), read 2026-10-13: canonical addresses; matching name or ticker at another address is not a Stock Token.
5. 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`.
6. Chainlink, [Robinhood tokenized equity feeds](https://docs.chain.link/data-feeds/tokenized-equity-feeds/robinhood): feed addresses and parameters.
7. Triport production reads, 2026-10-13, block `0x539e561` and transaction `0x7b9aa9f622ed9ebb8751a9761a7d4d423a4ae4100193c209697fd1682d95f9c1`, via `https://triport.io/rpc/robinhood`: the values in the tables above; `totalSupplyUI()` and `uiValue` matched the integer formula with zero difference on two contracts.
8. Triport, [eth_call](https://triport.io/docs/robinhood/json-rpc/eth_call), [eth_getLogs](https://triport.io/docs/robinhood/json-rpc/eth_getLogs) and [limits](https://triport.io/docs/robinhood/limits) on Robinhood Chain: `robinhood_read_rpc` and `robinhood_read_rpc_heavy` 15 / 20 / 100 / 250 (default, unmeasured).

## Try it

- Reference: [Track Stock Token transfers on Robinhood Chain](https://triport.io/docs/robinhood/guides/track-stock-token-transfers)
- Network: [Robinhood Chain RPC — endpoints, limits and plans](https://triport.io/robinhood-rpc)
