Architecture & Technical Details
This page provides a detailed overview of the Depredict protocol's technical architecture and implementation.
Protocol Overview
Depredict is built on Solana using the Anchor framework, with three main components:
- On-chain Program (
programs/depredict/): Rust smart contracts - TypeScript SDK (
sdk/): Client library for protocol interaction - Documentation (
depredict-docs/): Guides and API reference
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)
- Planned: Trade with any SPL token
- Planned flow: Convert user token to market mint via Jupiter aggregator
- Current behavior: Users must pay with the market mint; ensure the user's ATA for the market mint is funded
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
- PDA Usage: Program-derived addresses for deterministic account creation
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.
