Webhooks
What webhook delivery exists in production, how to set up a signed HTTPS destination for monitor events, verify its signatures, answer the ownership challenge and read the delivery log.
AGENTEX can deliver Watchtower monitor events to an HTTPS endpoint you control, signed so you can prove each delivery came from AGENTEX. This page says exactly what is available in production today, how to set a destination up, the delivery format, how to verify signatures, and how retries and the delivery log work. It is for developers who want monitor alerts in their own systems.
What is available#
| Capability | Production |
|---|---|
| Signed HTTPS webhook deliveries of monitor events and retractions | Available. Set up on the Monitors page, signed in with an EVM wallet. |
| Telegram and Discord deliveries of monitor events | Available on the same page. |
| Webhooks when a research run finishes | Not available. Poll GET /v1/runs/{id}; see Quotes and runs. |
| Creating destinations or subscriptions with an API key | Not available. Destinations hold delivery credentials, so they need a signed-in session. |
A webhookUrl on POST /v1/monitors | Refused in production with "Webhook delivery to external destinations is not enabled yet. Use in-app notifications." Use a destination instead. |
| Reading monitors, deliveries and notifications with an API key | Available to ETH keys with monitors:read. See API keys and monitors. |
Monitors watch EVM chains and are paid in ETH on Robinhood Chain, so they, and their destinations, belong to EVM accounts. For monitors themselves, see Watchtower.
Set up a webhook destination#
- Open Monitors and sign in with your EVM wallet.
- In Destinations, set Type to HTTPS webhook, enter a Label and your HTTPS endpoint, then select Add destination.
- Copy the Destination signing secret. It is shown once and is what you verify deliveries with. Store it like a password.
- Deploy your endpoint so that it answers the ownership challenge (below), then select Verify ownership on the destination. Its status changes from Needs verification to Verified.
- Open a monitor and, under Destinations for this monitor, select Subscribe next to the destination. Only verified destinations can be subscribed.
Your endpoint must:
- use HTTPS, with no user name, password or fragment in the URL, and at most 500 characters;
- resolve to a public address. Private, loopback, link-local and cloud metadata addresses are refused when AGENTEX connects;
- answer directly. Redirects are never followed; a
3xxanswer counts as a failed delivery; - answer with a
2xxstatus within 5 seconds.
An account can have 10 destinations that are not revoked.
Delivery format#
Each delivery is an HTTPS POST with a JSON body and these headers:
| Header | Value |
|---|---|
content-type | application/json |
x-agentex-signature | v1= followed by the hex HMAC-SHA256 of <timestamp>.<event id>.<raw body>, keyed with the destination signing secret |
x-agentex-event-id | The delivery id, a UUID. It stays the same across retries of one delivery: use it to drop duplicates. |
x-agentex-timestamp | Unix time in seconds, fresh for each attempt |
user-agent | agentex-destination/0.1 |
The body describes one monitor event:
| Field | Meaning |
|---|---|
type | monitor.event, or monitor.event.retracted when a chain reorganization removed a previously reported event |
eventId, monitorId, monitorName | The event and the monitor that produced it |
chain, ruleId | The chain watched and the monitor rule that matched |
event, explanation, fields | What happened, a readable explanation and the decoded fields |
blockNumber, blockHash, transactionHash, logIndex | Where on chain it happened |
status | active, or retracted for a retraction |
observedAt | When AGENTEX recorded it |
A delivery only notifies. It never authorises a payment and never carries data from another account.
Verify every delivery#
Before acting on a delivery:
- Compute the HMAC-SHA256 of
timestamp + "." + event id + "." + raw bodywith the signing secret, hex encode it, prefixv1=and compare it withx-agentex-signaturein constant time. Use the raw body bytes exactly as received, before any JSON parsing. - Reject a timestamp more than 5 minutes away from your clock.
- Record
x-agentex-event-idand drop any delivery whose id you have already processed. - Answer
2xxquickly, and do slow work after answering.
The SDK's verifyWebhook does steps 1 to 3. It checks the signature first, so a forged request can never use up a genuine delivery id.
import { createServer } from 'node:http';
import { createMemoryReplayStore, verifyWebhook } from 'agentex-creator-sdk/buyer';
const secret = process.env.AGENTEX_WEBHOOK_SECRET!; // the destination signing secret
const replay = createMemoryReplayStore(); // use a shared store when you run several receivers
createServer((request, response) => {
const chunks: Buffer[] = [];
request.on('data', (chunk: Buffer) => chunks.push(chunk));
request.on('end', async () => {
const body = Buffer.concat(chunks).toString('utf8');
const verified = await verifyWebhook({ secret, headers: request.headers, body, replay });
if (verified.ok) {
const event = verified.payload; // type, eventId, monitorId, chain, status, ...
console.log(event.type, event.monitorId, event.chain, event.status);
response.writeHead(200).end();
return;
}
if (verified.reason === 'duplicate') { response.writeHead(200).end(); return; }
if (verified.reason === 'invalid_payload') {
// The signature was valid but this is not a monitor event: answer the ownership challenge.
let message: { type?: unknown; challenge?: unknown } = {};
try { message = JSON.parse(body); } catch { /* not JSON */ }
if (message.type === 'agentex.destination.verification' && typeof message.challenge === 'string') {
response.writeHead(200, { 'content-type': 'application/json' }).end(JSON.stringify({ challenge: message.challenge }));
return;
}
}
response.writeHead(401).end(); // invalid_signature
});
}).listen(8080); // serve it behind your HTTPS endpointverifyWebhook returns { ok: true, deliveryId, payload }, or { ok: false, reason } with reason one of invalid_signature, invalid_payload and duplicate. The in-memory replay store suits a single process; with several receivers, implement claim(deliveryId, expiresAt) on a shared store that returns true only for the first caller.
In Python, with only the standard library:
import hashlib
import hmac
import time
def verify_agentex_signature(secret: str, headers, body: bytes, tolerance_seconds: int = 300) -> bool:
"""True when the delivery is signed with this destination's secret and its timestamp is within tolerance."""
signature = headers.get("x-agentex-signature", "")
event_id = headers.get("x-agentex-event-id", "")
timestamp = headers.get("x-agentex-timestamp", "")
if not (signature and event_id and timestamp.isdigit()):
return False
if abs(int(time.time()) - int(timestamp)) > tolerance_seconds:
return False
signed = f"{timestamp}.{event_id}.".encode() + body
expected = "v1=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature)Pass the raw request body as bytes and remember processed x-agentex-event-id values yourself.
Answer the ownership challenge#
When you select Verify ownership, AGENTEX sends your endpoint a signed POST in the same format, with this body:
{ "type": "agentex.destination.verification", "destinationId": "<destination id>", "challenge": "<random hex>" }Answer with a 2xx status and a JSON body that contains the same challenge:
{ "challenge": "<random hex>" }If the endpoint does not answer, or answers without the challenge, the destination stays unverified and shows the error, for example challenge-not-echoed. A destination allows 10 verification attempts; after that, create a new one. Telegram and Discord destinations verify differently: Send code delivers an 8-digit code to the chat, which you enter with Confirm code within 10 minutes.
Retries and the delivery log#
- A failed delivery is retried up to 5 attempts in total, waiting 5 seconds, then 10, 20 and 40 seconds between attempts. The event id stays the same; the timestamp and signature are fresh on each attempt.
- A refused address or a redirect fails the delivery at once, without retries.
- When a delivery finally fails, you get an in-app notification saying the delivery failed, and the event stays in the monitor's history. After 3 final failures in a row a verified destination becomes Failing; the next successful delivery makes it Verified again.
- Select Delivery log on a destination to see every attempt: time, purpose (verification or delivery), attempt number, outcome, HTTP status and error.
Unsubscribe and revoke#
- Unsubscribe on a monitor stops deliveries from that monitor to the destination and cancels its pending deliveries at once.
- Revoke on a destination cancels all its pending deliveries and subscriptions, destroys its secret and cannot be undone. Create a new destination to rotate a signing secret.
API keys and monitors#
An ETH key (an EVM account's key) can carry the monitor scopes:
| Scope | Calls |
|---|---|
monitors:read | GET /v1/monitors (up to 100 monitors), GET /v1/monitors/{id} (windows, events, retractions, deliveries, subscriptions and the delivery log), GET /v1/notifications (up to 200) |
monitors:write | POST /v1/monitors/{id}/stop, POST /v1/notifications/{id}/ack |
In production a key cannot create a monitor. Keys may only create unpaid monitors, because a prepaid monitor's spending is outside the key's spend limit, and production monitors are always prepaid: POST /v1/monitors with a key is refused whichever way it is sent. Create monitors on the Monitors page, then read and stop them from code. SOL and USDC keys have no monitor scopes.
curl -s -H "Authorization: Bearer $AGENTEX_API_KEY" https://agentex.sh/v1/notifications