Guide
Setting Up Webhooks
Receive real-time events from MPPCash and wire them into your alerting, logging, and application logic.
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"]}'