# Build Robinhood Chain price alerts with the WebSocket stream

> Build Robinhood Chain price alerts on the Crius WebSocket price channel, with a runnable Node.js alert bot that checks liquidity and token security first.

Source: https://docs.crius.sh/guides/build-price-alerts/ · Updated 2026-10-28

Short answer

Subscribe to `price:` plus the token address on `wss://api.crius.sh/v1/robinhood/stream`. You get a snapshot right away, then updates with `usd`, `eth` and `liquidity_usd` whenever the token trades (at most 10 a second). Compare `usd` with your thresholds, ignore updates with too little `liquidity_usd`, and check `GET /v1/robinhood/token/{address}/security` once before you alert. Free plan: 10 tokens per connection.

[GET `/token/{address}/price`](https://docs.crius.sh/reference/tokens/token-price/) [GET `/token/{address}/security`](https://docs.crius.sh/reference/security/security-checks/) [GET `/prices`](https://docs.crius.sh/reference/tokens/batch-prices/)

A price alert on Robinhood Chain is a rule (“tell me when AI crosses $0.10”, “tell me when TSLA moves 3% in 15 minutes”) checked against a live price. The Crius WebSocket gives you that price on the `price:<token>` channel: a snapshot as soon as you subscribe, then a new `usd`, `eth` and `liquidity_usd` every time the token trades. A good alert bot adds two checks the price alone can’t give it: enough liquidity behind the price, and a token that isn’t a trap.

## What the price channel sends

Subscribe with `{"op": "subscribe", "channels": ["price:0x2e8c31162b855a2ffa90f6f8634643ad6f111e18"]}`. Right after the `subscribed` reply, the server sends the current price with `"snapshot": true` and no `seq`; after that, each update is a normal stream message.

| Field | Meaning |
| --- | --- |
| `data.usd` | Price per whole token in USD |
| `data.eth` | Price per whole token in ETH |
| `data.liquidity_usd` | Liquidity of the pool the price came from (2 × its quote side) |
| `block`, `ts` | The block and chain time (unix ms) of the trade that moved the price |
| `snapshot` | `true` on the first message after subscribing |

Updates are capped at 10 a second per token, and only come when the token trades. The [stream reference](https://docs.crius.sh/reference/stream/) covers the envelope, heartbeats and reorgs.

## Check the price source before you rely on it

The REST price has the same `usd`, `eth` and `liquidity_usd` plus the pool, a `confidence` level and a thin-liquidity flag. Call it once when a user creates an alert, to decide whether the token is worth alerting on at all.

Example [Anonymous · no key · 1 credit](https://docs.crius.sh/reference/tokens/token-price/)

### Artificial Inu's price and where it comes from

A user wants an alert on Artificial Inu (AI, 0x2e8c31162b855a2ffa90f6f8634643ad6f111e18). Is its price solid enough to alert on?

GET/v1/robinhood/token/0x2e8c31162b855a2ffa90f6f8634643ad6f111e18/price

  

**cURL**

```bash
curl "https://api.crius.sh/v1/robinhood/token/0x2e8c31162b855a2ffa90f6f8634643ad6f111e18/price"
```

**JavaScript**

```js
const res = await fetch("https://api.crius.sh/v1/robinhood/token/0x2e8c31162b855a2ffa90f6f8634643ad6f111e18/price");
const { data, meta } = await res.json();
```

**Python**

```python
import requests

r = requests.get(
    "https://api.crius.sh/v1/robinhood/token/0x2e8c31162b855a2ffa90f6f8634643ad6f111e18/price",
)
body = r.json()
print(body["data"])
```

**Real response 200 Captured Oct 28, 2026 · 25 lines (arrays trimmed)**

```json
{
  "data": {
    "confidence": "high",
    "dex": "uniswap_v4",
    "eth": 0.0000341958,
    "eth_usd": 2355.49,
    "liquidity_usd": 657440,
    "quote": "0x39dbed3a2bd333467115de45665cc57f813c4571",
    "quote_liquidity_usd": 328720,
    "source_pool": "0x230bc4a0fb5fb7ab27f6e72350dfa3f2489b0cdbaf34f0189d68672086ccb919",
    "symbol": "AI",
    "thin_liquidity": false,
    "token": "0x2e8c31162b855a2ffa90f6f8634643ad6f111e18",
    "usd": 0.0805479
  },
  "meta": {
    "block": 100410151,
    "block_age_ms": 315,
    "block_ts": 1793225421000,
    "chain": "robinhood",
    "finalized_block": 100401329,
    "lag_ms": 26,
    "safe_block": 100405028
  }
}
```

What this tells you

-   `usd: 0.0805479` comes from a Uniswap v4 pool (`dex: "uniswap_v4"`, `source_pool` is its 32-byte pool id), quoted in another token rather than ETH (`quote` is `0x39dbed3a2bd333467115de45665cc57f813c4571`).
-   `liquidity_usd: 657440` is 2 × the real quote side (`quote_liquidity_usd: 328720`). That’s the same number the stream sends in `liquidity_usd`, so a floor such as $10,000 works on both.
-   `confidence: "high"` and `thin_liquidity: false`: a single small trade can’t move this price far. For a token showing `thin_liquidity: true`, require a larger move or a second update before alerting.

[Token price reference](https://docs.crius.sh/reference/tokens/token-price/) Runs from your browser on the anonymous plan [Data looks wrong? Report it](mailto:support@crius.sh?subject=Wrong%20data%20in%20docs%20example%3A%20Artificial%20Inu's%20price%20and%20where%20it%20comes%20from&body=Page%3A%20https%3A%2F%2Fdocs.crius.sh%2Fguides%2Fbuild-price-alerts%2F%23price-source%0ARequest%3A%20GET%20%2Fv1%2Frobinhood%2Ftoken%2F0x2e8c31162b855a2ffa90f6f8634643ad6f111e18%2Fprice%0A%0AWhat's%20wrong%20or%20missing%3F%0A%0A)

Example [Anonymous · no key · 1 credit](https://docs.crius.sh/reference/security/security-checks/)

### The same token's security, checked once per alert

Before I tell users AI is pumping, is anything wrong with the token?

GET/v1/robinhood/token/0x2e8c31162b855a2ffa90f6f8634643ad6f111e18/security

  

**cURL**

```bash
curl "https://api.crius.sh/v1/robinhood/token/0x2e8c31162b855a2ffa90f6f8634643ad6f111e18/security"
```

**JavaScript**

```js
const res = await fetch("https://api.crius.sh/v1/robinhood/token/0x2e8c31162b855a2ffa90f6f8634643ad6f111e18/security");
const { data, meta } = await res.json();
```

**Python**

```python
import requests

r = requests.get(
    "https://api.crius.sh/v1/robinhood/token/0x2e8c31162b855a2ffa90f6f8634643ad6f111e18/security",
)
body = r.json()
print(body["data"])
```

**Real response 200 Captured Oct 28, 2026 · 127 lines (arrays trimmed)**

```json
{
  "data": {
    "contract": {
      "blacklist": false,
      "can_change_limits": false,
      "can_change_tax": false,
      "cooldown": false,
      "implementation": null,
      "is_proxy": false,
      "mintable": false,
      "owner": "0xeb7c034704ef8dcd2d32324c1545f62fb4ad0862",
      "ownership": "active",
      "pausable": false,
      "proxy_kind": null,
      "self_destruct": false,
      "trading_switch": false,
      "upgradeable": false,
      "verified": "none"
    },
    "flags": [
      "ownership_active",
      "unverified"
    ],
    "honeypot": {
      "is_honeypot": false,
      "reason": null,
      "selective": false
    },
    "hook": null,
    "known_asset": false,
    "launch": {
      "flags": [],
      "lp_top_wallet": {
        "address": "0x9701fb0ade1e269c8f64ec0c7b3cfadb31a13a52",
        "liquidity_usd": 703900,
        "pct": 19.8021
      },
      "sniper_supply_pct": 1.70207,
      "sniper_top_wallet_pct": 1.70207,
      "sniper_wallets": 1,
      "sniper_window_blocks": 10,
      "uniform_holders": null,
      "wallet_farm": null
    },
    "limits": {},
    "liquidity": {
      "status": "partially_locked",
      "summary": {
        "burned_pct": 14.4982,
        "locked_pct": 0,
        "next_unlock_ts": 1794049200000,
        "protocol_fee_pct": 0,
        "unlocked_in_wallets_pct": 73.5823,
        "weighting": "by pool liquidity_usd"
      }
    },
    "primary_dex": "uniswap_v4",
    "risk": "low",
    "score": 85,
    "tax": {
      "buy_pct": 0,
      "confidence": "high",
      "realized": {
        "buy_pct": 0,
        "by_wallet_class": {
          "dev_sell_pct": null,
          "others_sell_pct": 0
        },
        "last_sample_ts": 1793225667000,
        "max_sell_pct_seen": 99.99,
        "samples": {
          "buys": 98750,
          "sells": 101250
        },
        "sell_pct": 0,
        "window": "30d"
      },
      "sell_pct": 0,
      "simulated": {
        "amount_eth": 0.042421762259500888,
        "buy_gas": 145730,
        "buy_pct": 0,
        "can_buy": true,
        "can_sell": true,
        "can_transfer": true,
        "from": "eoa",
        "revert_reason": null,
        "router": "0x89e5db8b5aa49aa85ac63f691524311aeb649eba",
        "sell_gas": 126783,
        "sell_pct": 0,
        "transfer_pct": 0
      },
      "source": "realized",
      "transfer_pct": 0
    },
    "tax_history": [
      {
        "from_block": 99878573,
        "pct": 0,
        "side": "buy"
      },
      {
        "from_block": 99878935,
        "pct": 100,
        "side": "buy"
      },
      {
        "from_block": 99880233,
        "pct": 0,
        "side": "buy"
      }
    ],
    "tax_history_total": 919,
    "tax_mode": "dynamic",
    "token": "0x2e8c31162b855a2ffa90f6f8634643ad6f111e18"
  },
  "meta": {
    "block": 100412468,
    "block_age_ms": 741,
    "block_ts": 1793225673000,
    "chain": "robinhood",
    "finalized_block": 100401329,
    "lag_ms": 5,
    "safe_block": 100405028,
    "simulation": "eth_simulateV1 via Uniswap V2 router at head"
  }
}
```

What this tells you

-   `score: 85`, `risk: "low"`, with `ownership_active` (the owner can still act on the contract) and `unverified` as the only flags.
-   No taxes: `tax.buy_pct` and `tax.sell_pct` are 0, measured over 98,750 buys and 101,250 sells (`tax.realized.samples`), and the simulated buy, transfer and sell all went through (`tax.simulated.can_sell: true`, via the Uniswap V2 router named in `meta.simulation`).
-   `liquidity.status: "partially_locked"`: about 73.6% of the LP sits unlocked in wallets (`unlocked_in_wallets_pct`) and `next_unlock_ts` is 1794049200000 (November 7, 2026). A sharp drop could be liquidity leaving, so an alert bot can also subscribe to `lp:` for the token.

[Security checks reference](https://docs.crius.sh/reference/security/security-checks/) Runs from your browser on the anonymous plan [Data looks wrong? Report it](mailto:support@crius.sh?subject=Wrong%20data%20in%20docs%20example%3A%20The%20same%20token's%20security%2C%20checked%20once%20per%20alert&body=Page%3A%20https%3A%2F%2Fdocs.crius.sh%2Fguides%2Fbuild-price-alerts%2F%23security-before-alert%0ARequest%3A%20GET%20%2Fv1%2Frobinhood%2Ftoken%2F0x2e8c31162b855a2ffa90f6f8634643ad6f111e18%2Fsecurity%0A%0AWhat's%20wrong%20or%20missing%3F%0A%0A)

## An alert bot in Node.js

The bot reads rules, checks each token once, subscribes to their price channels and sends a message when a rule triggers. Each rule fires once, then re-arms after the price moves back past it. Delivery here is a webhook (`{"content": ...}` works with Discord webhooks); swap `notify` for email, Telegram or a push service.

```js
// alerts.mjs: CRIUS_API_KEY=... WEBHOOK_URL=... node alerts.mjs  (Node.js 22+)
const KEY = process.env.CRIUS_API_KEY;
const API = "https://api.crius.sh/v1/robinhood";
const MIN_LIQUIDITY_USD = 10_000;

const RULES = [
  { token: "0x2e8c31162b855a2ffa90f6f8634643ad6f111e18", name: "AI", above: 0.1 },
  { token: "0x2e8c31162b855a2ffa90f6f8634643ad6f111e18", name: "AI", below: 0.07 },
  { token: "0x322f0929c4625ed5bad873c95208d54e1c003b2d", name: "TSLA", movePct: 3, windowMs: 15 * 60_000 },
];

const blocked = new Set(); // tokens that failed the security check
const history = {}; // token -> [{ ts, usd }] for move rules
let backoff = 1000;

async function notify(text) {
  console.log(text);
  if (process.env.WEBHOOK_URL) {
    await fetch(process.env.WEBHOOK_URL, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify({ content: text }) });
  }
}

async function screen(token) {
  const res = await fetch(`${API}/token/${token}/security`, { headers: { "x-api-key": KEY } });
  const { data } = await res.json();
  if (!data || data.risk === "critical" || data.honeypot?.is_honeypot) {
    blocked.add(token);
    console.warn(`not alerting on ${token}: ${data?.risk} ${data?.flags?.join(", ")}`);
  }
}

function evaluate(token, p, ts) {
  if (blocked.has(token) || p.liquidity_usd < MIN_LIQUIDITY_USD) return;
  const h = (history[token] ??= []);
  h.push({ ts, usd: p.usd });
  while (h.length && h[0].ts < ts - 60 * 60_000) h.shift(); // keep one hour

  for (const r of RULES.filter((r) => r.token === token)) {
    let hit = false;
    let why = "";
    if (r.above != null) [hit, why] = [p.usd >= r.above, `above $${r.above}`];
    if (r.below != null) [hit, why] = [p.usd <= r.below, `below $${r.below}`];
    if (r.movePct != null) {
      const base = h.find((x) => x.ts >= ts - r.windowMs)?.usd;
      const pct = base ? ((p.usd - base) / base) * 100 : 0;
      [hit, why] = [Math.abs(pct) >= r.movePct, `${pct.toFixed(2)}% in ${r.windowMs / 60_000} min`];
    }
    if (hit && !r.fired) notify(`${r.name} $${p.usd} (${why}), liquidity $${Math.round(p.liquidity_usd)}`).catch(console.error);
    r.fired = hit; // re-arm once the condition is no longer true
  }
}

function connect() {
  const tokens = [...new Set(RULES.map((r) => r.token))].filter((t) => !blocked.has(t));
  const ws = new WebSocket(`wss://api.crius.sh/v1/robinhood/stream?api_key=${KEY}`);
  let idle;
  const touch = () => {
    clearTimeout(idle);
    idle = setTimeout(() => ws.close(), 45_000);
  };
  ws.onopen = () => {
    backoff = 1000;
    touch();
    ws.send(JSON.stringify({ op: "subscribe", channels: tokens.map((t) => `price:${t}`) }));
  };
  ws.onmessage = (e) => {
    touch();
    const m = JSON.parse(e.data);
    if (m.ch === "error") return console.error(m.data);
    if (!m.ch.startsWith("price:") || m.data.removed) return;
    evaluate(m.ch.slice(6), m.data, m.ts ?? Date.now()); // the snapshot has no ts: use the clock
  };
  ws.onclose = () => {
    clearTimeout(idle);
    setTimeout(connect, backoff + Math.random() * 500);
    backoff = Math.min(backoff * 2, 30_000);
  };
  ws.onerror = () => ws.close();
}

for (const t of new Set(RULES.map((r) => r.token))) await screen(t);
connect();
```

1.  **Screen once, not per update.** One security call per token (1 credit, or free without a key at 2 requests per second) when the alert is created. Re-check daily, or subscribe to `tax:` and `lp:` for the token to hear about tax changes and liquidity removal.
    
2.  **Use the snapshot.** After every reconnect the snapshot gives the current price immediately. A price alert doesn’t need `resume`: the latest price is what counts, and the snapshot is it.
    
3.  **Floor the liquidity.** Skip updates whose `liquidity_usd` is under your floor. Meme tokens often have one deep pool and several thin ones; the stream’s price comes from the deepest, but a token whose only pool is thin is easy to push across a threshold.
    
4.  **Mind your subscription count.** The `hello` message carries `max_subscriptions`. Past it, the server answers `channel_denied` with the channels it refused; split them across connections (Starter and up) or poll [batch prices](https://docs.crius.sh/reference/tokens/batch-prices/) for the rest.
    

Plan and cost

The price stream needs a Free key with the `stream` scope and costs 1 credit per connection. On the Free plan that’s 10 tokens on one connection; more tokens need Starter or above, or REST polling. See [Plans & limits](https://docs.crius.sh/plans-and-limits/).

## Frequently asked questions

**How often does the price channel update?**

Every time the token trades, at most once per 100 ms per token. A token that doesn't trade sends nothing, so a quiet price is not a stale connection: the 15-second heartbeat tells you the socket is alive.

**How many tokens can one alert bot watch?**

One `price:` subscription per token. The Free plan allows 10 subscriptions on 1 connection, Starter 100 on each of 3 connections, Pro 1,000 on each of 10. For a long watch list on a small plan, poll [batch prices](https://docs.crius.sh/reference/tokens/batch-prices/) (100 tokens per 1-credit call) and stream only the tokens closest to a threshold.

**Why did my alert fire on a price nobody can trade at?**

A thin pool can print a price far from the real market. Each price message carries `liquidity_usd` (2 × the quote side of the pool the price came from): skip updates below a floor you choose. The REST [price](https://docs.crius.sh/reference/tokens/token-price/) also returns `confidence` and `thin_liquidity` for a second look before you send.

**Do price alerts need an API key?**

The stream does: a Free key with the `stream` scope. The REST calls the bot makes (price, security, batch prices) also work without a key on the anonymous plan, at 2 requests per second.

## Related

[Guide **Get a token price** Get the live USD and ETH price of any Robinhood Chain token, the pool it comes from and how much to trust it, with one fast API call. Real examples, no key. Read](https://docs.crius.sh/guides/get-token-price/) [Guide **Token security score** Score any Robinhood Chain token from 0 to 100 with one API call. Honeypot, buy and sell taxes, owner powers, LP locks, v4 hook risk and insider launches, with real examples. Read](https://docs.crius.sh/guides/token-security-score/) [Guide **Stream trades** Stream every trade of a Robinhood Chain token, pool or wallet over the Crius WebSocket, with Node.js and Python clients that reconnect, resume and backfill. Read](https://docs.crius.sh/guides/stream-trades-websocket/) [API reference **WebSocket stream** Real-time Robinhood Chain data over one WebSocket - trades, prices, live candles, new launches, graduations, new pools, LP and tax changes, wallet activity, with replay and reorg handling. Read](https://docs.crius.sh/reference/stream/) [Concept **Token prices** How the Crius API prices Robinhood Chain tokens from on-chain pools, how the price pool is picked, and what confidence and thin\_liquidity mean. Read](https://docs.crius.sh/concepts/token-prices/)
