What is relayfor.si?

Webhooks

Signed events for launches, payouts, low credit and keys at their limit, as they happen.

Add an endpoint (a public https:// URL) on the dashboard's Webhooks page or with POST /webhooks, for some or all events. The answer carries its signing secret (whsec_...): store it, as only a retry with the same Idempotency-Key shows it again. A project has up to 5 endpoints.

Events

  • launch.confirmedA launch the chain confirmed: its token is on its route.launch
  • launch.failedA launch the chain refused, or whose route came out other than prepared.launch
  • token.importedAn import the chain confirmed: the existing token is on its route.launch
  • token.graduatedA token's curve completed: it trades on PumpSwap, its fees still on its route.tokengraduated_at
  • payout.creditedA payout of a token's creator fees became AI credit.payout
  • account.low_balanceAn account's spendable credit fell under the threshold: once, until a credit lifts it back.accountthreshold_usd
  • key.limit_reachedA router key's call was refused for its spending limit: once in each of the limit's periods.key
  • route.changedA token's creator fees no longer follow the route relayfor locked (a takeover on pump.fun): its payouts stop.tokenreasondetected_at

Each event's fields are in the reference. POST /webhooks/{id}/test sends a ping to one endpoint.

Every delivery is a POST with the same envelope. data holds what the event is about, in the API's own shapes:

{
  "id": "evt_4c9LGfliJda80U3V",
  "type": "payout.credited",
  "created_at": "2026-10-07T13:40:01.000Z",
  "project": "prj_joucOmKkV9Ikdf92",
  "data": {
    "payout": {
      "token": "MASi45ub7Qe4ZE36UT5G6cU4ud8Fhhe4deS4F3cw9KTA",
      "account": "acc_arqp1qhbpvjhzifE",
      "signature": "dYmM6J4tmCUz5J2h...",
      "event": "2.3",
      "slot": "372918466",
      "quote_mint": "So11111111111111111111111111111111111111112",
      "distributed_lamports": "61500000",
      "ai_bps": 5600,
      "ai_lamports": "34440000",
      "sol_usd": {
        "price": "221.04",
        "confidence": "0.064",
        "published_at": "2026-10-07T13:39:58.000Z"
      },
      "credited_usd": "7.610413",
      "created_at": "2026-10-07T13:40:01.000Z"
    }
  }
}

Its headers name the event too: relayfor-event-id, relayfor-event-type, and the signature.

Checking a signature

Each delivery carries relayfor-signature: t=<unix seconds>,v1=<hex>: the HMAC-SHA256 of <t>.<raw body> under the endpoint's secret. Recompute it over the raw body, compare in constant time, and refuse a t more than 5 minutes from your clock.

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

export function verify(body: string, header: string, secret: string): boolean {
  const parts = header.split(",");
  const t = Number(parts.find((part) => part.startsWith("t="))?.slice(2));
  if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${body}`).digest();
  return parts
    .filter((part) => part.startsWith("v1="))
    .some((part) => {
      const given = Buffer.from(part.slice(3), "hex");
      return given.length === expected.length && timingSafeEqual(given, expected);
    });
}

Read the body as raw text before parsing it: parsing and serializing again changes the bytes. After POST /webhooks/{id}/rotate, deliveries carry both secrets' signatures for a day.

Delivery

On this page