1 · Set up
- Open Bank → Agents, press Generate (or paste the address of a key your agent already has) and choose the limits.
- Fund the vault from your bank balance. The vault is the most the agent can ever spend; return it to your balance whenever you like.
- Send the agent key a little ETH for gas (the Send gas button does it).
- Give the agent its private key and its agent number. That is all it needs.
2 · The module
One file, ethers v6, no other dependency. Download blackveil-agent.mjs
import { BlackveilAgent } from './blackveil-agent.mjs'
const agent = new BlackveilAgent({ privateKey: process.env.AGENT_KEY, agentId: 1, bank: '0x…' })
// ask first — free, no transaction
await agent.route('ledger-co', '15.00') // { route: 'Settles', reason: 'None' }
// then pay; a handle or a 0x address both work
const r = await agent.pay('ledger-co', '15.00', { ref: 'inv 0x8a19' })
// { route: 'Settles', sent: true, hash: '0x…' }
// { route: 'Queues', sent: true, pendingId: 3 } ← waits for the owner
// { route: 'Refuses', reason: 'OverPerPayment', sent: false }
await agent.status() // vault, spentToday, limits, paused/revoked
From a shell: AGENT_KEY=0x… AGENT_ID=1 node blackveil-agent.mjs pay ledger-co 15 "inv 0x8a19"
By default a refused payment is not sent, to save gas. Pass recordRefusals: true if you want every refused attempt written on chain as an AgentRefused event.
3 · Settles, queues, refuses
routeFor(agentId, to, amount) runs the same checks as agentPay, in this order:
| Check | Outcome |
|---|---|
| Agent revoked or paused | Refuses |
| Allowlist on and the recipient is not on it | Refuses |
| Amount above the per-payment limit | Refuses |
| Vault smaller than amount + fee | Refuses |
| Amount above the review line | Queues — the amount and fee are reserved from the vault until the owner approves or rejects |
| Today's spend + amount above the 24-hour limit | Refuses |
| Otherwise | Settles — paid at once, counted toward the 24-hour window |
The 24-hour window starts with the first settled payment and resets 24 hours later. Payments the owner approves from the queue do not count toward it — the owner already decided.
4 · Reason codes
| Code | Name | What to do |
|---|---|---|
| 1 | Paused | The owner paused the agent. Wait. |
| 2 | Revoked | This key is finished. Stop. |
| 3 | ZeroAmount | Send a positive amount. |
| 4 | NotAllowlisted | Ask the owner to allow this recipient. |
| 5 | OverPerPayment | Split the bill or ask for a higher limit. |
| 6 | OverDaily | Try after the window resets. |
| 7 | InsufficientVault | Ask the owner to fund the vault. |
5 · What the key cannot do
- Withdraw from the vault, or touch the owner's balance.
- Change its own limits, allowlist, status or key — those calls check the owner, not the signer.
- Approve its own queued payments.
- Keep working after the owner revokes or rotates it.
If the key leaks, the most it can move is what the mandate allows until you pause it. Revoking returns the whole vault to your balance in the same transaction.
6 · Calling the contract directly
// Solidity / any client — Robinhood Chain (4663), USDG has 6 decimals
function routeFor(uint256 agentId, address to, uint256 amount) view returns (uint8 route, uint8 reason)
function agentPay(uint256 agentId, address to, uint256 amount, bytes32 ref, bool toWallet) returns (uint8 route, uint256 pendingId)
function resolve(string handle) view returns (address)
event Payment(address indexed from, address indexed to, uint256 amount, uint256 fee, bytes32 ref, uint8 kind, uint256 refId, bool toWallet)
event Queued(uint256 indexed pendingId, uint256 indexed agentId, address indexed to, uint256 amount, bytes32 ref)
event AgentRefused(uint256 indexed id, address indexed to, uint256 amount, uint8 reason, bytes32 ref)
ref is any 32-byte reference — an invoice id, an order number. It travels into the Payment event so you can reconcile. toWallet = true sends USDG straight to the recipient's wallet instead of their BLACKVEIL balance. Full ABI: /abi/BlackveilBank.json · source: Verify.