BlockReq
Back to all articles

Recipes

Robinhood Chain activity alerts from RPC: density metrics, thresholds, cooldown, and dedupe

Turn Robinhood Chain newHeads and logs into density metrics, threshold alerts, cooldown, and dedupe.

BlockReq EngineeringSeptember 5, 202612 min read

In this post, a “spot” means an observable, measurable object on Robinhood Chain—a pool contract, a watch-address balance, swap / Transfer logs in a post-launch time window, block-production rhythm—not a stock/coin pick, a “copy-trade position,” or a sniping playbook.

Goal: on top of Part 1 (newHeads heartbeat) and Part 2 (mint / pool-create logs), turn “high-volatility windows” into metrics + thresholds + cooldown alerts. Hearing a spike ≠ you should buy; alerts only guarantee that public facts got noisier.

UseEndpoint
Public HTTPShttps://robinhood-mainnet-rpc.blockreq.com/v1/rpc/public
Public WSSwss://robinhood-mainnet-rpc.blockreq.com/v1/rpc/public
Private HTTPS / WSSReplace trailing public with your {API_KEY} (do not put it in demos)

Chain identity: EVM, chainId = 0x1237 (decimal 4663). Docs: Public endpoints · eth_subscribe · Supported Networks.

Series split

- Part 1: How to watch opens · RPC subscribe (robinhood-open-watch-rpc-subscribe)—newHeads + optional narrow logs. - Part 2: Listen for meme / new-token launches · logs and mint signals (robinhood-meme-mint-listen-logs)—topic filters, getLogs backfill. - This post (Part 3): turn those signals into metrics and alerts for monitorable spots (frequency, cooldown, dedupe). - Solana: Watch new Solana mints in the browser—another chain’s logsSubscribe; different protocol—don’t mix clients.

1. What a “spot” means (observable objects, not stock picks)

Treat a “spot” as a config item you are willing to measure continuously, not “an asset we recommend you hold”:

“Spot” dimensionOn-chain observableWhat you measure
Liquidity / poolPool or pair contract address; Swap / Sync / Mint / Burn logs (per real ABI)Swaps per unit time, rough amount spikes, sudden silence
Watch accountVault, MM, or self-custody wallet addressesNative balance changes, ERC-20 Transfer in/out, nonce jumps
Post-launch windowN blocks or T minutes after a mint / PairCreatedEvent density in the window, whether it crosses your “noisy” threshold
Tape rhythmHeight and timestamp deltas from newHeadsWhether block intervals stretch, whether gasUsed looks abnormal (congestion feel)

Recommended mindset:

  • Monitorable spot = address / topics / time window in config + a metric formula.
  • Not a stock pick = don’t name specific memes or imply “add size here.”
  • High volatility = a metric steepens vs baseline; that is not opportunity, and risk is not priced.

Split “metric fired” from “should I trade”: this post only covers the first half—observation stack, not trading stack.

2. RPC / log metrics you can pull

On Robinhood Chain (EVM), metrics are almost always composed from these RPC families:

Metric familyPrimary methodsRaw signalsExample derived measures
Block rhythmWSS eth_subscribe → newHeads; or poll eth_blockNumbernumber, timestamp, gasUsedΔt (seconds between heads), blocks/min, gas utilization
Swap / event streamWSS logs; HTTPS eth_getLogsaddress + topic0 (Swap / Transfer…)events/min, activity after txHash dedupe
Balance changeHTTPS eth_getBalance; optional eth_call for ERC-20 balanceOfHex wei`
Account activityNarrow logs related to that address; or eth_getTransactionCountTransfer / custom events; nonceIn/out counts, nonce jumps
Backfill completenessChunked eth_getLogsGap block heightsMissed-sub duration, backfill count (ops metrics)

2.1 Balance snapshots (HTTPS, low frequency is fine)

curl -sS https://robinhood-mainnet-rpc.blockreq.com/v1/rpc/public \
  -H 'Content-Type: application/json' \
  -d '{"jsonrpc":"2.0","id":1,"method":"eth_getBalance","params":["0xYourWatchAddress","latest"]}'

Record returned result (hex wei) into a time series; alert on change magnitude / change frequency, not on a single absolute balance as a “buy/sell signal.”

2.2 Swap / Transfer density (WSS logs → sliding-window counts)

{
  "jsonrpc": "2.0",
  "id": 1,
  "method": "eth_subscribe",
  "params": [
    "logs",
    {
      "address": "0xYourPoolOrTokenAddress",
      "topics": ["0xYOUR_SWAP_OR_TRANSFER_TOPIC0"]
    }
  ]
}
  • ERC-20 Transfer topic0 (standard): 0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef
  • DEX Swap / Sync, etc.: compute topic from that pool’s real ABI; write placeholders as 0xYOUR_SWAP_TOPIC0—don’t copy another chain’s factory address.

Common client-side derivation:

// Pseudocode: sliding-window event density
const windowMs = 60_000;
const timestamps = []; // push Date.now() on each push

function eventsPerMinute() {
  const now = Date.now();
  while (timestamps.length && now - timestamps[0] > windowMs) timestamps.shift();
  return timestamps.length; // ≈ events in last 1 minute (by arrival time; production may use block timestamp)
}

2.3 Block rhythm (wires into Part 1 newHeads)

// Pseudocode: seconds between adjacent newHeads
let prevTs = null;
function onHead(head) {
  const ts = parseInt(head.timestamp, 16);
  if (prevTs != null) {
    const gapSec = ts - prevTs;
    // sudden large gapSec → “blocks slowed” ops alert (not a trading signal)
  }
  prevTs = ts;
}

2.4 Metric-design notes

  1. Narrow filters first — naked whole-chain Transfer will blow public quota in high vol and distort metrics.
  2. Dedupe keys — txHash + logIndex (logs) or blockHash (heads); otherwise reconnect replay creates fake spikes.
  3. Time anchor — demos can use local clock; serious dashboards prefer block.timestamp and accept skew vs wall clock.
  4. Decode amounts carefully — data / indexed args need ABI decoding; a mis-decoded “huge swap” is worse than a missed alert. Examples here favor count-based metrics to avoid false precision.

3. Alert-threshold ideas (frequency, cooldown, dedupe)

The alert triad matters more than “what number is the threshold”:

MechanismRoleTypical default (sketch—tune yourself)
Frequency thresholdWhat counts as “noisy”≥ N narrowly filtered logs in last 60s; or `
CooldownDon’t spam the same spotAfter fire, suppress same alertKey for T seconds
DedupeReplay / multi-sourceSet or LRU: txHash:logIndex; alert side also alertKey + bucket

Extra practical items:

  • Hysteresis — fire on a high threshold, clear on a lower one, to avoid boundary chatter.
  • Quiet baseline — collect p50/p95 on a dry run before picking N; density is naturally high in the first minutes after launch—give “time-window spots” their own thresholds.
  • Channel degradation — on public 429, downsample and widen windows instead of retry-looping your IP into the ground.

3.1 Threshold state machine (pseudocode)

const COOLDOWN_MS = 120_000;
const THRESHOLD_EPM = 30; // events in last ~1 minute
const lastSent = new Map(); // alertKey -> lastSentAt

function maybeAlert(alertKey, epm, detail) {
  if (epm < THRESHOLD_EPM) return;
  const now = Date.now();
  const prev = lastSent.get(alertKey) || 0;
  if (now - prev < COOLDOWN_MS) return; // cooling down
  lastSent.set(alertKey, now);
  // send to Webhook / logs / desktop notification—log only here, no trading
  console.log(JSON.stringify({
    type: "high_vol_position",
    alertKey,
    epm,
    detail,
    ts: new Date(now).toISOString(),
    disclaimer: "observable_only_not_investment_advice",
  }));
}

3.2 Copyable mini-script: sliding window + cooldown (Node, public WSS)

Save as monitor-alerts.mjs, replace the placeholder with your watch contract address; connects only to public—no key. Needs Node 18+ (built-in fetch; see comment for WebSocket).

// monitor-alerts.mjs — observation alert demo; not investment advice, no orders
import WebSocket from "ws"; // Node 22+ can use global WebSocket instead

const WSS = "wss://robinhood-mainnet-rpc.blockreq.com/v1/rpc/public";
const ADDRESS = process.env.WATCH_ADDRESS || "0xYourPoolOrTokenAddress";
const TOPIC0 = process.env.TOPIC0 ||
  "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
const WINDOW_MS = 60_000;
const THRESHOLD = Number(process.env.THRESHOLD || 20);
const COOLDOWN_MS = Number(process.env.COOLDOWN_MS || 120_000);

if (!/^0x[a-fA-F0-9]{40}$/.test(ADDRESS) || ADDRESS.includes("YourPool")) {
  console.error("set WATCH_ADDRESS=0x… (real watch contract); refusing placeholder");
  process.exit(1);
}

const seen = new Set();
const stamps = [];
let lastAlertAt = 0;
let nextId = 1;
let backoff = 1000;

function trim(now) {
  while (stamps.length && now - stamps[0] > WINDOW_MS) stamps.shift();
}

function considerAlert(meta) {
  const now = Date.now();
  trim(now);
  const epm = stamps.length;
  if (epm < THRESHOLD) return;
  if (now - lastAlertAt < COOLDOWN_MS) return;
  lastAlertAt = now;
  console.log(JSON.stringify({
    alert: "high_event_density",
    address: ADDRESS,
    epm,
    threshold: THRESHOLD,
    cooldownMs: COOLDOWN_MS,
    meta,
    note: "observable spike; not investment advice",
  }));
}

function connect() {
  const ws = new WebSocket(WSS);
  ws.on("open", () => {
    backoff = 1000;
    const id = nextId++;
    ws.send(JSON.stringify({
      jsonrpc: "2.0",
      id,
      method: "eth_subscribe",
      params: ["logs", { address: ADDRESS, topics: [TOPIC0] }],
    }));
    console.error("subscribed logs", ADDRESS.slice(0, 10) + "…");
  });
  ws.on("message", (buf) => {
    let msg;
    try { msg = JSON.parse(String(buf)); } catch { return; }
    if (msg.method !== "eth_subscription") return;
    const r = msg.params && msg.params.result;
    if (!r || !r.transactionHash) return;
    const key = r.transactionHash + ":" + r.logIndex;
    if (seen.has(key)) return;
    seen.add(key);
    if (seen.size > 50000) seen.clear();
    const now = Date.now();
    stamps.push(now);
    trim(now);
    considerAlert({ tx: r.transactionHash, block: r.blockNumber, epm: stamps.length });
  });
  ws.on("close", () => {
    console.error("ws closed; reconnect in " + backoff + "ms");
    setTimeout(connect, backoff);
    backoff = Math.min(backoff * 2, 30000);
  });
  ws.on("error", () => { try { ws.close(); } catch (e) {} });
}

connect();

Run example:

# On Node 22+ with global WebSocket, you can drop the import WebSocket line
WATCH_ADDRESS=0xYourRealContract THRESHOLD=25 COOLDOWN_MS=180000 node monitor-alerts.mjs

Extensions (still observation stack):

  • Also subscribe newHeads and fire ops alerts when gapSec > threshold (wires into Part 1).
  • Give mint-from-zero (Part 2) its own alertKey so it doesn’t mix with plain Transfer density.
  • Webhooks should only POST JSON logs; do not auto-trade inside the callback.

3.3 Minimal browser alert bar (optional)

Same single-page style as Parts 1–2: subscribe narrow logs, show “events in last 1 minute” locally, highlight when over threshold + cooldown. Save as robinhood-high-vol-monitor.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8" />
  <title>Robinhood Chain · high-vol spot monitor (not investment advice)</title>
  <style>
    body { font-family: ui-sans-serif, system-ui, sans-serif; margin: 1.5rem; max-width: 52rem; }
    button { margin-right: 0.5rem; }
    #status { margin: 0.75rem 0; color: #334155; }
    #epm { font-size: 1.5rem; font-weight: 600; }
    .alert { color: #b91c1c; }
    pre { background: #0f172a; color: #e2e8f0; padding: 1rem; min-height: 12rem;
          overflow: auto; font-size: 12px; border-radius: 8px; }
    code { background: #f1f5f9; padding: 0.1rem 0.35rem; border-radius: 4px; }
  </style>
</head>
<body>
  <h1>Monitorable spot · event-density alerts</h1>
  <p>Public WSS · observe only · <strong>no stock picks, no orders, no return guarantee</strong></p>
  <p>WSS: <code>wss://robinhood-mainnet-rpc.blockreq.com/v1/rpc/public</code></p>
  <p><input id="addr" placeholder="0x… pool or token (required)" size="48" /></p>
  <p>Threshold <input id="th" type="number" value="20" min="1" style="width:4rem" /> / min · cooldown sec
    <input id="cd" type="number" value="120" min="1" style="width:4rem" /></p>
  <button id="start" type="button">Start</button>
  <button id="stop" type="button" disabled>Stop</button>
  <div id="status">Idle</div>
  <div>Events in last 1 min: <span id="epm">0</span></div>
  <pre id="out"></pre>
  <script>
    const WSS = "wss://robinhood-mainnet-rpc.blockreq.com/v1/rpc/public";
    const TRANSFER =
      "0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef";
    const out = document.getElementById("out");
    const status = document.getElementById("status");
    const epmEl = document.getElementById("epm");
    let ws, want = false, nextId = 1, backoff = 1000, lastAlert = 0;
    const seen = new Set();
    const stamps = [];

    function log(line) {
      const t = new Date().toISOString().slice(11, 19);
      out.textContent = "[" + t + "] " + line + "\n" + out.textContent;
    }
    function trim(now) {
      while (stamps.length && now - stamps[0] > 60000) stamps.shift();
      epmEl.textContent = String(stamps.length);
    }
    function send(method, params) {
      ws.send(JSON.stringify({ jsonrpc: "2.0", id: nextId++, method, params }));
    }
    function connect() {
      if (!want) return;
      status.textContent = "connecting…";
      ws = new WebSocket(WSS);
      ws.onopen = () => {
        backoff = 1000;
        const addr = document.getElementById("addr").value.trim();
        if (!/^0x[a-fA-F0-9]{40}$/.test(addr)) {
          log("enter a valid address");
          want = false;
          document.getElementById("start").disabled = false;
          document.getElementById("stop").disabled = true;
          ws.close();
          return;
        }
        send("eth_subscribe", ["logs", { address: addr, topics: [TRANSFER] }]);
        status.textContent = "connected · observing (≠ investment advice)";
      };
      ws.onmessage = (ev) => {
        let msg; try { msg = JSON.parse(ev.data); } catch (e) { return; }
        if (msg.method !== "eth_subscription") return;
        const r = msg.params && msg.params.result;
        if (!r || !r.transactionHash) return;
        const key = r.transactionHash + ":" + r.logIndex;
        if (seen.has(key)) return;
        seen.add(key);
        const now = Date.now();
        stamps.push(now);
        trim(now);
        const th = Number(document.getElementById("th").value) || 20;
        const cd = (Number(document.getElementById("cd").value) || 120) * 1000;
        if (stamps.length >= th && now - lastAlert >= cd) {
          lastAlert = now;
          epmEl.classList.add("alert");
          log("ALERT epm=" + stamps.length + " th=" + th + " tx=" + r.transactionHash.slice(0, 12) + "…");
        }
      };
      ws.onclose = () => {
        status.textContent = "closed";
        if (!want) return;
        status.textContent = "reconnect in " + backoff + "ms";
        setTimeout(connect, backoff);
        backoff = Math.min(backoff * 2, 30000);
      };
      ws.onerror = () => { try { ws.close(); } catch (e) {} };
    }
    document.getElementById("start").onclick = () => {
      want = true;
      document.getElementById("start").disabled = true;
      document.getElementById("stop").disabled = false;
      connect();
    };
    document.getElementById("stop").onclick = () => {
      want = false;
      document.getElementById("start").disabled = false;
      document.getElementById("stop").disabled = true;
      epmEl.classList.remove("alert");
      if (ws) try { ws.close(); } catch (e) {}
      status.textContent = "stopped";
    };
  </script>
</body>
</html>

StackBlitz slot: TBD—the example is planned under blockreq/blog-demos at examples/robinhood-high-vol-monitor; for now, run the HTML / monitor-alerts.mjs above.

4. How to wire BlockReq query / subscribe (public + keyed)

ItemPublic …/publicPrivate …/{API_KEY}
HTTPS querieseth_getBalance, eth_blockNumber, eth_getLogs, eth_callSame methods; higher plan limits
WSS subscribeeth_subscribe: newHeads, logsSame; fits 24×7 alerts
Rate limitBest-effort, per IPPer plan RU / req/s
KeyNoneCreate in Dashboard; never put it in public HTML / repos / screenshots
FitLearning, short high-vol window experimentsLong-running “monitorable spot” dashboards and alerts

Wiring tips (same as the series):

  1. Subscribe for low latency; HTTP for gaps — after WSS disconnect, chunked eth_getLogs backfill, then recompute density (don’t only splice on local clock).
  2. One narrow sub per spot — address + needed topics; multiple spots via a config list, not one “whole-chain logs” sub.
  3. Separate alert process from UI — browser pages are fine for watching; cooldown alerts fit a small script / worker so sleep-dropped subs aren’t misread as “calm.”
  4. Switch to keyed — change only the URL suffix; keep the key in env vars (e.g. replace public with your key placeholder).
  5. 429 / instant disconnect — narrow width, raise cooldown and sampling interval, switch to keyed at peaks; don’t stack a dozen wide subs on one IP.

Backfill sketch (POST HTTPS public):

{
  "jsonrpc": "2.0",
  "id": 3,
  "method": "eth_getLogs",
  "params": [{
    "fromBlock": "0xFROM",
    "toBlock": "0xTO",
    "address": "0xYourPoolOrTokenAddress",
    "topics": ["0xYOUR_SWAP_OR_TRANSFER_TOPIC0"]
  }]
}

More: Public endpoints · Plans & pricing.

5. Risk disclaimer: no return guarantee, not investment advice

This post and sample code are for observable monitoring of public on-chain data and engineering demos only. They are not investment advice, stock/coin recommendations, or any promise of returns.

Please be clear:

  1. No return guarantee — high event density, large balance moves, and slower blocks do not imply profitability; high volatility often comes with thin liquidity, fake books, contract risk, and information asymmetry.
  2. Not investment advice — “spot” here = monitoring config, not a recommended holding; we do not provide or recommend any specific token or pool address.
  3. No manipulation playbooks — no sniping, wash trading, volume washing, or manuals for bypassing Robinhood / DEX / platform risk controls.
  4. Keys and compliance — don’t put API keys in public demos; whether trading is allowed and how you disclose it follows local law and platform rules.
  5. Tool limits — public RPC is best-effort; log delay, replay, and ABI mis-decoding all produce false alerts or missed alerts.

Compliance boundary (same as the series): observable ≠ tradable signal; monitoring stack ≠ trading stack.

---

Summary

  1. A “monitorable spot” = configurable observables (pool / watch address / post-launch window / block rhythm)—not a recommended position.
  2. Metrics come from newHeads, narrow logs, eth_getBalance / eth_getLogs, etc.; prefer counts and intervals; amount decoding needs a real ABI.
  3. Alerts rely on frequency threshold + cooldown + dedupe; sample scripts and HTML only log / highlight—no auto-trading.
  4. BlockReq: short runs on public; long-running alerts on keyed; subscribe + getLogs backfill split; keys stay out of the repo.
  5. Continue with: Part 1 open watch · Part 2 mint/logs · Solana new mint.

Typical next steps: spot list in config → baseline stats → Webhook alerts → persist checkpoints. Still an observation stack; no return guarantee, not investment advice.