Dev.to Security πŸ” Cybersecurity πŸ‘ 0 πŸ“– 3 min read

Why your agent webhook hears about escrow but not wallet transfers

You follow the webhook docs, register one https URL, subscribe with * so you catch everything, and wait. A checkout gets funded and an event lands. Then your agent pays a vendor through the API and your server hears noth

You follow the webhook docs, register one https URL, subscribe with * so you catch everything, and wait. A checkout gets funded and an event lands. Then your agent pays a vendor through the API and your server hears nothing.

That is not a delivery problem. It is the subscription rule: * covers escrow events only. Wallet events are their own group, and you have to name them.

Two event families, one endpoint

OpenClawCash sends two kinds of webhook event, and they answer two different questions.

  • wallet.transaction.confirmed answers "did money move on my wallets". It is a wallet event: it fires when a transaction is recorded on a wallet you hold, including every payment your agents make through the API.
  • escrow.funded, escrow.released, escrow.refunded, escrow.failed, escrow.disputed and the other escrow.* types answer "what happened to this job". They follow one escrow through its lifecycle.

Both are registered at the same route, POST /api/agent/checkout/webhooks, with the same X-Agent-Key header. The split lives in the subscription, not in the endpoint.

The two rules that catch people

  • A * subscription is accepted on create and update, and it expands to escrow events only.
  • wallet.transaction.confirmed is never sent to a * subscription, so it has to be listed by name.

An endpoint subscribed to * is therefore an escrow-only endpoint. If your agent paid outside a checkout and no event arrived, that is the whole explanation.

Subscribe to both, on purpose

You do not need two URLs to watch both families. Name the wallet event next to the escrow ones:

curl -X POST https://openclawcash.com/api/agent/checkout/webhooks \
  -H "Content-Type: application/json" \
  -H "X-Agent-Key: occ_your_api_key" \
  -H "Idempotency-Key: webhook-create-001" \
  -d '{
    "url": "https://example.com/occ-webhook",
    "eventTypes": ["escrow.released", "wallet.transaction.confirmed"],
    "enabled": true
  }'

The response carries a publicId such as wh_a1b2c3d4e5f6 and a one-time secret starting with whsec_. Store that secret on your server: the delivery body is a signed JSON POST with { eventId, eventType, createdAt, data }, and the signature is v1,<base64 HMAC-SHA256 of "{id}.{timestamp}.{body}"> in the webhook-signature header, keyed on the base64-decoded part of the secret after whsec_. Read the raw body before parsing, reject a webhook-timestamp older than five minutes, de-duplicate on webhook-id, and answer 2xx within 10 seconds.

What a wallet event carries, and its two quirks

For a wallet event, data holds walletId, walletAddress, network, transactionId, type, status, direction, hash, from, to, value, fee and platformFee. value and the fees are strings in base units, so do the decimal math with the token's decimals.

Two things to know before you count money:

  1. A transfer between two wallets you hold writes a row for each of them, so one payment produces two events, one outgoing and one incoming, both with the same hash. Match on hash.
  2. A transfer that fails is refused before it is recorded, so there is no failed wallet event. These events report what landed.

Escrow events behave differently on purpose: they track a job, not a balance, so they carry the escrow lifecycle and are managed apart from the wallet webhooks in the dashboard. Two surfaces, one endpoint.

Where to check it

List your subscriptions with GET /api/agent/checkout/webhooks, press Test at https://openclawcash.com/webhooks, or make a small transfer on a test network such as Sepolia. The full reference, including the update and delete calls, is at https://openclawcash.com/docs.

πŸ“° Read the original article on Dev.to Security

Originally published by Dev.to Security. Aggregated on AIWithGhost for educational purposes β€” full credit and traffic to the original publisher.