Arc Mainnet is now live on Chainstack! Deploy reliable nodes for stablecoin finance today.    Start building
  • Agents
  • Pricing

How to build a wallet monitoring application on Robinhood Chain

Created Oct 2, 2026 Updated Oct 7, 2026
Banner: How to build a wallet monitoring app on Robinhood Chain with Chainstack and MadeOnSol, with binoculars and the Robinhood Chain logo

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:

  1. Ask MadeOnSol for indexed swaps attributed to a specified wallet.
  2. Ask Chainstack for the corresponding transaction receipts.
  3. Emit each newly observed swap with a separate receipt-evidence status.
Diagram of the wallet monitor: the MadeOnSol API returns indexed swaps for a wallet and the Chainstack Robinhood Chain RPC returns transaction receipts; monitor.mjs combines them and prints newline-delimited JSON with each swap and its 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 0x wallet 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:

DeploymentWhat it providesWhen to consider it
Global NodesShared, geographically distributed RPC accessA straightforward starting point for this tutorial
Dedicated NodesCompute resources reserved for your deploymentWorkloads needing resource isolation or more configuration control
Self-HostedNodes operated on infrastructure you control, with deployment and lifecycle toolingTeams 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:

NetworkChain IDGas currency
Mainnet4663ETH
Testnet46630ETH

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:

ParameterPurpose
limitPage size, from 1 to 200; default 50
sinceInclusive lower bound on block time, as an ISO 8601 datetime with a timezone
beforePagination cursor, passed back exactly as returned in next_before
actionOptional buy or sell filter
tokenOptional 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:

  • action can be null when 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 often null on 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 statusInterpretation
receipt_and_log_presentThe returned receipt matches the transaction and block, reports success, and includes the expected log index
receipt_unavailableThe node returned no receipt; retry or investigate endpoint or history availability
execution_not_successfulThe receipt did not report successful execution
receipt_mismatch or block_mismatchThe API record and the RPC response disagree on transaction or block identity
log_unavailableThe 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

SymptomWhat to check
Mainnet check failsThe Chainstack endpoint must return chain ID 4663
HTTP 401The MadeOnSol key or the Chainstack endpoint credentials may be missing or invalid
HTTP 403Check plan access, key restrictions and edge or security restrictions
HTTP 429Slow the workload and honor Retry-After; do not retry in a tight loop
Zero recordsConfirm the wallet has indexed swaps within the ten-minute window
Page budget reachedAdjust the window or page budget and measure request cost before resuming
History coverage warningInspect the response’s history object and resolve the warning before treating the scan as reconciled
Receipt unavailableConfirm 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

What does this wallet monitor do?

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.

Do I need a paid Chainstack plan?

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.

Why check receipts if the API already returns trades?

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.

Why can action be null for a swap?

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.

Does a successful receipt mean the trade is final?

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.

Why does receipt.from differ from the monitored wallet?

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

SHARE THIS ARTICLE
Cxh 530x281 logo

Chainstack announces support for Harmony

We are ecstatic to announce that Harmony is now supported on Chainstack, giving Harmony developers and the community more access to on-chain utility with enterprise grade blockchain infrastructure.

Janson 150x150 logo
Janson Lee
Mar 18
Customer Stories

SMARTy Pay

Automating infrastructure network operations with databases and the blockchain application.

Copperx

Making recurring multi-chain billing and payment links accessible.

Darkpool Liquidity

Develop on various networks and protocols with ease, expanding at scale in a short period of time.