Route a watch's alerts to a Slack channel or to your own HTTP endpoint, signed so your handler can trust them. Two destinations, one --to on the watch's line.
The examples use example-vault; swap in your own contract.
Connect the Sentinel Slack app to your workspace once, then name a channel.
sentinel connect slack
$ sentinel destinations add slack '#oncall' --default
$ sentinel when call example-vault set-paused --to slack:#oncallconnect slack prints a link that adds the app (it works once, for 10 minutes). After you allow the app, a page names your Sentinel account and asks you to confirm: nothing is connected until you press Connect. --default sends every watch that names no --to to this channel.
A private channel needs /invite @Sentinel, then its channel id (#C0123ABCD) instead of its name. An alert posted by the app keeps Mute 1 h and Mark read as buttons. Anyone in the channel can press them. A pasted Slack incoming-webhook URL works too: sentinel destinations add slack https://hooks.slack.com/services/… --label "#oncall".
sentinel destinations add webhook https://example.com/hook --label ops
$ sentinel when call example-vault set-paused --to webhook:opsThe signing secret is printed once, when you add the destination. Store it as SENTINEL_WEBHOOK_SECRET. Each destination has its own.
Over MCP, destinations_add takes the same arguments: destinations_add { kind: "webhook", target: "https://example.com/hook", label: "ops" }.
Every webhook body is one envelope. type says what data is, and id is also the webhook-id header.
{
"id": "evt_alr_…",
"type": "alert.created",
"version": 1,
"createdAt": "2026-10-09T14:02:11.000Z",
"test": false,
"data": { "id": "alr_…", "alertLevel": "urgent", "headline": "…", "facts": [], "signals": [] }
}data is the alert: its headline, typed facts, the signals that fired it, and the inspect commands. The full field list is in the reference.
Each request carries webhook-id, webhook-timestamp and webhook-signature. verifyWebhook from @secondlayer/sentinel checks them, refuses a timestamp more than 5 minutes off, and returns the event. Pass the raw body, before any JSON parsing.
import { verifyWebhook } from "@secondlayer/sentinel";
export async function POST(req: Request) {
const event = await verifyWebhook(await req.text(), req.headers, process.env.SENTINEL_WEBHOOK_SECRET!);
if (await seen(event.id)) return new Response(null, { status: 204 }); // a retry
if (event.type === "alert.created" && event.data.alertLevel === "urgent") await page(event.data);
return new Response(null, { status: 204 });
}Without the package, the signature is the Standard Webhooks scheme: v1, then the base64 HMAC-SHA256 of webhook-id.webhook-timestamp.body. Details are in Verify the signature.
sentinel test-alert example-vault --yesThis sends the newest real match of the contract's watches from the last 90 days, marked as a test with its date. The webhook body carries test: true.
| After the block | An alert arrives after the block that holds the event, never from the mempool. The measured delay is public: sentinel status. |
| Retries | A failed delivery is retried with backoff, up to 5 attempts. A retry reuses the same webhook-id and is signed again with a fresh timestamp, so dedupe on webhook-id only. |
| Failed endpoint | If every attempt fails, you get an email and your other webhooks get delivery.failed. Fix the endpoint, then run sentinel redeliver <alert>. |
| Slack buttons | Anyone in the channel can press Mute and Mark read. |
| Cost | Alerts are never charged. Live mainnet alerts start at Builder, $19 per contract a month (Pricing). |