Push matched events to your webhook URL. Subscriptions bind to a subgraph table — every row your handler writes that matches the filter triggers a signed HTTP POST. For pull semantics see Streams.


Quickstart

# Scaffold a receiver project; runtimes: inngest | trigger | cloudflare | node
sl create subscription my-watcher --runtime node

# Or programmatically:
import { createClient } from "@secondlayer/sdk";
await createClient(...).subscriptions.create({
  name: "my-watcher",
  subgraphName: "my-watcher",
  tableName: "transfers",
  url: "https://my-app.com/webhook",
  format: "standard-webhooks",
});

Filters

// {column: value} maps. Bare value = eq. Operators: eq, neq, gt, gte, lt, lte, in.
{
  filter: {
    recipient: "SP1ABC...",
    amount: { gte: "1000000" }
  }
}

on.* factories

import { on } from "@secondlayer/stacks";

// Each factory takes {subgraph, table} first — bind to a table you own.
const spec = on.transferTo(
  { subgraph: "my-watcher", table: "transfers" },
  "SP1ABC...",
  { asset: "SP1...usdc::usdc-token" },
);

await sdk.subscriptions.create({ ...spec, name: "watch", url: "https://..." });

// Available: on.transferTo, on.sip010Transfer, on.sip009Transfer,
//            on.bnsName, on.poxStack, on.sbtcDeposit, on.sbtcWithdrawal

Signing

# Default format: standard-webhooks. Every delivery carries:
webhook-id:        msg_<id>
webhook-timestamp: <unix-seconds>
webhook-signature: v1,<base64-hmac>

# signature = HMAC-SHA256("<id>.<timestamp>.<body>", secret)
# Rotate via: sl subscriptions rotate-secret <id>
#
# Other formats: inngest, trigger, cloudflare, cloudevents, raw

Replay & DLQ

# Failed deliveries fall back through 30s → 2m → 10m → 1h → 6h → 24h → 72h.
# After 7 attempts they land in the DLQ; 20 consecutive failures pause the sub.
# Inspect at /platform/subgraphs/<name>/subscriptions/<id>.

await sdk.subscriptions.replay(id, { fromBlock: 123000, toBlock: 124000 });