robinhood-chain-sdk 0.9.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -15,6 +15,10 @@ Robinhood Chain (RHC) is an **Arbitrum Orbit L2, chain id 4663**. This SDK wraps
15
15
 
16
16
  The KOL→EVM mapping is unique to MadeOnSol: each tracked Solana KOL's Robinhood-Chain wallet is recovered by tracing their Solana→EVM bridge deposits (deBridge / Relay / Mayan / Wormhole), then attributed on-chain to the effective trading account (`tx.from`, or the ERC-4337 userOp sender when the trade was bundled). Robinhood Chain coverage is **bundled into every MadeOnSol tier at no extra cost — same `msk_` API key, same base URL** as the Solana product.
17
17
 
18
+ > **New in 0.10.0 — named subscriptions: several independent subscriptions per socket.** `subscribe({ subId, channels, filters })`, `updateSubscription(subId, filters)`, `unsubscribe(subId)`, `getSubscriptions()` / `listSubscriptions()`. Each named subscription has its own channels and filters (the server caps the total per connection, default included: PRO 5, ULTRA 10, BUSINESS 20); frames carry `evt.sub_id`; an event matching several subscriptions is delivered once per subscription (dedupe per `(sub_id, id)`). Resume is per subscription with one commit for the connection. The plain `subscribe(channels, filters)` API is unchanged. See "Named subscriptions" in the stream section.
19
+
20
+ > **Also in 0.10.0 — `rhc:token_prices` and enriched RHC trade payloads (WS Phase 2).** A tenth RHC channel, `rhc:token_prices` (PRO+, address-scoped): subscribe with `filters.addresses` (25 / 100 / 250 per connection on PRO / ULTRA / BUSINESS, rejected above the cap) and receive one `snapshot: true` frame per address, then ticks derived from the RHC trade feed at most once per address per 250 ms, each typed `RhcTokenPriceTick` with `quality` fresh | stale | unreliable and a `quality_reason`; a stale or unreliable price is never delivered as fresh. `rhc:dex_trade` / `rhc:dex_trade_unattributed` frames now carry additive enrichment, typed `RhcDexTradeEvent` / `RhcDexTradeUnattributedEvent`: exact `amount_in_raw` / `amount_out_raw` decimal strings (never floats), `token` + `quote` identity with decimals, `metadata_status`, `price_status` / `price_source` / `price_observed_at`, `mc_status` (why `mc_usd` is null) and `side` / `side_reason`. Every existing field keeps its name.
21
+
18
22
  > **New in 0.9.0 — stream recovery: resume cursor, de-duplication, honest gaps.** The managed stream now tracks the cursor `{ instance, seq, ts }` of the last frame your handlers finished and resumes after it on every reconnect (the v1 `resume` request, with an automatic fallback to `replay_since_seq` / `replay_since_ts` on older servers). Delivery is at-least-once, de-duplicated by event `id`; new lifecycle events `cursor`, `replay`, `gap` (what could not be recovered — a `seq` gap is never loss) and `fatal`. Close codes are handled: 4001 re-fetches the token (bounded), 4002 waits ≥ 60 s instead of looping every second, 4003 stops, 4008 resumes; the backoff resets only after a `subscribed` ack. Every server `warning` frame is emitted (incl. `channels_rejected` / `channels_revoked`). `StreamChannel` / `STREAM_CHANNELS` now list all nine RHC channels (adds `rhc:dex_trades_unattributed`, `rhc:new_tokens`, `rhc:token_locks`); `RhcPriceAlertEvaluation.mode` is `"event_driven" | "polled"` with the new optional `trigger` / `fallback_poll_seconds`. See the stream section's "Recovery" notes.
19
23
 
20
24
  > **New in 0.8.2 — mutation calls are no longer retried automatically (security fix, SDK-02).** A lost response or transient network error after a `POST`/`PATCH`/`DELETE` (rule create, watchlist change, `stream.rotate()`) used to retry automatically — which could duplicate a rule or rotate a token twice. Mutating calls (including the batch-read POST endpoints) now make exactly one attempt; `GET` retries/backoff (`maxRetries`) are unchanged. If a mutating call fails, check current state before deciding whether to retry by hand. No public API/type changes.
@@ -247,9 +251,11 @@ const { events, has_more, next_before, coverage } = await client.lpEvents({ toke
247
251
  for (const ev of events) {
248
252
  if (ev.provider_is_token_deployer) console.warn("deployer pulled liquidity:", ev.tx_hash, ev.dex, ev.token_amount_raw);
249
253
  }
250
- console.log(coverage.adds_persisted); // false — always
254
+ console.log(coverage.events); // ["remove"] — the default request is removals only
251
255
  ```
252
256
 
257
+ Since 2026-09-23 `action: "add" | "pool_created" | "all"` opts into liquidity adds (kept 7 days) and pool creations, and every row carries the depth fields `tick_lower` / `tick_upper`, `liquidity_delta`, `in_range`, `active_liquidity_delta`, `active_share` (v3/v4), `share_of_reserves` (v2) and `material` — null where the pool state was unknown and on older rows.
258
+
253
259
  > **Removals ONLY.** Liquidity adds are not persisted (v4 adds share the topic and are dropped at decode; v2/v3 `Mint` is not subscribed), so every row is `event: "remove"` and an empty page means "no removals seen", never "no liquidity activity" — the `coverage` block spells this out. Amounts are **raw uint256 decimal strings** (`liquidity`, `amount0` / `amount1`, plus pre-resolved `token_amount_raw` / `quote_token` / `quote_amount_raw`) — do not `Number()` them; v4 rows carry `liquidity` only because the pool manager emits no token amounts. Filters: `token`, `pool` (v2/v3 address or v4 bytes32 poolId), `provider`, `dex`. Data since 2026-08-05.
254
260
 
255
261
  ## Token intelligence — `client.tokens`
@@ -579,7 +585,7 @@ await client.kol.firstTouchSubscriptions.update(subscription.id, { filters: {} }
579
585
 
580
586
  ## Streaming — `client.stream` (PRO+)
581
587
 
582
- Managed WebSocket with token fetch on every (re)connect, auto-reconnect with backoff, heartbeat liveness, and typed events. Stream tokens **never expire** (since 2026-08-27) — there is no refresh timer; `client.stream.getToken()` returns the same token every call (`expires_at` / `next_refresh_at` are always `null`), and `getToken({ rotate: true })` replaces it (the old one keeps working for 60 s). Nine RHC channels:
588
+ Managed WebSocket with token fetch on every (re)connect, auto-reconnect with backoff, heartbeat liveness, and typed events. Stream tokens **never expire** (since 2026-08-27) — there is no refresh timer; `client.stream.getToken()` returns the same token every call (`expires_at` / `next_refresh_at` are always `null`), and `getToken({ rotate: true })` replaces it (the old one keeps working for 60 s). Eleven RHC channels:
583
589
 
584
590
  | Channel | Emits | Tier | Scope |
585
591
  |---|---|---|---|
@@ -591,7 +597,17 @@ Managed WebSocket with token fetch on every (re)connect, auto-reconnect with bac
591
597
  | `rhc:price_alert:events` | `rhc:price_alert:dip`, `rhc:price_alert:recovery` | PRO+ | user-scoped; event-driven off each trade (a few seconds), not sub-second |
592
598
  | `rhc:kol:coordination` | `rhc:kol:coordination` | PRO+ | user-scoped — only **your** rules' fires |
593
599
  | `rhc:kol:first_touches` | `rhc:kol:first_touch` | PRO+ | broadcast — ULTRA gates only the first-touch *subscription CRUD*, not this channel |
594
- | `rhc:token_locks` | `rhc:token_lock` | PRO+ | broadcast — a token lock / vesting contract created on chain |
600
+ | `rhc:token_locks` | `rhc:token_lock` (+ `rhc:token_unlock_upcoming`, `rhc:token_unlock_available` with `filters.lifecycle: true`) | PRO+ | broadcast — a token lock / vesting contract created on chain; opt-in unlock-schedule events (claims / cancels are not observable on RHC) |
601
+ | `rhc:token_prices` | `rhc:token_price` | PRO+ | **address-scoped** — `filters.addresses` required (25 / 100 / 250 per connection); one `snapshot: true` frame per address, then ≤ 1 tick per address per 250 ms with `quality` fresh / stale / unreliable + reason (`RhcTokenPriceTick`); no `seq` / `id` |
602
+ | `rhc:lp_events` | `rhc:lp_event` | **ULTRA+** | broadcast — liquidity `add` / `remove` / `pool_created` on tracked Uniswap v2/v3/v4 pools with `in_range`, `active_share`, `share_of_reserves`, `material` (`RhcLpStreamEvent`); filters `RhcLpEventsFilters`; durable resume |
603
+
604
+ **Liquidity events and lock schedule (server 2026-09-23).** `rhc:lp_events` frames carry raw `amount0` / `amount1` strings (null on v4), the position range and, for v3/v4, `in_range` / `active_liquidity_delta` / `active_share` — a share of liquidity at the current price, not of TVL (an out-of-range removal is `active_share: 0`); v2 carries `share_of_reserves`; `material: true` = a removal of ≥ 25 %. When the pool's tick was not known the active fields are `null` with `active_share_reason: "pool_state_unknown"` — never guessed. `provider` is usually the router / position manager. No USD field. On `rhc:token_locks`, `filters.lifecycle: true` adds `rhc:token_unlock_upcoming` (within 24 h) and `rhc:token_unlock_available` (passed within 30 min — **claimable per the schedule, not claimed**; `withdrawals_tracked: false`).
605
+
606
+ ```ts
607
+ const lp = client.stream.connect();
608
+ lp.subscribe({ subId: "rugs", channels: ["rhc:lp_events"], filters: { actions: ["remove"], material_only: true } });
609
+ lp.on("rhc:lp_event", (e: RhcLpStreamEvent) => console.log(e.dex, e.pool, e.action, e.active_share ?? e.share_of_reserves));
610
+ ```
595
611
 
596
612
  > **Deprecated:** `rhc:trades` was never a real channel — 0.4.0 subscribers got a `channels_rejected` warning and silence. The server now accepts it as an alias of `rhc:dex_trades` (and acks it under the canonical name), and the SDK keeps the literal marked `@deprecated` so 0.4.0 code compiles. Use `rhc:dex_trades`.
597
613
 
@@ -639,6 +655,22 @@ stream.on("gap", (g) => console.warn("may be missing:", g.reasons, g.skipped));
639
655
  stream.on("fatal", (f) => console.error("stream stopped:", f.code, f.reason));
640
656
  ```
641
657
 
658
+ ### Named subscriptions *(new in 0.10.0)*
659
+
660
+ One socket can hold several independent subscriptions, each with its own channels and filters; the server caps the total per connection, the default one included (PRO 5, ULTRA 10, BUSINESS 20). `subscribe(channels, filters)` stays the connection's `"default"` subscription and its wire is unchanged; `subscribe({ subId, channels, filters })` opens a named one (`subId`: 1-64 characters of `A-Z a-z 0-9 _ . -`). A frame delivered under a named subscription carries `evt.sub_id`. **An event that matches several subscriptions is delivered once per matching subscription**, each copy stamped with its `sub_id`: the client dedupes per `(sub_id, id)`, so the same event can legitimately reach a handler twice, under two sub_ids. Filters of one subscription never affect another. `updateSubscription(subId, filters)` REPLACES that subscription's filters (`"default"` addresses the plain one), `unsubscribe(subId)` removes it, `getSubscriptions()` is the local view and `listSubscriptions()` asks the server (`list` / `subscriptions`). Server refusals arrive as `warning` frames carrying the `sub_id` and one of `invalid_sub_id`, `too_many_subscriptions`, `unknown_sub_id`, `invalid_filters`, `channels_rejected`, `channels_revoked`, `replay_in_progress`; a subscription refused as `too_many_subscriptions` or `invalid_sub_id` is dropped locally so reconnects stop re-requesting it. Lifecycle events `updated` and `unsubscribed` surface the server acks.
661
+
662
+ **Resume with several subscriptions** is per subscription: on every reconnect each subscription is re-sent with the same cursor, the server serves one replay per subscription, one after another (`replay_start` … `replay_end` each carry the `sub_id`; live frames are held until the last one ends), and the cursor commits once ALL of them have ended, at the smallest `last_seq` / `last_ts` across them. The `replay` event lists `subscriptions` and the raw `ends` per subscription; a gap's `channels` entries are keyed `sub_id/channel` for named subscriptions, and an incomplete retryable replay is retried for those subscriptions only. Against an older server that ignores `sub_id`, the client emits `warning` `named_subscriptions_unsupported` once.
663
+
664
+ ```ts
665
+ const stream = client.stream.connect();
666
+ stream.subscribe({ subId: "kol-buys", channels: ["rhc:kol_trades"], filters: { action: "buy" } });
667
+ stream.subscribe({ subId: "firehose", channels: ["rhc:dex_trades"], filters: {} });
668
+ stream.on("rhc:kol_trade", (t, evt) => console.log(evt!.sub_id, t)); // "kol-buys"
669
+ stream.updateSubscription("kol-buys", { action: "buy" });
670
+ console.log(await stream.listSubscriptions()); // [{ subId, channels, filters }, …]
671
+ stream.unsubscribe("firehose");
672
+ ```
673
+
642
674
  ## Error handling
643
675
 
644
676
  Every method throws `RobinhoodError` on a non-2xx response, with `.status`, `.body`, `.message`, and `.requestId` (the API's `_rid` — include it when reporting issues). GET requests retry network failures, rate-limits (`429`) and transient server errors (`5xx`) up to `maxRetries`, with exponential backoff and `Retry-After` / `X-RateLimit-Reset` hints.
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { RobinhoodStream } from "./stream.js";
2
2
  import type { StreamClientOptions } from "./stream.js";
3
3
  export { RobinhoodStream, STREAM_CHANNELS } from "./stream.js";
4
- export type { StreamClientOptions, StreamChannel, StreamEventName, StreamEvent, StreamLifecycleEvent, StreamWarning, StreamCursor, StreamReplayResult, StreamGap, StreamFatal, StreamTokenLike, } from "./stream.js";
4
+ export type { StreamClientOptions, StreamChannel, StreamEventName, StreamEvent, StreamLifecycleEvent, StreamWarning, StreamCursor, StreamReplayResult, StreamGap, StreamFatal, StreamSubscription, StreamSubscribeOptions, StreamTokenLike, } from "./stream.js";
5
5
  /** Robinhood Chain — Arbitrum Orbit L2, chain id 4663. */
6
6
  export declare const CHAIN_ID = 4663;
7
7
  /** Thrown for any non-2xx API response (and exhausted network retries). */
@@ -369,17 +369,26 @@ export interface RhcLpEventsParams {
369
369
  provider?: string;
370
370
  /** Filter by DEX version. */
371
371
  dex?: UniswapVersion;
372
+ /**
373
+ * Which actions to return (2026-09-23). Default `"remove"` = the historical
374
+ * removals-only feed, unchanged. `"add"` (kept 7 days), `"pool_created"` or
375
+ * `"all"` opt into rows persisted since the WS Phase 3 producer.
376
+ */
377
+ action?: RhcLpAction | "all";
372
378
  /** Opaque cursor from `next_before` (same (block_time,id) keyset as /rhc/trades). */
373
379
  before?: string;
374
380
  }
381
+ /** A liquidity action on Robinhood Chain. */
382
+ export type RhcLpAction = "add" | "remove" | "pool_created";
375
383
  /**
376
- * One liquidity REMOVAL from `/rhc/lp-events`. Every row is `event: "remove"` —
377
- * adds are not persisted. Amounts are raw on-chain uint256 integers as decimal
378
- * STRINGS; v4 rows carry `liquidity` only (the pool manager emits no token
379
- * amounts), so `amount0` / `amount1` / `token_amount_raw` are null there.
384
+ * One liquidity event from `/rhc/lp-events`. Without `action` every row is
385
+ * `event: "remove"` (the default feed is removals only). Amounts are raw
386
+ * on-chain uint256 integers as decimal STRINGS; v4 rows carry `liquidity` only
387
+ * (the pool manager emits no token amounts), so `amount0` / `amount1` /
388
+ * `token_amount_raw` are null there.
380
389
  */
381
390
  export interface RhcLpEvent {
382
- event: "remove";
391
+ event: RhcLpAction;
383
392
  pool: string;
384
393
  dex: UniswapVersion;
385
394
  fee_tier: number | null;
@@ -411,6 +420,116 @@ export interface RhcLpEvent {
411
420
  block_time: string;
412
421
  tx_hash: string;
413
422
  log_index: number;
423
+ tick_lower?: number | null;
424
+ tick_upper?: number | null;
425
+ /** Signed liquidity change (v3/v4), int256 as string. */
426
+ liquidity_delta?: string | null;
427
+ /** tick_lower <= current tick < tick_upper just before the event; null = tick unknown. */
428
+ in_range?: boolean | null;
429
+ /** liquidity_delta when in range, "0" out of range. */
430
+ active_liquidity_delta?: string | null;
431
+ /** |active delta| / active liquidity before (v3/v4); 0 out of range; adds can exceed 1. */
432
+ active_share?: number | null;
433
+ /** v2 only: amount / reserve before (from the pair's Sync). */
434
+ share_of_reserves?: number | null;
435
+ /** true = a removal of ≥ 25 % of reserves / active liquidity; null for adds, creations and unknown shares. */
436
+ material?: boolean | null;
437
+ }
438
+ /** Why `active_share` is null / not the relevant share. */
439
+ export type RhcLpActiveShareReason = "pool_created" | "not_concentrated" | "pool_state_unknown" | "no_active_liquidity";
440
+ /**
441
+ * `rhc:lp_event` frame `data` (WS channel `rhc:lp_events`, ULTRA / BUSINESS,
442
+ * 2026-09-23). Frame id = `rhc:lp_event:<tx_hash>:<log_index>`. Every key is
443
+ * always present (null when unknown). `in_range` / `active_*` are null with
444
+ * `active_share_reason: "pool_state_unknown"` when the pool's tick was not
445
+ * known — never guessed. `provider` is usually a router / position manager,
446
+ * not the beneficial owner. No USD field.
447
+ */
448
+ export interface RhcLpStreamEvent {
449
+ chain: Chain;
450
+ action: RhcLpAction;
451
+ dex: UniswapVersion;
452
+ pool: string;
453
+ token_address: string | null;
454
+ token: {
455
+ address: string | null;
456
+ symbol: string | null;
457
+ decimals: number | null;
458
+ };
459
+ token0: string | null;
460
+ token1: string | null;
461
+ provider: string | null;
462
+ liquidity: string | null;
463
+ /** NULL on v4 — ModifyLiquidity reports no token amounts. */
464
+ amount0: string | null;
465
+ amount1: string | null;
466
+ tick_lower: number | null;
467
+ tick_upper: number | null;
468
+ liquidity_delta: string | null;
469
+ in_range: boolean | null;
470
+ active_liquidity_delta: string | null;
471
+ active_share: number | null;
472
+ active_share_reason: RhcLpActiveShareReason | null;
473
+ share_of_reserves: number | null;
474
+ material: boolean | null;
475
+ block_number: number | null;
476
+ block_time: string | null;
477
+ tx_hash: string;
478
+ log_index: number | null;
479
+ }
480
+ /** Per-subscription filters for `rhc:lp_events` (all optional, AND; invalid values reject the channel). */
481
+ export interface RhcLpEventsFilters {
482
+ addresses?: string[];
483
+ pools?: string[];
484
+ dexes?: UniswapVersion[];
485
+ actions?: RhcLpAction[];
486
+ material_only?: boolean;
487
+ /** 0..1000 — only events whose relevant share (reserves on v2, active on v3/v4) is known and ≥ this. */
488
+ min_share?: number;
489
+ }
490
+ /** Unlock-schedule event names on `rhc:token_locks` (only with `filters.lifecycle: true`). */
491
+ export type RhcTokenLockLifecycleEventName = "rhc:token_unlock_upcoming" | "rhc:token_unlock_available";
492
+ /**
493
+ * Filters that switch `rhc:token_locks` to lifecycle mode. Claims, extensions
494
+ * and cancels are NOT observable on Robinhood Chain — only the unlock schedule.
495
+ */
496
+ export interface RhcTokenLockLifecycleFilters {
497
+ lifecycle: true;
498
+ events?: RhcTokenLockLifecycleEventName[];
499
+ unlock_kinds?: Array<"cliff" | "final" | "tranche">;
500
+ /** Token scope for creates AND lifecycle (≤ 500); applied only with `lifecycle: true`. */
501
+ addresses?: string[];
502
+ }
503
+ /**
504
+ * `rhc:token_unlock_upcoming` (the lock's NEXT unlock, when it is within 24 h) /
505
+ * `rhc:token_unlock_available` (passed within the last 30 min — claimable PER
506
+ * THE SCHEDULE, NOT claimed). Frame id = `<event>:<event_key>`.
507
+ */
508
+ export interface RhcTokenUnlockScheduleEvent {
509
+ event_key: string;
510
+ lock_id: string;
511
+ token_address: string;
512
+ family: string;
513
+ observed_at: string | null;
514
+ unlock_at: string;
515
+ unlock_kind: "cliff" | "final" | "tranche";
516
+ amount_raw: string | null;
517
+ amount_reason: string | null;
518
+ unlocked_total_raw: string | null;
519
+ release_model: "at_end" | "linear" | "tranched";
520
+ claimable?: true;
521
+ chain: Chain;
522
+ locker: string | null;
523
+ kind: string | null;
524
+ subject: string | null;
525
+ lp_kind: string | null;
526
+ lp_pool: string | null;
527
+ sender: string | null;
528
+ recipient: string | null;
529
+ locked_amount_raw: string | null;
530
+ amount_unit: string | null;
531
+ decimals: number | null;
532
+ withdrawals_tracked: false;
414
533
  }
415
534
  export interface RhcLpEventsResponse {
416
535
  chain: Chain;
@@ -419,10 +538,11 @@ export interface RhcLpEventsResponse {
419
538
  has_more: boolean;
420
539
  /** Opaque pagination cursor; pass back as `before`. */
421
540
  next_before: string | null;
422
- /** Honesty block: `events: ["remove"]`, `adds_persisted: false`, note, since. */
541
+ /** Honesty block: the actions this response covers (["remove"] by default), adds_persisted, note, since. */
423
542
  coverage: {
424
543
  events: string[];
425
- adds_persisted: boolean;
544
+ adds_persisted: boolean | null;
545
+ adds_retention_days?: number;
426
546
  note: string;
427
547
  since: string;
428
548
  };
@@ -1995,6 +2115,130 @@ export interface RhcPriceAlertEvent {
1995
2115
  delivered: boolean;
1996
2116
  delivered_at: string | null;
1997
2117
  }
2118
+ /** One side of a swap as the server resolved it. `decimals` null = not yet enriched. */
2119
+ export interface RhcSideIdentity {
2120
+ address: string | null;
2121
+ symbol: string | null;
2122
+ decimals: number | null;
2123
+ }
2124
+ /** known: decimals AND symbol resolved · pending: token seen, enrichment queued · unknown: never seen. */
2125
+ export type RhcMetadataStatus = "known" | "pending" | "unknown";
2126
+ /** fresh: swap-derived quote inside the producer's 60-min freshness rule · stale: priced through the v4 pool-state fallback · none: unpriced. */
2127
+ export type RhcPriceStatus = "fresh" | "stale" | "none";
2128
+ export type RhcPriceSource = "swap_quote" | "pool_state_quote";
2129
+ /** Why `mc_usd` is null (or "ok"). `liquidity_gate` = MC exceeded pool TVL x 50,000, the same gate that nulls the stored MC. */
2130
+ export type RhcMcStatus = "ok" | "no_price" | "no_supply" | "dust" | "ceiling" | "liquidity_gate" | "no_liquidity";
2131
+ export type RhcSideReason = "both_sides_quote" | "no_recognized_quote";
2132
+ /**
2133
+ * `rhc:dex_trade` frame `data` (channel `rhc:dex_trades`, ULTRA+). Every field
2134
+ * that existed before 2026-09-22 keeps its name; the block from
2135
+ * `amount_in_raw` down is the additive enrichment. `*_raw` amounts are uint256
2136
+ * as EXACT decimal strings — never parse them into a float. A KOL trade also
2137
+ * appears on `rhc:kol_trades` with the same `tx_hash` + `log_index` (different
2138
+ * event id). The durable backfill rebuilds every key it can and lists the rest
2139
+ * in the frame's `missing` (liquidity_usd, quote.usd, quote.observed_at).
2140
+ */
2141
+ export interface RhcDexTradeEvent {
2142
+ chain: "robinhood";
2143
+ token_address: string;
2144
+ token_symbol: string | null;
2145
+ action: "buy" | "sell";
2146
+ dex: string;
2147
+ pool: string;
2148
+ eth_amount: number | null;
2149
+ price_usd: number | null;
2150
+ mc_usd: number | null;
2151
+ /** Pool TVL at broadcast (null = not yet tracked, never "zero"). */
2152
+ liquidity_usd: number | null;
2153
+ trader: string | null;
2154
+ tx_hash: string;
2155
+ log_index: number;
2156
+ block_number: number;
2157
+ traded_at: string;
2158
+ amount_in_raw: string | null;
2159
+ amount_out_raw: string | null;
2160
+ token_amount_raw: string | null;
2161
+ quote_amount_raw: string | null;
2162
+ token: RhcSideIdentity;
2163
+ quote: RhcSideIdentity & {
2164
+ usd: number | null;
2165
+ observed_at: string | null;
2166
+ };
2167
+ token_decimals: number | null;
2168
+ quote_decimals: number | null;
2169
+ metadata_status: RhcMetadataStatus;
2170
+ price_status: RhcPriceStatus;
2171
+ price_source: RhcPriceSource | null;
2172
+ /** ISO block time of the print the price was observed at; null when unpriced. */
2173
+ price_observed_at: string | null;
2174
+ mc_status: RhcMcStatus | null;
2175
+ /** `action` when a canonical quote (WETH / ETH / USDG / VIRTUAL or an equity quote) defines "the token", else null. */
2176
+ side: "buy" | "sell" | null;
2177
+ side_reason: RhcSideReason | null;
2178
+ launchpad: string | null;
2179
+ }
2180
+ /** `rhc:dex_trade_unattributed` frame `data` (channel `rhc:dex_trades_unattributed`, ULTRA+). Raw legs plus per-leg identity; never a buy/sell side. */
2181
+ export interface RhcDexTradeUnattributedEvent {
2182
+ chain: "robinhood";
2183
+ dex: string;
2184
+ pool: string;
2185
+ token_in: string;
2186
+ token_out: string;
2187
+ amount_in: string | number | null;
2188
+ amount_out: string | number | null;
2189
+ liquidity_usd: number | null;
2190
+ trader: string | null;
2191
+ tx_hash: string;
2192
+ log_index: number;
2193
+ block_number: number;
2194
+ traded_at: string;
2195
+ amount_in_raw: string | null;
2196
+ amount_out_raw: string | null;
2197
+ token_in_meta: RhcSideIdentity;
2198
+ token_out_meta: RhcSideIdentity;
2199
+ metadata_status: RhcMetadataStatus;
2200
+ price_status: "none";
2201
+ price_source: null;
2202
+ price_observed_at: null;
2203
+ side: null;
2204
+ side_reason: RhcSideReason;
2205
+ }
2206
+ /** Why a tick is not `fresh`. Every rule reuses a server-side pricing guard; none is client-tunable. */
2207
+ export type RhcTickQualityReason = "no_price" | "dust_trade" | "curve_live" | "state_quote_fallback" | "low_liquidity" | "price_age" | "mc_ceiling";
2208
+ /**
2209
+ * `rhc:token_price` frame `data` (channel `rhc:token_prices`, PRO+, address-
2210
+ * scoped). On subscribe every address first gets ONE frame with
2211
+ * `snapshot: true` (the current price row; `source: "rhc_token_prices"`), then
2212
+ * ticks derived from the RHC trade feed (`source: "rhc-dex-stream:trade"`),
2213
+ * at most one per address per 250 ms with the latest value. A `stale` or
2214
+ * `unreliable` price is only ever delivered with its flag — treat `quality`
2215
+ * as part of the price. State stream: no `seq` / `id`, not replayed; a resume
2216
+ * sends a fresh snapshot.
2217
+ */
2218
+ export interface RhcTokenPriceTick {
2219
+ chain: "robinhood";
2220
+ address: string;
2221
+ symbol: string | null;
2222
+ price_usd: number | null;
2223
+ /** ETH-denominated price; present on snapshots, null on trade-derived ticks. */
2224
+ price_native: number | null;
2225
+ market_cap_usd: number | null;
2226
+ liquidity_usd: number | null;
2227
+ source_pool: string | null;
2228
+ source_dex: string | null;
2229
+ /** ISO block time of the print (tick) or the last trade (snapshot). */
2230
+ observed_at: string | null;
2231
+ /** Age of `observed_at` when the frame was built (ms). */
2232
+ price_age_ms: number | null;
2233
+ quality: "fresh" | "stale" | "unreliable";
2234
+ quality_reason: RhcTickQualityReason | null;
2235
+ price_source: RhcPriceSource | null;
2236
+ /** The print this tick came from; null on a snapshot. */
2237
+ tx_hash: string | null;
2238
+ log_index: number | null;
2239
+ block_number: number | null;
2240
+ source: "rhc_token_prices" | "rhc-dex-stream:trade";
2241
+ }
1998
2242
  export interface RhcPriceAlertEventsResponse {
1999
2243
  chain: Chain;
2000
2244
  events: RhcPriceAlertEvent[];