pnp-core
v0.1.0
Published
Lean TypeScript SDK for the PnP prediction-market protocol on Solana (P2P/parimutuel + Pythagorean AMM).
Maintainers
Readme
pnp-sdk-core
A small, friendly TypeScript SDK for the PnP prediction-market protocol on Solana.
Every function is named for what it does, and every input/output is a plain object with labelled fields — so you can read a call and know exactly what's happening without digging into Solana internals.
- P2P / parimutuel markets (V3) —
client.p2p.* - Pythagorean / AMM markets (V2) —
client.amm.*
One program, live on mainnet and devnet: 8PyE2dizL52ga7ytqLtqRyjwWp4yXEx8M5Z4BAHgHuTb.
Install
npm install pnp-sdk-coreConnect
import { PnpClient } from "pnp-sdk-core";
import { Keypair } from "@solana/web3.js";
// Read-only (reads + address lookups, no signing):
const reader = PnpClient.mainnet();
// With a signer — pass a Keypair (Node/scripts) or any browser wallet:
const client = PnpClient.devnet({ wallet: myKeypair, rpcUrl: process.env.RPC_URL });A note on amounts
All token amounts are in base units (the smallest unit of the collateral, like cents for a
dollar). USDC has 6 decimals, so 100 USDC = 100_000000. Use the helper so you don't count zeros:
import { uiToBaseUnits } from "pnp-sdk-core";
uiToBaseUnits(100, 6); // 100_000000n (100 USDC)The whole lifecycle in one glance
import { PnpClient, uiToBaseUnits } from "pnp-sdk-core";
// 1. A creator opens a market and bets YES with 100 USDC
const creator = PnpClient.devnet({ wallet: creatorKp });
const { market } = await creator.p2p.createMarket({
question: "Will ETH close above $5,000 on 2027-01-01?",
initialAmount: uiToBaseUnits(100, 6),
side: "yes",
endTime: new Date("2027-01-01T00:00:00Z"),
});
// 2. Someone else bets NO with 50 USDC
const bettor = PnpClient.devnet({ wallet: bettorKp });
await bettor.p2p.buy({ market, side: "no", amount: uiToBaseUnits(50, 6) });
// 3. Anyone can read the live state
const info = await reader.p2p.getMarket(market);
console.log(info.status, info.impliedYesProbability);
// 4. After it ends, an authorized settler declares the winner
await creator.p2p.settle({ market, winner: "yes" });
// 5. Winners claim their payout
await bettor.p2p.redeem({ market });Function reference (client.p2p.*)
Each write function returns a signature (the transaction id) plus the key things it touched.
"Signer" is which wallet must be attached to the client.
Create & bet
createMarket({ question, initialAmount, side, endTime, ... }) — signer: creator
Opens a new market. You back one side with initialAmount of collateral; others take the other
side. Uses the protocol's default oracle (safe).
→ { signature, marketId, market, yesMint, noMint }
createMarketCustom({ ...same, oracle }) — signer: creator
Same as createMarket, but you pick the market's oracle. ⚠️ That wallet can settle the market —
only use it when you deliberately want a custom settler.
→ { signature, marketId, market, yesMint, noMint, extension }
buy({ market, side, amount, minTokensOut? }) — signer: bettor
Place a bet: spend amount collateral to get side tokens. minTokensOut is an optional slippage
floor (default 0 = accept any).
→ { signature, market, side }
Money out
creatorWithdraw({ market }) — signer: creator
Take back the part of your creator stake that no one ever bet against (unmatched collateral).
→ { signature, market }
redeem({ market }) — signer: winner
Claim your winnings after the market is settled. A 2% fee is taken (60% to the creator, 40% to the
protocol); you get the rest.
→ { signature, market }
Manage (authorized wallets)
settle({ market, winner }) — signer: oracle/admin
Declare the winning side ("yes" or "no"). Only an authorized settler can call it.
→ { signature, market, winner }
setEndTime({ market, endTime }) — signer: admin/oracle
Change when the market ends. endTime is a Date or unix seconds; can be in the past to make it
settleable now.
→ { signature, market, endTime }
updateCreator({ market, newCreator }) — signer: current creator
Hand the creator role (and future creator fees) to another wallet.
→ { signature, market, newCreator }
A market's numeric
marketid or its PDA address both work anywhere amarketis expected.
Settlement criteria (off-chain, no Solana tx)
setSettlementCriteria({ market, criteria, data? }) — signer: creator
Attach the rules for how a market resolves. Your wallet signs a message (not a transaction) and
it's sent to the PnP criteria server, which checks you're the market's creator and stores it for the
oracle. criteria is human-readable text; data is optional structured JSON for the oracle.
Needs a wallet that can sign messages (a Keypair, or a browser wallet with signMessage).
→ { ok, criteria: { market_address, cluster, creator, settlement_criteria, settlement_data, updated_at } }
await client.p2p.setSettlementCriteria({
market,
criteria: "Resolves YES if ETH >= $5,000 on 2027-01-01 (UTC) per CoinGecko.",
data: { source: "coingecko", asset: "ethereum", threshold: 5000 },
});getSettlementCriteria(market) — no signer
Read a market's stored criteria. Returns the record, or null if none has been set.
→ { market_address, cluster, creator, settlement_criteria, settlement_data, updated_at } | null
const c = await client.p2p.getSettlementCriteria(market);
if (c) console.log(c.settlement_criteria);Read (no signer needed)
getMarket(id | pubkey) → a P2PMarket snapshot:
| field | meaning |
|---|---|
| status | "open" · "ended" (awaiting settlement) · "resolved" |
| question | the market's question |
| creator, creatorSide | who made it and which side they back |
| winner | "yes" / "no" once resolved, else null |
| yesPot, noPot, totalPot | collateral staked on each side (base units) |
| impliedYesProbability | YES's share of the pool, 0..1 |
| endTime, creationTime | timestamps (unix seconds) |
| yesMint, noMint, collateralMint | token addresses |
| oddsBps, oracle | custom odds / oracle if set (else defaults) |
fetchGlobalConfig() → the protocol's global settings.
nextMarketId() → the id the next created market will get.
Pythagorean / AMM markets (client.amm.*)
A second market type: instead of two matched pots, an AMM issues YES/NO tokens off a
Pythagorean bonding curve (reserves R = √(YES² + NO²), so yesPrice² + noPrice² = 1).
createMarket({ question, initialLiquidity, endTime }) — signer: creator
Open an AMM market with 50/50 odds and the protocol oracle.
→ { signature, marketId, market }
createMarketCustom({ ...same, oddsBps?, oracle? }) — signer: creator
Set custom YES odds (oddsBps, 100–9900; default 5000 = 50/50) and/or a custom oracle ⚠️.
→ { signature, marketId, market, extension }
createCoinMarket({ question, coinAddress, chain?, initialLiquidity, endTime, oddsBps?, oracle? })
A market about a specific token. Stores the type in the question as "<question>? <chain>:<coinAddress>"
(default chain "solana"), exactly like the PnP frontend so the oracle/indexer recognize it.
createYoutubeMarket({ question, youtubeLink, initialLiquidity, endTime, oddsBps?, oracle? })
A market about a YouTube video. Stores the type as "<question>? <youtubeLink>".
await client.amm.createCoinMarket({
question: "Will BONK 2x by Friday",
coinAddress: "DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263", // chain defaults to "solana"
initialLiquidity: uiToBaseUnits(50, 6),
endTime: new Date("2027-01-01"),
});
// on-chain question: "Will BONK 2x by Friday? solana:DezXAZ8z7Pnr…"readPrice(market) → { yesPrice, noPrice } — each in [0,1] (matches the on-chain price getter).
readMultiplier(market) → { yesMultiplier, noMultiplier } — potential payout multiples
(1 + (otherSupply / thisSupply)²).
getMarket(market) → full AmmMarket (status, reserves, supplies, mints, prices, multipliers).
fetchGlobalConfig() · nextMarketId() — shared with P2P.
const reader = PnpClient.mainnet();
const { yesPrice, noPrice } = await reader.amm.readPrice(9);
const { yesMultiplier, noMultiplier } = await reader.amm.readMultiplier(9);Fresh AMM markets read
0.5 / 0.5and2× / 2×until the first trade mints decision tokens.
Oracle status (client.oracle.*)
After a market is created, your oracle's AI decides whether it can be objectively resolved and
generates settlement criteria. These reads (via the PnP indexer — no wallet needed) let you watch
that. This is the ORACLE's decision — distinct from the creator-set criteria on client.p2p.*.
getResolvableStatus(market) → "checking" | "resolvable" | "unresolvable"
checking = the AI is still processing; the others are its verdict. market is a PDA or base58 address.
getSettlementCriteria(market) → the AI's { resolvable, criteria, reasoning?, category? } or null
(null while still checking).
waitForResolvable(market, { intervalMs?, timeoutMs? }) — the feedback loop: polls until the AI
finishes, then returns { status: "resolvable" | "unresolvable", criteria } (throws on timeout).
const { market } = await client.amm.createMarket({ /* … */ });
const { status, criteria } = await client.oracle.waitForResolvable(market);
if (status === "resolvable") console.log("live!", criteria);
else console.log("oracle rejected — refunds enabled");Wallets
- Node / scripts: pass a
Keypairaswallet. - Browser: pass your wallet-adapter wallet (anything with
publicKey+signTransaction). - No wallet: reads and address derivations still work; write functions throw a clear error.
What's intentionally not here
Whitelist-gate management (init/add-taker/close gate) is owned by the frontend/admin, not the
SDK. buy still works on gated markets — it passes the gate automatically.
See PLAN.md for build status and what's next.
