Documentation
Developer docs
How to give an agent a wallet, a budget and a way to ask for money, and what stops it when it asks for too much.
Quickstart
From nothing to a first payment
You need a wallet to own the agent and some USDG or ETH to fund it. The first three steps happen in the dashboard and your wallet. The last one is code.
1. Create an agent
Open Create Agent Wallet and connect the wallet that will own the agent. AgentSpend works out a smart account address with your wallet as the root signer. The address commits to your wallet, so whoever relays the deployment cannot change who controls it.
2. Fund it
Send USDG or ETH to the agent's wallet address. Funds stay in your smart account, and the owner keeps ultimate control.
3. Issue a session key
Set the budget, the vendors and contracts the agent may use, the assets it may spend and an expiry date. Your wallet signs all of it as one EIP-712 message, and the rules go onchain.
The dashboard then shows the agent's credential once, and AgentSpend stores only its hash. It starts with as_live_, or as_test_ on testnet and local chains. Keep it with your agent's other secrets.
4. Make the first payment
npm install @agentspend/sdkimport { AgentSpend } from "@agentspend/sdk";
const spend = new AgentSpend({
agentId: process.env.AGENTSPEND_AGENT_ID,
sessionKey: process.env.AGENTSPEND_API_KEY,
baseUrl: process.env.AGENTSPEND_API_URL,
});
const payment = await spend.pay({
vendor: "gpufarm",
amount: "15",
asset: "USDG",
purpose: "compute",
});
if (payment.decision === "APPROVED") {
const done = await spend.waitForExecution(payment.intentId);
console.log(done.status, done.txHash); // EXECUTED 0x…
}sessionKey is not a chain key. Despite the name, it is the agent's scoped API credential. Agents hold no chain key in this model. The constructor refuses anything that does not look like a credential, so a private key passed by mistake is never sent.pay returns the policy's decision. A refusal is an answer, not an error, so nothing is thrown. Always read decision.
APPROVED- Queued for execution, not finished yet.
waitForExecutiontells you when it lands. REQUIRES_APPROVAL- A person has to decide. The request stays open for 24 hours.
BLOCKED- The policy refused. Nothing was signed or spent, and sending it again will not change the answer.
Concepts
The five ideas behind every request
Smart account
Each agent gets its own ERC-4337 smart account, built on ZeroDev Kernel v3.1 on Robinhood Chain. Your wallet is the root signer. The AgentSpend policy module is installed next to it and is the only thing that can accept a session key.
Funds stay in your smart account, and the owner keeps ultimate control. You can call the account directly from your wallet at any time, with no AgentSpend service involved.
Session key
A session key is a signing key with a policy attached. AgentSpend's signing service generates it, stores it encrypted, and uses it only after a request has passed the policy. Your agent never sees it.
The agent holds a scoped API credential instead, which is what the SDK's sessionKey option takes. A credential reaches one agent's wallet, budget, approvals and expenses. It holds no chain key, so a stolen credential can only ask.
- Every session key has an expiry date.
- Rotating a key swaps the signer and nothing else. Expiry, limits, rules and budget counters stay as they are.
- Freezing an agent revokes every key it has, onchain, with the key's own last signature, so no wallet prompt is needed. The key material is then destroyed.
- Unfreezing never restores spending. The owner has to issue a new key, with a new signature.
Policy
A policy says what an agent may do. Part of it is enforced onchain by the module, against rules the owner signed. All of it is enforced by the policy engine before anything is signed.
- Onchain and in the engine
- Validity window, per-transaction, daily, weekly and monthly caps, transactions per day, gas per day, allowed contracts and selectors, allowed assets, allowed and blocked recipients, and whether arbitrary transfers or unlimited approvals are allowed.
- In the engine only
- Live USD valuation, simulation, slippage limits, blocked contracts, the approval threshold, how many approvers must agree, group budgets, and whether an unknown recipient is blocked or sent to a person.
Tightening a policy applies at once with no signature, because it can only reduce what the agent may do. The engine enforces a tightened policy, and the onchain rules stay as they were until the owner signs a new version. Anything that widens a policy stays a draft until the owner signs it. Changing a policy never refills a spent budget.
Budget windows
Budgets run in UTC, and the module and the engine use the same windows.
- Daily
- Starts at 00:00 UTC.
- Weekly
- Starts on Monday at 00:00 UTC.
- Monthly
- A calendar month, starting on the 1st at 00:00 UTC.
The engine reserves a spend against every window at the moment it approves the request, so simultaneous requests cannot overspend. If the request then fails, the reservation is released.
Onchain, an operation that passes validation is charged during validation. If its execution then fails, that charge stays until the window rolls over. Failing closed is deliberate.
Rate snapshot
Onchain caps are in USD, but a contract cannot read a market price while it validates an operation. So each allowed asset carries a fixed rate that the owner signs at issuance, usd6PerUnitE18. This is what makes a daily cap one cap across all assets rather than one per asset.
That rate does not move with the market. The policy engine values each request at the live price, using Chainlink feeds for ETH and USDG and Robinhood quotes for Stock Tokens, and it takes the larger of what the calldata declares and what simulation shows leaving the wallet. A request has to pass both. If an asset moves far from its signed rate, sign a new one.
USD amounts in the API come in two forms: micro-dollars as an integer string, where "15000000" is $15, and dollars as a decimal string, "15".
API reference
Eight endpoints, one credential
Requests go to your AgentSpend host. The SDK takes it from baseUrl, then from AGENTSPEND_API_URL. Bodies and responses are JSON.
Authentication
Send the agent's credential as a bearer token. A credential reaches one agent. Any other agent's id gets the same 404 as an id that does not exist.
curl "$AGENTSPEND_API_URL/v1/agents/$AGENT_ID/budget" \
-H "Authorization: Bearer as_live_…"Errors
A decision is not an error. APPROVED, REQUIRES_APPROVAL and BLOCKED all come back as 200. Everything else that goes wrong comes back as JSON with one plain sentence.
{ "error": "This Idempotency-Key was already used for a different request" }- 400
- The request is malformed. The message names the field. Unknown fields are refused.
- 401
- The credential is missing, wrong or revoked.
- 404
- Not found, or it belongs to another agent. The API does not say which.
- 409
- The Idempotency-Key was used for a different request, its first request is still being evaluated, or that attempt was stopped and a new key is needed.
- 429
- A rate limit.
Retry-Aftersays how many seconds to wait. - 503
- AgentSpend could not finish the request and failed closed. The message says whether to try again or to read the intent first.
Rate limits
Limits are per agent, in fixed windows.
- 300 requestsper minute
- Every authenticated request.
- 60 spend requestsper minute
- Calls to
POST /v1/paymentsandPOST /v1/agents/:id/intent. - 20 blocked requestsper 5 minutes
- Requests the policy blocked. Once full, new spend requests are paused until the window ends.
- 20 approval requestsper hour
- Requests sent to a person. Once full, new spend requests are paused until the window ends.
Idempotency-Key
Both POST routes accept an Idempotency-Key header of 1 to 255 visible ASCII characters, scoped to the agent. Repeating a request with the same key returns the recorded decision. It never runs the policy again, it cannot pay twice, and it does not count against the spend limits. The same key on a different request gets a 409.
The SDK sends a key with every pay and requestTransaction call, and reuses it when it retries. A new call gets a new key, so if your agent can crash and start the same payment again, pass your own idempotencyKey, derived from the work it belongs to.
Endpoints
POST/v1/agents/:id/intent
Asks to run one call, or a batch that runs together. Use this for anything that is not a plain vendor payment.
Body
targetaddress- The contract or wallet to call.
calldatahex, optional- The call data, up to 64 KB. Defaults to empty.
valuewei string, optional- ETH to send with the call. Defaults to 0.
callsarray, optional- Up to 16 calls of
target,dataandvalue, sent instead of the three fields above. reasonstring, required- Why the agent wants this, up to 1,000 characters. Approvers see it.
purposestring, optional- Used to categorize the expense.
POST /v1/agents/7c1e4a52-…/intent
Authorization: Bearer as_live_…
Idempotency-Key: data-2291
Content-Type: application/json
{
"target": "0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168",
"calldata": "0xa9059cbb…",
"value": "0",
"reason": "Pay for 40 papers of data"
}{
"calls": [
{ "target": "0x5fc5…d168", "data": "0x095ea7b3…" },
{ "target": "0x9c1f…22ab", "data": "0x38ed1739…", "value": "0" }
],
"reason": "Approve, then swap"
}POST/v1/payments
Asks to pay a vendor. The agent names a vendor and an amount, and the server builds the transaction, so the agent never handles addresses.
Body
agentIdstring, required- Must be the credential's own agent.
vendorIdstring, required- A vendor slug or id.
amountdecimal string, required- Whole units of the asset, such as
"15". More decimal places than the asset allows is refused, so it never pays a rounded amount. currencystring, required- An approved asset symbol, such as
USDGorETH. purposestring, required- What the money is for.
reasonstring, optional- Why the agent wants this. Approvers see it.
POST /v1/payments
Authorization: Bearer as_live_…
Idempotency-Key: job-2291-gpu
Content-Type: application/json
{
"agentId": "7c1e4a52-…",
"vendorId": "gpufarm",
"amount": "15",
"currency": "USDG",
"purpose": "compute",
"reason": "Run a 40-minute GPU job"
}{
"intentId": "c41d7f06-8b13-4e2a-b5f4-6a90e17d3c28",
"status": "APPROVED",
"decision": "APPROVED",
"estimatedUsdValue": "15000000",
"estimatedUsd": "15",
"policyChecks": [
{ "id": "recipient_allowed", "label": "Recipient valid", "status": "PASS" },
{ "id": "daily_budget", "label": "Under daily budget", "status": "PASS", "detail": "$8.20 used + $15.00 of $250.00" },
{ "id": "onchain_policy", "label": "Onchain rules agree", "status": "PASS" }
],
"risk": "LOW",
"findings": []
}{
"intentId": "6f1c0a52-8f3e-4f6a-9d1b-2f7f0c3f8e11",
"status": "BLOCKED",
"decision": "BLOCKED",
"reason": "Recipient not approved. Onchain rules would reject this.",
"estimatedUsdValue": "50000000",
"estimatedUsd": "50",
"policyChecks": [
{ "id": "recipient_allowed", "label": "Recipient valid", "status": "FAIL", "detail": "0x7a00…0091 is not on the allowlist" },
{ "id": "onchain_policy", "label": "Onchain rules agree", "status": "FAIL", "detail": "Recipient is not on the onchain allowlist" }
],
"risk": "HIGH",
"findings": ["First payment to this counterparty"]
}The decision
Both POST routes return the same object.
intentId- The request's id. Use it to read its status or its approval.
status- Where the request is now.
APPROVEDmeans queued for execution, not finished. decisionAPPROVED,REQUIRES_APPROVALorBLOCKED.reason- Why it was blocked, or why a person has to decide.
estimatedUsdValue,estimatedUsd- The USD value in micro-dollars and in dollars.
policyChecks- One row per rule:
id,label,statusofPASS,FAIL,WARNorSKIP, and an optionaldetail. riskLOW,MEDIUMorHIGH.findings- Things worth a person's attention that did not change the decision.
simulation- What the transaction would do: token movements, approval changes and gas.
GET/v1/agents/:id/balance
The wallet's balances in every approved asset. amount is in base units and formatted is in whole units.
{
"agentId": "7c1e4a52-…",
"wallet": "0x81…3A1",
"balances": [
{ "asset": "0x5fc5…d168", "symbol": "USDG", "name": "Global Dollar", "decimals": 6, "kind": "stablecoin", "amount": "96000000", "formatted": "96" }
]
}GET/v1/agents/:id/budget
Each budget window as it stands. spentUsd is settled, and reservedUsd is approved but not settled yet.
{
"agentId": "7c1e4a52-…",
"budgets": [
{
"period": "day",
"limitUsd": "250",
"spentUsd": "8.2",
"reservedUsd": "15",
"remainingUsd": "226.8",
"limitUsd6": "250000000",
"settledUsd6": "8200000",
"reservedUsd6": "15000000",
"remainingUsd6": "226800000",
"windowStart": "2026-11-07T00:00:00.000Z"
}
]
}GET/v1/agents/:id/vendors
The vendors this agent's organization knows. verified means a person in the organization confirmed the address, and discovered means it was learned from a payment. A vendor with no addresses on this chain cannot be paid here.
{
"vendors": [
{
"id": "e2a7c1d4-…",
"name": "GPUFarm",
"slug": "gpufarm",
"category": "Compute",
"website": null,
"verified": true,
"discovered": false,
"addresses": [{ "address": "0x1000…0a02", "kind": "wallet" }]
}
]
}GET/v1/agents/:id/expenses
The agent's expenses, newest first.
Query
limit1 to 100- Defaults to 25.
cursorexpense id- The
nextCursorfrom the page before. It isnullon the last page.
{
"expenses": [
{
"id": "1b7e…",
"intentId": "c41d7f06-…",
"description": "API credits",
"vendor": { "name": "OpenAI", "slug": "openai" },
"counterparty": "0x1000…0a01",
"category": "AI/API",
"asset": "0x5fc5…d168",
"assetSymbol": "USDG",
"amount": "4",
"amountBaseUnits": "4000000",
"valueUsd": "4",
"valueUsd6": "4000000",
"gasUsd": null,
"sponsored": false,
"state": "CONFIRMED",
"chainId": 4663,
"txHash": "0x4be1…",
"userOpHash": "0x9d02…",
"timestamp": "2026-11-07T12:42:08.000Z"
}
],
"nextCursor": null
}GET/v1/intents/:id
A request as it stands now, including what happened after the decision. decision and risk are null only while the first evaluation is running.
{
"intentId": "c41d7f06-…",
"status": "EXECUTED",
"decision": "APPROVED",
"estimatedUsdValue": "15000000",
"estimatedUsd": "15",
"policyChecks": [],
"risk": "LOW",
"findings": [],
"kind": "payment",
"purpose": "compute",
"userOpHash": "0x9d02…",
"txHash": "0x4be1…",
"createdAt": "2026-11-07T12:42:06.000Z",
"updatedAt": "2026-11-07T12:42:08.000Z",
"expiresAt": "2026-11-07T12:52:06.000Z"
}Status
EVALUATING- The policy is running.
PENDING_APPROVAL- Waiting for a person, for up to 24 hours.
APPROVED- Queued for execution. It must start within 10 minutes.
SIGNING,SUBMITTED- On its way to the chain.
EXECUTED- Done. This is the only status where money moved.
BLOCKED- The policy refused it.
REJECTED- A person said no.
EXPIRED- It ran out of time, or its key or agent was stopped first.
FAILED- It could not be completed.
errorsays why.
GET/v1/approvals/:intentId
The state of a request that needs a person. status is NOT_REQUIRED, PENDING, APPROVED, REJECTED or EXPIRED.
Approval does not skip the checks. They run again, so an approved request can still be blocked. intentStatus is the field that says whether it went ahead.
{
"intentId": "0b9f6d1e-…",
"status": "APPROVED",
"intentStatus": "EXECUTED",
"rule": "Human approval required above $100.00",
"requiredVotes": 1,
"votes": 1,
"mode": "ONCE",
"expiresAt": "2026-11-08T09:12:00.000Z",
"resolvedAt": "2026-11-07T09:40:11.000Z"
}MCP setup
Give a model the same requests as tools
@agentspend/mcp lets a model use AgentSpend over stdio. It is built on the SDK, so it can do what an agent credential can do and nothing more: read one agent's finances and ask for spending. No tool touches a key, approves anything or changes a limit.
Configuration
Add the server to your MCP client's config. Each server reaches one agent, so add one entry per agent, such as researchbot and traderbot. It needs Node 20 or newer.
{
"mcpServers": {
"researchbot": {
"command": "npx",
"args": ["-y", "@agentspend/mcp"],
"env": {
"AGENTSPEND_API_URL": "https://your-agentspend-host",
"AGENTSPEND_AGENT_ID": "7c1e4a52-…",
"AGENTSPEND_API_KEY": "as_live_…"
}
}
}
}AGENTSPEND_API_URL- Your AgentSpend host.
AGENTSPEND_AGENT_ID- The agent's id.
AGENTSPEND_API_KEY- The same scoped credential the SDK calls
sessionKey. It is not a chain key, and the server refuses to start with anything that does not look like a credential.
Tools
get_balance- Wallet balance in ETH and every approved asset.
get_budget- Daily, weekly and monthly budgets in dollars.
list_vendors- The vendors in the agent's organization.
request_payment- Asks to pay a vendor.
request_transaction- Asks to run one call or a batch.
get_approval_status- Where an earlier request stands, including whether a person decided.
get_expenses- Recorded expenses, newest first.
request_payment and request_transaction return the policy's decision. BLOCKED and REQUIRES_APPROVAL are normal answers, not tool errors, and the tool tells the model to report them rather than retry around them.
Security model
What holds, and who holds what
Two locks
Every spend passes two independent checks. The first is the policy module on your smart account, which enforces the rules you signed. These hard limits hold even if AgentSpend itself is down or compromised. The second is the policy engine, which runs before anything is signed and adds live USD valuation, simulation, approvals and anomaly alerts.
The engine also asks the module for its verdict on every request, so the two agree before a key is touched.
The policy module has not had an external audit yet. It is built on ZeroDev Kernel v3.1, and its test suite runs against the exact bytecode live on Robinhood Chain.
Who holds what
- The owner
- Holds the owner key in their own wallet. Only the owner can issue a key, widen a policy or move funds at will. The agent never gets the owner key.
- The agent
- Holds a scoped API credential that reaches one agent's wallet, budget, approvals and expenses. It holds no chain key, so a stolen credential can only ask.
- AgentSpend
- Holds session keys encrypted in its signing service, and credentials as hashes. A session key can do only what the owner signed. The relayer that pays for transactions can authorize nothing.
- The funds
- Stay in your smart account, and the owner keeps ultimate control.
What a session key can never do
- Sign on the account's behalf. The module refuses every ERC-1271 request, so session keys can never sign Permit approvals.
- Call the smart account itself, the policy module or the EntryPoint, apart from revoking itself or rotating its own signer.
- Run a delegatecall. Only single and batch calls reach the account.
- Move value through another door. Calls such as
transferFrom,permit,setApprovalForAll,transferOwnershipand contract upgrades are refused, even on an allowlisted contract. - Grant an unlimited token approval, unless the owner allowed that in the policy.
- Widen its own limits. Rotating a key changes the signer and nothing else.
Failing closed
- If there is no reliable USD price, the request is blocked.
- If simulation fails or is unavailable, the request is blocked.
- If the chain cannot confirm the onchain rules, the request is blocked.
- If the signing service is down, nothing is signed, so nothing is spent.
People and approvals
- An approval is a wallet signature, not a button press. The server only records what a member's wallet signed, so nobody with database or admin access can approve anything quietly.
- An approval never skips the checks. The whole pipeline runs again against current state, so an approved request can still be blocked.
- Approvers see the agent's reason, but the policy never relies on it.
- Only the owner and approvers can approve. Issuing a key or raising a limit takes the owner's signature, so an admin account cannot do either.
Stopping and recovery
Freezing an agent stops autonomous spending the moment it commits, because the worker reads the agent's state again right before every signature. Requests waiting for a signature or an approval are cancelled. A request already submitted to the chain cannot be recalled.
The owner can recover funds directly, with no AgentSpend service involved: your wallet calls execute on the smart account. Your wallet can also revoke a session key or freeze the account at the module itself.
Identity
An agent can carry an optional ERC-8004 identity. It says who an agent is. It has nothing to do with what the agent can spend.