@ucash/agents
v0.6.11
Published
Monetize AI agents with non-custodial HTTP-402 crypto payments. Zero-dependency JS client for agents.u.cash: sell priced resources, get paid to your own wallet across 40+ chains, with scoped subagent keys.
Maintainers
Keywords
Readme
@ucash/agents
Zero-dependency JavaScript client for the agents.u.cash API - the 402 Online Protocol for non-custodial agent payments. Works for both sides of the network: an agent selling (manage resources, watch settlements) and a buyer (fetch a 402 door, pay, and settle - automatically via on-chain detection, or instantly by submitting the tx hash). Supports ~40 native coins (Bitcoin + Lightning, Ethereum + 15 EVM L2s, Solana, Tron, XRP, USDT/USDC, USDC-on-Base gasless, UCASH) plus custom tokens on 20 chains (ERC-20 / TRC-20 / SPL / ...) - see agents.u.cash for the full live list.
Uses the global fetch (Node 18+ and modern browsers). No dependencies.
Highlights
- Monetize AI agents pay-per-call: sell priced resources over HTTP 402; buyers pay direct to your own wallet.
- Non-custodial: the platform never holds funds. 40+ chains (BTC + Lightning, ETH + 15 EVM L2s, SOL, TRX, XRP, USDC-on-Base, UCASH, custom tokens).
- Zero-dependency JS client (Node 18+ / browsers) + a Python SDK + an MCP server (77 tools).
- Scoped subagent (RBAC) keys, per-caller spend caps, HMAC-signed settlement webhooks with a built-in verifier.
- Two-sided: sell (resources, one-off payment links, shop, stores, landers, payout-info/OTC) and buy (402 door, UCP checkout sessions).
Docs: agents.u.cash · OpenAPI · llms.txt · CHANGELOG
Contents
- Install · Sell · Buy · UCP checkout sessions
- Manage your store · Subagents · Spend caps
- Verifying webhooks · Asset codes · Resource vs payment request
- API · Error handling
Install
npm install @ucash/agentsOr copy index.js - it has no dependencies.
Sell (as an agent)
import { AgentsUCash } from '@ucash/agents';
// Get an API key once (wallet-first; works at $0 immediately - verify email optionally for free credit):
// const { api_key } = await new AgentsUCash().signup({ email: '[email protected]', password: 'longpass' });
const agent = new AgentsUCash({ apiKey: process.env.UXC_API_KEY });
await agent.setWallet('btc', 'bc1q…');
await agent.setWebhook('https://my.bot/webhook');
const { res_id, checkout_url } = await agent.createResource({ amount: 0.05 }); // priced resource
const { accepts } = await agent.createChallenge(res_id); // what a buyer pays
// Share the checkout_url - the pay.u.cash buyer door (/checkout/<enc>?cloud=<token>):
// a human opens the HTML checkout, an agent fetches checkout_url + '?agent=1' for the 402 manifest,
// an x402 client sends X-PAYMENT. (https://agents.u.cash/r/{res_id} is a back-compat alias.)
// Exact vs dust: save 3+ addresses per coin (await agent.setWallet('ucash', '0xaaa...,0xbbb...,0xccc...'))
// for exact-amount unique-address payments (500.00000000); one address adds sub-unit dust (500.00004180).
// One-off link instead of a persistent resource (closes when paid or after expiry):
const { url } = await agent.createPaymentRequest({ amount: 5, expiry: '24h' }); // -> pay.u.cash /id/<enc>
const settled = await agent.getSettlements(); // your earnings logBuy (as a buyer)
import { AgentsUCash } from '@ucash/agents';
const buyer = new AgentsUCash(); // no key needed to buy
const { accepts } = await buyer.viewDoor(resId); // the 402 door (JSON)
// 1. pick an entry, pay entry.payTo exactly entry.amount from your wallet (out of band)
// 2. the platform auto-detects the on-chain payment and settles it.
// Optionally POST the tx hash to settle instantly instead:
const result = await buyer.verify(accepts[0].challengeId, txHash);
// -> { settled: true } | { status: 'pending', confirmations, required }A human-friendly payable page is also available: await buyer.viewDoor(resId, { html: true }) returns the HTML.
UCP checkout sessions (buyer)
Multi-item, mixed-currency carts over the Universal Commerce Protocol. The merchant is resolved from the custom-domain baseUrl, or from a cloud merchant token on the shared host. No API key.
const buyer = new AgentsUCash(); // baseUrl = the merchant's domain (or the platform host + cloud)
const cart = await buyer.createCheckout({
lineItems: [{ item: { id: resIdA }, quantity: 1 }, { item: { id: resIdB }, quantity: 2 }],
currency: 'USD', // optional: cart currency for mixed-currency carts
cloud: '<merchant-token>', // only on the shared platform host
});
// -> { id, status:'incomplete', currency, line_items, totals, ap2:{ merchant_authorization, nonce } }
const ready = await buyer.completeCheckout(cart.id, { cloud: '<merchant-token>' });
// -> ready_for_complete + payment_handlers[] (pay each challenge on-chain)
const order = await buyer.getOrder(cart.id, { cloud: '<merchant-token>' }); // per-item fulfillment statusOptional AP2 (dev.ucp.shopping.ap2_mandate): pass completeCheckout(id, { ap2: { checkout_mandate }, cloud }) with a buyer-signed SD-JWT-VC for holder-proof authorization. Responses are RFC 9421-signed (ES256) with the merchant key.
Manage your store (full merchant surface)
Beyond priced 402 resources, an agent is a first-class merchant over its own account: full transaction history + actions, multi-store, shop products, landers, payout-info/OTC, discount codes, checkout custom fields, and billing. All key-authenticated; tenant-scoped to the agent.
// Transactions: full history, CSV export, + actions
const txs = await agent.getTransactions({ status: 'C', limit: 50 });
const csv = await agent.downloadTransactions({ dateFrom: '2026-01-01' });
await agent.refundTransaction(txId); // self-guarding: only if you connected a refund-capable node/coinbase
await agent.resendWebhook(txId);
await agent.submitHash(txId, '0xabc...');
// Stores (sub-merchants)
const store = await agent.createStore({ label: 'Store B' }); // api_key+cloud_token+webhook_secret returned once
await agent.rotateStoreCredential(store.store.id, 'cloud_token');
// Shop products, landers, payout-info/OTC
await agent.createShopProduct({ title: 'Ebook', price: 9.99, currency: 'USD' });
await agent.createLander({ checkoutId: flagId });
await agent.createPayoutInfo({ amount: 50, currency: 'USD', email: '[email protected]' }); // emails the payee a link
// Discount codes (amount = price multiplier: 0.9 = 10% off) + checkout custom fields
await agent.addDiscountCode({ code: 'LAUNCH', amount: 0.9, checkoutIds: 'all' });
await agent.addCustomField({ type: 'select', label: 'Size', options: ['S', 'M', 'L'] });
// Billing: balances + capacity
const bill = await agent.getBilling(); // { credit_balance, ucash_points, lander_slots:{...} }
await agent.buyLanderPack(10); // debits credit_balance, grows slots
await agent.redeemUcash(1000); // fee-credit points -> fee creditSubagents (scoped RBAC keys)
Delegate a LIMITED credential to another automated principal. A subagent is a scoped sa_ API key with a
staff role: it authenticates against your tenant, but the existing RBAC (uxc_can) enforces a limited
capability set on every endpoint, so it can only do what the role allows. Owner-only (you create them; a
subagent cannot create subagents). Reuses the merchant staff model + seat billing (a subagent with no free
slot debits credit_balance).
// Built-in role: a clerk can create resources + read, but not edit settings, refund, or manage stores
const sub = await agent.createSubagent({ role: 'clerk', displayName: 'Fulfillment bot' });
// sub.api_key is the sa_ key, returned ONCE. The key works on every /v1/* endpoint; writes its role
// lacks return 403, and managing stores stays owner-only.
// Or a custom permission set (slugs: transactions.view, checkouts.edit, payment-links.create, ...).
// storeScope (store ids) restricts it to those stores (empty = all stores).
const ro = await agent.createSubagent({
role: 'custom', permissions: ['transactions.view', 'checkouts.view'], storeScope: [storeId],
});
await agent.getSubagents(); // list (never returns the api_key)
await agent.updateSubagent(sub.subagent.id, { role: 'manager', status: 'suspended' });
const fresh = await agent.rotateSubagentKey(sub.subagent.id); // old key stops working; new key ONCE
await agent.deleteSubagent(sub.subagent.id); // key stops working; seat slot freesSpend caps (per-caller limits)
Cap how much a single payer can spend on a resource in a rolling window. A payer whose settled spend in the
window reaches amount is refused new authorization (x402 before-charge; on detect the platform refuses to
credit an over-cap payment). Caller identity is the payer wallet (or, with by: 'ip', the buyer IP, enforced at the door before any charge). Optional; off by default.
const res = await agent.createResource({ amount: 0.05, currency: 'USD', maxPerCaller: { amount: 1.00, windowHours: 24 } }); // add by:'ip' to cap by buyer IP at the door instead of by wallet
await agent.setResourceCap(res.res_id, { amount: 0.50, windowHours: 6 }); // change it
await agent.setResourceCap(res.res_id, null); // clear it
const callers = await agent.getResourceCallers(res.res_id); // [{ caller, spend, payments }]
await agent.deleteResource(res.res_id); // soft-disable (settled history kept)
const relay = await agent.getExactRelay(); // gasless relayer config (on by default)
await agent.setExactRelay(false); // opt out to buyer-pays-gas (gas + 21% while on)Verifying webhooks
When a payment settles, agents.u.cash POSTs an HMAC-signed event to your webhook URL. Verify it with the
static helper (it runs on your server; no instance or API key needed). The signature is HMAC-SHA256 of
"<t>.<rawBody>" in the X-Webhook-Signature: t=<unix>,v1=<hex> header. Use the raw request body -
re-encoding the JSON breaks the signature.
import { AgentsUCash } from '@ucash/agents';
// Express - capture the RAW body on this route (do NOT put express.json() in front of it):
app.post('/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
const valid = await AgentsUCash.verifyWebhookSignature(
req.body.toString(), // the EXACT raw bytes you received
req.get('X-Webhook-Signature'), // t=<unix>,v1=<hex>
process.env.UXC_WEBHOOK_SECRET, // from setWebhook()/rotateWebhookSecret() - shown ONCE
);
if (!valid) return res.status(401).send('bad signature');
const event = JSON.parse(req.body.toString());
// Deduplicate by event.event_id (also in X-Webhook-Event-Id) - a settled txn may be delivered >1x.
res.status(200).send('ok');
});The 300-second replay window is on by default; pass { tolerance: 0 } to skip the freshness check.
Asset codes
acceptedAssets takes coin codes (omit it to accept all your configured wallets). Common built-in codes:
| Coin | Code | Note |
|---|---|---|
| Bitcoin | btc | |
| Bitcoin Lightning | btc_ln | NOT lightning or btc-ln |
| Ethereum | eth | |
| EVM L2s | eth_base, eth_arb, eth_op, eth_linea, eth_unichain, eth_world, eth_scroll, eth_ink, eth_abstract, eth_plasma | plus native mnt, bera, s, mon, hype |
| USDC on Base | usdc_base | the x402 gasless rail |
| Stablecoins | usdc, usdt, usdt_tron, usdt_bsc | |
| Others | sol, trx, xrp, ltc, doge, bnb, pol, avax, xmr, algo, bch, dot, xlm, xtz, ucash | |
Plus any custom-token code you added via setCustomToken(). A misspelled code throws with
.code = 'uxc_unknown_asset' ("Unsupported asset: X"). The live canonical list grows as chains are
added - see agents.u.cash.
Resource vs payment request
Two ways to get paid - pick by use case:
| | createResource | createPaymentRequest |
|---|---|---|
| Lifetime | persistent (payable many times) | one-off link, closes on pay or expiry |
| Door | /r/{res_id} / checkout_url | /id/<enc> url |
| Multi-coin acceptedAssets | yes | no |
| Per-caller cap (maxPerCaller) | yes | no |
| title / note / expiry / redirect / externalReference | no | yes |
A resource is a standing price buyers pay repeatedly; a payment request is a single invoice link.
API
| Method | Auth | Description |
|---|---|---|
| signup({ email, password, primaryWallet? }) | - | Register; returns api_key |
| topUp(amount) | key | Create a ≥$1 top-up checkout (adds platform credit; activates if not yet) |
| getAgent() | key | Account snapshot (balance, wallets, webhook, earnings summary) |
| setWebhook(url) | key | Set the settlement webhook (auto-generates the HMAC secret; shown once) |
| getWebhook() / rotateWebhookSecret() / clearWebhook() | key | Read (masked) / rotate / clear the webhook |
| AgentsUCash.verifyWebhookSignature(rawBody, sigHeader, secret, { tolerance? }) | - | Verify an incoming webhook's HMAC signature (static; runs on your server) |
| setWallet(asset, address) | key | Set your receive address for an asset |
| setStripe({ secretKey, productId, webhookSecret, publishableKey? }) | key | Connect your Stripe account (card rail); verifies the key + product |
| getStripe() | key | Masked Stripe config + the webhook endpoint to register |
| clearStripe() | key | Disconnect your Stripe account |
| setCustomToken({ type, code, contractAddress, decimals, name, rate?, rateUrl? }) | key | Add a custom token (ERC-20/TRC-20/SPL); then setWallet({ asset: code, address }) to set its receive address |
| getCustomTokens() | key | List your custom tokens |
| deleteCustomToken(code) | key | Remove a custom token |
| getSettings() | key | Read safe settings (confirmations, webhook url+secret, currency, payment prefs, notifications, branding) |
| setSettings(partial) | key | Partially update safe settings |
| getIntegrations() | key | Read stored third-party integration credentials (Discord, Telegram, BigCommerce, Ecwid, Wix) |
| setIntegrations(integrations) | key | Store third-party integration credentials |
| createResource({ amount, currency?, acceptedAssets?, webhookUrl? }) | key | Create a priced resource |
| getResources(resId?) | key | List resources, or fetch one |
| createChallenge(resId) | key | Build the multi-coin accepts[] |
| verify(challengeId, hash) | key optional | Verify + settle (buyer-push: no key needed) |
| getSettlements() | key | Earnings log |
| viewDoor(resId, { html? }) | - | The public 402 door (JSON, or HTML) |
| createCheckout({ lineItems, currency?, buyer?, context?, cloud? }) | - | UCP checkout session (multi-item, mixed-currency cart) |
| getCheckout(id, { cloud? }) | - | Fetch a checkout session |
| completeCheckout(id, { ap2?, cloud? }) | - | Mint challenges → ready_for_complete (optional AP2 mandate) |
| cancelCheckout(id, { cloud? }) | - | Cancel a checkout session |
| getOrder(id, { cloud? }) | - | A checkout session as a UCP order (per-item fulfillment) |
| searchCatalog({ query?, filters?, pagination?, cloud? }) | - | Search the merchant catalog |
| getProduct(id, { cloud? }) | - | Fetch a single catalog product by id |
| lookupProducts(ids, { cloud? }) | - | Batch catalog lookup by ids |
| getTransactions({ status?, search?, cryptocurrency?, checkoutId?, dateFrom?, dateTo?, limit?, offset? }) | key | Full transaction history |
| getTransaction(id, { webhookLog? }) | key | One transaction, or its webhook log |
| downloadTransactions({ ...filters }) | key | CSV export (raw text) |
| refundTransaction(id) | key | Refund (guarded: needs a connected refund-capable node/coinbase) |
| resendWebhook(id) | key | Force a webhook re-delivery |
| submitHash(id, hash) | key | Attach an on-chain hash (replay-guarded) |
| getStores() / createStore({ label?, webhookUrl? }) / updateStore(id, { label?, webhookUrl? }) / deleteStore(id) | key | Multi-store CRUD |
| rotateStoreCredential(id, which) / testStoreWebhook(id) | key | Rotate api_key/webhook_secret/cloud_token; send a test webhook |
| getShopProducts(id?) / createShopProduct(fields) / updateShopProduct(id, fields) / deleteShopProduct(id) | key | Shop products (/v1/checkouts) |
| getLanders({ landerId?, offers? }) / createLander({ checkoutId, tpl? }) / updateLander({ landerId?, offerId?, ... }) / deleteLander(id) | key | Landers + offer status |
| createPayoutInfo({ amount, currency, ... }) / getPayoutInfo(id) / completePayout(id) | key | Payout-info / OTC requests |
| getDiscountCodes() / addDiscountCode({ code, amount, checkoutIds? }) / setDiscountCodes([...]) / deleteDiscountCode(code) | key | Discount codes (amount = multiplier) |
| getCustomFields() / addCustomField(field) / setCustomFields({ customFields, title? }) / deleteCustomField(index) | key | Checkout custom fields |
| getBilling() / buyLanderPack(qty) / redeemUcash(amount) | key | Balances + capacity (lander pack, fee-credit redeem) |
All calls return the parsed response object and throw on API errors (the error has .code and .status).
Input contracts (strict)
The API validates every write before touching state, so nothing is ever silently ignored:
- Unknown or misspelled keys ->
400 uxc_unknown_paramlisting the Allowed keys (this SDK'sassertKeysraises the same error client-side before the request is sent). - Wrong shapes ->
400 uxc_invalid_requestnaming the field + expected shape, e.g.confirmations must be an object: {default, coins, increase}. - Booleans must be true/false (or the strings "true"/"false"/"1"/"0"); "yes"/"maybe" are 400s.
- Updates are true partials: unsent fields keep their values (
updateShopProduct(id, {price: 5})changes only the price). - Re-setting an existing webhook secret returns
has_secret: true+ a hint; userotateWebhookSecret()to mint + reveal a new one.
Error handling
All calls return the parsed response object on success and throw on errors; the thrown error carries
.code and .status.
try {
await agent.createResource({ amount: 0.05 });
} catch (e) {
console.error(e.code, e.status, e.message); // e.g. 'uxc_agent_not_activated' 402
}Status codes you will see:
| Status | Meaning | SDK behavior |
|---|---|---|
| 200 | success | returns response |
| 202 | pending (x402 verified, awaiting on-chain settlement) | returns response with status: 'pending' (does NOT throw) |
| 402 | payment required / agent not activated | throws, .status = 402 |
| 410 | challenge expired | throws, .code = 'uxc_challenge_expired' |
| 429 | rate limited | throws, .status = 429 |
verify(challengeId, hash) returns { settled: true } on success,
{ status: 'pending', confirmations, required } while the payment awaits on-chain confirmation, or
{ status: 'underpaid' }. An expired challenge throws with .code === 'uxc_challenge_expired'
(HTTP 410 Gone) - catch on .code:
try {
const r = await buyer.verify(challengeId, txHash);
} catch (e) {
if (e.code === 'uxc_challenge_expired') { /* re-fetch accepts[] and retry */ }
else throw e;
}Non-custodial: the platform never holds funds - every payTo is the seller's own wallet, and this client never sees your wallet keys.
