Skip to content

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.

javascript
// 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.

javascript
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.

javascript
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.

javascript
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.

javascript
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.

javascript
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:

javascript
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

Was this page helpful?

Last updated February 11, 2026