Pellet Docs

Webhooks

HMAC-signed event delivery for peg breaks, flow anomalies, and system health.

Pellet pushes events to your URL when something material happens — a peg breaks, a stable's flow spikes, or our own system drifts. Each delivery is HMAC-signed so you can verify it came from us.

Event types

EventWhen
peg_break.startedA stable just crossed a peg threshold (mild >10bps for 5min, severe >50bps for 1min)
peg_break.endedA previously-ongoing peg break has resolved
flow_anomaly.detectedCross-stable flow exceeded 7-day baseline by ≥3σ in a 15-min window
system.health_driftPellet's own ingestion fell behind chain head or peg sampling stalled

Subscribing

Currently admin-only via API (self-serve UI coming with Pellet Pro). Endpoint:

curl -X POST https://pelletfi.com/api/admin/webhooks \
  -H "Authorization: Bearer $YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "label": "Slack incident channel",
    "url": "https://hooks.slack.com/services/T0.../B0.../...",
    "event_types": ["peg_break.started", "peg_break.ended"],
    "stable_filter": ["0x20c000000000000000000000b9537d11c60e8b50"]
  }'

Returns:

{
  "id": "wh_a1b2c3d4e5f6...",
  "secret": "whsec_a1b2c3...",
  "url": "https://hooks.slack.com/...",
  "event_types": ["peg_break.started", "peg_break.ended"]
}

Save the secret — it's only returned once. You'll need it to verify signatures.

stable_filter is optional. Omit to subscribe to events across all stables.

Delivery

Pellet POSTs JSON to your URL with these headers:

Content-Type: application/json
X-Pellet-Signature: t=1712345678,v1=ab12cd34...
X-Pellet-Event: peg_break.started
X-Pellet-Delivery: 12345
User-Agent: Pellet-Webhook/1 (+https://pelletfi.com)

Body:

{
  "event": "peg_break.started",
  "data": {
    "stable": "0x20c000000000000000000000b9537d11c60e8b50",
    "severity": "mild",
    "started_at": "2026-04-14T11:20:00Z",
    "max_deviation_bps": 14.3,
    "started_block": 14710001
  },
  "delivery_id": 12345
}

Verifying signatures

The signature is HMAC-SHA256 of {timestamp}.{body} with your subscription secret.

import { createHmac, timingSafeEqual } from "node:crypto";

function verify(req: Request, body: string, secret: string): boolean {
  const header = req.headers.get("x-pellet-signature");
  if (!header) return false;
  const parts = Object.fromEntries(
    header.split(",").map((p) => p.split("=") as [string, string])
  );
  const ts = parts.t;
  const sig = parts.v1;

  // Reject replays older than 5 minutes
  if (Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;

  const expected = createHmac("sha256", secret)
    .update(`${ts}.${body}`)
    .digest("hex");
  return timingSafeEqual(Buffer.from(sig), Buffer.from(expected));
}

Retries

A delivery is considered successful on any 2xx response. On any other response or timeout (10s), Pellet retries with exponential backoff:

AttemptDelay
230s
32m
410m
51h
66h
724h

After the 6th retry (7 total attempts), the delivery is marked failed and dropped.

Best practices

  • Verify the signature on every request before processing.
  • Respond fast — return 2xx quickly and process async if needed. Long handlers risk timing out.
  • Handle replaysdelivery_id is unique per delivery; track it if your handler isn't idempotent.
  • Use HTTPS — webhooks to plain HTTP endpoints are accepted but discouraged.