npm package: @pacspace-io/sdk
Description: Official PacSpace Balance API SDK — zero-dependency TypeScript client
Latest version: 0.2.1

# @pacspace-io/sdk

Official TypeScript SDK for the [PacSpace Balance API](https://docs.pacspace.io). Zero dependencies, fully typed, built for Node.js 18+.

## Install

```bash
npm install @pacspace-io/sdk
```

## Quick Start

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

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

// Record a delta
const delta = await pac.balance.emit('cust_123', -42.50, 'usage_charge');
console.log(delta.receiptId); // Store for later verification

// Derive balance
const { computedBalance } = await pac.balance.derive('cust_123');

// Compare against counterparty
const report = await pac.balance.compare('cust_123', {
  yours: 95000,
  theirs: 98000,
});

// Period-end checkpoint
const checkpoint = await pac.balance.checkpoint('cust_123', {
  period: '2026-02',
});
```

## API Reference

### `new PacSpace(config)`

| Option | Type | Default | Description |
|--------|------|---------|-------------|
| `apiKey` | `string` | **required** | Your Balance API key (`pk_live_*` or `pk_test_*`) |
| `baseUrl` | `string` | auto-routed | API base URL (overrides auto-routing if provided) |
| `sandboxUrl` | `string` | `https://api-sandbox-<hash>.run.app` | Custom sandbox URL (used for `pk_test_*` keys) |
| `productionUrl` | `string` | `https://api.pacspace.io` | Custom production URL (used for `pk_live_*` keys) |
| `chainId` | `number` | auto-detected | Default chain ID (296 for sandbox, 295 for production) |
| `maxRetries` | `number` | `2` | Max retries on transient errors |
| `timeout` | `number` | `30000` | Request timeout in ms |
| `webhookSecret` | `string` | — | Webhook signing secret (enables `pac.webhooks`) |

### Balance API

#### `pac.balance.emit(customerId, delta, reason, options?)`

Record a credit or debit delta. Returns immediately with `QUEUED` status.

```typescript
const delta = await pac.balance.emit('cust_123', -42.50, 'usage_charge', {
  referenceId: 'inv_001',
  metadata: { plan: 'growth' },
});
```

#### `pac.balance.emitAndWait(customerId, delta, reason, options?)`

Record a delta and poll until it reaches a terminal status (`VERIFIED` or `FAILED`).

```typescript
const verified = await pac.balance.emitAndWait('cust_123', -42.50, 'usage', {
  timeout: 30_000,
  pollInterval: 1000,
});
```

#### `pac.balance.derive(customerId, options?)`

Derive a customer's balance from all verified deltas.

```typescript
const result = await pac.balance.derive('cust_123');
console.log(result.computedBalance);

// With checkpointing for efficiency:
const next = await pac.balance.derive('cust_123', {
  startingBalance: result.computedBalance,
  startingCheckpoint: result.latestReceiptId!,
});
```

#### `pac.balance.compare(customerId, balances, options?)`

Compare balances against neutral truth for dispute resolution.

```typescript
const report = await pac.balance.compare('cust_123', {
  yours: 95000,
  theirs: 98000,
});

if (report.matchesYours) {
  console.log('Your balance is correct');
}
```

#### `pac.balance.receipt(customerId, options?)`

Generate a verifiable receipt. With `options.period` (YYYY-MM), returns a period-specific receipt with `proofRoot`, `verifyUrl`, and `verificationReference` — share the `verifyUrl` on invoices for counterparty verification.

```typescript
// Period receipt (shareable proof for invoices)
const receipt = await pac.balance.receipt('cust_123', { period: '2026-02' });
console.log(receipt.proofRoot);   // Proof root for verification
console.log(receipt.verifyUrl);   // balance-api.pacspace.io/verify/{proofRoot}
console.log(receipt.finalBalance);

// Full record receipt (all verified deltas)
const full = await pac.balance.receipt('cust_123');
console.log(full.finalBalance);
console.log(full.verification.itemHashes);
```

#### `pac.verify(proofRoot)`

Verify a proof root via the public verification endpoint. No API key required — the proof root is the access key.

```typescript
const result = await pac.verify(receipt.proofRoot);
if (result.verified) {
  console.log(result.verification?.verificationExplorerUrl);
  console.log(result.summary?.recordCount);
}
```

#### `pac.balance.checkpoint(customerId?, options?)`

Commit a period-end checkpoint. The proof root is recorded on the public verification layer for independent verification.

```typescript
const checkpoint = await pac.balance.checkpoint('cust_123', {
  period: '2026-02',
});
console.log(checkpoint.proofRoot); // Include in your invoice
```

### Webhooks

Verify webhook signatures and process events with type safety.

```typescript
const pac = new PacSpace({
  apiKey: process.env.PACSPACE_API_KEY!,
  webhookSecret: process.env.PACSPACE_WEBHOOK_SECRET!,
});

// Express middleware
app.use('/webhooks', express.json({
  verify: (req, _res, buf) => { (req as any).rawBody = buf.toString(); }
}));

app.post('/webhooks/pacspace', pac.webhooks!.middleware(), (req, res) => {
  const event = (req as any).pacspaceEvent;

  if (event.event === 'delta.verified') {
    console.log('Delta verified:', event.data.receiptId);
  }

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

// Manual verification
const event = pac.webhooks!.verify(signature, timestamp, rawBody);
```

### Error Handling

All errors extend `PacSpaceError` with structured data:

```typescript
import { PacSpace, InsufficientCreditsError, RateLimitError } from '@pacspace-io/sdk';

try {
  await pac.balance.emit('cust_123', -1000, 'charge');
} catch (err) {
  if (err instanceof InsufficientCreditsError) {
    console.log('Buy more credits');
  } else if (err instanceof RateLimitError) {
    console.log(`Retry after ${err.retryAfter}s`);
  }
}
```

| Error Class | Status | When |
|-------------|--------|------|
| `ValidationError` | 400 | Invalid request data |
| `InvalidApiKeyError` | 401 | Bad or missing API key |
| `InsufficientCreditsError` | 402 | Not enough credits |
| `NotFoundError` | 404 | Resource not found |
| `ContractNotDeployedError` | 412 | No contract provisioned |
| `RateLimitError` | 429 | Rate limit exceeded |
| `TimeoutError` | — | Polling timeout exceeded |

## Environment Detection & Auto-Routing

The SDK automatically routes requests to the correct API endpoint based on your API key prefix:

- `pk_test_*` → Sandbox API (`https://api-sandbox-<hash>.run.app`)
- `pk_live_*` → Production API (`https://api.pacspace.io`)

### Examples

**Sandbox (automatic):**
```typescript
const pac = new PacSpace({ apiKey: 'pk_test_...' });
// Automatically uses sandbox API URL
```

**Production (automatic):**
```typescript
const pac = new PacSpace({ apiKey: 'pk_live_...' });
// Automatically uses production API URL
```

**Custom URLs:**
```typescript
// Override sandbox URL
const pac = new PacSpace({
  apiKey: 'pk_test_...',
  sandboxUrl: 'https://custom-sandbox.example.com',
});

// Override production URL
const pac = new PacSpace({
  apiKey: 'pk_live_...',
  productionUrl: 'https://custom-production.example.com',
});

// Override with explicit baseUrl (disables auto-routing)
const pac = new PacSpace({
  apiKey: 'pk_test_...',
  baseUrl: 'https://custom-api.example.com',
});
```

## Requirements

- Node.js 18+ (uses native `fetch`)
- TypeScript 5+ (for full type inference)