Skip to content

SaaS Usage Metering & Verifiable Receipts

Track 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: record usage as it happens, lock the period, generate a receipt, embed the proof root in your invoice, and give your customer a way to verify independently via GET /api/v1/verify/:proofRoot.


Prerequisites

  • A PacSpace 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. Record usage — each API call your customer makes becomes a 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: Record API Usage

Every time a customer makes an API call, record it as a delta. The Balance API handles fingerprinting and permanent verification automatically.

TypeScript (SDK)

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

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

// Call this from your API middleware
async function recordApiCall(
  customerId: string,
  endpoint: string,
  responseTimeMs: number,
) {
  const result = await pac.balance.emit({
    customerId,
    delta: -1,  // Each API call = 1 unit of usage
    reason: `api_call:${endpoint}`,
    referenceId: `${customerId}-${Date.now()}`,
  });

  console.log(`Recorded: ${customerId} → ${endpoint} (${result.anchorId})`);
  return result;
}

// Express middleware example
app.use(async (req, res, next) => {
  const start = Date.now();
  res.on('finish', () => {
    if (req.customerId) {
      recordApiCall(req.customerId, req.path, Date.now() - start);
    }
  });
  next();
});

Python

python
import requests
from datetime import datetime, timezone

API_BASE = "https://balance-api.pacspace.io"
HEADERS = {
    "X-Api-Key": "YOUR_API_KEY",
    "Content-Type": "application/json",
}


def record_api_call(customer_id: str, endpoint: str, response_time_ms: int):
    """Record a single API call as a verified usage delta."""
    payload = {
        "customerId": customer_id,
        "delta": -1,
        "reason": f"api_call:{endpoint}",
        "referenceId": f"{customer_id}-{datetime.now(timezone.utc).timestamp():.0f}",
    }

    response = requests.post(
        f"{API_BASE}/api/v1/balance/delta", headers=HEADERS, json=payload
    )
    response.raise_for_status()
    result = response.json()["data"]

    print(f"Recorded: {customer_id} → {endpoint} ({result['anchorId']})")
    return result


# Flask middleware example
@app.after_request
def track_usage(response):
    if hasattr(request, "customer_id"):
        record_api_call(
            customer_id=request.customer_id,
            endpoint=request.path,
            response_time_ms=int(request.elapsed_ms),
        )
    return response

Step 2: Check Usage Mid-Period

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

TypeScript (SDK)

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

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

Python

python
def get_usage(customer_id: str):
    """Get the current usage total for a customer."""
    response = requests.get(
        f"{API_BASE}/api/v1/balance/derive/{customer_id}", headers=HEADERS
    )
    response.raise_for_status()
    data = response.json()["data"]

    return {
        "customer_id": customer_id,
        "total_calls": abs(data["computedBalance"]),
        "verified_deltas": data["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 delta.

TypeScript (SDK)

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

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

Python

python
def lock_period(customer_id: str, period: str):
    """Checkpoint the period."""
    response = requests.post(
        f"{API_BASE}/api/v1/balance/checkpoint",
        headers=HEADERS,
        json={"customerId": customer_id, "period": period},
    )
    response.raise_for_status()
    data = response.json()["data"]

    print(f"Period locked. Proof root: {data['merkleRoot']}")
    return data

Step 4: Generate the Receipt

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

TypeScript (SDK)

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.receiptId ?? receipt.verification?.itemsRoot ?? checkpoint.merkleRoot}`);
console.log(`Delta count: ${receipt.deltasCount}`);
console.log(`Final balance: ${receipt.finalBalance}`);

Python

python
def generate_receipt(customer_id: str, period: str):
    """Generate the receipt for a period."""
    response = requests.get(
        f"{API_BASE}/api/v1/balance/receipt/{customer_id}",
        headers=HEADERS,
        params={"period": period},
    )
    response.raise_for_status()
    return response.json()["data"]

Step 5: Build the Invoice

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

TypeScript (SDK)

typescript
async function generateInvoice(customerId: string, period: string, ratePerCall: number) {
  const checkpoint = await pac.balance.checkpoint(customerId, { period });
  const receipt = await pac.balance.receipt(customerId, { period });
  const proofRoot = receipt.receiptId ?? receipt.verification?.itemsRoot ?? checkpoint.merkleRoot;

  const invoice = {
    invoiceId: `inv-${customerId}-${period}`,
    customerId,
    period,
    totalApiCalls: receipt.deltasCount,
    ratePerCall,
    subtotal: receipt.deltasCount * ratePerCall,
    proofRoot,
    verifyUrl: `https://balance-api.pacspace.io/api/v1/verify/${proofRoot}`,
  };

  // 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
  API Calls:          1,247
  Rate:               $0.0020 / call
  Subtotal:           $2.49
  Proof root:         0x7f3a9b2c...
  Verify at:          https://balance-api.pacspace.io/api/v1/verify/0x7f3a9b2c...
============================================================

Step 6: Customer Verification

Give your customer a way to independently verify the invoice. Share the proof root or a direct link to the public verify endpoint — no API key or account needed.

Option A (recommended): Link directly to PacSpace's public verify endpoint:

https://balance-api.pacspace.io/api/v1/verify/:proofRoot

The proof root is the access key. No authentication required. See Verify — Proof Root & Compare.

Option B: If you need custom logic (e.g., combine with your invoice data), build your own endpoint:

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 public verify or add custom logic
app.get('/verify/:proofRoot', (req, res) => {
  res.redirect(`https://balance-api.pacspace.io/api/v1/verify/${req.params.proofRoot}`);
});

// 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: 'itemsRoot',
  });

  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,
    yourBalance: invoice.endingBalance,
    theirBalance: 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. Record usage throughout the month
  console.log('Recording API usage...\n');
  await pac.balance.emit({ customerId, delta: -1, reason: 'api_call:/search' });
  await pac.balance.emit({ customerId, delta: -1, reason: 'api_call:/search' });
  await pac.balance.emit({ customerId, delta: -1, reason: 'api_call:/analytics' });
  await pac.balance.emit({ customerId, delta: -1, reason: 'api_call:/export' });
  await pac.balance.emit({ customerId, delta: -1, reason: 'api_call:/search' });

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

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

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

  console.log('============================================================');
  console.log(`  INVOICE: inv-${customerId}-${period}`);
  console.log('============================================================');
  console.log(`  API Calls:      ${receipt.deltasCount}`);
  console.log(`  Rate:           $0.0020 / call`);
  console.log(`  Subtotal:       $${(receipt.deltasCount * 0.002).toFixed(2)}`);
  console.log(`  Proof root:     ${proofRoot}`);
  console.log(`  Verify at:      https://balance-api.pacspace.io/api/v1/verify/${proofRoot}`);
  console.log('============================================================');
}

runBillingCycle().catch(console.error);

Next Steps