Install
openclaw skills install pmt-skillPredict pump.fun token graduations (YES/NO) on Solana mainnet via PumpMarket parimutuel betting markets.
openclaw skills install pmt-skillTrade 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.
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:
Market outcomes:
Before an agent can trade, it needs:
Keypair.fromSecretKey(...))https://api.mainnet-beta.solana.com or a Helius/Quicknode endpoint)The public https://api.mainnet-beta.solana.com is rate-limited. For production agents, use a dedicated RPC provider (Helius, Quicknode, Triton, etc.).
PumpMarket requires users to accept terms before their activity is fully tracked. This is a one-time operation:
GET https://pumpbet-mainnet.up.railway.app/api/users/{wallet}/terms/message
Response:
{ "message": "PumpMarket Terms of Service\nI agree to the terms at https://pumpmarket.fun/terms\nWallet: ...\nTimestamp: ...", "timestamp": 1772413074485, "wallet": "..." }
message string 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);
POST https://pumpbet-mainnet.up.railway.app/api/users/{wallet}/terms
Body: { "signature": "<base58 signature>", "timestamp": <timestamp from step 1> }
GET https://pumpbet-mainnet.up.railway.app/api/users/{wallet}/terms
→ { "wallet": "...", "termsAccepted": true }
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.
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:
600 (chmod 600 keyfile.json)*.json keypair paths to .gitignoreThis skill targets mainnet-beta where bets use real SOL. To test transaction construction without spending:
beta.pumpmarket.fun) used for internal testing. The devnet program ID is 3mNbBV3Xc3rNJ4E87pSFzW7VhUZySHQDQVyd4MP2VFG6 (same binary, different state). Devnet is not guaranteed to be stable or have active markets.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 |
| 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 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
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)"
| Address | Purpose |
|---|---|
3mNbBV3Xc3rNJ4E87pSFzW7VhUZySHQDQVyd4MP2VFG6 | PumpBets Program ID |
4iFYGzxKGH2SAeVaR5AxPiCfLCSQD9fdPK8tsDBbmx3f | Treasury |
3cHDNTUqsqV4XSDdzuinvzaENKaUTKmud9m8enWFMfTh | Keeper Authority |
| 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 |
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).
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/health | Health check — DB/Redis status, uptime, version |
| GET | /api/stats | Platform stats — marketsResolved, activeMarkets, totalVolumeSol |
| 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..." }
| 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"
}
| 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
}
| 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://..." }
}
| 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
}
| 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 |
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 |
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.
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 StreamOn 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 FeedOn 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
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()));
Since @pumpmarket/sdk is not published to npm, agents must construct transactions using @coral-xyz/anchor and @solana/web3.js directly.
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.
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
);
}
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.
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);
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);
// 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);
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);
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.
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)
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-bonding
If 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 createMarket transaction (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>" }
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-bonding
If valid: false, stop — do not proceed with the bet.
Fetch on-chain totalBets for 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 placeBet transaction (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
→ Retry
No 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>" }
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 send claimPayout (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>" }
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 send claimRefund (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>" }
Bonding Progress (bondingProgress): 0–100 scale. Higher = closer to graduation.
Delta (delta from /api/markets/:address): bondingProgress - yesImplied
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.
/api/markets/:address) returns deadlineSlot but not slotsUntilResolution or estimatedResolutionMinutes.const deadlineSlot = Number(market.deadlineSlot); const currentSlot = await connection.getSlot(); const slotsLeft = deadlineSlot - currentSlot; const minutesLeft = (slotsLeft * 0.4) / 60;slotsUntilResolution and estimatedResolutionMinutes only appear in GET /api/markets/resolved for pending status markets.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.
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.
POST /api/prices/live (up to 50 mints) instead of individual GETs/prices WebSocket for live bonding progress instead of polling /api/tokensvalidate-bondingMarkets are resolved by a fully automated keeper service. Agents cannot trigger resolution — only the hardcoded keeper authority wallet can call resolveMarket.
| 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 |
These are current implementation details, not protocol guarantees. Resolution behavior may change.
| 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 |
If your placeBet transaction fails because the UserBet PDA already exists (another user claimed that index first):
market.totalBets from the on-chain accountNo funds are lost. Solana atomically rolls back failed transactions.
{
"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).
| 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.
POST /api/prices/live with up to 50 mints per calltotalBets, rebuild PDA, retryPOST /api/bets/sync for immediate API visibility (optional){ "type": "pong" } within 30 secondsPumpMarket 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:
validate-bonding returns an error or timeout, do not guess a bondingProgress value — wait and retry/prices/live returns stale data, rug detection may lag — factor this into NO-side riskNo. Only the keeper authority wallet (3cHDNTUqsqV4XSDdzuinvzaENKaUTKmud9m8enWFMfTh) can call resolveMarket. Resolution is fully automated.
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.
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).
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.
At market creation. resolution_slot = current_slot + 9,000 (~60 minutes at 400ms/slot). There is no way to extend or reset it.
No. The validate-bonding endpoint checks migration status, and the on-chain bonding check (< 95%) catches near-graduation tokens.
| 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) |
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.
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.
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;
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.
pumpmarket.funpumpbet-mainnet.up.railway.app| 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 |
| 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 |
| Offset | Size | Type | Field |
|---|---|---|---|
| 8 | 32 | Pubkey | market |
| 40 | 1 | u8 | bump |
| 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 |
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.