The bankroll

Risk, origins and asset classes

A single number between 0 and 100 decides both how much of the vault may be at venues and whether perpetuals and options are reachable at all. The second effect is governed by a table set once at initialize and never writable again.

The slider's two effects#

Policy.risk is a u8 from 0 to 100, chosen by the deployer inside the origin's ceiling. It does two unrelated things, both enforced on chain.

EffectEnforced inFailure
Caps deployment: at most risk percent of vault equity may sit at venues at oncepassive_deploy and agent_fund_venueRiskCapExceeded
Gates asset classes: a class is unreachable until risk clears its minimumregister_venue_accountRiskBelowAssetClass
cap = equity * risk / 100

risk 0    ──▶ cap is zero. The bankroll accumulates and never trades.
risk 40   ──▶ at most 40% of equity may be out at venues at once.
risk 100  ──▶ the entire vault may be deployed.

The cap is checked against equity, which is the bankroll plus everything already deployed, so it is a cap on the share of the whole vault rather than on a single transfer. A policy already at its cap cannot deploy another unit until either the bankroll grows or something comes back.

The origin also caps the slider itself: risk may not exceed the origin's max_risk, checked at both init_policy and set_policy, failing with RiskAboveOrigin.

The seven asset classes#

One bit each, so an origin's mask is a set of them. The bit position is also the index into the min_risk_for_class array.

ClassBitValueMinimum risk
STABLE010
MAJOR120
SOL_MEME240
STOCK3810
PERP41680
OPTION53280
LP6640
(unused)71280

The array is [u8; 8] while only seven classes are defined, so the eighth slot exists and is zero. Minimum risk values are the live Config.min_risk_for_class on this deployment.

A venue account carries exactly one class bit. register_venue_account requires asset_class to be non-zero and a power of two, so the two gates below are unambiguous.

The immutable risk gate#

min_risk_for_class can never be changed after initialize

It is set by initialize and no instruction writes it afterwards. set_protocol_params does not touch it, and there is no other path. Whatever the protocol was initialised with is what it enforces for the life of that Config account.

GET /analytics, protocolConfig
"minRiskForClass": [0, 0, 0, 10, 80, 80, 0, 0]

In plain terms, on this deployment:

  • stablecoins, majors, Solana memecoins and LP positions are reachable at any risk, including risk 0;
  • stocks need risk at least 10;
  • perpetuals and options both need risk at least 80.

That 80 is the number that decides whether a policy can ever trade perpetuals or options at all, so any interface offering a perps or options choice has to show it next to the risk slider.

Where the gate actually bites#

The gate is in register_venue_account, not init_policy

init_policy checks only that the risk is within range and within the origin's ceiling. It does not check min_risk_for_class, because at that point no venue account exists to check a class against. So a policy pointed at perpetuals can be created at risk 50 quite happily, and fails only later, with RiskBelowAssetClass, the first time it tries to register the venue it was created for.

init_policy            ──▶ checks risk <= 100 and risk <= origin.max_risk
                           ✔ a risk-50 perps policy is created

register_venue_account ──▶ checks risk >= min_risk_for_class[PERP] = 80
                           ✘ RiskBelowAssetClass

There is a way out, as long as the policy is not locked: set_policy can raise the risk, and the venue registration will then succeed. After lock_policy the risk is frozen, and a locked policy created below the threshold can never register the venue it needs.

The asset class gate sits in register venue account, not in init policy. A policy created at risk 50 for a perpetuals venue passes init policy, because init policy checks only that risk is within range and within the origin ceiling and never reads the minimum risk table. In a later, separate transaction the same policy fails at register venue account with the error risk below asset class, because the perpetuals class requires risk 80. The way out is to raise the risk with set policy, which only works while the policy is unlocked.A perpetuals policy, created at risk 50init_policypasseschecks risk is 0 to 100checks risk is within the origin ceilingnever reads min_risk_for_classa separate transaction,possibly much laterregister_venue_accountfailsPERP needs risk 80, this policy has 50RiskBelowAssetClassraise it with set_policy, if the policy is still unlocked

The gate lives in the second transaction, so a policy can be created below the threshold and looks fine until it tries to open the venue it was created for.

Origins#

An origin is where an agent comes from: a protocol-level registry entry holding a name, an asset_class_mask and a max_risk. The mask decides which asset classes a policy of that origin may ever touch, so the origin genuinely determines what gets bought. A venue account outside the mask cannot be registered at all.

Origins are permanent and append-only

create_origin uses Anchor's init, not init_if_needed, and there is no update_origin and no close instruction. An origin can never be renamed, re-masked or deleted once it exists. A wrong mask is fixed only by adding a new id, and the old one stays visible forever.

That is why the registry below contains two origins named perps and two named options: the later pair could not replace the earlier pair, only sit beside them.

The origin registry, live#

All six of these are written on mainnet, read back through /origins. Every origin currently has max_risk 100, so the origin ceiling is not what limits any of them; the class gate is.

The addresses are the same on every cluster

An Origin derives from ["origin", id] and nothing else, so origin 5 has the same address on a local validator as it does on mainnet. That makes the pubkeys below safe to hardcode in a test, and it means a mask read on one cluster is not evidence about another: same address, independently written account.

IdNameMaskClassesExclusive?
1stocks-only79STABLE MAJOR SOL_MEME STOCK LPno
2perps-and-options127all sevenno
3options111STABLE MAJOR SOL_MEME STOCK OPTION LPno
4perps95STABLE MAJOR SOL_MEME STOCK PERP LPno
5perps17STABLE PERPyes
6options33STABLE OPTIONyes
GET /origins
{
  "source": "chain:Origin accounts",
  "items": [
    { "id": 1, "pubkey": "4LjMhsTWQrwqLPbHY6z5gf1CCxVennQcMSKmwTpfAw5g", "name": "stocks-only",       "asset_class_mask": 79,  "max_risk": 100 },
    { "id": 2, "pubkey": "2tz9VZc4HDbsL5txrrhrv1EVBxxLZXpSS2LucJ75nL2v", "name": "perps-and-options", "asset_class_mask": 127, "max_risk": 100 },
    { "id": 3, "pubkey": "8N9BrFu5uexDaA47sms2qQnA4Lb6BGxe6qXKoNNvA42R", "name": "options",           "asset_class_mask": 111, "max_risk": 100 },
    { "id": 4, "pubkey": "4eW1t36s7CbF3vjYHsC28z53JrKsGrLNDbw71Q9eSTSn", "name": "perps",             "asset_class_mask": 95,  "max_risk": 100 },
    { "id": 5, "pubkey": "HG4WUkdk1gHpnPgKcYQLcdesCdjjz4FDNnDmUjryWipy", "name": "perps",             "asset_class_mask": 17,  "max_risk": 100 },
    { "id": 6, "pubkey": "3MByFXNXWXpyzrHJdoPpidP1GbqvG8EPAdzqFktADiW9", "name": "options",           "asset_class_mask": 33,  "max_risk": 100 }
  ]
}

The exclusive pair, and why it exists#

Ids 3 and 4 carry the right venue bit but also STABLE, MAJOR, SOL_MEME, STOCK and LP. An interface pill labelled "PERPS" over id 4 would claim more exclusivity than the chain actually enforces: a policy on that origin could legitimately register a stock venue instead.

Ids 5 and 6 were added for exactly that pill. Mask 17 is STABLE plus PERP; mask 33 is STABLE plus OPTION. Nothing else. Those two are the exclusive pair the interface uses, and mask 17 has a working precedent as the test origin in the program's own test suite.

If an interface saysUse originBecause
PERPS, exclusivelyid 5, mask 17STABLE and PERP only, nothing else registrable
OPTIONS, exclusivelyid 6, mask 33STABLE and OPTION only
Perps and options togetherid 2, mask 127every class
Stocks, no derivativesid 1, mask 79no PERP or OPTION bit at all

Why the LP bit buys nothing#

The LP bit is deliberately absent from origins 5 and 6, and its absence costs nothing. compound_lp does not go through a registered venue account at all: it forwards an instruction to the pinned AMM program with remaining accounts, signing as its own lp_auth address, and never touches the allowlist. So an LP bit in a mask does not enable anything that would otherwise be blocked.

The bit still exists for completeness

LP = 64 is defined, and could gate a future venue account whose asset class is a liquidity position. Nothing in the program does that today, so a policy on an origin with the LP bit and a policy on one without it behave identically with respect to LP compounding.

Reading a policy's risk position#

GET /coins/:mint, an Agent policy on the exclusive perps origin
"origin": {
  "pubkey": "HG4WUkdk1gHpnPgKcYQLcdesCdjjz4FDNnDmUjryWipy",
  "name": "perps",
  "assetClassMask": 17,
  "maxRisk": 100
},
"risk": 80,
"targetLeverageX": 5,
"venueAccounts": [
  {
    "tokenAccount": "3KZXskLQHPTu2Vtjb4MVBpF4VVFjiUouLMjGzZ7bAVdF",
    "kind": "Remote",
    "assetClass": 16,
    "revoked": false,
    "bridgedOut": { "value": "50000000000", "source": "chain:VenueAccount.bridged_out" }
  }
]

Risk is exactly 80, which is the minimum the PERP class requires, and assetClass 16 is the PERP bit. Had the deployer set risk 79, the venue registration would have failed and this policy would have no way to trade.