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=20Validate 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/marketsGet 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/detailsFor 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/detailsFor 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) returns `deadlineS
- The single market endpoint (
…(truncated)