Architecture
How the pieces fit together — on-chain program, web app, sync layer, IPFS, agent surfaces.
P&L has more moving parts than most documentation lets on. This page describes them honestly so you can integrate at the right layer and avoid reinventing things that exist.
The layers, top-down
Source of truth
The on-chain program at C5mVE2BwSehWJNkNvhpsoepyKwZkvSLZx29bi4MzVj86 is the only authoritative source for:
- Pool balances (each market's vault PDA holds SOL)
- Vote counts and per-user positions (each Position PDA holds a user's stake)
- Resolution outcome (the Market struct's
resolutionfield, written exclusively by the program) - Fee accumulation (the Treasury PDA)
- Vesting state (Team Vesting and Founder Vesting PDAs)
Everything else is a derivative — MongoDB caches market metadata, the web UI snapshots vote totals, the indexer materializes timelines. If any of these diverge from chain, chain wins. The web app exposes a "sync this market from chain" debug endpoint for exactly this case.
Off-chain index (MongoDB)
MongoDB Atlas stores:
- Project metadata — title, description, thesis, image IPFS CID, social links, category. This is the content of a market; the on-chain program only knows the IPFS CID that points to it.
- Materialized vote counts and totals — cached for fast list/browse queries. Updated by the blockchain-sync service.
- User profiles — Privy auth state, wallet linkings, display names. None of this is on-chain.
- Notifications, chat messages, comments — purely off-chain, non-authoritative.
The reason metadata is off-chain: Solana account storage is expensive. A 50KB market description would cost $5 in rent for a single account. We pin the full description to IPFS, store the IPFS CID on-chain (~50 bytes), and cache the resolved content in MongoDB for fast reads. Anyone can re-resolve from IPFS independently.
Blockchain sync service
The Helius WebSocket subscription watches the program's account space and pushes deltas into a Redis queue. A worker processes the queue:
- Identifies the account type (Market, Position, Treasury, Vesting)
- Parses the account data using the same Rust struct layouts the program uses
- Writes the parsed state to MongoDB
- Broadcasts the update to connected Socket.IO clients
Cron jobs run alongside for things WebSockets don't cover:
- Price snapshots for launched tokens (Birdeye, every minute)
- Holder counts for launched tokens (Helius DAS, hourly)
- Cache warmups for the markets-list endpoint
Source: apps/web/src/services/blockchain-sync/ in the repo.
Pump.fun CPI
When a market resolves YES, resolve_market performs a cross-program invocation into pump.fun's program (6EF8rrecthR5Dkzon8Nwu78hRvfCKubJ14M5uBEwF6P) to:
- Create a new token mint
- Deposit pooled SOL as initial liquidity
- Set the founder's wallet as the pump.fun creator (so they receive ongoing royalties)
- Mint the supply: 65% goes to YES voters (pending claim), 33% to the founder team (pending claim + vesting), 2% to the P&L platform
The launched token then trades on pump.fun's bonding curve until it graduates to a Raydium pool (per pump.fun's protocol, not ours).
We did not modify pump.fun. We invoke their public mint instruction.
Agent surfaces
The agent-facing surfaces are intentionally minimal and read-only today:
/llms.txtathttps://pnl.market/llms.txt— the protocol summary in llmstxt.org format. Any AI agent can drop this into context and reason about P&L./robots.txt— explicit allow-list for 14+ AI crawlers./api/markets/list— public JSON. The same endpoint the web app uses for browsing./sitemap.xml— every market URL.
The write path (creating markets, voting) is fully permissionless on-chain but routed through Privy-auth'd web APIs for human users. Agents can either:
- Bypass the web app entirely and call the on-chain program directly via
@solana/web3.jswith their own keypair (see On-chain program) - Wait for the MCP server that we're building, which exposes draft-link tools so the agent prepares the action and the user confirms in their wallet (see Agent integration)
Where to hook in
If you're building an integration, here are the recommended entry points:
| Use case | Entry point |
|---|---|
| Read market state for display | GET /api/markets/list?status=active |
| Read a single market | GET /api/markets/<id> |
| Real-time updates | Socket.IO at pnl.market/socket (events: market:updated, market:created, vote:cast) |
| Create a market programmatically | Direct on-chain create_market instruction |
| Vote programmatically | Direct on-chain buy_yes / buy_no instruction |
| Crank an expired market | Direct on-chain expire and then resolve_market instructions (both permissionless) |
| Subscribe to all program events | Helius WebSocket on the program account |
Going through the on-chain program is always faster and more authoritative than the off-chain APIs. The web API is there for convenience and rate-limit-friendliness — it caches, batches, and pre-resolves IPFS for you. The on-chain program is there when you need real-time, ground-truth state.
What we don't have yet
Honest list of architectural pieces not yet built:
- TypeScript SDK (
@pnl/sdkon npm) — the closest thing today is readingpackages/shared/src/solana/anchor-program.tsin the repo - MCP server (
@pnl/mcp-server) — see Agent integration for what it'll expose - Proper Anchor IDL for use with
@coral-xyz/anchor— see known limitations - Auto-cranker for expired markets — see known limitations
If you need any of these urgently for an integration, open an issue — having a real consumer accelerates the priority.