Skip to main content

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 single create_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

Totalis vault flow diagram showing persistent vaults, position creation, and settlement

Key Design Decisions

One Vault Per Participant

Each participant (user or market maker) has exactly one vault, derived from their wallet address:
Vault PDA = ["vault", owner_wallet]
Vaults are created lazily on first interaction and reused for all subsequent bets. The vault holds:
  • Gross balance — Total USDC deposited
  • Locked collateral — Sum of collateral across all active positions
  • Free balancegross_balance - locked_collateral, available for new positions or withdrawal

Portfolio Margining via ILP

Market makers don’t lock full mm_risk for each position. Instead, the system calculates mm_collateral_locked — the incremental worst-case exposure — using an Integer Linear Programming (ILP) solver.
mm_collateral_locked = worst_case(existing + new) - worst_case(existing)
Properties:
  • 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
Example: If an MM has opposing positions on correlated markets, they can’t lose both simultaneously. The ILP solver recognizes this and requires less collateral than the naive sum of all mm_risk values.

Single Atomic Transaction

Position creation happens in one transaction with both user and MM as signers:
create_position(
    position_id,           // UUID from backend
    user_stake,            // User's bet amount
    mm_risk,               // MM's potential payout to user
    mm_collateral_locked,  // ILP-calculated incremental exposure
    legs[]                 // Parlay leg details
)
Both parties’ collateral is locked atomically — no intermediate states where one party is funded but not the other.

Role-Separated Signers

The program uses a Config PDA (seeds: ["config"]) to store role-separated keys, allowing different security postures per role:
RoleDescriptionKey Type
AdminUpdates config PDA (key rotation, fee changes)Cold key, rarely used
Fee PayerPays transaction fees and rent for all instructionsHot wallet, easily rotated
Settlement AuthoritySigns settle_position, cancel_position, close_positionWarm wallet
Fee Vault OwnerOwns the protocol fee vault token accountCold 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.
TransactionFee PayerSigners
initialize_vaultFee PayerFee Payer
deposit_to_vaultOwnerOwner
withdraw_from_vaultOwnerOwner
create_positionFee PayerFee Payer + User + MM (via Privy TEE)
settle_positionFee PayerSettlement Authority
cancel_positionFee PayerSettlement Authority
close_positionFee PayerSettlement 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):
  1. Frontend calls addSessionSigners() with the app’s key quorum ID when the user clicks “Enable Trading”
  2. User sees a Privy consent dialog and approves
  3. Backend can now request signing via signTransaction() with the authorization private key
This is semi-custodial: the user explicitly grants and can revoke signing permission at any time.

signTransaction vs signAndSendTransaction

We use Privy’s signTransaction (sign-only) instead of signAndSendTransaction (sign + broadcast) for position creation. This is critical because:
  • signAndSendTransaction broadcasts via Privy’s RPC, which may have different blockhash state than our RPC, causing “Blockhash not found” errors
  • signTransaction just signs the transaction bytes and returns them. We then add the fee payer’s signature and broadcast via our own RPC, ensuring blockhash consistency
The flow for position creation:
  1. Build transaction with our RPC’s blockhash, fee payer as fee payer
  2. Privy TEE signs for the user and MM (sign-only, no broadcast)
  3. Add fee payer’s co-signature locally
  4. Broadcast via our own RPC (same one we got the blockhash from)

Vault Program Accounts

The parlay_vaults Solana program (Anchor-based) manages three account types:
AccountSeedsDescription
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
Each vault has an associated Vault Token Account (USDC) owned by the vault PDA. This holds the actual tokens backing the vault’s gross_balance.

On-Chain State

Vault Struct

FieldTypeDescription
bumpu8PDA bump seed
ownerPubkeyWallet address that owns this vault
gross_balanceu64Total USDC deposited (6 decimals)
locked_collateralu64Sum of collateral locked across active positions
created_ati64Unix timestamp
last_updated_ati64Unix timestamp of last state change
Derived value: free_balance = gross_balance - locked_collateral

Position Struct

FieldTypeDescription
bumpu8PDA bump seed
position_id[u8; 16]Unique identifier (UUID from backend)
user_vaultPubkeyUser’s vault PDA
mm_vaultPubkeyMM’s vault PDA
user_stakeu64User’s bet amount (USDC 6 decimals)
user_collateral_lockedu64Always equals user_stake
mm_risku64Potential payout to user if they win
total_payoutu64user_stake + mm_risk
mm_collateral_lockedu64ILP-calculated incremental exposure
statusenumActive, SettledWin, SettledLoss, Cancelled
settlement_authorityPubkeySnapshotted from config at creation
created_ati64Unix timestamp
num_legsu8Number of parlay legs (1-5)
legs[ParlayLeg; 5]Market ticker (64 bytes) + side (0=Yes, 1=No)
fee_bpsu16Protocol fee in basis points, snapshotted from config at position creation

Instructions

InstructionDescriptionRequired Signers
initialize_configCreates singleton config PDAadmin (deployer only)
update_configUpdates config roles and fee_bpsadmin
initialize_vaultCreates vault PDA + token account for a participantfee_payer
deposit_to_vaultTransfers USDC from owner’s wallet to vaultowner
withdraw_from_vaultTransfers USDC from vault to owner’s wallet (up to free balance)owner
create_positionCreates position PDA, locks collateral in both vaultsfee_payer + user + mm
settle_positionTransfers funds to winner, deducts fee, unlocks collateralsettlement_authority
cancel_positionUnlocks collateral in both vaults (no fund transfer)settlement_authority
close_positionCloses 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:
StateConditionAvailable Actions
Emptygross_balance = 0Deposit
Availablefree_balance > 0Deposit, Withdraw (up to free), Create Position
Fully Lockedgross_balance > 0 AND free_balance = 0Deposit only

Position States

create_position()   → Active
settle_position()   → SettledWin | SettledLoss
cancel_position()   → Cancelled
StatusDescription
ActivePosition created, awaiting market resolution
SettledWinUser won, funds transferred to user vault
SettledLossMM won, funds transferred to MM vault
CancelledPosition voided (invalid leg), collateral unlocked
Only terminal positions (SettledWin, SettledLoss, Cancelled) can be closed.

Backend Transient States

The backend tracks additional transient states for operational safety:
Backend StatusPurpose
settlingAtomic claim lock while settlement transaction is in-flight. Prevents double-execution.
cancellingAtomic claim lock while cancellation transaction is in-flight.
These states are used by 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 winssettle_position(user_won: true): MM vault transfers mm_risk minus fee to user vault
  • MM winssettle_position(user_won: false): User vault transfers user_stake minus fee to MM vault
  • Invalid legcancel_position(): Both vaults’ collateral is unlocked, no fund transfer

Fee Calculation

The fee is calculated on profit only (not on the total payout):
profit = user_won ? mm_risk : user_stake
fee    = profit * fee_bps / 10_000
winner_receives = profit - fee
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_collateral decreases by user_collateral_locked (= user_stake)
  • MM’s locked_collateral decreases by mm_collateral_locked
The 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 a push outcome.

Background Services

Two background services manage the vault position lifecycle:
ServiceBase IntervalDescription
vault-job-service5sProcesses pending positions: resolves Privy wallets, calculates ILP exposure, executes create_position
vault-settlement-service30sPolls market results, settles or cancels active positions based on outcomes

vault-job-service Flow

  1. Atomically claim one pending position using a transaction-based lock (claimAndLockPendingPosition)
  2. Resolve wallet addresses via Privy (getOrCreateSolanaWallet)
  3. Ensure vaults exist for both parties (initialize_vault if needed)
  4. Calculate mm_collateral_locked via mm-exposure-api ILP solver
  5. Execute create_position with all resolved values
  6. Update position status to active

vault-settlement-service Flow

  1. Fetch active positions with their legs
  2. Check market results (Kalshi API)
  3. Determine outcome: settle (win/loss) or cancel (invalid leg)
  4. Claim position atomically (status = 'settling' or 'cancelling')
  5. Execute on-chain settlement/cancellation
  6. close_position is executed in the same TX as settle_position/cancel_position — not a separate step
  7. 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_vault and withdraw_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:
  1. Market exists in cache — ensures we have metadata for the ticker
  2. Market is active on Kalshi — live API check confirms the market hasn’t closed
All other filtering (resolution window, volume, category) is done at display time in the frontend. If a market is shown in the UI, it’s valid to bet on.