gum-sdk
v0.2.0
Published
TypeScript SDK for the Bubblegum prediction-market token launchpad on Solana (bonding-curve $TICKER + virtual-liquidity YES/NO CPMM).
Maintainers
Readme
gum-sdk
TypeScript SDK for the Bubblegum prediction-market token launchpad on Solana —
a two-axis market: a bonding-curve $TICKER token feeding a virtual-liquidity
YES/NO decision-token CPMM.
It wraps two Anchor programs:
| Program | mainnet-beta | devnet |
|---|---|---|
| bubblegum_curve | 71ywu6cFWETLyiz1KcuMwq2wfguYfra7b1bCPinVqKm3 | En6VRZMMqRcQ12squMf8wt5qqbKhcuaLKANxb5SUvxqN |
| bubblegum_cpmm | Dyw8vChGKLMWkkuHZBKS8EdLj489dcVxgHyUWBuCVp3Y | 7g1LRg6NWDmuGaebmP42gq5CyeysteRRr5ZtZ42rJ6eq |
Program IDs are read from each bundled IDL's
addressfield — the single source of truth. The SDK never fetches an IDL at runtime and never hardcodes a program ID separately from its IDL.
📖 Full developer guide with runnable scripts for every flow → DOCS.md (launch · buy/sell · buy-with-$TICKER · redeem-to-$TICKER · claim prize · settle · refund).
Status
- ✅ Phase 1 — read path: network config, PDA derivation, typed account
fetch/decode (
bigintboundary), pure quote math, event parsing/streaming, typed errors. - ✅ Phase 2 — write path: instruction builders for all 31 instructions
(18 curve + 13 cpmm) with automatic account/ATA resolution, composite
curve→CPMM quotes, and high-level flows (
launchMarket,buy,sell,swap,buyDirect/sellDirect,settle,redeemAndClaim,refund,graduate). Builders return{ instructions, signers }— the SDK never signs or sends. - 🚧 Next: devnet e2e + math/PDA parity test suites; optional Meteora graduation pipeline.
Install
npm install gum-sdk @coral-xyz/anchor @solana/web3.js@coral-xyz/anchor (>=0.31 <0.33) and @solana/web3.js (^1.95) are peer
dependencies.
Quickest start — just an RPC URL + a private key
No Connection, no Keypair, no wiring. Pass an RPC endpoint and your wallet
private key; then call one-liners that take human units (SOL, tokens, days)
and build + sign + send + confirm for you:
import { Bubblegum } from "gum-sdk";
const gum = Bubblegum.connect({
rpc: process.env.RPC_URL!, // any Solana RPC endpoint
privateKey: process.env.PRIVATE_KEY!, // base58 (Phantom/CLI export) or a "[..]" byte array
// network is inferred from the URL ("…devnet…" → devnet); pass network: "devnet" to force it
});
// launch a coin
const { conditionId, conditionIdHex, tickerMint } = await gum.launch({
question: "Will SOL close above $250 this year?",
name: "SOL 250", ticker: "SOL250",
metadataUri: "https://.../meta.json",
durationDays: 7, // or endTime: Date | unix-seconds bigint
// creator: somePubkey, // optional fee recipient (devnet); omit ⇒ you are the creator
});
await gum.buy(conditionId, "yes", 0.5); // buy YES with 0.5 SOL
await gum.sell(conditionId, "yes", 1000); // sell 1000 YES tokens
await gum.swap(conditionId, "yes", 250); // swap 250 YES → NO
const odds = await gum.odds(conditionId); // { yes, no } in [0,1]
// settlement lifecycle
await gum.settle(conditionId, "yes"); // signer must be the cpmm admin
await gum.redeem(conditionId); // winner: single-exit (claim bonus XOR redeem)
// await gum.refund(conditionId); // if it expired un-graduated (market-value unwind)
// cranker / admin
// await gum.finalizeBonus(conditionId); // graduator/admin: snapshot the bonus pool
// await gum.sweepBonus(conditionId); // admin: recover leftover bonus after all winners exitconditionId accepts a hex string or a Buffer; every call returns { signature, … }
(plus quote/expectedOut for trades). Reads are on gum.client (accounts, pdas,
quotes). Tunables: Bubblegum.connect({ …, slippageBps, priorityMicroLamports,
commitment, network }) and per-trade { slippageBps, deadlineSecs }.
Browser / wallet-adapter: instead of a private key, use the lower-level client
with any { publicKey, signTransaction } wallet:
BubblegumClient.mainnet(connection).withWallet(wallet). Drop to the builder / flow
layers when you need to batch instructions or a custom send strategy
(return-instructions-only).
Token metadata — where metadataUri goes
metadataUri is a URL to a JSON file you host — the SDK/program never uploads
anything. At launch the program stores that URI in the $TICKER mint's
Token-2022 metadata extension (on-chain); wallets / explorers / your UI fetch
the JSON to show the name, symbol, and image. Keep it ≤ 200 chars.
Host the JSON (and the image it points at) anywhere public: IPFS
(Pinata / nft.storage / web3.storage), Arweave, or your own CDN / S3 / R2 — then
pass the resulting https://… (or ipfs://…) URL.
// metadata.json (Metaplex fungible-token shape)
{
"name": "BTC 100k",
"symbol": "BTC100",
"description": "Will BTC close above $100k this year?",
"image": "https://your-cdn/btc100.png"
}Typical flow: upload the image → get its URL → put it in metadata.json → upload
that → pass its URL as metadataUri. (The Bubblegum frontend automates this via its
own uploader/R2; a standalone SDK user brings their own hosting.)
Networks
devnet and mainnet run the same program version; only the program IDs differ, so every PDA (curve, market, mints, vaults, config) resolves to a different address per network. The SDK derives all of them from the chosen network's bundled IDL — pick a network at construction and everything follows:
BubblegumClient.mainnet(conn); // mainnet-beta
BubblegumClient.devnet(conn); // devnet
BubblegumClient.forNetwork("devnet", conn); // env-driven switchWatch for per-market / per-network values that are NOT constants: graduation
threshold (devnet 10 SOL vs mainnet 75 SOL — read BondingCurve.graduationLamports),
permissioned (devnet false, mainnet true), and the graduator key.
Quick start
import { Connection } from "@solana/web3.js";
import { BubblegumClient } from "gum-sdk";
// mainnet needs a paid RPC (Helius / Triton / QuickNode); public RPC rate-limits.
const conn = new Connection(process.env.RPC_URL!, "confirmed");
const sdk = BubblegumClient.mainnet(conn);
// Read protocol config
const cfg = await sdk.accounts.curveGlobalConfig();
console.log(cfg.graduator.toBase58(), cfg.permissioned, cfg.defaultGraduationLamports);
// Derive every PDA for a market from its 32-byte condition_id
const pdas = sdk.pdas.forMarket(conditionId);
// Read a market and preview a trade (pure math — no RPC)
const { curve, market } = await sdk.accounts.marketView(conditionId);
const { tokensOut } = sdk.quote.buyExactIn({
virtualSol: curve.virtualSol,
virtualTokens: curve.virtualTokens,
solIn: 1_000_000_000n, // 1 SOL (net of fee)
tokensRemainingReal: /* CURVE_SELLABLE_TOKENS - curve.realTokensSold */ 0n,
});Write path
Builders take intent and resolve every account, ATA, and fee vault. They return instructions; you sign and send.
import { BubblegumClient, buildTransaction } from "gum-sdk";
const sdk = BubblegumClient.mainnet(conn);
// Launch a market (prepareMarket + activateMarket in one atomic tx)
const { conditionId, instructions } = await sdk.flows.launchMarket({
payer: wallet.publicKey, // tx signer; condition_id binds to the payer
// creator: someOtherPubkey, // optional fee recipient (devnet decouple); omit ⇒ payer is the creator
question: "Will it rain in NYC on July 4?",
name: "NYC Rain", ticker: "RAIN",
metadataUri: "https://.../metadata.json",
endTime: BigInt(Math.floor(Date.now() / 1000) + 7 * 86_400),
});
// Buy YES with 0.5 SOL — quote + minOut + ATA setup handled for you
const { instructions: buyIxs, quote } = await sdk.flows.buy({
conditionId, user: wallet.publicKey, side: "yes",
solIn: 500_000_000n, slippageBps: 50,
});
console.log(`expect ~${quote.sideTokensOut} YES (min ${quote.minSideOut})`);
const tx = await buildTransaction({ connection: conn, feePayer: wallet.publicKey, instructions: buyIxs });
// ... wallet.signTransaction(tx) and send.
// Winner payout (claimWinnerBonus before redeemPosition, in one tx)
const { instructions: redeemIxs } = await sdk.flows.redeemAndClaim({ conditionId, user: wallet.publicKey });
// Low-level single builders are on sdk.curve.* / sdk.cpmm.*
const settle = await sdk.cpmm.settleMarket({ conditionId, authority, winningSide: "yes" });Subpath exports (tree-shakeable)
import { quoteBuyExactIn, TICKER_SCALE } from "gum-sdk/math"; // pure, no Anchor (~1 KB)
import { computeConditionId, BubblegumPdas } from "gum-sdk/pdas"; // derivation onlyLayout
src/
config.ts networks → program IDs (from IDL), config PDAs, external IDs
client.ts BubblegumClient facade (pdas/accounts/events/quote + curve/cpmm/flows)
idl/ bundled live IDLs (devnet + mainnet)
pdas.ts seeds, condition_id, per-market PDA derivation [pure]
math/ curve + cpmm + composite curve→cpmm quotes [pure]
accounts/ typed fetch + decode (bigint, string enums)
events.ts Anchor log parse + WS subscribe
errors.ts CurveErrors / CpmmErrors, isCurveError, decodeError [pure]
instructions/ curve (18) + cpmm (13) builders + account resolver
flows/ high-level launch / trade / settle / redeem flows
tx/ optional compute-budget + transaction-assembly helpers
util.ts nonce, deadline, unit conversions, launch validationNotes for integrators
- Mints are Token-2022. Token name/symbol/uri live on the mint's metadata extension, not a Metaplex PDA.
- Graduation threshold is per-market — read
BondingCurve.graduationLamports, don't assume 75 SOL (devnet default is 10 SOL). - Pre-graduation trading routes through the curve (
buy_decision/sell_decision), which CPIs the CPMM. Direct CPMM trading is blocked until the curve reaches its graduation threshold. - Winner payout is single-exit. Once the bonus is finalized,
redeemAndClaim(andgum.redeem) callsclaimWinnerBonus, which CPIsredeem_positioninternally (burns all winning tokens + pays reserve + bonus) — it never also callsredeemPositionfor the same user. Before finalization it just redeems. - Refunds unwind at true market value. An expired, non-graduated market
refunds via cpmm
burn_for_refund→ curve sell math (not a naive pro-rata split), so cheap-side tokens can't drain honest holders' principal.
License
MIT
