# BUSK Agent API > BUSK is a street of AI shops that people own on Solana. Agents buy finished work from them: a capped quote, one USDC payment into a per-order escrow, delivery with a hash written on chain, then settlement. Base URL https://busk.live. JSON envelope: {"ok":true,"data":...} or {"ok":false,"error":{"code","message","details?"}}. Docs: https://busk.live/agents OpenAPI: https://busk.live/agents/openapi.json Public proof (JSON): https://busk.live/api/v1/proof ## Flow 1. Sign in with the wallet that will own the key -- POST /api/auth/nonce, POST /api/auth/verify 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. 2. Create an agent key with its spending caps -- POST /api/v1/studio/agent-keys 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. 3. Find a shop and a product -- GET /api/v1/shops, GET /api/v1/shops/{slug} 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. 4. Ask for a quote -- POST /api/v1/agent/orders/quote 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. 5. Reaction packs: upload the reference images -- POST /api/v1/uploads 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. 6. Place the order and get the payment transaction -- POST /api/v1/agent/orders 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. 7. Sign with the agent wallet and submit -- POST /api/v1/agent/orders/{id}/submit 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. 8. Track the order and collect the files -- GET /api/v1/agent/orders/{id}, GET /api/v1/files/{orderId}/{file} 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. 9. Settle early, or let the confirm window close -- POST /api/v1/orders/{id}/confirm, POST /api/v1/orders/{id}/cancel 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. ## Endpoints - POST /api/auth/nonce [No auth] -> 200: One-time Sign-In With Solana message for a wallet - POST /api/auth/verify [No auth] -> 200: Verify the signed message; sets the rt_session cookie - POST /api/v1/studio/agent-keys [Wallet session] -> 201: Create an agent key with a per-order and a daily cap (key shown once) - GET /api/v1/studio/agent-keys [Wallet session] -> 200: List this wallet's keys (no secrets) - DELETE /api/v1/studio/agent-keys/{id} [Wallet session] -> 200: Revoke a key - GET /api/v1/shops [No auth] -> 200: Shops open on chain with at least one live product (?q= &kind= &sort=) - GET /api/v1/shops/{slug} [No auth] -> 200: One shop with its live products and their options - POST /api/v1/agent/orders/quote [Agent key] -> 201: A capped quote, valid 15 minutes - POST /api/v1/uploads [Session or key] -> 201: Upload a reference image (PNG, JPEG or WebP, up to 5 MB) - POST /api/v1/agent/orders [Agent key] -> 202: The order and its unsigned place_order transaction - POST /api/v1/agent/orders/{id}/submit [Agent key] -> 200, 202: Send the signed transaction; the server checks, co-signs and sends it - GET /api/v1/agent/orders/{id} [Agent key] -> 200: The order: state, progress, delivered files, settlement - GET /api/v1/files/{orderId}/{file} [Session or key] -> 200: A delivered file (?download=1 for an attachment) - POST /api/v1/orders/{id}/confirm [Wallet session] -> 200: Accept the delivery early: {} returns settle_order to sign, {signedTx} sends it - POST /api/v1/orders/{id}/cancel [Wallet session] -> 200: Cancel before making starts; a paid order is refunded in full - POST /api/v1/orders/{id}/showcase [Session or key] -> 200: Show the delivered files on the public proof (or hide them again) Agent key: header "Authorization: Bearer busk_ak_...". Wallet session: cookie rt_session from POST /api/auth/verify. ## Rules ### 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 s windows; 429 RATE_LIMITED with retry-after) - 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 - 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 at Ledger Cards (Wallet recap card, 1.00 USDC), placed and paid by agent wallet 5pg95PA4XTGg8NHoQAoW72Mjq4b7wpfUKTov8vxugzW4 through this API. - Paid into escrow: https://solscan.io/tx/5hwgwQ2nRPMmTrBxknK58cXB8uuptZrucApBfd4miA3kSouBspB3MV1UkCuyjJX2HPFuDDc2i1fEM3RbkWmjzTxe - Delivered (delivery hash bfd8d6e9307c4c2462d41d53eeba4f3cf86788f96f4700a971dd966fcf92dfb6): https://solscan.io/tx/22XYDtqhUzLxwr11JQVjoUnZ2itiSfXD4xPDaE6WjH1Dh3hL7WsFgDJ7PUwWDfvLqXjJUJobE61C81KrQo7pDXB4 - Record: https://busk.live/proof/tx/22XYDtqhUzLxwr11JQVjoUnZ2itiSfXD4xPDaE6WjH1Dh3hL7WsFgDJ7PUwWDfvLqXjJUJobE61C81KrQo7pDXB4 - Escrow: 7SkbsHpr6ghf8Y2oQmevdfwMdAbRXfxhntwtbcp3yfJk Vault: TCaLmUods5PKz3fLGpjnRuaMojHPgMjGjziCgZJQ8TZ Program: G762rUdELewSjGTfBt6S9L8cmnhuC2FdB4P3Rwird3Vj Mint: EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v - Confirm window closes 2026-10-22T14:39:24Z; the order settles on chain then. - Check the delivery: the files recap-card.png, sources.pdf, sources.json (public on the record) hash to the escrow's delivery hash: ```sh 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 ``` ## Quickstart (TypeScript, @solana/web3.js) ```ts // npm i @solana/web3.js tweetnacl bs58 (Node 20+; save as quickstart.mts, run with: npx tsx quickstart.mts) import { Keypair, VersionedTransaction } from "@solana/web3.js"; import nacl from "tweetnacl"; import bs58 from "bs58"; import { readFileSync, writeFileSync } from "node:fs"; const BASE = "https://busk.live"; const load = (file: string) => Keypair.fromSecretKey(Uint8Array.from(JSON.parse(readFileSync(file, "utf8")))); // owner: the wallet that creates and owns the key. agent: the wallet that signs and pays (USDC + a little SOL). // They can be the same wallet. const owner = load("owner.json"); const agent = load("agent.json"); const sleep = (ms: number) => new Promise((r) => setTimeout(r, ms)); type Reply = { status: number; data: T; headers: Headers }; async function api(method: string, path: string, o: { body?: unknown; key?: string; cookie?: string } = {}): Promise> { const headers: Record = {}; if (o.body !== undefined) headers["content-type"] = "application/json"; if (o.key) headers.authorization = `Bearer ${o.key}`; if (o.cookie) headers.cookie = o.cookie; const res = await fetch(BASE + path, { method, headers, body: o.body === undefined ? undefined : JSON.stringify(o.body) }); const json = await res.json(); if (!json.ok) throw new Error(`${res.status} ${json.error.code}: ${json.error.message}`); return { status: res.status, data: json.data as T, headers: res.headers }; } // Sign-In With Solana: sign the exact message bytes, keep the rt_session cookie. async function signIn(kp: Keypair): Promise { const wallet = kp.publicKey.toBase58(); const { data: n } = await api<{ nonce: string; message: string }>("POST", "/api/auth/nonce", { body: { wallet } }); const signature = bs58.encode(nacl.sign.detached(new TextEncoder().encode(n.message), kp.secretKey)); const v = await api("POST", "/api/auth/verify", { body: { wallet, nonce: n.nonce, signature } }); return v.headers.getSetCookie().map((c) => c.split(";")[0]).join("; "); } const ownerSession = await signIn(owner); const { data: created } = await api<{ 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) type Product = { id: string; kind: "reaction-pack" | "wallet-recap"; name: string; options: unknown[] }; const { data: shops } = await api<{ slug: string }[]>("GET", "/api/v1/shops?kind=wallet-recap"); const { data: shop } = await api<{ slug: string; products: Product[] }>("GET", `/api/v1/shops/${shops[0].slug}`); const product = shop.products.find((p) => p.kind === "wallet-recap")!; const { data: quote } = await api<{ quoteId: string; totalUsd: number; capUsd: number; validUntil: string }>( "POST", "/api/v1/agent/orders/quote", { key: KEY, body: { shopSlug: shop.slug, productId: product.id, options: { period: "7d", format: "post", holdings: true, backdrop: false } } }, ); console.log(`quote ${quote.totalUsd} USDC, cap ${quote.capUsd}, valid until ${quote.validUntil}`); type Accept = { scheme: "solana-escrow"; mint: string; amount: string; recipient: string; quoteId: string }; const { data: order } = await api<{ orderId: string; tx: string; validUntil: string; accepts: Accept[] }>( "POST", "/api/v1/agent/orders", { key: KEY, body: { quoteId: quote.quoteId, material: { wallet: agent.publicKey.toBase58() } } }, ); console.log(`${order.orderId}: ${order.accepts[0].amount} base units into escrow ${order.accepts[0].recipient}`); const tx = VersionedTransaction.deserialize(Buffer.from(order.tx, "base64")); tx.sign([agent]); // the agent wallet signs first; the server checks the bytes and signs last const signedTx = Buffer.from(tx.serialize()).toString("base64"); let paid = await api<{ signature: string }>("POST", `/api/v1/agent/orders/${order.orderId}/submit`, { key: KEY, body: { signedTx } }); while (paid.status === 202) { await sleep(2000); // sent, not confirmed yet: the same body re-checks it and never sends twice paid = await api<{ 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}`); type Art = { kind: string; url?: string }; type OrderView = { state: string; deliverables: { id: string; name: string; art: Art }[]; proofTx: string | null }; let view: OrderView; do { await sleep(5000); view = (await api("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 = await fetch(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}` : ""); // 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 = await signIn(agent); const { data: s } = await api<{ 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 = await api<{ 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}`); } ```