# @curless/agentbank-merchant-sdk

Merchant-side SDK for [agentbank](https://mcp.curless.ai/doc).
Drop into a merchant's backend (Shopify storefront, Sinocare checkout,
TikTok Shop seller service, anything that wants to **accept agent payments**)
and you get:

- **`checkout.openACP`** — merchant-quoted **card** checkout. **You** price an order from your own catalog and open a checkout; an agent pays it with a card credential; the money lands in your account.
- **`checkout.openUCP`** — merchant-quoted **card** checkout over the **UCP** wire ([ucp.dev](https://ucp.dev)) — the UCP twin of `openACP` (same card settlement; reaches agents in the UCP ecosystem).
- **`checkout.openX402`** — merchant-quoted **USDC (x402)** checkout. You set the authoritative USDC amount; the gateway freezes it and returns the x402 challenge; a buyer agent signs an EIP-3009 authorization and pays `{ sessionId }` on-chain. Settlement is on Base (mainnet or Sepolia, decided by your account/key) — your USDC lands at your address. The stablecoin twin of `openACP`.
- **`checkout.chargeSkyfire`** — accept a **Skyfire KYAPay** token: the buyer agent hands you a signed PAY token and you charge it for your merchant (`checkout.chargeSkyfire({ token })` → the gateway verifies the token's issuer/audience + settles). Merchant-quoted; the amount is carried in the signed token.
- **`checkout.openMPP`** — accept an **MPP** (Machine Payments Protocol) charge, incl. **Tempo** on-chain settlement. First call with no credential returns the HTTP-402 `WWW-Authenticate` challenge you hand back to the buyer (`{ status: 'payment_required', wwwAuthenticate }`); the buyer pays (e.g. on the Tempo chain) and returns a `Payment` credential; the second call with `{ credential }` settles (`{ status: 'settled', paymentIntentId, paymentStatus }`). `checkout.openMPP({ amount, currency, method?, reference?, credential? })`.
- **`orders`** — read your settled orders: `orders.list({ status, protocol, currency, from, to, limit, offset })` (filtered + paginated, with a total), `orders.get(id)` (one order in full + the card it was paid with), `orders.summary({…})` (count + gross by currency + breakdowns by protocol/status — a dashboard in one call).
- **`orders` refunds** — work the refund queue: `orders.listRefundRequests({ status })` (buyers' refund requests on your orders), `orders.approveRefund(id)` (approve → forwarded to Curless), `orders.rejectRefund(id, { note })`, and `orders.refund(orderId, { amount, reason })` (merchant-initiated direct refund, no buyer request). See **Refunds** below.
- **`AgentbankMerchant`** — typed client; also exposes `PaymentIntents` (create/get/capture/list/ledger-entries across ACP / x402 / AP2 / UCP / A2A), `PaymentLinks`, `Customers`, `BalanceTransactions`, and `WebhookEndpoints`. (`PaymentIntents` has no `refund()` — the refund flow lives on `orders`.)
- **`verifyWebhook` / `parseVerifiedWebhook`** — HMAC-SHA256 signature verification (Stripe-style `t=…,v1=…`), constant-time, **edge-safe** (Web Crypto, runs on Vercel Edge / Cloudflare Workers). Async — `await` them.
- **`x402`** — emit/parse payments yourself (`buildChallenge()`, `decodeXPaymentHeader()`) **plus zero-crypto-knowledge helpers**: `usdc(network)` (contract + EIP-712 domain + decimals — no hardcoded hex), `isMainnet/isTestnet(network)`, `explorerTxUrl(network, txHash)` (a basescan / sepolia.basescan receipt link), `formatUsdc(baseUnits)` (exact 6-dp string, no float drift).

agentbank never stores your catalog — the price you pass is authoritative, and you accept every agent-payment protocol with zero protocol code. One runtime dependency (`@curless/agentbank-core`). **Edge-safe** — Web Crypto only, no `node:` builtins. Node 18+.

> **Refunds**: a buyer opens a refund request, or the merchant refunds directly
> via `orders.refund(orderId)`. The merchant works the queue with
> `orders.listRefundRequests` / `approveRefund` / `rejectRefund`. Approve forwards
> the refund to Curless (which holds the money and executes the card refund); the
> order flips to `refunded` / `partially_refunded` once Curless confirms via
> webhook. Read the resulting state via `orders.list({ status: 'refunded' })` or
> `paymentIntents.get()`. (agentbank never touches the rail directly — Curless
> originates the actual money movement.)

## Fast path — open a merchant-quoted checkout

```ts
import { AgentbankMerchant } from '@curless/agentbank-merchant-sdk';

const agentbank = new AgentbankMerchant({
  baseUrl: 'https://mcp.curless.ai',
  apiKey: process.env.AGENTBANK_MERCHANT_TOKEN!, // merchant:write
  merchantId: process.env.AGENTBANK_MERCHANT_ID!,
});

// CARD (ACP) — YOU set the price; an agent pays it; money lands in your account.
const checkout = await agentbank.checkout.openACP({
  currency: 'EUR',
  items: [{ sku: 'resort-bali', name: 'Sinocare Bali — 7 nights', quantity: 1, unitPrice: 192000 }],
});
return { checkoutId: checkout.id, amount: checkout.amount, currency: checkout.currency };

// USDC (x402) — YOU set the USDC amount (base units, 6 dp). Hand the challenge
// to the buyer agent; it signs + pays `{ sessionId }` on-chain. The network
// (base / base-sepolia) is decided by your account — same code on testnet/mainnet.
const x402co = await agentbank.checkout.openX402({
  amount: 1_050_000, // 1.05 USDC
  items: [{ sku: 'resort-bali', name: 'Sinocare Bali — 7 nights', quantity: 1, unitPrice: 1_050_000 }],
});
return { sessionId: x402co.sessionId, accepts: x402co.accepts, network: x402co.network };
```

That's the whole integration: install, set three env vars, call one method. You
stay the source of truth for your catalog and prices — nothing to sync.

## Why there is no `checkout.openUAAG`

Every method on `checkout` promises the same thing: **you price it, an agent pays it, the money lands in your account.** UnionPay Agent Pay cannot promise that, so it is not there.

UPI authenticates the cardholder on their own page and hands the **agent** a one-time payment token and a DTVC — shaped like an ordinary UnionPay card (token in the card-number field, DTVC in the CVV2 field). **You** charge that through your own card acceptance. agentbank is not in that settlement path, so it never captures the intent and the money never passes through it. An `openUAAG` that looked like its siblings would be a lie in the one place you most need the truth.

What you need from us instead is **evidence**, and it is on the payment intent:

```ts
const pi = await merchant.paymentIntents.get('pi_…');
const uaag = pi.protocolContext?.uaag;
if (uaag?.payloadAt) {
  // A payable credential was issued for this order at uaag.payloadAt.
  // If nothing arrives at your acquirer, that is the gap to investigate —
  // the intent will stay uncaptured either way, by design.
  console.log(uaag.credential?.tokenLastFour, uaag.credential?.tokenExpiration);
}
```

The credential itself is never copied onto the intent — a last four and an expiry cannot pay for anything. What a merchant needs to accept these is UnionPay acceptance, not an agentbank feature.

## Read your orders

```ts
// Filtered + paginated (status / protocol / currency / date range):
const { data, pagination } = await agentbank.orders.list({ status: 'paid', protocol: 'x402', limit: 50 });

// One order in full (line items + the card it was paid with):
const { order, payment } = await agentbank.orders.get(data[0]!.id);

// A dashboard in one call: count + gross by currency + by protocol/status.
const summary = await agentbank.orders.summary({ from: '2026-06-01' });
// summary.count · summary.byCurrency[{ currency, count, totalAmount }] · summary.byProtocol · summary.byStatus
```

## x402 helpers — display a settled payment, zero crypto knowledge

```ts
import { x402 } from '@curless/agentbank-merchant-sdk';

x402.explorerTxUrl('base', '0xa596…');  // → https://basescan.org/tx/0xa596…  (sepolia.basescan on testnet)
x402.formatUsdc(1_050_000);             // → "1.050000"   (exact string math, no float drift)
x402.formatUsdc(1_050_000, { symbol: true }); // → "1.050000 USDC"
x402.isMainnet('base');                 // → true       (isTestnet('base-sepolia') → true)
x402.usdc('base')!.contract;            // → "0x833589…02913"  (no hardcoded hex)
```

## Lower-level — PaymentIntents, webhooks, self-emitted 402

```ts
import { AgentbankMerchant, parseVerifiedWebhook, x402 } from '@curless/agentbank-merchant-sdk';

const client = new AgentbankMerchant({ baseUrl: 'https://mcp.curless.ai', apiKey: process.env.AGENTBANK_KEY! });

// Create a PaymentIntent the agent will pay against
const pi = await client.paymentIntents.create({
  organizationId: 'org_123',
  curlessMerchantId: 'curless_mch_sinocare_bali',
  amount: 1160,
  currency: 'USD',
  protocol: 'x402',
  items: [{ catalogItemId: 'room-suite', quantity: 1, unitPrice: 1160 }],
  idempotencyKey: req.headers['idempotency-key'],
});

// Verify an inbound agentbank webhook (edge-safe; HMAC is async)
const event = await parseVerifiedWebhook(
  process.env.WEBHOOK_SECRET!, // whsec_… from /v1/webhook-endpoints
  await req.text(),            // RAW body, NOT JSON-parsed
  req.headers['x-agentbank-signature'],
);

// Or emit your own 402 challenge from your route. buildChallenge auto-fills the
// right EIP-712 domain per network ("USD Coin" on base, "USDC" on base-sepolia)
// — a wrong name reverts on-chain, so let the helper get it right.
const usdc = x402.usdc('base')!;
return new Response(
  JSON.stringify(
    x402.buildChallenge({
      asset: usdc.contract,   // no hardcoded hex
      network: 'base',
      payTo: '0xYourUsdcAddress',
      amount: '1050000',      // 1.05 USDC (6 dp)
      resource: 'https://shop.example/api/checkout',
    }),
  ),
  { status: 402, headers: { 'content-type': 'application/json' } },
);
```

## Timeouts & errors

Every request carries a 30s default deadline (`timeoutMs` on the constructor
or per request; `0` disables) and supports `AbortSignal` via
`RequestOptions.signal`. ALL failures throw `AgentbankError` — transport
failures use `status === 0` with code `timeout` / `aborted` / `network_error`.
