pumpmarket skill
Predict pump.fun token graduations (YES/NO) on Solana mainnet via PumpMarket parimutuel betting markets.
Description
name: pumpmarket version: 1.0.0 description: Predict pump.fun token graduations (YES/NO) on Solana mainnet via PumpMarket parimutuel betting markets. homepage: https://pumpmarket.fun metadata: {"openclaw":{"emoji":"🎰","category":"defi","homepage":"https://pumpmarket.fun","requires":{"bins":["curl"],"npm":["@coral-xyz/anchor","@solana/web3.js"]}}}
PumpMarket Agent Skill
Trade SOL on pump.fun token graduation outcomes. Predict which tokens will graduate to PumpSwap within 1 hour.
Network: Solana mainnet-beta
API: https://pumpbet-mainnet.up.railway.app
WebSocket: wss://pumpbet-mainnet.up.railway.app
Program ID: 3mNbBV3Xc3rNJ4E87pSFzW7VhUZySHQDQVyd4MP2VFG6
IDL: https://pumpmarket.fun/skill.json
Treasury: 4iFYGzxKGH2SAeVaR5AxPiCfLCSQD9fdPK8tsDBbmx3f
Naming: Brand is PumpMarket. On-chain program name is pumpbets (legacy). API host pumpbet-* retains the original name.
This is mainnet. Bets use real SOL. The program is deployed (100 markets, 290 bets on-chain), the treasury is funded (9.29 SOL), and the keeper authority is operational (0.12 SOL). All addresses, API URLs, and code examples target Solana mainnet-beta.
Table of Contents
- What Is PumpMarket
- First-Run Setup
- On-Chain Parameters
- API Reference
- WebSocket Reference
- Transaction Construction
- Transaction Flows
- Signal Evaluation & Strategy
- Resolution Logic
- Error Handling
- Rate Limits & Best Practices
- FAQ & Gotchas
- Appendix: Account Data Layout
- Appendix: On-Chain Events
- Appendix: Dry-Run Simulation
1. What Is PumpMarket
PumpMarket is a parimutuel prediction market for pump.fun token graduations on Solana. Users bet SOL on whether a pump.fun token will "graduate" (complete its bonding curve and migrate to PumpSwap DEX) within approximately 1 hour.
How it works:
- A user creates a market for a pump.fun token, choosing YES or NO and staking SOL
- Other users bet YES (token will graduate) or NO (it won't) within the ~1-hour window
- An automated keeper resolves the market based on on-chain data
- Winners split the total pool proportionally (minus 4.5% fees)
Market outcomes:
- YES wins — token graduated (bonding curve hit 100%, migrated to PumpSwap)
- NO wins — token did not graduate within the deadline (timeout or rug)
- VOIDED — one-sided pool (all bets on the same side) or oracle void — full refunds, zero fees
2. First-Run Setup
Before an agent can trade, it needs:
Wallet
- A Solana wallet with a keypair (e.g.,
Keypair.fromSecretKey(...)) - Connected to mainnet-beta RPC (e.g.,
https://api.mainnet-beta.solana.comor a Helius/Quicknode endpoint) - Funded with SOL for bets and transaction fees
RPC Endpoint
The public https://api.mainnet-beta.solana.com is rate-limited. For production agents, use a dedicated RPC provider (Helius, Quicknode, Triton, etc.).
Terms Acceptance (Required Once)
PumpMarket requires users to accept terms before their activity is fully tracked. This is a one-time operation:
- Get the message to sign:
Response:GET https://pumpbet-mainnet.up.railway.app/api/users/{wallet}/terms/message{ "message": "PumpMarket Terms of Service\nI agree to the terms at https://pumpmarket.fun/terms\nWallet: ...\nTimestamp: ...", "timestamp": 1772413074485, "wallet": "..." } - Sign the
messagestring with your wallet and encode as base58:import nacl from 'tweetnacl'; import bs58 from 'bs58'; const sig = nacl.sign.detached(new TextEncoder().encode(message), keypair.secretKey); const signatureBase58 = bs58.encode(sig); - Submit the signed acceptance:
POST https://pumpbet-mainnet.up.railway.app/api/users/{wallet}/terms Body: { "signature": "<base58 signature>", "timestamp": <timestamp from step 1> } - Verify acceptance:
GET https://pumpbet-mainnet.up.railway.app/api/users/{wallet}/terms → { "wallet": "...", "termsAccepted": true }
Dependencies
npm install @coral-xyz/anchor @solana/web3.js
The @pumpmarket/sdk npm package is not published (internal monorepo only). All code examples in this document use raw @coral-xyz/anchor and @solana/web3.js. You need the program IDL, which is provided in Section 6.
Keypair Security
Never hardcode private keys in source code or commit them to repositories.
Loading a keypair from file:
import { Keypair } from '@solana/web3.js';
import fs from 'fs';
// Ensure keyfile permissions: chmod 600 ~/.config/solana/id.json
const secret = JSON.parse(fs.readFileSync(process.env.KEYPAIR_PATH!, 'utf-8'));
const keypair = Keypair.fromSecretKey(Uint8Array.from(secret));
Loading from environment variable:
import bs58 from 'bs58';
const keypair = Keypair.fromSecretKey(bs58.decode(process.env.PRIVATE_KEY!));
Best practices:
- Store keys in environment variables or encrypted keyfiles — never in source code
- Set keyfile permissions to
600(chmod 600 keyfile.json) - Add
*.jsonkeypair paths to.gitignore - Use a dedicated betting wallet with limited funds — not your main wallet
- For production agents, consider a secrets manager (e.g. AWS Secrets Manager, Doppler)
Testing Without Real SOL
This skill targets mainnet-beta where bets use real SOL. To test transaction construction without spending:
- Dry-run simulation: See Appendix: Dry-Run Simulation — builds and simulates transactions on-chain without submitting them.
- Devnet: PumpMarket has a devnet deployment (
beta.pumpmarket.fun) used for internal testing. The devnet program ID is3mNbBV3Xc3rNJ4E87pSFzW7VhUZySHQDQVyd4MP2VFG6(same binary, different state). Devnet is not guaranteed to be stable or have active markets. - Start small: If testing on mainnet, budget at least 0.2 SOL per bet (0.1 SOL bet + 0.1 SOL market creation fee). Bets below 0.1 SOL are likely unprofitable after fees.
3. On-Chain Parameters
All values verified against the Anchor smart contract at packages/contracts/programs/pumpbets/src/constants.rs.
| Parameter | Value | Lamports | On-Chain Enforcement |
|---|---|---|---|
| Market Creation Fee | 0.1 SOL | 100,000,000 | createMarket — transferred to treasury |
| Min Bet | 0.01 SOL | 10,000,000 | createMarket + placeBet |
| Max Bet | 10 SOL | 10,000,000,000 | createMarket + placeBet |
| Platform Fee | 3.5% | 350 BPS | resolveMarket — from total pool to treasury |
| Creator Fee | 1.0% | 100 BPS | resolveMarket — from total pool to creator |
| Total Fee | 4.5% | 450 BPS | Combined (platform + creator) |
| Market Duration | ~1 hour | 9,000 slots | createMarket — resolution_slot = current + 9000 |
| Max Bonding Progress | < 95 | — | Strict < check: 0–94 accepted, 95+ rejected |
| Rug Threshold | $4,500 USD | — | Off-chain keeper checks (constants defined on-chain) |
| Rug Duration | ~5 minutes | 750 slots | Must sustain below threshold continuously |
| Claim Period | ~7 days | 1,512,000 slots | Vault can be closed (rent reclaimed) after this |
| Emergency Delay | ~1 day | 216,000 slots | Only treasury can emergency withdraw after this |
Mainnet Activity (Indexer snapshot — 2026-03-02)
| Metric | Value |
|---|---|
| Program accounts | 490 (100 markets, 290 bets, 100 vaults) |
| Markets resolved | 53 (47 voided) |
| Total volume | 76.14 SOL |
| Treasury balance | 9.29 SOL |
| Keeper balance | 0.12 SOL |
These values are from PumpMarket's internal indexer at the timestamp above and may differ from current on-chain state.
Verify Mainnet Deployment
# Verify API is healthy
curl -s https://pumpbet-mainnet.up.railway.app/api/health | python3 -m json.tool
# Verify program on-chain (requires solana CLI)
solana program show 3mNbBV3Xc3rNJ4E87pSFzW7VhUZySHQDQVyd4MP2VFG6 --url mainnet-beta
# Verify treasury is funded
solana balance 4iFYGzxKGH2SAeVaR5AxPiCfLCSQD9fdPK8tsDBbmx3f --url mainnet-beta
# Verify active markets exist
curl -s https://pumpbet-mainnet.up.railway.app/api/stats | python3 -m json.tool
Reproduce Account Counts via RPC
Estimate on-chain account counts by querying program-owned accounts by data size. Account sizes: BettingMarket = 123 bytes, UserBet = 91 bytes, Vault = 41 bytes.
Some public RPCs rate-limit
getProgramAccounts. Use a paid RPC if needed.
RPC=https://api.mainnet-beta.solana.com
PID=3mNbBV3Xc3rNJ4E87pSFzW7VhUZySHQDQVyd4MP2VFG6
count_by_size () {
curl -s $RPC -X POST -H 'Content-Type: application/json' -d "{
\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"getProgramAccounts\",
\"params\":[\"$PID\",{
\"encoding\":\"base64\",
\"dataSlice\":{\"offset\":0,\"length\":0},
\"filters\":[{\"dataSize\":$1}]
}]
}" | jq '.result | length'
}
echo "BettingMarket (123): $(count_by_size 123)"
echo "UserBet (91): $(count_by_size 91)"
echo "Vault (41): $(count_by_size 41)"
Mainnet Addresses
| Address | Purpose |
|---|---|
3mNbBV3Xc3rNJ4E87pSFzW7VhUZySHQDQVyd4MP2VFG6 |
PumpBets Program ID |
4iFYGzxKGH2SAeVaR5AxPiCfLCSQD9fdPK8tsDBbmx3f |
Treasury |
3cHDNTUqsqV4XSDdzuinvzaENKaUTKmud9m8enWFMfTh |
Keeper Authority |
PDA Seeds
| Account | Seeds | Constraint |
|---|---|---|
| Market | ["market", token_mint] |
One market per token mint |
| User Bet | ["bet", market, user, bet_index_u32_le] |
Seed is "bet" (not "user_bet") |
| Vault | ["vault", market] |
Holds all SOL for a market |
4. API Reference
Base URL: https://pumpbet-mainnet.up.railway.app
All endpoints are public with no authentication — no API keys, no wallet signatures, no headers required. The only exception is POST /api/users/:wallet/terms which requires a wallet signature (one-time terms acceptance).
Health & Stats
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/health |
Health check — DB/Redis status, uptime, version |
| GET | /api/stats |
Platform stats — marketsResolved, activeMarkets, totalVolumeSol |
Tokens
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/tokens |
List tokens (paginated, filterable) |
| GET | /api/tokens/featured |
Top 5 tokens nearest graduation with active markets |
| GET | /api/tokens/trending |
Trending tokens by volume |
| GET | /api/tokens/graduated |
Recently graduated tokens |
| GET | /api/tokens/prices/live?mints=mint1,mint2 |
Birdeye prices (max 20 mints via GET) |
| GET | /api/tokens/top-trades |
Biggest trades in last 15 minutes |
| GET | /api/tokens/:mint |
Single token details |
| POST | /api/tokens/:mint/validate-bonding |
Fresh bonding validation (anti-stale-data) (no request body) |
| POST | /api/tokens/:mint/refresh |
Force refresh bonding data from chain |
GET /api/tokens Query Parameters:
| Param | Values | Default |
|---|---|---|
sort |
volume_desc, created_desc, bonding_desc, pool_desc, bonding_active_desc |
created_desc |
has_market |
true, false |
— |
user |
wallet address | — |
creator |
wallet address | — |
cursor |
string (from nextCursor) |
— |
limit |
1–100 | 20 |
Response:
{
"tokens": [{
"id": 64934529,
"mintAddress": "7QMT...pump",
"name": "Dodger",
"symbol": "DRAFT",
"imageUrl": "https://ipfs.io/...",
"bondingProgress": 3.65,
"marketCapUsd": 2443.34,
"solReserves": 0.834,
"createdAt": "2026-03-01T23:50:41.654Z",
"updatedAt": "2026-03-01T23:50:41.654Z",
"graduatedAt": null,
"isGraduated": false
}],
"nextCursor": "2026-03-01T23:50:36.703Z:64934477",
"cached": false
}
GET /api/tokens/:mint Response — verified live (includes nested market if one exists):
{
"id": 53217785,
"mintAddress": "2XPS...pump",
"name": "Kuzya",
"symbol": "KUZYA",
"imageUrl": "https://...",
"bondingProgress": 0,
"marketCapUsd": 1692.45,
"solReserves": 0,
"createdAt": "2026-02-22T14:47:48.437Z",
"graduatedAt": "2026-02-22T14:57:46.634Z",
"isGraduated": true,
"market": {
"marketAddress": "8ydD...zfKs",
"yesPool": 0, "noPool": 1,
"status": "voided",
"creationSlot": 443918000,
"resolutionSlot": 443927000,
"totalBets": 1
}
}
POST /api/tokens/:mint/validate-bonding — no request body required. The server fetches fresh data from Bitquery, bypassing all caches.
Response:
{ "valid": true, "bondingProgress": 42, "isGraduated": false, "priceUsd": 0.001, "verifiedAt": "..." }
or:
{ "valid": false, "bondingProgress": 97, "reason": "Token is at 97% bonding progress..." }
Markets
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/markets |
Active (unresolved) markets only |
| GET | /api/markets/resolved |
Resolved/voided/pending markets (filterable) |
| GET | /api/markets/graduated |
Alias for resolved with filter=resolved |
| GET | /api/markets/:address |
Single market — full details, odds, delta |
| GET | /api/markets/:address/bets |
Paginated bets for a market |
| GET | /api/markets/:address/can-bet |
Check if market accepts bets |
| POST | /api/markets/:address/check-graduation |
Trigger manual graduation check |
| GET | /api/markets/by-creator/:wallet |
Markets created by a wallet |
GET /api/markets returns active/unresolved markets only. No pagination — returns all active markets at once. No status filter. For historical data, use /api/markets/resolved.
GET /api/markets Response — verified from source (routes/markets.ts:119-147):
{
"markets": [{
"address": "AJAi...4kDZ",
"tokenMint": "DPMo...pump",
"token": { "name": "WASM AI", "symbol": "WASM", "image": "https://...", "bondingProgress": "45.20", "marketCapUsd": 8525.77, "priceUsd": 0.001 },
"creator": "Ax7R...UZMg",
"deadlineSlot": "403565031",
"pools": { "yes": 0.5, "no": 1, "total": 1.5 },
"odds": { "yes": 3, "no": 1.5, "yesImplied": 33.33, "noImplied": 66.67 },
"delta": 0,
"totalVolume": 1.5,
"createdAt": "2026-02-21T18:56:32.279Z"
}]
}
Note: The active list uses address (same as single market). creator is truncated. Includes live bondingProgress, marketCapUsd, priceUsd in the token object, and computed odds/delta. No nextCursor — all active markets returned in one response.
Type caveat:
bondingProgressinside thetokenobject on market endpoints is a string (e.g."45.20"). On/api/tokensit's a number. AlwaysparseFloat()before arithmetic (e.g.delta = parseFloat(token.bondingProgress) - yesImplied).
GET /api/markets/resolved Response — verified live:
{
"markets": [{
"id": 473,
"marketAddress": "CJyX...EkhZ",
"tokenMint": "2yok...pump",
"token": { "name": "Oil coin", "symbol": "Oilcoin", "image": "https://..." },
"status": "voided",
"outcome": null,
"voided": true,
"resolutionReason": null,
"pools": { "yes": 0.1, "no": 0 },
"totalVolume": 0.1,
"totalBets": 1,
"resolvedAt": null,
"graduatedAt": "2026-03-01T18:53:57.484Z",
"minutesSinceGraduation": 406,
"deadlineSlot": "403565031",
"slotsUntilResolution": null,
"estimatedResolutionMinutes": null,
"marketCapUsd": "8525.77",
"creatorWallet": "9F89...wCZT",
"holderCount": null
}],
"nextCursor": null,
"_meta": { "currentSlot": 403621384, "pendingMarketsIncluded": true }
}
Beware: The resolved list uses marketAddress (not address like the active list and single market endpoints). It also uses creatorWallet instead of creator. No odds/delta fields — those are only on active markets and the single market endpoint.
GET /api/markets/resolved Query Parameters:
| Param | Values | Default |
|---|---|---|
filter |
all, resolved, pending, voided |
all |
cursor |
string | — |
limit |
1–100 | 20 |
includeHolders |
true |
— |
Single Market Response (/api/markets/:address) — verified live:
{
"address": "AJAi1rndzVCZ4SroBnaHv8LDrYsPMteFCwkTun8B4kDZ",
"tokenMint": "DPMo...pump",
"creator": "Ax7RXZZSr8eZPDJdeazeZpn9EgnpnEHVzhhYwik5UZMg",
"token": { "name": "WASM AI", "symbol": "WASM", "image": "https://...", "bondingProgress": "0.00" },
"status": "resolved",
"outcome": true,
"voided": false,
"pools": { "yes": 0.5, "no": 1, "total": 1.5 },
"odds": { "yes": 3, "no": 1.5, "yesImplied": 33.33, "noImplied": 66.67 },
"delta": 0,
"totalVolume": 1.5,
"deadlineSlot": "443738815",
"resolvedAt": null,
"createdAt": "2026-02-21T18:56:32.279Z",
"summary": { "yesCount": 1, "noCount": 2, "yesTotal": 0.5, "noTotal": 1, "uniqueBettors": 2 }
}
Note: The single market endpoint uses address (not marketAddress), and odds uses yes/no (decimal odds, not yesOdds/noOdds). Includes a summary field with bet counts. Full wallet addresses only appear here — list endpoints truncate to "Xxxx...xxxx".
GET /api/markets/:address/can-bet Response — verified live:
{ "canBet": false, "reason": "Market is already voided", "marketStatus": "voided", "tokenGraduated": true }
GET /api/markets/:address/bets Response — verified live:
{
"bets": [{
"id": 3963,
"betAddress": "5V9x...oZS",
"user": "7oQJ...GmKT",
"amount": 0.5,
"prediction": "NO",
"potentialPayout": 0,
"claimed": false,
"claimedAmount": null,
"createdAt": "2026-02-21T18:59:42.249Z"
}],
"nextCursor": "2026-02-21T18:59:42.249Z"
}
Users
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/users/:wallet/stats |
User statistics (volume/profit in lamport strings, not SOL) |
| GET | /api/users/:wallet/bets |
Bet history (filterable) |
| GET | /api/users/:wallet/bets/counts |
Bet counts by status |
| GET | /api/users/:wallet/claimable |
Count of claimable bets |
| GET | /api/users/:wallet/claimable/details |
Claimable bets with estimated payouts |
| GET | /api/users/:wallet/terms |
Check terms acceptance |
| POST | /api/users/:wallet/terms |
Accept terms (requires wallet signature) |
| GET | /api/users/:wallet/terms/message |
Get terms message to sign |
GET /api/users/:wallet/bets Query Parameters:
| Param | Values | Default |
|---|---|---|
status |
active, won, lost, refunded, all |
all |
cursor |
string | — |
limit |
1–100 | 20 |
GET /api/users/:wallet/stats Response — verified live:
{
"wallet": "GVEt...RZW4",
"totalBets": 57,
"totalWins": 5,
"totalVolume": "43100000000",
"totalProfit": "805083332",
"marketsCreated": 51,
"creatorEarnings": "0",
"termsAccepted": false,
"termsAcceptedAt": null
}
Important: totalVolume and totalProfit are lamport strings (not SOL floats). Divide by 1e9 to get SOL. This differs from leaderboard endpoints which return SOL floats.
GET /api/users/:wallet/bets Response — verified live:
{
"bets": [{
"id": 4173,
"betAddress": "7szH...eyP",
"market": {
"marketAddress": "74p2...9nk",
"tokenMint": "BPba...pump",
"resolved": true, "voided": false, "outcome": false,
"deadlineSlot": 443736779,
"yesPool": 0.1, "noPool": 1
},
"token": { "mint": "BPba...pump", "name": "...", "symbol": "...", "imageUrl": "..." },
"amount": 1,
"prediction": false,
"odds": 1.1,
"status": "won",
"payout": 1.0505,
"claimed": false,
"createdAt": "2026-02-21T19:09:28.382Z"
}],
"nextCursor": "2026-02-21T19:09:28.382Z"
}
GET /api/users/:wallet/bets/counts Response — verified live:
{ "active": 0, "won": 15, "lost": 12, "refunded": 11, "all": 38 }
GET /api/users/:wallet/claimable/details Response:
{
"wallet": "Ax7R...UZMg",
"count": 3,
"bets": [{
"betAddress": "...",
"marketAddress": "...",
"amount": 0.5,
"prediction": true,
"outcome": true,
"isRefund": false,
"estimatedPayout": 0.85,
"token": { "name": "...", "symbol": "...", "imageUrl": "..." }
}],
"totalEstimatedPayout": 2.55
}
Bets
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/bets/:betAddress |
Single bet by on-chain address |
| POST | /api/bets/sync |
Sync bet from chain to DB (optional — background service catches up) |
| POST | /api/bets/sync-market |
Sync all bets for a market (optional) |
| POST | /api/bets/sync-user |
Sync all bets for a user (optional) |
| POST | /api/bets/claim/:betAddress |
Record on-chain claim in DB (optional) |
There is no GET /api/bets list endpoint. Use /api/users/:wallet/bets or /api/markets/:address/bets.
All sync/claim POST endpoints are optional DB convenience calls. Bets exist on-chain regardless. A background sync service runs continuously and will pick up unsynced bets within minutes. Calling sync immediately after a transaction makes the bet visible in the API/UI sooner.
POST /api/bets/sync: { "betAddress": "<on-chain bet PDA>" } — returns 400 if betAddress missing
POST /api/bets/sync-market: { "marketAddress": "...", "userWallet": "<optional>" }
POST /api/bets/sync-user: { "userWallet": "...", "fullSync": false } — returns { "success": true, "synced": N, "skipped": N, "total": N }
POST /api/bets/claim/:betAddress: { "signature": "<tx sig>", "amount": <lamports>, "userWallet": "..." }
GET /api/bets/:betAddress Response — verified live:
{
"address": "5V9x...oZS",
"market": "AJAi...4kDZ",
"user": "7oQJ...GmKT",
"amount": 0.5,
"prediction": false,
"claimed": false,
"claimedAmount": null,
"payout": null,
"marketResolved": true,
"marketVoided": false,
"marketOutcome": true,
"token": { "name": "WASM AI", "symbol": "WASM", "image": "https://..." }
}
Prices
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/prices/sol |
SOL/USD price |
| GET | /api/prices/:mint |
Cached token price |
| POST | /api/prices/live |
Batch live prices (max 50 mints) |
POST /api/prices/live Body:
{ "mints": ["mint1", "mint2", "mint3"] }
Max 50 mints per request. Response:
{
"prices": [{ "mint": "...", "priceUsd": 0.001, "marketCapUsd": 50000, "bondingProgress": 45, "timestamp": "..." }],
"cached": 2,
"fetched": 1
}
Leaderboard
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/leaderboard |
Ranked users |
| GET | /api/leaderboard/top |
Top 5 across all categories |
| GET | /api/leaderboard/categories |
Available categories and time ranges |
GET /api/leaderboard Query Parameters:
| Param | Values | Default |
|---|---|---|
category |
profit, winrate, volume, creators |
profit |
range |
24h, 7d, 30d, all |
all |
wallet |
wallet address | — |
limit |
1–500 | 100 |
5. WebSocket Reference
WebSocket URLs use the same host as the API, with wss:// protocol.
| Path | URL | Purpose |
|---|---|---|
/prices |
wss://pumpbet-mainnet.up.railway.app/prices |
Real-time token price updates |
/live-trades |
wss://pumpbet-mainnet.up.railway.app/live-trades |
Live $1000+ pump.fun trades |
/user-events |
wss://pumpbet-mainnet.up.railway.app/user-events |
User-specific bet/market notifications |
/activity |
wss://pumpbet-mainnet.up.railway.app/activity |
Platform-wide activity stream |
Ping/Pong (All Endpoints)
Server sends { "type": "ping" } every 30 seconds. You must respond with { "type": "pong" } or the connection will be dropped. Use text-frame JSON pings — Railway's proxy does not support binary WebSocket ping frames.
Reconnection
Use exponential backoff: base 3s delay, 1.5x multiplier, max 10 attempts.
Stateless agents: If your agent cannot maintain persistent connections between turns, prefer polling
GET /api/marketsandGET /api/tokenson a 30-second interval instead of WebSockets. The polling approach misses real-time graduation events but is simpler for request-response agent architectures.
/prices — Live Price Stream
On connect: Receives { "type": "connected", "timestamp": "..." } followed by the full price cache.
Subscribe to specific tokens:
{ "type": "subscribe", "mints": ["mint1", "mint2"] }
Subscribe to all updates:
{ "type": "subscribe_all" }
Unsubscribe:
{ "type": "unsubscribe", "mints": ["mint1"] }
If subscribedMints is empty (no subscribe message sent), the client receives ALL price updates.
Incoming price update:
{ "mint": "...", "priceUsd": 0.001, "marketCapUsd": 50000, "bondingProgress": 45.2, "timestamp": "..." }
Incoming graduation event:
{ "type": "graduation", "mint": "...", "timestamp": "...", "transactionSignature": "..." }
Throttled to max 5 updates per token per second.
/live-trades — Trade Feed
On connect: Receives { "type": "initial", "trades": [...] } with the last 50 trades.
Incoming trade:
{
"type": "trade",
"trade": {
"mint": "...", "name": "...", "symbol": "...", "imageUrl": "...",
"side": "buy", "amountUsd": 1500, "amountSol": 10,
"timestamp": "...", "signature": "...", "wallet": "..."
}
}
/user-events — Wallet-Scoped Notifications (No Privacy Guarantees)
Requires wallet identification within 30 seconds or disconnection (code 4001):
{ "type": "auth", "wallet": "YourFullWalletAddress" }
A valid wallet address (32–44 chars) is sufficient — no cryptographic signature is required. This means anyone who knows your wallet address can subscribe to your events. Do not treat this channel as private or access-controlled. All bet and market data is on-chain and publicly observable regardless.
Subscribe to a market:
{ "type": "subscribe_market", "marketAddress": "..." }
Unsubscribe:
{ "type": "unsubscribe_market", "marketAddress": "..." }
Incoming event types: BET_UPDATE, MARKET_UPDATE, NOTIFICATION, SYSTEM
Minimal WebSocket Example
import WebSocket from 'ws';
const ws = new WebSocket('wss://pumpbet-mainnet.up.railway.app/prices');
ws.on('open', () => {
console.log('Connected');
ws.send(JSON.stringify({ type: 'subscribe_all' }));
});
ws.on('message', (data) => {
const msg = JSON.parse(data.toString());
if (msg.type === 'ping') {
ws.send(JSON.stringify({ type: 'pong' })); // REQUIRED — text frame, not binary
return;
}
if (msg.type === 'graduation') {
console.log('Graduation:', msg.mint);
return;
}
// Price update
if (msg.mint) {
console.log(`${msg.mint}: ${msg.bondingProgress}% bonding, $${msg.marketCapUsd} mcap`);
}
});
ws.on('close', (code, reason) => console.log('Disconnected:', code, reason.toString()));
6. Transaction Construction
Since @pumpmarket/sdk is not published to npm, agents must construct transactions using @coral-xyz/anchor and @solana/web3.js directly.
Constants
import { PublicKey } from '@solana/web3.js';
import { BN } from '@coral-xyz/anchor';
const PROGRAM_ID = new PublicKey('3mNbBV3Xc3rNJ4E87pSFzW7VhUZySHQDQVyd4MP2VFG6');
const TREASURY = new PublicKey('4iFYGzxKGH2SAeVaR5AxPiCfLCSQD9fdPK8tsDBbmx3f');
const LAMPORTS_PER_SOL = 1_000_000_000;
const MIN_BET_LAMPORTS = 10_000_000; // 0.01 SOL
const MAX_BET_LAMPORTS = 10_000_000_000; // 10 SOL
const CREATION_FEE_LAMPORTS = 100_000_000; // 0.1 SOL
Deployment constants (single source of truth).
PROGRAM_IDandTREASURYabove MUST match the on-chain program deployment. If you switch environments (devnet/mainnet), update them everywhere. If your transaction fails with error 6019InvalidTreasury, your client is using the wrong treasury address for this deployment.
PDA Derivation
function findMarketAddress(tokenMint: PublicKey): [PublicKey, number] {
return PublicKey.findProgramAddressSync(
[Buffer.from('market'), tokenMint.toBuffer()],
PROGRAM_ID
);
}
function findUserBetAddress(
market: PublicKey, user: PublicKey, betIndex: number
): [PublicKey, number] {
const indexBuf = Buffer.alloc(4);
indexBuf.writeUInt32LE(betIndex);
return PublicKey.findProgramAddressSync(
[Buffer.from('bet'), market.toBuffer(), user.toBuffer(), indexBuf],
PROGRAM_ID
);
}
function findVaultAddress(market: PublicKey): [PublicKey, number] {
return PublicKey.findProgramAddressSync(
[Buffer.from('vault'), market.toBuffer()],
PROGRAM_ID
);
}
Program Setup
import { Program, AnchorProvider, web3 } from '@coral-xyz/anchor';
// Fetch IDL: https://pumpmarket.fun/skill.json
// Or direct: https://pumpmarket.fun/pumpbets.json
import IDL from './pumpbets.json';
const connection = new web3.Connection('https://api.mainnet-beta.solana.com', 'confirmed');
const provider = new AnchorProvider(connection, wallet, { commitment: 'confirmed' });
const program = new Program(IDL as any, PROGRAM_ID, provider);
bondingProgressis caller-supplied — there is no on-chain oracle. The value you pass is stored as-is; the program does not independently verify it against the bonding curve. Always callPOST /api/tokens/{mint}/validate-bondingimmediately before building your transaction and pass the returned integer. This keeps your bet's metadata accurate and avoids betting on stale data (e.g. a token that already graduated).Integer rule: The on-chain argument is
u8(integer 0–100). Always use the integer fromvalidate-bondingfor transactions. Never pass the float/string from market endpoints (e.g."45.20") as the on-chain argument — it will fail or truncate silently.
Create Market
const tokenMint = new PublicKey('YOUR_TOKEN_MINT');
const [marketPDA] = findMarketAddress(tokenMint);
const [vaultPDA] = findVaultAddress(marketPDA);
const [creatorBetPDA] = findUserBetAddress(marketPDA, wallet.publicKey, 0);
// Step 1: Get bonding progress from validate-bonding API
const validation = await fetch(
`https://pumpbet-mainnet.up.railway.app/api/tokens/${tokenMint.toBase58()}/validate-bonding`,
{ method: 'POST' }
).then(r => r.json());
if (!validation.valid) throw new Error(`Token not valid: ${validation.reason}`);
// Step 2: Pass bondingProgress integer as the third argument
const tx = await program.methods
.createMarket(
true, // prediction: true=YES, false=NO
new BN(10_000_000), // amount: 0.01 SOL (minimum bet) in lamports
validation.bondingProgress // bonding_progress: integer from validate-bonding response
)
.accounts({
market: marketPDA,
creatorBet: creatorBetPDA,
creator: wallet.publicKey,
vault: vaultPDA,
tokenMint: tokenMint,
treasury: TREASURY,
systemProgram: web3.SystemProgram.programId,
})
.transaction();
// Step 3: Sign and send (see "Sign and Send" helper below)
const sig = await signAndSend(connection, tx, wallet);
Place Bet
const marketPDA = new PublicKey('MARKET_ADDRESS');
const [vaultPDA] = findVaultAddress(marketPDA);
// Step 1: Get bonding progress from validate-bonding API
const tokenMint = 'TOKEN_MINT_ADDRESS';
const validation = await fetch(
`https://pumpbet-mainnet.up.railway.app/api/tokens/${tokenMint}/validate-bonding`,
{ method: 'POST' }
).then(r => r.json());
if (!validation.valid) throw new Error(`Token not valid: ${validation.reason}`);
// Step 2: Fetch current totalBets from chain for bet index
const marketAccount = await program.account.bettingMarket.fetch(marketPDA);
const betIndex = marketAccount.totalBets;
const [userBetPDA] = findUserBetAddress(marketPDA, wallet.publicKey, betIndex);
// Step 3: Pass bondingProgress integer as the third argument
const tx = await program.methods
.placeBet(
true, // prediction: true=YES, false=NO
new BN(50_000_000), // amount: 0.05 SOL
validation.bondingProgress // bonding_progress: integer from validate-bonding response
)
.accounts({
market: marketPDA,
userBet: userBetPDA,
user: wallet.publicKey,
vault: vaultPDA,
systemProgram: web3.SystemProgram.programId,
})
.transaction();
// Step 4: Sign and send (see "Sign and Send" helper below)
const sig = await signAndSend(connection, tx, wallet);
Claim Payout
// betAddress and marketAddress from /api/users/{wallet}/claimable/details are base58 strings
const marketPDA = new PublicKey('MARKET_ADDRESS'); // from claimable bet's marketAddress
const userBetPDA = new PublicKey('BET_ADDRESS'); // from claimable bet's betAddress
const [vaultPDA] = findVaultAddress(marketPDA);
const tx = await program.methods
.claimPayout()
.accounts({
market: marketPDA,
userBet: userBetPDA,
user: wallet.publicKey,
vault: vaultPDA,
systemProgram: web3.SystemProgram.programId,
})
.transaction();
const sig = await signAndSend(connection, tx, wallet);
Claim Refund (Voided Market)
const marketPDA = new PublicKey('MARKET_ADDRESS');
const userBetPDA = new PublicKey('BET_ADDRESS');
const [vaultPDA] = findVaultAddress(marketPDA);
const tx = await program.methods
.claimRefund()
.accounts({
market: marketPDA,
userBet: userBetPDA,
user: wallet.publicKey,
vault: vaultPDA,
systemProgram: web3.SystemProgram.programId,
})
.transaction();
const sig = await signAndSend(connection, tx, wallet);
Sign and Send
All transaction flows above use this helper. It handles blockhash, signing, sending, and confirmation:
async function signAndSend(
connection: web3.Connection,
tx: web3.Transaction,
wallet: web3.Keypair
): Promise<string> {
const { blockhash, lastValidBlockHeight } = await connection.getLatestBlockhash('confirmed');
tx.recentBlockhash = blockhash;
tx.feePayer = wallet.publicKey;
tx.sign(wallet);
const sig = await connection.sendRawTransaction(tx.serialize(), {
skipPreflight: false,
preflightCommitment: 'confirmed',
});
await connection.confirmTransaction(
{ signature: sig, blockhash, lastValidBlockHeight },
'confirmed'
);
return sig;
}
If you use AnchorProvider with a wallet adapter, program.methods.xxx().rpc() handles this automatically. The .transaction() + signAndSend() pattern above is for agents using raw Keypair without a provider wallet adapter.
Calculation Functions
Odds:
yesImplied = yesPool / (yesPool + noPool) * 100
noImplied = noPool / (yesPool + noPool) * 100
yesOdds = (yesPool + noPool) / yesPool (decimal odds, e.g. 1.58x)
noOdds = (yesPool + noPool) / noPool (decimal odds, e.g. 2.72x)
Delta signal:
delta = bondingProgress - yesImplied
Positive = market underpricing YES. Negative = market overpricing YES.
Payout (for winners):
totalPool = yesPool + noPool
distributable = totalPool * 0.955 (after 4.5% fees)
payout = (yourBet / winningPool) * distributable
Refund (voided markets):
refund = original bet amount (100%, zero fees)
7. Transaction Flows
Flow 1: Create a Market
Cost: 0.1 SOL (creation fee) + bet amount (min 0.01 SOL) + ~0.003 SOL (rent + tx fees) = ~0.113 SOL minimum
-
Find a token without a market:
GET https://pumpbet-mainnet.up.railway.app/api/tokens?has_market=false&sort=bonding_desc&limit=20 -
Validate bonding progress (on-chain rejects >= 95):
POST https://pumpbet-mainnet.up.railway.app/api/tokens/{mint}/validate-bondingIf
valid: false, stop. The on-chain program will also reject if bonding >= 95 (error 6018), so worst case you lose the transaction fee. -
Build, sign, and send the
createMarkettransaction (see Section 6) -
Sync to database (optional, speeds up API visibility):
POST https://pumpbet-mainnet.up.railway.app/api/bets/sync Body: { "betAddress": "<creator bet PDA at index 0>" }
Flow 2: Place a Bet
Cost: bet amount (min 0.01 SOL) + ~0.003 SOL (rent + tx fees) = ~0.013 SOL minimum
-
Find an active market:
GET https://pumpbet-mainnet.up.railway.app/api/markets -
Get market details:
GET https://pumpbet-mainnet.up.railway.app/api/markets/{marketAddress} -
Validate bonding (on-chain rejects >= 95):
POST https://pumpbet-mainnet.up.railway.app/api/tokens/{tokenMint}/validate-bondingIf
valid: false, stop — do not proceed with the bet. -
Fetch on-chain
totalBetsfor bet index:const market = await program.account.bettingMarket.fetch(marketPDA); const betIndex = market.totalBets;This must come from the on-chain account, not the API, to avoid stale data.
-
Build, sign, and send the
placeBettransaction (see Section 6) -
If transaction fails with PDA-already-exists error (bet index collision):
→ Re-fetch market.totalBets from chain → Rebuild transaction with new index → RetryNo funds are lost on a failed
init— Solana rolls back the transaction. -
Sync to database (optional):
POST https://pumpbet-mainnet.up.railway.app/api/bets/sync Body: { "betAddress": "<user bet PDA>" }
Flow 3: Claim Payout (You Won)
Cost: ~0.000005 SOL (transaction fee only)
-
Check claimable bets:
GET https://pumpbet-mainnet.up.railway.app/api/users/{wallet}/claimable/details -
For each bet where
isRefund: false: build and sendclaimPayout(see Section 6) -
Record claim (optional):
POST https://pumpbet-mainnet.up.railway.app/api/bets/claim/{betAddress} Body: { "signature": "<tx sig>", "amount": <payout_lamports>, "userWallet": "<wallet>" }
Flow 4: Claim Refund (Market Voided)
Cost: ~0.000005 SOL (transaction fee only)
-
Check claimable bets:
GET https://pumpbet-mainnet.up.railway.app/api/users/{wallet}/claimable/details -
For each bet where
isRefund: true: build and sendclaimRefund(see Section 6) -
Record claim (optional):
POST https://pumpbet-mainnet.up.railway.app/api/bets/claim/{betAddress} Body: { "signature": "<tx sig>", "amount": <refund_lamports>, "userWallet": "<wallet>" }
8. Signal Evaluation & Strategy
Key Signals
-
Bonding Progress (
bondingProgress): 0–100 scale. Higher = closer to graduation.- 80%+ = strong YES signal
- < 20% with > 30 min elapsed = strong NO signal
-
Delta (
deltafrom/api/markets/:address):bondingProgress - yesImplied- Delta > +20: Market underpricing YES — potential value bet on YES
- Delta < -20: Market overpricing YES — potential value bet on NO
- Delta near 0: Efficiently priced — lower expected value
-
Market Cap (
marketCapUsd): Below $4,500 triggers rug detection (5-minute countdown to NO resolution). -
Time Remaining: Markets with < 10 minutes and bonding < 50% are likely NO.
- The single market endpoint (
/api/markets/:address) returnsdeadlineSlotbut notslotsUntilResolutionorestimatedResolutionMinutes. - Compute it yourself:
const deadlineSlot = Number(market.deadlineSlot); const currentSlot = await connection.getSlot(); const slotsLeft = deadlineSlot - currentSlot; const minutesLeft = (slotsLeft * 0.4) / 60; slotsUntilResolutionandestimatedResolutionMinutesonly appear inGET /api/markets/resolvedforpendingstatus markets.
- The single market endpoint (
-
Pool Sizes (
pools.yes,pools.no): Imbalanced pools offer better odds for the minority side. Fully one-sided pools (0 on one side) will be voided. -
SOL Reserves (
solReserves): Higher = more bonding curve liquidity = healthier token.
Recommended Loop — Event-Driven + Polling Hybrid
For maximum efficiency, use the /prices WebSocket for real-time bonding updates and graduation events, and only poll /api/markets and /api/users/{wallet}/claimable/details on a 30-second interval.
On startup:
Connect to wss://pumpbet-mainnet.up.railway.app/prices
Subscribe: { "type": "subscribe_all" }
On each WebSocket price update (real-time):
Track bondingProgress per mint in local state
On graduation event → mark token as graduated, skip future bets
Every 30 seconds (polling):
1. GET /api/markets
→ Get all active markets (includes pools, odds, delta)
2. For each active market:
a. Compare delta, local bondingProgress, time remaining, pool balance
b. If actionable signal:
- POST /api/tokens/{mint}/validate-bonding
- If valid: place bet
3. GET /api/users/{wallet}/claimable/details
→ Claim any unclaimed winnings or refunds
The WebSocket delivers bonding progress and graduation events in real-time (up to 5 updates/token/sec), eliminating the need to poll /api/tokens. The 30-second poll covers market discovery and claims.
Good Citizen Patterns
- Cache token metadata (name, symbol, image) — it never changes
- Batch price lookups via
POST /api/prices/live(up to 50 mints) instead of individual GETs - Use
/pricesWebSocket for live bonding progress instead of polling/api/tokens - Listen for graduation events on the WebSocket instead of polling
validate-bonding
9. Resolution Logic
Markets are resolved by a fully automated keeper service. Agents cannot trigger resolution — only the hardcoded keeper authority wallet can call resolveMarket.
Resolution Priority
| Priority | Condition | Outcome | When |
|---|---|---|---|
| 1 | One-sided pool (one side = 0) | VOID | After deadline |
| 2 | Token graduated (Bitquery confirms migration) | YES | Immediately (early resolution) |
| 3 | Market cap < $4,500 for 750+ slots (~5 min) | NO (rug) | Within 60s of threshold |
| 4 | Deadline passed + not graduated | NO (timeout) | Within ~5s of deadline |
Keeper Internals
- Check interval: Every 5 seconds
- Graduation detection: Bitquery batch API + Bitquery WebSocket + on-chain watcher (multi-layer)
- Rug detection: Helius prices recorded every 60s. Requires ALL samples below $4,500 for 750 slots, minimum 10 data points, no recovery above threshold
- Bitquery retry: Up to 3 attempts at 5-minute intervals if Bitquery is unreliable at deadline
- Typical delay: < 10 seconds from triggering condition to on-chain resolution
These are current implementation details, not protocol guarantees. Resolution behavior may change.
What Agents Should Know
- Markets resolve automatically — just wait
- YES can resolve early (graduation before deadline)
- NO only resolves after deadline (timeout) or during the market (rug)
- Voided markets (one-sided) resolve after deadline
- After resolution, claim winnings/refunds immediately — no rush, but no reason to wait
10. Error Handling
On-Chain Errors
| Code | Name | Agent Action |
|---|---|---|
| 6000 | AlreadyResolved |
Market done — check outcome, claim if applicable |
| 6001 | MarketExpired |
Can't bet — deadline passed |
| 6002 | NotResolved |
Can't claim yet — wait for keeper |
| 6003 | AlreadyClaimed |
Already claimed — skip |
| 6004 | DidNotWin |
Wrong prediction — no payout |
| 6005 | TooEarlyToResolve |
Only keeper can resolve before deadline |
| 6006 | MarketNotVoided |
Tried claimPayout on a voided market — use claimRefund instead |
| 6007 | TokenAlreadyGraduated |
Can't create market — token graduated |
| 6008 | MarketAlreadyExists |
One market per token — bet on existing market instead |
| 6009 | BetTooSmall |
Increase to >= 0.01 SOL |
| 6010 | BetTooLarge |
Decrease to <= 10 SOL |
| 6011 | InvalidTokenMint |
Invalid mint address — verify the token mint pubkey |
| 6012 | InsufficientFunds |
Not enough SOL — check wallet balance covers bet + fees |
| 6017 | ClaimPeriodNotEnded |
Vault can't be closed yet — wait 7 days after resolution |
| 6018 | BondingProgressTooHigh |
Bonding >= 95% — can't bet or create market |
| 6019 | InvalidTreasury |
Wrong treasury address — check you're using mainnet treasury |
PDA Collision (Bet Index Race)
If your placeBet transaction fails because the UserBet PDA already exists (another user claimed that index first):
- Re-fetch
market.totalBetsfrom the on-chain account - Derive a new UserBet PDA with the updated index
- Rebuild and resubmit the transaction
No funds are lost. Solana atomically rolls back failed transactions.
API Error Format
{
"error": {
"message": "Not found",
"code": "NOT_FOUND",
"path": "/api/endpoint"
}
}
HTTP codes: 400 (bad request), 404 (not found), 422 (validation), 429 (rate limited), 500 (server error).
11. Rate Limits & Best Practices
API Rate Limits
| Metric | Value |
|---|---|
| Requests per window | 1,000 |
| Window duration | 60 seconds |
| Response headers | ratelimit-limit, ratelimit-remaining, ratelimit-reset |
| Header format | IETF RateLimit (not X-RateLimit) |
1,000 req/min = ~16.6 req/s. An agent polling 50 markets every 30s uses ~100 req/min — well within limits.
Best Practices
- Prefer WebSockets over polling for prices and market updates
- Cache immutable data — token name/symbol/image never changes
- Batch price requests —
POST /api/prices/livewith up to 50 mints per call - Always validate bonding before any on-chain transaction — saves wasted tx fees
- Handle bet index collisions — refetch
totalBets, rebuild PDA, retry - Sync bets after placing —
POST /api/bets/syncfor immediate API visibility (optional) - Reconnect WebSockets with exponential backoff (3s base, 1.5x, max 10 attempts)
- Respond to WebSocket pings —
{ "type": "pong" }within 30 seconds
External Data Dependencies
PumpMarket relies on three external data providers. All are off-chain — if they fail, the platform degrades but on-chain funds remain safe.
| Data | Provider | Used For | Failure Impact |
|---|---|---|---|
| Bonding progress | Bitquery | validate-bonding, market creation |
Cannot create markets or validate bets; keeper retries resolution up to 3× at 5-min intervals |
| Graduation detection | Bitquery | YES resolution trigger | Resolution delayed until Bitquery recovers; markets stay open longer |
| Token prices | Birdeye | /prices/live, rug detection |
Stale prices may delay rug-based NO resolution |
| Token metadata | Helius | Token names, images, DAS | Display-only — no impact on betting or resolution |
What this means for agents:
- If
validate-bondingreturns an error or timeout, do not guess a bondingProgress value — wait and retry - If
/prices/livereturns stale data, rug detection may lag — factor this into NO-side risk - On-chain funds (vaults, bets) are never at risk from API downtime — only resolution timing is affected
12. FAQ & Gotchas
Can agents trigger market resolution?
No. Only the keeper authority wallet (3cHDNTUqsqV4XSDdzuinvzaENKaUTKmud9m8enWFMfTh) can call resolveMarket. Resolution is fully automated.
What if I skip the sync/claim API calls?
Everything still works on-chain. The POST /api/bets/sync and POST /api/bets/claim/:betAddress endpoints only update the database for API/UI visibility. A background service syncs untracked bets within minutes. Your on-chain SOL is never affected.
Can the same user bet multiple times on one market?
Yes. Each bet gets a unique index from market.totalBets, creating a unique PDA. You can bet multiple times, even on opposite sides (YES and NO).
What is the 95% bonding rule exactly?
The on-chain check is bonding_progress < 95 (strict less-than). Values 0–94 are accepted. 95 and above are rejected. This is enforced in BOTH createMarket and placeBet instructions, AND server-side via the validate-bonding endpoint.
When does the 1-hour countdown start?
At market creation. resolution_slot = current_slot + 9,000 (~60 minutes at 400ms/slot). There is no way to extend or reset it.
Can a market be created for a graduated token?
No. The validate-bonding endpoint checks migration status, and the on-chain bonding check (< 95%) catches near-graduation tokens.
What's the minimum SOL to participate?
| Action | Cost |
|---|---|
| Create market | 0.1 SOL (fee) + 0.01 SOL (min bet) + ~0.003 SOL (rent/tx) = ~0.113 SOL |
| Place bet | 0.01 SOL (min bet) + ~0.003 SOL (rent/tx) = ~0.013 SOL |
| Claim payout/refund | ~0.000005 SOL (tx fee only) |
Are wallet addresses truncated?
List endpoints (leaderboard, markets/resolved) truncate to "Xxxx...xxxx". Use /api/markets/:address for the full creator wallet. Use /api/bets/:betAddress for full bet details.
What's the bonding progress formula?
bondingProgress = 100 - (((Pool_Base_PostAmount - 206900000) * 100) / 793100000)
Calculated off-chain from Bitquery/Helius data and passed to on-chain instructions. The API's validate-bonding endpoint verifies against fresh Bitquery data — always use it.
Normalizing across endpoints
Market list and single-market endpoints use different field names for the same data. Use this pattern to normalize:
const marketId = market.address ?? market.marketAddress;
const bonding = typeof token.bondingProgress === 'string'
? parseFloat(token.bondingProgress) : token.bondingProgress;
Does dust remain in the vault after claims?
Yes. Parimutuel integer math may leave small amounts (< 1 lamport per bet) in the vault after all winners claim. This is recovered via closeVault after the 7-day claim period.
What infrastructure runs PumpMarket?
- Frontend: Vercel (Next.js) at
pumpmarket.fun - Backend: Railway (Express.js) at
pumpbet-mainnet.up.railway.app - CDN: Fastly via Railway
- Database: PostgreSQL (Railway)
- Cache: Redis (Railway)
- Blockchain: Solana mainnet-beta
13. Appendix: Account Data Layout
BettingMarket (123 bytes: 8 discriminator + 115 data)
| Offset | Size | Type | Field |
|---|---|---|---|
| 0 | 8 | — | Discriminator |
| 8 | 32 | Pubkey | tokenMint |
| 40 | 32 | Pubkey | creator |
| 72 | 8 | u64 | creationSlot |
| 80 | 8 | u64 | resolutionSlot |
| 88 | 8 | u64 | yesPool (lamports) |
| 96 | 8 | u64 | noPool (lamports) |
| 104 | 4 | u32 | totalBets (use this for bet index derivation) |
| 108 | 1 | bool | resolved |
| 109 | 1 | bool | voided |
| 110 | 1+1 | Option<bool> | outcome (None=unresolved, Some(true)=YES, Some(false)=NO) |
| 112 | 1+8 | Option<u64> | rugDetectionStartSlot |
| 121 | 1 | u8 | bondingProgressAtStart |
| 122 | 1 | u8 | bump |
UserBet (91 bytes: 8 discriminator + 83 data)
| Offset | Size | Type | Field |
|---|---|---|---|
| 8 | 32 | Pubkey | market |
| 40 | 32 | Pubkey | user |
| 72 | 8 | u64 | amount (lamports) |
| 80 | 1 | bool | prediction (true=YES, false=NO) |
| 81 | 8 | u64 | slot |
| 89 | 1 | bool | claimed |
| 90 | 1 | u8 | bump |
Vault (41 bytes: 8 discriminator + 33 data)
| Offset | Size | Type | Field |
|---|---|---|---|
| 8 | 32 | Pubkey | market |
| 40 | 1 | u8 | bump |
14. Appendix: On-Chain Events
| Event | Emitted By | Key Fields |
|---|---|---|
MarketCreated |
createMarket |
market, tokenMint, creator, initialAmount, initialPrediction, creationSlot, resolutionSlot |
BetPlaced |
createMarket, placeBet |
market, user, betAddress, amount, prediction, slot, yesPoolAfter, noPoolAfter |
MarketResolved |
resolveMarket |
market, outcome, voided, totalPool, platformFee, creatorFee, resolutionSlot |
PayoutClaimed |
claimPayout |
market, user, betAddress, payoutAmount, originalBet, prediction |
RefundClaimed |
claimRefund |
market, user, betAddress, refundAmount |
VaultClosed |
closeVault |
market, vault, rentRecovered, recipient |
EmergencyWithdraw |
emergencyWithdraw |
market, vault, amountWithdrawn, recipient, triggeredBy |
15. Appendix: Dry-Run Simulation
Development tool. Use this to verify your transaction construction is correct before spending real SOL. This simulates the transaction on-chain and returns success/failure without submitting.
Verify end-to-end integration without spending SOL. This script fetches an active market, builds a placeBet transaction, and simulates it on-chain.
import { Connection, PublicKey, Transaction } from '@solana/web3.js';
import { Program, AnchorProvider, BN, web3 } from '@coral-xyz/anchor';
import IDL from './pumpbets.json';
const PROGRAM_ID = new PublicKey('3mNbBV3Xc3rNJ4E87pSFzW7VhUZySHQDQVyd4MP2VFG6');
const API = 'https://pumpbet-mainnet.up.railway.app';
async function dryRun(wallet: web3.Keypair) {
const connection = new Connection('https://api.mainnet-beta.solana.com', 'confirmed');
const provider = new AnchorProvider(
connection,
{ publicKey: wallet.publicKey, signTransaction: async (tx) => { tx.sign(wallet); return tx; }, signAllTransactions: async (txs) => { txs.forEach(tx => tx.sign(wallet)); return txs; } },
{ commitment: 'confirmed' }
);
const program = new Program(IDL as any, PROGRAM_ID, provider);
// 1. Fetch active markets
const res = await fetch(`${API}/api/markets`);
const data = await res.json();
if (!data.markets?.length) { console.log('No active markets — nothing to simulate.'); return; }
const market = data.markets[0];
console.log(`Simulating placeBet on market: ${market.address} (${market.token?.symbol})`);
const marketPDA = new PublicKey(market.address);
const [vaultPDA] = PublicKey.findProgramAddressSync(
[Buffer.from('vault'), marketPDA.toBuffer()], PROGRAM_ID
);
// 2. Determine bet index from on-chain market account
const marketAccount = await program.account.bettingMarket.fetch(marketPDA);
const betIndex = marketAccount.totalBets;
const [userBetPDA] = PublicKey.findProgramAddressSync(
[Buffer.from('bet'), marketPDA.toBuffer(), wallet.publicKey.toBuffer(), new BN(betIndex).toArrayLike(Buffer, 'le', 4)],
PROGRAM_ID
);
// 3. Build transaction (0.01 SOL YES bet, bonding_progress = 50 placeholder — simulation only)
const ix = await program.methods
.placeBet(true, new BN(10_000_000), 50)
.accounts({
market: marketPDA,
userBet: userBetPDA,
user: wallet.publicKey,
vault: vaultPDA,
systemProgram: web3.SystemProgram.programId,
})
.instruction();
const tx = new Transaction().add(ix);
tx.recentBlockhash = (await connection.getLatestBlockhash()).blockhash;
tx.feePayer = wallet.publicKey;
// 4. Simulate — does NOT send or spend SOL
const sim = await connection.simulateTransaction(tx, [wallet]);
if (sim.value.err) {
console.error('Simulation FAILED:', JSON.stringify(sim.value.err));
console.error('Logs:', sim.value.logs?.join('\n'));
} else {
console.log('Simulation SUCCESS — transaction would execute correctly.');
console.log('Logs:', sim.value.logs?.join('\n'));
}
}
// Usage: dryRun(yourKeypair);
Note: Simulation uses
bonding_progress = 50as a placeholder. In production, always fetch the real value fromPOST /api/tokens/{mint}/validate-bonding. Simulation does not debit your wallet.
Reviews (0)
No reviews yet. Be the first to review!
Comments (0)
No comments yet. Be the first to share your thoughts!