Skip to content

Autonomous Recordation Agent

Build an agent that listens to webhooks, stores proofs, detects mismatches, and alerts an operator.

This example builds an agent using Express.js. It listens to PacSpace webhooks, stores proof fields, checks for mismatches against a local record store, and alerts an operator when something needs review.


Prerequisites

  • Node.js 18+
  • An Express.js server
  • A configured webhook endpoint pointing to your server
  • Your webhook secret (from webhook creation)

Full Example

javascript
const express = require('express');
const crypto = require('crypto');

const app = express();
app.use(express.json());

// --- Configuration ---
const WEBHOOK_SECRET = process.env.PACSPACE_WEBHOOK_SECRET;
const API_KEY = process.env.PACSPACE_API_KEY;
const API_BASE = 'https://app.pacspace.io';
const SLA_MAX_LATENCY_MS = 30_000; // 30 seconds
const DISCREPANCY_THRESHOLD = 0.01; // 1% tolerance

// --- Local record store (in production, use a database) ---
const localRecords = new Map();
const proofHistory = [];

// --- 1. Signature Verification ---
function verifySignature(rawBody, signature, timestamp) {
  const signedContent = `${timestamp}.${rawBody}`;
  const expected = 'v1=' + crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(signedContent)
    .digest('hex');
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature));
}

// --- 2. Proof Continuity Check ---
function validateProofContinuity(event) {
  const lastProof = proofHistory[proofHistory.length - 1];

  if (lastProof && event.data.proof?.previousProofHash) {
    // Check continuity when previous proof fields are present
    if (event.data.proof.previousProofHash !== lastProof.proofHash) {
      return { valid: false, reason: 'Previous proof fingerprint does not match' };
    }
  }

  proofHistory.push({
    proofHash: event.data.proof?.proofHash,
    timestamp: event.timestamp,
    hash: crypto.createHash('sha256').update(JSON.stringify(event.data)).digest('hex')
  });

  return { valid: true };
}

// --- 3. Discrepancy Detection ---
function checkDiscrepancy(key, reportedBits) {
  const localRecord = localRecords.get(key);

  if (!localRecord) {
    console.warn(`No local record for ${key} - first event or missing data`);
    return { discrepancy: false, note: 'no_local_record' };
  }

  const diff = Math.abs(localRecord.bits - reportedBits);
  const tolerance = localRecord.bits * DISCREPANCY_THRESHOLD;

  if (diff > tolerance) {
    return {
      discrepancy: true,
      localBits: localRecord.bits,
      reportedBits,
      difference: diff,
      percentOff: ((diff / localRecord.bits) * 100).toFixed(2) + '%'
    };
  }

  return { discrepancy: false };
}

// --- 4. SLA Monitoring ---
function checkSLA(event) {
  const submittedAt = new Date(event.data.submittedAt).getTime();
  const verifiedAt = new Date(event.data.verifiedAt).getTime();
  const latency = verifiedAt - submittedAt;

  return {
    withinSLA: latency <= SLA_MAX_LATENCY_MS,
    latencyMs: latency,
    maxAllowedMs: SLA_MAX_LATENCY_MS
  };
}

// --- 5. Usage Tracking ---
function trackUsage(event) {
  const { periodId, consumedDeltas, remainingDeltas } = event.data.usage || {};

  const warningThreshold = 1000;
  if (remainingDeltas !== undefined && remainingDeltas < warningThreshold) {
    escalate('low_usage', {
      periodId,
      remainingDeltas,
      threshold: warningThreshold,
      message: `Plan usage is high for ${periodId}: ${remainingDeltas} deltas remaining`
    });
  }

  return { periodId, consumedDeltas, remainingDeltas };
}

// --- 6. Local Record Update ---
function updateLocalRecords(event) {
  const items = event.data.items || [];

  for (const item of items) {
    const existing = localRecords.get(item.key) || { bits: 0, writes: 0 };
    localRecords.set(item.key, {
      bits: existing.bits + item.bits,
      writes: existing.writes + 1,
      lastUpdated: event.timestamp
    });
  }
}

// --- Escalation Handler ---
function escalate(type, details) {
  console.error(`[ESCALATION] ${type}:`, JSON.stringify(details, null, 2));

  // In production, send alerts via Slack, PagerDuty, email, etc.
  // Example:
  // await slack.send(`PacSpace alert: ${type}`, details);
  // await pagerduty.trigger(type, details);
}

// --- Webhook Handler ---
app.post('/webhooks/pacspace', (req, res) => {
  const signature = req.headers['x-pacspace-signature'];
  const timestamp = req.headers['x-pacspace-timestamp'];
  const event = req.body;
  const rawBody = req.rawBody || JSON.stringify(event);

  // Step 1: Verify signature
  if (!verifySignature(rawBody, signature, timestamp)) {
    console.error('Invalid webhook signature - rejecting');
    return res.status(401).json({ error: 'Invalid signature' });
  }

  console.log(`Received event: ${event.event} at ${event.timestamp}`);

  // Step 2: Check proof continuity
  const continuityResult = validateProofContinuity(event);
  if (!continuityResult.valid) {
    escalate('proof_continuity_break', { reason: continuityResult.reason, event });
    return res.status(200).json({ received: true, warning: continuityResult.reason });
  }

  // Step 3: Check for discrepancies
  if (event.data.items) {
    for (const item of event.data.items) {
      const discResult = checkDiscrepancy(item.key, item.bits);
      if (discResult.discrepancy) {
        escalate('discrepancy', { key: item.key, ...discResult });
      }
    }
  }

  // Step 4: Monitor SLA compliance
  if (event.data.submittedAt && event.data.verifiedAt) {
    const slaResult = checkSLA(event);
    if (!slaResult.withinSLA) {
      escalate('sla_breach', {
        latencyMs: slaResult.latencyMs,
        maxAllowedMs: slaResult.maxAllowedMs,
        receiptId: event.data.receiptId
      });
    }
  }

  // Step 5: Track plan usage
  if (event.data.usage) {
    trackUsage(event);
  }

  // Step 6: Update local records
  updateLocalRecords(event);

  res.status(200).json({ received: true });
});

// --- Health Check ---
app.get('/health', (req, res) => {
  res.json({
    status: 'ok',
    recordCount: localRecords.size,
    proofHistoryLength: proofHistory.length,
    lastProof: proofHistory[proofHistory.length - 1] || null
  });
});

// --- Start Server ---
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Recordation agent listening on port ${PORT}`);
});

How It Works

1. Signature Verification

Every incoming webhook is validated using HMAC-SHA256 with your webhook secret. Requests with invalid signatures are rejected immediately.

2. Proof Continuity

The agent tracks proof fingerprints and checks each new proof against the previous one when that field is present. If the value does not match, alert an operator.

3. Discrepancy Detection

Each verified item is compared against local records. If the reported value differs by more than the configured tolerance (1% by default), the agent escalates.

4. SLA Monitoring

The time between submission and verification is measured. If it exceeds the SLA threshold (30 seconds by default), the agent flags an SLA breach.

5. Usage Awareness

The agent tracks plan usage from each event. When the remaining deltas drop below a threshold, it triggers an alert so you can upgrade or manage overage.

6. Local Record Store

All verified events update local records, keeping a running total per record. In production, replace the in-memory Map with a database.


Running the Agent

bash
# Set environment variables
export PACSPACE_WEBHOOK_SECRET="whsec_your_secret_here"
export PACSPACE_API_KEY="pk_live_PUBLIC.SECRET"

# Install dependencies
npm install express

# Start the agent
node recordation-agent.js

Then configure a webhook in the PacSpace Dashboard pointing to https://your-server.com/webhooks/pacspace.


See Also

Next Steps

  • Replace the in-memory record store with a persistent database (PostgreSQL, SQLite, etc.)
  • Add Slack or PagerDuty integration for production escalations
  • See SaaS Usage Metering for a billing-focused integration
  • Review Billing Overview to understand plan usage