femtos

Webhooks

Femtos webhooks: bridges, a wallet’s activity, liquidations, large positions and launches pushed to your HTTPS URL, signed (Standard Webhooks), retried, with a delivery log and replay.

Same channels and filters as the WebSocket. Create one with POST /v1/webhooks (or the MCP tool create_webhook): {url, description, subscriptions: [{channel, filter, batch}]}. The answer carries the whsec_ secret once; your URL first gets a signed webhook.verification POST to answer {"challenge": "<data.challenge>"}, nothing else is sent before.

Channels

ChannelEventsFilter
bridge.transfersHyperCore ↔ HyperEVM transfers, both legs matched (initiated, completed, stuck)token, direction (evm_to_core / core_to_evm), account, min_amount
elysium.bridgeHyperEVM ↔ Elysium bridge messages (sent, executed, failed)token, direction (to_child / to_parent)
walletEverything touching up to 100 addresses: fills, orders, ledger, liquidations, positions, bridges, Elysium swapsaddress (required: one, a comma list or an array), channel (event kinds)
hc.liquidationsHyperCore liquidationscoin, user, min_notional
hc.positionsLarge HyperCore position changes (one per block, user and coin)coin, user, min_notional (>= 10000, default 100000)
elysium.launchesNew launches on Elysium launchpads (Ascend)launchpad, include_spam
hyperevm.launchesNew LiquidLaunch tokens on HyperEVMinclude_spam

Verify the signature

Standard Webhooks: webhook-id (stable across retries: dedupe on it), webhook-timestamp, webhook-signature = v1,<base64 HMAC-SHA256(key, "{id}.{timestamp}.{body}")>, the key being the base64 part of your secret. Two signatures for 24 h after a rotation: accept either.

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

/** true when one of the signatures is ours and the timestamp is within 5 minutes */
export function verifyFemtos(secret: string, headers: Headers, body: string): boolean {
  const id = headers.get('webhook-id');
  const ts = headers.get('webhook-timestamp');
  const sigs = headers.get('webhook-signature');
  if (!id || !ts || !sigs || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
  const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
  const want = createHmac('sha256', key).update(`${id}.${ts}.${body}`).digest();
  return sigs.split(' ').some((s) => {
    const [version, b64] = s.split(',');
    const got = Buffer.from(b64 ?? '', 'base64');
    return version === 'v1' && got.length === want.length && timingSafeEqual(got, want);
  });
}
import base64, hashlib, hmac, time

def verify_femtos(secret: str, headers, body: bytes) -> bool:
    """True when one of the signatures is ours and the timestamp is within 5 minutes."""
    msg_id = headers.get("webhook-id")
    ts = headers.get("webhook-timestamp")
    sigs = headers.get("webhook-signature")
    if not (msg_id and ts and sigs) or abs(time.time() - int(ts)) > 300:
        return False
    key = base64.b64decode(secret.removeprefix("whsec_"))
    want = hmac.new(key, f"{msg_id}.{ts}.".encode() + body, hashlib.sha256).digest()
    return any(
        version == "v1" and hmac.compare_digest(base64.b64decode(b64), want)
        for version, _, b64 in (s.partition(",") for s in sigs.split(" "))
    )
import http from 'node:http';

http
  .createServer((req, res) => {
    let body = '';
    req.on('data', (c) => (body += c));
    req.on('end', () => {
      if (!verifyFemtos(process.env.FEMTOS_WEBHOOK_SECRET!, new Headers(req.headers as Record<string, string>), body)) return res.writeHead(401).end();
      const msg = JSON.parse(body);
      // the challenge of a new (or re-enabled) webhook: echo it
      if (msg.type === 'webhook.verification') return res.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ challenge: msg.data.challenge }));
      // answer at once; dedupe on webhook-id (a retry carries the same), then handle msg.data (the events) later
      res.writeHead(204).end();
      queue.push({ id: req.headers['webhook-id'], msg });
    });
  })
  .listen(8080); // behind your TLS proxy: we only POST to https on 443 or 8443

Delivery

Delivered = a 2xx within 10 s. Anything else (a 3xx too: redirects are not followed) is retried after 5 s, 5 min, 30 min, 2 h, 5 h, 10 h; then the delivery is dead, kept 7 days and replayable. 410 Gone, or 50 failures in a row over 24 h, disables the webhook: its GET says so and every answer of your key carries X-Femtos-Notice. A gap delivery names a window to re-read from the REST API after a restart of our stream.

Limits

TierWebhooksSubscriptionsDeliveries a day
free2510,000
builder1050500,000
pro505005,000,000
scale2005000unlimited

Everything else (management routes, replay, payloads): this page as Markdown · agents.md · OpenAPI (tag Webhooks).