v0.1 — Beta

Register a webhook

typescript
const webhook = await client.webhooks.create({
  url: "https://your-service.example.com/mppcash/events",
  events: [
    "transfer.settled",
    "transfer.rejected",
    "session.settled",
    "session.cap_warning",
    "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("/mppcash/events", express.raw({ type: "application/json" }));

app.post("/mppcash/events", (req, res) => {
  const signature = req.headers["x-mppcash-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.MPPPAL_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 "session.settled":
      await onSessionSettled(event.data);
      break;
    case "session.cap_warning":
      await onSessionCapWarning(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: `MPPCash policy violation: ${data.account_id}`,
      body: data.rejection_reason,
    });
  }
}

session.settled — record batch settlement

typescript
async function onSessionSettled(data: any) {
  console.log(
    `Session ${data.session_id} settled: $${data.settled_usdc} USDC. Tx: ${data.tx_signature}`
  );

  // Update your billing records — this is the authoritative settlement amount
  await billingDb.recordSettlement({
    sessionId: data.session_id,
    amount: data.settled_usdc,
    txSignature: data.tx_signature,
    settledAt: data.settled_at,
  });
}

session.cap_warning — proactive balance management

typescript
async function onSessionCapWarning(data: any) {
  // Fires when session running total exceeds 80% of cap
  // Use this to proactively close and re-open a session before cap_reached
  console.warn(
    `Session ${data.session_id}: $${data.running_total_usdc} of $${data.cap_usdc} cap used`
  );
}

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.mppcash.xyz/v1/webhooks \
  -H "Authorization: Bearer $MPPPAL_API_KEY" \
  -d '{"url": "https://abc123.ngrok.io/mppcash/events", "events": ["transfer.settled", "session.settled"]}'