How to build a wallet monitoring application on Robinhood Chain

A wallet monitor needs two kinds of information: an activity feed that is useful to an application, and the underlying transaction evidence that lets you inspect what happened onchain.
This guide builds a small, read-only Node.js application that combines Chainstack’s Robinhood Chain RPC with the MadeOnSol Robinhood Chain data API. It retrieves indexed swaps for one wallet, follows pagination, and checks the corresponding receipts and log indices through Chainstack. The output is newline-delimited JSON suitable for a backend worker or a dashboard’s ingestion process.
The application observes public activity. It does not sign transactions, execute trades or require the monitored wallet’s private key.
TL;DR: This guide builds a read-only Node.js monitor for a Robinhood Chain wallet. It pulls indexed swaps from the MadeOnSol API, then checks each swap’s transaction receipt and log through a Chainstack Robinhood Chain RPC endpoint, and prints newline-delimited JSON with a separate evidence status. You need Node.js 24, a Chainstack mainnet endpoint (a Global Node on the free Developer plan is enough for the tutorial), and a MadeOnSol key with Pro or higher access. It is a starting point, not a production alert service.
What you will build
The monitor has three responsibilities:
- Ask MadeOnSol for indexed swaps attributed to a specified wallet.
- Ask Chainstack for the corresponding transaction receipts.
- Emit each newly observed swap with a separate receipt-evidence status.

Keeping those responsibilities separate makes the output easier to interpret. A decoded buy or sell is an indexer interpretation; a transaction receipt provides execution evidence. Finding a receipt and a log does not independently validate the decoder’s wallet attribution, token quantity or buy/sell classification.
This tutorial implements a command-line backend, with an optional polling mode. It is a starting point for a monitoring application, not a complete accounting ledger or a production alert-delivery service.
Prerequisites
You need:
- Node.js 24, with its built-in
fetch. No npm dependencies are required. - A Chainstack Robinhood Chain mainnet HTTPS endpoint.
- A server-side MadeOnSol API key with Pro or higher access to the wallet-trades endpoint.
- A complete
0xwallet address to observe, preferably one with recent indexed swap activity.
Create your MadeOnSol key through the developer dashboard. The free and demo keys do not grant access to the wallet-trades route used here. See the API reference for authentication and endpoint access.
Use server-side credentials. Do not put either provider’s credentials into a browser bundle or commit your environment file to a repository.
Build the wallet monitor step by step
1. Choose and deploy a Chainstack endpoint
Chainstack’s Robinhood Chain page describes three deployment models for Robinhood Chain:
| Deployment | What it provides | When to consider it |
|---|---|---|
| Global Nodes | Shared, geographically distributed RPC access | A straightforward starting point for this tutorial |
| Dedicated Nodes | Compute resources reserved for your deployment | Workloads needing resource isolation or more configuration control |
| Self-Hosted | Nodes operated on infrastructure you control, with deployment and lifecycle tooling | Teams that want to manage their own infrastructure |
The Global Node documentation and Dedicated Node documentation describe their operating models. Choose based on your workload and operating requirements. This example does not depend on a particular paid add-on, and a Global Node on the free Developer plan is sufficient for this tutorial, subject to its usage limits.
In the Chainstack console, click Add node, select Robinhood Chain and Mainnet, and deploy your chosen configuration. Once it is running, copy its HTTPS endpoint from Access and credentials.
Keep mainnet and testnet separate
Robinhood’s network configuration lists:
| Network | Chain ID | Gas currency |
|---|---|---|
| Mainnet | 4663 | ETH |
| Testnet | 46630 | ETH |
The code checks eth_chainId before reading trades. It stops if the RPC endpoint is not mainnet, which prevents testnet receipt lookups from being mixed with mainnet indexed data. Chainstack documents this method in its chain-info reference.
For separate experiments that send testnet transactions, select Robinhood Chain testnet in the Chainstack faucet and request testnet funds for your test wallet. Faucet funds are not needed for this read-only monitor. Switching only the RPC URL to testnet would not turn the MadeOnSol mainnet dataset into a testnet dataset.
Transaction ordering and MEV
Robinhood Chain is an Arbitrum Orbit layer 2. Its sequencer orders transactions first come, first served, and transactions are not broadcast to a public mempool, so there is no third-party front-running to shield against. Chainstack’s add-on for L2 sequencer chains such as this one is Optimized Submission, a Marketplace add-on that routes transactions through a private, optimized path designed to reduce inclusion delays. On Robinhood Chain Mainnet it is available on Global Nodes on every plan, including the Developer plan, and you enable it from the node’s Add-ons tab. MEV protection covers Ethereum and BNB Smart Chain, where a public mempool exists. Optimized Submission is a transaction submission feature. The monitor below performs reads only, so it neither uses nor tests it. If you later add transaction execution, consider it and treat routing, slippage and execution controls as separate concerns in your application.
2. Understand the wallet trades request
The application calls:
GET https://madeonsol.com/api/v1/rhc/wallet/{address}/trades
Authorization: Bearer YOUR_MADEONSOL_API_KEY
The endpoint accepts these query parameters:
| Parameter | Purpose |
|---|---|
limit | Page size, from 1 to 200; default 50 |
since | Inclusive lower bound on block time, as an ISO 8601 datetime with a timezone |
before | Pagination cursor, passed back exactly as returned in next_before |
action | Optional buy or sell filter |
token | Optional token-contract address filter |
Results are newest first. Pass next_before back unchanged, URL-encoded, as before. Do not construct a cursor from the final row’s timestamp: multiple swaps can share a timestamp, and the cursor includes a tiebreaker. Because since is inclusive and repeated scans overlap, deduplicate on transaction hash and log index, as the code below does.
The response contains chain, address, a top-level trades array, count, has_more, next_before and a history object. This is a swap feed, not a complete list of native transfers, token transfers or every transaction involving the address. An empty result means no matching indexed swaps were returned for the request.
Some fields can be null. Preserve that distinction:
actioncan benullwhen the indexer could not classify the swap as a buy or a sell, for example an intermediate hop inside a multi-step swap. Token fields are oftennullon such rows too. Treat them as unclassified activity and do not fill in a direction yourself.- Token metadata and amounts can be
null. An unknown token amount is not a zero amount.
Robinhood Chain supports ERC-4337 account abstraction. When a swap is made through a smart account, the wallet the API attributes the swap to can differ from the transaction’s from address in the receipt. Do not compare receipt.from with the monitored wallet as a proof of attribution.
3. Configure the application
Create a folder containing monitor.mjs and a .env file:
CHAINSTACK_RPC_URL=https://YOUR_CHAINSTACK_MAINNET_ENDPOINT
MADEONSOL_API_KEY=YOUR_MADEONSOL_API_KEY
WATCH_WALLET=0xREPLACE_WITH_A_FULL_40_HEX_CHARACTER_ADDRESS
Replace all three placeholders with your own values. The wallet address must contain 0x followed by 40 hexadecimal characters. Add .env to .gitignore.
The Chainstack tooling guide includes alternatives using ethers.js, viem and web3.py. Here we use Node’s native HTTP support to make each provider request visible.
4. Implement the monitor
Save the four parts below, in order, as one file named monitor.mjs. Each part is the same code you will run, split only to make it easier to read.
Request helper. requestJSON wraps fetch with a 15-second abort signal, reads Retry-After from failed responses, and marks authentication and configuration errors as fatal.
import { setTimeout as sleep } from 'node:timers/promises';
import { pathToFileURL } from 'node:url';
export async function requestJSON(url, init = {}) {
let response;
try {
response = await fetch(url, {
...init, redirect: 'error', signal: AbortSignal.timeout(15000),
});
} catch {
throw new Error('Network request failed or timed out; endpoint withheld.');
}
if (!response.ok) {
const retry = response.headers.get('retry-after');
const seconds = retry === null ? NaN : Number(retry);
const delay = Number.isFinite(seconds)
? seconds * 1000 : Date.parse(retry ?? '') - Date.now();
const error = new Error(`HTTP ${response.status}; check credentials, plan or service status.`);
error.retryMs = Number.isFinite(delay) ? Math.max(0, delay) : 0;
error.fatal = [400, 401, 403, 404].includes(response.status);
throw error;
}
return response.json();
}
Pagination. scanWallet walks backward through the indexed-trades pages and passes next_before back unchanged. It fails the scan on an unexpected response, a history warning, a repeated cursor, or an exhausted page budget (20 pages by default).
export async function scanWallet(pageRequest, since, maxPages = 20) {
const rows = new Map();
const cursors = new Set();
let before;
for (let page = 0; page < maxPages; page++) {
const data = await pageRequest({ since, before });
if (data.chain !== 'robinhood' || !Array.isArray(data.trades)) {
throw new Error('Unexpected wallet-trades response.');
}
if (data.history?.truncated || data.history?.note) {
throw new Error('History coverage warning; inspect the API history metadata.');
}
for (const trade of data.trades) {
if (!/^0x[0-9a-f]{64}$/i.test(trade.tx_hash ?? '') ||
!Number.isSafeInteger(trade.log_index) || trade.log_index < 0 ||
!Number.isFinite(Date.parse(trade.block_time))) {
throw new Error('Missing or invalid trade identity/time.');
}
rows.set(`4663:${trade.tx_hash.toLowerCase()}:${trade.log_index}`, trade);
}
if (data.has_more === false) return rows;
if (data.has_more !== true || typeof data.next_before !== 'string' ||
!data.next_before || cursors.has(data.next_before)) {
throw new Error('Missing or repeated pagination cursor.');
}
before = data.next_before;
cursors.add(before);
}
throw new Error('Page budget reached; scan incomplete. Narrow the window or raise the budget.');
}
Receipt evidence. receiptEvidence compares each indexed trade with its receipt and returns one of the statuses described in step 5.
export function receiptEvidence(trade, receipt) {
if (!receipt) return 'receipt_unavailable';
if (receipt.transactionHash?.toLowerCase() !== trade.tx_hash.toLowerCase()) {
return 'receipt_mismatch';
}
if (receipt.status !== '0x1') return 'execution_not_successful';
if (trade.block_number == null || receipt.blockNumber == null ||
BigInt(receipt.blockNumber) !== BigInt(trade.block_number)) {
return 'block_mismatch';
}
const log = receipt.logs?.find(item =>
item.logIndex != null && BigInt(item.logIndex) === BigInt(trade.log_index));
return log && !log.removed ? 'receipt_and_log_present' : 'log_unavailable';
}
Main loop. main reads the configuration, checks that the endpoint is Robinhood Chain mainnet (chain ID 4663), collects the indexed pages, fetches each receipt once per transaction, skips records already matched, and either exits or, with --watch, repeats the scan.
export async function main() {
const rpcURL = process.env.CHAINSTACK_RPC_URL;
const key = process.env.MADEONSOL_API_KEY;
const wallet = process.env.WATCH_WALLET?.toLowerCase();
if (!rpcURL || !key || !/^0x[0-9a-f]{40}$/.test(wallet ?? '')) {
throw new Error('Set CHAINSTACK_RPC_URL, MADEONSOL_API_KEY and a full WATCH_WALLET address.');
}
if (new URL(rpcURL).protocol !== 'https:') throw new Error('Use an HTTPS RPC endpoint.');
let requestId = 0;
async function rpc(method, params = []) {
const id = ++requestId;
const body = await requestJSON(rpcURL, {
method: 'POST', headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jsonrpc: '2.0', id, method, params }),
});
if (body.id !== id || body.error || !Object.hasOwn(body, 'result')) {
throw new Error(`RPC ${method} failed; inspect provider diagnostics.`);
}
return body.result;
}
if (BigInt(await rpc('eth_chainId')) !== 4663n) {
throw new Error('Use Robinhood Chain mainnet (4663), not testnet.');
}
const pageRequest = ({ since, before }) => {
const url = new URL(`https://madeonsol.com/api/v1/rhc/wallet/${wallet}/trades`);
url.searchParams.set('limit', '200');
url.searchParams.set('since', since);
if (before) url.searchParams.set('before', before);
return requestJSON(url, { headers: { Authorization: `Bearer ${key}` } });
};
const seen = new Map();
const watch = process.argv.includes('--watch');
const lookbackMs = 10 * 60 * 1000;
let failures = 0;
do {
let waitMs = 60000;
const cutoff = Date.now() - lookbackMs;
try {
const rows = await scanWallet(pageRequest, new Date(cutoff).toISOString());
const receipts = new Map();
for (const [id, trade] of rows) {
if (seen.has(id)) continue;
const hash = trade.tx_hash.toLowerCase();
if (!receipts.has(hash)) receipts.set(hash, await rpc('eth_getTransactionReceipt', [hash]));
const receipt = receipts.get(hash);
const evidence = receiptEvidence(trade, receipt);
console.log(JSON.stringify({
id, wallet, observed_at: new Date().toISOString(),
block_time: trade.block_time, action: trade.action,
token: trade.token_address, symbol: trade.token_symbol ?? null,
token_amount: trade.token_amount ?? null, tx_hash: hash,
block_hash: receipt?.blockHash ?? null, evidence,
}));
if (evidence === 'receipt_and_log_present') seen.set(id, Date.parse(trade.block_time));
}
console.error(`Scan complete: ${rows.size} indexed swap records in the lookback window.`);
failures = 0;
} catch (error) {
console.error(error.message);
if (!watch || error.fatal) throw error;
failures++;
waitMs = Math.max(60000, Math.min(300000, 1000 * 2 ** Math.min(failures, 9)), error.retryMs ?? 0);
}
for (const [id, time] of seen) if (time < cutoff) seen.delete(id);
if (watch) await sleep(waitMs);
} while (watch);
}
if (process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href) {
main().catch(error => {
console.error(`Monitor stopped: ${error.message}`);
process.exitCode = 1;
});
}
Run one scan:
node --env-file=.env monitor.mjs
Or keep polling until you stop the process:
node --env-file=.env monitor.mjs --watch
A successful scan reports how many indexed records were returned. An inactive wallet may produce zero records. For an initial check, choose a wallet that you know has traded recently.
Example output from a live run against Robinhood Chain mainnet on 1 October 2026. The two records come from one transaction: a classified buy and an intermediate hop of the same multi-step swap, which the indexer left unclassified (action is null):
{"id":"4663:0x3dda7294f0eb6d8e6d22704b6f3e28d6b60477937e4ca9aaf2a83ea3e57a5c4a:9","wallet":"0x90924c7d483cb1e1060c8de34acdf72aa54c738b","observed_at":"2026-10-01T16:36:02.192Z","block_time":"2026-10-01T16:35:49+00:00","action":"buy","token":"0x449122c28b60500a3c1685c0c385c4fc9b09bb6f","symbol":"BOARD","token_amount":11446638.24876448,"tx_hash":"0x3dda7294f0eb6d8e6d22704b6f3e28d6b60477937e4ca9aaf2a83ea3e57a5c4a","block_hash":"0x701beb78a4cfca75d29d843a8412af164bebef11375bee066eeb002acf1d2903","evidence":"receipt_and_log_present"}
{"id":"4663:0x3dda7294f0eb6d8e6d22704b6f3e28d6b60477937e4ca9aaf2a83ea3e57a5c4a:7","wallet":"0x90924c7d483cb1e1060c8de34acdf72aa54c738b","observed_at":"2026-10-01T16:36:02.192Z","block_time":"2026-10-01T16:35:49+00:00","action":null,"token":null,"symbol":null,"token_amount":null,"tx_hash":"0x3dda7294f0eb6d8e6d22704b6f3e28d6b60477937e4ca9aaf2a83ea3e57a5c4a","block_hash":"0x701beb78a4cfca75d29d843a8412af164bebef11375bee066eeb002acf1d2903","evidence":"receipt_and_log_present"}
Standard error then reports the scan size, for example Scan complete: 24 indexed swap records in the lookback window.
How the code handles repeated and delayed data
Each scan walks backward through the API’s pages for the previous ten minutes. It collects the pages before starting receipt checks. A page-budget failure, history warning or repeated cursor fails the scan rather than silently declaring it complete.
In watch mode, the next scan starts from a new ten-minute lower bound. This deliberately overlaps previous scans, so recently indexed records can be discovered even when they arrive after an earlier poll. Successfully matched records are deduplicated in memory using chain ID, transaction hash and log index. Multiple swap logs in one transaction remain separate records, while the receipt is fetched only once per transaction within a scan.
The polling interval is at least 60 seconds after a scan finishes. Errors increase the delay, and HTTP Retry-After is respected. Authentication and common configuration errors stop the process. Each fetch is started with a 15-second abort signal, and the default page budget is 20 pages.
These are tutorial defaults, not service guarantees. The ten-minute lookback must be longer than the worst delay between a swap happening and it appearing in the indexed feed: a record that first appears after its block time has already left the window is never emitted. Delays or outages longer than the lookback window can leave gaps.
Records whose receipt check does not reach receipt_and_log_present are not added to the deduplication set, so they are printed again on each scan while they stay in the window. That is intentional: a downstream consumer can update the status once the evidence resolves. The endpoint is also not a transactionally frozen snapshot across pages; a later overlapping scan can pick up late rows within the window. Deduplication lives in memory only, so a restarted process emits records from the current window again.
5. Read the receipt evidence status correctly
The application uses Chainstack’s eth_getTransactionReceipt method. It checks the transaction hash, successful execution status, block number and the presence of the expected log index.
| Output status | Interpretation |
|---|---|
receipt_and_log_present | The returned receipt matches the transaction and block, reports success, and includes the expected log index |
receipt_unavailable | The node returned no receipt; retry or investigate endpoint or history availability |
execution_not_successful | The receipt did not report successful execution |
receipt_mismatch or block_mismatch | The API record and the RPC response disagree on transaction or block identity |
log_unavailable | The expected log is missing or marked removed |
Only receipt_and_log_present enters the in-memory deduplication set. Other records can be reconsidered during later scans while they remain in the lookback window.
This evidence status is intentionally narrow. The script does not decode the log’s topics and data, validate the swap amounts, prove that the wallet owns the swap, or establish finality. On Robinhood Chain a successful receipt establishes at least a soft confirmation from the sequencer; the receipt alone does not identify the current finality stage. According to Robinhood’s finality documentation, after the batch is posted to Ethereum, its ordering can change only if Ethereum reorganizes. Full Ethereum finality typically follows about 13 minutes after posting. For applications that act on the data, add a separate confirmation and reorganization policy rather than renaming this field “verified trade.”
6. Turn the prototype into a durable service
The example is useful for understanding the integration. Before using it for persistent alerts or customer-facing activity histories, add the following application components.
Durable event storage. Store events with a uniqueness constraint on (chain_id, transaction_hash, log_index), together with the monitored wallet, receipt block hash, observation time and evidence status. The example forgets its deduplication state after a restart. If you monitor multiple wallets, design the event-to-wallet association explicitly.
A recovery workflow. Persist the interval your application has reconciled successfully. After an outage, walk the missed interval with pagination and overlap. Keep delivery state separate from scan state so that a notification failure cannot erase the obligation to retry. A ten-minute rolling window alone is not enough for long outages or late backfills.
Reorganization handling. Recheck stored transaction and block evidence according to your confirmation policy. The sample skips already-matched events while they remain in memory, so it does not detect later changes to those receipts. Store corrections and invalidate affected downstream alerts when required.
A bounded request queue. The sample checks receipts sequentially. For larger workloads, introduce concurrency limits and backpressure matched to your provider plans. Monitor page count and duration as well as HTTP errors. More wallets mean more indexed-data requests and more receipt lookups. If request cost matters, the Unlimited Node add-on turns a Chainstack RPC node into a flat-fee plan with an RPS cap.
Freshness and coverage indicators. Record the last successful scan time separately from the last observed swap time. A quiet wallet is not proof that the indexer is stale, and an HTTP 200 response is not proof of complete chain coverage. Surface history warnings, unresolved receipt checks and recovery gaps to operators.
An explicit display policy. Keep null fields visible as unknown, including unclassified swaps. Do not infer token holdings from swap records alone: transfers, bridges and other balance changes require additional data. Receipt matching also cannot discover swaps that are absent from the indexed feed.
For a product that exposes provider data to customers, review the applicable data-use and redistribution terms. Endpoint access alone does not establish redistribution rights.
Troubleshooting
| Symptom | What to check |
|---|---|
| Mainnet check fails | The Chainstack endpoint must return chain ID 4663 |
| HTTP 401 | The MadeOnSol key or the Chainstack endpoint credentials may be missing or invalid |
| HTTP 403 | Check plan access, key restrictions and edge or security restrictions |
| HTTP 429 | Slow the workload and honor Retry-After; do not retry in a tight loop |
| Zero records | Confirm the wallet has indexed swaps within the ten-minute window |
| Page budget reached | Adjust the window or page budget and measure request cost before resuming |
| History coverage warning | Inspect the response’s history object and resolve the warning before treating the scan as reconciled |
| Receipt unavailable | Confirm the network, transaction hash and the endpoint’s receipt availability; keep the status unresolved |
Conclusion
You now have a backend monitor that combines wallet-filtered indexed swaps with direct transaction evidence. A useful next step is to persist its JSON records and expose them through your own backend API, keeping the provider credentials server-side.
Use the MadeOnSol API documentation to explore wallet-related data, and the Chainstack Robinhood methods reference when adding direct chain reads. Extend the application one data source at a time, with a clear distinction between decoded activity, observed receipt evidence and the decisions your own application makes.
Chainstack gives you the RPC layer for the receipt checks and for anything you add later. Deploy a Robinhood Chain node as a Global Node, a Dedicated Node, or a Self-Hosted deployment, and keep the monitor’s provider credentials server-side. To see Robinhood Chain transactions before indexers and RPC can report them, read our guide to tracking memecoin trades on Robinhood Chain and Solana, which runs two experimental open-source tools against live mainnet.
FAQ
It is a read-only Node.js application. It asks the MadeOnSol API for indexed swaps attributed to one wallet, follows pagination, then fetches each transaction receipt through a Chainstack Robinhood Chain endpoint and checks that the expected log is present. It prints each swap as JSON with a separate evidence status.
No. The tutorial works with a Global Node on the free Developer plan, subject to its usage limits. It does not depend on a paid add-on. You do need a MadeOnSol key with Pro or higher access to the wallet-trades endpoint.
A decoded buy or sell is an indexer interpretation, while a receipt is execution evidence. Finding the receipt and the log does not validate the wallet attribution, the token amount, or the buy or sell classification, so the monitor reports them as separate fields.
The indexer could not classify the row as a buy or a sell, for example an intermediate hop inside a multi-step swap. Token fields are often null too. Treat these rows as unclassified activity and do not fill in a direction yourself.
No. On Robinhood Chain a successful receipt gives at least a soft confirmation from the sequencer, but it does not tell you the finality stage. Add a separate confirmation and reorganization policy before your application acts on the data.
Robinhood Chain supports ERC-4337 account abstraction. When a swap goes through a smart account, the wallet the API attributes the swap to can differ from the transaction’s from address, so do not use receipt.from as proof of attribution.
Additional resources
- Chainstack Robinhood Chain — Global, Dedicated, and Self-Hosted nodes
- Robinhood Chain tooling — connect with ethers.js, viem, and web3.py
- Robinhood Chain methods — supported RPC methods
- eth_getTransactionReceipt — the receipt method used in this guide
- Optimized Submission — private transaction routing on L2 sequencer chains
- Chainstack faucet — testnet funds for experiments that send transactions
- Unlimited Node — flat-fee RPS tiers for any Chainstack RPC node
- Robinhood Chain sequencer feed — experimental reference implementation
- How to track memecoin trades on Robinhood Chain and Solana — see transactions before indexers do