Skip to main content
A shared budget is one limit that several agents draw on. The budget holder sets it in Budgets or with admin.spending. Agents read it and check purchases against it with conto.spending. These methods need SDK 0.8.0 or later.
This is separate from a budget request, which one agent asks for and spends.
A budget stops several payment paths for the agents it covers, even while it is paused. Read what a budget stops and try a budget first on one agent with a Tempo Testnet wallet.

Create a budget

Use an organization API key with policies:write. Amounts are exact decimal strings.
A budget created with AGENT or AGENTS reads back with scope set to MEMBERS, and participantIds still lists its agents. A new budget counts from when it is saved. A payment or purchase already in progress for an agent it covers doesn’t count against it. POST /api/spending/budgets/impact returns how many are in progress and their total before you save.

More rules

SDK 0.8.0 and later type the fields above. These also work on POST /api/spending/budgets and PATCH /api/spending/budgets/{id}: Category lists use what Conto observes about a purchase, such as the card network’s merchant code, the merchant’s domain and the payment method. They never use a label the agent supplies. With a category list set, a purchase Conto cannot place in a category is refused.

Which budgets apply

Each budget that applies must pass, and each one is charged. The agent never names a budget. Two things decide which budgets apply: whether the budget covers the agent, and, with sourceMode set to ATTACHED, whether it lists the card or wallet paying. A card or wallet that a budget lists spends only through a budget that lists it. An agent no such budget covers is refused with BUDGET_SOURCE_NOT_ATTACHED, on the paths a budget stops as well. An agent whose budgets all list other cards and wallets gets the same refusal.

Currency

A budget covers purchases in its own currency, so set it to what the wallets pay in: USDC on Base, USDC.e on Tempo or pathUSD on Tempo Testnet. A USD, USDC, USDC.e or USDT budget counts those four and pathUSD at $1 each. Nothing converts, and other currencies stay separate. A pathUSD budget covers only pathUSD. The levels are workspace, members and the task a purchase names. A purchase counts against each budget that covers it at every level, and it must fit in each one. The exception is a pathUSD purchase: where a level has a pathUSD budget, only pathUSD budgets at that level count it, so test money stays off the dollar budgets beside it. Connected card draws, saved-card checkouts and checkout checkpoints are in USD, so a level with only pathUSD budgets refuses them. A managed wallet payment or connected card draw is refused, whatever the budget’s state, when a level has budgets for that agent and none covers its currency. A budget’s currency cannot be changed. A checkout checkpoint, including checkout.getLinkCard(), and a guarded browser checkout (runGuardedCheckout()) hold spend against each budget that covers the agent. The hold is written when the checkout begins, after the agent’s own limits pass and before Stripe Link is asked for a card. When the checkout is reported paid, the purchase settles on the budget. When it is cancelled or closed unpaid, the budget gets the amount back. A confirmed checkout with no outcome stays held until someone records what happened. A linked buyer’s checkout gives an owner workspace’s budget its amount back only as Purchases and evidence describes. A checkout paying with Stripe Link counts as STRIPE_LINK, and one paying any other way counts as CHECKOUT. Merchant and category lists use the checkout page’s domain, not the merchant ID the agent sends. The agent runs the checkout, so Conto declines approval at the limit. A payment the agent makes without asking Conto is not counted.

What a budget stops

While a budget exists, it stops these paths for every agent it covers:
  • x402 and MPP payments
  • External wallet payments
  • Agent-to-agent payments the covered agent makes. It can still receive them
  • Payment grants
  • Card checkout that Conto completes for an approved cart (merchant:purchase)
  • Hosted Stripe Link
  • Policy checks on customer-owned cards (cards.approve())
A paused or expired budget still stops them. While paused and after its end date, a budget also refuses the managed wallet payments, connected card draws, saved-card checkouts, checkout checkpoints and guarded browser checkouts it covers, for those agents. preview() and connectedCards.draw() report BUDGET_PAUSED while it is paused and BUDGET_EXPIRED after its end date. A budget stops and refuses nothing until its start date. Approving a task creates a budget for that task. A task’s budget stops nothing on other paths. It applies only to a managed wallet payment or connected card draw that names the task, and it refuses that payment or draw while it is paused. Agents no budget covers are not affected, except a buyer linked to an owner workspace. The paths in the list above never open for a linked buyer. They fail with reasonCode LINKED_BUDGET_REQUIRED until a budget in that workspace covers it, and with EXECUTION_PATH_NOT_CONNECTED after that. A revoked link fails with BUYER_LINK_REVOKED. With no budget in that workspace, the buyer’s managed wallet payments, connected card draws, saved-card checkouts and checkout checkpoints are refused too. See linked buyers.

Change or end a budget

updateBudget() needs the budget’s current version. It can change the amount, pause or resume the budget with isActive, or move periodEnd to any time in the future, earlier or later. The API also takes the fields in more rules. Currency, period and start date cannot be changed. A task’s budget can only change its active state. Pausing stops new purchases on the budget and does not lift what it stops on other payment paths. A purchase already in progress keeps its hold when a change would now refuse it. An organization API key with policies:write, an Owner or an Admin can make any change. A Manager who owns the budget can tighten it, and any other Manager’s change fails with reasonCode BUDGET_CHANGE_NOT_ALLOWED. Raising or widening a budget needs an Owner or Admin. Removing a member, detaching a card or wallet, or starting a list of cards and wallets counts as widening, because each frees something from the limit. Organizations can require a second approver for creating, widening and deleting budgets. To end a budget, delete it in Budgets or call deleteBudget(). It then stops applying on every path, and its purchases stay in history. A linked buyer left with no budget in that workspace then fails with reasonCode LINKED_BUDGET_REQUIRED.
deleteBudget() fails with reasonCode BUDGET_HAS_HOLDS while a purchase holds spend against the budget, and TASK_BUDGET for a task’s budget, which ends by closing the task.

Check a purchase

Use an agent SDK key. Nothing on conto.spending can raise a limit, approve a task, or release a hold.
preview() needs a managed wallet linked to the agent, and that wallet’s stablecoin as currency. It holds nothing and approves nothing, so keep the request, approval and execute flow. A purchase the budgets would refuse throws a ContoError. payments.request() can return APPROVED for a purchase a budget refuses. payments.execute() then throws status 409 and code SHARED_BUDGET_BLOCKED. With autoExecute, the request stays APPROVED and nothing is thrown. To see why, call preview() with the same taskRequestId and purchaseKey. context.purchaseKey identifies one purchase. When context.invoice has vendorId and id, Conto builds the key from those two values and ignores purchaseKey. A key cannot be used again, so a second payment of that invoice is refused whatever purchaseKey it names. preview() takes no invoice, so it cannot show this refusal. Use a new key for a new attempt. The exception is a managed wallet payment that Conto saw go nowhere: its transfer reverted onchain and the chain has finalized that revert, or it stopped before Conto sent it and was then declined, cancelled or expired. It also can’t have been flagged for reconciliation. Then a new payment can use the key, and the earlier purchase still shows it. Until a revert is final, a payment that uses the key is refused like any second payment. That takes about a minute on Tempo, Polygon and Solana, about 20 minutes on Ethereum, and about 25 minutes on Base and Arbitrum. If the chain can’t show the revert final, the key stays taken. For a linked buyer, the purchase in the owner workspace has to qualify too. A key can’t start with released: or linked:.

Tasks

When a budget requires a task, the agent asks for one and the budget holder decides.
Once the task is approved, pass its id as context.taskRequestId on payments.request() from a managed wallet, or as taskRequestId on connectedCards.draw(). A buyer linked to an owner workspace cannot use a task. A purchase that names one is refused with LINKED_TASK_UNSUPPORTED, so a linked buyer cannot spend from an owner budget that requires a task. A self-custody wallet cannot use a task. When a budget covers the agent, payments.request() throws status 409 with reasonCode EXECUTION_PATH_NOT_CONNECTED. When none does, a request that names a task ends in DENIED. payments.approve() refuses the task fields. A draw a budget refuses keeps its single-purchase authorization, unless another purchase takes the last of the budget at that same moment. requestTask() also takes permittedMerchants and permittedSourceIds, up to 100 each. A purchase outside them fails with MERCHANT_NOT_AUTHORIZED or SOURCE_NOT_AUTHORIZED. decideTask() also takes amount to approve a different amount, and participantIds to let other agents draw on the task. The list must include the agent that asked, or the call fails with INVALID_PARTICIPANTS.

Purchases and evidence

purchases() lists this agent’s purchases against budgets in its own organization, not Commerce conto.purchases. It is empty when no budget has applied to this agent’s purchases. A buyer linked to an owner workspace has no row for a purchase that only an owner budget covered. The owner reads those with admin.spending.purchases(). attachEvidence() takes a PDF, PNG or JPEG of at most 2 MiB, with up to 10 files per purchase. Set the Blob’s type, for example new Blob([bytes], { type: 'application/pdf' }). A Blob with no type, or with a type that does not match the file’s first bytes, fails with reasonCode INVALID_FILE. Conto does not take the amount, currency or merchant from the file. The budget holder enters them with reviewEvidence(). exportPurchases() returns CSV bytes and fails with reasonCode EXPORT_TOO_LARGE above 10,000 purchases. An uncertain outcome keeps the spend held until someone records what happened. An Owner can do this in Budgets. An organization API key with spending:reconcile can do it with reconcile(). Recording the outcome of a connected card draw moves its purchase too. A charge settles it at the charged amount, and a draw that was not charged releases the hold. A store cancelling a reported order refunds the purchase, or releases it if it never settled. A settlement already recorded on the purchase refuses an outcome that contradicts it. For a linked buyer, the purchase in the owner workspace waits for an Owner of that workspace, for card draws and checkouts alike. A buyer’s record, or anyone else’s, that nothing was charged marks it UNKNOWN and keeps the hold. A recorded charge settles it, and any difference from the hold stays held. Until an Owner records what happened in Budgets, the hold counts against the budget and stops deleteBudget() and reconnecting that buyer. Conto releases the hold itself only when it saw that nothing could be charged, such as the checkout provider refusing the card, or a checkout closing before Conto cleared it to pay and before Stripe Link was asked for a card. A record that leaves the owner’s budget short raises a LINKED_BUDGET_NEEDS_OWNER alert in the owner workspace, including a store cancelling a purchase the owner has as settled. Owners and the billing address get it by email, under the workspace’s alert email settings. It closes when a later record leaves nothing held, such as an Owner recording the outcome in Budgets.
kind is RELEASED, SETTLED, REFUNDED or UNKNOWN. SETTLED needs settledAmount and REFUNDED needs refundedAmount, each the verified total so far. Set final: true on either one to release the rest of the hold. RELEASED always releases it. evidenceRef has at least 10 characters. eventKey has at least 8 characters, and sending it again with different values fails with reasonCode EVIDENCE_CONFLICT. A purchase with settled spend cannot be RELEASED and fails with SETTLED_PURCHASE, so record a refund instead.

Errors

Most budget errors have code POLICY_VIOLATION, so read reasonCode on ContoError and ContoAdminError. Some also have nextActions. reasonCode can have other values, so handle one you do not recognize. These calls report a budget differently:
  • A refused checkout.begin(), getLinkCard() or replaceLinkCard() closes the checkpoint, so a retry with the same idempotencyKey throws CheckoutCheckpointClosedError. Use a new key. SDK 0.8.2 and later return each reasonCode above on these calls.
  • payments.execute() and payments.pay() have code SHARED_BUDGET_BLOCKED and no reasonCode.
  • connectedCards.draw() returns DENIED with a code in reasonCodes. See connected cards.
A VALIDATION_ERROR on a spending call usually lists the fields in fieldErrors. It does not when the call got a field it does not accept, such as periodStart or currency on updateBudget(). A payments.request() validation failure never has fieldErrors.

Method reference

tasks() and purchases() page with nextCursor.

conto.spending

Takes an agent SDK key. budgets() also returns buyerConnection ({ revoked, participantId }) when the agent is a shopping buyer linked to an owner’s workspace. Linking happens in the dashboard and the buyer’s chat, not in the SDK.

admin.spending

Takes an organization API key. An admin SDK key cannot change budgets, decide tasks, or reconcile. The Standard preset for organization API keys does not include spending:reconcile. Use the Admin preset, which only an Owner can create, or name the scope when you create the key with POST /api/api-keys.