Skip to content

SaaS Usage Metering & Verifiable Receipts

Track daily per-customer API usage with the Balance API and generate receipts your customers can independently verify.

This example shows the complete receipt-and-verification workflow: flush daily usage totals, lock the period, generate a receipt, embed the proof root in your invoice, and give your customer the returned verifyUrl so they can verify independently.

Prerequisites

  • A Balance API key
  • Node.js 18+ or Python 3.8+
  • The PacSpace SDK (npm install @pacspace-io/sdk) or requests library for Python

The Workflow

  1. Flush daily usage - each 24-hour metering window becomes one verified delta
  2. Lock the period - checkpoint at month-end to freeze the billing window
  3. Generate a proof - pull the receipt with the proof root and all deltas
  4. Build the invoice - embed the proof root and a verification link
  5. Customer verifies - your verification endpoint lets them independently confirm every charge

Step 1: Flush Daily API Usage

At the end of each metering day, aggregate the customer's usage in your system and emit one delta for that 24-hour window. The Balance API handles fingerprinting and permanent verification automatically.

typescript
import { PacSpace } from '@pacspace-io/sdk';

const pac = new PacSpace({ apiKey: process.env.PACSPACE_API_KEY });

async function flushDailyUsage(
  customerId: string,
  usageDate: string,
  totalApiCalls: number,
) {
  const result = await pac.balance.emit(
    customerId,
    -totalApiCalls,
    `daily_usage_flush:${usageDate}`,
    { referenceId: `usage:${customerId}:${usageDate}` },
  );

  console.log(`Flushed ${totalApiCalls} calls for ${customerId} on ${usageDate}`);
  return result;
}

Step 2: Check Usage Mid-Period

At any time, derive the current balance to see how much daily-flushed usage has accumulated:

typescript
const balance = await pac.balance.derive('cust_acme_001');

console.log(`Total usage: ${Math.abs(balance.computedBalance)} API calls`);
console.log(`Daily flushes verified: ${balance.deltasCount}`);

Step 3: Lock the Period

At month-end, checkpoint to freeze the period. This produces a proof root - a single fingerprint covering every verified daily delta.

typescript
const checkpoint = await pac.balance.checkpoint('cust_acme_001', {
  period: '2026-02',
});

// Store this - it goes on the invoice
const proofRoot = checkpoint.proofRoot;
console.log(`Period locked. Proof root: ${proofRoot}`);

Step 4: Generate the Receipt

Pull the complete receipt for the period - all daily deltas, the proof root, and verification data.

typescript
const checkpoint = await pac.balance.checkpoint('cust_acme_001', { period: '2026-02' });
const receipt = await pac.balance.receipt('cust_acme_001', { period: '2026-02' });

console.log(`Proof root: ${receipt.proofRoot ?? checkpoint.proofRoot}`);
console.log(`Delta count: ${receipt.deltaCount}`);
console.log(`Final balance: ${receipt.finalBalance}`);

Step 5: Build the Invoice

Embed the proof root and a verification link in your invoice:

typescript
async function generateInvoice(customerId: string, period: string, ratePerUnit: number) {
  const checkpoint = await pac.balance.checkpoint(customerId, { period });
  const receipt = await pac.balance.receipt(customerId, { period });
  const proofRoot = receipt.proofRoot ?? checkpoint.proofRoot;
  const verifyUrl = receipt.verifyUrl;
  const totalUsageUnits = Math.abs(receipt.finalBalance);

  const invoice = {
    invoiceId: `inv-${customerId}-${period}`,
    customerId,
    period,
    totalUsageUnits,
    recordedDays: receipt.deltaCount,
    ratePerUnit,
    subtotal: totalUsageUnits * ratePerUnit,
    proofRoot,
    verifyUrl,
  };

  // Store in your database, send to customer, etc.
  return invoice;
}

const invoice = await generateInvoice('cust_acme_001', '2026-02', 0.002);

Expected Output

============================================================
  INVOICE: inv-cust_acme_001-2026-02
============================================================
  Customer:           cust_acme_001
  Period:             2026-02
  Usage Units:        1,247
  Recorded Days:      28
  Rate:               $0.0020 / call
  Subtotal:           $2.49
  Proof root:         0x7f3a9b2c...
  Verify at:          https://customer-links.pacspace.io/c/cus_abc123
============================================================

Step 6: Customer Verification

Give your customer a way to independently verify the invoice. Share the returned verifyUrl from the receipt response; treat it as an opaque URL because it may be a Shared Record permalink or a proof-root verification URL.

Option A (recommended): Store and show the returned receipt URL:

invoice.verifyUrl = receipt.verifyUrl

No API key is required for the outer verification page. See Receipts & Verification.

Option B: If you need custom logic (e.g., combine with your invoice data), expose a custom verify endpoint in your own server:

typescript
import express from 'express';
import { PacSpace } from '@pacspace-io/sdk';

const app = express();
const pac = new PacSpace({ apiKey: process.env.PACSPACE_API_KEY });

// Optional: redirect to the stored opaque verification URL or add custom logic
app.get('/verify/:invoiceId', async (req, res) => {
  const invoice = await db.invoices.find(req.params.invoiceId);
  res.redirect(invoice.verifyUrl);
});

// Or derive for custom verification
app.get('/verify/custom/:customerId/:period', async (req, res) => {
  const { customerId, period } = req.params;
  const lastInvoice = await db.invoices.findPrevious(customerId, period);

  const result = await pac.balance.derive(customerId, {
    startingBalance: lastInvoice?.endingBalance ?? 0,
    startingCheckpoint: lastInvoice?.proofRoot,
    startingCheckpointType: 'proofRoot',
  });

  const currentInvoice = await db.invoices.findByPeriod(customerId, period);

  res.json({
    period,
    invoicedBalance: currentInvoice.endingBalance,
    computedBalance: result.computedBalance,
    verified: currentInvoice.endingBalance === result.computedBalance,
    deltasVerified: result.deltasCount,
    proofRoot: result.latestReceiptId,
  });
});

// Dispute endpoint - when a customer disagrees
app.post('/dispute/:customerId/:period', async (req, res) => {
  const { customerId, period } = req.params;
  const { claimedBalance } = req.body;

  const invoice = await db.invoices.findByPeriod(customerId, period);

  const comparison = await pac.balance.compare(customerId, {
    yours: invoice.endingBalance,
    theirs: claimedBalance,
  });

  res.json({
    invoicedBalance: invoice.endingBalance,
    claimedBalance,
    verifiedBalance: comparison.computedBalance,
    matchesInvoice: comparison.matchesYours,
    matchesClaim: comparison.matchesTheirs,
    resolution: comparison.discrepancy?.resolution,
  });
});

Full Working Example

Here's the complete workflow from start to finish:

typescript
import { PacSpace } from '@pacspace-io/sdk';

const pac = new PacSpace({ apiKey: process.env.PACSPACE_API_KEY });

async function runBillingCycle() {
  const customerId = 'cust_acme_001';
  const period = '2026-02';

  // 1. Flush usage once per metering day
  console.log('Flushing daily usage...\n');
  await pac.balance.emit(customerId, -420, 'daily_usage_flush:2026-02-01', {
    referenceId: 'usage:cust_acme_001:2026-02-01',
  });
  await pac.balance.emit(customerId, -390, 'daily_usage_flush:2026-02-02', {
    referenceId: 'usage:cust_acme_001:2026-02-02',
  });
  await pac.balance.emit(customerId, -437, 'daily_usage_flush:2026-02-03', {
    referenceId: 'usage:cust_acme_001:2026-02-03',
  });

  // 2. Check mid-period usage
  const balance = await pac.balance.derive(customerId);
  console.log(`Mid-period usage: ${Math.abs(balance.computedBalance)} units\n`);

  // 3. Lock the period
  const checkpoint = await pac.balance.checkpoint(customerId, { period });
  console.log(`Period locked. Proof root: ${checkpoint.proofRoot}\n`);

  // 4. Generate receipt
  const receipt = await pac.balance.receipt(customerId, { period });
  const proofRoot = receipt.proofRoot ?? checkpoint.proofRoot;
  const verifyUrl = receipt.verifyUrl;

  console.log('============================================================');
  console.log(`  INVOICE: inv-${customerId}-${period}`);
  console.log('============================================================');
  console.log(`  Usage Units:    ${Math.abs(receipt.finalBalance)}`);
  console.log(`  Recorded Days:  ${receipt.deltaCount}`);
  console.log(`  Rate:           $0.0020 / call`);
  console.log(`  Subtotal:       $${(Math.abs(receipt.finalBalance) * 0.002).toFixed(2)}`);
  console.log(`  Proof root:     ${proofRoot}`);
  console.log(`  Verify at:      ${verifyUrl}`);
  console.log('============================================================');
}

runBillingCycle().catch(console.error);

Next Steps