CIP-28: Cowboy Agent Banking
- Status: Draft
- Date: 2026-05-12
- In scope: BankActor system actor, card data model, instruction set, gas charge path, the policy triad (limits / whitelist / freeze), multi-bank + fiat bridge, roadmap & compatibility
- Out of scope: On-chain KYC, multi-holder cards, protocol-level paymaster abstraction, card-to-card transfer primitives, UI design (delivered separately — see
examples/cip28_agent_banking/index.html)
0. Summary
Decouple “gas funds + risk controls + compliance handle” from regular actor addresses, and lift them into a first-class banking account primitive. A new system actorBankActor (0x0D):
- Card = on-chain counterpart of a physical bank card: deterministically derived 20-byte address, holds multi-token balances (vault model), supports expiry/renew, spending limits, whitelist, freeze, ownership transfer.
- agent = cardholder, owner = guardian (initially a user; later may transfer to the agent itself). Separation of duty.
- Third gas-charge path: when a tx’s
fee_payer_overridepoints at a card address, the BankActor validation pipeline kicks in — coexists with the current actor-pays / owner-pays paths. - Single compliance perimeter: nothing else in the Cowboy ecosystem (actor / token / session / cbss) needs to be compliant — the compliance handle lives inside BankActor + each bank’s operator + off-chain gateway.
- Funding narrative: “Every agent in the Agent era needs a banking account; traditional banks don’t support that; we do.”
1. Architecture Overview & System Actor Position
1.1 Positioning
Cowboy Agent Banking is a new system actorBankActor at address 0x0000…000D. It carries four responsibilities:
Protocol / Bank — two layers: this CIP defines the protocol Cowboy Agent Banking (= the BankActor primitive + card derivation rules); the first bank deployed on top of it at genesis is Cowboy Banking (bank_id = 1). The latter is just the first instance of the former — analogous to Visa (network) vs. Chase (issuer).
- Card lifecycle (issue / renew / close / transfer ownership)
- Multi-token balance custody (card address is itself a token holder)
- Risk controls (rolling-period spending limits, receiver / syscall whitelist, freeze)
- Fiat-bridge mint voucher verification (FiatMintVoucher signed by the off-chain gateway)
1.2 Position in the protocol stack
1.3 Relation to existing systems
1.4 Roles & authorities
2. Data Model & Card Address Derivation
2.1 Top-level state layout (under BankActor 0x0D)
Following theb"<tag>:" ASCII prefix style used by other system actors (CBSS uses b"secret:", SessionActor uses b"session:", CIP-20 token uses b"bal:"):
Token balances themselves are not stored in BankActor — the card address is a plain token holder; balances live in the existing CBY ledger / CIP-20 token actor. BankActor only stores non-balance card metadata.
2.2 BankEntry
bank_id = 1, operator = Cowboy Banking operator multisig (signer set determined by CIP-12 governance).
2.3 CardEntry
gas_payment_token may only be Native CBY or the official stablecoin U (a whitelist of token addresses configured in genesis). Other CIP-20 tokens may sit on the card as reserves / payroll, but cannot directly pay gas.
2.4 CardPolicy
gas_payment_token wei, not gas units.
2.5 SpendWindow
Fixed-window (not sliding) for day-1 simplicity:
2.6 Card address derivation
agentis in the derivation formula → a card is intrinsically bound to one agent; re-binding = must issue a new card (matches the “one card, one identity” mental model).owneris in the derivation formula but acts only as salt →TransferOwnershipdoes not change the address, onlyCardEntry.owner(otherwise transferring ownership would invalidate every reference to the card — UX disaster).issue_noncelets the same (bank, owner, agent) tuple yield arbitrarily many cards (expiry → re-issue → new nonce → new address).
2.7 Default card resolution
When the engine processes a tx, it resolvesfee_payer in this order:
3. Instruction Set
BankInstruction enum, dispatched in the style of SessionActor.
3.1 Owner-submitted
3.2 Agent-or-Owner submitted
Two-key model, last-writer-wins. Lets the guardian set it on the agent’s behalf early on; lets the agent switch it later once grown.
3.3 BankOperator submitted (the compliance handle)
MintFromFiatVoucher is broadcast-by-anyone; the signature must come from fiat_mint_signer. That puts the “when does it land on chain” key in the user’s hands.
3.4 Governance submitted (CIP-12 governance proposal)
3.5 Engine internal call (not a tx instruction, not addressable)
3.6 Events
4. Gas Charge Path
4.1 Engine fee-settle fork point
b"card:" || addr lookup. Bloom-filter caching is on the roadmap.
4.2 BankActor.charge_gas pipeline
Phase 1 — Pre-flight Reserve (at block admission)
Phase 2 — Post-execution Settle (after handler returns)
Async discipline (per the lesson from commit90c3073): BankActor handlers stayasync fn+.awaitthroughout (same as CBSS handlers inexecution/src/cbss.rs). Storage IO triggered from a PVM handler call stack insidecharge_gas(reads onb"bank:"/b"card:") must return via the async path; when a!Sendfuture must be driven, reuseexecution::actor_instruction::block_on_local, do not usefutures::executor::block_on— the latter panics under nested executors (EnterError).
4.3 Timer-deferred tx specifics
Today: timers pre-chargemax_cost from fee_payer_override at scheduling time; at firing time the tx is “already paid”. With cards involved:
Implication: limits are anchored to the scheduling moment, not the firing moment. The guardian can see “the kid is queueing up tasks again” at the start of the month — no surprise at month-end.
4.4 Edge cases
4.5 Error codes
NewBankErr::* family; the engine’s top-level ErrorMap maps them uniformly:
4.6 Receipts & Indexer
GasCharged events carry tx_digest, so the Indexer can join: each tx → 1 GasCharged event (if fee_payer is a card). The UI’s “card statement” view = all events on that card, sorted by time.
5. Policy Triad: Semantics in Detail
5.1 Limits (per-hour / per-day / per-month)
Unit & meaning
- Unit = wei of
card.gas_payment_token(not gas units) - The three tiers are independent; any failing tier rejects the charge. Monotonicity is not enforced.
None= no cap on that tier
Window constants (compile-time constants in BankActor, governance-tunable)
Block-based instead of wall-clock for determinism.
Period-id
Cap-rejection error precision
BankErr::CapExceeded { tier: Hour | Day | Month, would_be: u128, cap: u128 } — the UI can directly render “Monthly cap is 100 U; this tx would push the total to 103 U”.
5.2 Whitelist
Two independent whitelists; both must pass.allowed_receivers: Vec<Address>
Primary receiver = tx’s
to field; for multi-instruction txs, the target of the first instruction. System actors match by address as well.
Capacity cap: ≤ 64. Larger sets → encourage splitting into multiple cards.
allowed_syscall_kinds: Vec<SyscallKind>
A fixed instruction → SyscallKind mapping (BankActor compile-time constant):
Capacity cap: ≤ 16.
Multi-instruction txs
For each instruction the (receiver, syscall_kind) pair must pass; any failure rejects the entire tx.Blacklist not in day-1
The whitelist + freeze combination already covers the compliance story; an explicit deny-list is semantically redundant.5.3 Freeze authority & state machine
Who can freeze
- Only
bank.operator(Cowboy Banking = Cowboy Banking operator multisig; third-party = their own multisig) - The owner cannot freeze (use
SetPolicy { caps = Some(0) }orCloseCardinstead) - The agent cannot freeze
State transition matrix
Frozen permits Deposit — matches a real bank’s behavior when investigating a suspicious transaction.
Reason field
Freeze.reason: Vec<u8> capped at 256 bytes, recorded on chain + included in the event.
Unfreeze
Operator only; ifblock_height ≥ expires_at at unfreeze, the card lands in Expired (owner must Renew).
5.4 locked_after_transfer precise semantics
5.5 Roadmap notes
6. Multi-Bank + Stripe Fiat Bridge
6.1 Multi-bank: registration & isolation
Cowboy Banking at genesis
Third-party bank registration
ViaRegisterBank. Caller must be authorized via CIP-12 governance. Self-service registration by arbitrary third parties is not allowed — hanging the “Cowboy on-chain bank charter” requires governance approval.
Inter-bank state isolation
A third-party bank is fully isolated after registration:- Same
BankInstructionset, distinguished bybank_id - Third-party operator can only freeze / pause cards under its own bank
- Trouble at one third-party bank does not affect Cowboy Banking
- Cross-bank fund movement = ordinary token transfer + Deposit/Withdraw sequence — no dedicated instruction
Cross-bank card migration: not supported
The card address hasbank_id in its derivation formula; changing banks would change the address. Treated as “close then reopen”: CloseCard → IssueCard.
6.2 Fiat bridge
On-chain / off-chain responsibility split
Trust assumptions
Voucher anti-forgery
- Signing domain
keccak256("CowboyBankFiatMint\x01" || rlp(voucher)), includes bank_id voucher_id32 bytes; recommendedhash(stripe_charge_id || chain_id || bank_id)expires_at_blockrecommended ~24 hours of block-equivalentfiat_referencestores the Stripe charge_id (or its hash) on chain for audit reconciliation
Off-ramp roadmap
Day-1 designs the on-ramp only. Off-ramp (on-chain balance → fiat) is heavier on compliance — placeholder instruction nameBurnToFiatRequest, deferred to v2.
6.3 Stripe integration (off-chain interface, design only)
fiat_mint_signer signature.
6.4 Compliance perimeter
7. Roadmap & Compatibility
7.1 Relation to the three existing charge paths
7.2 Rollout milestones
M1+M2 is the minimal demoable combination.
7.3 Feature gate
New governance parameterbank_activation_height: u64:
- Below this height: BankActor does not exist;
fee_payer_overridepointing at a card address → treated as a missing EOA → OutOfFunds - Above this height: BankActor is active; the §4.1 fork goes live; the genesis Cowboy Banking entry is materialized
7.4 State migration impact
- Existing state untouched: actor / token / session / cbss / entitlement / storage are zero-invasion
- Only additions, no edits:
b"bank:" / b"card:" / b"card_by_*:" / …are all new namespaces, no collision with existing system actors - The existing
ScheduledTimer.fee_payer_override(timer subsystem) is left alone; this CIP introduces a separatetx.fee_payer_override: Option<Address>at the tx top level — same name, different location, no interference
7.5 Cross-references with existing CIPs
7.6 Roadmap
7.7 Day-1 hard-noes
- ❌ On-chain KYC (PII stays off chain)
- ❌ Cards with multiple holder agents (breaks the “one card, one identity” narrative)
- ❌ Protocol-level paymaster abstraction (separate storyline)
- ❌ Card-to-card transfer primitives (plain token transfers suffice)
- ❌ Reusing SessionActor for banking (already ruled out at proposal selection)
7.8 Risks & open questions
- Cap units vs basefee volatility: the user sets caps in token units (CBY or U), but
cycle_basefee/cell_basefeeeach move independently under CIP-3 dual-metered EIP-1559. The UI needs to show “how many tx you can run at the current dual-metered basefee”. - Cowboy Banking operator governance: the Cowboy Banking operator multisig is held by Cowboy Labs in the early phase → “is this a centralized bank?” will come up in investor diligence; the narrative should land on “decentralization roadmap — see CIP-12 governance” (note: this is not the same as Cowboy Foundation from CIP-12 §3.1 — Foundation has zero protocol authority).
- The minimum set for third-party banks at M5: a complete story needs at least one third-party bank actually live by M5; the recommendation is to line up a partner in parallel.
- Stripe chargeback vs. on-chain voucher timing: gateway-internal parameter (48h / 72h?), not specified by this CIP, but should appear on the review checklist.
Appendix A — Glossary
Appendix B — Mapping to meeting points
End of document. Ready to enter the implementation-plan phase (CIP-28 landing in M1–M5 stages).

