20 min read
Overview
Robinhood Chain is a permissionless, EVM-compatible Arbitrum Orbit Layer-2 built on Ethereum, using ETH as its native gas token. Stock Tokens, ERC-20 tokens named for individual stocks and ETFs, are available 24/7, composable with other smart contracts, and accessible through standard Ethereum tooling and RPC endpoints.
In this guide, you will use Quicknode RPC and viem to read Stock Token data directly from Robinhood Chain. You'll build a single TypeScript script with four functions to retrieve token metadata, total supply, wallet balances, and Transfer event history for Stock Tokens including TSLA, AAPL, NVDA, and AMZN.
What You Will Do
- Read the
name,symbol, anddecimalsfor each Stock Token in a single multicall request - Fetch
totalSupply()to see how much of each Stock Token is minted onchain - Query
balanceOf(wallet)to check wallet balances across all tracked tokens - Decode
Transferevent history for a chosen token over a configurable block range
What You Will Need
- A Quicknode account with a Robinhood Chain mainnet endpoint
- Node.js v20.6 or higher and npm (for native
.envloading via--env-file) - Basic familiarity with TypeScript and ERC-20 token concepts
- Stock Tokens (TSLA, AAPL, NVDA, AMZN, and more) are ERC-20 tokens on Robinhood Chain
- Standard ERC-20 methods (
name,symbol,decimals,totalSupply,balanceOf) and theTransferevent work via any Quicknode Robinhood Chain RPC endpoint - viem's
multicallbatches all reads into a single RPC call;formatUnits(supply, decimals)converts raw bigints to whole-token amounts - Quicknode Streams allows you to get real-time updates to your destination, without polling
Project Setup
Create the project directory, install dependencies, and configure package.json from the command line:
mkdir robinhood-chain-reader && cd robinhood-chain-reader
npm init -y
npm install viem
npm install -D tsx typescript @types/node
npm pkg set type=module
tsx runs TypeScript directly with no build step, but your editor's TypeScript language server still needs a tsconfig.json to resolve @types/node (otherwise it flags globals like process as errors) and to know the project targets ESM. Add one at the project root:
{
"compilerOptions": {
"target": "ESNext",
"module": "ESNext",
"moduleResolution": "bundler",
"types": ["node"],
"strict": true,
"skipLibCheck": true
},
"include": ["*.ts"]
}
The --env-file=.env flag (used below) loads environment variables natively, so there's no need for a dotenv dependency.
Create a .env file at the project root (never commit this file):
RPC_URL=YOUR_QUICKNODE_ROBINHOOD_ENDPOINT
# Optional: only needed to run the balance command without passing an address
WALLET_ADDRESS=0xYourWalletAddress
To get your RPC_URL, create a Robinhood Chain endpoint by following the Quicknode Robinhood quickstart.
Configure the Script
Create read-stocks.ts. The top of the file sets up the viem client, defines the token registry, and declares a single shared ABI used by all four functions.
import {
createPublicClient,
http,
defineChain,
parseAbi,
formatUnits,
getAddress,
Address,
} from 'viem';
const RPC_URL = process.env.RPC_URL;
if (!RPC_URL) throw new Error('RPC_URL is not set in .env');
const { result: chainIdHex } = await fetch(RPC_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_chainId', params: [] }),
}).then((res) => res.json());
const client = createPublicClient({
chain: defineChain({
id: parseInt(chainIdHex, 16),
name: 'Robinhood Chain',
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
rpcUrls: { default: { http: [RPC_URL] } },
contracts: {
multicall3: { address: '0xcA11bde05977b3631167028862bE2a173976CA11' },
},
}),
transport: http(RPC_URL),
});
const TOKENS: Record<string, { name: string; address: Address }> = {
TSLA: { name: 'Tesla Inc.', address: '0x322F0929c4625eD5bAd873c95208D54E1c003b2d' },
AAPL: { name: 'Apple Inc.', address: '0xaF3D76f1834A1d425780943C99Ea8A608f8a93f9' },
NVDA: { name: 'NVIDIA Corporation', address: '0xd0601CE157Db5bdC3162BbaC2a2C8aF5320D9EEC' },
AMZN: { name: 'Amazon.com Inc.', address: '0x12f190a9F9d7D37a250758b26824B97CE941bF54' },
MSFT: { name: 'Microsoft Corp.', address: '0xe93237C50D904957Cf27E7B1133b510C669c2e74' },
GOOGL: { name: 'Alphabet Inc.', address: '0x2e0847E8910a9732eB3fb1bb4b70a580ADAD4FE3' },
META: { name: 'Meta Platforms', address: '0xc0D6457C16Cc70d6790Dd43521C899C87ce02f35' },
MSTR: { name: 'MicroStrategy', address: '0xec262a75e413fAfD0dF80480274532C79D42da09' },
SPY: { name: 'SPDR S&P 500 ETF', address: '0x117cc2133c37B721F49dE2A7a74833232B3B4C0C' },
QCOM: { name: 'Qualcomm Inc.', address: '0x0f17206447090e464C277571124dD2688E48AEA9' },
};
const SYMBOLS = Object.keys(TOKENS);
const ABI = parseAbi([
'function name() view returns (string)',
'function symbol() view returns (string)',
'function decimals() view returns (uint8)',
'function totalSupply() view returns (uint256)',
'function balanceOf(address) view returns (uint256)',
'event Transfer(address indexed from, address indexed to, uint256 value)',
]);
const TRANSFER_EVENT = ABI.find((x) => x.type === 'event' && x.name === 'Transfer')!;
const col = (s: string, w: number) => s.slice(0, w).padEnd(w);
const padCol = (s: string, w: number) => s.padEnd(w);
defineChain registers Robinhood Chain with viem using the chain ID fetched directly from your RPC endpoint via eth_chainId, but you can also specify it manually if needed. The contracts.multicall3 entry points to the canonical Multicall3 deployment, which Robinhood Chain includes at the standard address; without it, viem's multicall() throws a ChainDoesNotSupportContract error. The TOKENS map is the single source of truth for all four operations. A single ABI array covers every function and the Transfer event; TRANSFER_EVENT extracts the typed event entry for use with getLogs.
The TOKENS map above uses Robinhood Chain mainnet token contract addresses. Quicknode supports both Robinhood Chain mainnet and testnet. This guide uses mainnet by default; if you want to test against Robinhood Chain testnet, update your RPC_URL and replace the token contract addresses with the matching testnet deployments.
Read Stock Data
With the chain client, token registry, and ABI in place, add the CLI dispatch next so the commands below actually do something. It reads the command name from process.argv[2] and calls the matching function. Add it directly below the setup code; you'll add the four functions it calls in the sections that follow, in order, so each command becomes runnable as soon as its function exists.
const cmd = process.argv[2];
const walletArg = process.argv[3] ?? process.env.WALLET_ADDRESS;
switch (cmd) {
case 'metadata':
await readMetadata();
break;
case 'supply':
await readSupply();
break;
case 'balance':
if (!walletArg) { console.error('Usage: npx tsx --env-file=.env read-stocks.ts balance <address>'); process.exit(1); }
await readBalances(getAddress(walletArg));
break;
case 'transfers':
await readTransfers(
process.env.TOKEN ?? 'TSLA',
parseInt(process.env.BLOCK_RANGE ?? '5000', 10),
);
break;
default:
await readMetadata();
await readSupply();
break;
}
Each npx tsx --env-file=.env read-stocks.ts <command> call below passes the command name as process.argv[2]; running with no recognized command (or no argument) runs metadata and supply together as a quick sanity check.
Read Token Metadata
readMetadata fetches name(), symbol(), and decimals() for every token in a single multicall request.
async function readMetadata() {
const contracts = SYMBOLS.flatMap((sym) => {
const { address } = TOKENS[sym];
return [
{ address, abi: ABI, functionName: 'name' as const },
{ address, abi: ABI, functionName: 'symbol' as const },
{ address, abi: ABI, functionName: 'decimals' as const },
];
});
const results = await client.multicall({ contracts, allowFailure: true });
const rows = SYMBOLS.map((sym, i) => {
const base = i * 3;
return {
sym,
name: results[base].status === 'success' ? String(results[base].result) : '?',
symbol: results[base+1].status === 'success' ? String(results[base+1].result) : '?',
decimals: results[base+2].status === 'success' ? String(results[base+2].result) : '?',
};
});
const symWidth = Math.max('Sym'.length, ...rows.map((row) => row.sym.length));
const nameWidth = Math.max('Name (onchain)'.length, ...rows.map((row) => row.name.length));
const symbolWidth = Math.max('Symbol'.length, ...rows.map((row) => row.symbol.length));
const decimalsWidth = 'Decimals'.length;
const tableWidth = symWidth + nameWidth + symbolWidth + decimalsWidth + 6;
console.log('\n── Token Metadata ──');
console.log(padCol('Sym', symWidth), padCol('Name (onchain)', nameWidth), padCol('Symbol', symbolWidth), 'Decimals');
console.log('─'.repeat(tableWidth));
rows.forEach(({ sym, name, symbol, decimals }) => {
console.log(padCol(sym, symWidth), padCol(name, nameWidth), padCol(symbol, symbolWidth), decimals);
});
}
multicall batches all calls (3 per token, 30 total for 10 stocks) into a single RPC request via the Multicall3 contract. allowFailure: true means one bad address does not abort the batch; errors surface per entry.
npx tsx --env-file=.env read-stocks.ts metadata
Expected output:
── Token Metadata ──
Sym Name (onchain) Symbol Decimals
───────────────────────────────────────────────────────
TSLA Tesla • Robinhood Token TSLA 18
AAPL Apple • Robinhood Token AAPL 18
NVDA NVIDIA • Robinhood Token NVDA 18
...
A successful table confirms each address is a live ERC-20 on mainnet.
Read Total Supply
readSupply fetches totalSupply() and decimals() per token, then converts the raw bigint to a human-readable token amount.
async function readSupply() {
const contracts = SYMBOLS.flatMap((sym) => {
const { address } = TOKENS[sym];
return [
{ address, abi: ABI, functionName: 'totalSupply' as const },
{ address, abi: ABI, functionName: 'decimals' as const },
];
});
const results = await client.multicall({ contracts, allowFailure: true });
console.log('\n── Total Supply ──');
console.log(col('Sym', 6), col('Supply (formatted)', 22), 'Raw');
console.log('─'.repeat(50));
SYMBOLS.forEach((sym, i) => {
const supplyR = results[i * 2];
const decimalsR = results[i * 2 + 1];
if (supplyR.status !== 'success' || decimalsR.status !== 'success') {
console.log(col(sym, 6), 'ERROR');
return;
}
const supply = supplyR.result as bigint;
const decimals = decimalsR.result as number;
console.log(col(sym, 6), col(formatUnits(supply, decimals), 22), supply.toString());
});
}
Stock Tokens are denominated in their smallest unit, similar to wei. A totalSupply of 1000000000000000000000 with 18 decimals equals 1,000 whole tokens. formatUnits(supply, decimals) handles the conversion for any decimal precision.
npx tsx --env-file=.env read-stocks.ts supply
Expected output:
── Total Supply ──
Sym Supply (formatted) Raw
──────────────────────────────────────────────────
TSLA 1250000.0 1250000000000000000000000
AAPL 980000.5 980000500000000000000000
...
Read Address Balances
readBalances queries balanceOf(wallet) for every token in the registry. Pass the target address as a CLI argument or set WALLET_ADDRESS in .env.
async function readBalances(wallet: Address) {
const contracts = SYMBOLS.flatMap((sym) => {
const { address } = TOKENS[sym];
return [
{ address, abi: ABI, functionName: 'balanceOf' as const, args: [wallet] },
{ address, abi: ABI, functionName: 'decimals' as const },
];
});
const results = await client.multicall({ contracts, allowFailure: true });
console.log(`\n── Balances for ${wallet} ──`);
console.log(col('Sym', 6), col('Balance', 22), 'Raw');
console.log('─'.repeat(50));
SYMBOLS.forEach((sym, i) => {
const balR = results[i * 2];
const decimalsR = results[i * 2 + 1];
if (balR.status !== 'success' || decimalsR.status !== 'success') {
console.log(col(sym, 6), 'ERROR');
return;
}
const balance = balR.result as bigint;
const decimals = decimalsR.result as number;
console.log(col(sym, 6), col(formatUnits(balance, decimals), 22), balance.toString());
});
}
getAddress (used in the CLI dispatch above) checksums and validates the wallet argument before it reaches the RPC, catching malformed addresses early. Tokens with zero balance are still printed so the output covers all tracked assets.
# Pass the wallet address as a CLI argument
npx tsx --env-file=.env read-stocks.ts balance 0xYourWalletAddress
# Or set WALLET_ADDRESS in .env and run without an argument
npx tsx --env-file=.env read-stocks.ts balance
Expected output:
── Balances for 0xYour... ──
Sym Balance Raw
──────────────────────────────────────────────────
TSLA 10.5 10500000000000000000
AAPL 0.0 0
NVDA 25.0 25000000000000000000
...
Read Recent Transfers
readTransfers fetches Transfer events for a chosen token over a configurable recent block range.
async function readTransfers(symbol: string, blockRange: number) {
const token = TOKENS[symbol];
if (!token) {
console.error(`Unknown token: ${symbol}. Available: ${SYMBOLS.join(', ')}`);
return;
}
const decimals = await client.readContract({
address: token.address,
abi: ABI,
functionName: 'decimals',
});
const latest = await client.getBlockNumber();
const fromBlock = latest - BigInt(blockRange) + 1n;
console.log(`\n── ${symbol} Transfers (blocks ${fromBlock}–${latest}) ──`);
const logs = await client.getLogs({
address: token.address,
event: TRANSFER_EVENT,
fromBlock,
toBlock: latest,
});
if (!logs.length) {
console.log('No transfers in this range. Try a larger BLOCK_RANGE.');
return;
}
console.log(col('Block', 12), col('From', 44), col('To', 44), 'Value');
console.log('─'.repeat(110));
for (const log of logs) {
const { from, to, value } = log.args as { from: string; to: string; value: bigint };
console.log(
col(log.blockNumber?.toString() ?? '-', 12),
col(from, 44),
col(to, 44),
formatUnits(value, decimals),
);
}
}
getLogs with a typed event ABI decodes the indexed from and to fields from event topics and the value from the data field automatically. You get a fully typed log.args object rather than raw hex strings. TRANSFER_EVENT is the typed ABI entry extracted earlier with .find(), so there is no fragile index lookup. A transfer from the zero address is a mint; a transfer to the zero address is a burn. fromBlock adds 1n because eth_getLogs treats both bounds as inclusive, so this keeps the queried range at exactly blockRange blocks.
Quicknode RPC endpoints cap eth_getLogs at a 10,000-block range per request. If you raise BLOCK_RANGE beyond that and hit a range-limit error, split the query into smaller chunks and combine the results.
# Default: TSLA, last 5000 blocks
npx tsx --env-file=.env read-stocks.ts transfers
# Custom token and block range
TOKEN=AAPL BLOCK_RANGE=10000 npx tsx --env-file=.env read-stocks.ts transfers
Expected output:
── TSLA Transfers (blocks 15600823–15605823) ──
Block From To Value
──────────────────────────────────────────────────────────────────────────────────────────────────────────────────
15605823 0x3d3871053e98b95d0d2e88d3c2c7d4659c855e36 0x2dda8e6fbc63b4be0291c9fcfc62a8abc12cf16d 5.0
15605824 0x0000000000000000000000000000000000000000 0x3d3871053e98b95d0d2e88d3c2c7d4659c855e36 100.0
The getLogs approach reads historical data on demand. For continuous monitoring, Quicknode Streams lets you subscribe to this same Transfer event filter and push new events to your backend as each block is finalized, with no polling loop. Configure a filter (such as getting only transfers to a specific address) with the token contract address, and Streams delivers to your destination (e.g., a webhook, Postgres or S3) automatically.
Complete read-stocks.ts - copy and paste to run all four operations
import {
createPublicClient,
http,
defineChain,
parseAbi,
formatUnits,
getAddress,
Address,
} from 'viem';
// ── Chain ─────────────────────────────────────────────────────────────────────
const RPC_URL = process.env.RPC_URL;
if (!RPC_URL) throw new Error('RPC_URL is not set in .env');
const { result: chainIdHex } = await fetch(RPC_URL, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id: 1, method: 'eth_chainId', params: [] }),
}).then((res) => res.json());
const client = createPublicClient({
chain: defineChain({
id: parseInt(chainIdHex, 16),
name: 'Robinhood Chain',
nativeCurrency: { name: 'Ether', symbol: 'ETH', decimals: 18 },
rpcUrls: { default: { http: [RPC_URL] } },
contracts: {
multicall3: { address: '0xcA11bde05977b3631167028862bE2a173976CA11' },
},
}),
transport: http(RPC_URL),
});
// ── Token Registry ────────────────────────────────────────────────────────────
const TOKENS: Record<string, { name: string; address: Address }> = {
TSLA: { name: 'Tesla Inc.', address: '0x322F0929c4625eD5bAd873c95208D54E1c003b2d' },
AAPL: { name: 'Apple Inc.', address: '0xaF3D76f1834A1d425780943C99Ea8A608f8a93f9' },
NVDA: { name: 'NVIDIA Corporation', address: '0xd0601CE157Db5bdC3162BbaC2a2C8aF5320D9EEC' },
AMZN: { name: 'Amazon.com Inc.', address: '0x12f190a9F9d7D37a250758b26824B97CE941bF54' },
MSFT: { name: 'Microsoft Corp.', address: '0xe93237C50D904957Cf27E7B1133b510C669c2e74' },
GOOGL: { name: 'Alphabet Inc.', address: '0x2e0847E8910a9732eB3fb1bb4b70a580ADAD4FE3' },
META: { name: 'Meta Platforms', address: '0xc0D6457C16Cc70d6790Dd43521C899C87ce02f35' },
MSTR: { name: 'MicroStrategy', address: '0xec262a75e413fAfD0dF80480274532C79D42da09' },
SPY: { name: 'SPDR S&P 500 ETF', address: '0x117cc2133c37B721F49dE2A7a74833232B3B4C0C' },
QCOM: { name: 'Qualcomm Inc.', address: '0x0f17206447090e464C277571124dD2688E48AEA9' },
};
const SYMBOLS = Object.keys(TOKENS);
// ── ABI ───────────────────────────────────────────────────────────────────────
const ABI = parseAbi([
'function name() view returns (string)',
'function symbol() view returns (string)',
'function decimals() view returns (uint8)',
'function totalSupply() view returns (uint256)',
'function balanceOf(address) view returns (uint256)',
'event Transfer(address indexed from, address indexed to, uint256 value)',
]);
const TRANSFER_EVENT = ABI.find((x) => x.type === 'event' && x.name === 'Transfer')!;
const col = (s: string, w: number) => s.slice(0, w).padEnd(w);
const padCol = (s: string, w: number) => s.padEnd(w);
// ── 1. Read Metadata ──────────────────────────────────────────────────────────
async function readMetadata() {
const contracts = SYMBOLS.flatMap((sym) => {
const { address } = TOKENS[sym];
return [
{ address, abi: ABI, functionName: 'name' as const },
{ address, abi: ABI, functionName: 'symbol' as const },
{ address, abi: ABI, functionName: 'decimals' as const },
];
});
const results = await client.multicall({ contracts, allowFailure: true });
const rows = SYMBOLS.map((sym, i) => {
const base = i * 3;
return {
sym,
name: results[base].status === 'success' ? String(results[base].result) : '?',
symbol: results[base+1].status === 'success' ? String(results[base+1].result) : '?',
decimals: results[base+2].status === 'success' ? String(results[base+2].result) : '?',
};
});
const symWidth = Math.max('Sym'.length, ...rows.map((row) => row.sym.length));
const nameWidth = Math.max('Name (onchain)'.length, ...rows.map((row) => row.name.length));
const symbolWidth = Math.max('Symbol'.length, ...rows.map((row) => row.symbol.length));
const decimalsWidth = 'Decimals'.length;
const tableWidth = symWidth + nameWidth + symbolWidth + decimalsWidth + 6;
console.log('\n── Token Metadata ──');
console.log(padCol('Sym', symWidth), padCol('Name (onchain)', nameWidth), padCol('Symbol', symbolWidth), 'Decimals');
console.log('─'.repeat(tableWidth));
rows.forEach(({ sym, name, symbol, decimals }) => {
console.log(padCol(sym, symWidth), padCol(name, nameWidth), padCol(symbol, symbolWidth), decimals);
});
}
// ── 2. Read Total Supply ──────────────────────────────────────────────────────
async function readSupply() {
const contracts = SYMBOLS.flatMap((sym) => {
const { address } = TOKENS[sym];
return [
{ address, abi: ABI, functionName: 'totalSupply' as const },
{ address, abi: ABI, functionName: 'decimals' as const },
];
});
const results = await client.multicall({ contracts, allowFailure: true });
console.log('\n── Total Supply ──');
console.log(col('Sym', 6), col('Supply (formatted)', 22), 'Raw');
console.log('─'.repeat(50));
SYMBOLS.forEach((sym, i) => {
const supplyR = results[i * 2];
const decimalsR = results[i * 2 + 1];
if (supplyR.status !== 'success' || decimalsR.status !== 'success') {
console.log(col(sym, 6), 'ERROR');
return;
}
const supply = supplyR.result as bigint;
const decimals = decimalsR.result as number;
console.log(col(sym, 6), col(formatUnits(supply, decimals), 22), supply.toString());
});
}
// ── 3. Read Balances ──────────────────────────────────────────────────────────
async function readBalances(wallet: Address) {
const contracts = SYMBOLS.flatMap((sym) => {
const { address } = TOKENS[sym];
return [
{ address, abi: ABI, functionName: 'balanceOf' as const, args: [wallet] },
{ address, abi: ABI, functionName: 'decimals' as const },
];
});
const results = await client.multicall({ contracts, allowFailure: true });
console.log(`\n── Balances for ${wallet} ──`);
console.log(col('Sym', 6), col('Balance', 22), 'Raw');
console.log('─'.repeat(50));
SYMBOLS.forEach((sym, i) => {
const balR = results[i * 2];
const decimalsR = results[i * 2 + 1];
if (balR.status !== 'success' || decimalsR.status !== 'success') {
console.log(col(sym, 6), 'ERROR');
return;
}
const balance = balR.result as bigint;
const decimals = decimalsR.result as number;
console.log(col(sym, 6), col(formatUnits(balance, decimals), 22), balance.toString());
});
}
// ── 4. Read Transfers ─────────────────────────────────────────────────────────
async function readTransfers(symbol: string, blockRange: number) {
const token = TOKENS[symbol];
if (!token) {
console.error(`Unknown token: ${symbol}. Available: ${SYMBOLS.join(', ')}`);
return;
}
const decimals = await client.readContract({
address: token.address,
abi: ABI,
functionName: 'decimals',
});
const latest = await client.getBlockNumber();
const fromBlock = latest - BigInt(blockRange) + 1n;
console.log(`\n── ${symbol} Transfers (blocks ${fromBlock}–${latest}) ──`);
const logs = await client.getLogs({
address: token.address,
event: TRANSFER_EVENT,
fromBlock,
toBlock: latest,
});
if (!logs.length) {
console.log('No transfers in this range. Try a larger BLOCK_RANGE.');
return;
}
console.log(col('Block', 12), col('From', 44), col('To', 44), 'Value');
console.log('─'.repeat(110));
for (const log of logs) {
const { from, to, value } = log.args as { from: string; to: string; value: bigint };
console.log(
col(log.blockNumber?.toString() ?? '-', 12),
col(from, 44),
col(to, 44),
formatUnits(value, decimals),
);
}
}
// ── CLI Dispatch ──────────────────────────────────────────────────────────────
const cmd = process.argv[2];
const walletArg = process.argv[3] ?? process.env.WALLET_ADDRESS;
switch (cmd) {
case 'metadata':
await readMetadata();
break;
case 'supply':
await readSupply();
break;
case 'balance':
if (!walletArg) { console.error('Usage: npx tsx --env-file=.env read-stocks.ts balance <address>'); process.exit(1); }
await readBalances(getAddress(walletArg));
break;
case 'transfers':
await readTransfers(
process.env.TOKEN ?? 'TSLA',
parseInt(process.env.BLOCK_RANGE ?? '5000', 10),
);
break;
default:
await readMetadata();
await readSupply();
break;
}
Beyond ERC-20
The script above covers the standard ERC-20 interface and is sufficient for reading balances, supply, and transfer history for any Stock Token. However, Robinhood-issued Stock Tokens may include additional contract logic beyond what the standard interface exposes.
Likely patterns to be aware of:
- Transfer restrictions: compliance checks on the sender and receiver addresses (for example, KYC or jurisdictional gates) that prevent direct peer-to-peer transfers outside the Robinhood ecosystem
- Controlled mint and burn: supply changes may require an authorized minter role rather than being open to any caller
- Upgradeable proxy: Robinhood-issued tokens use the OpenZeppelin beacon proxy pattern, meaning all token contracts share a single upgradeable implementation. The ERC-20 interface remains stable, but the underlying logic may change
The ERC-20 interface gives you full read access: balances, supply, and event history all work as shown in this guide. To inspect the full contract implementation, look up each token address in a Robinhood Chain explorer and review the verified source code before building applications that depend on contract internals.
Conclusion
In this guide, you built a single TypeScript script that reads metadata, total supply, wallet balances, and Transfer history for any Stock Token on Robinhood Chain through Quicknode RPC and viem. The same patterns apply to any ERC-20-compatible token on the chain: add more entries to the TOKENS map as additional Stock Tokens are listed onchain.
Next Steps
- Robinhood Chain RPC API Reference: full list of supported JSON-RPC methods on Quicknode
- Robinhood Endpoint Security: protect your Quicknode endpoint with allowlists, rate limits, and JWT authentication
- Quicknode Streams: real-time blockchain data delivery to Postgres, S3, Snowflake, BigQuery, or a webhook
- Endpoint Security Best Practices: key rotation, rate limiting, and domain masking for production endpoints
- What is ERC-3643?: the token standard for compliant tokenized real-world assets on EVM chains, for broader RWA context
Frequently Asked Questions
What token standard do Stock Tokens on Robinhood Chain use?
Stock Tokens are issued as ERC-20 tokens. Standard ERC-20 reads (name, symbol, decimals, totalSupply, balanceOf, getLogs for Transfer) all work with the contract addresses as shown in this guide. The contracts may include additional compliance and access-control logic beyond the base interface.
Is there a testnet available for testing before using mainnet?
Yes. Robinhood Chain's testnet is publicly available with chain ID 46630. Network configuration details are available at https://docs.robinhood.com/chain/connecting (testnet token addresses differ from mainnet, and not all Stock Tokens may be deployed on testnet).
What is the gas token on Robinhood Chain?
ETH is the native gas token on Robinhood Chain, consistent with its Arbitrum Orbit L2 architecture. All transactions are paid in ETH.
How can I track token transfers in real time?
Use Quicknode Streams to subscribe to the Transfer event for any token contract. Configure a stream filter that matches the Transfer event topic on Robinhood Chain. Streams pushes decoded events to your chosen destination (webhook, Postgres, S3) as each block arrives, eliminating the need to poll getLogs. Full setup details are at https://www.quicknode.com/docs/streams
Where can I find contract addresses for Stock Tokens on Robinhood Chain?
This guide includes mainnet contract addresses for the TSLA, AAPL, NVDA, AMZN, MSFT, GOOGL, META, MSTR, SPY, and QCOM examples in the TOKENS map. For additional assets or testnet usage, confirm the correct contract address in the Robinhood Chain explorer or official Robinhood Chain resources before using it in production code.
We ❤️ Feedback!
Let us know if you have any feedback or requests for new topics. We'd love to hear from you.
