# Pumpmarket

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

- Skill: `dvcrn/pumpmarket` (Agent Skill, multi-file: 2 files)
- Install (CLI): `npx skillmds@latest add dvcrn/pumpmarket`
- Raw SKILL.md: https://api.skillmd.com/api/skills/dvcrn/pumpmarket/raw
- Safety review: pending (external: skill-scanner WARNING, skillspector WARNING)
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Coding & Dev Tools
- Author: dvcrn (https://skillmd.com/u/dvcrn)
- Updated: 2026-09-08
- Page: https://skillmd.com/skills/dvcrn/pumpmarket

---


# 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`](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](#1-what-is-pumpmarket)
2. [First-Run Setup](#2-first-run-setup)
3. [On-Chain Parameters](#3-on-chain-parameters)
4. [API Reference](#4-api-reference)
5. [WebSocket Reference](#5-websocket-reference)
6. [Transaction Construction](#6-transaction-construction)
7. [Transaction Flows](#7-transaction-flows)
8. [Signal Evaluation & Strategy](#8-signal-evaluation--strategy)
9. [Resolution Logic](#9-resolution-logic)
10. [Error Handling](#10-error-handling)
11. [Rate Limits & Best Practices](#11-rate-limits--best-practices)
12. [FAQ & Gotchas](#12-faq--gotchas)
13. [Appendix: Account Data Layout](#13-appendix-account-data-layout)
14. [Appendix: On-Chain Events](#14-appendix-on-chain-events)
15. [Appendix: Dry-Run Simulation](#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:
   ```json
   { "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:**
   ```typescript
   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
```bash
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](#6-transaction-construction).

### Keypair Security

> **Never hardcode private keys in source code or commit them to repositories.**

**Loading a keypair from file:**
```typescript
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:**
```typescript
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](#15-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`.

| Parameter | Value | Lamports | On-Chain Enforcement |
|-----------|-------|----------|---------------------|
| **Market Creation Fee** | 0.1 SOL | 100,000,000 | `createMarket` — transferred to treasury |
| **Min Bet** | 0.01 SOL | 10,000,000 | `createMarket` + `placeBet` |
| **Max Bet** | 10 SOL | 10,000,000,000 | `createMarket` + `placeBet` |
| **Platform Fee** | 3.5% | 350 BPS | `resolveMarket` — from total pool to treasury |
| **Creator Fee** | 1.0% | 100 BPS | `resolveMarket` — from total pool to creator |
| **Total Fee** | 4.5% | 450 BPS | Combined (platform + creator) |
| **Market Duration** | ~1 hour | 9,000 slots | `createMarket` — `resolution_slot = current + 9000` |
| **Max Bonding Progress** | < 95 | — | Strict `<` check: 0–94 accepted, **95+ rejected** |
| **Rug Threshold** | $4,500 USD | — | Off-chain keeper checks (constants defined on-chain) |
| **Rug Duration** | ~5 minutes | 750 slots | Must sustain below threshold continuously |
| **Claim Period** | ~7 days | 1,512,000 slots | Vault can be closed (rent reclaimed) after this |
| **Emergency Delay** | ~1 day | 216,000 slots | Only treasury can emergency withdraw after this |

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

| Metric | Value |
|--------|-------|
| Program accounts | 490 (100 markets, 290 bets, 100 vaults) |
| Markets resolved | 53 (47 voided) |
| Total volume | 76.14 SOL |
| Treasury balance | 9.29 SOL |
| Keeper balance | 0.12 SOL |

> These values are from PumpMarket's internal indexer at the timestamp above and may differ from current on-chain state.

### Verify Mainnet Deployment

```bash
# 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.

```bash
RPC=https://api.mainnet-beta.solana.com
PID=3mNbBV3Xc3rNJ4E87pSFzW7VhUZySHQDQVyd4MP2VFG6

count_by_size () {
  curl -s $RPC -X POST -H 'Content-Type: application/json' -d "{
    \"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"getProgramAccounts\",
    \"params\":[\"$PID\",{
      \"encoding\":\"base64\",
      \"dataSlice\":{\"offset\":0,\"length\":0},
      \"filters\":[{\"dataSize\":$1}]
    }]
  }" | jq '.result | length'
}

echo "BettingMarket (123): $(count_by_size 123)"
echo "UserBet (91):         $(count_by_size 91)"
echo "Vault (41):           $(count_by_size 41)"
```

### Mainnet Addresses

| Address | Purpose |
|---------|---------|
| `3mNbBV3Xc3rNJ4E87pSFzW7VhUZySHQDQVyd4MP2VFG6` | PumpBets Program ID |
| `4iFYGzxKGH2SAeVaR5AxPiCfLCSQD9fdPK8tsDBbmx3f` | Treasury |
| `3cHDNTUqsqV4XSDdzuinvzaENKaUTKmud9m8enWFMfTh` | Keeper Authority |

### PDA Seeds

| Account | Seeds | Constraint |
|---------|-------|-----------|
| Market | `["market", token_mint]` | One market per token mint |
| User Bet | `["bet", market, user, bet_index_u32_le]` | Seed is `"bet"` (not `"user_bet"`) |
| Vault | `["vault", market]` | Holds all SOL for a market |

---

## 4. API Reference

**Base URL:** `https://pumpbet-mainnet.up.railway.app`

All endpoints are **public with no authentication** — no API keys, no wallet signatures, no headers required. The only exception is `POST /api/users/:wallet/terms` which requires a wallet signature (one-time terms acceptance).

### Health & Stats

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/health` | Health check — DB/Redis status, uptime, version |
| GET | `/api/stats` | Platform stats — `marketsResolved`, `activeMarkets`, `totalVolumeSol` |

### Tokens

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/tokens` | List tokens (paginated, filterable) |
| GET | `/api/tokens/featured` | Top 5 tokens nearest graduation with active markets |
| GET | `/api/tokens/trending` | Trending tokens by volume |
| GET | `/api/tokens/graduated` | Recently graduated tokens |
| GET | `/api/tokens/prices/live?mints=mint1,mint2` | Birdeye prices (max 20 mints via GET) |
| GET | `/api/tokens/top-trades` | Biggest trades in last 15 minutes |
| GET | `/api/tokens/:mint` | Single token details |
| POST | `/api/tokens/:mint/validate-bonding` | Fresh bonding validation (anti-stale-data) (**no request body**) |
| POST | `/api/tokens/:mint/refresh` | Force refresh bonding data from chain |

**GET `/api/tokens` Query Parameters:**

| Param | Values | Default |
|-------|--------|---------|
| `sort` | `volume_desc`, `created_desc`, `bonding_desc`, `pool_desc`, `bonding_active_desc` | `created_desc` |
| `has_market` | `true`, `false` | — |
| `user` | wallet address | — |
| `creator` | wallet address | — |
| `cursor` | string (from `nextCursor`) | — |
| `limit` | 1–100 | 20 |

**Response:**
```json
{
  "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):**
```json
{
  "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:
```json
{ "valid": true, "bondingProgress": 42, "isGraduated": false, "priceUsd": 0.001, "verifiedAt": "..." }
```
or:
```json
{ "valid": false, "bondingProgress": 97, "reason": "Token is at 97% bonding progress..." }
```

### Markets

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/markets` | **Active (unresolved) markets only** |
| GET | `/api/markets/resolved` | Resolved/voided/pending markets (filterable) |
| GET | `/api/markets/graduated` | Alias for resolved with `filter=resolved` |
| GET | `/api/markets/:address` | Single market — full details, odds, delta |
| GET | `/api/markets/:address/bets` | Paginated bets for a market |
| GET | `/api/markets/:address/can-bet` | Check if market accepts bets |
| POST | `/api/markets/:address/check-graduation` | Trigger manual graduation check |
| GET | `/api/markets/by-creator/:wallet` | Markets created by a wallet |

**`GET /api/markets` returns active/unresolved markets only.** No pagination — returns all active markets at once. No status filter. For historical data, use `/api/markets/resolved`.

**GET `/api/markets` Response — verified from source (`routes/markets.ts:119-147`):**
```json
{
  "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:**
```json
{
  "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:**
```json
{
  "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:**
```json
{ "canBet": false, "reason": "Market is already voided", "marketStatus": "voided", "tokenGraduated": true }
```

**`GET /api/markets/:address/bets` Response — verified live:**
```json
{
  "bets": [{
    "id": 3963,
    "betAddress": "5V9x...oZS",
    "user": "7oQJ...GmKT",
    "amount": 0.5,
    "prediction": "NO",
    "potentialPayout": 0,
    "claimed": false,
    "claimedAmount": null,
    "createdAt": "2026-02-21T18:59:42.249Z"
  }],
  "nextCursor": "2026-02-21T18:59:42.249Z"
}
```

### Users

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/users/:wallet/stats` | User statistics (**volume/profit in lamport strings, not SOL**) |
| GET | `/api/users/:wallet/bets` | Bet history (filterable) |
| GET | `/api/users/:wallet/bets/counts` | Bet counts by status |
| GET | `/api/users/:wallet/claimable` | Count of claimable bets |
| GET | `/api/users/:wallet/claimable/details` | **Claimable bets with estimated payouts** |
| GET | `/api/users/:wallet/terms` | Check terms acceptance |
| POST | `/api/users/:wallet/terms` | Accept terms (**requires wallet signature**) |
| GET | `/api/users/:wallet/terms/message` | Get terms message to sign |

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

| Param | Values | Default |
|-------|--------|---------|
| `status` | `active`, `won`, `lost`, `refunded`, `all` | `all` |
| `cursor` | string | — |
| `limit` | 1–100 | 20 |

**GET `/api/users/:wallet/stats` Response — verified live:**
```json
{
  "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:**
```json
{
  "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:**
```json
{ "active": 0, "won": 15, "lost": 12, "refunded": 11, "all": 38 }
```

**GET `/api/users/:wallet/claimable/details` Response:**
```json
{
  "wallet": "Ax7R...UZMg",
  "count": 3,
  "bets": [{
    "betAddress": "...",
    "marketAddress": "...",
    "amount": 0.5,
    "prediction": true,
    "outcome": true,
    "isRefund": false,
    "estimatedPayout": 0.85,
    "token": { "name": "...", "symbol": "...", "imageUrl": "..." }
  }],
  "totalEstimatedPayout": 2.55
}
```

### Bets

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/bets/:betAddress` | Single bet by on-chain address |
| POST | `/api/bets/sync` | Sync bet from chain to DB (**optional** — background service catches up) |
| POST | `/api/bets/sync-market` | Sync all bets for a market (**optional**) |
| POST | `/api/bets/sync-user` | Sync all bets for a user (**optional**) |
| POST | `/api/bets/claim/:betAddress` | Record on-chain claim in DB (**optional**) |

**There is no `GET /api/bets` list endpoint.** Use `/api/users/:wallet/bets` or `/api/markets/:address/bets`.

All sync/claim POST endpoints are **optional DB convenience calls**. Bets exist on-chain regardless. A background sync service runs continuously and will pick up unsynced bets within minutes. Calling sync immediately after a transaction makes the bet visible in the API/UI sooner.

**POST `/api/bets/sync`:** `{ "betAddress": "<on-chain bet PDA>" }` — returns 400 if betAddress missing
**POST `/api/bets/sync-market`:** `{ "marketAddress": "...", "userWallet": "<optional>" }`
**POST `/api/bets/sync-user`:** `{ "userWallet": "...", "fullSync": false }` — returns `{ "success": true, "synced": N, "skipped": N, "total": N }`
**POST `/api/bets/claim/:betAddress`:** `{ "signature": "<tx sig>", "amount": <lamports>, "userWallet": "..." }`

**GET `/api/bets/:betAddress` Response — verified live:**
```json
{
  "address": "5V9x...oZS",
  "market": "AJAi...4kDZ",
  "user": "7oQJ...GmKT",
  "amount": 0.5,
  "prediction": false,
  "claimed": false,
  "claimedAmount": null,
  "payout": null,
  "marketResolved": true,
  "marketVoided": false,
  "marketOutcome": true,
  "token": { "name": "WASM AI", "symbol": "WASM", "image": "https://..." }
}
```

### Prices

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/prices/sol` | SOL/USD price |
| GET | `/api/prices/:mint` | Cached token price |
| POST | `/api/prices/live` | Batch live prices (max 50 mints) |

**POST `/api/prices/live` Body:**
```json
{ "mints": ["mint1", "mint2", "mint3"] }
```
Max 50 mints per request. Response:
```json
{
  "prices": [{ "mint": "...", "priceUsd": 0.001, "marketCapUsd": 50000, "bondingProgress": 45, "timestamp": "..." }],
  "cached": 2,
  "fetched": 1
}
```

### Leaderboard

| Method | Endpoint | Description |
|--------|----------|-------------|
| GET | `/api/leaderboard` | Ranked users |
| GET | `/api/leaderboard/top` | Top 5 across all categories |
| GET | `/api/leaderboard/categories` | Available categories and time ranges |

**GET `/api/leaderboard` Query Parameters:**

| Param | Values | Default |
|-------|--------|---------|
| `category` | `profit`, `winrate`, `volume`, `creators` | `profit` |
| `range` | `24h`, `7d`, `30d`, `all` | `all` |
| `wallet` | wallet address | — |
| `limit` | 1–500 | 100 |

---

## 5. WebSocket Reference

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

| Path | URL | Purpose |
|------|-----|---------|
| `/prices` | `wss://pumpbet-mainnet.up.railway.app/prices` | Real-time token price updates |
| `/live-trades` | `wss://pumpbet-mainnet.up.railway.app/live-trades` | Live $1000+ pump.fun trades |
| `/user-events` | `wss://pumpbet-mainnet.up.railway.app/user-events` | User-specific bet/market notifications |
| `/activity` | `wss://pumpbet-mainnet.up.railway.app/activity` | Platform-wide activity stream |

### Ping/Pong (All Endpoints)

Server sends `{ "type": "ping" }` every 30 seconds. You **must** respond with `{ "type": "pong" }` or the connection will be dropped. Use text-frame JSON pings — Railway's proxy does not support binary WebSocket ping frames.

### Reconnection

Use exponential backoff: base 3s delay, 1.5x multiplier, max 10 attempts.

> **Stateless agents:** If your agent cannot maintain persistent connections between turns, prefer polling `GET /api/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:**
```json
{ "type": "subscribe", "mints": ["mint1", "mint2"] }
```

**Subscribe to all updates:**
```json
{ "type": "subscribe_all" }
```

**Unsubscribe:**
```json
{ "type": "unsubscribe", "mints": ["mint1"] }
```

If `subscribedMints` is empty (no subscribe message sent), the client receives ALL price updates.

**Incoming price update:**
```json
{ "mint": "...", "priceUsd": 0.001, "marketCapUsd": 50000, "bondingProgress": 45.2, "timestamp": "..." }
```

**Incoming graduation event:**
```json
{ "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:**
```json
{
  "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):**
```json
{ "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:**
```json
{ "type": "subscribe_market", "marketAddress": "..." }
```

**Unsubscribe:**
```json
{ "type": "unsubscribe_market", "marketAddress": "..." }
```

**Incoming event types:** `BET_UPDATE`, `MARKET_UPDATE`, `NOTIFICATION`, `SYSTEM`

### Minimal WebSocket Example

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
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

```typescript
// 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)

```typescript
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:

```typescript
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](#create-market))

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:**
   ```typescript
   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](#place-bet))

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](#claim-payout))

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](#claim-refund-voided-market))

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 `deadlineS

…(truncated)
