pumpmarket skill

v1.0.0

Predict pump.fun token graduations (YES/NO) on Solana mainnet via PumpMarket parimutuel betting markets.

0· 255·0 current·0 all-time
byKing0@notking0
Security Scan
VirusTotalVirusTotal
Benign
View report →
OpenClawOpenClaw
Suspicious
medium confidence
!
Purpose & Capability
The skill's stated purpose (betting on PumpMarket markets using a Solana wallet) is coherent with the actions described in SKILL.md (constructing transactions, signing, posting to an API). However the registry metadata lists no required environment variables or keyfile paths while the instructions explicitly show loading private keys from a KEYPAIR_PATH or PRIVATE_KEY env var and using npm packages. The absence of declared credential requirements and the metadata/instruction mismatch is a coherence issue.
!
Instruction Scope
SKILL.md instructs an agent to read local keyfiles or environment variables, sign messages with a wallet private key, post signatures and wallet activity to an external API (https://pumpbet-mainnet.up.railway.app), and submit on-chain transactions. Those steps are functionally necessary for an autonomous betting agent, but they broaden scope to sensitive local secrets and external network I/O not reflected in the declared requirements. Posting signatures and wallet identifiers to third-party endpoints is explicit in the instructions.
Install Mechanism
There is no install spec (instruction-only skill), which limits direct disk writes. SKILL.md and metadata reference npm packages (@coral-xyz/anchor, @solana/web3.js) but the registry's top-level requirements only list curl. That mismatch (metadata claiming npm deps but no install declared) is an engineering inconsistency rather than an immediate supply-chain red flag, but you should confirm how/where those Node dependencies will be installed and from which registries before running.
!
Credentials
The skill needs a wallet private key to place bets and sign the 'terms' acceptance, which is proportionate to its betting purpose. However the registry does not declare any required env vars or config paths while SKILL.md shows examples using environment variables (PRIVATE_KEY, KEYPAIR_PATH). Declaring sensitive env vars but not listing them in metadata is an incoherence. Also posting signed messages to an external service and running autonomous transactions increases the risk if a high-value wallet is used.
Persistence & Privilege
The skill does not request always: true and does not include install-time scripts; persistence/privacy footprint is limited to the runtime instructions the agent follows. However, because the skill enables on-chain actions that spend real SOL, granting it access to wallet private keys (even temporarily) would let it perform transactions autonomously — a high-impact capability if keys are mishandled. This is a behavioral risk rather than a metadata misconfiguration.
What to consider before installing
This skill would legitimately need access to a Solana wallet to place bets, but the SKILL.md instructs the agent to read private keys from env vars or local files even though the registry metadata doesn't declare those secrets. Before installing: (1) Do not expose your main wallet private key—use a dedicated betting wallet funded with a small amount of SOL. (2) Verify ownership of the API/WebSocket host (pumpbet-mainnet.up.railway.app) and the homepage (pumpmarket.fun) — confirm they are official and review the posted IDL and program ID on-chain. (3) Prefer dry-run simulation and review the IDL/contract on Solana explorers to ensure the program behaves as claimed. (4) Confirm how Node dependencies will be installed and from which registries. (5) If you must provide a private key, use a short-lived or hardware-backed key and avoid storing it in plaintext environment variables or commits. If you want, provide the skill author/source or the exact IDL file URL and I can help verify the on-chain program and API endpoints further.

Like a lobster shell, security has layers — review code before you run it.

Runtime requirements

🎰 Clawdis
Binscurl
latestvk9767rt3cptsprc1gjddevs18s826hk6
255downloads
0stars
1versions
Updated 1mo ago
v1.0.0
MIT-0

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

  1. What Is PumpMarket
  2. First-Run Setup
  3. On-Chain Parameters
  4. API Reference
  5. WebSocket Reference
  6. Transaction Construction
  7. Transaction Flows
  8. Signal Evaluation & Strategy
  9. Resolution Logic
  10. Error Handling
  11. Rate Limits & Best Practices
  12. FAQ & Gotchas
  13. Appendix: Account Data Layout
  14. Appendix: On-Chain Events
  15. 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:

  1. A user creates a market for a pump.fun token, choosing YES or NO and staking SOL
  2. Other users bet YES (token will graduate) or NO (it won't) within the ~1-hour window
  3. An automated keeper resolves the market based on on-chain data
  4. 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.com or 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:

  1. Get the message to sign:
    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": "..." }
    
  2. Sign the 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);
    
  3. Submit the signed acceptance:
    POST https://pumpbet-mainnet.up.railway.app/api/users/{wallet}/terms
    Body: { "signature": "<base58 signature>", "timestamp": <timestamp from step 1> }
    
  4. 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 *.json keypair 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 is 3mNbBV3Xc3rNJ4E87pSFzW7VhUZySHQDQVyd4MP2VFG6 (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.

ParameterValueLamportsOn-Chain Enforcement
Market Creation Fee0.1 SOL100,000,000createMarket — transferred to treasury
Min Bet0.01 SOL10,000,000createMarket + placeBet
Max Bet10 SOL10,000,000,000createMarket + placeBet
Platform Fee3.5%350 BPSresolveMarket — from total pool to treasury
Creator Fee1.0%100 BPSresolveMarket — from total pool to creator
Total Fee4.5%450 BPSCombined (platform + creator)
Market Duration~1 hour9,000 slotscreateMarketresolution_slot = current + 9000
Max Bonding Progress< 95Strict < check: 0–94 accepted, 95+ rejected
Rug Threshold$4,500 USDOff-chain keeper checks (constants defined on-chain)
Rug Duration~5 minutes750 slotsMust sustain below threshold continuously
Claim Period~7 days1,512,000 slotsVault can be closed (rent reclaimed) after this
Emergency Delay~1 day216,000 slotsOnly treasury can emergency withdraw after this

Mainnet Activity (Indexer snapshot — 2026-03-02)

MetricValue
Program accounts490 (100 markets, 290 bets, 100 vaults)
Markets resolved53 (47 voided)
Total volume76.14 SOL
Treasury balance9.29 SOL
Keeper balance0.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

AddressPurpose
3mNbBV3Xc3rNJ4E87pSFzW7VhUZySHQDQVyd4MP2VFG6PumpBets Program ID
4iFYGzxKGH2SAeVaR5AxPiCfLCSQD9fdPK8tsDBbmx3fTreasury
3cHDNTUqsqV4XSDdzuinvzaENKaUTKmud9m8enWFMfThKeeper Authority

PDA Seeds

AccountSeedsConstraint
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

MethodEndpointDescription
GET/api/healthHealth check — DB/Redis status, uptime, version
GET/api/statsPlatform stats — marketsResolved, activeMarkets, totalVolumeSol

Tokens

MethodEndpointDescription
GET/api/tokensList tokens (paginated, filterable)
GET/api/tokens/featuredTop 5 tokens nearest graduation with active markets
GET/api/tokens/trendingTrending tokens by volume
GET/api/tokens/graduatedRecently graduated tokens
GET/api/tokens/prices/live?mints=mint1,mint2Birdeye prices (max 20 mints via GET)
GET/api/tokens/top-tradesBiggest trades in last 15 minutes
GET/api/tokens/:mintSingle token details
POST/api/tokens/:mint/validate-bondingFresh bonding validation (anti-stale-data) (no request body)
POST/api/tokens/:mint/refreshForce refresh bonding data from chain

GET /api/tokens Query Parameters:

ParamValuesDefault
sortvolume_desc, created_desc, bonding_desc, pool_desc, bonding_active_desccreated_desc
has_markettrue, false
userwallet address
creatorwallet address
cursorstring (from nextCursor)
limit1–10020

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

MethodEndpointDescription
GET/api/marketsActive (unresolved) markets only
GET/api/markets/resolvedResolved/voided/pending markets (filterable)
GET/api/markets/graduatedAlias for resolved with filter=resolved
GET/api/markets/:addressSingle market — full details, odds, delta
GET/api/markets/:address/betsPaginated bets for a market
GET/api/markets/:address/can-betCheck if market accepts bets
POST/api/markets/:address/check-graduationTrigger manual graduation check
GET/api/markets/by-creator/:walletMarkets 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: bondingProgress inside the token object on market endpoints is a string (e.g. "45.20"). On /api/tokens it's a number. Always parseFloat() 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:

ParamValuesDefault
filterall, resolved, pending, voidedall
cursorstring
limit1–10020
includeHolderstrue

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

MethodEndpointDescription
GET/api/users/:wallet/statsUser statistics (volume/profit in lamport strings, not SOL)
GET/api/users/:wallet/betsBet history (filterable)
GET/api/users/:wallet/bets/countsBet counts by status
GET/api/users/:wallet/claimableCount of claimable bets
GET/api/users/:wallet/claimable/detailsClaimable bets with estimated payouts
GET/api/users/:wallet/termsCheck terms acceptance
POST/api/users/:wallet/termsAccept terms (requires wallet signature)
GET/api/users/:wallet/terms/messageGet terms message to sign

GET /api/users/:wallet/bets Query Parameters:

ParamValuesDefault
statusactive, won, lost, refunded, allall
cursorstring
limit1–10020

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

MethodEndpointDescription
GET/api/bets/:betAddressSingle bet by on-chain address
POST/api/bets/syncSync bet from chain to DB (optional — background service catches up)
POST/api/bets/sync-marketSync all bets for a market (optional)
POST/api/bets/sync-userSync all bets for a user (optional)
POST/api/bets/claim/:betAddressRecord 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

MethodEndpointDescription
GET/api/prices/solSOL/USD price
GET/api/prices/:mintCached token price
POST/api/prices/liveBatch 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

MethodEndpointDescription
GET/api/leaderboardRanked users
GET/api/leaderboard/topTop 5 across all categories
GET/api/leaderboard/categoriesAvailable categories and time ranges

GET /api/leaderboard Query Parameters:

ParamValuesDefault
categoryprofit, winrate, volume, creatorsprofit
range24h, 7d, 30d, allall
walletwallet address
limit1–500100

5. WebSocket Reference

WebSocket URLs use the same host as the API, with wss:// protocol.

PathURLPurpose
/priceswss://pumpbet-mainnet.up.railway.app/pricesReal-time token price updates
/live-tradeswss://pumpbet-mainnet.up.railway.app/live-tradesLive $1000+ pump.fun trades
/user-eventswss://pumpbet-mainnet.up.railway.app/user-eventsUser-specific bet/market notifications
/activitywss://pumpbet-mainnet.up.railway.app/activityPlatform-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/markets and GET /api/tokens on 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_ID and TREASURY above MUST match the on-chain program deployment. If you switch environments (devnet/mainnet), update them everywhere. If your transaction fails with error 6019 InvalidTreasury, 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);

bondingProgress is 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 call POST /api/tokens/{mint}/validate-bonding immediately 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 from validate-bonding for 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

  1. Find a token without a market:

    GET https://pumpbet-mainnet.up.railway.app/api/tokens?has_market=false&sort=bonding_desc&limit=20
    
  2. 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.

  3. Build, sign, and send the createMarket transaction (see Section 6)

  4. 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

  1. Find an active market:

    GET https://pumpbet-mainnet.up.railway.app/api/markets
    
  2. Get market details:

    GET https://pumpbet-mainnet.up.railway.app/api/markets/{marketAddress}
    
  3. 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.

  4. 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.

  5. Build, sign, and send the placeBet transaction (see Section 6)

  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.

  7. 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)

  1. Check claimable bets:

    GET https://pumpbet-mainnet.up.railway.app/api/users/{wallet}/claimable/details
    
  2. For each bet where isRefund: false: build and send claimPayout (see Section 6)

  3. 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)

  1. Check claimable bets:

    GET https://pumpbet-mainnet.up.railway.app/api/users/{wallet}/claimable/details
    
  2. For each bet where isRefund: true: build and send claimRefund (see Section 6)

  3. 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

  1. Bonding Progress (bondingProgress): 0–100 scale. Higher = closer to graduation.

    • 80%+ = strong YES signal
    • < 20% with > 30 min elapsed = strong NO signal
  2. Delta (delta from /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
  3. Market Cap (marketCapUsd): Below $4,500 triggers rug detection (5-minute countdown to NO resolution).

  4. Time Remaining: Markets with < 10 minutes and bonding < 50% are likely NO.

    • The single market endpoint (/api/markets/:address) returns deadlineSlot but not slotsUntilResolution or estimatedResolutionMinutes.
    • Compute it yourself: 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.
  5. 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.

  6. 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 /prices WebSocket 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

PriorityConditionOutcomeWhen
1One-sided pool (one side = 0)VOIDAfter deadline
2Token graduated (Bitquery confirms migration)YESImmediately (early resolution)
3Market cap < $4,500 for 750+ slots (~5 min)NO (rug)Within 60s of threshold
4Deadline passed + not graduatedNO (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

CodeNameAgent Action
6000AlreadyResolvedMarket done — check outcome, claim if applicable
6001MarketExpiredCan't bet — deadline passed
6002NotResolvedCan't claim yet — wait for keeper
6003AlreadyClaimedAlready claimed — skip
6004DidNotWinWrong prediction — no payout
6005TooEarlyToResolveOnly keeper can resolve before deadline
6006MarketNotVoidedTried claimPayout on a voided market — use claimRefund instead
6007TokenAlreadyGraduatedCan't create market — token graduated
6008MarketAlreadyExistsOne market per token — bet on existing market instead
6009BetTooSmallIncrease to >= 0.01 SOL
6010BetTooLargeDecrease to <= 10 SOL
6011InvalidTokenMintInvalid mint address — verify the token mint pubkey
6012InsufficientFundsNot enough SOL — check wallet balance covers bet + fees
6017ClaimPeriodNotEndedVault can't be closed yet — wait 7 days after resolution
6018BondingProgressTooHighBonding >= 95% — can't bet or create market
6019InvalidTreasuryWrong 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):

  1. Re-fetch market.totalBets from the on-chain account
  2. Derive a new UserBet PDA with the updated index
  3. 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

MetricValue
Requests per window1,000
Window duration60 seconds
Response headersratelimit-limit, ratelimit-remaining, ratelimit-reset
Header formatIETF 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

  1. Prefer WebSockets over polling for prices and market updates
  2. Cache immutable data — token name/symbol/image never changes
  3. Batch price requestsPOST /api/prices/live with up to 50 mints per call
  4. Always validate bonding before any on-chain transaction — saves wasted tx fees
  5. Handle bet index collisions — refetch totalBets, rebuild PDA, retry
  6. Sync bets after placingPOST /api/bets/sync for immediate API visibility (optional)
  7. Reconnect WebSockets with exponential backoff (3s base, 1.5x, max 10 attempts)
  8. 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.

DataProviderUsed ForFailure Impact
Bonding progressBitqueryvalidate-bonding, market creationCannot create markets or validate bets; keeper retries resolution up to 3× at 5-min intervals
Graduation detectionBitqueryYES resolution triggerResolution delayed until Bitquery recovers; markets stay open longer
Token pricesBirdeye/prices/live, rug detectionStale prices may delay rug-based NO resolution
Token metadataHeliusToken names, images, DASDisplay-only — no impact on betting or resolution

What this means for agents:

  • If validate-bonding returns an error or timeout, do not guess a bondingProgress value — wait and retry
  • If /prices/live returns 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?

ActionCost
Create market0.1 SOL (fee) + 0.01 SOL (min bet) + ~0.003 SOL (rent/tx) = ~0.113 SOL
Place bet0.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)

OffsetSizeTypeField
08Discriminator
832PubkeytokenMint
4032Pubkeycreator
728u64creationSlot
808u64resolutionSlot
888u64yesPool (lamports)
968u64noPool (lamports)
1044u32totalBets (use this for bet index derivation)
1081boolresolved
1091boolvoided
1101+1Option<bool>outcome (None=unresolved, Some(true)=YES, Some(false)=NO)
1121+8Option<u64>rugDetectionStartSlot
1211u8bondingProgressAtStart
1221u8bump

UserBet (91 bytes: 8 discriminator + 83 data)

OffsetSizeTypeField
832Pubkeymarket
4032Pubkeyuser
728u64amount (lamports)
801boolprediction (true=YES, false=NO)
818u64slot
891boolclaimed
901u8bump

Vault (41 bytes: 8 discriminator + 33 data)

OffsetSizeTypeField
832Pubkeymarket
401u8bump

14. Appendix: On-Chain Events

EventEmitted ByKey Fields
MarketCreatedcreateMarketmarket, tokenMint, creator, initialAmount, initialPrediction, creationSlot, resolutionSlot
BetPlacedcreateMarket, placeBetmarket, user, betAddress, amount, prediction, slot, yesPoolAfter, noPoolAfter
MarketResolvedresolveMarketmarket, outcome, voided, totalPool, platformFee, creatorFee, resolutionSlot
PayoutClaimedclaimPayoutmarket, user, betAddress, payoutAmount, originalBet, prediction
RefundClaimedclaimRefundmarket, user, betAddress, refundAmount
VaultClosedcloseVaultmarket, vault, rentRecovered, recipient
EmergencyWithdrawemergencyWithdrawmarket, 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 = 50 as a placeholder. In production, always fetch the real value from POST /api/tokens/{mint}/validate-bonding. Simulation does not debit your wallet.

Comments

Loading comments...