npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@curless/agentbank-sdk

v0.11.0

Published

Official buyer/agent SDK for agentbank — OAuth, agent + customer management, Wallets, and the Pay spend-control plane.

Readme

@curless/agentbank-sdk

Official buyer-side SDK for agentbank — agent-commerce payments.

Two things live here:

  • The buyer's wallet — a person signs in, binds a card, sets their own spend limits, and mints a one-off credential to pay a merchant with. This is what most integrators come for.
  • The Pay spend-control plane — agent OAuth, agent/customer management, authorize/capture against an agent's budget.
npm install @curless/agentbank-sdk

Install this package, not @curless/agentbank-core. Core is the shared kernel (crypto, errors, HTTP) that this package is built on; it arrives as a transitive dependency and has nothing in it you should be calling.

The buyer's wallet

The wallet is independent of any merchant. It holds the buyer's card and the buyer's limits, and it issues a credential; it never talks to a shop. The credential is what travels.

Sign in

The buyer signs in in their own browser, not in your app — verifying an email means clicking a link in an inbox, and tokenizing a real card means Stripe.js on a page. Neither fits in a chat window or a CLI. So sign-in is the RFC 8628 device flow: you get a URL, you show it, you wait.

The wallet and the gateway are two origins: createBuyerSession takes the wallet's (https://wallet.curless.ai), while Agentbank and the merchant clients take the gateway's (https://mcp.curless.ai).

import { createBuyerSession } from '@curless/agentbank-sdk';

// The WALLET's origin — not the gateway's. The buyer wallet is its own service
// (wallet.curless.ai); the gateway (mcp.curless.ai) serves nothing under
// /v1/buyer and answers 401 there.
const wallet = createBuyerSession({ baseUrl: 'https://wallet.curless.ai' });

const login = await wallet.startDeviceLogin();
console.log(`Open ${login.verificationUriComplete} to sign in`);
const user = await login.wait(); // resolves when they finish in the browser

wait() polls for you and tells pending / slow_down / expired / denied apart — an expired sign-in is a different sentence from one still in progress. The device code itself never leaves the returned object: it is the bearer credential that collects the session, so a caller who only needs a URL never sees it.

There is also wallet.login(email, password) for a deployment configured with a single account.

Pay for something

// The merchant priced this and opened a checkout; you have its id and total.
const credential = await wallet.payCredential({
  amount: 192_000,          // minor units — €1,920.00
  currency: 'EUR',
  merchantRef: merchantId,  // checked against the buyer's own allowlist
});

// Hand credential.token to that merchant's checkout as the payment token.
// You never see a card number, and neither does the merchant.

The credential is a Stripe Shared Payment Token: one seller, one currency, one maximum, fifteen minutes. It cannot be replayed against a different merchant or a larger sum.

The buyer's own limits are evaluated here, before Stripe is asked for anything. A refusal is the buyer's limit talking, not a payment failure — worth saying to them in those words:

try {
  await wallet.payCredential({ amount: 192_000, currency: 'EUR', merchantRef });
} catch (err) {
  if (AgentbankError.is(err) && err.status === 403) {
    // "this is over the daily limit you set" — not "the payment failed"
  }
}

The rest of the wallet

await wallet.me();            // who this is + spendPolicy + spentToday/Month
await wallet.setLimits({ dailyLimit: 50_000, merchantAllowlist: ['sinocare'] });
await wallet.balance();
await wallet.orders({ limit: 20 });

await wallet.cards();               // { cards, unavailable? }  ← see below
await wallet.bindCard('pm_card_visa');
await wallet.unbindCard('pm_123');
await wallet.enrollUnionPayCard();  // { enrollUrl, status:'pending' }  ← a URL, not a card
await wallet.unionPayCards();       // { cards }  ← only ones UnionPay confirmed
await wallet.setDefaultCard({ rail: 'uaag', id: 'uaagcard_1' });  // the card decides the rail

await wallet.logout();        // revokes server-side, then forgets it

wallet.fetch<T>(path, init) is the escape hatch for anything not wrapped above — it carries the session and the same 401/403 handling. Reach for a method first: the /v1/buyer/* paths are ours to change, and the methods are the part we keep.

UnionPay binds differently, and the difference matters to what you tell the person. bindCard takes a Stripe reference the browser already tokenised. enrollUnionPayCard() takes nothing and gives back a URL: UnionPay captures the card on their own page, with 3DS and a passkey, so no card detail ever reaches this SDK. ⚠️ The card is not bound when the promise resolves — it exists only once UnionPay's callback lands, which may be a minute later or never if the page is abandoned. Show the person enrollUrl, then read unionPayCards(); never report success from the enrol call.

One default, and the card decides how the payment is made. setDefaultCard takes a rail with the id, because those are the same choice: a stripe card is charged by this wallet, which mints a one-time credential against it and enforces the buyer's own limits at that moment; a uaag card is never charged here at all — the agent is handed the card, UnionPay authenticates the cardholder on their page, and the merchant charges what UnionPay issues.

⚠️ So payCredential refuses outright while the default is on a rail this wallet cannot charge, and names the way out. It does not quietly fall back to another card: the buyer chose this one, and paying with a different card while reporting it as theirs is the worst answer available.

id is required on uaag and optional on stripe, which is the rule rather than an inconsistency — a UnionPay card is the default only because someone picked it, while a cleared stripe choice falls back to the first bound card.

cards() returns unavailable for a reason. An empty cards with no unavailable means the buyer has bound none. An empty cards with it means we could not read them. Flatten the two and you tell someone their cards are gone, and watch them bind another.

setLimits is a change: send only what changes, null removes a limit, everything else stays. And it only tightens — the session belongs to an assistant, and the limits are what stand between it and the card. Lowering or adding a limit applies at once (applied); raising or removing one, or widening an allowlist, does not apply at all and comes back as needsConfirmation.url, the wallet's limits page filled in, for the person to check and save themselves.

const r = await wallet.setLimits({ dailyLimit: 90_000 });
if (r.needsConfirmation) show(r.needsConfirmation.url); // nothing changed yet

Every wallet request has a deadline (createBuyerSession({ baseUrl, timeoutMs }), 30 s by default): a stalled wallet fails with status === 0, code timeout, rather than hanging like a payment still in flight.

Which card pays this one

import { cardChoice, isUnionPayCard } from '@curless/agentbank-sdk';

const choice = await cardChoice(wallet); // { cards: [{ id, label, charges }], selectedId }
// show choice.cards with choice.selectedId preselected, let the person pick, then:
if (isUnionPayCard(choice, pickedId)) {
  // UnionPay: the merchant charges — see @curless/agentbank-protocols/uaag
} else {
  await wallet.payCredential({ amount, currency, merchantRef, cardId: pickedId });
}

The preselection is the buyer's default, resolved the way the payer resolves it — never "the first card we managed to read". Picking a different card here does not change the default. charges: false marks a card the wallet does not charge (UnionPay): the button above it must not promise a charge. Structural types only, so any object with me / cards / unionPayCards works.

Session state

  • Only a 401 ends the session. A 403 does not: hitting a limit you set yourself must not sign you out, or you cannot reach the session you would need to raise it.
  • wallet.current() returns the signed-in user or null, no round-trip.
  • Calls made while signed out throw with code buyer_not_logged_in; calls made after expiry throw buyer_session_expired. They are deliberately different strings, because "your session expired" rendered as "you have no orders" is the same bug twice.

Which checkout a request reaches

A buyer client that opens merchant checkouts sends an idempotency key with each open. One rule for all of them: key = H(buyer secret, cart, attempt).

import { CheckoutBook, buyerSecret, canonicalLines, openCheckoutFor } from '@curless/agentbank-sdk';

const secret = await buyerSecret(SERVER_KEY, sessionId); // stable per buyer, unguessable
const book = new CheckoutBook<{ checkoutId: string; status: string }>(); // ≤ 20 carts, 24 h idle

const lines = canonicalLines(cart.lines, shopifyIdTail); // merged, sorted, ids normalised
const checkout = await openCheckoutFor({
  book,
  secret,
  lines,
  open: (idempotencyKey) => shop.openCheckout({ lines: cart.lines, idempotencyKey }),
  isOver: (c) => c.status !== 'open', // paid / expired / canceled
});
  • The same request reaches the same open checkout (a double tap, a host re-firing a hung call).
  • book.retire(lines) once a checkout is over: the next purchase is a new one. The checkout stays findable by book.get(checkoutId) (the last keep, 50 by default) — to show its receipt again, or to tell a checkout this buyer opened from one it never did. A replayed checkout that comes back already over (the book lives in memory; after a restart the attempt is 0 again) is retired and opened once more.
  • A tool that opens AND pays in one call uses book.settled(lines) instead: the paid checkout stays the answer for replayMs (60 s), so a re-fired call cannot open and pay a second one.
  • A client that cannot see a checkout's status on the open passes offerMs below the shop's checkout lifetime: an unpaid checkout older than that is not keyed on again.

The Pay spend-control plane

Admin (manage agents, fund, approve)

import { Agentbank } from '@curless/agentbank-sdk';

const ab = new Agentbank({
  baseUrl: 'https://mcp.curless.ai',
  apiKey: 'agb_admin_...', // an agentbank:admin / pay:admin key
});

const agent = await ab.agents.create({
  name: 'procurement-bot',
  spendPolicy: { perTransactionLimit: 50_00, dailyLimit: 500_00, approvalRequiredAbove: 100_00 },
});

await ab.pay.deposit({ amount: 1000_00, currency: 'USD' });
await ab.pay.fundAgent(agent.id, { amount: 500_00, currency: 'USD' });
const cred = await ab.agents.issueCredential(agent.id); // cred.secret shown once

Agent (spend, with auto-managed token)

const agentClient = Agentbank.withClientCredentials({
  baseUrl: 'https://mcp.curless.ai',
  clientSecret: cred.secret,
});

const auth = await agentClient.pay.authorize({
  agentId: agent.id,
  merchantRef: 'acme.example',
  amount: 12_00,
  idempotencyKey: 'order-123',
});
// auth.status: 'authorized' | 'pending_approval' | 'denied'
if (auth.status === 'authorized') await agentClient.pay.capture(auth.id);

SpendPolicy is one type across both halves — an agent's budget and a buyer's wallet limits are the same shape, evaluated by the same code on the server.

Notes

  • Amounts are minor-unit integers (USD = cents, EUR = cents).
  • Never send a card number. Cards are bound by Stripe reference (pm_… / tok_…); a PAN reaching a server is a compliance incident, and this API refuses one rather than storing it.
  • Errors throw AgentbankError with .status + .code — including transport failures: status === 0 with code timeout / aborted / network_error means no HTTP response happened. One catch type. Use AgentbankError.is(err), not instanceof, so the guard survives two copies of the package in one dependency tree.
  • Timeouts: every request has a 30s deadline by default. Tune per client (new Agentbank({ ..., timeoutMs })) or per request (RequestOptions.timeoutMs; 0 disables). RequestOptions.signal accepts an AbortSignal for caller-side cancellation.
  • ESM-only; Node ≥ 18 (uses global fetch).
  • Pass fetch in the constructor to inject a custom implementation (tests, proxies).

Prefer not to write code?

npx -y @curless/agentbank-mcp is this wallet as an MCP server — ten tools, no configuration, the buyer signs in at runtime. Same session, same limits.