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:

  • ContractSolidity. The contract logic.
  • ReserveReserve contract holding all capital. USDC 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 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.

CreateReserve.solcustos · src
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;
}
TypeScript · Custos SDKcustos · src
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…
.005Lanes

Lanes.

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.

open_lane · TypeScriptcustos · src
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");
.006Governance

Governance.

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

  • max_per_txMaximum allowed for a single transaction.
  • max_per_dayRolling 24-hour cap. Resets via block timestamp.
  • lifetime_capOptional total ceiling that never resets.
enforce. executePayment fragmentcustos · src
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"
);
.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.

add_to_rostercustos · src
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.

.008Hours

Hours.

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.

set_hourscustos · src
await lane.setHours({
  windows: [
    { startHour: 9, endHour: 17, days: [1, 2, 3, 4, 5] }, // Mon–Fri
  ],
});
.009Replenish

Replenish.

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.

configure_replenishcustos · src
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);
.010Quorum

Quorum.

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 flowcustos · 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 TypeScript SDK provides a typed interface over the contract. It handles address derivation, transaction construction, confirmation, and account decoding.

MethodAction
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.

install · plannedcustos · src
npm install @custos/sdk viem