A working reference for Custos. What each call does, what it verifies, and how to invoke it from a script. Read in order, or jump to the chapter you need.
Custos is a treasury operating system for autonomous agents, built as a single contract on Robinhood Chain. Capital will be held in a reserve contract. The reserve. Each agent is given a lane within that reserve, with its own balance, ledger, and constraint envelope.
The system replaces the server-side budget. Where most stacks ask you to trust an operator with rules and balances, Custos stores the rules on-chain and verifies them in the same call that moves the funds. There is no middleware. There is no key handed to the agent.
Posture. The chain holds the capital. The contract holds the rules. The agent only signs intent. Never authority.
The system is composed of four on-chain elements:
The authority is the wallet that deployed the reserve. Only the authority can create lanes, set constraints, or move the roster. The agent itself only invokes payment calls. Authority over the reserve is never transferred to it.
When an agent asks to pay, it does not transfer directly. It invokes the reserve contract with an intent: a lane, an amount, and a destination. The contract runs every check in a single call:
If any check fails, the entire transaction reverts. Nothing moves. If all pass, the contract signs the transfer from the reserve to the recipient and writes the new balance, spent count, and transaction count to the lane.
The reserve is the root account. It is created once and lives for the lifetime of the system. The reserve contract address is derived from the authority key plus a small set of salts, so the address is deterministic and computable off-chain.
function createReserve(
string calldata name,
uint256 initialDeposit
) external returns (address reserveAddr) {
reserveAddr = _deployReserve(msg.sender, name); // CREATE2, deterministic
Reserve storage reserve = reserves[reserveAddr];
reserve.authority = msg.sender;
reserve.name = name;
reserve.balance = initialDeposit;
reserve.createdAt = block.timestamp;
}import { Custos } from "@custos/sdk";
// RPC: https://rpc.mainnet.chain.robinhood.com
const custos = await Custos.connect(provider, wallet);
const reserve = await custos.deployReserve({
name: "production",
initialDeposit: 10_000n * 10n ** 6n,
});
console.log("Reserve:", reserve.address); // 0x…Each agent gets a lane. A partition within the reserve. Lanes are deterministic sub-accounts derived from the reserve and a label. They are deterministic; their addresses can be computed from the label alone.
A lane carries balance, spent count, transaction count, and a status flag. Authority can create, pause, resume, or close a lane at any time. A closed lane cannot be reopened.
const lane = await reserve.openLane({
agentId: "research_agent",
budget: 2_000n * 10n ** 6n, // 2,000 USDC
});
// Compute the lane address off-chain
const address = Custos.deriveLane(reserve.address, "research_agent");Governance is the constraint envelope on a lane. It defines spending discipline as account data:
require(amount <= rules.maxPerTx, "ExceedsMaxPerTx");
uint256 elapsed = block.timestamp - rules.dayStart;
uint256 spent = elapsed >= 86_400 ? 0 : rules.spentToday;
require(
spent + amount <= rules.maxPerDay,
"ExceedsDailyCap"
);The roster is the per-lane allowlist of recipient addresses. Each entry is a small on-chain record. Address plus label plus the timestamp it was added. The contract checks every outbound payment against the lane's roster before signing.
const ROSTER = [
{ address: "0x4a2c…openai", label: "OpenAI · API" },
{ address: "0x7b3e…claude", label: "Anthropic · Claude" },
{ address: "0x9d17…market", label: "Feed · Market data" },
];
for (const entry of ROSTER) {
await lane.addToRoster(entry);
}A compromised agent cannot exfiltrate to an unapproved address. The contract will not sign the transfer. Period.
Hours bound when a lane may move funds. Define windows in UTC, with an optional day-of-week filter. Outside any defined window, transactions are rejected. The block timestamp is the time source. The agent's server clock is irrelevant.
await lane.setHours({
windows: [
{ startHour: 9, endHour: 17, days: [1, 2, 3, 4, 5] }, // Mon–Fri
],
});Replenish is the auto-refill call. Set a floor and a target on a lane. When the lane's balance drops below the floor, anyone can crank the call. But only the contract can move the funds, and only up to the target.
await lane.configureReplenish({
floor: 500n * 10n ** 6n, // refill when below 500 USDC
target: 2_000n * 10n ** 6n, // refill up to 2,000 USDC
enabled: true,
});
// Permissionless crank. Anyone can call this
await reserve.crankReplenish(lane.address);Quorum is the multi-signer rule. Above a defined amount, a payment becomes a proposal account on-chain. Each required signer approves the proposal; once the threshold is met, anyone may execute. Proposals expire after a configurable TTL. Signer sets are stored on-chain and rotated by the authority.
await reserve.configureQuorum({
signers: [w1.address, w2.address, w3.address],
threshold: 2, // 2-of-3
amountThreshold: 5_000n * 10n ** 6n,
proposalTtl: 86_400, // 24h
});
const proposal = await reserve.createProposal({
destination: vendor,
amount: 10_000n * 10n ** 6n,
});
await proposal.approve(w1);
await proposal.approve(w2); // threshold met
await proposal.execute();The TypeScript SDK provides a typed interface over the contract. It handles address derivation, transaction construction, confirmation, and account decoding.
| Method | Action |
|---|---|
| Custos.connect() | Bind provider + wallet. |
| custos.deployReserve() | Deploy the reserve contract. |
| reserve.openLane() | Open a per-agent lane. |
| lane.setGovernance() | Set max-per-tx + daily cap. |
| lane.addToRoster() | Add an approved recipient. |
| lane.setHours() | Configure time windows. |
| lane.configureReplenish() | Set auto-refill envelope. |
| reserve.configureQuorum() | Configure m-of-n quorum. |
| reserve.createProposal() | Open a quorum proposal. |
| proposal.approve() / execute() | Submit and finalise. |
The TypeScript SDK is in private preview. The signatures here reflect the target API. Reach out for early access.
npm install @custos/sdk viem