Webhooks
Autonomous Agent Pattern
Design pattern for building autonomous agents that react to verification webhooks.
An autonomous agent is a webhook handler that goes beyond simple logging — it makes decisions based on the verification data PacSpace sends. This pattern turns your application into a self-monitoring system that can detect anomalies, enforce policies, and trigger workflows without human intervention.
What an Agent Can Do
Every delta.verified webhook contains rich data across six dimensions. Here's what an agent can do with each:
1. Identify — Match to Internal Records
Use delta.customerId and delta.referenceId to match the verified delta back to your internal records. This confirms PacSpace received and verified the exact transaction you submitted.
// Match the verified delta to your internal ledger
const internalRecord = await db.ledger.findUnique({
where: { referenceId: data.delta.referenceId }
});
if (!internalRecord) {
alert('Verified delta has no matching internal record');
}
2. Verify — Check Verification Status
Inspect verification.verified and verification.discrepancies to confirm the delta passed verification. If discrepancies exist, your agent can flag the record for review.
if (!data.verification.verified) {
await flagForReview(data.receiptId, data.verification.discrepancies);
}
3. Chain-Validate — Confirm Proof Continuity
Check that proof.previousProofHash matches the last proof hash you stored. A mismatch means the chain was broken — deltas may have been inserted, removed, or reordered.
const lastStoredHash = await getLastProofHash(data.delta.customerId);
if (lastStoredHash && data.proof.previousProofHash !== lastStoredHash) {
await alert('Proof chain discontinuity detected', {
expected: lastStoredHash,
received: data.proof.previousProofHash
});
}
// Store this delta's proof hash for the next check
await storeProofHash(data.delta.customerId, data.proof.proofHash);
4. Detect Drift — Compare Period Totals
Use period.netDelta to compare PacSpace's running total against your own. Drift between the two indicates a missing or extra delta somewhere.
const ourTotal = await computeNetDelta(data.period.id);
const pacspaceTotal = data.period.netDelta;
const drift = Math.abs(ourTotal - pacspaceTotal);
if (drift > DRIFT_THRESHOLD) {
await alert('Period drift detected', {
ours: ourTotal,
pacspace: pacspaceTotal,
drift
});
}
5. Cost-Aware Decisions — Monitor Usage
Use usage.remainingDeltas and usage.overageRate to make cost-aware decisions. Your agent can throttle low-priority deltas, alert when approaching limits, or automatically upgrade plans.
const { remainingDeltas, consumedDeltas, includedDeltas } = data.usage;
const utilizationPct = (consumedDeltas / includedDeltas) * 100;
if (utilizationPct > 90) {
await notify('Usage at ' + utilizationPct.toFixed(1) + '% — consider upgrading');
}
if (remainingDeltas < 100) {
await throttleLowPriorityDeltas();
}
6. Auto-Settle — Enforce SLA
Use verification.latencyMs to monitor verification performance. If latency exceeds your SLA threshold, the agent can flag it for investigation or trigger compensating actions.
const SLA_THRESHOLD_MS = 5000;
if (data.verification.latencyMs > SLA_THRESHOLD_MS) {
await logSlaViolation({
receiptId: data.receiptId,
latencyMs: data.verification.latencyMs,
threshold: SLA_THRESHOLD_MS
});
}
Full Agent Example
Here's a complete webhook handler that implements all six agent behaviors:
const crypto = require('crypto');
const express = require('express');
const app = express();
app.use('/webhooks', express.json({
verify: (req, res, buf) => { req.rawBody = buf.toString(); }
}));
app.post('/webhooks/pacspace', async (req, res) => {
// --- Authenticate ---
const secret = process.env.PACSPACE_WEBHOOK_SECRET;
const signature = req.headers['x-pacspace-signature'];
const timestamp = req.headers['x-pacspace-timestamp'];
const eventId = req.headers['x-event-id'];
const signedContent = `${timestamp}.${req.rawBody}`;
const expected = 'v1=' + crypto
.createHmac('sha256', secret)
.update(signedContent)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature))) {
return res.status(401).json({ error: 'Invalid signature' });
}
// --- Idempotency ---
if (await isEventProcessed(eventId)) {
return res.status(200).json({ received: true, duplicate: true });
}
// Respond immediately
res.status(200).json({ received: true });
// --- Process asynchronously ---
const { event, data } = req.body;
if (event === 'delta.verified') {
await handleDeltaVerified(data, eventId);
}
});
async function handleDeltaVerified(data, eventId) {
const actions = [];
// 1. Identify — match to internal records
const record = await db.ledger.findUnique({
where: { referenceId: data.delta.referenceId }
});
if (!record) {
actions.push({ type: 'alert', reason: 'unmatched_delta' });
}
// 2. Verify — check verification passed
if (!data.verification.verified) {
actions.push({
type: 'flag_for_review',
reason: 'verification_failed',
discrepancies: data.verification.discrepancies
});
}
// 3. Chain-validate — confirm proof continuity
const lastHash = await getLastProofHash(data.delta.customerId);
if (lastHash && data.proof.previousProofHash !== lastHash) {
actions.push({
type: 'alert',
reason: 'chain_discontinuity',
expected: lastHash,
received: data.proof.previousProofHash
});
}
await storeProofHash(data.delta.customerId, data.proof.proofHash);
// 4. Detect drift — compare period totals
const ourTotal = await computeNetDelta(data.period.id);
const drift = Math.abs(ourTotal - data.period.netDelta);
if (drift > 0.01) {
actions.push({
type: 'alert',
reason: 'period_drift',
ours: ourTotal,
pacspace: data.period.netDelta,
drift
});
}
// 5. Cost-aware decisions — monitor usage
const { consumedDeltas, includedDeltas, remainingDeltas } = data.usage;
const utilization = (consumedDeltas / includedDeltas) * 100;
if (utilization > 90) {
actions.push({ type: 'notify', reason: 'high_utilization', utilization });
}
if (remainingDeltas < 100) {
actions.push({ type: 'throttle', reason: 'approaching_limit' });
}
// 6. Auto-settle — enforce SLA
if (data.verification.latencyMs > 5000) {
actions.push({
type: 'sla_violation',
latencyMs: data.verification.latencyMs
});
}
// Execute all actions
for (const action of actions) {
await executeAction(action);
}
// Mark event as processed
await markEventProcessed(eventId);
// Store receipt for audit trail
await db.verificationReceipts.create({
data: {
receiptId: data.receiptId,
deltaIndex: data.deltaIndex,
proofHash: data.proof.proofHash,
verified: data.verification.verified,
actions: JSON.stringify(actions),
processedAt: new Date()
}
});
}
async function executeAction(action) {
switch (action.type) {
case 'alert':
await sendAlert(action);
break;
case 'flag_for_review':
await createReviewTicket(action);
break;
case 'notify':
await sendNotification(action);
break;
case 'throttle':
await enableThrottling();
break;
case 'sla_violation':
await logSlaViolation(action);
break;
}
}
app.listen(3000, () => console.log('Agent listening on port 3000'));
Design Principles
Respond first, process later
Always return 200 OK immediately after signature verification. Run your agent logic asynchronously. This prevents timeouts and duplicate deliveries.
Be idempotent
Use X-Event-ID to ensure you only process each event once. Network retries can deliver the same webhook multiple times.
Store proof hashes
Persist every proofHash you receive. You'll need the chain for validation, audits, and dispute resolution.
Alert, don't block
When your agent detects an anomaly, alert your team — don't silently block operations. Most discrepancies have benign explanations (clock skew, race conditions), so start with monitoring before adding automated interventions.
Degrade gracefully
If your agent's dependencies (database, notification service) are down, still return 200 OK and queue the event for retry in your own system. Don't let infrastructure failures cause webhook retries from PacSpace.
Next Steps
- Webhook Overview — Setup and configuration
- Signature Verification — Authenticate webhooks before processing
- Payload Reference — Full event payload schemas
- How Verification Works — Understand the verification pipeline
Last updated February 11, 2026