The manual.

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.

.001Preface

Preface.

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.

.002Architecture

Architecture.

The system is composed of four on-chain elements:

  • Smart contractSolidity. The reserve logic.
  • CACUSTOS token. Launching soon.
  • ReserveReserve contract holding all capital. USDG by default.
  • LaneDeterministic sub-account per agent. Derived from reserve + label.
  • Account dataConstraints, roster, hours. Stored per lane.

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.

.003Settlement

Settlement.

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:

  • StatusThe lane is active. Not paused, not closed.
  • AmountWithin per-tx maximum and rolling daily cap.
  • HoursInside an allowed window (if configured).
  • RosterRecipient is on the lane's allowlist.
  • BalanceLane has sufficient funds.

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.

.004The Reserve

The Reserve.

The reserve is the root account. It is created once per authority and lives for the lifetime of the system. It binds a name and a settlement token to your wallet; from then on, every lane you open draws from it.

CustosReserve.solcustos · src
function createReserve(string calldata name, address token) external {
    if (reserves[msg.sender].exists) revert ReserveExists();
    if (token == address(0)) revert ZeroAddress();
    reserves[msg.sender] = Reserve({
        name: name,
        token: IERC20(token),
        laneCount: 0,
        exists: true
    });
    emit ReserveCreated(msg.sender, name, token);
}
ethers.js · deploy from a scriptcustos · src
import { Contract } from "ethers";
import { RESERVE_ABI } from "./reserveAbi";

// RPC: https://rpc.mainnet.chain.robinhood.com
const reserve = new Contract(CUSTOS_RESERVE, RESERVE_ABI, wallet);
await reserve.createReserve("production", USDG_ADDRESS);
.005Lanes

Lanes.

Each agent gets a lane. A partition within the reserve. A lane binds one agent wallet to one budget: the address you register is the only address allowed to execute payments from it.

A lane carries balance, spent count, transaction count, and a status flag. Authority can create, pause, resume, or close a lane at any time. Closing a lane returns its remaining balance to the authority, and a closed lane cannot be reopened.

openLane · ethers.jscustos · src
await reserve.openLane(
  "research_agent",          // label
  agentWallet,               // the only address that can pay
  2_000n * 10n ** 6n,        // lifetime budget · 2,000 USDG
);
.006Governance

Governance.

Governance is the constraint envelope on a lane. It defines spending discipline as account data:

  • maxPerTxMaximum allowed for a single transaction.
  • maxPerDayRolling 24-hour cap. Resets via block timestamp.
  • totalBudgetOptional lifetime ceiling that never resets.
executePayment · enforcement fragmentcustos · src
if (lane.maxPerTx != 0 && amount > lane.maxPerTx)
    revert ExceedsMaxPerTx();

if (lane.maxPerDay != 0) {
    if (block.timestamp >= lane.dayStart + 1 days) {
        lane.dayStart = block.timestamp;
        lane.spentToday = 0;
    }
    if (lane.spentToday + amount > lane.maxPerDay)
        revert ExceedsDailyCap();
    lane.spentToday += amount;
}
.007Roster

Roster.

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.

addToRoster · ethers.jscustos · src
const ROSTER = [
  { address: "0x4a2c…openai", label: "OpenAI · API" },
  { address: "0x7b3e…claude", label: "Anthropic · Claude" },
  { address: "0x9d17…market", label: "Feed · Market data" },
];

for (const { address, label } of ROSTER) {
  await reserve.addToRoster(laneId, address, label);
}

A compromised agent cannot exfiltrate to an unapproved address. The contract will not sign the transfer. Period.

.008Hours

Hours.

Hours bound when a lane may move funds. Define a daily window in UTC; outside it, transactions revert. The window may wrap midnight. The block timestamp is the time source. The agent's server clock is irrelevant.

setHours · ethers.jscustos · src
// Allow payments 09:00–17:00 UTC, every day
await reserve.setHours(
  laneId,
  9 * 3600,   // windowStart · seconds into the UTC day
  17 * 3600,  // windowEnd
  true,       // enabled
);
.009Replenish

Replenish.

Ships in Phase 02. Replenish is part of contract v2 and is not in the deployed contract yet. The design below is the target behaviour.

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.

configureReplenish · target designcustos · src
await reserve.configureReplenish(
  laneId,
  500n * 10n ** 6n,    // floor · refill when below 500 USDG
  2_000n * 10n ** 6n,  // target · refill up to 2,000 USDG
  true,
);

// Permissionless crank. Anyone can call this
await reserve.crankReplenish(laneId);
.010Quorum

Quorum.

Ships in Phase 02. Quorum is part of contract v2 and is not in the deployed contract yet. The design below is the target behaviour.

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.

quorum flow · target designcustos · src
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();
.011Interface

Interface.

The contract is the interface. Every operation below is a public function on the deployed reserve, callable from any EVM tooling. The console wraps them, and a script with ethers.js and the ABI reaches the exact same surface.

FunctionAction
createReserve(name, token)Create your reserve. Once per authority.
openLane(name, agent, budget)Open a per-agent lane.
setGovernance(laneId, maxPerTx, maxPerDay)Set the spending caps.
setHours(laneId, start, end, enabled)Configure the UTC window.
addToRoster(laneId, recipient, label)Approve a recipient.
removeFromRoster(laneId, recipient)Revoke a recipient.
deposit(laneId, amount)Fund a lane from the authority wallet.
withdraw(laneId, amount)Pull funds back. Authority only.
pauseLane / resumeLane / closeLaneLane lifecycle. Kill switch included.
executePayment(authority, laneId, to, amount)The agent's only entry point.

Everything in this table is live on Robinhood Chain mainnet today. The agent holds no funds and no authority; it can only call executePayment, and the contract verifies every rule before anything moves.