# Pumpfun Recipes

End-to-end snippets for the most common Pump.fun bots and tools. Each
recipe starts with the **Build Brief** so you can sanity-check the API
choice before coding.

---

## 1. Sniper bot — react to new launches, buy in ⚡ Lightning

**Brief:** Bot service on a VPS. Server-side keys are fine. One wallet per
user (Lightning). Need lowest latency from "new token event" to "buy
signed".

```javascript
import WebSocket from 'ws';

const API_KEY = process.env.PUMPDEV_API_KEY;
const ws = new WebSocket('wss://pumpdev.io/ws');

ws.on('open', () => {
  ws.send(JSON.stringify({ method: 'subscribeNewToken' }));
});

ws.on('message', async (raw) => {
  const e = JSON.parse(raw);
  if (e.txType !== 'create') return;

  // Cheap filter — keep this synchronous and fast.
  if (!/pump$/i.test(e.symbol)) return;

  const r = await fetch(
    `https://pumpdev.io/api/trade-lightning?api-key=${API_KEY}`,
    {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        action: 'buy',
        mint: e.mint,
        amount: 0.05,
        denominatedInSol: true,
      }),
    },
  );
  const { signature, error } = await r.json();
  console.log('snipe', e.mint, signature || error);
});
```

**Hardening:**
- Reconnect logic with backoff.
- A per-mint dedupe cache (bots can see the same create twice).
- A daily/weekly SOL spend cap.

---

## 1b. Python sniper / launch filter — create events with dev buy > 2 SOL

**Brief:** Python bot service. Stream new launches over WebSocket, recover
dev-buy SOL from `initialBuy`, and only react when the launch clears a
minimum threshold.

```python
import asyncio
import json
import os

import aiohttp
import websockets

WS_URL = "wss://pumpdev.io/ws"
API_KEY = os.environ["PUMPDEV_API_KEY"]

INITIAL_V_SOL = 30.0
INITIAL_V_TOKENS = 1_073_000_000.0
K = INITIAL_V_SOL * INITIAL_V_TOKENS
MIN_DEV_BUY_SOL = 2.0


def dev_buy_sol(initial_buy_tokens: float) -> float:
    return K / (INITIAL_V_TOKENS - initial_buy_tokens) - INITIAL_V_SOL


async def snipe(session: aiohttp.ClientSession, mint: str) -> None:
    async with session.post(
        f"https://pumpdev.io/api/trade-lightning?api-key={API_KEY}",
        json={
            "action": "buy",
            "mint": mint,
            "amount": 0.05,
            "denominatedInSol": True,
        },
    ) as resp:
        data = await resp.json()
        print("snipe", mint, data.get("signature") or data.get("error"))


async def main() -> None:
    seen_mints: set[str] = set()

    async with aiohttp.ClientSession() as session:
        async for ws in websockets.connect(WS_URL, ping_interval=20, ping_timeout=20):
            try:
                await ws.send(json.dumps({"method": "subscribeNewToken"}))

                async for raw in ws:
                    event = json.loads(raw)
                    if event.get("txType") != "create":
                        continue

                    mint = event["mint"]
                    if mint in seen_mints:
                        continue
                    seen_mints.add(mint)

                    initial_buy = float(event.get("initialBuy") or 0)
                    dev_sol = dev_buy_sol(initial_buy)
                    if dev_sol <= MIN_DEV_BUY_SOL:
                        continue

                    print(
                        "launch",
                        mint,
                        event.get("symbol"),
                        f"dev_buy={dev_sol:.4f} SOL",
                    )
                    await snipe(session, mint)
            except websockets.ConnectionClosed:
                await asyncio.sleep(1)
                continue


if __name__ == "__main__":
    asyncio.run(main())
```

Notes:
- Install deps with `pip install websockets aiohttp`.
- Use `initialBuy`, not `solAmount`, for create-event dev-buy filtering.
- The create event exposes the bonding curve **before** the dev buy is
  applied, so `vSolInBondingCurve - 30` will not work.

---

## 2. Copy trader — mirror a whale's trades ⚡ Lightning

```javascript
ws.send(JSON.stringify({
  method: 'subscribeAccountTrade',
  keys: ['WhaleWalletPubkey'],
}));

ws.on('message', async (raw) => {
  const e = JSON.parse(raw);
  if (e.txType !== 'buy' && e.txType !== 'sell') return;

  await fetch(`https://pumpdev.io/api/trade-lightning?api-key=${API_KEY}`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      action: e.txType,
      mint:   e.mint,
      amount: Math.min(e.solAmount, 0.1),  // cap exposure
      denominatedInSol: true,
    }),
  });
});
```

---

## 3. Self-custody trade button — 🔐 Local (browser, Phantom)

**Brief:** Web app. Keys must stay in the user's wallet. Phantom signs the
unsigned tx we receive.

```javascript
import { VersionedTransaction, Connection } from '@solana/web3.js';

async function buy(mint, solAmount) {
  const pubkey = window.solana.publicKey.toBase58();

  const r = await fetch('https://pumpdev.io/api/trade-local', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      publicKey: pubkey,
      action: 'buy',
      mint,
      amount: solAmount,
      denominatedInSol: 'true',
    }),
  });

  const tx = VersionedTransaction.deserialize(new Uint8Array(await r.arrayBuffer()));
  const { signature } = await window.solana.signAndSendTransaction(tx);
  return signature;
}
```

---

## 4. One-call launcher with dev buy — ⚡ Lightning create

```javascript
const r = await fetch(
  `https://pumpdev.io/api/create-lightning?api-key=${API_KEY}`,
  {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      name: 'Pumpfun Demo',
      symbol: 'PDEMO',
      image: 'https://example.com/logo.png',
      twitter: 'https://x.com/PumpDevIO',
      website: 'https://pumpdev.io',
      buyAmountSol: 0.5,        // dev buys 0.5 SOL of own token in same tx
      // mintKeypair: 'base58...',   // vanity mint ending in "pump"
    }),
  },
);
const { signature, mint, metadataUri } = await r.json();
```

---

## 5. Atomic multi-wallet launch — 📦 bundle-lightning (Jito)

```javascript
await fetch('https://pumpdev.io/api/bundle-lightning', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    jitoTip: 0.01,
    accounts: [
      { apiKey: K_DEV,  type: 'create',
        name: 'Bundle Demo', symbol: 'BNDL',
        image: 'https://example.com/logo.png' },
      { apiKey: K_BUY1, type: 'buy', amount: 0.5, denominatedInSol: true },
      { apiKey: K_BUY2, type: 'buy', amount: 0.3, denominatedInSol: true },
      { apiKey: K_BUY3, type: 'buy', amount: 0.2, denominatedInSol: true },
    ],
  }),
});
```

---

## 6. Fee auto-claimer — cron, local-sign

```javascript
import { Connection, Keypair, VersionedTransaction } from '@solana/web3.js';
import bs58 from 'bs58';

const API_URL = 'https://pumpdev.io';
const RPC_URL = process.env.RPC_URL;
const keypair = Keypair.fromSecretKey(bs58.decode(process.env.SOL_SECRET));
const connection = new Connection(RPC_URL, 'confirmed');

async function buildSignSendBinary(url, body) {
  const r = await fetch(url, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify(body),
  });
  if (!r.ok) throw new Error(JSON.stringify(await r.json()));

  const tx = VersionedTransaction.deserialize(new Uint8Array(await r.arrayBuffer()));
  tx.sign([keypair]);
  const sig = await connection.sendTransaction(tx, { maxRetries: 3 });
  await connection.confirmTransaction(sig, 'confirmed');
  return sig;
}

// Claim creator fees for a specific mint.
// `publicKey` is the fee payer AND the only required signer — always derive it
// from the signing keypair, or the RPC rejects the tx for a bad signature.
// The wallet needs ~0.01 SOL for fees and commission, even for USDC-quoted fees.
const claimSig = await buildSignSendBinary(`${API_URL}/api/claim-account`, {
  publicKey: keypair.publicKey.toBase58(),
  mint: 'TokenMintAddress',       // pass mint whenever possible
});
console.log('claimed', claimSig);

// If this 500s with "Could not resolve quote mint for <mint>", retry once, then
// verify the mint, then check GET /api/claim-account for where the fees are.
// Dropping `mint` builds a wrapped-SOL creator-vault sweep that succeeds but
// silently skips graduated PumpSwap, non-SOL quote, and fee-sharing balances —
// a fallback for diagnosis, not a fix. Raising priorityFee never helps here.

// Fee-sharing coins: distribute to all configured shareholders.
const distributeSig = await buildSignSendBinary(`${API_URL}/api/claim-distribute`, {
  publicKey: keypair.publicKey.toBase58(), // payer; need not be a shareholder
  mint: 'FeeSharingTokenMint',
});
console.log('distributed', distributeSig);
```

For a read-only preview before building a transaction:

```javascript
const preview = await fetch(
  `${API_URL}/api/claim-account?publicKey=${keypair.publicKey.toBase58()}&mint=TokenMintAddress`,
);
console.log(await preview.json());
```

Batch claims for a curated mint list return base58 unsigned transactions:

```javascript
const batch = await fetch(`${API_URL}/api/claim-all`, {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    publicKey: keypair.publicKey.toBase58(),
    mints: ['Mint1', 'Mint2'],
  }),
});
const { transactions } = await batch.json();
```

---

## 7. Live analytics feed — subscribeTokenTrade

The token trade stream follows Pump.fun tokens across bonding-curve trades and canonical PumpSwap pool trades. Non-canonical PumpSwap pools and other platforms are ignored by default.

```javascript
ws.send(JSON.stringify({
  method: 'subscribeTokenTrade',
  keys: trackedMints,           // array of mint addresses
}));

ws.on('message', (raw) => {
  const e = JSON.parse(raw);
  db.insertTrade({
    mint: e.mint,
    side: e.txType,
    sol:  e.solAmount,
    tokens: e.tokenAmount,
    trader: e.traderPublicKey,
    ts: e.timestamp,
  });
});
```

---

## 8. PONS sniper — 🟣 Robinhood Chain, local signing

EVM, so `ethers` rather than `@solana/web3.js`, and a different host. Launches
stream free; the trade endpoint needs no key at all. The nonce is managed
locally because the chain has no mempool and orders by arrival — without that,
every build reads the same pending count and only one buy lands.

```javascript
import WebSocket from 'ws';
import { JsonRpcProvider, Wallet } from 'ethers';

const API = 'https://rhc.pumpdev.io';
const provider = new JsonRpcProvider(process.env.RHC_RPC_URL || 'https://rpc.mainnet.chain.robinhood.com', 4663, { staticNetwork: true });
const wallet = new Wallet(process.env.RHC_PRIVATE_KEY, provider);

let nonce = await provider.getTransactionCount(wallet.address, 'pending');
const seen = new Set();

const ws = new WebSocket('wss://rhc.pumpdev.io/ws');   // launches are free, no key
ws.on('open', () => ws.send(JSON.stringify({ method: 'subscribeNewToken' })));

ws.on('message', async (raw) => {
  const e = JSON.parse(raw);
  if (e.status !== 'pending') return;        // 'pending' is the head start; 'confirmed' repeats it
  if (seen.has(e.txHash)) return;            // deduplicate by txHash, not by token
  seen.add(e.txHash);

  const res = await fetch(`${API}/api/trade-local`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({
      publicKey: wallet.address,
      action: 'buy',
      token: e.mint,                 // the 0x address of the new token
      amount: 0.02,                  // 0.02 of the pair's quote asset
      denominatedInQuote: 'true',
      slippage: 25,                  // a token seconds old moves fast
      maxSnipeTaxBps: 700,           // accept up to 7% launch tax; 99% first second still refused (425)
      nonce: nonce++                 // number them yourself
    })
  });

  if (res.status === 425) {                  // snipe tax above slippage: 99% in the launch second
    const { snipeTaxBps, retryAfterMs } = await res.json();
    return console.log(`snipe tax ${snipeTaxBps} bps — retry in ${retryAfterMs}ms`);
  }
  if (!res.ok) return console.error('build failed:', (await res.json()).error);

  const { transaction, quote } = await res.json();
  console.log(`buying ${e.mint} — floor ${quote.amountOutMin}, snipe tax ${quote.snipeTaxBps} bps`);
  wallet.sendTransaction(transaction).catch((err) => console.error(err.message));
});
```

Selling is the same call with `action: 'sell'` and `amount: '100%'`, plus one
extra step the first time per token: if the response carries
`approvalTransaction`, send it, wait for it, then **build again** so the gas
limit is measured rather than the fallback.

Watch out for `priorityFee` — it is **gwei per gas** here, default `0.05`, and
values above 100 are refused. Passing a Solana-sized number is hundreds of ETH
in tip, paid on inclusion and unrecoverable. A bigger tip buys nothing anyway:
the sequencer orders by arrival, not by fee.

---

## Checklists

**Before shipping:**
- [ ] API key stored in env, never bundled into a browser build.
- [ ] Server defaults used for slippage and priorityFee (override only when explicitly needed).
- [ ] Signature treated as "submitted", not "confirmed" — confirm via WS
      or `getSignatureStatuses`.
- [ ] WebSocket reconnect with backoff + re-subscribe on open.
- [ ] Create-event dev-buy filters use `initialBuy` + bonding-curve inversion,
      not `solAmount`.
- [ ] Claim/transfer endpoints treated as unsigned local-sign flows, not
      Lightning API-key flows.
- [ ] Per-wallet spend caps.
- [ ] Logging captures `signature`, `mint`, `publicKey`, and `error` for
      every call.

**On PONS / Robinhood Chain, additionally:**
- [ ] `priorityFee` understood as gwei per gas, not a total in ETH.
- [ ] Own nonce tracked when firing trades back to back.
- [ ] Sells rebuilt after `approvalTransaction` confirms, so `gasEstimated` is `true`.
- [ ] `quote.priceAgeMs` checked before trusting `expectedOut`; slippage widened
      or `amountOutMin` set by hand when the price is stale.
- [ ] Launch events deduplicated by `txHash` across `pending` and `confirmed`.
- [ ] Amounts read as decimal strings in base units — never parsed as JS numbers.
