Skip to content

Merchant SDK

@curless/agentbank-merchant-sdk is the entire gateway integration for a merchant backend. You price orders from your own catalog and open a checkout; the gateway holds no catalog of yours.

Terminal window
npm install @curless/agentbank-merchant-sdk
import { AgentbankMerchant } from '@curless/agentbank-merchant-sdk';
const agentbank = new AgentbankMerchant({
baseUrl: 'https://mcp.curless.ai',
apiKey: process.env.AGENTBANK_MERCHANT_TOKEN!,
merchantId: process.env.AGENTBANK_MERCHANT_ID!,
});
Method Protocol / rail
checkout.openACP({ currency, items }) ACP — card
checkout.openUCP({ currency, items }) UCP — card or stablecoin
checkout.openX402({ amount, items, currency? }) x402 — stablecoin (USDC/RLUSD)
checkout.chargeSkyfire({ token, description? }) Skyfire — charge a KYAPay token
checkout.openMPP({ amount, currency, method?, credential?, description? }) MPP — HTTP-402, incl. Tempo on-chain

items are { sku, name, quantity, unitPrice } in minor units — for ACP/UCP you set unitPrice and the total is derived. openX402 and openMPP instead take an authoritative top-level amount (the item list is descriptive). openMPP with no credential returns the 402 challenge; call again with the buyer’s credential to settle.

  • orders.list({ status, protocol, currency, from, to, limit, offset }) — filtered + paginated, with a total.
  • orders.get(id) — one order in full (line items + how it was paid).
  • orders.summary({ … }) — count + gross by currency + breakdowns.
  • balanceTransactions.list({ limit, offset }) — a paginated fund-flow statement (your live per-currency wallet balance is in the MCP connector’s get_balance).

orders.listRefundRequests({ status }), orders.approveRefund(id), orders.rejectRefund(id, { note }), and orders.refund(orderId, { amount, reason }) (merchant-initiated). Approve forwards the refund to Curless, which executes it.

Register an endpoint (POST /v1/webhook-endpoints, returns a whsec_ once) and verify deliveries — HMAC-SHA256, constant-time, edge-safe (Web Crypto). verifyWebhook(secret, rawBody, signature) returns a boolean; parseVerifiedWebhook(secret, rawBody, signature) verifies and returns the parsed event (or throws).

The list of event types an endpoint can receive is in the API reference → Webhook events.

import { parseVerifiedWebhook } from '@curless/agentbank-merchant-sdk';
const event = await parseVerifiedWebhook(whsec, rawBody, signatureHeader);

Deliveries are at-least-once — dedupe on event.id

Section titled “Deliveries are at-least-once — dedupe on event.id”

A delivery is retried with backoff until your endpoint returns 2xx, so the same event can arrive more than once: if your handler commits and then the response is lost in flight, we never saw the 2xx and will send it again. This is normal, not an error condition — a handler that books a refund or a fulfilment on every delivery will eventually do it twice.

event.id is the dedupe key. It is assigned once, when the event is queued, and every retry re-sends that stored payload verbatim — so it is stable across retries and safe to persist:

const event = await parseVerifiedWebhook(whsec, rawBody, signatureHeader);
// Unique index on event_id. Already seen → ack and stop.
const fresh = await db.markSeen(event.id);
if (!fresh) return new Response('ok', { status: 200 });
await handle(event);
return new Response('ok', { status: 200 });

Ack duplicates with a 2xx, as above — returning an error re-queues the event you already processed.

One caveat if you register more than one endpoint: a single event fans out to each of them carrying the same event.id. That is intentional (it is one event), but if two endpoints feed the same handler, key your dedupe on the endpoint plus event.id rather than event.id alone — otherwise the second endpoint’s delivery looks like a duplicate and gets dropped.

Every non-2xx throws a typed AgentbankError with status + code — catch it to handle failures uniformly.