What an agent needs to spend inside a mandate, and what a provider needs to charge one.
Every limit in this system is enforced by the mandate account on chain. A payment outside them does not settle, whatever an agent was told to do and whichever of these interfaces sent it. The SDK signs with an account you pass it. The MCP server holds a key only when you tell it to by name, and hands the work to a signer you run when you would rather it did not.
Read any mandate with no key, then pay from one in three lines.
No key and nothing signed. These are the same reads the console makes.
import { formatUsdg, mandateAccount } from '@bursar/sdk';
const mandate = await mandateAccount('0x8605853aC6A64dA11F4ED0Ff0Ad128961Cc3cd5c');
const { balance, limits, remaining, paused } = await mandate.status();
console.log({
balance: formatUsdg(balance),
perPayment: formatUsdg(limits.perCallCap),
leftThisPeriod: formatUsdg(remaining.daily),
periodResets: remaining.dailyResetsAt,
paused,
});Install it with npm install @bursar/sdk viem, on Node 22 or newer. The source is the public repository. The address above is the live example mandate, so the figures printed are the ones the console shows.
An agent pays from its mandate, inside the limits.
import { mandateAccount, usdg } from '@bursar/sdk';
const mandate = await mandateAccount(ACCOUNT, { account: AGENT_KEY });
const receipt = await mandate.pay({
to: PROVIDER,
amount: usdg('2.50'),
capability: 'gpu.render:1',
});pay reads the limits, opens an escrow lock against the account and returns the lock id with the transaction. The provider is paid when it delivers. If it never does, the deadline returns the money and credits the allowance back. The escrow opens no lock under its smallest payment, which the status page shows, and pay refuses a smaller amount before anything is sent.
Amounts are micro-USD held as bigint, the six decimals the settlement asset uses. usdg('2.50') is 2500000. Transaction fees are paid in ETH, which is a different asset with eighteen decimals of its own, and the two are never added together.
Contract addresses ship with the package, so none is pasted in by hand. Pass rpc with two endpoints at different hosts to turn on the failover pool.
x402: the provider answers 402 with a price, the mandate pays, the call goes through.
const paid = await mandate.fetch('https://api.provider.dev/render', {
method: 'POST',
body: JSON.stringify({ prompt: 'a koi' }),
capability: 'gpu.render:1',
lane: 'mandate',
});
paid.payment?.lock; // the escrow lock the mandate opened for this callWith lane: 'mandate' the account pays the quoted price through the same checks as pay. The daily and monthly windows move by the amount paid, and a call they do not cover is refused on chain before any money moves.
Leave the lane out and the agent's own wallet pays by signing a USDG transfer for the exact price. The SDK checks the mandate's per-call cap, providers and capabilities before signing, but a wallet payment is not counted against the daily or monthly window on chain. Use the mandate lane when those windows have to hold.
The same check the contract will make, without sending anything.
const decision = await mandate.preview({
to: PROVIDER,
amount: usdg('2.50'),
capability: 'gpu.render:1',
});
if (!decision.allowed) {
// decision.reason is 'daily-cap', 'per-call-cap', 'merchant-not-allowed', …
// decision.daily.resetsAt says when a spent window frees up.
}A refused payment throws MandateDeniedError, which carries the same reason and, for a window, the moment it frees up. An agent that reads it can wait instead of retrying into the same refusal.
For an agent that reaches its tools over Model Context Protocol.
{
"mcpServers": {
"mandate": {
"command": "npx",
"args": ["-y", "@bursar/mcp"],
"env": {
"RHC_RPC_PRIMARY": "https://rpc.mainnet.chain.robinhood.com",
"RHC_RPC_FALLBACK": "https://robinhood.drpc.org",
"MANDATE_ACCOUNT": "<the mandate account this server speaks for>"
}
}
}
}One server, one mandate. Every value in angle brackets is one you supply, starting with the account you created in the console. The client fetches the server on first use; nothing is built or cloned.
As printed it holds no key and offers the 7 tools that read. To let it pay, choose one of two ways. Set BURSAR_SIGNER=local and BURSAR_SIGNER_KEY and the key stays in this process, able to sign for the one account above and for three calls on it. Point BURSAR_RELAY_URL at a signer you run instead and the key never reaches the process at all. Either way it offers all 14. Both at once is refused, and so is a key arriving under a name the server was not told to hold, such as AGENT_PRIVATE_KEY or PRIVATE_KEY. The refusal names the variable and the server does not start.
| Tool | What it does | Needs a signer |
|---|---|---|
| mandate_inspect | The caps, what each window has left, when each resets, the approval threshold, the funded balance, and whether the mandate is running. | No |
| mandate_quote_spend | What the mandate would decide about a payment, before making it. Names the limit that would stop it. | No |
| mandate_list_settlements | What this mandate has paid for, newest first, and where each payment stands. | No |
| mandate_get_settlement | One settlement in full, with the next decision and the time it has to be made by. | No |
| mandate_get_dispute | Where a contested payment stands: the phase, the clock on it, and the ruling once there is one. | No |
| mandate_collateral | The collateral posted, what it counts for after the haircut, the debt, and how much more the mandate can draw. | No |
| shielded_pool_status | Whether the shielded USDG pool takes deposits, what it holds, and how much room is left. | No |
| mandate_pay_provider | Locks the amount in escrow against the mandate and hands the job to the provider. | Yes |
| mandate_hire_agent | The same against a brief: the task, what it runs on, and what counts as delivered, published with the payment. | Yes |
| mandate_open_dispute | Contests a settlement and hands the split to the resolver. | Yes |
| mandate_buy_stock | Buys an eligible stock token with the mandate’s USDG, checked against the reference price. | Yes |
| mandate_sell_stock | Sells a stock the owner has released for sale back to USDG in the mandate, checked the same way. | Yes |
| mandate_collateral_deposit | Posts a stock or treasury token from the signer as collateral for the mandate. | Yes |
| mandate_collateral_repay | Repays the mandate’s credit in USDG from the signer. | Yes |
A payment at or above the approval threshold needs consent the account owner signed for that provider, capability and amount. Without one it is refused before it reaches the chain.
What a provider runs to take payment from a mandate over x402.
A provider charging for a call sends the payment it received to POST /verify and POST /settle. Verification reads and costs nothing. Settlement records the payment once, so a replay is worthless, and for a wallet payment it also broadcasts the transfer, drawing on a daily budget that stops when it runs out.
Offer two schemes. escrow is paid by the mandate account itself: the payment names the escrow lock its spend opened, verification checks that lock against your price, address and the request, and you collect by releasing it once the call is served. exact is paid from the agent's wallet with an EIP-3009 USDG transfer that settlement broadcasts. Each wallet payment is checked on its own; the mandate's daily and monthly windows do not count it.
The signing domain is read from the token, never assumed. The network identifier is eip155:4663.
One priced route, paid by agents over x402 and settled through Bursar. One command to deploy.
npm create @bursar/provider my-api
cd my-api && npm install
npx wrangler secret put BURSAR_FACILITATOR_TOKEN
npx wrangler deployThe project it writes has one priced route, POST /render at 0.01 USDG. Set your provider address, the capability and the prices in wrangler.toml. An agent that calls the route without paying gets a 402 with the price; one that pays from its mandate gets the result, and the payment lands in escrow for your address.
An existing Worker takes the same route: install @bursar/provider-worker, wrap the fetch handler, and price routes with BURSAR_PRICES. Routes without a price pass through as before.
import { paymentOf, withBursar } from '@bursar/provider-worker';
export default {
fetch: withBursar(async (request) => {
const payment = paymentOf(request);
return Response.json({ rendered: 'a koi', paidBy: payment?.payer });
}),
};Your address has to be listed on the provider desk first, and the hosted facilitator answers providers that hold a token. Releasing each escrow lock is what pays you: give the worker your provider key as a secret and it releases after every call, or release from the desk.
The worker that delivers and collects.
It watches the escrow for locks naming your address, runs the capability the payer asked for, and calls release with a commitment to what it delivered. That pays you in the same transaction. It signs your calls only, against funds the escrow already holds, and it cannot move a payer's money.
finalizeRelease writes the reputation counter that sets the largest single job a payer may lock against you, and it reverts until the payer's time to contest has run out. Anyone can make it and nobody is obliged to, so a provider that never finalises holds its own ceiling down without seeing why. The sidecar makes it. So does the provider screen, one job at a time.
What happens when a payer contests a job before paying for it.
A disputed payment stays in escrow until bonded resolvers rule on it. All three resolvers on the registry are operated by Bursar and follow a published policy: what counts as delivered, the evidence a provider can send, the deadlines, and when Bursar may override. The reasons behind each ruling are published once the votes are revealed.
Once the reveal window closes, anyone can settle the vote. If too few resolvers revealed, the payment goes back on hold for the provider with a new deadline and the contest bond is returned. A payout the token issuer blocks is kept by the escrow for its recipient, and anyone can claim it for them once the address can receive again.
A provider sends signed delivery evidence from its desk, or has the sidecar send it by setting SIDECAR_EVIDENCE_URL to https://app.bursar.world/api/evidence.
Borrowing against posted stock and treasury tokens.
A mandate created in the collateral lane can post eligible stock and treasury tokens and borrow USDG against them, within the credit limits the haircut page reads from chain. When a payment needs more USDG than the mandate holds, it borrows the difference in the same transaction. Borrowing and withdrawals are checked at the after-hours haircut, whatever the time. Prefunded mandates cannot borrow.
Gas measured on real transactions, priced at what Robinhood Chain has been charging.
Fund a mandate once and debit against the on-chain limit per call. The fee is paid in ETH and is the same size whatever the payment is worth, so what a mandate costs to run does not grow with what it spends.