Overview
Totalis uses on-chain Solana vaults to hold participant collateral across all positions. Each participant (user or market maker) has a single persistent vault derived from their wallet address. When a trade is matched, both parties’ collateral is locked atomically in a singlecreate_position transaction — no intermediate states. All wallet signing is done server-side via Privy TEE (Trusted Execution Environment) wallets, so users never need to manually sign transactions. The fee payer covers gas for position creation and settlement — users and market makers only need USDC in their vaults.
Key benefits:
- Faster execution — Single atomic transaction for position creation
- Capital efficiency — MMs lock only incremental worst-case exposure, not full risk per position
- Lower rent costs — Vaults are reused; only position accounts are created/closed per bet
Architecture Diagram

Key Design Decisions
One Vault Per Participant
Each participant (user or market maker) has exactly one vault, derived from their wallet address:- Gross balance — Total USDC deposited
- Locked collateral — Sum of collateral across all active positions
- Free balance —
gross_balance - locked_collateral, available for new positions or withdrawal
Portfolio Margining via ILP
Market makers don’t lock fullmm_risk for each position. Instead, the system calculates mm_collateral_locked — the incremental worst-case exposure — using an Integer Linear Programming (ILP) solver.
- Incremental — Only the marginal exposure increase is locked, not the full risk
- Netting — Hedged or correlated positions reduce collateral requirements
- Snapshot — Calculated once at position creation and stored immutably
mm_risk values.
Single Atomic Transaction
Position creation happens in one transaction with both user and MM as signers:Role-Separated Signers
The program uses a Config PDA (seeds:["config"]) to store role-separated keys, allowing different security postures per role:
| Role | Description | Key Type |
|---|---|---|
| Admin | Updates config PDA (key rotation, fee changes) | Cold key, rarely used |
| Fee Payer | Pays transaction fees and rent for all instructions | Hot wallet, easily rotated |
| Settlement Authority | Signs settle_position, cancel_position, close_position | Warm wallet |
| Fee Vault Owner | Owns the protocol fee vault token account | Cold wallet |
Gas Sponsorship
For most operations, the fee payer covers gas. However,deposit_to_vault and withdraw_from_vault are owner-signed and require the owner to have SOL for transaction fees.
| Transaction | Fee Payer | Signers |
|---|---|---|
initialize_vault | Fee Payer | Fee Payer |
deposit_to_vault | Owner | Owner |
withdraw_from_vault | Owner | Owner |
create_position | Fee Payer | Fee Payer + User + MM (via Privy TEE) |
settle_position | Fee Payer | Settlement Authority |
cancel_position | Fee Payer | Settlement Authority |
close_position | Fee Payer | Settlement Authority |
Privy TEE Wallets with Session Signers
Privy embedded wallets run inside a Trusted Execution Environment. The private key never leaves the TEE — not even Privy can extract it. Server-side signing works via session signers (key quorums):- Frontend calls
addSessionSigners()with the app’s key quorum ID when the user clicks “Enable Trading” - User sees a Privy consent dialog and approves
- Backend can now request signing via
signTransaction()with the authorization private key
signTransaction vs signAndSendTransaction
We use Privy’s signTransaction (sign-only) instead of signAndSendTransaction (sign + broadcast) for position creation. This is critical because:
signAndSendTransactionbroadcasts via Privy’s RPC, which may have different blockhash state than our RPC, causing “Blockhash not found” errorssignTransactionjust signs the transaction bytes and returns them. We then add the fee payer’s signature and broadcast via our own RPC, ensuring blockhash consistency
- Build transaction with our RPC’s blockhash, fee payer as fee payer
- Privy TEE signs for the user and MM (sign-only, no broadcast)
- Add fee payer’s co-signature locally
- Broadcast via our own RPC (same one we got the blockhash from)
Vault Program Accounts
Theparlay_vaults Solana program (Anchor-based) manages three account types:
| Account | Seeds | Description |
|---|---|---|
| Config PDA | ["config"] | Singleton storing admin, fee payer, settlement authority, fee vault owner, and fee_bps |
| Vault PDA | ["vault", owner_wallet] | Per-participant vault storing balances and collateral state |
| Position PDA | ["position", position_id] | Per-bet position storing stakes, legs, and settlement authority |
gross_balance.
On-Chain State
Vault Struct
| Field | Type | Description |
|---|---|---|
bump | u8 | PDA bump seed |
owner | Pubkey | Wallet address that owns this vault |
gross_balance | u64 | Total USDC deposited (6 decimals) |
locked_collateral | u64 | Sum of collateral locked across active positions |
created_at | i64 | Unix timestamp |
last_updated_at | i64 | Unix timestamp of last state change |
free_balance = gross_balance - locked_collateral
Position Struct
| Field | Type | Description |
|---|---|---|
bump | u8 | PDA bump seed |
position_id | [u8; 16] | Unique identifier (UUID from backend) |
user_vault | Pubkey | User’s vault PDA |
mm_vault | Pubkey | MM’s vault PDA |
user_stake | u64 | User’s bet amount (USDC 6 decimals) |
user_collateral_locked | u64 | Always equals user_stake |
mm_risk | u64 | Potential payout to user if they win |
total_payout | u64 | user_stake + mm_risk |
mm_collateral_locked | u64 | ILP-calculated incremental exposure |
status | enum | Active, SettledWin, SettledLoss, Cancelled |
settlement_authority | Pubkey | Snapshotted from config at creation |
created_at | i64 | Unix timestamp |
num_legs | u8 | Number of parlay legs (1-5) |
legs | [ParlayLeg; 5] | Market ticker (64 bytes) + side (0=Yes, 1=No) |
fee_bps | u16 | Protocol fee in basis points, snapshotted from config at position creation |
Instructions
| Instruction | Description | Required Signers |
|---|---|---|
initialize_config | Creates singleton config PDA | admin (deployer only) |
update_config | Updates config roles and fee_bps | admin |
initialize_vault | Creates vault PDA + token account for a participant | fee_payer |
deposit_to_vault | Transfers USDC from owner’s wallet to vault | owner |
withdraw_from_vault | Transfers USDC from vault to owner’s wallet (up to free balance) | owner |
create_position | Creates position PDA, locks collateral in both vaults | fee_payer + user + mm |
settle_position | Transfers funds to winner, deducts fee, unlocks collateral | settlement_authority |
cancel_position | Unlocks collateral in both vaults (no fund transfer) | settlement_authority |
close_position | Closes position PDA, reclaims rent (included in same TX as settle/cancel) | settlement_authority |
State Machines
Vault States
Vaults don’t have explicit status fields — their state is derived from balances:| State | Condition | Available Actions |
|---|---|---|
| Empty | gross_balance = 0 | Deposit |
| Available | free_balance > 0 | Deposit, Withdraw (up to free), Create Position |
| Fully Locked | gross_balance > 0 AND free_balance = 0 | Deposit only |
Position States
| Status | Description |
|---|---|
Active | Position created, awaiting market resolution |
SettledWin | User won, funds transferred to user vault |
SettledLoss | MM won, funds transferred to MM vault |
Cancelled | Position voided (invalid leg), collateral unlocked |
SettledWin, SettledLoss, Cancelled) can be closed.
Backend Transient States
The backend tracks additional transient states for operational safety:| Backend Status | Purpose |
|---|---|
settling | Atomic claim lock while settlement transaction is in-flight. Prevents double-execution. |
cancelling | Atomic claim lock while cancellation transaction is in-flight. |
vault-settlement-service to ensure only one process settles or cancels a given position, even under concurrent execution.
Settlement Logic
Settlement transfers funds between vaults and deducts a protocol fee on profit:- User wins →
settle_position(user_won: true): MM vault transfersmm_riskminus fee to user vault - MM wins →
settle_position(user_won: false): User vault transfersuser_stakeminus fee to MM vault - Invalid leg →
cancel_position(): Both vaults’ collateral is unlocked, no fund transfer
Fee Calculation
The fee is calculated on profit only (not on the total payout):fee_bps is stored in the Config PDA and can be updated by the admin (e.g., 100 = 1%). The rate is snapshotted onto each position at creation time — settlement always uses the rate that was active when the position was created, not the current config value. The fee is transferred to the fee_vault_token_account in the same settle_position instruction.
Collateral Release
On settlement or cancellation:- User’s
locked_collateraldecreases byuser_collateral_locked(=user_stake) - MM’s
locked_collateraldecreases bymm_collateral_locked
gross_balance changes only reflect actual token transfers — winner gains, loser loses.
Invalid Leg Cancellation
If any leg in a parlay resolves as invalid (e.g., the event is voided or the market is delisted), the entire position is cancelled rather than settled. Both parties’ collateral is unlocked — no one wins or loses on an invalid market. Invalid legs are tracked with apush outcome.
Background Services
Two background services manage the vault position lifecycle:| Service | Base Interval | Description |
|---|---|---|
| vault-job-service | 5s | Processes pending positions: resolves Privy wallets, calculates ILP exposure, executes create_position |
| vault-settlement-service | 30s | Polls market results, settles or cancels active positions based on outcomes |
vault-job-service Flow
- Atomically claim one pending position using a transaction-based lock (
claimAndLockPendingPosition) - Resolve wallet addresses via Privy (
getOrCreateSolanaWallet) - Ensure vaults exist for both parties (
initialize_vaultif needed) - Calculate
mm_collateral_lockedvia mm-exposure-api ILP solver - Execute
create_positionwith all resolved values - Update position status to
active
vault-settlement-service Flow
- Fetch active positions with their legs
- Check market results (Kalshi API)
- Determine outcome: settle (win/loss) or cancel (invalid leg)
- Claim position atomically (
status = 'settling'or'cancelling') - Execute on-chain settlement/cancellation
close_positionis executed in the same TX assettle_position/cancel_position— not a separate step- Record fee in database, broadcast WebSocket event
Key Properties
- Non-custodial — Totalis never holds user funds. All collateral is locked in on-chain Solana vault PDAs controlled by the program.
- Capital efficient — Portfolio margining via ILP allows MMs to operate with less locked capital.
- Atomic execution — Position creation is a single transaction with no intermediate failure states.
- Gas-sponsored — The fee payer covers gas for most operations. Owners need SOL only for
deposit_to_vaultandwithdraw_from_vault. - TEE-secured signing — Private keys never leave the Privy Trusted Execution Environment.
- Persistent vaults — Participants maintain one vault across all bets, reducing account overhead and rent costs.
- Role-separated — Admin, fee payer, settlement authority, and fee vault owner are distinct keys with different security postures.
- Transparent — All vaults, positions, and settlements are verifiable on the Solana blockchain.
Market Validation
When an RFQ is submitted, two checks are performed:- Market exists in cache — ensures we have metadata for the ticker
- Market is active on Kalshi — live API check confirms the market hasn’t closed
