RH INTELlog in

API documentation

Public page. Auth for the API itself is an X-API-Key header (or ?key= on the WebSocket); keys are issued per consumer. Everything documented here is running.

Read API

endpointreturns
GET /v1/wallet/{address}/scoretier, hit_rate, realized_pnl_usd, closed_positions, median_multiple, weights_version, computed_ts
GET /v1/wallet/{address}class, first_seen, funder, flags, tx_count
GET /v1/token/{address}status, labels[], age_seconds, deployer, initial/last mcap, liquidity, pools[]
GET /v1/token/{address}/clusterdistinct_buyers, sum_quote_usd, buyers[], funding_cluster, window_start
GET /v1/candles/{pool}?interval=1m&from=&to=own candles — quote-asset stamped
GET /v1/leaderboard?window=7dranked by realized PnL (closed, scam-excluded)
GET /v1/signals?type=&token=&wallet=&limit=signal history with full state snapshots
GET /v1/healthingest lag, stream lengths, gap counts, last reconciliation
WS /v1/stream?key=…live cluster signals AND the per-trade stream (DNS-only host — no bot challenge)

Answers come from precomputed Redis state and target < 5 ms server-side. If an answer is not cached the response is {"status":"not_indexed"} — the API never falls through to the database on the read path, which is what keeps consumer traffic off the pipeline. /v1/signals is the one documented exception: signal history is the record, not a cached answer.

Wallet lists

These resolve the wallet_set predicate on both streams. Authenticated, and every call writes an audit row before the mutation is applied — a failed audit write is a refused mutation.

endpointnotes
GET /v1/listsevery list with its member and blacklist counts
GET /v1/lists/{name}members and blacklist in full
POST /v1/lists {name}create (idempotent)
DELETE /v1/lists/{name}clear membership — the blacklist is PRESERVED
POST /v1/lists/{name}/add {wallets:[…]}add — 409 in full if any address is blacklisted
POST /v1/lists/{name}/remove {wallets:[…]}remove
POST /v1/lists/{name}/blacklist {wallets:[…]}remove AND permanently exclude
DELETE /v1/lists/{name}/blacklist {wallets:[…]}ADMIN-AUTHED — the only undo

The blacklist is permanent. An add naming a blacklisted address is refused in full — nothing is added — with 409 and a reason, and the refusal is itself audited. Deleting a list keeps its blacklist, so recreating the list by the same name inherits the same exclusions. Un-blacklisting needs a separate admin credential and removes from the blacklist only; it does not add the address back.

Stream API

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, e.g. list:james_tracked
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