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
| Existing piece | Relation |
|---|---|
fee_payer_override | New tx-level field tx.fee_payer_override: Option<Address>. Note: the existing ScheduledTimer.fee_payer_override (pvm_executor.rs:24, timer subsystem only) is a same-named but distinct field; this CIP introduces a new field at the tx top level that reuses the semantic but not the struct location. When the engine resolves tx.fee_payer, if the address hits a BankActor card derivation, charging is routed through the BankActor handler instead of the plain debit path |
SESSION_ACTOR (0x0C) | Isomorphic, independent. Session is a one-shot escrow + voucher settlement; Bank is a long-lived account + policy. SessionActor is not reused |
| CBSS (CIP-24) | Borrows its namespacing + policy/version state layering style (CBSS’s actual rolling-window is a proxy-local rate limiter, semantically different from BankActor’s on-chain windows) |
| Token (CIP-20) | A card holds CIP-20 balances using the standard ledger — the card address is just a normal token holder |
| CIP-12 Governance | RegisterBank and other protocol-level actions go through CIP-12 governance proposals (Tier 1 registry write; Tier 3 if a BankActor bytecode upgrade is also needed). Cowboy Banking’s BankOperator is bound by default to a Cowboy Banking operator multisig (signer set determined by CIP-12 governance — not the Cowboy Foundation from CIP-12 §3.1; the Foundation has no protocol authority). Third-party banks bring their own operator multisig at registration time |
1.4 Roles & authorities
| Role | On-chain identity | Can do | Cannot do |
|---|---|---|---|
| Card Holder Agent | actor address | Spend gas via the card (subject to policy) | Modify rules, withdraw, freeze |
| Card Owner | EOA or the agent itself | Deposit, SetPolicy, Renew, TransferOwnership, close the card | Touch other people’s cards |
| BankOperator (one per bank) | multisig | Freeze / Unfreeze cards under its bank; sign FiatMintVoucher | Modify card rules, seize funds (freeze only disables charges, doesn’t confiscate) |
| BankActor protocol layer | 0x0D | Enforce invariants; charge gas; record rolling windows | No independent power — every privileged action originates from one of the roles above |
| Off-chain Compliance Gateway | off-chain | KYC, Stripe collection, sign mint vouchers | No on-chain write rights; on-chain redemption requires BankOperator-recognized signature |
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:"):
| Key pattern | Value | Purpose |
|---|---|---|
b"bank:" || bank_id_be4 | BankEntry | Bank registry entry |
b"bank_seq" | u32_be4 | Next bank_id |
b"card:" || card_addr_20 | CardEntry | Full card state (policy, window) |
b"card_by_owner:" || owner_20 || bank_id_be4 || idx_be4 | card_addr_20 | Owner-side card list index |
b"card_by_agent:" || agent_20 || bank_id_be4 || idx_be4 | card_addr_20 | Agent-side card list index |
b"agent_default_card:" || agent_20 | card_addr_20 | Agent’s default gas card |
b"issue_nonce:" || bank_id_be4 || owner_20 || agent_20 | u64_be8 | Next derivation nonce for the (bank, owner, agent) triple |
b"voucher_used:" || voucher_id_32 | 1 | FiatMintVoucher replay-protection marker |
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
| Instruction | Fields | Key validation | Side effects |
|---|---|---|---|
IssueCard | bank_id, agent, gas_payment_token, initial_policy, expires_at: Option<u64> | bank Active; gas_payment_token ∈ {Native, whitelisted stable}; policy field bounds; caller = tx.from (becomes the owner) | bump issue_nonce; derive card_address; write CardEntry and owner/agent indices |
RenewCard | card_address, new_expires_at: Option<u64> | caller == owner; status ∈ {Active, Expired}; new_expires_at > block_height (if Some) | status → Active; update expires_at / last_renewed_at |
CloseCard | card_address, refund_to: Address | caller == owner; status ≠ Closed; not the default card | transfer all token balances out; status → Closed; remove indices; clear window |
Deposit | card_address, token, amount | bank.status == Active; card Active or Frozen (frozen card can still receive deposits); token ∈ {CBY, valid CIP-20} | token moves from caller to card_address |
Withdraw | card_address, token, amount, to | caller == owner; card status ≠ Frozen; balance sufficient | token moves from card_address to to |
SetPolicy | card_address, new_policy | caller == owner; if locked_after_transfer && owner == agent → reject; policy field bounds | overwrite CardEntry.policy (window untouched) |
TransferOwnership | card_address, new_owner, set_locked: bool | caller == owner; new_owner ≠ 0x00 | owner = new_owner; if set_locked, set locked_after_transfer = true |
3.2 Agent-or-Owner submitted
| Instruction | Fields | Validation | Side effects |
|---|---|---|---|
SetDefaultCard | agent, card_address: Option<Address> | caller == agent or caller == card.owner; if Some(addr): card.agent == agent and status = Active | write/delete b"agent_default_card:" || agent |
3.3 BankOperator submitted (the compliance handle)
| Instruction | Fields | Validation | Side effects |
|---|---|---|---|
Freeze | card_address, reason: Vec<u8>≤256 | caller == bank.operator | status → Frozen; emit event with reason |
Unfreeze | card_address | caller == bank.operator | status → Active (or Expired if already past expires_at) |
PauseBank | bank_id, reason | caller == bank.operator | bank.status → Paused; all charge / deposit / issue under this bank stop (withdraw still allowed) |
UnpauseBank | bank_id | caller == bank.operator | bank.status → Active |
MintFromFiatVoucher | voucher: FiatMintVoucher, signature: [u8;65] | signature from bank.fiat_mint_signer; voucher.bank_id matches; voucher_id not in b"voucher_used:"; block_height ≤ expires_at_block; bank.status == Active; card.status ≠ Closed | credit card_address with +amount in voucher.token; write b"voucher_used:" || voucher_id; emit FiatMinted |
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)
| Instruction | Fields | Validation | Side effects |
|---|---|---|---|
RegisterBank | name, operator, fiat_mint_signer: Option<Address> | caller authorized via CIP-12 governance proposal (Tier 1 registry write; Tier 3 if also upgrading BankActor bytecode); name 1..=32 ASCII; operator ≠ 0x00 | bump b"bank_seq"; write b"bank:" || id; emit BankRegistered |
SetBankOperator | bank_id, new_operator | caller == current operator, or authorized via CIP-12 governance | rotate operator |
SetBankFiatMintSigner | bank_id, new_signer: Option<Address> | caller == operator | rotate / remove the fiat-mint signing key |
3.5 Engine internal call (not a tx instruction, not addressable)
| Internal function | Trigger | Behavior |
|---|---|---|
BankActor::charge_gas(card, receiver, syscall_kind, amount, height) | Engine, during tx fee-settle, when fee_payer is determined to be a card address | Run the §4 pipeline; on failure, fall back to an OutOfFunds-equivalent reject |
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:
| Moment | Caps accounting | Settlement |
|---|---|---|
| Block where the timer is scheduled | Add max_cost to the window of “schedule time” | debit card for max_cost upfront |
| Block where the timer fires | Don’t account again | Refund the excess per §4.2 Phase 2 |
4.4 Edge cases
| Scenario | Handling |
|---|---|
| Card frozen after reserve but before settle | The current tx still settles (already pre-charged); the next reserve is rejected |
| Card expires after reserve but before settle (block crosses expiry) | The current tx still settles; the next reserve fails with CardExpired |
| Reserve and settle straddle a period boundary | Refund credits only the “current” period accumulator at settle time; the prior period’s inflated count is not reclaimed (conservative, acceptable) |
Card is Withdraw’d externally after reserve | Cannot happen; Withdraw checks balance, and the reserve has already debited |
| Multiple charges on the same card in the same block | Sequential accumulation; the last reserve sees the cumulative state after prior reserves — semantics are clear |
gas_payment_token = U with different precision than CBY | Genesis writes the protocol-fixed peg (day-1: 1:1 or a constant); oracle integration deferred to roadmap |
SetPolicy tightens after reserve | Settle uses the policy snapshot at reserve time; the next reserve uses the new policy |
| FiatMintVoucher arrives after reserve | Doesn’t affect the current tx; the next reserve sees the new balance |
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)
| Constant | Day-1 value | Note |
|---|---|---|
BLOCKS_PER_HOUR | 3_600 / target_block_secs | rounded |
BLOCKS_PER_DAY | 86_400 / target_block_secs | rounded |
BLOCKS_PER_MONTH | BLOCKS_PER_DAY * 30 | calibrated as 30 days, not calendar months |
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>
| State | Behavior |
|---|---|
Vec::new() (empty) | any receiver allowed |
| non-empty | the “primary receiver” of the tx must ∈ set; else BankErr::ReceiverNotInWhitelist |
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):
| Instruction | SyscallKind |
|---|---|
SystemInstruction::Send / Transfer / CallActor | Send |
SystemInstruction::Token* | Token |
ActorInstruction::DeployActor | DeployActor |
LibraryInstruction::PublishLibrary / RemoveLibrary | PublishLibrary |
SessionInstruction::* | Session |
CbssInstruction::* | Cbss |
| Cross-chain settlement | CrossChain |
| Other | Custom(opcode_u16) |
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
| Current status | Allowed | Disallowed |
|---|---|---|
Active | anything | — |
Frozen | Deposit / SetPolicy / Renew / TransferOwnership / unset-default / Unfreeze (operator) | charge_gas / Withdraw / set-as-default |
Expired | Deposit / Renew (owner) / Withdraw (owner, get balance back) | charge_gas / set-as-default |
Closed | Withdraw (owner, residual rescue) — receives leftovers from timer-deferred refunds | charge_gas / Deposit / SetPolicy / Renew / TransferOwnership / any other write |
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
| Item | Day-1 | Future |
|---|---|---|
| True sliding window | Fixed-period | deque + upper bound |
| Blacklist | Not done | explicit deny list |
| Per-token tiered caps | Single token dimension | per-token caps |
| Whitelist capacity | 64 / 16 hard caps | off-chain signed whitelist extension |
| Calendar months | 30-day block count | indexer-layer presentation |
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
| Isolation dimension | Implementation |
|---|---|
| Funds | Each card is an independent address; banks do not hold pooled funds |
| Authority | One-to-one operator multisig |
| Compliance pause | PauseBank affects only that bank’s cards |
| Fiat bridge | Each bank has its own fiat_mint_signer; they don’t recognize each other |
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
| Assumption | Impact |
|---|---|
| Gateway signer key not leaked | Leaked = money printer; rotation via SetBankFiatMintSigner; governance-driven emergency table flush is roadmap |
| Gateway will not sign forged vouchers | Triangulated by KYC + Stripe receipts + gateway internal audit |
| Stripe chargeback vs. on-chain mint timing | Gateway must delay signing until the Stripe risk window has elapsed |
| User does not lose the voucher | Gateway can re-sign the same voucher_id given the original receipt; chain accepts only the first |
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
| Stage | Contents | Demo highlight |
|---|---|---|
| M1 BankActor skeleton | 0x0D placement; genesis-write Cowboy Banking; IssueCard / Deposit / Withdraw / CloseCard; card address derivation; card_by_owner/agent indices | ”Every agent has its own on-chain bank card” |
| M2 Charge fork point | Engine fee-settle adds the §4.1 fork; charge_gas Phase 1+2; SetDefaultCard; timer-deferred integration | ”Pay gas with a card — balance is visible and auditable” |
| M3 Policy triad | SetPolicy; SpendWindow; whitelist match; Freeze/Unfreeze; PauseBank; locked_after_transfer | ”Limits, whitelist, freeze — all in place” |
| M4 Fiat bridge | MintFromFiatVoucher; voucher replay protection; SetBankFiatMintSigner; off-chain gateway + Stripe | ”Top up gas with a credit card” |
| M5 Multi-bank + ownership transfer | RegisterBank; SetBankOperator; third-party bank isolation; TransferOwnership | ”Cowboy is the on-chain bank charter system” |
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
| CIP | Touch point | Treatment |
|---|---|---|
| CIP-3 (dual-metered fee) | charge_gas strictly follows the CIP-3 §2.4 dual-metered formula cycles × cycle_basefee + cells × cell_basefee | ReservationToken must snapshot both basefees so that settle/refund stays consistent |
| CIP-20 (token) | A card holds CIP-20 balances | A card address is a valid token holder |
| CIP-24 (CBSS) | State layout style | Borrowed; no dependency |
| CIP-12 (governance) | RegisterBank is a Tier 1 registry write; BankActor bytecode upgrades are Tier 3 system-actor upgrades | Cowboy Banking BankOperator’s signer set is decided by CIP-12 governance (not Foundation/Security Council; a separately specified multisig instance) |
| Session (0x0C) | A card is a long-lived account; a session is a one-shot escrow | A session_id can be added to allowed_receivers to let a card pay session settlements only |
7.6 Roadmap
| Item | Source | Priority |
|---|---|---|
| True sliding window | §2.5 / §5.1 | P1 |
| Per-token tiered caps | §5.5 | P1 |
Off-ramp BurnToFiatRequest | §6.2.3 | P1 |
| Stripe gateway implementation + KYC backend | §6.3 | P0 (mandatory for M4 launch) |
| BankOperator emergency table flush (signer leak) | §6.2.1 | P2 |
| Explicit deny list | §5.2.4 | P3 |
| Cross-bank card migration | §6.1.4 | P3 |
| Per-token oracle conversion for paying gas | §2.3 | P2 |
| Bloom-filter cache for the active card set | §4.1 | P3 |
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
| Term | Meaning |
|---|---|
| BankActor | The system actor at 0x0D that carries every on-chain responsibility defined in this CIP |
| Bank | A registry entry inside BankActor — e.g. Cowboy Banking, or a third-party bank |
| Card | One card; on chain represented as a CardEntry + derived address; analogous to a physical bank card |
| Card Address | The 20-byte address derived from (bank_id, owner, agent, issue_nonce) via keccak256 |
| Card Owner | The card’s rule-controller — initially the user (guardian); potentially the agent itself later |
| Card Holder Agent | The actor the card serves — i.e. “the agent that pays gas using this card” |
| BankOperator | A bank’s operating multisig; can freeze / pause / sign fiat vouchers |
| Vault model | Card balances are held under the card’s own address (not the owner’s wallet) — like a prepaid debit card |
| FiatMintVoucher | The fiat-deposit receipt signed by the gateway; the user broadcasts it on chain to mint card balance |
Appendix B — Mapping to meeting points
| Meeting quote (paraphrased) | CIP section |
|---|---|
| ”Build an actor bank / open an account for each agent” | §1, §2, §3 |
| ”Can accept CBY, U, and many tokens” | §2.3 (gas_payment_token) + §3.1 (Deposit any token) |
| “Hook up Stripe so fiat can pay gas” | §6.2 fiat bridge |
| ”Max spend per month, hourly cap” | §5.1 three-tier limits |
| ”Whitelist: card has money but can only be spent at McDonald’s / KFC” | §5.2 allowed_receivers / allowed_syscall_kinds |
| ”Cards can expire / renew” | §3.1 RenewCard, §5.3 state machine |
| ”Blacklist the card, not the whole person” | §3.3 Freeze, §5.3 state machine |
| ”One card, one owner / agent” | §2.6 derivation (agent as salt) + §3.1 validation |
| ”Early: owner = guardian; later: agent itself” | §3.1 TransferOwnership + §5.4 locked_after_transfer |
| ”Cowboy Banking plus possibly other banks (CCB-like)“ | §6.1 multi-bank |
| ”Need a default card to pay from” | §2.7 + §3.2 SetDefaultCard |
| ”Cowboy as a whole need not be compliant — banking suffices” | §6.4 compliance perimeter |
End of document. Ready to enter the implementation-plan phase (CIP-28 landing in M1–M5 stages).

