RH INTELlog in

API documentation

Read-only access to the RH Intelligence Indexer — Robinhood Chain, chain id 4663. One credential per consumer. Everything documented on this page was called against the live API before it was written down.

Connect with Claude (MCP)

The fastest way in. An MCP client gets the tools below with no code.

claude mcp add --transport http rh-indexer \
  https://int.nexus-aio.com/mcp/<YOUR-KEY>/

Claude Desktop — claude_desktop_config.json:

{
  "mcpServers": {
    "rh-indexer": {
      "type": "http",
      "url": "https://int.nexus-aio.com/mcp/<YOUR-KEY>/"
    }
  }
}

Transport is MCP over Streamable HTTP, stateless — POST only. Your key is in the URL. That is a deliberate trade: an MCP client cannot set an arbitrary header on a connection, so the credential travels in the path and is lifted into the header the API reads. Treat the whole URL as the secret — it does not belong in a shared config file, a screenshot or a bug report. For direct REST the key goes in a header instead; see Authentication.

toolanswersscope
rh_get_healthHow far the index has reached: chain head, last indexed block, lag, per-collection counts.health
rh_get_recent_deploysWhat launched recently, newest first, with deployer and explorer links.signals
rh_get_token_infoOne token in full: symbol, status, age, deployer, labels, pools, signals, bytecode siblings.token
rh_get_wallet_activityOne wallet: score record, positions, who funded it, signals naming it.wallet
rh_get_new_poolsRecently created pools chain-wide. Not available on this deployment — see below.—
rh_get_swapsIndividual swaps by token or wallet. Not available on this deployment — see below.—

rh_get_new_pools and rh_get_swaps return an explicit unavailable error on this deployment rather than an empty page. They read the indexer's database directly, which would bypass the scope system entirely, so that path is switched off here. An empty list would have been a lie; the error is the truth.

Reading rules for this chain

Authentication

One key, two places it can go, and only one of them works per surface:

REST   X-API-Key: <YOUR-KEY>          header, on every /v1/ request
MCP    /mcp/<YOUR-KEY>/                 in the path — MCP only
WS     ?key=<YOUR-KEY>                  query param — WebSocket only

?key= is valid on the WebSocket and invalid on HTTP routes. It exists because a browser cannot set a header on a WebSocket handshake. On an HTTP route it is half-honoured in the worst possible way — it satisfies the rate limiter and the request log, and is then refused 401 by the authorizer — so a /v1/ call must use the header.

Keep it secret. The key is shown once at issue and only a SHA-256 digest is stored, so a lost key cannot be recovered — it is replaced. If you think it has leaked, say so and it will be revoked; revocation takes effect within 30 seconds and needs no restart of anything.

Scopes

Every route requires one. A consumer key normally carries token wallet candles signals leaderboard health — all read-only. Stream families are their own scopes: trades deploys clusters rugs. early_buyers is separate and is not granted by default: it joins a token's entry order to a per-wallet track record, which is the most valuable derived product here, so it is a decision rather than a default.

401 and 403 mean different things. 401 is "this key is unknown, or revoked". 403 is "this key is fine, and it does not carry the scope this route needs" — and it names which, so you never have to guess:

$ curl -s -H "X-API-Key: <YOUR-KEY>" \
    "https://int.nexus-aio.com/v1/token/0x39dbed3a2bd333467115de45665cc57f813c4571/early-buyers?n=3"

{
  "error": "missing scope",
  "required_scope": "early_buyers",
  "key": "<your-key-id>",
  "scopes": ["candles","health","leaderboard","signals","token","wallet"]
}

Through MCP the same refusal arrives as tool content rather than a crash: {"error":"auth_error","upstream_status":403,"upstream":{…}} carrying the identical required_scope. A suspended or over-limit key fails inside the tool exactly as it would over REST — the MCP service holds no credential of its own and grants nothing.

Rate limits

Read API

methodpathscopereturns
GET/v1/token/{address}tokenstatus, labels[], age_seconds, deployer, initial/last mcap, liquidity, pools[]
GET/v1/token/{address}/detailtokenthe token plus its pools, liquidity (with the instant it was priced), bytecode siblings, signals, deployer funding
GET/v1/token/{address}/clustertokendistinct_buyers, sum_quote_usd, buyers[], funding_cluster, window_start
GET/v1/token/{address}/early-buyers?n=early_buyersfirst N distinct buyers in chain order, entry_mcap_usd each, coverage{}. NOT on consumer keys — see the 403 example
GET/v1/wallet/{address}/scorewallettier, hit_rate, realized_pnl_usd, closed_positions, median_multiple, weights_version, scored
GET/v1/wallet/{address}/detailwalletclass and its evidence, flags, tx_count, funding edges both ways, positions (capped at 100), score history, signals
GET/v1/candles?pool=|token=&interval=&from=&to=&limit=candlesOHLC + volume, quote-asset stamped. interval: 1m 5m 15m 1h 4h 1d. limit ≤ 1000, default 200
GET/v1/leaderboard?window=&limit=leaderboardranked by realized PnL, closed positions only, scam-excluded. window: 7d | all — those two are built, nothing else
GET/v1/signals?type=&token=&wallet=&limit=signalssignal history with full state snapshots. limit ≤ 500, default 60
GET/v1/healthhealthingest lag, stream lengths, collection counts, gap counts, last reconciliation

There is no cursor pagination. Every list endpoint takes limit and nothing else — no cursor, no offset, no next_cursor in any response. To walk further back, narrow with the filters the endpoint offers (from/to on candles, token/wallet/type on signals) rather than paging. Said explicitly because the opposite is the common assumption.

Answers come from precomputed Redis state and target < 5 ms server-side. If an answer is not cached the response is {"status":"not_indexed"} with HTTP 200 — the API never falls through to the database on the read path, which is what keeps consumer traffic off the pipeline. not_indexed is an answer, not a failure: it means this layer has never described that id. Retrying it in a loop will not change it. /v1/signals is the one documented exception — signal history is the record, not a cached answer.

Example

$ curl -s -H "X-API-Key: <YOUR-KEY>" \
    "https://int.nexus-aio.com/v1/wallet/0x32096290ea8b1d92e7ee018c96eea3c741db2dd6/score"

{"address":"0x32096290ea8b1d92e7ee018c96eea3c741db2dd6","tier":0.0,"hit_rate":0.333333,
 "realized_pnl_usd":-11.417941,"closed_positions":6.0,"median_multiple":1.068386,
 "weights_version":1.0,"computed_ts":1789588669.0,"scored":true,"server_us":593}

scored travels beside tier deliberately. tier: null, scored: false means this wallet has never been scored; tier: 0, scored: true means scored and in the lowest band. They are different statements and this API does not conflate them.

$ curl -s -H "X-API-Key: <YOUR-KEY>" \
    "https://int.nexus-aio.com/v1/leaderboard?window=7d&limit=2"

{"window":"7d","entries":[
  {"wallet":"0xb35084b05285fcea9b7718f689980f28d1054b3e","realized_pnl_usd":388049.3038822},
  {"wallet":"0x62cc49d34520f821f851f7f7073e8d6e4184675c","realized_pnl_usd":349703.14523355156}],
 "server_us":492}

window accepts 7d and all. Those are the two that are built; asking for a window that is not built answers not_indexed rather than an empty list, because "nobody qualifies" and "that window was never computed" are different facts.

$ curl -s -H "X-API-Key: <YOUR-KEY>" \
    "https://int.nexus-aio.com/v1/candles?token=0x39dbed3a2bd333467115de45665cc57f813c4571&interval=1h&limit=2"

{"pool":"0x1cfe977181c8980d80834caa8c5b326ac897a34b","interval":"1h",
 "quote_asset":"0x5fc5360d0400a0fd4f2af552add042d716f1d168",
 "candles":[{"_id":"0x1cfe…:1h:1789592400","o":…,"h":…,"l":…,"c":0.5617444917001556,…}],
 "count":2,"server_us":…}

Pass pool for one pool, or token to have the busiest pool resolved for you — the answer stamps which pool and which quote_asset it used, so a series is never ambiguous about what it is priced in. from and to are unix seconds.

Stream API

wss://int.nexus-aio.com/v1/stream?key=<YOUR-KEY>

# or, where your client can set headers on the handshake:
wss://int.nexus-aio.com/v1/stream          X-API-Key: <YOUR-KEY>

This is the one surface where ?key= is valid, because a browser cannot set a header on a WebSocket handshake. It is not valid on /v1/ HTTP routes — see Authentication. Either form works here; the header wins if both are present.

Up to ws_max_connections sockets per key, default 3. The slot is claimed before the upgrade — a connection refused at the handshake is one the server never has to hold — and released when the socket closes, including on a crash, so a slot cannot leak. Over the cap the handshake is refused rather than the socket being opened in order to say it should not be open.

The subscribe ack reports wallet_sets — how many wallets each named list you filtered on actually holds. null there means the list does not exist, so nothing will ever match, which is otherwise indistinguishable from a quiet chain.

Two event families on one socket. A subscribe frame selects one by its event_types; mixing them is rejected at subscribe time, because half the cluster fields have no value to compare against for a single trade. Open two subscriptions if you want both.

familyevent typespayload
clustercluster_buy fresh_cluster serial_deployer_launch rug_warning insider_distribution narrative_cluster token_deployed first_liquidityaggregate over a 30-minute window
tradebuy sell swapone swap, every classified trade on the chain

Subscribe with named fields and operators only. There are no expressions and nothing from client input is evaluated beyond these schemas. Predicates are checked server-side, one pass per event. An unknown field is rejected at subscribe time rather than ignored — a filter you mistyped must fail loudly, not silently firehose you. A cluster-shaped field on a trade subscription is rejected with an error that names it and says which schema it belongs to.

Trade frame

{
  "type": "buy",
  "id": "1789995244123-0",          // Redis stream id — monotonic, and the replay cursor
  "wallet": "0x...",
  "tx": "0x...",
  "block": 48123456,
  "token_in":  { "address": "0x...", "symbol": "MOON",
                 "amount": "1842300.550000000000000000", "mcap_usd": 145200 },
  "token_out": { "address": "0x...", "symbol": "USDG",
                 "amount": "127.500000", "mcap_usd": null },
  "value_usd": 127.50,
  "priced": true,
  "venue": "v4",
  "quote_asset": "USDG",
  "ts_block": 1789995243,           // the chain's clock
  "ts_detected": 1789995244         // ours, stamped at emission — the latency receipt
}

Trade predicate schema

fieldtypeoperatorsnotes
wallet_setstringin / == / !=named server-side list
min_value_usdnumber>= <= > < == !=the trade’s USD value; an unpriced trade never satisfies it
wallet_classstring or list== != intrader | bot | router | contract_wallet | bridge | contract | unknown
token_age_secondsnumber>= <= > < == !=since first sighting
mcap_usdnumber>= <= > < == !=evaluated on the NON-registry side — the subject of the trade
venuestring== != inv2 | v3 | v4
quote_assetstring== != insymbol, e.g. USDG / WETH
pricedboolean== !=false = neither leg is a registry asset; value and mcap are null
min_wallet_tiernumber>= <= > < == !=ONLY with "enrich": ["wallet"] — a trade has one wallet

Side selection happens via event_types, not via a filter. A field the server cannot supply does not match — an unpriced trade never satisfies min_value_usd >= 50, an unknown market cap never satisfies a ceiling, an unclassified wallet is not a trader.

Subscribe frame — trades

{
  "action": "subscribe",
  "id": "my-bot-1",
  "event_types": ["buy", "sell", "swap"],
  "from_id": "1789995243000-7",        // optional: resume strictly AFTER this id
  "enrich": ["wallet", "token"],       // optional, per subscription
  "filters": [
    { "field": "wallet_set",    "op": "in", "value": "list:test" },
    { "field": "min_value_usd", "op": ">=", "value": 50 }
  ]
}

The ack reports the size of every named list: "wallet_sets":{"test":4}. A null there means the list does not exist and the filter will match nothing — without which “I subscribed and received nothing” is indistinguishable from a quiet chain.

Ordering

Replay on reconnect

from_id resumes strictly after the id you give — the field means “the last one I saw”. You receive the gap once each, in order, then the live feed. If the cursor has aged out:

{"replay": "truncated", "earliest": "1789980000000-0",
 "note": "the requested from_id is older than the stream's retention; replay begins
          at `earliest` and the gap before it is not recoverable from this stream"}

Replay then begins at earliest. We do not silently start from the head: told “truncated, earliest is X” you can fill the hole from the Read API, whereas a silent reposition leaves a gap you cannot see. The window is 400,000 frames — measured at 611 bytes each, ≈ 244 MB, ≈ 2 h 51 m at the sustained 39 trades/s. It is a count, not a duration, so it shrinks under a burst: working a backlog the emitter has sustained 857 frames/s, which is the same 400,000 frames in under eight minutes.

Opt-in enrichment

The base frame stays lean. "enrich": ["wallet", "token"] appends, per subscription. Enrichment only ever adds keys — the base frame's types never change, whatever you ask for, which is why the wallet block is wallet_info and not wallet. wallet is the plain address string on every frame we send.

"wallet_info": { "address", "class", "tier", "hit_rate", "nonce", "funder",
                 "funder_tier", "is_first_buy", "in_lists", "weights_version" },
"token":  { "address", "age_seconds", "labels", "deployer", "deployer_flags",
            "deploy_to_liquidity_seconds", "bytecode_match", "liquidity_usd" },

// on a SELL, with wallet enrichment, one more block:
"position": { "pct_sold": 25.0, "realized_pnl_usd_so_far": 5.5 }

Cluster predicate schema

fieldtypeoperatorsnotes
wallet_setstringin / == / !=named server-side list
min_wallet_tiernumber>= <= > < == !=satisfied by the BEST wallet in the cluster
min_quote_usd_eachnumber>= <= > < == !=satisfied by the SMALLEST buy — every buy must clear it
sum_quote_usdnumber>= <= > < == !=total quote value across the window, counted wallets only
distinct_buyersnumber>= <= > < == !=distinct wallets, not trades — bots and routers excluded
token_age_secondsnumber>= <= > < == !=since first sighting
mcap_usdnumber>= <= > < == !=derived from pool price × supply
funding_clusterboolean== !=two or more buyers share a funding parent
wallet_tx_count_maxnumber>= <= > < == !=satisfied by the BUSIEST wallet; from the on-chain nonce
deployer_flagboolean== !=
venuestring== != inv2 | v3 | v4
quote_assetstring== != inregistry address

distinct_buyers counts people, not machines. Wallets classed bot or router are removed server-side before any predicate is evaluated, and the exclusion applies to every derived figure on the payload — sum_quote_usd, the buyers array, window_start — because 3 buyers printed next to five buyer rows is worse than not filtering at all. Two fields make the rule visible: counted_classes and excluded_buyers. unknown counts: the classifier will not judge a wallet below a minimum transaction count, so “unknown” is overwhelmingly “too new to judge”, and fresh wallets are the population worth finding. Signals logged before this rule carry counted_classes: ["all"] rather than being retro-labelled.

Subscribe frame — clusters

{
  "action": "subscribe",
  "id": "james-cluster-1",
  "event_types": ["cluster_buy", "fresh_cluster", "serial_deployer_launch",
                  "rug_warning", "insider_distribution", "narrative_cluster",
                  "token_deployed", "first_liquidity"],
  "filters": [
    { "field": "wallet_set",          "op": "in",  "value": "list:james_tracked" },
    { "field": "min_wallet_tier",     "op": ">=",  "value": 2 },
    { "field": "min_quote_usd_each",  "op": ">=",  "value": 50 },
    { "field": "distinct_buyers",     "op": ">=",  "value": 3 },
    { "field": "token_age_seconds",   "op": "<=",  "value": 21600 },
    { "field": "mcap_usd",            "op": "<=",  "value": 500000 },
    { "field": "funding_cluster",     "op": "==",  "value": false },
    { "field": "wallet_tx_count_max", "op": "<=",  "value": 10 }
  ]
}

Delivery guarantees