v0.1 — Phase 1

Register a webhook

typescript
const webhook = await client.webhooks.create({
  url: "https://your-service.example.com/clawdgo/events",
  events: [
    "transfer.settled",
    "transfer.rejected",
    "balance.low_threshold",
  ],
  low_balance_threshold_usdc: 50, // alert when balance drops below $50
});

console.log("Webhook secret:", webhook.secret);
// Store this — you'll use it to verify incoming payloads

Handle incoming events

A minimal Express handler that verifies signatures and processes events:

typescript
import express from "express";
import crypto from "node:crypto";

const app = express();
app.use("/clawdgo/events", express.raw({ type: "application/json" }));

app.post("/clawdgo/events", (req, res) => {
  const signature = req.headers["x-clawdgo-signature"] as string;
  const payload = req.body.toString();

  // 1. Verify signature first — reject anything that doesn't match
  const expected = crypto
    .createHmac("sha256", process.env.CLAWDGO_WEBHOOK_SECRET)
    .update(payload)
    .digest("hex");

  const isValid = crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expected)
  );

  if (!isValid) {
    return res.status(401).send("Invalid signature");
  }

  // 2. Respond 200 immediately — process async to avoid timeouts
  res.sendStatus(200);

  // 3. Process event asynchronously
  processEvent(JSON.parse(payload)).catch(console.error);
});

async function processEvent(event: any) {
  switch (event.type) {
    case "transfer.settled":
      await onTransferSettled(event.data);
      break;
    case "transfer.rejected":
      await onTransferRejected(event.data);
      break;
    case "balance.low_threshold":
      await alertLowBalance(event.data);
      break;
  }
}

Event handlers

transfer.rejected — alert on policy violations

typescript
async function onTransferRejected(data: any) {
  // Log to your incident system
  console.error(`Policy violation on account ${data.account_id}:`, data.rejection_reason);

  // If it's unexpected — alert the team
  if (!isExpectedRejection(data)) {
    await pagerduty.trigger({
      title: `ClawdGo policy violation: ${data.account_id}`,
      body: data.rejection_reason,
    });
  }
}

balance.low_threshold — trigger a top-up

typescript
async function alertLowBalance(data: any) {
  // Option 1: alert a human to top up manually
  await slack.post({
    channel: "#ops-alerts",
    text: `Agent account ${data.account_id} balance is $${data.balance_usdc} USDC. Please top up.`,
  });

  // Option 2: auto top-up via Circle API (if Circle integration is configured)
  if (AUTO_TOPUP_ENABLED) {
    await circle.transfer({
      destination: data.usdc_token_account,
      amount: TOP_UP_AMOUNT,
    });
  }
}

Testing webhooks locally

Use a tunneling tool like ngrok or cloudflared to expose a local server during development:

bash
ngrok http 3000
# → https://abc123.ngrok.io

# Register the tunnel URL as your webhook endpoint:
curl -X POST https://api.clawdgo.com/v1/webhooks \
  -H "Authorization: Bearer $CLAWDGO_API_KEY" \
  -d '{"url": "https://abc123.ngrok.io/clawdgo/events", "events": ["transfer.settled"]}'