Architecture & Technical Details
This page provides a detailed overview of the Depredict protocol's technical architecture and implementation.
Smart Contract Architecture
Core Accounts
Config Account
- Purpose: Protocol-wide configuration and fee management
- Key Fields:
fee_amount: Protocol fee in basis pointsauthority: Admin authority for config updatesfee_vault: Address to receive protocol feesnext_market_id: Auto-incrementing market ID
Market State Account
- Purpose: Individual market data and state
- Key Fields:
market_id: Unique market identifierquestion: Market question (max 80 characters)market_start/end: Market timingbetting_start: When trading begins (for future markets)oracle_type: Switchboard or manual resolutionmarket_type: Live or future marketyes_liquidity/no_liquidity: Current liquidity poolspages_allocated: Number of allocated position pagesmintanddecimals: Token used for trading and its decimalsmarket_vault: Vault address holding market funds
Market Creator Account
- Purpose: Represents a platform/team that creates markets. Holds NFT collection and merkle tree references used for positions.
- Key Fields:
name: Display name of the market creatorcreator_fee_bps: Creator fee in basis points (max capped in program)fee_vault: Creator fee vaultcore_collection: MPL Core collection used for position NFTsmerkle_tree: Bubblegum compression tree for scalable minting
Position Pages
- Purpose: Scalable, paged storage of positions per market to support high throughput.
- Details:
- Each market has multiple
position_pageaccounts, with fixed slots (e.g., 16 entries per page) - New pages are created on-demand and may be pre-warmed when capacity is low
- SDK auto-discovers or creates a page with available slots when opening a position
- Each market has multiple
Position NFT
- Purpose: Individual position representation
- Implementation: MPL Core NFT (compressed), minted at position open and burned at settlement
- Key Fields:
position_id: Unique position identifierdirection: YES or NO positionamount: Position sizestatus: OPEN, WAITING, SETTLED, etc.
Account Relationships (Three-tier model)
- Protocol Tier:
Configcontrols global parameters and thenext_market_id. - Market Creator Tier:
MarketCreatoris verified by linking an MPL Corecore_collectionand Bubblegummerkle_treebefore creating markets. - Markets/Users Tier: Each
MarketStateownsPositionPageaccounts and a marketvault. Users open positions that mint compressed NFTs under the creator’s collection/tree.
Market Types
Live Markets
- Start: Trading begins immediately when market is created
- Use Case: Real-time events, breaking news, etc.
Future Markets
- Start: Trading begins at a specified future time
- Use Case: Scheduled events, elections, etc.
For broader categories like binary, multi-outcome, scalar, and tournament markets, see Market Types.
Oracle Integration
Switchboard Oracle
- Usage: Automated, decentralized resolution
- Cost: ~$0.15 per resolution
- Setup: Requires oracle pubkey during market creation
Manual Resolution
- Usage: Admin-controlled resolution
- Cost: Gas fees only
- Process: Admin calls
resolveMarketwith outcome
Instruction Flows
Create Market
- Precondition: The
market_creatormust be verified with a valid MPL Core collection and Bubblegum merkle tree - Derive PDAs for
config,market,position_page_0, andmarket_creator(from payer) - Initialize market with question, timings, market type, oracle type, and mint/decimals
- Allocate first position page
SDK: client.trade.createMarket({ question, startTime, endTime, oracleType, marketType, bettingStartTime?, metadataUri, payer })
Open Position
- Fetch market and its mint, market vault, and market creator
- Find or create a position page with available slots
- Mint a compressed MPL Core NFT representing the position
- Transfer funds into the market vault
SDK: client.trade.openPosition({ marketId, amount, direction, payer, metadataUri })
Resolve Market
- If manual, pass resolution value; if oracle-driven, read Switchboard result
- Update market state and winning direction
SDK: client.trade.resolveMarket({ marketId, payer, resolutionValue? })
Settle Position (Payout)
- Provide compressed NFT proof (root, hashes, nonce, index)
- Single
settlePositioninstruction burns the cNFT and transfers winnings (if any) in the same path - Losing positions still execute the burn gate; payout resolves to zero so the transaction only consumes compute
- All settlement flows use the same instruction, so batching winners and losers in one transaction is supported
SDK: client.trade.payoutPosition({ marketId, payer, assetId, rpcEndpoint, returnMode? })
Close Market
- Collect protocol and creator fees
- Close market accounts and vaults
SDK: client.trade.closeMarket(marketId, payer)
Token Integration
Automatic Token Swapping (coming soon)
- Current behavior: Market mints can be SPL Token or Token-2022; the program enforces the passed token program, and the SDK auto-detects the mint owner. Users must pay with the market mint; ensure the user's ATA for that mint exists/funded.
- Planned: Add automatic swap (e.g., via Jupiter) so users can pay with other tokens and receive the correct market mint under the hood.
Fee Structure
- Protocol Fee: Configurable percentage of trade volume
- Oracle Fee: Per-resolution cost for Switchboard markets
- NFT Minting: ~0.002 SOL per position
Position Management
NFT-Powered Positions
- Technology: MPL Core for NFT minting
- Benefits:
- True ownership and transferability
- Secondary market trading
- Composability with other protocols
Position Lifecycle
- Creation: User opens position, NFT is minted
- Trading: Position can be transferred or held
- Resolution: Market outcome determines position value
- Settlement: User claims payout by burning NFT
Sub-Position Accounts
- Purpose: Handle multiple positions per user per market
- Implementation: Hierarchical account structure
- Benefits: Efficient storage and gas optimization
Security Features
Access Control
- Config Authority: Controls protocol parameters
- Market Authority: Controls individual market settings
- Position Authority: Controls individual positions
Validation
- Market State: Prevents invalid state transitions
- Timing: Enforces market start/end times
- Liquidity: Ensures sufficient liquidity for trades
Error Handling
- Custom Errors: Specific error types for different scenarios
- Graceful Degradation: Markets can be paused or closed
- Recovery Mechanisms: Admin tools for emergency situations
Performance Considerations
Gas Optimization
- Deterministic PDAs (seeds): Every account (config, market, position pages, market creator, vaults) is derived from seeds, so clients don't pass arbitrary addresses. This keeps instructions small and predictable and lets lookup tables be reused safely.
Address Lookup Tables (LUTs)
- Motivation: Settling a position requires many accounts (market, position page, mint, Bubblegum programs, proof path). Reusing a LUT keeps the user claim to a single transaction even when proofs are large.
- Two-tier Strategy:
- Creator LUT: Stores accounts shared by every market under a market creator (config, creator PDA, merkle tree, collection, program IDs). Created once and reused.
- Per-Market LUTs: Store only market-specific PDAs (market state, market vault, position pages, additional proof nodes). Created alongside the market so rent deposits stay small and can be reclaimed when payouts finish.
- Lifecycle:
- Build or extend the creator LUT during market-creator setup.
- After
createMarket, create/extend a market LUT supplyingcreatorLookupTableAddressso shared accounts are excluded. - When fetching a proof for settlement, extend the market LUT with any new proof nodes before the user signs their claim.
- Once all positions settle, deactivate and close the market LUT to reclaim the rent deposit; repeat for the creator LUT when no markets remain.
import { PublicKey } from '@solana/web3.js';
import DepredictClient, { buildV0Message } from '@endcorp/depredict';
const client = new DepredictClient(connection);
// 1. Ensure creator LUT (shared across every market this authority runs)
const creatorLut = await client.trade.ensureMarketCreatorLookupTable({
authority: admin.publicKey,
payer: admin.publicKey,
});
if (creatorLut.createTx) await sendTx(creatorLut.createTx, [admin]);
for (const tx of creatorLut.extendTxs) await sendTx(tx, [admin]);
// 2. Ensure market LUT right after createMarket
const marketLut = await client.trade.ensureMarketLookupTable({
marketId,
authority: admin.publicKey,
payer: admin.publicKey,
creatorLookupTableAddress: creatorLut.lookupTableAddress,
pageIndexes: [0, 1], // prewarm the first two pages
});
if (marketLut.createTx) await sendTx(marketLut.createTx, [admin]);
for (const tx of marketLut.extendTxs) await sendTx(tx, [admin]);
// 3. Before a user settles, extend the market LUT with new proof nodes (if any)
await client.trade.extendMarketLookupTable({
marketId,
authority: admin.publicKey,
lookupTableAddress: marketLut.lookupTableAddress,
creatorLookupTableAddress: creatorLut.lookupTableAddress,
proofNodes: proof.proof, // strings or PublicKeys
});
// 4. Build the settle instruction; combine both LUTs in the final transaction
const { instruction } = await client.trade.buildSettleInstructionWithProof({
marketId,
claimer: user.publicKey,
assetId,
pageIndex: proofPageIndex,
slotIndex: proofSlotIndex,
proof: {
root: proof.root,
dataHash: proof.dataHash,
creatorHash: proof.creatorHash,
nonce: proof.nonce,
leafIndex: proof.index,
proofNodes: proof.proof,
},
});
const { message } = await buildV0Message(
client.program,
[instruction],
user.publicKey,
[creatorLut.lookupTableAddress.toBase58(), marketLut.lookupTableAddress.toBase58()],
);
await sendVersioned(message, [user]);
// 5. After payouts complete
const { deactivateTx, closeTx } = await client.trade.buildMarketLookupTableCloseTxs({
authority: admin.publicKey,
lookupTableAddress: marketLut.lookupTableAddress,
});
await sendTx(deactivateTx, [admin]);
await waitOneSlot();
await sendTx(closeTx, [admin]); // rent reclaimed- Costs: A typical market LUT (~20 addresses) locks <0.0005 SOL rent, fully refunded upon close. The creator LUT deposit (~15–20 addresses) is paid once and can be recovered when the creator retires the table. Transaction fees for create/extend/deactivate/close are a few thousand lamports each.
- Batch Operations: Multiple operations in single transaction
- Account Reuse: Efficient account structure design
Scalability
- Parallel Processing: Solana's concurrent transaction processing
- State Management: Efficient account state updates
- Memory Usage: Optimized data structures
Development Workflow
Local Development
# Build program
anchor build
# Run tests
anchor run test-runner-continue
# Deploy to localnet
anchor deploySDK Development
cd sdk
yarn install
yarn buildDocumentation
cd depredict-docs
yarn install
yarn devIntegration Points
External Protocols
- Jupiter: Token swapping and aggregation
- Switchboard: Oracle data feeds
- MPL Core: NFT minting and management
Developer Tools
- Anchor: Framework for Solana development
- TypeScript: SDK and tooling
- Vocs: Documentation site generation
This architecture provides a robust, scalable foundation for decentralized prediction markets while maintaining security, performance, and developer experience.
