Your agent asks a shop for a capped quote, signs one Solana transaction that holds the USDC in escrow, and collects the files when the shop delivers. Settlement runs on chain, under the same rules as an order from a person.
Your code talks to BUSK over HTTPS. Money moves only through the BUSK orders program: from the agent wallet into the order's own escrow, and out of it to the shop at settlement or back to the agent wallet on a refund.
BUSK to Your agent: 202 place_order txaccepts: mint, amount, escrow
Your agent to Your agent: sign at index 0the agent wallet pays the fee
Your agent to BUSK: POST .../submitsignedTx
BUSK to Solana: co-sign and sendUSDC into the order's vault
BUSK to BUSK: the shop makes itinside the quoted cost
BUSK to Solana: mark_delivereddelivery hash, 72 h window opens
Your agent to BUSK: GET order, filessame Bearer key
BUSK to Solana: settle_orderon your confirm, or after 72 h
Quickstart
Every call below is shown in TypeScript (@solana/web3.js, tweetnacl, bs58) and curl. The TypeScript steps run in order as one script; steps 1 to 4 send no transaction. You need Node 20 or later and an agent wallet that holds the quoted USDC plus a little SOL for the network fee and the escrow rent, which comes back to it.
success { "ok": true, "data": ... }
failure { "ok": false,
"error": { "code": "QUOTE_EXPIRED", "message": "This quote expired. Ask for a new one." } }
01
Step 1: Sign in with the wallet that will own the key
Keys are made by a wallet session. Ask for a one-time Sign-In With Solana message, sign its exact bytes with the wallet's ed25519 key and send the signature back (base58 or base64). The answer sets an rt_session cookie, valid 7 days. Signing in sends no transaction and costs nothing.
The message names the site it was made for, so send both calls to the same host. A nonce is good for 5 minutes and one use.
BUSK=https://busk.live
curl -s -X POST $BUSK/api/auth/nonce -H'content-type: application/json' \
-d'{"wallet":"<OWNER_WALLET>"}'# sign data.message (UTF-8 bytes) with the wallet key, then:curl -s -c cookies.txt -X POST $BUSK/api/auth/verify -H'content-type: application/json' \
-d'{"wallet":"<OWNER_WALLET>","nonce":"<nonce>","signature":"<base58 signature>"}'
200POST /api/auth/nonce, recorded on busk.live
{
"ok": true,
"data": {
"nonce": "3e2f77e3c2bb6e669cb65ee961d88999",
"message": "busk.live wants you to sign in with your Solana account:\n3XhfkLat9rwbY9UxgsbFqLvGsXZwgEzPfwDokTo8FKkm\n\nSign in with your wallet. This request does not trigger a transaction or cost any fees.\n\nURI: https://busk.live\nVersion: 1\nNonce: 3e2f77e3c2bb6e669cb65ee961d88999\nIssued At: 2026-10-19T19:05:15.800Z\nExpiration Time: 2026-10-19T19:10:15.800Z",
"expiresAt": "2026-10-19T19:10:15.800Z"
}
}
02
Step 2: Create an agent key with its spending caps
POST/api/v1/studio/agent-keysWallet session
Every key carries two caps in USD: perOrderUsd (0.1 to 1,000) and dailyUsd (at least the per-order cap, up to 10,000 per UTC day). A key without caps is refused. Name an agentWallet to bind the key to the wallet that pays; leave it out and every quote and order request names its own agentWallet instead.
The key (busk_ak_...) is returned once and only its sha256 is stored. A wallet holds up to 10 active keys. Revoke one with DELETE /api/v1/studio/agent-keys/{id}; it stops working at once.
const { data: created } = awaitapi<{ id: string; key: string }>("POST", "/api/v1/studio/agent-keys", {
cookie: ownerSession,
body: { label: "recap agent", perOrderUsd: 5, dailyUsd: 20, agentWallet: agent.publicKey.toBase58() },
});
const KEY = created.key; // shown once: keep it as a secret (only its sha256 is stored)
curl -s -b cookies.txt -X POST $BUSK/api/v1/studio/agent-keys -H'content-type: application/json' \
-d'{"label":"recap agent","perOrderUsd":5,"dailyUsd":20,"agentWallet":"<AGENT_WALLET>"}'export BUSK_KEY=busk_ak_... # from data.key
201recorded on busk.live, key value withheld (that key is revoked)
The street lists shops that are open on chain with at least one live product. Filter with kind=reaction-pack or kind=wallet-recap, search with q, sort by orders, newest, price or speed.
A shop's products carry their option list: each option has an id, a type (choice, toggle or count), its price (addUsd on a choice or toggle, unitUsd per unit on a count) and its default. Send option ids and values in the quote; anything left out takes the default.
The server prices the product from the shop's current price and the options you send, and returns the total, the cap and the split between generation cost, platform fee and the shop. The cap equals the total: the escrow is funded with exactly that amount and nothing more is ever charged.
A quote is valid for 15 minutes, can be used once, and only by the key that asked for it. Pro and Max shops take an optional presetId for one of their brand presets.
Step 5: Reaction packs: upload the reference images
POST/api/v1/uploadsSession or key
A reaction pack is drawn from 1 to 3 reference images of a mascot you own or license. Upload each one as multipart form data (field file, PNG, JPEG or WebP, up to 5 MB) with the same agent key; the order may only use uploads made by that key.
The material for a reaction pack is { refs, rightsConfirmed: true, reactions? } with up to one reaction label per sticker (letters, digits, space and ! ? ' -, 1 to 24 characters). A wallet recap card takes { wallet } instead: any public Solana wallet.
// Reaction packs only: upload 1-3 reference images with the same key first.const form = newFormData();
form.append("file", newBlob([readFileSync("mascot.png")], { type: "image/png" }), "mascot.png");
const up = awaitfetch(BASE + "/api/v1/uploads", { method: "POST", headers: { authorization: `Bearer ${KEY}` }, body: form });
const { data: upload } = (await up.json()) as { data: { ref: string; sha256: string; bytes: number } };
// material for a reaction pack: { refs: [upload.ref], rightsConfirmed: true, reactions: ["gm", "wagmi"] }
curl -s -X POST $BUSK/api/v1/uploads -H"authorization: Bearer $BUSK_KEY" -F file=@mascot.png
201shape from app/api/v1/uploads/route.ts
{
"ok": true,
"data": { "ref": "up-<uuid>/<sha256>.png", "sha256": "<sha256 of the file>", "bytes": 48213 }
}
06
Step 6: Place the order and get the payment transaction
POST/api/v1/agent/ordersAgent key
The answer is 202 with the order id, an unsigned place_order transaction for the agent wallet, and an accepts entry that states the payment: scheme solana-escrow, the USDC mint, the amount in base units (6 decimals) and the escrow account that will hold it.
The key's caps are checked here: a total above perOrderUsd, or above what is left of dailyUsd for the UTC day, is refused with 403 AGENT_CAP. The order counts toward the daily cap when it is created. Calling again with the same quoteId while the order is unpaid returns the same order with a fresh transaction.
The agent wallet is account 0: it pays the network fee and the rent of the escrow and its vault (both come back to it when the order settles or is refunded), and it signs first. The server checks that the instructions, accounts and blockhash are the ones it issued, adds its own signature last and sends the transaction.
Your wallet may add ComputeBudget instructions before or after the block, or a Lighthouse assertion after it. Anything else (a new signer, a new writable account, another blockhash, address lookup tables) is refused with 400 TX_REJECTED.
200 means the escrow is funded. 202 means sent and not confirmed yet: call again with the same body, which re-checks the same signature and never sends a second payment. On TX_FAILED or TX_EXPIRED, POST /api/v1/agent/orders again with the same quoteId for a new transaction.
const tx = VersionedTransaction.deserialize(Buffer.from(order.tx, "base64"));
tx.sign([agent]); // the agent wallet signs first; the server checks the bytes and signs lastconst signedTx = Buffer.from(tx.serialize()).toString("base64");
let paid = awaitapi<{ signature: string }>("POST", `/api/v1/agent/orders/${order.orderId}/submit`, { key: KEY, body: { signedTx } });
while (paid.status === 202) {
awaitsleep(2000); // sent, not confirmed yet: the same body re-checks it and never sends twice
paid = awaitapi<{ signature: string }>("POST", `/api/v1/agent/orders/${order.orderId}/submit`, { key: KEY, body: { signedTx } });
}
console.log("paid into escrow", `https://solscan.io/tx/${paid.data.signature}`);
# sign the base64 transaction with the agent wallet (index 0), then:curl -s -X POST $BUSK/api/v1/agent/orders/BSK-FTM3WG8Q/submit \
-H"authorization: Bearer $BUSK_KEY" -H'content-type: application/json' \
-d"{\"signedTx\":\"$SIGNED_TX\"}"
200the payment signature of BSK-FTM3WG8Q on mainnet
{
"ok": true,
"data": {
"signature": "5hwgwQ2nRPMmTrBxknK58cXB8uuptZrucApBfd4miA3kSouBspB3MV1UkCuyjJX2HPFuDDc2i1fEM3RbkWmjzTxe",
"order": { "id": "BSK-FTM3WG8Q", "state": "order", "buyerKind": "agent", "...": "the full order, as in step 8" }
}
}
08
Step 8: Track the order and collect the files
GET/api/v1/agent/orders/{id}Agent keyGET/api/v1/files/{orderId}/{file}Session or key
Poll the order. Its state moves order (paid into escrow) -> making -> handoff (delivered) -> settled; it can end failed, refunded or cancelled. Each delivered file is a deliverable whose art.url is a same-origin path; fetch it with the same Bearer key.
When the shop delivers, the server writes a delivery hash on chain with mark_delivered: sha256 over the lines name:sha256 of every file, sorted by name and joined with a newline. Hash the files you received the same way to check them against the chain.
type Art = { kind: string; url?: string };
type OrderView = { state: string; deliverables: { id: string; name: string; art: Art }[]; proofTx: string | null };
let view: OrderView;
do {
awaitsleep(5000);
view = (awaitapi<OrderView>("GET", `/api/v1/agent/orders/${order.orderId}`, { key: KEY })).data;
} while (!["handoff", "settled", "failed", "refunded", "cancelled"].includes(view.state));
for (const d of view.deliverables) {
if (d.art.kind !== "asset" || !d.art.url) continue;
const file = awaitfetch(BASE + d.art.url, { headers: { authorization: `Bearer ${KEY}` } });
writeFileSync(d.id, Buffer.from(await file.arrayBuffer()));
}
console.log(view.state, view.proofTx ? `${BASE}/proof/tx/${view.proofTx}` : "");
Delivery opens a 72-hour confirm window. When it closes the server settles the order on chain by itself. To settle sooner, sign in as the agent wallet (step 1 with that wallet) and confirm: {} returns the settle_order transaction, signing it and sending {signedTx} settles at once.
Before making starts, the same session can cancel: an unpaid order is dropped, a paid one is refunded in full to the agent wallet.
// Optional: accept the delivery now instead of waiting for the 72-hour window. Sign in as the agent wallet.if (view.state === "handoff") {
const agentSession = awaitsignIn(agent);
const { data: s } = awaitapi<{ tx: string }>("POST", `/api/v1/orders/${order.orderId}/confirm`, { cookie: agentSession, body: {} });
const settle = VersionedTransaction.deserialize(Buffer.from(s.tx, "base64"));
settle.sign([agent]);
const done = awaitapi<{ signature: string }>("POST", `/api/v1/orders/${order.orderId}/confirm`, {
cookie: agentSession,
body: { signedTx: Buffer.from(settle.serialize()).toString("base64") },
});
console.log("settled", `https://solscan.io/tx/${done.data.signature}`);
}
# with a session cookie of the agent wallet (step 1, signed by the agent wallet):curl -s -b agent-cookies.txt -X POST $BUSK/api/v1/orders/BSK-FTM3WG8Q/confirm \
-H'content-type: application/json' -d'{}'# sign data.tx with the agent wallet, then:curl -s -b agent-cookies.txt -X POST $BUSK/api/v1/orders/BSK-FTM3WG8Q/confirm \
-H'content-type: application/json' -d"{\"signedTx\":\"$SIGNED_SETTLE_TX\"}"
Rules the counter keeps
Read from the API code and the orders program. They are the same for an agent and for a person.
Quotevalid 15 min
Paid, makingdeadline: turnaround + 24 h
Deliveredconfirm window 72 h
Settledshop paid, vault closed
Escrow
Every order is its own escrow account in the BUSK orders program, with a USDC vault the program controls. place_order moves exactly the quoted total from the agent wallet into the vault.
The escrow stores the quote hash, the generation cost, the fee ceiling (10 %), the delivery deadline and, after delivery, the delivery hash.
On settlement the vault pays the generation cost and the platform fee to the platform account and the rest to the shop's payout account, then closes; its rent returns to the agent wallet.
Caps
Key caps: perOrderUsd and dailyUsd are required on every key and checked on every order (403 AGENT_CAP).
Quote cap: the escrow holds the quoted total, which is also the cap. Generation runs inside the quoted generation cost and the shop's own spend cap.
Fee ceiling: the platform fee is judged at settlement from the shop's tier (10 % Standard, 8 % Pro, 6 % Max) and can never exceed the 10 % written into the escrow.
Clock
A quote is valid for 15 minutes; an order needs at least 1 minute left on it.
The delivery deadline is the product's turnaround plus 24 hours from the moment the order is created.
Delivery opens a 72-hour confirm window (the program's confirm_window_secs, 259200 on mainnet). The buyer may settle earlier; after it closes the server settles.
Refunds
Cancelled before making starts: refunded in full (POST /api/v1/orders/{id}/cancel as the agent wallet).
Generation failed: the server refunds the full amount.
Deadline passed with nothing delivered: the server refunds the full amount, and the program also lets the buyer wallet sign refund_order itself once the deadline has passed.
A refund returns the whole vault balance to the agent wallet's USDC account and the rent to the agent wallet.
Rate limits
Fixed 60-second windows. Over a limit the answer is 429 RATE_LIMITED with a retry-after header and details.retryAfterSec.
Requests
Limit
Every request with an agent key (quote, order, submit, order view, uploads, files)
60 per minute per key
Uploads
20 per minute per IP
Shops, shop pages, proof
240 per minute per IP
Confirm, cancel, showcase
30 per minute per IP
Creating agent keys
10 per minute per IP
Sign-in nonce / verify
20 / 30 per minute per IP
Error codes
Every failure is { ok: false, error: { code, message, details? } } with one of these codes.
Status
Code
When
400
BAD_REQUEST
A field is missing or malformed; the message names the field
400
BAD_MATERIAL
material.refs holds uploads made by another key
400
BAD_TX
signedTx is not a base64 transaction
400
TX_REJECTED
The signed transaction differs from the one issued beyond the allowed additions
401
UNAUTHORIZED
No key, an unknown key or a revoked key (or no session where one is needed)
403
FORBIDDEN
The key is bound to another agent wallet
403
AGENT_CAP
The total is above the key's per-order cap or the rest of its daily cap
403
TIER_REQUIRED
presetId was sent to a Standard shop
404
NOT_FOUND
Unknown shop, product, quote or order, or one this key did not create
409
SHOP_PAUSED
The shop is not taking orders right now
409
PRODUCT_UNAVAILABLE
The product is not on sale right now
409
QUOTE_EXPIRED
Less than a minute left on the quote; ask for a new one
409
QUOTE_USED
The quote already belongs to another order
409
ORDER_PLACED
The order is already paid
409
NO_PENDING_TX
Submit came before the order transaction was issued
409
TX_FAILED
The payment failed on chain (for example another order took the same order id); ask for a new transaction
409
TX_EXPIRED
The blockhash expired before the payment landed; ask for a new transaction
409
NOT_DELIVERED
Confirm before delivery
409
CANNOT_CANCEL
Making has started or the order is past the paid state
413
PAYLOAD_TOO_LARGE
An upload over 5 MB or a body over its limit
429
RATE_LIMITED
Over a rate limit; wait retry-after seconds
503
CHAIN_LAGGING
An account is not readable at the needed slot yet; retry shortly
Proven on Solana mainnet
Agent order BSK-FTM3WG8Q went through every call on this page: an agent key, a quote, a place_order transaction signed by the agent wallet, delivery by the shop with the hash written on chain.
BSK-FTM3WG8QLedger Cards / Wallet recap card / 1.00 USDC / ordered by an agent
Paid into escrow2026-10-19 14:39 UTCSolscanRecord5hwgwQ2nRPMmTrBxknK58cXB8uuptZrucApBfd4miA3kSouBspB3MV1UkCuyjJX2HPFuDDc2i1fEM3RbkWmjzTxe
Check the delivery yourself: the three files on the record hash to the delivery hash in the escrow.
cd "$(mktemp -d)"
for f in recap-card.png sources.json sources.pdf; do curl -sO https://busk.live/api/v1/proof/files/BSK-FTM3WG8Q/$f; done
for f in recap-card.png sources.json sources.pdf; do echo "$f:$(sha256sum $f | cut -d' ' -f1)"; done | head -c -1 | sha256sum
# bfd8d6e9307c4c2462d41d53eeba4f3cf86788f96f4700a971dd966fcf92dfb6