High-volatility “monitorable spots”: RPC metrics and alerts (not investment advice)
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.
| Use | Endpoint |
|---|---|
| Public HTTPS | https://robinhood-mainnet-rpc.blockreq.com/v1/rpc/public |
| Public WSS | wss://robinhood-mainnet-rpc.blockreq.com/v1/rpc/public |
| Private HTTPS / WSS | Replace 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’slogsSubscribe; 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” dimension | On-chain observable | What you measure |
|---|---|---|
| Liquidity / pool | Pool or pair contract address; Swap / Sync / Mint / Burn logs (per real ABI) | Swaps per unit time, rough amount spikes, sudden silence |
| Watch account | Vault, MM, or self-custody wallet addresses | Native balance changes, ERC-20 Transfer in/out, nonce jumps |
| Post-launch window | N blocks or T minutes after a mint / PairCreated | Event density in the window, whether it crosses your “noisy” threshold |
| Tape rhythm | Height and timestamp deltas from newHeads | Whether 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 family | Primary methods | Raw signals | Example derived measures |
|---|---|---|---|
| Block rhythm | WSS eth_subscribe → newHeads; or poll eth_blockNumber | number, timestamp, gasUsed | Δt (seconds between heads), blocks/min, gas utilization |
| Swap / event stream | WSS logs; HTTPS eth_getLogs | address + topic0 (Swap / Transfer…) | events/min, activity after txHash dedupe |
| Balance change | HTTPS eth_getBalance; optional eth_call for ERC-20 balanceOf | Hex wei | ` |
| Account activity | Narrow logs related to that address; or eth_getTransactionCount | Transfer / custom events; nonce | In/out counts, nonce jumps |
| Backfill completeness | Chunked eth_getLogs | Gap block heights | Missed-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
Transfertopic0 (standard):0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef - DEX
Swap/Sync, etc.: compute topic from that pool’s real ABI; write placeholders as0xYOUR_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
- Narrow filters first — naked whole-chain Transfer will blow public quota in high vol and distort metrics.
- Dedupe keys —
txHash + logIndex(logs) orblockHash(heads); otherwise reconnect replay creates fake spikes. - Time anchor — demos can use local clock; serious dashboards prefer
block.timestampand accept skew vs wall clock. - 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”:
| Mechanism | Role | Typical default (sketch—tune yourself) |
|---|---|---|
| Frequency threshold | What counts as “noisy” | ≥ N narrowly filtered logs in last 60s; or ` |
| Cooldown | Don’t spam the same spot | After fire, suppress same alertKey for T seconds |
| Dedupe | Replay / multi-source | Set 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
newHeadsand fire ops alerts whengapSec > threshold(wires into Part 1). - Give mint-from-zero (Part 2) its own
alertKeyso 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-demosatexamples/robinhood-high-vol-monitor; for now, run the HTML /monitor-alerts.mjsabove.
4. How to wire BlockReq query / subscribe (public + keyed)
| Item | Public …/public | Private …/{API_KEY} |
|---|---|---|
| HTTPS queries | eth_getBalance, eth_blockNumber, eth_getLogs, eth_call | Same methods; higher plan limits |
| WSS subscribe | eth_subscribe: newHeads, logs | Same; fits 24×7 alerts |
| Rate limit | Best-effort, per IP | Per plan RU / req/s |
| Key | None | Create in Dashboard; never put it in public HTML / repos / screenshots |
| Fit | Learning, short high-vol window experiments | Long-running “monitorable spot” dashboards and alerts |
Wiring tips (same as the series):
- Subscribe for low latency; HTTP for gaps — after WSS disconnect, chunked
eth_getLogsbackfill, then recompute density (don’t only splice on local clock). - One narrow sub per spot —
address+ neededtopics; multiple spots via a config list, not one “whole-chain logs” sub. - 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.”
- Switch to keyed — change only the URL suffix; keep the key in env vars (e.g. replace
publicwith your key placeholder). - 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:
- 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.
- Not investment advice — “spot” here = monitoring config, not a recommended holding; we do not provide or recommend any specific token or pool address.
- No manipulation playbooks — no sniping, wash trading, volume washing, or manuals for bypassing Robinhood / DEX / platform risk controls.
- 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.
- 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
- A “monitorable spot” = configurable observables (pool / watch address / post-launch window / block rhythm)—not a recommended position.
- Metrics come from
newHeads, narrowlogs,eth_getBalance/eth_getLogs, etc.; prefer counts and intervals; amount decoding needs a real ABI. - Alerts rely on frequency threshold + cooldown + dedupe; sample scripts and HTML only log / highlight—no auto-trading.
- BlockReq: short runs on public; long-running alerts on keyed; subscribe + getLogs backfill split; keys stay out of the repo.
- 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.