@vercora-protocol/sdk
v0.8.0
Published
TypeScript SDK for Vercora Protocol on Solana — client for the outcome-markets Anchor program (issuer utility infra; more product lanes on the roadmap).
Downloads
946
Readme
@vercora-protocol/sdk
TypeScript SDK for Vercora Protocol on Solana (Anchor). Vercora is an outcome markets protocol with a public platform + creator agency narrative; this package documents only the outcome markets program today (PredictionMarketClient, bundled IDL). Other protocol surfaces are not exposed in this SDK until they are documented in this package.
Install
npm install @vercora-protocol/sdk
# or
yarn add @vercora-protocol/sdkPeer dependencies (install alongside, keep versions aligned; see IDL, account metas, and peer deps for SendTransactionError notes):
npm install @coral-xyz/anchor @solana/web3.js @solana/spl-token bn.js
# or
yarn add @coral-xyz/anchor @solana/web3.js @solana/spl-token bn.jsQuick start
import * as anchor from "@coral-xyz/anchor";
import { Connection, clusterApiUrl } from "@solana/web3.js";
import { IDL, PROGRAM_ID, PredictionMarketClient } from "@vercora-protocol/sdk";
import type { Vercora } from "@vercora-protocol/sdk";
const connection = new Connection(clusterApiUrl("devnet"));
const provider = new anchor.AnchorProvider(connection, wallet, {});
anchor.setProvider(provider);
// Bundled IDL, program id is embedded in `IDL.address` (`PROGRAM_ID` matches it)
const program = new anchor.Program<Vercora>(IDL, provider);
const client = new PredictionMarketClient(program);Most transaction methods use provider.wallet.publicKey as the signer (user, authority, creator, etc.). Pass a wallet that can sign; read-only flows can use a read-only provider for fetch helpers only.
Package exports
| Export | Description |
| ------------------------ | -------------------------------------------------------------------------------------------- |
| PredictionMarketClient | High-level program wrapper (see below). |
| IDL, PROGRAM_ID | Anchor IDL JSON and PublicKey for the deployed program. |
| pda helpers | deriveMarket, deriveVault, deriveParimutuelState, … (see PDA helpers). |
| types | Params and account shapes (CreateMarketParams, ListedMarket, …). |
| marketUi | Pure UI helpers (getMarketLifecycleStatus, formatTimeLeft, …). |
| Vercora (type) | Anchor Program generic for Program<Vercora>. |
| agents/skill.md | Short AI agent entrypoint (production: https://vercora.xyz/agents/skill.md). Human UI with the same resolution narrative: https://vercora.xyz/docs/agents. |
IDL, account metas, and peer deps (browser + raw Anchor)
These topics come up often in integrator issues; the published IDL in this package (dist/idl/vercora.json, same bytes as IDL export) is generated from the same Anchor build as the on-chain program we ship against.
platform_treasury_wallet writable flag (SOL fee CPIs)
For claim_resolver_stake, resolve_refute, and upsert_user_profile, the program may transfer platform_fee_lamports (lamports) from the signer to platform_treasury_wallet before SPL CPIs (profile upsert has no SPL CPIs, but uses the same SOL fee path). The treasury wallet account must therefore appear as writable in the outer transaction’s AccountMetas. If your IDL marks it read-only, the runtime can raise PrivilegeEscalation / “writable privilege escalated” on that pubkey.
Mitigation: Use the IDL bundled with the same @vercora-protocol/sdk version you installed (or regenerate from the program source). Do not fork the JSON IDL with weaker writable flags for those instructions.
Served IDL vs package IDL (drift and disputerCollateral not provided)
If the app loads IDL from fetchIdl(), a static public/idl/vercora.json, or a CDN URL, it can drift from node_modules/@vercora-protocol/sdk/dist/idl/vercora.json. Anchor then builds wrong or incomplete account lists while PredictionMarketClient assumes the bundled layout, e.g. Account 'disputerCollateral' not provided when metas omit accounts present in the current IDL.
Recommendation: Prefer import { IDL } from '@vercora-protocol/sdk' (or copy dist/idl/vercora.json from the exact npm version) so the Program client and high-level client agree. If you must hot-reload IDL from HTTP, version and cache-bust the URL to match the deployed program + SDK release.
Disputer collateral on resolve_refute
disputer_collateral is the refuter’s collateral SPL ATA for the market mint, the same role as refuter_collateral on open_refute. It is always in the resolve_refute account list: on dismiss the handler does not transfer the refute bond into it, but the account must still be present and valid (mint + owner = ResolutionState.disputer). On accept, the bond is refunded into this ATA.
It is not the market trading vault, pari pool vault, or treasury ATA.
Who may sign resolve_refute (vs market resolvers)
Signers: GlobalConfig primary or secondary authority, or PlatformRegistry.profile_authority (platform operator). These are config / registry keys, not the per-market voteResolution resolver wallets.
GlobalConfig.dispute_resolution_authority (if still present in older docs) is legacy / unused for resolve_refute authorization; do not assume market resolver PDAs can sign it.
SendTransactionError: Unknown action 'undefined' (tooling)
Some @coral-xyz/anchor + @solana/web3.js combinations surface this when error deserialization paths disagree. It is not Vercora-specific. Stay on the peer dependency ranges listed in this package’s package.json, or align @solana/web3.js with the version Anchor’s release notes recommend for your Anchor minor line.
PredictionMarketClient API
Public properties: program, connection, globalConfig (PDA).
Global config (authority)
| Method | Purpose |
| ----------------------------------- | ---------------------------------------------- |
| initializeConfig(params) | One-time init; authority = connected wallet. |
| updateConfig(params) | Update fees, treasury, authorities. |
| addAllowedCollateralMint(mint) | Allowlist a collateral mint. |
| removeAllowedCollateralMint(mint) | Remove from allowlist. |
secondaryAuthority in InitializeConfigParams / UpdateConfigParams: both map to the instruction data field secondary_authority on-chain (not only the secondaryAuthority account meta). Every updateConfig rewrites GlobalConfig.secondary_authority from params.secondaryAuthority. When you only change fees or treasury, pass the existing secondary pubkey you want to keep (from fetchGlobalConfig()); passing the primary key here collapses secondary into primary and removes the backup signer used for platform-only actions (e.g. voiding markets that still hold user liquidity).
Platforms & categories
| Method | Purpose |
| ------------------------------ | ------------------------------------------------------------------------ |
| registerPlatform(params) | Next platform_id from GlobalConfig; returns { platformId, sig }. |
| createMarketCategory(params) | Next category id per PlatformRegistry (must match next_category_id). |
| updateMarketCategory(params) | Rename / toggle active. |
Market creation
| Method | Purpose |
| --------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| createMarket(creator, collateralMint, creatorFeeAccount, params) | Step 1: market + vault. |
| initializeMarketResolverSlots(marketPda, params, opts?, parimutuelStateParams?) | Resolver PDAs; optional parimutuelStateParams appends initializeParimutuelState in the same tx. |
| initializeMarketOutcomeSlots(marketPda, params) | MarketOutcome labels (complete-set & pari). |
| initializeMarketMints(marketPda, marketId) | Mint 8 outcome SPLs (complete-set only). |
| initializeParimutuelState(marketPda, params) | Standalone pari pool + penalty params (if not bundled with resolvers). |
| createMarketFull(creator, collateralMint, creatorFeeAccount, resolverPubkeys, params) | Runs create → outcomes → resolvers (+ mints or pari state in one flow). |
| updateParimutuelState(marketPda, params) | Creator updates penalty split, isEarlyWithdrawAllowed, maxWalletOutcomeInvestment, and isWalletOutcomeStakeExact (open pari pool). |
CreateMarketParams: requires platformId (> 0, BN) with a registered platform + upsertPlatformProfile (global config authority; resolver stake, challenge window, and refute bond are set on the profile, not per market). CreateMarketParams (pari-mutuel): optional isEarlyWithdrawAllowed (default true). Optional maxWalletOutcomeInvestment (BN, 0 = unlimited per-wallet per-outcome net stake cap in collateral base units). Optional isWalletOutcomeStakeExact: when true (parimutuel only), each stake must bring the wallet’s net on that outcome to exactly maxWalletOutcomeInvestment; requires maxWalletOutcomeInvestment > 0. Setting exact mode on a complete-set market fails with InvalidWalletOutcomeStakeExactConfig. Ignored for complete-set markets otherwise.
UpdateParimutuelStateParams: every call writes all fields, pass current values from fetchMarket / fetchParimutuelState for anything you are not changing. Includes earlyWithdrawPenaltyBps, penaltyKeptInPoolBps, isEarlyWithdrawAllowed, maxWalletOutcomeInvestment, isWalletOutcomeStakeExact. If isWalletOutcomeStakeExact is true, maxWalletOutcomeInvestment must be > 0 (same rule as create). Use this to tighten or relax per-wallet caps and to toggle exact vs ceiling mode after launch (market must still be open and not resolved/voided).
When isEarlyWithdrawAllowed is false, early parimutuelWithdraw fails with EarlyWithdrawNotAllowed until close or resolution (voided markets are exempt, full net-stake refunds, no penalty fees). Read fetchMarket(marketPda) for isEarlyWithdrawAllowed, maxWalletOutcomeInvestment, isWalletOutcomeStakeExact.
Complete-set trading
| Method | Purpose |
| ---------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------ |
| mintCompleteSet(user, marketPda, collateralMint, userCollateralAta, platformTreasury, creatorFeeAccount, params, opts?, tokenProgram?) | Mint full set; creates missing outcome ATAs. |
| redeemCompleteSet(user, marketPda, collateralMint, userCollateralAta, params) | Burn full set for collateral. |
| redeemWinning(user, marketPda, collateralMint, userCollateralAta, params) | After resolution, redeem winning outcome tokens. |
Parimutuel trading
| Method | Purpose |
| --------------------------------------- | ------------------------------------------------------- |
| parimutuelStake(marketPda, params) | Stake; signer is provider wallet (user on-chain). |
| parimutuelWithdraw(marketPda, params) | Early exit (penalty may apply) or, after voidMarket, full net-stake refund (no fees). Early path errors if isEarlyWithdrawAllowed is false; void path ignores that flag. |
| parimutuelClaim(marketPda, params) | Claim after resolution. |
Resolution & lifecycle
| Method | Purpose |
| ----------------------------------------- | --------------------------------------------- |
| voteResolution(marketPda, params) | Resolver vote; may escrow resolver stake to the market’s resolver stake vault (see platform policy). |
| revokeResolutionVote(marketPda, params) | Clear vote before changing (blocked once a proposal exists). |
| finalizeResolution(marketPda, params) | When M-of-N agree on one outcome, proposes that outcome, sets ResolutionState.proposed_outcome + proposal_ts; does not set Market.winning_outcome_index yet. |
| confirmResolution(marketPda, params) | After the challenge window elapses with no open dispute, anyone can call this to set Market.winning_outcome_index (final approval / settlement). |
| openRefute(marketPda, params) | During the challenge window: post a refute bond and name an alternative winning outcome (dispute). |
| resolveRefute(marketPda, params) | params.accept === false: slash refute bond to treasury. accept === true: refund bond to refuter and set winning_outcome_index to disputed outcome (no confirmResolution). Signer (not market resolvers): global primary or secondary authority, or platform profileAuthority. Optional params.disputerCollateral defaults to the refuter’s ATA derived from ResolutionState.disputer. |
| claimResolverStake(marketPda, params) | After resolution: matching vote → stake returned to resolver; otherwise stake slashed to platform treasury. |
| fetchResolutionState(marketPda) | Read proposal, dispute, refute bond locked (ResolutionState). Challenge length and resolver stake size come from fetchPlatformProfile. |
| closeMarketEarly(marketPda, params) | Creator / config authority before close_at. |
| voidMarket(marketPda, params) | Void market. Creator cannot void while pari outcome pools (active stakes) or complete-set outstanding is non-zero, use a global config authority to cancel live markets. Cannot void while a refute dispute is open. Parimutuel: then parimutuelWithdraw for full net refunds. |
| claimVoidedParimutuelSurplus(marketPda, params) | Global config authority only. After void, if pari outcome pools are zero but total_pool still holds early-exit pool-keep (no stakers left), sweeps that amount from the vault to the platform treasury. |
| abandonMarket(marketPda, params) | Creator abandons empty market (reclaim rent). |
resolveRefute, account meta disputerCollateral (common pitfall): Anchor’s JS client expects camelCase keys in .accounts() / .accountsStrict(), matching the camelCase IDL. The JSON IDL field is disputer_collateral; if you pass disputer_collateral in the accounts object, Anchor ignores it and you get Account 'disputerCollateral' not provided. Use disputerCollateral. The account is always required for the instruction (see Disputer collateral on resolve_refute under IDL, account metas, and peer deps, dismiss does not fund it, but it must be present). For raw program.methods, supply every remaining account explicitly; as never on the accounts object is only to satisfy stale generated TypeScript, the chain still needs the meta. IDL drift (served JSON vs npm bundle) also produces “not provided”; prefer import { IDL } from '@vercora-protocol/sdk' or the same dist/idl/vercora.json as your installed package version.
resolveRefute examples
High-level client (derives refuter ATA when disputerCollateral is omitted):
await client.resolveRefute(marketPda, {
marketId,
accept: false, // dismiss: bond → platform treasury
});
// Optional: pass the refuter collateral ATA explicitly (same as derive from ResolutionState.disputer)
import { disputerCollateralAta } from "@vercora-protocol/sdk";
import { TOKEN_PROGRAM_ID } from "@solana/spl-token";
const rs = await client.fetchResolutionState(marketPda);
const mint = (await client.fetchMarket(marketPda)).collateralMint;
// Use TOKEN_2022_PROGRAM_ID when the collateral mint uses Token-2022.
const ata = disputerCollateralAta(mint, rs.disputer as PublicKey, TOKEN_PROGRAM_ID);
await client.resolveRefute(marketPda, { marketId, accept: true, disputerCollateral: ata });Raw Anchor (wallet = authority signer). Every account meta below uses camelCase names:
import { SystemProgram } from "@solana/web3.js";
import {
disputerCollateralAta,
deriveGlobalConfig,
derivePlatformRegistry,
deriveResolverStakeVault,
} from "@vercora-protocol/sdk";
import { getAssociatedTokenAddressSync, TOKEN_PROGRAM_ID, ASSOCIATED_TOKEN_PROGRAM_ID } from "@solana/spl-token";
const marketAccount = await program.account.market.fetch(marketPda);
// After openRefute: read disputer from chain (use your resolution PDA)
const rs = await program.account.resolutionState.fetch(resolutionStatePk);
const disputer = rs.disputer as PublicKey;
const disputerCollateral = disputerCollateralAta(collateralMint, disputer, TOKEN_PROGRAM_ID);
const treasuryAta = getAssociatedTokenAddressSync(
collateralMint,
platformTreasuryWallet,
false,
TOKEN_PROGRAM_ID,
ASSOCIATED_TOKEN_PROGRAM_ID
);
await program.methods
.resolveRefute({ marketId, accept: false })
.accounts({
authority: wallet.publicKey,
market: marketPda,
resolutionState: resolutionStatePk,
resolverStakeVault: deriveResolverStakeVault(program.programId, marketPda),
collateralMint,
disputerCollateral, // required, camelCase key (`disputer_collateral` in JSON IDL is ignored)
globalConfig: deriveGlobalConfig(program.programId),
platformRegistry: derivePlatformRegistry(program.programId, marketAccount.platformId),
platformTreasuryWallet,
platformTreasuryAta: treasuryAta,
collateralTokenProgram: TOKEN_PROGRAM_ID,
systemProgram: SystemProgram.programId,
} as any) // narrow only if your typegen omits `disputerCollateral`
.rpc();See Resolution approval flow below for the full sequence and SDK usage.
Discovery & reads
| Method | Purpose |
| ----------------------------------------------------------------- | --------------------------------------------------------------------------------------------- |
| fetchGlobalConfig() | Global config account. |
| fetchMarket(marketPda) | Market account. |
| fetchAllMarkets(platformId?) | All markets (optional memcmp on platform_id); includes outcome labels. |
| fetchMarketsByPlatform(platformId) | Same as fetchAllMarkets(platformId). |
| fetchVoidedMarkets() | Market accounts with is_voided only (RPC memcmp + dataSize). |
| fetchVoidedParimutuelSurplusCandidates() | Subset: voided pari, not resolved, sum(outcome_pools)=0, total_pool > 0 (claimable). |
| fetchMarketsByCreator(creator) | Raw rows { pubkey, account }[], memcmp on creator. |
| getUsersMarkets(creator, filters?) | ListedMarket[] for creator; optional platformId (RPC) + categoryId (client filter). |
| fetchMarketOutcomeLabels(marketPda, outcomeCount) | Outcome labels. |
| fetchVaultBalance(marketPda) | Vault balance (bigint base units). |
| fetchOutcomeBalance(marketPda, user, outcomeIndex) | Single outcome token balance (complete-set). |
| fetchAllOutcomeBalances(marketPda, user, outcomeCount) | All outcome balances for user. |
| fetchMarketCategory(categoryPda) | One category. |
| fetchAllMarketCategories() | All categories (sorted). |
| fetchResolutionVote(marketPda, resolverIndex) | Vote PDA or null. |
| fetchResolutionState(marketPda) | Proposal, dispute, refute bond (ResolutionState PDA). Use fetchPlatformProfile for challenge window and resolver stake. |
| fetchMarketOutcomesSnapshot(marketPda, outcomeCount) | Decoded outcomes + tallies. |
| fetchOutcomeTallyCounts(marketPda) | Quick tally array. |
| fetchAllowedCollateralMints() | Allowlist mints. |
| fetchUserProfile(wallet) | User profile or null. |
| fetchPlatformProfile(platformId) | Platform profile or null. |
| fetchParimutuelState(marketPda) | Pari pool state. |
| fetchParimutuelPosition(marketPda, user, outcomeIndex) | Position or null. |
| fetchParimutuelActiveStakesBatch(marketPda, user, outcomeCount) | Active stakes per outcome. |
| computeParimutuelOdds(state, outcomeCount) | Implied probs + payout multipliers. |
| fetchResolver(marketPda, index) | One resolver account. |
| fetchAllResolvers(marketPda, numResolvers, { debug }) | Initialized resolver slots; optional debug: true logs PDA presence to the console. |
Profiles
| Method | Purpose |
| ---------------------------------- | ------------------------------------------- |
| upsertUserProfile(params) | Display name / URL; charges flat SOL fee per save when configured. |
| closeUserProfile() | Close caller’s profile. |
| verifyUserProfile(params) | Verifier marks verified. |
| upsertPlatformProfile(params) | Platform profile (scoped by platform_id); global config primary or secondary authority must sign. |
| closePlatformProfile(platformId) | Close profile. |
| verifyPlatformProfile(params) | Verify platform profile. |
Examples
Global config (first deploy)
import { PublicKey } from "@solana/web3.js";
import BN from "bn.js";
await client.initializeConfig({
// System program / “none” placeholder, use a real secondary authority pubkey in production
secondaryAuthority: new PublicKey("11111111111111111111111111111111"),
depositPlatformFeeBps: 100,
platformTreasuryWallet: treasuryPubkey,
platformFeeLamports: new BN(357_000),
parimutuelWithdrawPlatformFeeBps: 50,
});Register platform & category
const { platformId } = await client.registerPlatform({
profileAuthority: profileSignerPubkey,
});
await client.createMarketCategory({
platformId,
name: "Politics",
});Create a market (manual steps, complete-set)
import BN from "bn.js";
const marketId = new BN(Date.now());
const { marketPda } = await client.createMarket(
creatorPubkey,
collateralMint,
creatorFeeAta,
{
marketId,
outcomeCount: 2,
resolutionThreshold: 1,
closeAt: new BN(Math.floor(Date.now() / 1000) + 86400),
creatorFeeBps: 50,
depositPlatformFeeBps: 0,
numResolvers: 1,
maxOutcomeInvestment: new BN(0),
title: "Will it rain tomorrow?",
marketType: "completeSet",
platformId: new BN(1), // must be > 0, register_platform + upsert_platform_profile first
categoryId: new BN(0),
},
);
await client.initializeMarketOutcomeSlots(marketPda, {
marketId,
labels: ["Yes", "No"],
});
await client.initializeMarketResolverSlots(marketPda, {
marketId,
resolverPubkeys: [resolverPubkey],
});
await client.initializeMarketMints(marketPda, marketId);Create market in one call (createMarketFull)
Complete-set (mints outcome tokens):
const marketPda = await client.createMarketFull(
creatorPubkey,
collateralMint,
creatorFeeAta,
[resolverPubkey],
{
marketId,
outcomeCount: 2,
resolutionThreshold: 1,
closeAt: new BN(Math.floor(Date.now() / 1000) + 86400),
creatorFeeBps: 50,
depositPlatformFeeBps: 0,
numResolvers: 1,
maxOutcomeInvestment: new BN(0),
title: "Two-outcome market",
marketType: "completeSet",
outcomeLabels: ["Yes", "No"],
platformId: new BN(1),
categoryId: new BN(0),
},
);Parimutuel (resolver tx also initializes pari state; optional parimutuelInit overrides defaults):
const marketPda = await client.createMarketFull(
creatorPubkey,
collateralMint,
creatorFeeAta,
[resolverPubkey],
{
marketId,
outcomeCount: 2,
resolutionThreshold: 1,
closeAt: new BN(Math.floor(Date.now() / 1000) + 86400),
creatorFeeBps: 50,
depositPlatformFeeBps: 0,
numResolvers: 1,
maxOutcomeInvestment: new BN(0),
title: "Pari pool",
marketType: "parimutuel",
outcomeLabels: ["A", "B"],
platformId: new BN(1),
categoryId: new BN(0),
// Optional, default true. Set false to lock stakes until close/resolution (no early parimutuelWithdraw).
isEarlyWithdrawAllowed: true,
parimutuelInit: {
earlyWithdrawPenaltyBps: 500,
penaltyKeptInPoolBps: 8000,
},
},
);Complete-set trading
const treasury = (await client.fetchGlobalConfig()).platformTreasury;
await client.mintCompleteSet(
userPubkey,
marketPda,
collateralMint,
userCollateralAta,
treasury,
creatorFeeAta,
{ marketId, amount: new BN(1_000_000) },
);
await client.redeemCompleteSet(
userPubkey,
marketPda,
collateralMint,
userCollateralAta,
{ marketId },
);
await client.redeemWinning(
userPubkey,
marketPda,
collateralMint,
userCollateralAta,
{
marketId,
amount: winningAmountBn,
},
);Parimutuel trading
Signer is always the connected wallet (no user first argument):
await client.parimutuelStake(marketPda, {
marketId,
outcomeIndex: 0,
amount: new BN(500_000),
});
await client.parimutuelWithdraw(marketPda, {
marketId,
outcomeIndex: 0,
amount: new BN(500_000),
});
// If Market.isEarlyWithdrawAllowed is false (from create or updateParimutuelState), parimutuelWithdraw throws EarlyWithdrawNotAllowed, unless the market is voided (then full net stake back, no fees).
// After voidMarket on a pari pool, recover stakes (repeat per outcome / until active stake is 0):
// await client.voidMarket(marketPda, { marketId });
// await client.parimutuelWithdraw(marketPda, { marketId, outcomeIndex: 0, amount: userActiveStakeBn });
await client.parimutuelClaim(marketPda, {
marketId,
outcomeIndex: 0,
});
const state = await client.fetchParimutuelState(marketPda);
const odds = client.computeParimutuelOdds(state, 2);Creator: update pari pool settings (updateParimutuelState)
Only the market creator can call this while the market is open (not resolved/voided). The instruction updates penalty bps, early-withdraw allowance, and per-wallet per-outcome stake rules in one transaction. Pass current values from fetchParimutuelState and fetchMarket for any field you are not changing:
const m = await client.fetchMarket(marketPda);
const pari = await client.fetchParimutuelState(marketPda);
await client.updateParimutuelState(marketPda, {
marketId,
earlyWithdrawPenaltyBps: pari.earlyWithdrawPenaltyBps,
penaltyKeptInPoolBps: pari.penaltyKeptInPoolBps,
isEarlyWithdrawAllowed: false, // block parimutuelWithdraw until close/resolution
maxWalletOutcomeInvestment: m.maxWalletOutcomeInvestment, // keep existing cap
isWalletOutcomeStakeExact: m.isWalletOutcomeStakeExact, // keep exact vs ceiling mode
});
// Re-enable early exit later (still pass wallet fields from `m`):
await client.updateParimutuelState(marketPda, {
marketId,
earlyWithdrawPenaltyBps: pari.earlyWithdrawPenaltyBps,
penaltyKeptInPoolBps: pari.penaltyKeptInPoolBps,
isEarlyWithdrawAllowed: true,
maxWalletOutcomeInvestment: m.maxWalletOutcomeInvestment,
isWalletOutcomeStakeExact: m.isWalletOutcomeStakeExact,
});
// Example: set a per-wallet cap (base units) and ceiling mode only:
await client.updateParimutuelState(marketPda, {
marketId,
earlyWithdrawPenaltyBps: pari.earlyWithdrawPenaltyBps,
penaltyKeptInPoolBps: pari.penaltyKeptInPoolBps,
isEarlyWithdrawAllowed: m.isEarlyWithdrawAllowed,
maxWalletOutcomeInvestment: new BN(1_000_000),
isWalletOutcomeStakeExact: false,
});Staking errors when caps apply: WalletOutcomeInvestmentCapExceeded (6050), WalletOutcomeExactStakeMismatch (6052) when exact mode is on, InvalidWalletOutcomeStakeExactConfig (6051) if exact mode is enabled with a zero cap (create or update).
Resolution (minimal)
Resolvers vote, then finalizeResolution records a proposal. After the challenge window, if no dispute blocks confirmation, confirmResolution sets the final winner on the market account. See the full flow in the next section.
await client.voteResolution(marketPda, {
marketId,
resolverIndex: 0,
outcomeIndex: 1,
});
// Records proposed_outcome + proposal_ts when M-of-N threshold is met (not final settlement yet).
await client.finalizeResolution(marketPda, { marketId });
// After challenge_window_secs (and no open refute blocking), anyone confirms final outcome:
await client.confirmResolution(marketPda, { marketId });Discovery
// Every market (heavy on RPC)
const all = await client.fetchAllMarkets();
// By platform (memcmp)
const byPlatform = await client.fetchAllMarkets(new BN(1));
// Markets created by a wallet, full rows with labels + filters
const mine = await client.getUsersMarkets(creatorPubkey, {
platformId: new BN(1), // optional RPC filter
categoryId: 2, // optional; applied after decode
});
// Raw accounts only (no labels)
const raw = await client.fetchMarketsByCreator(creatorPubkey);User profile
Requires sufficient SOL for the configured platform_fee_lamports on each save (plus rent on first create).
await client.upsertUserProfile({
displayName: "Alice",
url: "https://example.com",
});
const profile = await client.fetchUserProfile(wallet.publicKey);Resolution approval flow (propose → challenge → confirm or refute-accept)
Settlement is two-phase: first a proposal is recorded on ResolutionState, then, after an optional challenge window, the market is approved into a final winner on the Market account.
Platform prerequisite,
create_marketrequiresplatform_id > 0: a registeredPlatformRegistry, an initializedPlatformProfile(singleresolver_stake,challenge_window_secs,refute_bondfor that platform), and PDAs forresolution_stateandresolver_stake_vault.Votes, Each assigned resolver calls
voteResolutionwith their outcome. Votes update per-outcome tallies; resolver stake (fromPlatformProfile.resolver_stake) transfers into the market’s resolver stake vault.Propose (
finalizeResolution), When at leastresolution_thresholdresolvers agree on the same outcome, anyone may callfinalizeResolution. On-chain this does not immediately setMarket.winning_outcome_index. It setsResolutionState.proposed_outcome,proposal_ts, and starts the challenge window (duration fromPlatformProfile.challenge_window_secs).Challenge window, Until
proposal_ts + challenge_window_secselapses, a participant mayopenRefutewith exactlyplatform_profile.refute_bond: lock that collateral and assert a different winning outcome. An authorized party (global primary or secondary authority, or platformprofileAuthority) mayresolveRefute:accept: falsedismisses (bond → treasury),accept: trueaccepts (bond refunded,winning_outcome_indexset immediately). While a dispute is active,confirmResolutionis rejected.voidMarketis rejected while a dispute is open.Confirm (final approval), After
proposal_ts + challenge_window, if there is no blocking dispute and the market was not already finalized byresolveRefute(accept: true), anyone callsconfirmResolution. This writesMarket.winning_outcome_index, the outcome is now final forredeemWinning/parimutuelClaim.Resolver stake, After settlement, resolvers use
claimResolverStake: stake returns to the resolver if their vote matched the final winner; otherwise it is transferred to the platform treasury.
Reads
fetchResolutionState(marketPda),proposed_outcome,proposal_ts,dispute_active,refute_bond_amount, etc.fetchPlatformProfile(platformId),challenge_window_secs,resolver_stake,refute_bond(challenge length and stake are not stored onResolutionState).fetchMarket(marketPda),winningOutcomeIndexisnulluntilconfirmResolutionorresolveRefute({ accept: true })succeeds.
UI / agent logic
- Treat
finalizeResolutionas “proposal passed”; show countdown untilconfirmResolutionis allowed. - Gate “resolved” UX on
winningOutcomeIndex !== null, not on proposal alone.
Platforms and categories
On-chain, platform_id is u32 and category_id is u8 in CreateMarketArgs (not pubkeys).
register_platform, assigns the next id fromGlobalConfig.next_platform_id(starts at 1); PDA["platform", platform_id le u32].upsert_platform_profile, metadata + resolution policy (resolver_stake,challenge_window_secs,refute_bond); signed by global config primary or secondary authority.create_market_category,category_idmust equalPlatformRegistry.next_category_id; PDA["market-category", platform_id, category_id].create_market, requiresplatform_id > 0,platform_registry,platform_profile,resolution_state,resolver_stake_vault. Usecategory_id == 0for uncategorized markets (omitmarket_categoryaccount). Non-zerocategory_idrequires an activemarket_categoryPDA for that platform.
Use derivePlatformRegistry, derivePlatformProfile, deriveMarketCategory, deriveResolutionState, deriveResolverStakeVault when building instructions manually.
Market types
Use marketType: 'parimutuel' or 'completeSet' at creation; the choice is permanent.
- Parimutuel, pooled stakes, no outcome SPLs; good default for prediction apps.
isEarlyWithdrawAllowedis set at create (default allowed) and can be changed by the creator viaupdateParimutuelState; when disabled, earlyparimutuelWithdrawis blocked until close or resolution.maxWalletOutcomeInvestmentandisWalletOutcomeStakeExactare set at create and can be updated viaupdateParimutuelState(same instruction as penalty / early-withdraw edits). AftervoidMarket, participantsparimutuelWithdrawfull net stakes (no penalty / withdraw fees). - Complete-set, outcome SPL tokens; you still need external liquidity for single-leg trading.
PDA helpers
From @vercora-protocol/sdk:
import {
PROGRAM_ID,
deriveGlobalConfig,
deriveAllowedMint,
deriveMarket,
deriveVault,
deriveOutcomeMint,
deriveAllOutcomeMints,
deriveResolver,
deriveAllResolvers,
deriveResolutionVote,
deriveMarketOutcome,
deriveAllMarketOutcomes,
deriveParimutuelState,
deriveParimutuelPosition,
deriveUserProfile,
derivePlatformRegistry,
derivePlatformProfile,
deriveMarketCategory,
deriveResolutionState,
deriveResolverStakeVault,
disputerCollateralAta,
bnLike,
bnToU32,
bnToU8,
} from "@vercora-protocol/sdk";
const marketPda = deriveMarket(PROGRAM_ID, creatorPubkey, marketId);
const vaultPda = deriveVault(PROGRAM_ID, marketPda);Offsets MARKET_ACCOUNT_CREATOR_MEMCMP_OFFSET, MARKET_ACCOUNT_PLATFORM_ID_MEMCMP_OFFSET, and MARKET_ACCOUNT_IS_VOIDED_MEMCMP_OFFSET are exported for custom getProgramAccounts filters. Use marketIsVoidedMemcmp(true) for the Borsh-encoded bool bytes at that offset. fetchVoidedMarkets / fetchVoidedParimutuelSurplusCandidates apply the voided filter so listing cost scales with voided markets only.
marketUi helpers
import {
getMarketLifecycleStatus,
formatTimeLeft,
listedMarketFeedTag,
} from "@vercora-protocol/sdk";
const status = getMarketLifecycleStatus({
isVoided: false,
isClosedEarly: false,
winningOutcomeIndex: null,
closeAt: ts,
});TypeScript types
import type {
CreateMarketParams,
UpdateParimutuelStateParams,
ListedMarket,
GetUsersMarketsFilters,
MarketAccount,
ParimutuelStateAccount,
ParimutuelOdds,
GlobalConfigAccount,
UserProfileAccount,
} from "@vercora-protocol/sdk";AI agent integration
Start from agents/skill.md in this package for a stable entrypoint; the full playbook is at https://vercora.xyz/agents/playbook.md (production), or docs/AI-AGENT-SDK-PLAYBOOK.md in the monorepo. Humans: the web app serves the same playbook at https://vercora.xyz/docs/agents with a Resolution at a glance summary (aligned with https://vercora.xyz/docs#resolution-flow).
- Bootstrap:
Connection→AnchorProvider→Program<Vercora>fromIDL→PredictionMarketClient. - Branch:
fetchMarket→market.marketType(completeSetvsparimutuel). - Flows:
- Optional:
registerPlatform→createMarketCategory→createMarketwith ids. - Create:
createMarketFullorcreateMarket+ resolver / outcome / mint or pari init. - Complete-set:
mintCompleteSet→redeemCompleteSet→redeemWinning. - Parimutuel:
parimutuelStake→ optionalparimutuelWithdraw(ifisEarlyWithdrawAllowed, or aftervoidMarketfor full refunds) →parimutuelClaim; creator may change penalty split, early withdraw, and per-wallet caps (maxWalletOutcomeInvestment,isWalletOutcomeStakeExact) withupdateParimutuelState(pass full params fromfetchMarket/fetchParimutuelStatewhen only changing some fields). - Resolution:
voteResolution→finalizeResolution(proposal) → challenge window → optionalopenRefute/resolveRefute(dismiss or accept) →confirmResolutionif not already finalized by accept →claimResolverStake; then redeem/claim for traders.
- Optional:
- Discovery:
fetchAllMarkets,getUsersMarkets,fetchMarketsByCreator,fetchVoidedMarkets,fetchVoidedParimutuelSurplusCandidates. - Safety: verify signers, PDAs, ATAs, and RPC limits on
getProgramAccounts.
Changelog
See CHANGELOG.md in the package (published to npm). Releases are generated with standard-version from Conventional Commits in app/sdk (e.g. feat:, fix:). After running a release script you can edit CHANGELOG.md or amend the release commit before npm publish.
