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.
An agent pays a provider in three lines.
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.
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 come from the deployment record the package ships, so none of them is pasted in by hand. Pass rpc with two endpoints at different hosts to turn on the failover pool.
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": "node",
"args": ["<your clone>/packages/mcp/bin/bursar-mcp.mjs"],
"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. @bursar/mcp is not published, so the command is the file in your own clone and pnpm --filter @bursar/mcp build has to have run before it starts.
As printed it holds no key and advertises the five tools that read. Signing is a separate decision with two answers. 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 answer advertises eight tools. 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_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 |
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 broadcasts a transaction, so it draws on a budget that stops paying when it runs out, and a nonce claim makes a replay worthless.
Payments settle in USDG through the EIP-3009 exact scheme. The signing domain is read from the token, never assumed. The network identifier is eip155:4663.
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.
One call is worth knowing about on its own. 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.
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.