Overview
x402 makes an agent pay before every call. Fivebar adds credit: a pool of lender USDC pays the seller at once, and the agent's owner settles a weekly statement. Larger jobs can be held in escrow until delivery is checked.
Robinhood Chain is Fivebar's home chain: agents onboard there and pay in USDG. Base works too, in USDC, and the credit contracts (tabs, the pool, escrow) run on Base Sepolia testnet today. Warden NFTs, the token and governance live on Ethereum. Solana is next. Six parts take part in each payment:
- AOwner
A person or company. Opens a line, posts USDC collateral, sets a mandate, repays the statement.
- BAgent
Holds a signing key registered on the line. Signs a payment intent when a seller answers
402. - CSeller
Any x402 API. Lists
tally-tabin itsacceptsand forwards the payment to the facilitator. - DFacilitator
Checks the intent, dry-runs it, then submits
TabManager.pay. Its key only pays gas and never holds funds. - ETabManager
Enforces the line, the mandate and the credit limit, bills the owner and pays the seller from the pool.
- FCreditPool
An ERC-4626 USDC vault. Lenders deposit and earn half of every seller fee.
Quickstart
Run the whole flow on a local chain. You need Node 22 and Foundry.
git clone https://github.com/fivebar-xyz/tally-main cd tally-main/contracts mkdir -p lib git clone --depth 1 --branch v5.1.0 https://github.com/OpenZeppelin/openzeppelin-contracts lib/openzeppelin-contracts git clone --depth 1 --branch v1.9.4 https://github.com/foundry-rs/forge-std lib/forge-std forge test # 87 contract tests cd ../services npm install npm run build:contracts npm test # 60 tests on a local chain npm run demo # an agent buys 4 quotes on credit
The demo starts anvil, deploys the contracts, funds the pool with 10,000 test USDC, opens a line with 100 USDC collateral and a $1 per call, $10 budget mandate, then buys four quotes from a demo seller.
Lines and mandates
A line belongs to an owner and names one agent key. Its credit limit is its collateral times maxLtvBps, which is 100% in v0. Every tab is fully secured, so lenders cannot lose principal to a default.
A mandate limits what the agent may spend on the line:
| Field | Type | Meaning |
|---|---|---|
maxPerPayment | uint128 | Largest single payment, in USDC base units (6 decimals). |
budget | uint128 | Total the agent may spend under this mandate. |
validUntil | uint64 | Unix time after which payments are refused. |
sellersRoot | bytes32 | Merkle root of allowed sellers. Zero allows any seller. |
Seller leaves follow the OpenZeppelin StandardMerkleTree format: keccak256(bytes.concat(keccak256(abi.encode(seller)))). Pass the proof as the third argument to pay.
Sponsored lines (Fivebar for Networks)
A network can back many agents with one deposit. It opens a sponsor account, deposits USDC once, then opens a line for each agent. Each line keeps its own mandate and statements; together they can never owe more than the sponsor’s deposit. A per-agent cap is optional: NO_CAP for none, 0 to freeze an agent.
cast send $TABS "openSponsor()" --account network # emits SponsorOpened(sponsorId, …) cast send $TABS "depositSponsorCollateral(uint256,uint256)" $SPONSOR 5000000000 --account network cast send $TABS "openSponsoredLine(uint256,address,uint128)" $SPONSOR $AGENT 100000000 --account network # then setMandate(lineId, …) for each agent, as for any line
One agent’s default is paid from the shared deposit and lowers the limit for the others. Sponsor ownership can’t be transferred, so own the sponsor from a Safe.
Vouching: agents underwrite agents
A new agent has no history, so it gets no credit. An established agent can vouch for it: it stakes USDC in a sponsor account, opens a line for the newcomer and hands day-to-day control to the newcomer's owner. The newcomer runs the line (mandate, agent key, workers, sweeps) but can't touch the stake or the cap. The voucher earns half of the treasury's share of every fee the line pays, and takes the loss if it defaults.
cast send $TABS "openSponsor()" --account voucher cast send $TABS "depositSponsorCollateral(uint256,uint256)" $SPONSOR 200000000 --account voucher # the stake cast send $TABS "openSponsoredLine(uint256,address,uint128)" $SPONSOR $NEW_AGENT 100000000 --account voucher cast send $TABS "setManager(uint256,address)" $LINE $NEWCOMER_OWNER --account voucher cast send $TABS "acceptManager(uint256)" $LINE --account newcomer # newcomer opts in cast send $TABS "claimVoucherFees(address)" $TO --account voucher # later: collect earnings
The voucher can freeze the line (setLineCap(line, 0)) or take control back (setManager(line, 0x0)) at any time.
Epochs
Each line has an epoch. Calling setMandate, revokeMandate or setAgent sets a fresh, unpredictable epoch, which invalidates every intent signed earlier, including any pre-signed for a guessed future epoch. Clients read the current epoch from getLine. To kill a single signed intent, the owner calls cancelNonce.
cast send $USDC "approve(address,uint256)" $TABS 100000000 --account owner cast send $TABS "openLine(address)" $AGENT --account owner # emits LineOpened(lineId, …) cast send $TABS "depositCollateral(uint256,uint256)" $LINE 100000000 --account owner cast send $TABS "setMandate(uint256,uint128,uint128,uint64,bytes32)" \ $LINE 1000000 10000000 $(($(date +%s)+2592000)) $(cast hash-zero) --account owner # $1 per payment, $10 budget, 30 days, any seller
Payment intents
The agent authorizes each payment by signing an EIP-712 PaymentIntent. Anyone can submit it; normally the facilitator does. Agents can be plain keys or ERC-1271 smart wallets.
domain = { name: "Fivebar TabManager", version: "1", chainId, verifyingContract: TabManager }
PaymentIntent(
uint256 lineId,
uint32 epoch, // must equal the line's current epoch
address seller,
uint256 amount, // USDC base units
bytes32 resource, // keccak256 of the exact URL being paid for
bool escrowed, // true: hold in escrow until delivery is checked
uint256 nonce, // any unused number; the client uses 32 random bytes
uint256 deadline // unix time
)pay checks, in order: the line exists, the amount is above zero, the seller is not a protocol address, the deadline, the epoch, the nonce, the signature, the mandate, the past-due state and the credit limit. Each failure reverts with a named error (see the reference).
Billing and default
Billing works like a card statement. The first billable spend opens a 7-day cycle. When the cycle closes, its spending becomes the statement, due 3 days later. After 2 more days of grace, anyone can liquidate the line.
- New spending stops once a statement is past its due date, until it is repaid.
- Repay with
repay(lineId, amount). It pays the statement first, then the open cycle. Anyone may repay a line. - Liquidation seizes the whole billable balance (statement plus open cycle) from collateral and returns it to the pool. Escrowed amounts are left alone and settle through escrow.
- v0 charges no interest. The admin can change the periods with
setParams; the billing period must be at least the payment window plus grace. Changes apply to cycles opened afterwards: a statement keeps the payment window and grace in force when its cycle opened.
Escrow
An intent with escrowed: true sends the money from the pool to the Escrow contract instead of the seller. The amount is held: it counts against the credit limit but is not billed and cannot be liquidated.
| Action | Who | When | Result |
|---|---|---|---|
pass | Verifier | Within the 1-day review window | Seller paid minus the fee; amount billed to the owner |
flag | Verifier | Within the review window | Hold is disputed; a 7-day ruling deadline starts |
claim | Anyone | After the review window, if not flagged | Released to the seller |
resolve | Arbiter | Any time, held or flagged | Refund (back to the pool, budget restored) or release |
expire | Anyone | After the ruling deadline, if still flagged | Refund |
For agents
The Fivebar Branch: banking tools for any agent (MCP)
The quickest way in. The Fivebar Branch is an MCP server that runs on your agent's own computer. It onboards the agent from nothing to paying on x402: onboard (the checklist), create_wallet, backup_wallet, confirm_backup, request_funding, register_identity (ERC-8004) and first_payment. Then it banks: whoami, balance, pay, check_seller and my_score, plus pair_with_owner, refill, statement and repay on testnet. Any framework that speaks MCP can use it: Claude, Cursor, LangGraph, ElizaOS and others.
claude mcp add fivebar -- npx -y fivebar
{
"mcpServers": {
"fivebar": {
"command": "npx",
"args": ["-y", "fivebar"],
"env": { "TALLY_MAX_PER_CALL": "1" }
}
}
}The wallet is created on the agent's computer and stored encrypted in ~/.fivebar/agent (standard v3 key file). Nothing is sent to Fivebar. Funding, the ERC-8004 ID and payments stay locked until the agent has written a backup to a folder its human chose, proved the backup restores, and the human has read back the check code from it: an agent key with no backup is one deleted folder away from a lost wallet. Agents that can read the web can start from onboard.md.
Settings: TALLY_NETWORK is base (default: real USDC, x402 and ERC-8004) or base-sepolia (testnet, which adds prepaid accounts and tabs: set TALLY_OWNER after pairing and TALLY_LINE_ID once a tab is open). TALLY_MAX_PER_CALL is the hard ceiling for any single payment, in USD (default 1). TALLY_AGENT_ADDRESS without a key makes the branch read-only.
fivebarFetch wraps fetch. When a seller answers 402 and accepts tally-tab, it reads the line's epoch, signs an intent for the exact price and URL, and retries with an X-PAYMENT header.
import { fivebarFetch } from "./agent/client.js";
const { response, paid } = await fivebarFetch("https://api.example.com/v1/quote/eth", {
account: agentKey, // viem LocalAccount registered on the line
lineId: 7n,
publicClient,
tabManagerAbi,
maxPerCall: 1_000_000n, // refuse anything above $1 before signing
escrowAtOrAbove: 25_000_000n, // hold jobs of $25 or more in escrow
});
console.log(await response.json(), paid?.settlement.transaction);Wallet first, Fivebar as overdraft
Give the client a wallet and it pays in this order: the agent's own USDC with the standard x402 exact scheme, then USDC it parked in the credit pool, then its Fivebar line. If a seller refuses the wallet payment, the client falls back to the line. An agent with a line never stalls on a payment.
const { paid } = await fivebarFetch(url, {
account: agentKey, lineId: 7n, publicClient, tabManagerAbi,
wallet: { chainId: 84532, usdc: USDC, idle }, // idle: see below (optional)
});
paid?.source; // "wallet" | "refill" | "pool" | "tab"Prepaid accounts and refills
An owner can fund all its agents from one prepaid account. Deposit USDC into AgentAccounts once, then give each agent wallet a rule: refill below a floor, up to a target, with a daily cap. When the agent's wallet runs low, fivebarFetch tops it up just before paying (pass refill in wallet); a keeper or the owner can also call refill. Funds only ever go to wallets the owner registered, within their rules, so a leaked agent key risks at most that agent's daily cap. Only the owner can withdraw or change rules, and setPaused stops every refill at once. Fivebar has no admin role on the contract. If the account is empty or the cap is used, the client falls back to the agent's Fivebar line.
Owners manage all of this on the Account page: connect a wallet, deposit, set Earn, and add agents three ways. A pairing link is the safest: the agent signs it with its own key (createPairingLink in services/src/agent/pairing.ts), so the address can't be mistyped or swapped. Adding by ERC-8004 id (setAgentById) funds the id's registered payment wallet and stops refills if the id later points elsewhere. A plain address or ENS name works for any agent, unverified.
cast send $USDC "approve(address,uint256)" $ACCOUNTS 50000000 --account owner cast send $ACCOUNTS "deposit(uint256)" 50000000 --account owner # $50 cast send $ACCOUNTS "setAgent(address,uint128,uint128,uint128)" $AGENT 5000000 20000000 50000000 --account owner # keep the agent between $5 and $20, at most $50 a day
const refill = { accounts: ACCOUNTS, owner: OWNER, abi: agentAccountsAbi, account: agentKey, publicClient, walletClient };
await fivebarFetch(url, { account: agentKey, lineId, publicClient, tabManagerAbi,
wallet: { chainId: 84532, usdc: USDC, refill } }); // paid.source === "refill" when it topped upEarn on a prepaid balance (opt-in)
An owner can supply its prepaid balance above a cash buffer to one ERC-4626 USDC vault of its choice: a Morpho vault, Aave through its ERC-4626 wrapper, or Fivebar's own CreditPool. setEarn(vault, buffer) turns it on, changes vault or (with address(0)) brings everything back to cash; sweep(owner) supplies new deposits. Refills and withdrawals pull from the vault when cash runs short, limited by what the vault can pay out at that moment. There is no leverage and no looping. Each owner's shares are tracked separately and every vault call is measured by what actually moved, so one owner's vault never touches another owner's money. The vault's own risks (smart contract, liquidity, rates) are the owner's; Fivebar does not pay or promise any return.
Curated vaults on Base mainnet (checked on chain: native USDC, Morpho Blue vaults and Aave's official wrapper), each tested end to end on a mainnet fork:
| Vault | Protocol | Address |
|---|---|---|
| Steakhouse USDC | Morpho | 0xbeeF010f9cb27031ad51e3333f9aF9C6B1228183 |
| Gauntlet USDC Prime | Morpho | 0xeE8F4eC5672F09119b96Ab6fB59C27E1b7e44b61 |
| Static Aave Base USDC | Aave | 0x4EA71A20e655794051D1eE8b6e4A3269B13ccaCc |
Morpho and Aave have no vaults for Circle's USDC on Base Sepolia, so testnet Earn uses the Fivebar CreditPool. Run the fork tests with BASE_RPC=… forge test --match-path "test/fork/*".
cast send $ACCOUNTS "setEarn(address,uint256)" $VAULT 50000000 --account owner cast call $ACCOUNTS "totalOf(address)(uint256)" $OWNER # cash + vault value cast send $ACCOUNTS "setEarn(address,uint256)" 0x0000000000000000000000000000000000000000 0 --account owner # opt out
Idle balance
Spare USDC can sit in the credit pool and earn the lenders' share of seller fees until it is needed. parkIdle deposits everything above a buffer; fivebarFetch withdraws just enough before a payment. Withdrawals are limited to the pool's idle cash, and the agent pays a little gas for each deposit and withdrawal.
import { parkIdle, idleBalance } from "./agent/idle.js";
const idle = { account: agentKey, publicClient, walletClient, usdc: USDC, pool: CREDIT_POOL, poolAbi };
await parkIdle(idle, 5_000_000n); // keep $5 liquid, park the rest
await idleBalance(idle); // { cash, parked, withdrawable }Sub-tabs: credit for agent teams
An orchestrator can give each worker it spawns its own key and budget on its line. The worker signs payments with its own key; the line's mandate still applies on top, and every sub-spend rolls up into one statement (DelegatePaid events give the tree). Any mandate or agent change revokes every worker at once.
// the orchestrator (the line's agent), or the owner, grants $5 for one hour await tabs.write.setDelegate([lineId, worker.address, 5_000_000n, BigInt(now + 3600)]); // the worker pays like any agent, with its own key and the parent's line await fivebarFetch(url, { account: worker, lineId, publicClient, tabManagerAbi }); // done early: revoke await tabs.write.setDelegate([lineId, worker.address, 0n, 0n]);
Self-funding agents
An agent that earns can pay its own bills. The owner sets a sweep: from then on, a share of everything the agent is paid through Fivebar repays its tab before the rest reaches its wallet. For income from elsewhere, repayFromWallet sends cash above a buffer to the tab. GET /pnl/:address on the Score API shows what the agent paid and earned each week.
cast send $TABS "setSweep(uint256,address,uint16)" $LINE $AGENT 5000 --account owner # 50% of Fivebar income import { repayFromWallet } from "./agent/selffund.js"; await repayFromWallet(cfg, 10_000_000n); // keep $10, repay the rest of what's owed
Credit history on ERC-8004
Link an agent's ERC-8004 identity to its line, and every statement it settles becomes public reputation: tally.statement feedback from Fivebar's reporter, graded on-time (100), late (50) or default (0). Partial payments are not graded. The link counts only if the identity's owner owns the line, or its agent wallet is the line's agent key.
cast send $IDENTITY_REGISTRY "setMetadata(uint256,string,bytes)" $AGENT_ID "tally.line" \ $(cast abi-encode "f(uint256,address,uint256)" 84532 $TAB_MANAGER $LINE_ID) --account owner
Anyone can read the record: getSummary(agentId, [reporter], "tally.statement", "") on the ERC-8004 ReputationRegistry. Remove the link by setting the key to empty.
For sellers
The seller kit adds the pay-later tally-tab scheme to your API in a few lines. It sits next to any x402 setup you already run: payments in other schemes pass straight through. You are paid in USDC by the pool as soon as the payment settles, and never carry the credit risk.
import { fivebarHono } from "@fivebar/services/seller-kit";
app.use("/v1/*", fivebarHono({
network: "base-sepolia",
payTo: "0xYourWallet", // where you get paid
price: "0.05", // USD, or a function of the request
facilitatorUrl: "http://localhost:4020",
extraAccepts: [yourExactOffer], // optional: keep your existing x402 offer
}));import { fivebarExpress } from "@fivebar/services/seller-kit";
app.use("/v1", fivebarExpress({ network: "base-sepolia", payTo: "0xYourWallet", price: "0.05",
facilitatorUrl: "http://localhost:4020" }));import { fivebarGate } from "@fivebar/services/seller-kit";
const gate = await fivebarGate(req, opts);
if (gate.kind === "payment-required") return gate.response; // 402
if (gate.kind === "not-fivebar") return yourExistingX402(req); // another scheme
// paid: serve, and add gate.responseHeaders (X-PAYMENT-RESPONSE)A bad payment or an unreachable facilitator gets a 402 with the reason; unpaid work is never served. The kit is in services/src/seller-kit/ and not on npm yet. Full guide: docs/sellers.md.
Price by Fivebar Score
Give trusted agents a better price, and keep low-score bots out. Agents announce their address in X-TALLY-AGENT; the kit looks up its score and quotes. The discount is safe: a payment is priced again for the key that actually signed it, so claiming someone else's address gets no discount. If the Score API is down, everyone pays the full price.
fivebarHono({
network: "base-sepolia", payTo: "0xYourWallet", price: "0.05", facilitatorUrl,
score: {
api: "https://tally402-score.fly.dev",
tiers: [{ minScore: 800, discountBps: 2000 }, { minScore: 600, discountBps: 1000 }], // 20% off at 800+
minScore: 300, // optional: 403 below this (new agents have no score)
},
});Without the kit
Add a tally-tab entry to the accepts list of your 402 response. You keep any other schemes you already accept.
{
"x402Version": 1,
"error": "X-PAYMENT header is required",
"accepts": [{
"scheme": "tally-tab",
"network": "base-sepolia",
"maxAmountRequired": "10000",
"resource": "https://api.example.com/v1/quote/eth",
"description": "Market data snapshot",
"mimeType": "application/json",
"payTo": "0xSeller…",
"maxTimeoutSeconds": 120,
"asset": "0xUSDC…",
"extra": { "tabManager": "0xTabManager…", "chainId": 84532 }
}]
}When a request arrives with X-PAYMENT, decode it (base64 JSON), post it with your requirements to the facilitator's /verify, then /settle. Serve the content only after settle succeeds, and return the settle result base64-encoded in X-PAYMENT-RESPONSE.
You receive the price minus the seller fee (1% in v0, rounded up) in the same transaction: a $0.05 call pays you $0.0495. Escrowed payments arrive when the hold is released.
Facilitator API
Follows the x402 v1 facilitator interface. Both POST routes take { paymentPayload, paymentRequirements }.
| Route | Returns |
|---|---|
GET /supported | { kinds: [{ x402Version: 1, scheme: "tally-tab", network }] } |
POST /verify | { isValid, invalidReason?, payer? }. payer is the line owner. |
POST /settle | { success, errorReason?, transaction?, network, payer?, escrowHoldId? } |
Rejection reasons
| Reason | Meaning |
|---|---|
invalid_x402_version · unsupported_scheme · invalid_network | Payload or requirements don't match this facilitator. |
wrong_tab_manager | extra.tabManager is not the contract this facilitator settles on. |
invalid_payee · invalid_amount · invalid_resource | The intent doesn't pay exactly the seller's price, to the seller, for this URL. |
intent_expired · unknown_line · invalid_signature | Deadline passed, line doesn't exist, or the signer isn't the line's agent. |
| Contract error names | The dry run of pay reverted, for example AboveBudget or PastDue. |
RPC_URL=… TAB_MANAGER=0x… NETWORK=base-sepolia RELAYER_PRIVATE_KEY=0x… npm run facilitator # :4020For lenders
CreditPool is an ERC-4626 vault. Deposit USDC and receive tpUSDC shares. Lent money stays on the books as outstanding, so share value never drops when the pool funds a tab.
- Half of every seller fee is paid into the pool and unlocks into share value over 24 hours (
unvestedYieldshows what is still locked), so a deposit made just before a payment earns nothing from it. The other half goes to the treasury. - The pool never lends more than 90% of its assets, so at least 10% stays available for withdrawals.
- Withdrawals are limited to idle cash (
maxWithdrawandmaxRedeemreport the current limit). - A deposit cap applies during testing (10,000 USDC by default).
Score API
Score indexes x402 payments on Ethereum and Base (USDC transferWithAuthorization settlements), removes wash activity and rates each payer from 0 to 1000. Every component is returned, so you can see why an address scored what it did.
| Route | Returns |
|---|---|
GET /score/:address | Score, components, metrics and agentLikely. 404 if the address never paid over x402. |
GET /stats | Raw and screened payment volume, broken down by screening reason. |
GET /top?limit=20 | Highest-scoring payers (up to 100). |
GET /agent/:agentId | ERC-8004 feedback for an agent: count, distinct clients, average value, tags. IMD seats included. |
GET /pnl/:address | Weekly profit and loss from x402: paid, received and net, newest week first. |
GET /profile/:query | Agent profile by ERC-8004 agent id, wallet or ENS name: identity, score and rank, x402 history, feedback, IMD and Claus activity, balances, and funding one hop deep (personal wallets shortened, never traced further). |
GET /proof/:address | The payer's leaf and Merkle proof in the latest published snapshot, ready for ScoreRegistry.verify or record. |
GET /claus | Claus Jobs deals indexed from its escrow on Ethereum. |
GET /imd | Orders paid to IMD's agent swarm, indexed from Ethereum. |
Screening
Follows the method TRM Labs used to measure real agent payments. A payment is dropped if it is:
- a self-payment (payer and payee are the same address);
- to a seller with fewer than 10 distinct buyers;
- to a seller whose top two payers bring more than 90% of its volume;
- part of a circular pair, where the payee has also paid the payer.
Components
| Component | Max | Measures |
|---|---|---|
| Breadth | 250 | Distinct sellers paid (full marks at 20) |
| Tenure | 200 | Days since first clean payment (full marks at 90) |
| Consistency | 150 | Share of weeks with activity since the first payment |
| Volume | 200 | Screened volume on a log scale (full marks at $10,000) |
| Cleanliness | 200 | Share of the payer's volume that survived screening |
agentLikely is true for payers with 20 or more clean payments, an average under $1, at least 2 distinct prices and at least 2 sellers.
CHAINS=ethereum,base RPC_URL_ETHEREUM=… FROM_BLOCK_ETHEREUM=… \
RPC_URL_BASE=… FROM_BLOCK_BASE=… npm run score # :4030
curl localhost:4030/score/0xYourAgentContract reference
TabManager
| Function | Caller | Does |
|---|---|---|
openLine(agent) | Anyone | Opens a line owned by the caller. |
setAgent(lineId, agent) | Owner | Rotates the agent key. Sets a fresh epoch. |
depositCollateral(lineId, amount) | Owner | Adds USDC collateral. |
withdrawCollateral(lineId, amount, to) | Owner | Withdraws collateral not securing the balance. |
setMandate(…) · revokeMandate(lineId) | Owner | Replaces or removes the mandate. Sets a fresh epoch. |
cancelNonce(lineId, nonce) | Owner or manager | Cancels one signed intent. |
setDelegate(lineId, worker, cap, validUntil) | Agent, owner or manager | Gives a worker key a budget on the line (sub-tab). 0, 0 revokes. |
setSweep(lineId, earner, bps) | Owner or manager | Repays the line from bps of what its agent or owner is paid through Fivebar. |
setManager(lineId, manager) · acceptManager(lineId) | Sponsor · manager | Hands day-to-day control of a sponsored line to someone else (vouching). |
claimVoucherFees(to) | Voucher | Collects the voucher's share of fees from lines it vouched for. |
openSponsor() · depositSponsorCollateral(id, amount) | Anyone | Opens a sponsor account and funds it. |
openSponsoredLine(id, agent, cap) | Sponsor owner | Opens a line backed by the sponsor’s deposit. |
setLineCap(lineId, cap) | Sponsor owner | Changes one agent’s cap. 0 freezes it. |
withdrawSponsorCollateral(id, amount, to) | Sponsor owner | Withdraws deposit not securing a balance. |
pay(intent, signature, proof) | Anyone | Executes a signed intent. Returns the escrow hold id, or 0. |
repay(lineId, amount) | Anyone | Repays billable balance, statement first. |
liquidate(lineId) | Anyone | Seizes collateral once a statement is past due plus grace. |
sweepFees() | Anyone | Sends accrued treasury fees to the treasury. |
Views on TabManager: getLine, outstandingOf, freeCollateral, delegates, hashIntent. Views on TabLens (a read-only companion that keeps TabManager under the size limit): statementOf (billed, unbilled, held, dueAt, cycleEnd), isPastDue, creditLimit, availableCredit.
Parameters
| Name | Default | Limit |
|---|---|---|
sellerFeeBps | 100 (1%) | At most 500 (5%) |
poolFeeShareBps | 5000 (50%) | Share of each fee paid to lenders |
maxLtvBps | 10000 (100%) | Capped at 100% in v0 |
billingPeriod | 7 days | 1 to 31 days |
paymentWindow | 3 days | 1 to 14 days |
gracePeriod | 2 days | Up to 14 days |
maxUtilizationBps (pool) | 9000 (90%) | Share of pool assets that can be lent |
Errors from pay
| Error | Cause |
|---|---|
UnknownLine | No line with that id. |
BadSeller | Seller is zero or a protocol address (pool, escrow, treasury, TabManager). |
IntentExpired | Past the intent's deadline. |
StaleEpoch | The mandate or agent changed after signing. |
DelegateInactive · AboveDelegateCap | The worker's budget expired, was revoked by a mandate or agent change, or is used up. |
AmountTooSmall | The fee would take the whole payment (for example 1 unit at 1%). |
NonceUsed | Already paid or cancelled. |
BadSignature | Not signed by the line's agent. |
MandateInactive | No mandate, or past validUntil. |
AboveMaxPerPayment · AboveBudget | Breaks the mandate's per-payment cap or total budget. |
SellerNotAllowed | Seller not in the mandate's allowlist. |
PastDue | A statement is past its due date. |
AboveCreditLimit(requested, available) | Not enough collateral for this payment. |
UtilizationCap(lentAfter, maxLent) | The pool would go above 90% utilization. |
ScoreRegistry (Ethereum, mirrored on Base)
| Function | Caller | Does |
|---|---|---|
publish(epoch, root, asOf, count, version, uri) | Publisher | Posts one Merkle root for a daily epoch. Epochs only move forward and never past today. |
verify(epoch, leaf, proof) | Anyone (view) | True if the score is in a live epoch. |
record(epoch, leaf, proof) | Anyone | Stores a proven score. Records only move forward in time, even after a revoke. |
scoreOf(subject) | Anyone (view) | Recorded score, agent flag, epoch and asOf. Check freshness yourself. |
revoke(epoch) | Admin | Withdraws a bad snapshot. The publisher can replace the latest one the same day; scores recorded under it stay dead. |
Each leaf: subject, score, the five components, screenedOutBps, agentLikely, keptVolumeCents, firstSeen. Leaf hash: keccak256(bytes.concat(keccak256(abi.encode(epoch, leaf)))).
Events
LineOpened, AgentSet, MandateSet, MandateRevoked, NonceCancelled, Paid, Billed, Repaid, RefundCredited, FeeDistributed, FeesSwept, Liquidated. Escrow emits Opened, Flagged, Released, Refunded.
Deployments
| Network | Contract | Address |
|---|---|---|
| Base Sepolia | USDC (Circle) | 0x036CbD53842c5426634e7929541eC2318f3dCF7e |
| Base Sepolia | CreditPool | 0x59b1597058C3315ac70324415cbD69F423579872 |
| Base Sepolia | TabManager | 0x41A118E004140C39bc82721625807bD68255cf97 |
| Base Sepolia | Escrow | 0xE80D0c0c70F174e15C2928d66f8805B5B34a53eC |
| Base Sepolia | ScoreRegistry (mirror) | 0x41E8A1B858B0fC41fdEABFdB8e1ed152AB6d8DEb |
| Base Sepolia | AgentAccounts (prepaid, Earn) | 0xB08e1E96BAE4697E6eB6B17B81bEAB7C189B8D19 |
| Base mainnet | CreditPool · TabManager · Escrow | After an external audit |
| Sepolia | ScoreRegistry | 0x74104F0b50092c474d948FC0c346fD25212EB2c5 |
| Sepolia | CreditPool · TabManager · Escrow (job-sized test setup) | Not deployed yet |
| Ethereum mainnet | ScoreRegistry | After review |
| Ethereum mainnet | Warden NFTs · token · governance | Phase 3 and later |
| Robinhood Chain | ERC-8004 IdentityRegistry · USDG | 0x8004A169FB4a3325136EB29fA0ceB6D2e539a432 · 0x5fc5360D0400a0Fd4f2af552ADD042D716F1d168 |
| Robinhood Chain | CreditPool · TabManager · Escrow | After an external audit |
| Solana | Payment contracts | Later |
Security
Tested properties: the pool's books always equal the sum of every line's balance; no tab ever exceeds its collateral; TabManager and Escrow hold exactly what they should; lenders never lose principal while the admin key is honest (on mainnet it is a Safe behind a timelock); intents cannot be replayed across chains, contracts, lines or nonces.
| Date | Review | Result |
|---|---|---|
| 2026-10-22 | Internal review | 3 medium, 5 low. All fixed with regression tests. |
| 2026-10-22 | pashov solidity-auditor v4.1 | 3 findings and 16 leads. All findings fixed; 7 leads addressed. |
| 2026-10-23 | Independent review of sponsored lines | 1 low. Fixed with a regression test. |
| 2026-10-24 | IMD agent swarm | 6 low, 3 info. 8 fixed with regression tests; 1 trust assumption documented. |
SECURITY.md in the repository.