Watch an Agent Payment Land Live, and Trigger Your Own Server

Wallet balances now update live, and every wallet transaction sends a signed webhook. Worked example: an agent pays an invoice and your backend confirms it.

The wallet release is live, and two of the changes matter the moment your agent starts moving money. Balances on the dashboard now update themselves when a transaction lands on a wallet. And wallets can now fire webhooks, so your own backend learns about a payment the second it is recorded.

This post walks both features through one worked example: an agent pays a supplier, the owner watches the balance move without touching the browser, and their backend marks the invoice paid as soon as the payment confirms. Every wallet id, amount, address, hash and secret below is fake, so treat the numbers as placeholders.

If you already have an agent key, you can run the whole flow yourself. Any wallet on any supported network works, including Sepolia, so you can try it with test funds.

OpenClawCash LIVE WEBHOOK infographic: a wallet settles a payment and your server triggers, captioned "Agent payment settles. Your server fires."


What this release changed

The dashboard used to pick up balance changes on a slow refresh cycle, so a payment could take a while to show. Now the balance updates itself when a transaction on that wallet is recorded, no refresh needed. The number flashes green when the balance goes up and red when it goes down, like a price ticker. This covers the balance on the wallet cards on the Wallets page, the wallet detail page, the token balances there, and the ETH, POL and SOL totals on the main dashboard. Updates arrive within a few seconds. If you have hide balances on, they stay masked, and the flash respects your reduced-motion setting.

The second change is the reason to care: wallet.transaction.confirmed. It fires when a transaction is recorded on one of your wallets, including transactions your agents make through the API. Subscribe to it by name. The * wildcard still means checkout, escrow events only.


The example: an invoice payer

A small setup. An agent pays a supplier from an OpenClawCash wallet, the owner watches it happen on the dashboard, and their backend marks the invoice paid on confirmation. Same shape as every other payment you will wire up, so the parts are worth copying.

Three moving pieces: one webhook, one transfer call, one receiver endpoint.


Step 1: Create the webhook

One curl, run once. Store the secret it returns.

curl -X POST https://openclawcash.com/api/agent/checkout/webhooks \
  -H "X-Agent-Key: $OCC_AGENT_KEY" \
  -H "Idempotency-Key: 7f3c1a90-invoice-payer-01" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/occ-webhook",
    "eventTypes": ["wallet.transaction.confirmed"],
    "enabled": true
  }'

The response carries publicId, url, enabled, eventTypes and a one-time secret that starts with whsec_:

{
  "publicId": "wh_A1B2C3D4E5F6",
  "url": "https://example.com/occ-webhook",
  "enabled": true,
  "eventTypes": ["wallet.transaction.confirmed"],
  "secret": "whsec_9d2c7f41b8a05e63d1f4c7a92b0e6835d4a1c7f2"
}

The secret is shown once, so store it in your secret store, not in your repo. Target URLs must be public https URLs.

You can also manage endpoints in the dashboard on the Wallet webhooks page at https://openclawcash.com/webhooks (sidebar: Webhooks → Wallet), where the wallet event types are opt-in checkboxes.


Step 2: The agent sends the payment

Your agent calls the documented transfer endpoint. Nothing new here, this is the call you already make.

curl -X POST https://openclawcash.com/api/agent/transfer \
  -H "X-Agent-Key: $OCC_AGENT_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "chain": "evm",
    "walletId": "Q7X2K9P",
    "to": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
    "amountDisplay": "0.01"
  }'

Wallet ids are short codes like Q7X2K9P.


Step 3: What the owner sees

Open https://openclawcash.com/wallets. The wallet card for Q7X2K9P is showing an old balance. Within a few seconds of the transfer being recorded, that number changes by itself and flashes red, because the balance went down. No refresh, no reconnect.

The same applies on the wallet detail page and to its token balances, and to the ETH, POL and SOL totals on the main dashboard. A payment coming in flashes green instead. If hide balances is on, the balance stays masked and does not flash, so nothing about the direction leaks. If your OS is set to reduce motion, the flash does not play.


Step 4: What your backend receives

The delivery is a JSON POST to your endpoint. Example body:

{
  "eventId": "evt_9c1e4f7a2b3d5e6f8a0b",
  "eventType": "wallet.transaction.confirmed",
  "createdAt": "2026-09-25T09:15:22.481Z",
  "data": {
    "walletId": "Q7X2K9P",
    "walletAddress": "0x9a1f0c2b7d3e4a5f6b7c8d9e0a1b2c3d4e5f6a7b",
    "network": "sepolia",
    "transactionId": 4127,
    "type": "transfer",
    "status": "confirmed",
    "hash": "0x3f2a91c4e7b0d5a8f1c2e3b4a5968778695a4b3c2d1e0f9a8b7c6d5e4f3a2b1c",
    "to": "0x742d35Cc6634C0532925a3b844Bc454e4438f44e",
    "value": "9900000000000000",
    "fee": "100000000000000"
  }
}

value and fee are strings in base units (wei for ETH), and the numbers above are placeholders. Every delivery carries webhook-id, webhook-timestamp and webhook-signature headers. The signature is v1,<base64>, an HMAC-SHA256 over {webhook-id}.{webhook-timestamp}.{raw body}, keyed with base64decode(secret without the whsec_ prefix). Reject timestamps older than 5 minutes. This follows the Standard Webhooks scheme, so standard libraries can verify it. The older x-occ-signature header is still sent, so existing receivers keep working.

This function was checked against a known signature. Use it as is:

import crypto from "node:crypto";

function verifyWebhook(secret, headers, rawBody) {
  const id = headers["webhook-id"];
  const timestamp = Number(headers["webhook-timestamp"]);
  if (!id || !Number.isInteger(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
  const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
  const expected = crypto.createHmac("sha256", key).update(`${id}.${timestamp}.${rawBody}`).digest("base64");
  return String(headers["webhook-signature"] || "").split(" ").some((entry) => {
    const given = Buffer.from(entry.split(",")[1] || "");
    const want = Buffer.from(expected);
    return given.length === want.length && crypto.timingSafeEqual(given, want);
  });
}

The handler needs the raw body, so read it before any JSON parser runs. Answer 2xx within 10 seconds, then do your own work:

import express from "express";

const app = express();
const seen = new Set();

app.post("/occ-webhook", express.raw({ type: "application/json" }), (req, res) => {
  const raw = req.body.toString("utf8");
  if (!verifyWebhook(process.env.OCC_WEBHOOK_SECRET, req.headers, raw)) {
    return res.status(400).end();
  }
  const webhookId = req.headers["webhook-id"];
  res.status(200).end();

  if (seen.has(webhookId)) return;
  seen.add(webhookId);

  const event = JSON.parse(raw);
  if (event.eventType === "wallet.transaction.confirmed" && event.data.type === "transfer") {
    markInvoicePaid(event.data.hash);
  }
});

De-duplicate on webhook-id. Retries can deliver the same event more than once, and markInvoicePaid is your own function: match the payment to an invoice (for example by to and value, or by the hash your agent reported when it sent the payment), mark it paid, and store the hash in your ledger.


If your backend is down

Nothing is lost. Anything other than a 2xx is retried with growing delays: 30 seconds, 2 minutes, 8 minutes, 32 minutes, about 2 hours, about 8.5 hours, then 12 hours. That is up to 8 attempts in total, roughly a day of cover. A 410 Gone response disables the endpoint instead of retrying it.


Try it

Run the example above against Sepolia, which is a supported test network, and you can exercise the whole path with test funds: create the webhook, send a transfer, watch the Wallets page at https://openclawcash.com/wallets, and confirm the delivery arrives at your own server before you point it at anything real.

Docs: https://openclawcash.com/docs#post-api-agent-checkout-webhooks Changelog: https://openclawcash.com/changelog

Checkout (escrow) webhooks use the same signed format, so this receiver verifies those deliveries too.

Related Articles

Public Pages