kite~/kite/docs/CoinGecko Agent Signals
v0.2.2
Tutorial

CoinGecko Buy/Sell Signals That Wake an AI Agent

Send CoinGecko price crossings through Kite, score urgency with OpenRouter or Levanto, wake Hermes selectively, and review event history on a schedule.

This tutorial turns CoinGecko price data into a small event stream, lets a cheap decision model decide whether each event deserves immediate attention, and gives a stronger agent a scheduled view across the events Kite retained.

The example is deliberately safe: it does not place trades, hold exchange credentials, or treat a threshold crossing as a recommendation. The thresholds are synthetic inputs you choose for learning and testing, not financial advice.

What you will build

text
CoinGecko Demo REST              CoinGecko signed Webhook
poll every 60 seconds            paid beta; price event private beta
          │                                 │
 threshold-crossing bridge          Kite HMAC verifier
          │                                 │
          └──────────────┬──────────────────┘
                         ▼
                  retained Kite event
                         │
                         ▼
             exec sink → cheap decision model
                         │
              significant now? ── yes ──→ wake Hermes
                         │ no
                         ▼
                  scheduled retrospective

CoinGecko documents REST, WebSocket, and Webhooks as different delivery methods. REST is available on the free Demo plan; WebSocket and Webhooks require a paid plan, and cg.coin.price.updated is currently private beta.[1][2][3]

That distinction matters. The main path below is near-real-time REST polling, not a native CoinGecko webhook. The Simple Price endpoint's Demo data updates every 60 seconds, so the bridge polls once per minute instead of wasting quota on faster requests.[4]

For the design story and tradeoffs, read The agent does not need every tick.

Prerequisites

  • Node.js 20 or later
  • The Kite CLI installed and authenticated with kite login
  • A free CoinGecko Demo API key for the polling path, or an eligible paid Webhooks plan for native delivery
  • An agent command; the included example uses Hermes Agent
  • An OpenRouter API key and a model that supports structured outputs, or a Levanto Sage API key

CoinGecko recommends sending Demo credentials in the x-cg-demo-api-key header and keeping the key in backend code or a secret manager, not in a query string.[5]

1. Create a dedicated Kite source

Create an endpoint for the normalized signal stream:

bash
kite endpoints create --source coingecko-signals

Copy the returned webhook URL. Treat the full URL as a secret because it contains the hook token. The bridge reads it from KITE_WEBHOOK_URL; do not paste it into source control, screenshots, or shell scripts.

2. Download the tested bridge

The complete example lives in the public Kite repository:

bash
git clone https://github.com/Alpha-Centauri-Cyberspace/kite-server.git
cd kite-server/examples/coingecko-signal-agent
node --test bridge.test.mjs

The bridge has four jobs:

  • Fetch one CoinGecko price sample per minute.
  • Refuse stale or malformed samples.
  • Emit only when price moves into a configured buy or sell zone.
  • Send a structured CloudEvent to the secret Kite webhook URL.

The core detector establishes a baseline on startup, which prevents an immediate wake when the first sample is already outside a threshold. It also suppresses repeated samples in the same zone and applies a per-signal cooldown after a new crossing.

javascript
export function classifyPrice(price, buyBelow, sellAbove) {
  if (buyBelow >= sellAbove) throw new Error("BUY_BELOW must be lower than SELL_ABOVE");
  if (price <= buyBelow) return "buy";
  if (price >= sellAbove) return "sell";
  return "neutral";
}

export class SignalDetector {
  constructor({ buyBelow, sellAbove, cooldownMs }) {
    this.buyBelow = buyBelow;
    this.sellAbove = sellAbove;
    this.cooldownMs = cooldownMs;
    this.previousPrice = undefined;
    this.previousZone = undefined;
    this.lastSignalAt = new Map();
  }

  observe(price, observedAtMs = Date.now()) {
    const zone = classifyPrice(price, this.buyBelow, this.sellAbove);
    const previousPrice = this.previousPrice;
    const previousZone = this.previousZone;
    this.previousPrice = price;
    this.previousZone = zone;

    if (previousZone === undefined || previousPrice === undefined) return null;
    if (zone === "neutral" || zone === previousZone) return null;

    const lastSignalAt = this.lastSignalAt.get(zone);
    if (lastSignalAt !== undefined && observedAtMs - lastSignalAt < this.cooldownMs) {
      return null;
    }

    this.lastSignalAt.set(zone, observedAtMs);
    return { signal: zone, price, previousPrice };
  }
}

3. Test the signal logic without credentials

Before making a live API call, feed the bridge a synthetic sequence:

bash
DRY_RUN=1 \
PRICE_SEQUENCE='100,90,89,100,120' \
BUY_BELOW=95 \
SELL_ABOVE=115 \
COOLDOWN_SECONDS=0 \
node bridge.mjs

The baseline price 100 produces no event. Crossing down to 90 emits one buy event, remaining at 89 emits nothing, returning to 100 resets the zone, and crossing up to 120 emits one sell event.

Expect exactly two JSON lines on stdout with these types:

text
com.coingecko.signal.buy
com.coingecko.signal.sell

Diagnostic sample messages go to stderr, keeping stdout machine-readable.

4. Understand the CloudEvent contract

A buy crossing becomes this shape:

json
{
  "specversion": "1.0",
  "id": "generated-uuid",
  "source": "https://www.coingecko.com/en/coins/bitcoin",
  "type": "com.coingecko.signal.buy",
  "subject": "bitcoin/usd",
  "time": "2026-09-09T12:00:00.000Z",
  "datacontenttype": "application/json",
  "kitesummary": "BUY signal for bitcoin/usd at 90",
  "data": {
    "provider": "coingecko",
    "delivery_mode": "demo-rest-poll",
    "strategy": "price-threshold-crossing",
    "signal": "buy",
    "coin_id": "bitcoin",
    "quote_currency": "usd",
    "price": 90,
    "previous_price": 100,
    "buy_below": 95,
    "sell_above": 115,
    "observed_at": "2026-09-09T12:00:00.000Z"
  }
}

Stable event types let Kite discard everything except com.coingecko.signal.buy and com.coingecko.signal.sell. The delivery_mode field prevents downstream code from confusing minute-scale Demo polling with a native webhook or WebSocket stream.

5. Configure the significance sink

The included kite.json keeps the two bridge signals plus the normalized native price-alert type. Its exec sink runs the tested decision layer for each surviving event:

json
{
  "name": "coingecko-market-signal-agent",
  "subscriptions": [
    { "source": "coingecko-signals" },
    { "source": "coingecko" }
  ],
  "filters": {
    "keep_only": [
      { "source": "*coingecko*", "type": "com.coingecko.signal.buy" },
      { "source": "*coingecko*", "type": "com.coingecko.signal.sell" },
      { "source": "*coingecko*", "type": "com.coingecko.coin.price.updated" }
    ]
  },
  "sink": {
    "type": "exec",
    "command": "node ./significance-sink.mjs"
  },
  "queue": {
    "retention": "24h",
    "max_size": "1000"
  }
}

Kite passes the full CloudEvent on stdin. significance-sink.mjs bounds the input, validates the event type and identifiers, calls exactly one configured decision provider, validates the response, and applies application-owned thresholds. It hashes the CloudEvent ID together with the signed x-cg-event-id when present, serializes concurrent retries for that key, and writes a durable wake claim before starting Hermes.

Provider failures and malformed responses fail closed: Hermes is not started and the sink exits non-zero so Kite can retry. A valid low-significance decision exits zero without waking Hermes. The event remains in Kite's retained history for the scheduled review.

OpenRouter supports strict JSON Schema for compatible model/provider endpoints. The sink requires that capability and then validates the returned significant, score, reason, and recommended_action fields again in application code.[10]

Levanto Sage exposes a purpose-built Yes/No decision with probability and confidence. The sink normalizes those fields into the same audit record and still owns the final thresholds itself.[11]

Run all companion tests without credentials:

bash
node --test *.test.mjs

6. Select one cheap decision provider

For OpenRouter, choose a model endpoint that currently advertises structured-output support. The example does not pin a model name because endpoint capabilities and pricing change:

bash
export SIGNIFICANCE_PROVIDER=openrouter
export OPENROUTER_API_KEY
export OPENROUTER_MODEL="${OPENROUTER_MODEL:-openai/gpt-4.1-nano}"
export SIGNIFICANCE_SCORE_THRESHOLD=0.85
export HERMES_PROFILE=crypto-research

An OpenRouter score is the model's structured assessment, not calibrated confidence. Keep the application threshold conservative and inspect the audit trail before relying on it.

To test Levanto Sage instead:

bash
export SIGNIFICANCE_PROVIDER=levanto
export LEVANTO_API_KEY
export SIGNIFICANCE_SCORE_THRESHOLD=0.85
export SIGNIFICANCE_CONFIDENCE_THRESHOLD=0.70
export HERMES_PROFILE=crypto-research

The sink invokes Hermes with an argument array, not a shell command:

text
hermes -p crypto-research chat -q <bounded research prompt>

It never includes the provider's free-form reason in the wake prompt, and it never asks Hermes to trade. The sink assumes the Kite server already authenticated and normalized native CoinGecko deliveries; it does not verify provider HMACs itself. Set SIGNIFICANCE_STATE_FILE to an absolute durable path if the default under ~/.local/state/kite/ is unsuitable. Per-event locks leave unrelated provider calls concurrent, while a separate shared-state lock preserves every event's read-modify-write update; active lock leases are refreshed until their operation finishes. While a record remains in the intact state file, retries reuse its decision and durable wake claim. An interrupted claimed wake is logged as wake_outcome_uncertain; confirm whether Hermes started before manually clearing that record. The sink deliberately does not auto-reset a corrupt state file because losing its claims could duplicate a wake. The state file keeps the latest 1,000 decisions as an operational retry cache. Set SIGNIFICANCE_AUDIT_LOG to an absolute path if you need a separate append-only stream of newline-delimited decision records.

7. Start Kite and the bridge

In the example directory, start the Kite pipeline:

bash
kite run --manifest kite.json

In another terminal, load secrets from your shell or secret manager and choose your own non-recommended demonstration thresholds:

bash
export COINGECKO_API_KEY
export KITE_WEBHOOK_URL
export BUY_BELOW
export SELL_ABOVE
export COIN_ID=bitcoin
export VS_CURRENCY=usd
export POLL_SECONDS=60
export COOLDOWN_SECONDS=900
node bridge.mjs

POLL_SECONDS cannot be lower than 60 in this Demo example. Transient failures use capped jittered backoff, and the bridge sends events only to the official Kite API origin. COOLDOWN_SECONDS=900 blocks another signal of the same kind for 15 minutes even if price leaves and re-enters that zone.

You should see a baseline sample first. When a later sample crosses a threshold, the bridge posts the CloudEvent, Kite's filter passes it, and significance-sink.mjs records a decision. Hermes starts only when the validated decision clears the configured wake policy.

8. Operate it without wake storms

  • Use crossings, not states. "Price is below X" fires every poll; "price crossed below X" fires once when the zone changes.
  • Keep a cooldown. It limits churn when price oscillates around one boundary.
  • Persist state in production. The demo keeps the previous zone in memory. A restart establishes a fresh baseline; a durable service should store zone and last-signal timestamps.
  • Treat delivery as at least once. The example uses a per-event lock and a durable pre-wake claim to prefer a missed wake over a duplicate wake after an uncertain process failure. Never let a retried analysis trigger an irreversible action twice.
  • Keep the hook URL secret. Rotate the endpoint if it leaks. Never log the full value.
  • Bound agent behavior. Ask for research, context, or escalation. Keep exchange keys and order placement outside this tutorial.
  • Monitor both sides. CoinGecko request failures appear as [poll-error]; Kite queue and logs commands show delivery failures and retries.

9. Receive signed CoinGecko Webhooks directly

CoinGecko Webhooks are a separate paid-plan feature. Their POSTs include x-cg-timestamp, x-cg-event-id, and an HMAC-SHA256 x-cg-signature computed over {timestamp}:{event_id}:{raw_body}. Verification must use the unparsed request bytes.[2]

The accompanying Kite server change adds a coingecko source verifier. It loads the endpoint's signing secret, verifies the HMAC in constant time, requires a bounded provider event ID, enforces an application-owned five-minute timestamp tolerance before parsing the body, and transactionally ignores a replayed event ID. Kite retains x-cg-event-id and x-cg-timestamp under the delivered CloudEvent's kiteoriginalheaders extension but strips x-cg-signature.

Do not configure the provider until that server version is deployed. Once it is, inject the CoinGecko signing secret through a secret manager and create a provider-authenticated endpoint without putting the secret in command arguments:

bash
export COINGECKO_WEBHOOK_SECRET
printf '%s' "$COINGECKO_WEBHOOK_SECRET" | \
  kite endpoints create --source coingecko --signing-secret -

Use the bare provider webhook URL printed by the CLI, not the bearer fallback URL, as the CoinGecko destination. The fallback URL embeds a second bearer credential that CoinGecko does not need when its HMAC is enabled. Store the signing secret in Infisical or another secret manager and never paste it into the manifest.

Kite maps a valid body with event_type: "cg.coin.price.updated" to com.coingecko.coin.price.updated. The cg.coin.price.updated event is still Early Access/Private Beta, so capture a real dashboard test payload before adding direction-specific field mappings.[3] The native path deliberately does not pretend an undocumented field means buy or sell. The significance sink receives the authenticated price alert as provider data and reads x-cg-event-id from kiteoriginalheaders, not from a top-level CloudEvent attribute.

WebSocket is another paid option for continuous price data. Keep the signal detector, cooldown, CloudEvent schema, and Kite manifest; replace only the Demo polling input with a WebSocket subscription.[1]

10. Add the scheduled retrospective

A Hermes Bot is a persistent profile with its own sessions, memory, skills, cron jobs, and canonical Bot Chat.[13][14] Closing Hermes Desktop does not delete the profile, so you can reopen the crypto-research Bot Chat later. It does not mean the bot is continuously working: scheduled routines run only while that profile's gateway daemon is alive.[12]

Install the profile gateway as a launchd user service on macOS:

bash
hermes -p crypto-research gateway install
hermes -p crypto-research gateway status

Create a six-hour retrospective that reads Kite's persisted summaries, gets fresh market context through the CoinGecko skill, and delivers the result into the same Bot Chat:

bash
WORKDIR="$(pwd)"
hermes -p crypto-research cron create \
  "every 6h" \
  "$(cat retrospective-prompt.md)" \
  --name "CoinGecko signal retrospective" \
  --deliver bot-chat:crypto-research \
  --skill coingecko \
  --workdir "$WORKDIR" \
  --provider openai-codex \
  --model gpt-5.6-sol \
  --reasoning-effort medium \
  --continuity

Replace the provider/model pair with the strong model you want to pay for. Keep both fields pinned so a later interactive model change cannot silently change scheduled spend. The prompt runs kite logs --limit 200, compares the current window with its previous output through --continuity, and stops rather than inventing data when Kite or CoinGecko is unavailable.

The Desktop window is not the service boundary. The Hermes profile gateway and the kite run --manifest kite.json pipeline both need to remain running, and the host must remain awake. Use an always-on machine for unattended operation.

Troubleshooting

No events appear

Run the synthetic DRY_RUN=1 sequence first. If it emits two JSON lines, the detector works. Then check that all four required live values are non-empty: COINGECKO_API_KEY, KITE_WEBHOOK_URL, BUY_BELOW, and SELL_ABOVE.

CoinGecko returns 401 or an auth error

Confirm this tutorial is using a Demo key with https://api.coingecko.com/api/v3 and the x-cg-demo-api-key header. Pro keys use a different root URL and header.[5]

Every poll says "no crossing"

That is expected while price remains in one zone. Use the synthetic sequence to test both crossings without waiting for a market move.

Kite receives events but the agent does not run

Read the JSON decision record on stderr or in SIGNIFICANCE_AUDIT_LOG. A deferred status is expected below the threshold. For failures, verify the selected provider key, OPENROUTER_MODEL when applicable, HERMES_PROFILE, and that hermes is on PATH.

The agent runs more than once

Keep the bridge cooldown enabled and retain the significance state file. Native retries use signed x-cg-event-id; bridge events use CloudEvent id. Inspect the Kite queue for provider or agent failures that keep the sink non-zero.

The retrospective stops after Desktop closes

Run hermes -p crypto-research gateway status. The profile persists even when its gateway is stopped, but cron does not run without the gateway daemon. Also confirm the host is awake and the separate Kite pipeline is still running.

Sources

[1] https://docs.coingecko.com/docs/data-delivery-methods - CoinGecko data delivery methods [2] https://docs.coingecko.com/webhooks - CoinGecko Webhooks [3] https://docs.coingecko.com/webhooks/cg-coin-price-updated - CoinGecko price target webhook event [4] https://docs.coingecko.com/reference/simple-price - CoinGecko Simple Price endpoint [5] https://docs.coingecko.com/demo/reference/authentication - CoinGecko Demo API authentication [10] https://openrouter.ai/docs/guides/features/structured-outputs - OpenRouter structured outputs [11] https://platform.levanto.ai/api/intelligence/skill - Levanto Sage decision API contract [12] https://hermes-agent.nousresearch.com/docs/user-guide/features/cron - Hermes cron and gateway execution [13] https://hermes-agent.nousresearch.com/docs/user-guide/bot-mode - Hermes Bot Mode [14] https://hermes-agent.nousresearch.com/docs/user-guide/profiles - Hermes profiles