Skip to content

Latest commit

 

History

History
197 lines (136 loc) · 16 KB

File metadata and controls

197 lines (136 loc) · 16 KB

🐤 canary

Place one tiny resting order at the price you care about, watch exactly one account, and get the instant fill signal the moment the market trades through your level — instead of drinking from a firehose of every trade on the venue.

Onchain, every account is public, so in principle you could learn "did price hit $X?" by watching every trade on a venue like Jupiter. But that's hundreds or thousands of accounts, almost all of it noise. A canary flips it around: you let Jupiter answer the question for you. Drop one resting canary order at your level, and subscribe to the single on-chain order account it creates. When price trades through, that one account changes — and a Quicknode Solana gRPC stream tells you in the same moment the chain knows.

Both approaches tell you the moment price trades through your level. One makes you drink from a firehose. The other just chirps. Quicknode Solana gRPC is what makes watching that one account instant.


Powered by Quicknode Solana gRPC

The whole demo hinges on one Quicknode add-on: Quicknode Solana gRPC. It streams account + slot updates over a long-lived gRPC connection, so the instant our one order account closes, the dashboard knows — in the same moment the chain does. That live wire is the proof-of-life pulse on the fold and the signal · Quicknode gRPC credit on the trigger banner.

Enable the add-on on a Quicknode Solana Mainnet endpoint (it runs on a dedicated port 10000); the docs are here. The subscription itself — one account, with reconnect + keepalive — lives in src/server/yellowstone.ts.


What you'll see (≈30 seconds)

  1. The fold shows almost nothing: the canary, one status word (Armed & watching), and a single quiet line — resting at $65,003 · 1% below BTC $65,660 · stream live. One glance tells the whole story.
  2. As spot nears your level the bird gets agitated and the word changes to Getting close….
  3. Price trades through → the order fills → the dashboard erupts: a red flash, a chirp, and a trigger banner whose fact chips spell out price · time · asset · signal · Quicknode gRPC · mode — then it settles into a calm, persistent Triggered at $X state (not alarm-red forever). An action chip shows what your strategy hook did (logged / webhook sent ✓ / swapped $X).
  4. Scroll down for the rest: the one watched account (linked to Solana Explorer), the live gRPC stream + slot counter, the firehose vs. canary panel, and the running event log.

The terminal tells the same story on its own — colored lifecycle lines and a big boxed CANARY TRIGGERED block — so it's a usable shot too.

The canary firing ↑ add a GIF/screenshot at docs/trigger.gif. The new fold (bird + status + one line) and the fire moment both capture well; replay mode re-fires infinitely for free via the replay ↻ button.


Quick start (zero config)

Replay mode is the default and needs no keys, no funds, no endpoint — it simulates the whole pipeline and fires a scripted trigger a few seconds in, so a fresh clone runs safely out of the box.

npm install
npm run dev          # opens http://localhost:5173 and fires in ~5s

npm run dev runs the single backend process and the dashboard together; the browser opens automatically.

Run the real pipeline

cp .env.example .env
# fill in QUICKNODE_GRPC_ENDPOINT, QUICKNODE_GRPC_TOKEN, and SOLANA_KEYPAIR
npm run dev:live     # real order at ~spot → fills within seconds (the credibility shot)
npm run dev:armed    # real order below spot → genuinely waits for the market

How it works (the real pipeline)

   Jupiter Trigger API                Quicknode Solana gRPC
  ┌────────────────────┐            ┌──────────────────────────┐
  │ createOrder         │  order     │ subscribe to ONE account │   account
  │  → sign → execute   │  account   │  (the canary pubkey)     │   closes
  └─────────┬───────────┘  pubkey    └───────────┬──────────────┘   = filled
            │  ───────────────────────────────▶  │                     │
            ▼                                     ▼                     ▼
     resting order on-chain              live stream, one account    🐤 FIRE → onTrigger hook
  1. Place the canary. Use Jupiter's Trigger API to place a tiny resting canary order (~$5) buying BTC with USDC at your target. createOrder builds an unsigned tx and returns the on-chain order account address; we sign it and land it via /execute. That single pubkey is the canary. Canary orders fill at zero slippage by nature.
  2. Watch one account. Open a Quicknode Solana gRPC stream (port 10000) and subscribe to only that one order account, plus slot updates as a heartbeat.
  3. Trigger. When the market trades through your level, Jupiter's keepers fill the order and close its account. The gRPC stream surfaces that change the instant the chain knows it → the canary fires.
  4. Act. On fire we call your strategy hook with the fill (price, amounts, time, order account, tx). See The strategy hook below — it ships safe log / webhook / swap actions and is the one place to wire your own.

The three files worth reading are the teaching artifacts, commented so you learn the pattern by reading them:


Architecture

A single Node/TypeScript process does all the real work (Jupiter calls + the Solana gRPC subscription) and pushes events over a local WebSocket to a Vite + vanilla-TS dashboard. The browser is a pure live view — no business logic, no localStorage. The only Quicknode dependency is the gRPC stream itself, which keeps Quicknode Solana gRPC the clear hero.

src/
  shared/events.ts     # the WS event protocol, imported by both sides
  server/
    index.ts           # entry: mode dispatch, banner, graceful shutdown
    config.ts          # env + mode parsing, keypair loading
    hub.ts             # state + console logging + WebSocket broadcast
    jupiter.ts         # ★ Jupiter Trigger API (teaching artifact)
    yellowstone.ts     # ★ Quicknode Solana gRPC (teaching artifact)
    swap.ts            # sell-side prerequisite: market-buy a little BTC
    pipeline.ts        # real live/armed orchestration (both directions)
    replay.ts          # the scripted simulation
    onTrigger.ts       # ★ the strategy hook (log / webhook / swap)
    cancel.ts          # one-shot emergency cancel (npm run cancel)
  web/                 # the dashboard (Vite + vanilla TS + CSS tokens)
    main.ts · styles.css · sound.ts (the chirp) · firehose.ts · format.ts
  web/public/          # static assets served at "/": chirp.mp3, favicon.svg

The strategy hook

When the canary fires, the fill is handed to one functionsrc/server/onTrigger.ts — with everything about it (price, amounts, time, order account, tx). That's the single place to wire your own logic. It ships three safe-by-default actions, picked with TRIGGER_ACTION:

TRIGGER_ACTION What it does Needs
log (default) Prints the fill. No side effects, no funds move.
webhook POSTs the fill JSON to TRIGGER_WEBHOOK_URL (sends text + content + raw event, so Slack / Discord / Telegram all work). TRIGGER_WEBHOOK_URL
swap A real small Jupiter spot swap (buys TRIGGER_SWAP_USD more BTC). I_UNDERSTAND_REAL_MONEY=true + an RPC

⚠️ swap moves real money. It's gated behind I_UNDERSTAND_REAL_MONEY=true and capped at $25 (default $2). Leave it off unless you mean it.

The dashboard's trigger banner shows an action chip for whatever actually ran (logged / webhook sent ✓ / swapped $X with a tx link). In replay the hook runs in describe-only mode (would webhook, would swap $X) and never touches the network or your wallet. There is deliberately no leveraged/perp action — shipping working leverage execution in a public clone-and-run demo is too easy to misfire; onTrigger.ts leaves a clearly-marked stub showing where you'd add your own.


Demo modes

Mode What it does Needs keys/funds?
replay (default) Clearly-labelled simulation. Fires a scripted fill a few seconds in. No real transaction, no spend. Re-run the trigger infinitely with the replay ↻ control — ideal for rehearsing and re-shooting the animation. No
armed Places a real order at TARGET_PRICE_USD and genuinely waits for the market. The direction is chosen automatically: a target below spot is a buy-the-dip canary; a target above spot is a breakout (sell) canary. Cancels the resting order on exit. Yes
live Places a real order at ~current spot so it fills within seconds on camera — the genuine end-to-end credibility shot. Yes

The dashboard shows a prominent REPLAY badge whenever it's simulating, so nothing is ever misleading.


Prerequisites

  • Node.js v22+.
  • A Quicknode Solana Mainnet endpoint with the Solana gRPC add-on enabled. gRPC runs on port 10000; pass the host as https://NAME.solana-mainnet.quiknode.pro:10000 and the token segment separately (see .env.example). (armed/live only)
  • A funded mainnet keypair: a little USDC (≥ the order size — Jupiter enforces a ~$5 minimum) and some SOL for fees. (armed/live only)
  • For a breakout (sell) canary only: an HTTP RPC to check the BTC balance and market-buy the small amount it sells. It's derived automatically from your Quicknode gRPC endpoint, or set SOLANA_RPC_URL.

Replay mode needs none of the above.

Configuration

Copy .env.example.env. Secrets are never sent to the browser, and .env / keypair files are git-ignored.

Variable Purpose
MODE replay | armed | live
QUICKNODE_GRPC_ENDPOINT Quicknode gRPC host on port 10000
QUICKNODE_GRPC_TOKEN the token segment from your endpoint URL
SOLANA_KEYPAIR / PRIVATE_KEY base58 secret key or path to a Solana CLI keypair .json
BTC_MINT pre-filled with cbBTC (Coinbase Wrapped BTC): cbbtcf3aa214zXHbiAZQwf4122FBYbraNdFqgw4iMij
TARGET_PRICE_USD armed: the level to wait for. Below spot → buy-the-dip; above spot → breakout (sell). live: auto-uses ~spot
ORDER_SIZE_USD order notional (default 5; ~$5 Jupiter minimum)
SOLANA_RPC_URL (optional) only for a sell-side canary; derived from the Quicknode gRPC endpoint if unset
JUP_API_KEY (optional) for higher rate limits; otherwise the keyless host is used
TRIGGER_ACTION what the strategy hook does on fire: log (default) | webhook | swap
TRIGGER_WEBHOOK_URL required for webhook; the fill JSON is POSTed here (Slack/Discord/Telegram-friendly)
TRIGGER_SWAP_USD swap notional in USD (default 2, capped at 25)
I_UNDERSTAND_REAL_MONEY must be true to enable the real-money swap action

Optional: route placement through Quicknode Metis

By default the canary is placed through Jupiter's free public API (lite-api.jup.ag) — so a fresh clone just runs, with no paid add-on. That keyless host is plenty for one small order.

For production, you can route placement through Quicknode Metis — Quicknode's hosted Jupiter API — for reliable, dedicated rate limits. It also keeps the whole pipeline on Quicknode: placement through Metis and the Solana gRPC stream that watches the order account. Metis is a paid add-on.

To switch, point the order calls in src/server/jupiter.ts at your Metis endpoint — https://jupiter-swap-api.quiknode.pro/<key> — instead of the lite-api host. See Quicknode's Metis limit-order docs for the request/response shapes.


Which BTC? (cbBTC)

There are several wrapped BTCs on Solana; this demo uses cbBTC (Coinbase Wrapped BTC), mint cbbtcf3aa214zXHbiAZQwf4122FBYbraNdFqgw4iMij. It's among the most liquid, most-integrated BTC representations on Solana, redeemable 1:1 for BTC by Coinbase, and Jupiter routes it deeply — so a tiny resting order fills cleanly. It has 8 decimals (verified on-chain, matching native BTC's satoshis), which is what the order-amount math is built around.

To watch a different BTC — e.g. Wormhole wBTC (3NZ9JMVBmGAqocybic2c7LQCJScmgsAZ6vQqTDzcqmJh) — set BTC_MINT, but confirm its decimal count first: the implied-price math assumes the configured mint's real decimals.

It's a tiny demo order, but it's still real money. Do your own diligence before sizing up. This isn't financial advice.


Notes & caveats

  • The strategy hook is real, and yours. On fire we call src/server/onTrigger.ts with the configured action (log / webhook / swap). swap moves real money and is gated + capped; there is no perp/leverage action (only a commented stub). See The strategy hook above.
  • Why Trigger V1? Jupiter's newer Trigger V2 keeps orders off-chain in a custodial vault — there's no on-chain order account to subscribe to, which would defeat the whole "watch one account" premise. This demo uses Trigger V1, the version that still exposes an on-chain order pubkey (the canary). V1 is in maintenance mode but functional; if it's ever retired, the watch-one-account pattern itself still applies to any on-chain order account.
  • Both directions, chosen automatically. A target below spot is a buy-the-dip canary (spend USDC, fires on a dip). A target above spot is a breakout canary — it sells BTC, so we first market-buy the small amount it needs, then place the resting sell order ("tell me the moment BTC breaks resistance"). On shutdown the resting order is cancelled; any leftover BTC dust stays in the wallet (no auto-unwind). live is always a marketable buy ~spot. A target more than ~15% from spot is flagged as a likely misconfiguration (it may never fill) but still placed.
  • The chirp. On trigger the dashboard plays web/public/chirp.mp3. Browsers block audio until you've interacted with the page, so the first gesture unlocks it — in replay the replay ↻ click is that gesture, so re-shoots always chirp. There's a mute toggle in the header (default on); the very first auto-fire on a cold page load may be silent.
  • Graceful shutdown + emergency cancel. In armed/live, Ctrl-C cancels any still-resting order so you don't leave dust behind. If the process dies before it can (or you closed the terminal), recover with ORDER_ACCOUNT=<pubkey> npm run cancel.
  • Don't commit secrets. .env and keypair files are git-ignored; nothing secret is exposed to the dashboard.

Built to show off Jupiter's Trigger API + Quicknode Solana gRPC. The canary is the convenience; the gRPC stream is what makes it instant.