Manual

The manual.

A working reference. What each call does, what it verifies, and how to reach it from a script. Read it in order, or jump to the chapter you need.

.001

Preface.

Kivo is a treasury operating system for autonomous agents, built as a single contract on Robinhood Chain. Capital sits in a reserve contract. Each agent is given a lane inside that reserve, with its own balance, its own ledger, and its own set of constraints.

It replaces the budget that normally lives in your backend. Where most stacks ask you to trust an operator with the rules and the balances, Kivo keeps the rules on-chain and verifies them in the same call that moves the funds. There is no middleware to keep alive, and no key handed to the agent.

Posture. The chain holds the capital. The contract holds the rules. The agent signs intent, never authority.

Network
Robinhood Chain
Contract
not deployed
Settlement
USDG
Tests
22 green
.002

Architecture.

The system is four on-chain objects and nothing else:

Contract
KivoReserve.sol · Solidity 0.8.24
Reserve
holds all capital, one per authority
Lane
sub-account per agent
Lane data
caps, roster, hours, counters

The authority is the wallet that created the reserve. Only that wallet can open lanes, set constraints, or edit a roster. The agent can call exactly one function, and it cannot become the authority by calling it.

.003

Settlement.

When an agent wants to pay, it does not transfer anything itself. It calls the reserve with an intent: a lane, an amount, a destination. The contract then runs every check in one call, in this order:

Agent
caller is the lane’s bound wallet
Status
lane is active, not paused or closed
Roster
recipient is approved for this lane
Hours
inside the window, if one is set
Amount
within per-tx, daily and lifetime limits
Balance
the lane can cover it

If one check fails the whole transaction reverts and nothing moves. If they all pass, the contract transfers from the reserve to the recipient and writes the new balance, spend totals and transaction count back to the lane.

.004

The reserve.

The reserve is the root account. It is created once per authority and lives as long as the system does. It binds a name and a settlement token to your wallet, and every lane you open afterwards draws from it.

KivoReserve.solsrc
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,
        balance: 0,
        exists: true
    });
    emit ReserveCreated(msg.sender, name, token);
}
ethers.js · from a scriptdeploy
// RPC: https://rpc.mainnet.chain.robinhood.com
import { Contract } from "ethers";
import { RESERVE_ABI } from "./reserveAbi";

const reserve = new Contract(KIVO_RESERVE, RESERVE_ABI, wallet);
await reserve.createReserve("production", USDG_ADDRESS);
.005

Lanes.

Each agent gets a lane. A lane binds one agent wallet to one budget, and the wallet you register is the only address allowed to spend from it. The lane carries a balance, a spend total, a transaction count and a status flag.

The authority can pause, resume or close a lane at any time. Closing returns whatever is left to the authority, and a closed lane cannot be reopened.

openLane · ethers.jslanes
const laneId = await reserve.openLane.staticCall(
  "research_agent",        // label
  agentWallet,             // the only address that can pay
  2_000n * 10n ** 6n,      // lifetime budget · 2,000 USDG, 0 = unlimited
);
await reserve.openLane("research_agent", agentWallet, 2_000n * 10n ** 6n);

// fund it from the authority wallet (approve the reserve first)
await usdg.approve(KIVO_RESERVE, 1_000n * 10n ** 6n);
await reserve.deposit(laneId, 1_000n * 10n ** 6n);
.006

Governance.

Governance is the constraint envelope on a lane. It is stored as plain lane data:

maxPerTx
ceiling for a single payment
maxPerDay
rolling 24 hours, reset by block timestamp
totalBudget
lifetime ceiling that never resets
executePayment · enforcement fragmentsrc
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;
}

A value of zero means no limit, so a lane with no governance set still cannot exceed its own balance, its roster, or its hours.

.007

Roster.

The roster is the per-lane allowlist of recipients. Each entry is a small on-chain record: address, label, and the timestamp it was added. Every outbound payment is checked against it before the contract signs anything.

addToRoster · ethers.jsroster
const ROSTER = [
  { address: "0x4a2c…", label: "Inference · API" },
  { address: "0x7b3e…", label: "Market data · feed" },
  { address: "0x9d17…", label: "Storage · bucket" },
];

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

// revoke immediately, no redeploy, no restart
await reserve.removeFromRoster(laneId, "0x9d17…");

A compromised agent cannot send funds to an address that is not on the list. The contract will not sign the transfer, whatever the agent believes it was told to do.

.008

Hours.

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

setHours · ethers.jshours
// 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
);

// a window that wraps midnight is valid: 22:00 → 04:00
await reserve.setHours(laneId, 22 * 3600, 4 * 3600, true);
.009

Replenish.

phase 02

Not in the deployed contract. Replenish is part of contract v2. What follows is the target behaviour, written down so it can be argued with before it is built.

Replenish is the auto-refill call. Set a floor and a target on a lane. When the balance drops under the floor, anyone can crank it. Only the contract can move the funds, and only up to the target.

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

// permissionless crank, anyone may call it
await reserve.crankReplenish(laneId);
.010

Quorum.

phase 02

Not in the deployed contract. Quorum is part of contract v2. The design below is the target behaviour.

Above an amount you define, a payment becomes a proposal stored on-chain. Signers approve it, and once the threshold is met anyone may execute. Proposals expire after a TTL you set. The signer set lives in the contract, not in a database.

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

Interface.

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

FunctionAction
createReserve(name, token)Create your reserve. Once per authority.
openLane(label, agent, budget)Open a lane for one agent wallet.
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.
simulatePayment(authority, laneId, agent, to, amount)Dry run. Same checks, same order, returns the reason.
getReserve / getLane / getRosterRead the state without an indexer.

The agent holds no funds and no authority. It can call executePayment, and the contract checks every rule before anything moves.

Try it before you fund it.

The sandbox on the home page runs the same rule engine as the contract. No wallet, no gas, same rejections.