Signed, retryable, auditable

Webhooks for every FOUR-LIFE event.

Subscribe a URL, get HMAC-signed JSON on every tier transition and protection-level change. Delivery retries on failure (30s / 2m / 15m) and auto-disables after 10 consecutive failures. Same event shape powers Telegram + Discord alerts.

Events

badge.tier_changedFires when a token's FOUR-LIFE Certified tier transitions (e.g. healthy → at_risk).
example payload
{
  "id": "evt_xxxx...",
  "type": "badge.tier_changed",
  "created_at": 1776380000,
  "token_address": "0xabc...",
  "from_tier": "healthy",
  "to_tier": "at_risk",
  "at": 1776380000,
  "why": [
    {
      "rule": "whale_extreme",
      "metric": "top_holder_pct",
      "value": 55.2,
      "threshold": 40,
      "operator": ">=",
      "passed": true
    }
  ],
  "metrics": { "curve_progress_pct": 42, "top_holder_pct": 55.2, "..." : "..." },
  "data_source": "live_monitor"
}
protection.level_changedFires when a token under Protection Mode transitions between safe / warn / critical.
example payload
{
  "id": "evt_xxxx...",
  "type": "protection.level_changed",
  "created_at": 1776380000,
  "token_address": "0xabc...",
  "from_level": "safe",
  "to_level": "critical",
  "at": 1776380000,
  "fired_rules": [
    {
      "rule": "contract_rug_critical",
      "metric": "contract_risk_score",
      "value": 80,
      "threshold": 60,
      "severity": "critical"
    }
  ],
  "recommended_actions": ["halt_content_posts", "fire_webhook_alert"],
  "thresholds": { "critical_whale_concentration": 55.0, "..." : "..." }
}

Subscribe

curl
# Subscribe — returns the shared secret EXACTLY ONCE
curl -X POST https://four-life.gudman.xyz/api/webhooks \
  -H "Authorization: Bearer $API_SECRET" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://your-service.example/fourlife-webhook",
    "events": ["badge.tier_changed", "protection.level_changed"],
    "token_filter": null
  }'

# Response contains:
#   "id"     → subscription id (whs_...)
#   "secret" → shared HMAC secret (whsec_...)  ← STORE THIS NOW
The secret is returned once and never recoverable. Use it to verify the signature on every delivery. Requires an API_SECRET bearer token; set one in the deployment env to lock writes.

Signature header

X-FourLife-Signature: t=<unix_ts>,v1=<hex_hmac_sha256>
The HMAC is computed as HMAC_SHA256(secret, f"{t}.{raw_body}").
Reject deliveries where |now - t| > 300 seconds to prevent replay attacks.

Verify — Node.js

verify-webhook.js
// Node.js / any runtime with crypto
const crypto = require("crypto");

function verifyFourLifeSignature({ secret, body, header, toleranceSeconds = 300 }) {
  const parts = Object.fromEntries(
    header.split(",").map(s => s.split("=").map(x => x.trim())),
  );
  const t = parseInt(parts.t, 10);
  const v1 = parts.v1;
  if (!t || !v1) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - t) > toleranceSeconds) return false;

  const expected = crypto
    .createHmac("sha256", secret)
    .update(`${t}.${body}`)
    .digest("hex");

  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}

// Express handler
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
  const body = req.body.toString("utf-8");
  const sig = req.headers["x-fourlife-signature"] || "";
  if (!verifyFourLifeSignature({ secret: process.env.WHSEC, body, header: sig })) {
    return res.status(401).send("bad signature");
  }
  const event = JSON.parse(body);
  console.log("received:", event.type, event.token_address);
  res.status(200).send("ok");
});

Verify — Python

verify_webhook.py
# Python (Flask or any framework that gives raw body)
import hmac, hashlib, time

def verify_fourlife_signature(*, secret: str, body: str, header: str, tolerance: int = 300) -> bool:
    parts = dict(seg.split("=", 1) for seg in header.split(",") if "=" in seg)
    try:
        t = int(parts.get("t", ""))
    except ValueError:
        return False
    if abs(int(time.time()) - t) > tolerance:
        return False
    expected = hmac.new(
        secret.encode(), f"{t}.{body}".encode(), hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))


@app.post("/webhook")
def webhook():
    body = request.get_data(as_text=True)
    sig = request.headers.get("X-FourLife-Signature", "")
    if not verify_fourlife_signature(secret=WHSEC, body=body, header=sig):
        return ("bad signature", 401)
    event = request.get_json()
    print("received:", event["type"], event["token_address"])
    return ("ok", 200)

Delivery semantics

Retry schedule
First attempt immediate. Failures retry after 30s, then 2m, then 15m. 4 attempts total.
Dead-letter
After 4 failed attempts the delivery is marked dead. The event is NOT re-enqueued on subsequent tier changes.
Auto-disable
A subscription with 10 consecutive dead deliveries is auto-disabled. Re-subscribe to resume.
At-least-once
Your handler may receive the same event_id twice if your 2xx response is slow. Dedupe on `id`.
Ordering
Events are delivered in enqueue order per subscription but NOT guaranteed across subscriptions.
Size
Payloads are typically < 2 KB. We do not inline images or heavy context — fetch via /api/token/{addr}/badge if needed.