Tab Docs

Threat model

This document enumerates every trusted component in Tab, states the exposure each one carries, and draws the line past which nothing is trusted at all. The organising fact is that the trusted set contains parties who can admit a counterparty, meter inside a ceiling, serve a stale figure, or waste their own Bond, and contains no party who can claim that money arrived.

Trusted components

ComponentTrust grantedBound on the damageMitigation
Curation authority: the 2-of-3 CurationMultisig on Mainnet, the deploying account on Testnetmove a Service to the Curated tier, and nothing elsecan admit a colluding Service into credit computation; cannot mint credit beyond that Service's Bond, cannot move Agent funds, cannot apply a Settlement, cannot touch a tabevery tier change is queued, held 48 hours, and announced by RegistryChangeQueued before RegistryChangeApplied, so a pending promotion is visible before it takes effect; the authority is fixed at construction and cannot be reassigned, so the role cannot be quietly moved to another key; a tier change from any other address reverts NotCurationAuthority
Service operatorrecord deliveries against an Agent's tab, queue changes to its own prices, Collection addresses, accepted Assets and Settlement Window, deposit and withdraw its own Bondcan charge only inside an unexpired spending authorisation the Agent set, and only at prices from its applied timelocked price list; can waste its own Bond; cannot charge an Agent that has not authorised it, cannot lower a tab, cannot read an Agent's balancethe authorisation ceiling is Agent-set and reverts AuthorisationExceeded past it; price changes take 48 hours and PriceListChangedMidCall blocks re-pricing inside a call; a delivery against a delinquent tab reverts TabIsDelinquent; the Agent can let an authorisation lapse
Registry operatorindex events and serve them over the read APIcan serve a stale figure or withhold one; cannot serve a wrong Credit Limit, because every figure it serves is cross-checked against TabBook.creditLimit on chain and anyone can recompute it from HistoryExtended logsa figure whose cross-check fails is served as unavailable with its reason; the Dashboard shows the block a figure was read at; nothing the registry serves is an input to any contract
Gateway operator, which is the Service operator's own infrastructurehold the operator key and meter on its behalfthe same bound as the Service operator, because it is the Service operator's key; can additionally waste gas on deliveries that revertevery metering request must carry a signature over the Agent, tool and unit count, from the operator, from the Agent itself, or from a delegate the Agent registered in MeteringDelegates; the gateway simulates recordDelivery before broadcasting it
Settlement relayer, which is the gateway operator's own infrastructuresubmit a Permit2 Settlement the Agent signed, and pay its gascan decline to submit, or submit late; cannot alter what is paid, because the witness the Agent signed binds the Service, Asset, amount, surface and chain, and Permit2 refuses any other use; cannot replay, because the nonce is spent on chainthe relay simulates before it submits and returns every refusal by name; an Agent that is refused can submit the same signature itself, or through anyone else, because settleWithPermit2 is permissionless
Fronting key, the key a Service pays x402 upstreams withpay the Hub or another upstream on the Service's behalfcan be drained by the Service's own callers only inside their headroom, because the delivery is simulated against TabBook before the upstream is paid; an Agent with no headroom costs the Service nothingthe key can be a dedicated one, GATEWAY_X402_PRIVATE_KEY, holding only what the Service is willing to front; the operator key is used only when it is unset
x402 facilitator, Monad's, on the prepaid path onlyverify and settle an EIP-3009 authorization the Agent signed for one callcan decline, or settle late; cannot move more than the authorization names or to anyone but the payTo in it; is never in the path of a Settlement, a limit or a taba payment that does not verify delivers nothing; a delivery that failed is never settled; the offer is optional and an Agent that ignores it keeps the 402 it was given
The Agent's own keysmove the Agent's own funds and set its own authorisationsno component of Tab holds themthe SDK never persists a key and the CLI never writes one into a client configuration; TabSettlement pulls only what the Agent approved and moves it only to a registered Collection address
Privy, for an Agent whose key is in a Privy server walletsign for the Agent, when asked, what the wallet's policy allowsa policy built by buildPrivyAgentPolicy allows only Tab's own calls on one chain with a value of zero, a Permit2 witness to TabSettlement, and metering claims, plus bounded x402 payments only when x402MaxBaseUnits adds them; Privy enforces it off chain, and the contracts treat the wallet like any other accountthe policy and the wallet are owned by a key that is never on the Agent's machine, so the Agent's credentials cannot loosen the policy; every signature is recovered and every signed transaction decoded and compared with the request before it is used
Reputation writer, which is the gateway operator's own infrastructurewrite one ERC-8004 feedback entry about the paying Agent after each Settlement the Service receivescan write a misleading entry under its own name, or none; cannot change a Settlement, a tab or a Credit Limit, because nothing in Tab reads the Reputation registryeach entry points at a document naming the Settlement's transaction and carries its hash, so anyone can check it against the Settled event
A passkey account on the Dashboardderive the owner and session keys from one passkey's PRF output, in the page, for the sessionthe seed exists in memory while the passkey is signed in and is zeroed when the session ends; the browser stores credential metadata and a key count, never a seed or a keythe owner key is never revealed; a session key is revealed once, deliberately, and the record says which ones have been; a reload forgets everything and one touch re-derives it
Deployerone-shot wiring at deploymentcan mis-wire once, in public; cannot re-point a wired contractsetSettlementSurface reverts AlreadyWired once set, the wiring authority is fixed at construction, and the verification script reads every address back keylessly

There is deliberately no component that can assert a Settlement. TabBook.applySettlement answers to one address, the wired TabSettlement, and TabSettlement calls it only inside the transaction that moves the Asset from the Agent to the Service's Collection address. The transfer and the ledger entry succeed together or revert together, so the question of who reports a payment never arises.

Where trust stops

The contracts know exactly what happened on their own chain, and Tab is designed around that rather than around anything softer.

  • Credit is backed by Service Bonds and never by Agent balances, because a balance is not a repayment record and no balance is ever read.
  • Delinquency is a timeout on the chain's own clock: once a tab's Settlement Window has closed with the tab open, anyone may mark it, and the mark zeroes credit rather than taking anything from anyone.
  • A Bond is real escrow, funded by transferFrom into the Bond contract and paid out only to the account that staked it; the free amount TabBook reads is staked minus withdrawn, and no other contract can move it.
  • A Settlement is recognised only from TabSettlement itself, so a plain token transfer to a Collection address is not a Settlement and does not lower a tab.
  • A Settlement is final when its block is, and Monad finalises a block in under a second, so there is no window in which a tab has fallen for a payment that might not stand.

What a malicious Service can do

A Service operator holds a key that can charge, so the question is how far that reaches.

It can meter deliveries the Agent did not receive, at its applied prices, up to the ceiling the Agent set in TabBook.authorise and no further. That ceiling is the whole reason the authorisation exists, and it is the Agent's: the Service cannot raise it, extend it, or meter without it. An Agent that sets maxCumulative to what it is willing to lose to a dishonest Service has bounded its exposure exactly, and letting the authorisation expire ends the relationship without any further transaction.

It cannot charge more than the price on chain, because recordDelivery carries the quoted unit price and reverts PriceListChangedMidCall if the applied price differs. It cannot change that price in less than 48 hours. It cannot lower a tab, mark one delinquent early, or touch a Settlement, because none of those paths take the operator's key. And it cannot vouch for an Agent's credit beyond its own Bond, because LimitLib caps the limit at 95 percent of the counterparties' free stake.

What a delinquent Agent costs

An Agent that never settles costs the Service the balance it carried, for the length of the Settlement Window. That is the Service's exposure, and it chose it: the window is a registration parameter, at most 24 hours, and the Credit Limit the Agent could reach was capped by Bonds the Service and its peers posted knowingly.

Once the window closes, anyone may call markDelinquent, and from that block the Agent's Credit Limit in that Asset is zero everywhere in the deployment, not only with the Service it stiffed. Every Service reading the Agent's limit sees the zero, because TabBook counts delinquent tabs per Agent and Asset before it computes anything. A Settlement that brings the tab back to zero clears the mark in the same transaction. Nothing is taken from the Agent beyond that, because nothing of the Agent's was ever held.

Wash-settlement bounding

An Agent and a Service under common control can settle back and forth to manufacture history. The Bond invariant bounds what that buys: the credit unlocked by a history is at most floor(sum(counterpartyBonds) * 9500 / 10000), which is strictly less than the Bonds the counterparties posted. To unlock X of credit a ring must lock strictly more than X in Bonds, in the same Asset, on Services holding the Curated tier. Four further terms compound the cost.

  • At least 3 distinct Curated bonded counterparties are required before any growth above the baseline.
  • No single counterparty may contribute more than 25 percent of the limit, so a ring needs breadth and not only depth.
  • Age weighting gives a burst of self-settlement a quarter of the weight of the same value seasoned over 30 days.
  • A Settlement counts only where a delivery for that Agent, Service and Asset strictly predates it, so the ring must also produce real metered records on chain.

The pure library that computes the limit guarantees the inequality over whatever Bond set it is handed; TabBook guarantees that the set is the real counterparties of that Agent, read from the Bond ledger and never supplied by the caller. A witness that cites a Bond the Agent has no relationship with reverts IneligibleBondEntry.

Residual risk: collusion between a bonded Curated Service and an Agent remains possible, and it is bounded by the Bond sum.

The witness is authenticated, not trusted

Every limit read takes a LimitWitness from the caller, and both halves of it are checked before anything is computed. The history is folded from zero into the same rolling hash the Settlement path writes and compared against the stored historyCommitment, so a record added, dropped, reordered or edited reverts HistoryCommitmentMismatch, and a history of the wrong length reverts HistoryLengthMismatch. Every Bond amount in the witness is discarded and read from Bond at the moment of the call. The witness can therefore choose which counterparties to cite and nothing else, and omitting one can only lower the limit.

Settlement identity and batching

A Settlement is identified by the settlementId that TabBook assigns, derived from the chain id, the book's own address and a nonce that only increments. It is unique per Settlement rather than per transaction, so a settleBatch carrying several Settlements has each one recorded, indexed and displayed on its own, and the same identifier can never be assigned twice. Because the payment and the record are one transaction, there is no second submission to guard against: a Settlement cannot be applied without moving the Asset again.

Network-exposed services

The registry read API and the Dashboard's read routes under /api are unauthenticated by decision, because every field they serve restates a public chain fact or the process's own liveness; a rate limit at the deployment's edge is what they owe. The Dashboard's /api/try is the one that spends: it signs a metered call for the site's demonstration Agent with the operator key, so it reaches only the endpoints this repository publishes, never a URL the request names, and on Mainnet it is rate-limited per IP address per minute and in total per day. The gateway's metering endpoint is different: it must authenticate the caller by signature, the Service operator's, the Agent's own or a delegate's the Agent registered, and identify the Agent by the Tab-Agent header, because an unauthenticated metering endpoint would let anyone charge any Agent up to its authorisation ceiling. The Agent's signature is recovered against the address it names, so a stranger who knows an Agent's address can put nothing on its tab; the operator's is the Service's own front vouching for a caller it authenticated. A delegate's is recovered against the key it names and then checked on chain, on every call, as one the Agent registered and has not let lapse or revoked, so a revocation takes effect on the next call; a leaked delegate key can cause metered calls at the listed price within the Agent's own authorisation ceilings until that expiry or revocation, and can move no funds, because no Settlement path reads MeteringDelegates. That authentication is load-bearing and is not deferrable. A prepaid call is open by the same decision: it carries a payment signed to this Service's own Collection address for the exact charge, nothing is metered and no gas of the operator's is spent, so requiring a metering signature there would only make the x402 offer unusable by the callers it is for. /relay/settle is open too: the body it accepts can do nothing but pay the Service the Agent signed for, and a bad body costs the relayer one simulation. The /hub routes are metered like any other route, and the upstream is paid only after the delivery has been simulated against the caller's headroom. The keeper's /tick is behind a shared secret, not because a mark is dangerous, since anyone may mark a tab, but because a public trigger is a free way to make the keeper spend gas. Inside the SDK, the server-side plugin treats Tab-Agent and Tab-Authorisation as claims and not as authentication; TabBook derives the authorisation key itself from the Agent, the Service and the Asset, so a header cannot redirect a charge, and metering is bounded by the on-chain authorisation the Agent set.

Other exposures

  • The credit history a Credit Limit is computed from is bounded at 512 records and 32 counterparties, and HistoryTooLong fails loudly rather than truncating credit silently.
  • A Settlement against a tab with nothing open banks as prepaid credit rather than failing, so money sent to the wrong Service by mistake is held on that tab rather than refunded; it is never lost and it is visible on the Agent's page.
  • A Service's endpoint URL is not on chain, so an Agent's tab.config is the authority on where a Service runs, and a wrong endpoint there reaches a wrong host with no charge, because only the registered operator can meter to the Agent's tab.
  • The 48-hour timelock protects Agents from a price moving under them and equally delays a Service rotating a compromised Collection address; in the meantime TabSettlement pays the address on chain, so an Agent settling against a compromised Service pays the attacker until the change applies.

Static analysis

The contracts in scope here have been through Slither, and every result is triaged in the Slither report. Slither reports 56 results and none is an exploitable vulnerability: one is a gas optimization, 29 are false positives, and 26 are accepted by design, such as the timestamp comparisons behind the 48-hour timelock and the Settlement Window. The report also records one liveness limit found by hand: past 512 Settlements in one Asset, the history bound above stops every delivery to that Agent in that Asset, including one prepaid credit would cover.

Keys and secrets

No private key appears in any tracked file; .env.example carries shape hints only. The design gives the gateway, the curation authority, the deployer and each Agent separate keys, and nothing in the contracts requires any two of them to be one account. The demo deployment is run by one person and does not separate them yet: one account deployed the Testnet contracts, operates tab.demo on both networks, holds the Testnet curation role and is one of the three Mainnet multisig owners. The hosted keepers are the exception: each sends its marks from a key of its own that holds only MON for gas. The SDK reads AGENT_PRIVATE_KEY at the moment a Settlement, a Permit2 witness or an x402 payment is built and never earlier, and connect refuses to write anything key-shaped into a client configuration. A Permit2 witness and an EIP-3009 authorization are each single-purpose by construction, so a signature that leaks can do the one thing it was signed for and nothing after it.

On this page