npm package: @paygentic/wrap
Description: assure payment for agentic workflows
Author: Leendert van Beelen
Latest version: 0.1.2

# Wrap SDK

Wrap Claude agent queries with automatic payment tracking and usage metering.

**Paygentic Platform:** [Production](https://platform.paygentic.io) | [Sandbox](https://platform.sandbox.paygentic.io)

## Quick Start

```typescript
import { createWrapClient } from "@paygentic/wrap";

const client = createWrapClient();
const plan = await client.plans.init();
const query = plan.wrap("customer_123");

// wrapped query supports all claude agent sdk options
const response = await query({
  model: "claude-sonnet-4-20250514",
  prompt: "Hello, world!",
});

for await (const message of response) {
  // Process messages as usual
  // Usage is automatically tracked on completion
}
```

## Installation

```bash
npm install @paygentic/wrap
```

## CLI Setup

The package includes an interactive setup command that creates the necessary Paygentic resources (product, billable metric, price, and plan) for your agent:

```bash
npx @paygentic/wrap setup
```

### What It Does

The setup wizard will:

1. **Prompt for configuration:**
   - Environment (`sandbox` or `prod`)
   - API Key (from [Paygentic Platform](https://platform.paygentic.io/merchant/developer/apikeys))
   - Merchant ID (from [Organization Settings](https://platform.paygentic.io/merchant/settings/organization))
   - Agent name (used for product naming)
   - Payment type (`instant` or `post-paid`)
   - Max expected usage per query (USD)
   - Margin percentage (markup on usage costs)

2. **Create Paygentic resources:**
   - Product for your agent
   - Billable metric for tracking usage
   - Price with your configured margin
   - Plan linking everything together

3. **Output environment variables** to add to your `.env` file

### Example Session

```
=== Paygentic Agent Billing Setup ===

Environment (sandbox/prod) [sandbox]: sandbox
Get your API key from: https://platform.sandbox.paygentic.io/merchant/developer/apikeys
API Key: pk_sandbox_xxxxx
Merchant ID: mer_xxxxx
Agent Name: My AI Assistant
Payment Type (instant/post-paid): instant
Max expected usage per query (USD) [2.00]: 2.00
Margin percentage [10]: 15

--- Creating Paygentic resources ---

Creating product...
Creating billable metric...
Creating price...
Creating plan...

=== Setup complete! ===

Add these to your .env file:

PAYGENTIC_WRAP_ENV=sandbox
PAYGENTIC_WRAP_API_KEY=pk_sandbox_xxxxx
PAYGENTIC_WRAP_MERCHANT_ID=mer_xxxxx
PAYGENTIC_WRAP_PRODUCT_ID=prod_xxxxx
PAYGENTIC_WRAP_PLAN_ID=plan_xxxxx
PAYGENTIC_WRAP_PRICE_ID=price_xxxxx
PAYGENTIC_WRAP_BILLABLE_METRIC_ID=bm_xxxxx
```

## Environment Variables

The SDK reads these environment variables as defaults:

| Variable | Description | Required |
|----------|-------------|----------|
| `PAYGENTIC_WRAP_API_KEY` | Your Paygentic API key | Yes (or pass `apiKey` option) |
| `PAYGENTIC_WRAP_ENV` | Environment: `prod` or `sandbox` | No (defaults to `prod`) |
| `PAYGENTIC_WRAP_MERCHANT_ID` | Your merchant ID | For setup script |
| `PAYGENTIC_WRAP_PLAN_ID` | Your plan ID | For your app |
| `PAYGENTIC_WRAP_PRICE_ID` | Your price ID | For your app |
| `PAYGENTIC_WRAP_PRODUCT_ID` | Your product ID | For reference |
| `PAYGENTIC_WRAP_BILLABLE_METRIC_ID` | Your billable metric ID | For reference |

Run `npx @paygentic/wrap setup` to create these resources and get the environment variables.

## How It Works

When you call `wrap`, the SDK:

1. Verifies the customer has an active subscription
2. Sets a safe budget limit (90% of maxPrice or maxPrice - $0.10)
3. Reserves funds for instant payments
4. Tracks usage and creates billing/payment events on completion
5. Handles overages by splitting into multiple events if needed

## Pricing Models

### Dynamic Pricing

Only pass through usage cost to end user:

```typescript
// Price config: maxPrice >= $0.20
const plan = await client.plans.init({
  planId: "plan_abc123",
  priceId: "price_dynamic",
});
```

### Percentage Pricing

Apply a markup to usage cost:

```typescript
// Price config: percentage, minCharge, maxCharge (maxCharge >= $0.20)
// Plan and price id default to PAYGENTIC_WRAP_PLAN_ID resp PAYGENTIC_WRAP_PRICE_ID
const plan = await client.plans.init({
  planId: "plan_abc123",
  priceId: "price_percentage",
});
```

## Subscriptions

### Create a Customer and Subscription

```typescript
const result = await client.subscriptions.create({
  customer: {
    name: "Example Inc",
    email: "billing@example.com",
    address: {
      line1: "123 Main St",
      city: "San Francisco",
      state: "CA",
      postalCode: "94105",
      country: "US",
    },
  },
  minWalletAmount: "20.00",
  redirectUrls: {
    onSuccess: "https://example.com/success",
    onFailure: "https://example.com/failure",
  },
});

const { customerId, subscriptionDetail } = result;
```

### Get Customer Portal URL

```typescript
const portal = await client.subscriptions.portal(subscriptionId);
// portal.url - redirect customers here to manage their subscription
// portal.expiresAt - URL expiration time
```

### Find Customer by Email

```typescript
const customerId = await client.subscriptions.findCustomer("billing@example.com");
```

## Payment Terms

- **Pre-paid** (default): Creates an entitlement before the query runs
- **In-arrears**: Records usage after the query completes

## Error Handling

### Insufficient Funds

When a customer doesn't have enough balance to process a query, the SDK throws an `InsufficientFundsError` with a portal URL so they can top up:

```typescript
import { createWrapClient, InsufficientFundsError } from "@paygentic/wrap";

const client = createWrapClient();
const plan = await client.plans.init();
const query = plan.wrap("customer_123");

try {
  const response = await query({ model: "claude-sonnet-4-20250514", prompt: "Hello" });
  for await (const message of response) {
    // Process messages
  }
} catch (error) {
  if (error instanceof InsufficientFundsError) {
    console.log("Please top up your balance:", error.portalUrl);
    console.log("Link expires at:", error.portalExpiresAt);
    console.log("Subscription ID:", error.subscriptionId);
    // Redirect customer to error.portalUrl to add funds
  } else {
    throw error;
  }
}
```

The `InsufficientFundsError` includes:

- `portalUrl` - URL where the customer can add funds to their wallet
- `portalExpiresAt` - When the portal URL expires (ISO timestamp)
- `subscriptionId` - The subscription that needs funding

### Query Stopped Due to Budget Limit

The SDK automatically sets a budget limit based on your pricing config to protect against runaway costs. When a query exceeds this limit, the agent stops and the result message has `subtype: "error_max_budget_usd"`.

You can resume the query by passing the `session_id` in the `options.resume` field:

```typescript
let sessionId: string | undefined;

const response = await query({ model: "claude-sonnet-4-20250514", prompt: "Complex task" });

for await (const message of response) {
  if (message.type === "result") {
    sessionId = message.session_id;

    if (message.subtype === "error_max_budget_usd") {
      console.log("Query stopped due to budget limit");
      console.log("Cost incurred:", message.total_cost_usd);
    }
  }
}

// Resume the query if it stopped due to budget limit
if (sessionId) {
  const continuation = await query({
    model: "claude-sonnet-4-20250514",
    prompt: "Please continue",
    options: { resume: sessionId },
  });

  for await (const message of continuation) {
    // Billing is handled automatically for the continuation
  }
}
```