# Allowance > Give your agent a budget, not your keys. A Safe the owner controls on Robinhood Chain, with Safe's own spending-limit module (AllowanceModule 0.1.1). The agent gets an allowance per token, with a reset period, and signs its own transfers. It can never spend more than its allowance per period, and the owner can change or remove it at any time. We never hold keys or funds. ## How it works 1. The owner creates a Safe (Safe v1.4.1) owned by their wallet, or attaches one they own, and enables the AllowanceModule on it. On https://agentallowance.online that is two transactions for a new Safe and one for an existing threshold-1 Safe, all signed by the owner's wallet. 2. The owner adds the agent's address as a delegate and sets an allowance per token: an amount and a reset period in minutes (daily = 1440, the default; 0 = a one-time budget that never renews; at most 65,535). 3. The agent signs a transfer (token, recipient, amount) with its own key. The signature covers the module's EIP-712 hash at the allowance's current nonce, so it is good for exactly one transfer. 4. Anyone submits it: the agent itself (it pays the gas), our relay (3 credits; our operator pays the gas), or any other relayer. The module checks the signature, the nonce and what is left, then has the Safe send the tokens. What the module enforces onchain, per Safe, agent and token: - the amount per period. A spend over what is left reverts. `spent` goes back to 0 once the period has passed since the last reset. - that the signer is a delegate of that Safe. Removing the agent (or its allowance) stops every future spend at once. - one use per signature: the nonce goes up with every transfer. What it does not do: - It has no list of approved payees: an agent can pay any address within its allowance. The dashboard on https://agentallowance.online has a payee watchlist, but it is offchain, kept in the owner's browser, and only flags payments to other addresses after the fact. It can't block anything. - Version 0.1.1 does not check a token's `transfer` return value. Tokens that return false instead of reverting would count as spent without moving. Standard ERC-20s, including Pons tokens, revert on failure. - It doesn't read prices: an allowance is in token units, not dollars. Contracts on Robinhood Chain (chain id 4663), Safe's own deployments, checked with eth_getCode: - AllowanceModule 0.1.1: 0xAA46724893dedD72658219405185Fb0Fc91e091C - SafeL2 v1.4.1: 0x29fcB43b46531BcA003ddC8FCB67FFE91900C762 - SafeProxyFactory v1.4.1: 0x4e1DCf7AD4e460CfD30791CCC4F9c8a4f820ec67 - CompatibilityFallbackHandler v1.4.1: 0xfd0732Dc9E303f09fCEf3a7388Ad10A83459Ec99 - MultiSendCallOnly v1.4.1: 0x9641d764fc13c8B624c04430C7356C1C7C8102e2 - Sources: github.com/safe-global/safe-modules (tag allowance/v0.1.1, modules/allowances) and github.com/safe-global/safe-smart-account (tag v1.4.1). Safe's repository publishes the module's audit reports in modules/allowances/docs. ## Agent SDK One file, ESM, built on viem: https://agentallowance.online/sdk.js ```bash npm i viem curl -o allowance-sdk.js https://agentallowance.online/sdk.js ``` ```js import { privateKeyToAccount } from 'viem/accounts'; import { allowance } from './allowance-sdk.js'; const a = allowance({ safe: '0xTheOwnersSafe', account: privateKeyToAccount(process.env.AGENT_KEY), // the delegate's key, never the owner's apiKey: process.env.ALLOWANCE_KEY, // only for relay(): ck_… from https://agentallowance.online/#credits }); const left = await a.read({ token: '0xToken' }); // { left, amount, spent, resetsAt, nonce, symbol, decimals, spendable, … } const signed = await a.sign({ token: '0xToken', to: '0xPayee', amount: '2.5' }); // token units as a string, or bigint base units await a.relay(signed); // our operator submits it once and pays the gas: 3 credits // or: await a.submit(signed); // the agent submits it and pays the gas ``` `sign()` refuses to sign over the allowance, checks its EIP-712 hash against the module's `generateTransferHash`, and checks that the signature recovers to the agent. Use `token: 'eth'` (or the zero address) for ETH. ## Without the SDK (cast) ```bash # What is left: [amount, spent, resetTimeMin, lastResetMin, nonce]. Next reset at (lastResetMin + resetTimeMin) * 60 (unix seconds). cast call 0xAA46724893dedD72658219405185Fb0Fc91e091C "getTokenAllowance(address,address,address)(uint256[5])" --rpc-url https://rpc.mainnet.chain.robinhood.com # The hash to sign, at the current nonce (payment token and payment are always 0 here) cast call 0xAA46724893dedD72658219405185Fb0Fc91e091C "generateTransferHash(address,address,address,uint96,address,uint96,uint16)(bytes32)" 0x0000000000000000000000000000000000000000 0 --rpc-url https://rpc.mainnet.chain.robinhood.com # Sign the hash itself, with no message prefix cast wallet sign --no-hash --private-key $AGENT_KEY # Submit it yourself (or send it to the relay below) cast send 0xAA46724893dedD72658219405185Fb0Fc91e091C "executeAllowanceTransfer(address,address,address,uint96,address,uint96,address,bytes)" 0x0000000000000000000000000000000000000000 0 --private-key $AGENT_KEY --rpc-url https://rpc.mainnet.chain.robinhood.com ``` The agent may also call `executeAllowanceTransfer` from its own address with an empty signature (`0x`): the module then treats the caller as the signer. ## API Base URL: https://agentallowance.online. JSON, CORS open. Amounts are base units as strings, with `…Text` copies in token units. | call | cost | |---|---| | GET /api/allow/status?safe=&delegate=&token= | free | | GET /api/allow/prepare?safe=&delegate=&token=&to=&amount= | free | | GET /api/allow/stats | free | | GET /api/allow/safes?owner= | free | | GET /api/allow/activity?safe=&delegate=&n=&kind= | 1 credit | | POST /api/allow/relay | 3 credits, never on the free tier | - `status`: the Safe (owners, threshold, version, whether the module is on), its agents and, per token, `amount`, `spent`, `left`, `period`, `resetsAt` (null for one-time), `nonce`, the Safe's balance and `spendableNow` (the smaller of left and balance). `delegate` and `token` are optional filters; `token=eth` means ETH. - `prepare`: the nonce, the module's hash and the EIP-712 typed data for one transfer, plus `withinAllowance`. Sign either the typed data or the raw hash. - `stats`: counts from the module's events since block 13,500,000: agents added and removed, allowances set, allowance transfers and Safes seen. These cover every Safe on Robinhood Chain that uses the module, not only Safes set up on this site. - `safes`: Safes that have used the module and list `owner` as an owner. - `activity`: the module's events for one Safe, newest first, decoded: `{ kind: transfer | allowance_set | allowance_removed | allowance_reset | agent_added | agent_removed | relayer_payment, time, block, tx, delegate, token, symbol, to, amount, amountText, nonce, period }`. `n` up to 100; `kind=transfers` for transfers only. Read from the chain's RPC, cached 20 seconds. ```bash curl "https://agentallowance.online/api/allow/status?safe=0xSafe&delegate=0xAgent" curl "https://agentallowance.online/api/allow/activity?safe=0xSafe&kind=transfers" -H "Authorization: Bearer ck_…" ``` ### The relay POST https://agentallowance.online/api/allow/relay with your key and the signed transfer: ```bash curl -X POST "https://agentallowance.online/api/allow/relay" -H "Authorization: Bearer ck_…" -H "content-type: application/json" \ -d '{"safe":"0xSafe","token":"0xToken","to":"0xPayee","amount":"2500000","delegate":"0xAgent","nonce":7,"signature":"0x…"}' ``` The operator checks that the module is on, that the nonce is current, that the amount is within what is left and that the signature recovers to the delegate. Then it simulates the call, signs one transaction, sends it once and waits up to 25 seconds. - 200 `{ status: "success", tx, block, gasUsed, transfer, pay }` - 202 `{ status: "pending", tx }`: sent and not mined yet. It is never resent: check `tx` before signing again. - 409 `{ error: "reverted", tx }`: reverted after it was sent (someone used the nonce first, say). The gas was spent, so no refund. - 400 `bad_signature` (with the expected `hash` and `nonce`), 409 `stale_nonce`, `over_allowance` or `module_off`, 422 `would_revert` or `gas_cap`, 502 `not_sent`, 503 `relayer_low_funds`: nothing was broadcast and the credits are refunded. - 503 `ops_off`: the relay is off (no operator key yet). Nothing is charged. Submit the transfer yourself. - GET /api/allow/relay shows whether it is on and the operator's address. The relay always signs with `payment = 0` and `paymentToken = 0x0`: it never takes a fee from the Safe. Its gas comes from the operator; you pay in credits. ## Credits: how calls are paid Paid calls spend credits. You buy credits by burning $ALLOW through the Credits contract: the tokens go from your wallet straight to the dead address and are gone for good. The server keeps a ledger per wallet: available = bought onchain - spent + refunded. $ALLOW has not launched yet, so only the free tier is open. You can already get a key, so your agent is ready on day one. | Call | Credits | |---|---| | `allow/relay` | 3 | | `allow/activity` | 1 | Free tier: 20 calls a day per IP without a key (UTC day), with a cap of 500 free calls a day for everyone together. Once $ALLOW is live, calls with a key always spend credits; before that, they use the free tier. Prices and totals: GET https://agentallowance.online/api/credits/info ### Get an API key Sign this exact message (EIP-191 personal_sign) with the wallet that will hold the credits. `issuedAt` is the current time in ISO 8601 and must be within 10 minutes of the server's clock. Smart-contract wallets (ERC-1271) work too. ``` Allowance API key Wallet: Issued: ``` ```js import { privateKeyToAccount } from 'viem/accounts'; const account = privateKeyToAccount(process.env.AGENT_PRIVATE_KEY); const issuedAt = new Date().toISOString(); const message = `Allowance API key\nWallet: ${account.address}\nIssued: ${issuedAt}`; const signature = await account.signMessage({ message }); const res = await fetch('https://agentallowance.online/api/credits/key', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ wallet: account.address, issuedAt, signature }), }); const { key } = await res.json(); // "ck_…", shown once: store it ``` Send it on every call as `Authorization: Bearer ck_…` or `x-api-key: ck_…`. Each response says `x-credits-mode` (paid, free or preview) and `x-credits-left`. - GET https://agentallowance.online/api/credits/balance (with your key) or `?wallet=0x…`: bought, spent, refunded, available and your last 50 calls. - POST https://agentallowance.online/api/credits/revoke (with your key): deletes that key. Get a new one any time. ### Buy credits Cost = credits × price, in token base units (18 decimals). `maxBurn` is the most you agree to burn, so a price change can't surprise you. `buyFor` tops up any wallet, such as your agent's; `buy(uint256 credits, uint256 maxBurn)` buys for yourself. ```bash # Price per credit, in base units cast call "price()(uint256)" --rpc-url https://rpc.mainnet.chain.robinhood.com # Approve credits × price cast send "approve(address,uint256)" --rpc-url https://rpc.mainnet.chain.robinhood.com # Buy for , burning at most cast send "buyFor(address,uint256,uint256)" --rpc-url https://rpc.mainnet.chain.robinhood.com ``` Credits count as soon as the transaction lands. Or buy on the site: https://agentallowance.online/#credits ### Errors This is plain HTTP 402 with buy instructions, not x402. ``` 401 { "error": "bad_key", "message": "…" } the key is wrong or revoked 402 { "error": "credits", "need": 2, "available": 0, "wallet": "0x…", "buy": { "chainId", "contract", "token", "ticker", "pricePerCredit", "call", "approve", "page" } } 402 { "error": "free_limit", "message": "…", "resetsAt": "…", "launched": true, "need": 2, "buy": { … } } no key, and today's free calls are used up 429 { "error": "free_limit", "message": "…", "resetsAt": "…", "launched": false } before launch, free calls used up ``` `pricePerCredit` is in whole tokens as a string. `call` is `buyFor(address account, uint256 credits, uint256 maxBurn)`. ### Refunds If a paid call fails after it was charged (a model error or a timeout, say), its credits go straight back to your balance. Every refund, paid or free, is listed in a public log: GET https://agentallowance.online/api/credits/refunds?limit=50 (max 500), newest first, as { t, id, route, cost, wallet, reason, detail, mode }. ### Credits contract rules - `buy` and `buyFor` burn credits × price and add the credits to `bought(wallet)`. At most 1,000,000 credits per purchase. Zero credits or a zero wallet revert, and so does a cost above `maxBurn`. - The owner can pause buying and set the price per credit between 0.001 and 10,000,000 tokens. Nothing else: no withdrawals, no minting credits, no editing balances. The contract never holds tokens. - Reads: `price()`, `bought(address)`, `totalCredits()`, `totalBurned()`, `paused()`. - Credits: - $ALLOW token: Note: the free tier covers `activity` only. The relay always needs a key and credits, because it spends the operator's ETH. ## ERC-8004 agent identity Allowance Relay is an ERC-8004 agent. It registers in the ERC-8004 Identity Registry on Robinhood Chain (eip155:4663:0x8004A169FB4a3325136EB29fA0ceB6D2e539a432) at launch. - Registration file: https://agentallowance.online/.well-known/agent-registration.json - Identity Registry: https://robinhoodchain.blockscout.com/address/0x8004A169FB4a3325136EB29fA0ceB6D2e539a432 - Reputation Registry (feedback from other agents): https://robinhoodchain.blockscout.com/address/0x8004BAa17C55a88189AE136b182e5fdA19dE9b63 - Sitemap: https://agentallowance.online/sitemap.xml