Skip to main content

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 actor BankActor (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_override points 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.”
Seven main sections, scoped tightly enough to drop straight into an implementation plan.

1. Architecture Overview & System Actor Position

1.1 Positioning

Cowboy Agent Banking is a new system actor BankActor 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).
  1. Card lifecycle (issue / renew / close / transfer ownership)
  2. Multi-token balance custody (card address is itself a token holder)
  3. Risk controls (rolling-period spending limits, receiver / syscall whitelist, freeze)
  4. 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 the b"<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

Cowboy Banking is written at genesis with bank_id = 1, operator = Cowboy Banking operator multisig (signer set determined by CIP-12 governance).

2.3 CardEntry

Day-1 constraint: 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

Cap unit is gas_payment_token wei, not gas units.

2.5 SpendWindow

Fixed-window (not sliding) for day-1 simplicity:
On each charge: check the period_id; if it doesn’t match what’s stored → reset the accumulator and bump the period_id; otherwise accumulate and check against the cap. Roadmap: true sliding windows (deque + upper bound) deferred to v2.

2.6 Card address derivation

Key design tradeoffs:
  • agent is 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).
  • owner is in the derivation formula but acts only as salt → TransferOwnership does not change the address, only CardEntry.owner (otherwise transferring ownership would invalidate every reference to the card — UX disaster).
  • issue_nonce lets 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 resolves fee_payer in this order:
Day-1 is backward-compatible with the existing owner-pays / actor-pays paths; cards are layered as a third path.

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

Cost of “is this a card?”: one 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 commit 90c3073): BankActor handlers stay async fn + .await throughout (same as CBSS handlers in execution/src/cbss.rs). Storage IO triggered from a PVM handler call stack inside charge_gas (reads on b"bank:" / b"card:") must return via the async path; when a !Send future must be driven, reuse execution::actor_instruction::block_on_local, do not use futures::executor::block_on — the latter panics under nested executors (EnterError).

4.3 Timer-deferred tx specifics

Today: timers pre-charge max_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

New BankErr::* 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

At reserve time, if the stored period_id mismatches: clear the accumulator, bump period_id, then check the cap. Refunds credit the current period; cross-period drift is not reclaimed.

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) } or CloseCard instead)
  • 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; if block_height ≥ expires_at at unfreeze, the card lands in Expired (owner must Renew).

5.4 locked_after_transfer precise semantics

Meaning: before handing over, the guardian can tick “I don’t want the kid to relax the limits”. Once ticked + transfer is complete, the agent cannot SetPolicy either — only issuing a new card works.

5.5 Roadmap notes


6. Multi-Bank + Stripe Fiat Bridge

6.1 Multi-bank: registration & isolation

Cowboy Banking at genesis

Third-party bank registration

Via RegisterBank. 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 BankInstruction set, distinguished by bank_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 has bank_id in its derivation formula; changing banks would change the address. Treated as “close then reopen”: CloseCardIssueCard.

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_id 32 bytes; recommended hash(stripe_charge_id || chain_id || bank_id)
  • expires_at_block recommended ~24 hours of block-equivalent
  • fiat_reference stores 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 name BurnToFiatRequest, deferred to v2.

6.3 Stripe integration (off-chain interface, design only)

Zero invasion on the on-chain side: BankActor only recognizes the fiat_mint_signer signature.

6.4 Compliance perimeter

In one sentence: everything else on Cowboy doesn’t need to be compliant — only the banking layer does, because banking is the sole entry/exit for money in and out of the ecosystem.

7. Roadmap & Compatibility

7.1 Relation to the three existing charge paths

All three coexist. New functionality is opt-in; no forced migration.

7.2 Rollout milestones

M1+M2 is the minimal demoable combination.

7.3 Feature gate

New governance parameter bank_activation_height: u64:
  • Below this height: BankActor does not exist; fee_payer_override pointing 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
Testnet → mainnet can configure the height independently; rolling back is just setting the height into the future.

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 separate tx.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

  1. Cap units vs basefee volatility: the user sets caps in token units (CBY or U), but cycle_basefee / cell_basefee each 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”.
  2. 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).
  3. 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.
  4. 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).