Robinhood Chain Testnet starter: keyless reads, WebSocket logs, then mainnet
A three-step viem starter for Robinhood Chain Testnet: keyless public JSON-RPC reads, programmatic wallet sign-in with WebSocket logs, then the same key on mainnet.
BlockVectra serves Robinhood Chain Testnet (chain ID 46630) over JSON-RPC and WebSocket, and the same API key also works on Robinhood Chain mainnet. This guide follows the Robinhood Chain Testnet starter template through three steps: read the testnet without a key, open an account programmatically and stream logs, then move the same key to mainnet. For endpoint parameters, method policy, and tokenized stock data, see the Robinhood Chain guide.
Run the template
git clone https://github.com/blockvectra/robinhood-testnet-starter
cd robinhood-testnet-starter
npm install
npm run typecheckNode.js 18 or newer. The three scripts are npm run step1, npm run step2, and npm run step3. Optional variables (BLOCKVECTRA_API_KEY, WALLET_PRIVATE_KEY) are read from the shell; nothing loads .env automatically.
Step 1: read the testnet without a key
The public endpoint https://api.blockvectra.com/v1/robinhood_testnet/public serves wallet JSON-RPC methods with no account and no API key. Query the EIP-155 chain ID directly:
curl -s "https://api.blockvectra.com/v1/robinhood_testnet/public" \
-H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"eth_chainId","params":[]}'The response is {"jsonrpc":"2.0","id":1,"result":"0xb626"}; 0xb626 is the hexadecimal chain ID, which is 46630 in decimal.
The template reads the chain entry from GET /v1/chains and builds a viem client from it, so the chain name and ID are not hard-coded. From src/shared.ts:
import { createPublicClient, defineChain, http, webSocket } from "viem";
export const API_BASE = "https://api.blockvectra.com/v1";
export interface ChainEntry {
chain: string;
name: string;
chain_id: number;
data: boolean;
ws: boolean;
subscriptions: string[];
public?: { url: string };
}
export async function getChain(slug: string): Promise<ChainEntry> {
const res = await fetch(`${API_BASE}/chains`);
if (!res.ok) throw new Error(`GET /v1/chains failed: ${res.status}`);
const { chains } = (await res.json()) as { chains: ChainEntry[] };
const entry = chains.find((c) => c.chain === slug);
if (!entry) throw new Error(`${slug} not found in /v1/chains`);
return entry;
}
export function toViemChain(entry: ChainEntry, rpcUrl: string) {
return defineChain({
id: entry.chain_id,
name: entry.name,
nativeCurrency: { name: "Ether", symbol: "ETH", decimals: 18 },
rpcUrls: { default: { http: [rpcUrl] } },
});
}
export function httpClient(entry: ChainEntry, url: string, apiKey?: string) {
return createPublicClient({
chain: toViemChain(entry, url),
transport: http(url, apiKey ? { fetchOptions: { headers: { "x-api-key": apiKey } } } : undefined),
});
}
export function wsClient(entry: ChainEntry, apiKey: string) {
const base = "wss://api.blockvectra.com/v1";
const url = `${base}/${entry.chain}/${apiKey}`;
return createPublicClient({ chain: toViemChain(entry, url), transport: webSocket(url) });
}With that module, src/step1-public-read.ts reads the chain ID, the latest block, and an address balance:
import { formatEther, type Address } from "viem";
import { API_BASE, getChain, httpClient } from "./shared.js";
const SLUG = "robinhood_testnet";
// Any address works; override with the first CLI argument.
const address = (process.argv[2] ?? "0x0000000000000000000000000000000000000000") as Address;
const chain = await getChain(SLUG);
const client = httpClient(chain, `${API_BASE}/${SLUG}/public`);
const [chainId, blockNumber, balance] = await Promise.all([
client.getChainId(),
client.getBlockNumber(),
client.getBalance({ address }),
]);
console.log(`chain : ${chain.name} (${SLUG})`);
console.log(`eth_chainId : ${chainId} (0x${chainId.toString(16)})`);
console.log(`block : ${blockNumber}`);
console.log(`balance : ${formatEther(balance)} ETH (${address})`);Step 2: open an account programmatically and subscribe to logs
src/step2-key-and-logs.ts signs in with a wallet (SIWE, EIP-191) at https://console-api.blockvectra.com/v1. A first sign-in creates the account; the client then creates an API key. New accounts get 30,000,000 CU on sign-up — no credit card.
import { generatePrivateKey, privateKeyToAccount } from "viem/accounts";
import type { Hex } from "viem";
const CONSOLE_API = "https://console-api.blockvectra.com/v1";
const REF = "gh-robinhood-testnet-starter";
async function post<T>(path: string, body: unknown, token?: string): Promise<T> {
const res = await fetch(`${CONSOLE_API}${path}`, {
method: "POST",
headers: { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}) },
body: JSON.stringify(body),
});
if (!res.ok) throw new Error(`${path} failed: ${res.status} ${await res.text()}`);
return (await res.json()) as T;
}
async function provisionKey(): Promise<string> {
const pk = (process.env.WALLET_PRIVATE_KEY as Hex | undefined) ?? generatePrivateKey();
const account = privateKeyToAccount(pk);
console.log(`wallet: ${account.address}`);
// SIWE: ask for a challenge, sign it verbatim (EIP-191), log in. A first sign-in creates the account.
const { message } = await post<{ message: string }>("/auth/siwe/challenge", {
address: account.address,
purpose: "login",
});
const signature = await account.signMessage({ message });
const login = await post<{ session: { token: string }; account_created: boolean }>("/auth/siwe/login", {
message,
signature,
ref: REF,
});
console.log(`account_created: ${login.account_created}`);
const created = await post<{ api_key: string }>("/keys", { label: "robinhood-testnet-starter" }, login.session.token);
return created.api_key;
}
const apiKey = process.env.BLOCKVECTRA_API_KEY?.trim() || (await provisionKey());If you already have a key, export BLOCKVECTRA_API_KEY=... skips sign-up. Without WALLET_PRIVATE_KEY, the script generates a throwaway wallet in memory and discards it when the run ends; set WALLET_PRIVATE_KEY from your local environment if you want to sign in to the same account again.
The WebSocket URL puts the key in the path: wss://api.blockvectra.com/v1/robinhood_testnet/{api_key}. The template checks the live ws flag and the subscriptions list from GET /v1/chains before subscribing:
const chain = await getChain("robinhood_testnet");
if (!chain.ws || !chain.subscriptions.includes("logs")) {
throw new Error(`robinhood_testnet does not offer a logs subscription in /v1/chains`);
}
console.log(`subscriptions (from /v1/chains): ${chain.subscriptions.join(", ")}`);
const client = wsClient(chain, apiKey);
const stop = client.watchEvent({
address: "0x1234567890123456789012345678901234567890",
onLogs: (logs) => {
for (const l of logs) console.log(`block ${l.blockNumber} ${l.address} topic0=${l.topics[0]}`);
},
onError: (e) => console.error("subscription error:", e.message),
});Every logs subscription filter must include an address or a topic0; replace the example address with the contract you want to watch. See the WebSocket subscriptions guide for filter rules, close codes, and reconnection.
Step 3: switch the same key to mainnet
Mainnet is a one-word change: use the slug robinhood_mainnet and the same API key. src/step3-mainnet.ts reads the mainnet block number and calls the Data API for Robinhood stock tokens:
const apiKey = process.env.BLOCKVECTRA_API_KEY?.trim();
if (!apiKey) throw new Error("Set BLOCKVECTRA_API_KEY (run step 2 first, or create a key in the console).");
const mainnet = await getChain("robinhood_mainnet");
// JSON-RPC with the key: the chain slug changes.
const client = httpClient(mainnet, `${API_BASE}/${mainnet.chain}`, apiKey);
console.log(`mainnet block: ${await client.getBlockNumber()}`);
// Data API: Robinhood stock-token leaderboard (see the Robinhood Chain guide).
const res = await fetch(`${API_BASE}/data/${mainnet.chain}/stocks`, { headers: { "x-api-key": apiKey } });
if (!res.ok) throw new Error(`Data API failed: ${res.status} ${await res.text()}`);
console.log(JSON.stringify(await res.json(), null, 2).slice(0, 2000));The same leaderboard endpoint in curl:
curl -s -H "x-api-key: $BLOCKVECTRA_API_KEY" \
"https://api.blockvectra.com/v1/data/robinhood_mainnet/stocks"The Data API is mainnet-only: GET /v1/chains reports data: false for robinhood_testnet and data: true for robinhood_mainnet. For request parameters and response envelopes, see the Tokenized stocks guide.
Discover limits and capabilities from the API
Read GET /v1/chains for the live jsonrpc, data, ws, and subscriptions flags, the allowed JSON-RPC methods, log block ranges, state retention, and each chain's public endpoint. Read GET /v1/plans for current prices and plan limits.
curl -s "https://api.blockvectra.com/v1/chains"Next steps
- Browse the datasets directory to see every dataset BlockVectra indexes.
- See the free plan and pricing to check what your account includes.
- Log in to the console to create an API key.
Last updated:
Robinhood Chain
Connect to Robinhood Chain mainnet via BlockVectra JSON-RPC and Data API: chain ID, endpoints, curl examples, method policy, and tokenized stock data.
Stock token multiplier
Understand the Robinhood Chain stock token multiplier, convert balances to shares, read uiMultiplier via eth_call, and track holders and daily activity via the Data API.