Skip to main content

How to Trade Kalshi Prediction Markets on Solana Using DFlow

Updated on
Feb 20, 2026

29 min read

Overview

Prediction markets let users take positions on the outcome of real-world events like sports, election results, economic indicators, cultural moments, and more. Positions are structured as Yes or No contracts that pay out if the chosen outcome occurs. Kalshi is the first CFTC-regulated prediction market exchange in the United States, which means trades on Kalshi are legal, exchange-regulated event contracts, not offshore or gray-market speculation.

Kalshi operates offchain, but through its integration with DFlow, those markets are now accessible directly from Solana applications. Solana developers can integrate DFlow's API into their dApps to offer prediction market functionality natively, with positions represented as real SPL tokens composable with the rest of DeFi.

DFlow is a Solana-native trading infrastructure layer that provides liquidity routing, order execution, and tokenization services for decentralized applications. DFlow's *Metadata API bridges Kalshi's regulated event contracts to the Solana ecosystem by tokenizing Yes/No positions as SPL tokens, enabling any Solana app to offer prediction market trading without building exchange infrastructure from scratch. The Trade API builds and returns ready-to-sign Solana transactions for buying outcome tokens or redeeming winnings.

This guide walks through building a TypeScript CLI that covers the full trade lifecycle: discovering live English Premier League (EPL) football markets, fetching Yes/No prices, placing a trade, tracking open positions, and redeeming winnings after settlement.

What You Will Do

In this guide, you'll build a TypeScript CLI that walks through the full lifecycle of a prediction market trade on Solana:


  • Find prediction markets using the DFlow Metadata API
  • Fetch Yes/No pricing
  • Buy Yes or No outcome tokens using the DFlow Trade API
  • Track open positions by mapping wallet token balances to prediction markets
  • Redeem winning outcome tokens for USDC after market settlement

What You Will Need


  • A Quicknode Solana endpoint for sending signed transactions — Sign Up to get started
  • A wallet with a small amount of SOL for transaction fees and USDC to buy outcome tokens
  • Node.js 20 or later
  • A DFlow API key for production

This guide uses these packages and tools:

DependencyVersion
Node24.8.0
typescript5.7.3
tsx4.20.3
@solana/kit6.1.0

How Kalshi Prediction Markets Work

Kalshi organizes tradable contracts in a four-level hierarchy: Categories → Series → Events → Markets.

Categories are the broadest grouping (Sports, Economics, Politics). Each category has one or more tags that further classify its content (Soccer, Basketball). Tags are how you filter down to a specific domain when searching the API.

A series is a named, recurring contract template within a category. It defines the rules, resolution sources, and fee structure that apply to every market it produces. For example, the series KXEPLGAME covers all English Premier League (EPL) game-result markets for the season.

An event represents one real-world occurrence within a series. An event groups together all the binary markets for that occurrence.

A market is an individual Yes/No contract within an event. Each market has a yesMint and noMint of the SPL token that represents positions on Solana. Markets resolve when the outcome is officially determined. The winning side pays out $1.00 per contract while the losing side expires worthless.

How DFlow Bridges Kalshi to Solana

DFlow's key innovation is Concurrent Liquidity Programs (CLP), a Solana-native framework that bridges off-chain Kalshi liquidity with on-chain Solana users:


  1. A trader expresses a trade intent on-chain (analogous to a limit order).
  2. Liquidity providers observe and fill the intent at competitive prices.
  3. The protocol mints SPL tokens representing the purchased prediction position.
  4. When the market resolves, winning tokens are redeemed back for their stablecoin payout through the same CLP.

Because positions are real Solana SPL tokens, not synthetic representations, they can be borrowed, lent, used as collateral, or traded on DEXs.

DFlow exposes two APIs for working with these tokens:

APIBase URL (Dev)Purpose
Metadata APIhttps://dev-prediction-markets-api.dflow.netDiscover markets, fetch pricing, check settlement
Trade APIhttps://dev-quote-api.dflow.netBuild transactions to buy outcome tokens or redeem winnings

Dev Endpoints

Both dev endpoints are open with no API key needed during development. Rate limits apply. For production, contact DFlow for a dedicated API key. "Dev" refers to the unauthenticated API access tier, not Solana devnet. All requests execute on Solana mainnet-beta.

Discover Prediction Markets

Before placing a trade, you need to identify the series ticker for the markets you want to trade, then find the specific event and outcome mints. DFlow's Metadata API exposes the full Kalshi hierarchy through a set of discovery endpoints.

For this guide, we'll be working with English Premier League (EPL) game markets. The same discovery steps apply to any other category — politics, economics, entertainment, and more.

Rather than writing code to search for the right series programmatically, we'll walk through the discovery process manually. Three API calls are all it takes, and once you have the series ticker it stays constant for the season.

Step 1: Get available tags

curl https://dev-prediction-markets-api.dflow.net/api/v1/tags_by_categories

Expected response (other categories omitted for brevity):

{
"tagsByCategories": {
"Climate and Weather": ["..."],
"Companies": ["..."],
"Crypto": ["..."],
"Economics": ["..."],
"Elections": null,
"Entertainment": ["..."],
"Financials": ["..."],
"Mentions": ["..."],
"Politics": ["..."],
"Science and Technology": ["..."],
"Social": null,
"Sports": [
"Soccer",
"Basketball",
"Baseball",
"Football",
"Hockey",
"Olympics",
"Golf",
"Tennis",
"Esports",
"MMA",
"Motorsport",
"Rugby",
"Cricket",
"Lacrosse",
"Boxing",
"Darts",
"Chess"
]
}
}

The Sports category contains a Soccer tag. Pass those two values as category and tags in the next step.

Step 2: List all Soccer series

curl "https://dev-prediction-markets-api.dflow.net/api/v1/series?category=Sports&tags=Soccer"

The response contains multiple series. Scan the title fields until you find "English Premier League Game" and note its ticker:

{
"series": [
{...},
{
"ticker": "KXEPLGAME",
"frequency": "custom",
"title": "English Premier League Game",
"category": "Sports",
"tags": ["Soccer"],
"settlementSources": [
{ "name": "ESPN", "url": "https://www.espn.com/" },
{ "name": "Fox Sports", "url": "https://www.foxsports.com/" }
],
"contractUrl": "...",
"contractTermsUrl": "...",
"productMetadata": {},
"feeType": "quadratic_with_maker_fees",
"feeMultiplier": 1.0,
"additionalProhibitions": ["..."]
}
]
}

The ticker is KXEPLGAME. That's the series to use for this guide. Other series (other soccer leagues) will appear in the full response.

Step 3: Get active EPL events

With the series ticker, this is the one call your app makes at runtime to get everything it needs:

curl https://dev-prediction-markets-api.dflow.net/api/v1/events?seriesTickers=KXEPLGAME&status=active&withNestedMarkets=true

We need to pass two parameters to get the results we want:


  • status=active: Limits results to events currently open for trading. Without it, the response includes determined, finalized, and suspended events you can't buy into.
  • withNestedMarkets=true: Embeds each event's markets (including yesMint, noMint, live pricing, and account data) directly in the response. One call returns everything needed to display and trade, with no follow-up requests per market.

Expected response (actual response includes all active EPL fixtures):

{
"events": [
{...},
{
"ticker": "KXEPLGAME-26FEB18WOLARS",
"seriesTicker": "KXEPLGAME",
"title": "Wolverhampton vs Arsenal",
"subtitle": "WOL vs ARS (Feb 18)",
"status": "active",
"markets": [
{
"ticker": "KXEPLGAME-26FEB18WOLARS-ARS",
"eventTicker": "KXEPLGAME-26FEB18WOLARS",
"title": "Wolverhampton vs Arsenal Winner?",
"yesSubTitle": "Arsenal",
"noSubTitle": "Arsenal",
"status": "active",
"result": "",
"openTime": 1770382800,
"closeTime": 1772654400,
"yesBid": "0.7600",
"yesAsk": "0.7700",
"noBid": "0.2300",
"noAsk": "0.2400",
"accounts": {
"CASHx9KJUStyftLFWGvEVf59SGeG9sh5FfcnZMVPCASH": {
"marketLedger": "C5sDtQq8iAZHqMGdH382uCT2yZTDAphEhVsMnGKAWDBT",
"yesMint": "GPGgr29ektC4ZB2TsPpbWmmNiCtNjq3vospE8cLweH5U",
"noMint": "7fLxMYQdQRdTCVTDXzxaimt6rjXSpgogWYiBXVE78roi",
"isInitialized": true,
"redemptionStatus": "pending"
},
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v": {
"marketLedger": "ERm2CMDxUJduckmBkZU28SBzzGycZzcdUbiRmzqRaoA",
"yesMint": "EahtAm7FJfTrGdEFvsQbztsadM6ohMqyPcLk1Q6YCRDx",
"noMint": "4kXWe1ofHoihJfSzTAT7Yecm5vsEuBFEKmrtLzuASLU8",
"isInitialized": true,
"redemptionStatus": "pending"
}
}
},
{
"ticker": "KXEPLGAME-26FEB18WOLARS-WOL",
"eventTicker": "KXEPLGAME-26FEB18WOLARS",
"title": "Wolverhampton vs Arsenal Winner?",
"yesSubTitle": "Wolverhampton",
"noSubTitle": "Wolverhampton",
"status": "active",
"result": "",
"openTime": 1770382800,
"closeTime": 1772654400,
"yesBid": "0.0700",
"yesAsk": "0.0800",
"noBid": "0.9200",
"noAsk": "0.9300",
"accounts": {
"CASHx9KJUStyftLFWGvEVf59SGeG9sh5FfcnZMVPCASH": {
"marketLedger": "ADBWy7L2VzSAfVima4ybNqse7zDGBqac1B6fGkYSEpVc",
"yesMint": "Hy8XLnGcyvmbxf3SBBQbkws5SDa46eF2AWhJLQXiuSci",
"noMint": "2Tw3tAE3is7TiCqrcS7DxXJ2UAeEsq7dbMDc8Nya57eg",
"isInitialized": true,
"redemptionStatus": "pending"
},
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v": {
"marketLedger": "5UHoukpeVPQbmSUaAPWnkXEKZMrjSmwTqqaD8eXmvKNn",
"yesMint": "CA7FMbzNTfeR7jkLzF113bBJupKwq98cixaQtc3b3frb",
"noMint": "D7ibW7tu2kvzfbDS78gF5i9UZTye7pqTP63yxYd43No3",
"isInitialized": true,
"redemptionStatus": "pending"
}
}
},
{
"ticker": "KXEPLGAME-26FEB18WOLARS-TIE",
"eventTicker": "KXEPLGAME-26FEB18WOLARS",
"title": "Wolverhampton vs Arsenal Winner?",
"yesSubTitle": "Tie",
"noSubTitle": "Tie",
"status": "active",
"result": "",
"openTime": 1770382800,
"closeTime": 1772654400,
"yesBid": "0.1500",
"yesAsk": "0.1600",
"noBid": "0.8400",
"noAsk": "0.8500",
"accounts": {
"CASHx9KJUStyftLFWGvEVf59SGeG9sh5FfcnZMVPCASH": {
"marketLedger": "BfdCBQiiNbxxWNgs1dYpNgsfXC5SDH19RmratW2hTxPG",
"yesMint": "6Ka59wvyvppd2v7D1LGfjKMb2LJ36KcnCpHZU2uP2EPB",
"noMint": "48CyDoWdsEL62gD81My4wHYybrsyH8dnHRHWJcaUHvtA",
"isInitialized": true,
"redemptionStatus": "pending"
},
"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v": {
"marketLedger": "GGViDLxL6RRQ4zTydGoiL6NnLugxyDGraydUBAQfo9iX",
"yesMint": "4qeSi2JVCbE9VQt1uzTJTpJSKdMFRsqWuvf3UL9fGa2P",
"noMint": "7GA2eFoEupkSqJaeKrHgS516dipVDfmQ4ZQ2BFdZpdb3",
"isInitialized": false,
"redemptionStatus": null
}
}
}
]
},
"..."
],
"cursor": null
}

Each event groups three markets, one per outcome (-ARS, -WOL, -TIE). Each market has two accounts entries keyed by settlement mint: CASH CASH...CASH (a USD-backed stablecoin issued by Phantom) and USDC EPjF...Dt1v. The yesMint and noMint addresses under each settlement account are the SPL token mints your wallet will hold after a trade.

This guide uses USDC as the settlement mint when constructing buy and redeem orders.

Build the Sample App

Now that you understand how Kalshi markets are structured and how DFlow surfaces them on Solana, it's time to write the code. In this section you'll build a small TypeScript CLI with scripts to get active events in a series, buy an outcome token, view your open positions, and redeem your winnings after settlement.

Set Up Wallet

You need a Solana keypair to sign transactions. Create a dedicated wallet for this guide using the Solana CLI:

solana-keygen new --outfile ~/dflow-wallet.json

Fund the wallet with:


  • A small amount of SOL (~0.01 SOL) to cover transaction fees
  • Some USDC to buy outcome tokens
Real Funds Required

All trades, including on the "dev" (unauthenticated) API endpoints, execute on Solana mainnet-beta with real USDC. Use a dedicated wallet and only fund it with what you are prepared to spend while testing.

Initialize the Project

Create a new project directory and initialize it with npm:

mkdir dflow-prediction-markets && cd dflow-prediction-markets
npm init -y
mkdir src

Install the dependencies:

npm install @solana/kit
npm install --save-dev typescript tsx @types/node

Configure Environment

Before writing any code, set up your environment variables. Create a .env file in the project root:

.env
METADATA_API_URL=https://dev-prediction-markets-api.dflow.net
TRADE_API_URL=https://dev-quote-api.dflow.net
DFLOW_API_KEY= # required for production endpoints, leave blank for dev
QUICKNODE_RPC_URL=https://docs-demo.solana-mainnet.quiknode.pro/abcd1234
KEYPAIR_PATH=/path/to/your/wallet/dflow-wallet.json
SERIES_TICKER=KXEPLGAME
USDC_MINT=EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v
VariableDescription
METADATA_API_URLDFlow Metadata API base URL for market discovery and pricing.
TRADE_API_URLDFlow Trade API base URL for buying and redeeming outcome tokens.
DFLOW_API_KEYLeave blank for dev endpoints. Required as x-api-key header in production.
QUICKNODE_RPC_URLYour Quicknode mainnet-beta endpoint. Sign up here to get one.
KEYPAIR_PATHPath to your Solana keypair JSON. This wallet signs trades and must hold SOL for fees and USDC to buy outcome tokens.
SERIES_TICKERThe Kalshi series to monitor. Set to KXEPLGAME for EPL game markets.
USDC_MINTUSDC mint used to select the right mint addresses from market accounts.

Create Types File

Before writing any scripts, create src/types.ts. Each script in this guide imports from this file, so defining the types once keeps the code consistent and fully type-safe across discovery, buying, positions, and redemption.

src/types.ts
export interface MarketAccountInfo {
yesMint: string;
noMint: string;
isInitialized?: boolean;
redemptionStatus: string;
scalarOutcomePercent: number | null;
}

export interface Market {
ticker: string;
title: string;
yesSubTitle?: string;
closeTime?: number | null;
status?: string;
result?: string | null;
yesBid: string | null;
yesAsk: string | null;
noBid: string | null;
noAsk: string | null;
accounts: Record<string, MarketAccountInfo>;
}

export interface Event {
ticker: string;
title: string;
subtitle: string | null;
markets: Market[];
}

export interface EventsResponse {
events: Event[];
cursor: number | null;
}

export interface OrderResponse {
outAmount: string;
executionMode: 'sync' | 'async';
transaction: string;
lastValidBlockHeight: number;
revertMint?: string;
}

export interface OrderStatusResponse {
status: 'pending' | 'expired' | 'failed' | 'open' | 'pendingClose' | 'closed';
outAmount: number;
reverts?: { signature: string }[];
}

Create Utilities File

Several functions are shared across all four scripts: loading the wallet, building request headers, making typed API calls, signing and sending transactions, polling for order status, and decoding token account data. Centralizing them in a shared module means each script stays focused on its own logic.

Create src/utils.ts:

src/utils.ts
import {
createKeyPairFromBytes,
getAddressFromPublicKey,
getAddressDecoder,
address,
sendTransactionWithoutConfirmingFactory,
assertIsTransactionWithinSizeLimit,
getTransactionDecoder,
signTransaction,
getSignatureFromTransaction,
} from '@solana/kit';
import type { createSolanaRpc } from '@solana/kit';
import fs from 'fs';
import path from 'path';
import os from 'os';
import type { OrderResponse, OrderStatusResponse } from './types';

type SolanaRpc = ReturnType<typeof createSolanaRpc>;

export function getHeaders(): Record<string, string> {
const h: Record<string, string> = { 'Content-Type': 'application/json' };
const apiKey = process.env.DFLOW_API_KEY;
if (apiKey) h['x-api-key'] = apiKey;
return h;
}

export async function loadWallet() {
const keypairPath = process.env.KEYPAIR_PATH || path.join(os.homedir(), '.config', 'solana', 'id.json');
const secretKey = Uint8Array.from(JSON.parse(fs.readFileSync(keypairPath, 'utf-8')));
const keyPair = await createKeyPairFromBytes(secretKey);
const address = await getAddressFromPublicKey(keyPair.publicKey);
return { keyPair, address };
}

export async function fetchJson<T>(url: string, options?: RequestInit): Promise<T> {
const res = await fetch(url, { headers: getHeaders(), ...options });
if (!res.ok) throw new Error(`API error ${res.status}: ${await res.text()}`);
return res.json() as Promise<T>;
}

export async function signAndSend(
order: OrderResponse,
keyPair: Awaited<ReturnType<typeof createKeyPairFromBytes>>,
rpc: SolanaRpc,
): Promise<string> {
const txBytes = Buffer.from(order.transaction, 'base64');
const transaction = getTransactionDecoder().decode(txBytes);
const signedTx = await signTransaction([keyPair], transaction);
const signature = getSignatureFromTransaction(signedTx);
assertIsTransactionWithinSizeLimit(signedTx);
const sendTx = sendTransactionWithoutConfirmingFactory({ rpc });
await sendTx(signedTx, { commitment: 'confirmed', skipPreflight: false });
return signature as string;
}

export async function waitForOrder(
sig: string,
lastValidBlockHeight: number,
tradeApiUrl: string,
): Promise<OrderStatusResponse> {
while (true) {
const status = await fetchJson<OrderStatusResponse>(
`${tradeApiUrl}/order-status?signature=${sig}&lastValidBlockHeight=${lastValidBlockHeight}`
);
console.log(` Status: ${status.status}`);
if (['closed', 'expired', 'failed'].includes(status.status)) return status;
await new Promise((r) => setTimeout(r, 2_000)); // Wait before next poll to avoid rate-limiting
}
}

export function parseMintAndBalance(accountData: [string, string]): { mint: string; amount: bigint } {
const [base64Data] = accountData;
const data = Buffer.from(base64Data, 'base64');
const mint = getAddressDecoder().decode(data.subarray(0, 32));
const amount = data.readBigUInt64LE(64);
return { mint, amount };
}

// SPL Token programs
export const TOKEN_PROGRAM = address('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
export const TOKEN_2022_PROGRAM = address('TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb');

export async function getWalletTokenAccounts(rpc: SolanaRpc, walletAddress: string) {
const [{ value: legacyAccounts }, { value: token2022Accounts }] = await Promise.all([
rpc.getTokenAccountsByOwner(address(walletAddress), { programId: TOKEN_PROGRAM }, { encoding: 'base64' }).send(),
rpc.getTokenAccountsByOwner(address(walletAddress), { programId: TOKEN_2022_PROGRAM }, { encoding: 'base64' }).send(),
]);
return [...legacyAccounts, ...token2022Accounts];
}

Get Active EPL Events

Create src/events.ts to call the /events endpoint and print a table of upcoming EPL events and their USDC Yes/No markets:

src/events.ts
import { fetchJson } from './utils';
import type { EventsResponse } from './types';

const METADATA_API = process.env.METADATA_API_URL;
const SERIES_TICKER = process.env.SERIES_TICKER;
const USDC_MINT = process.env.USDC_MINT;

// Helpers
function toUsd(price: string | null): string {
if (!price) return ' N/A ';
return `$${Number(price).toFixed(2)}`;
}

function closeDate(ts: number): string {
return new Date(ts * 1000).toLocaleString(undefined, {
month: 'short', day: 'numeric', year: 'numeric',
hour: '2-digit', minute: '2-digit',
});
}

// Main
async function listEvents() {
const url = `${METADATA_API}/api/v1/events?seriesTickers=${SERIES_TICKER}&status=active&withNestedMarkets=true`;
const { events } = await fetchJson<EventsResponse>(url);

if (events.length === 0) {
console.log(`No active events found for series "${SERIES_TICKER}".`);
return;
}

console.log(`\n${SERIES_TICKER}${events.length} active event(s)\n`);

for (const event of events) {
// closeTime lives on the markets, not the event itself
const closeTime = event.markets[0]?.closeTime;

console.log(`┌─ ${event.title}`);
console.log(`│ Ticker: ${event.ticker}`);
if (event.subtitle) console.log(`│ Subtitle: ${event.subtitle}`);
if (closeTime) console.log(`│ Closes: ${closeDate(closeTime)}`);
console.log('│');

for (const market of event.markets) {
// Prefer USDC accounts; fall back to first account available
const acct = market.accounts[USDC_MINT] ?? Object.values(market.accounts)[0];

console.log(`│ ▸ YES label: ${market.yesSubTitle}`);
console.log(`│ Ticker: ${market.ticker}`);
console.log(`│ YES mint: ${acct?.yesMint ?? 'not initialized'}`);
console.log(`│ NO mint: ${acct?.noMint ?? 'not initialized'}`);
console.log(`│ Price (ask): YES ${toUsd(market.yesAsk)} NO ${toUsd(market.noAsk)}`);
console.log('│');
}

console.log('└─────────────────────────────────────────────────────\n');
}
}

listEvents().catch(console.error);

Run the events script to get a list of active events for the series:

tsx --env-file=.env src/events.ts

Expected output (this only shows the first event returned, but the actual results will include all active events):

KXEPLGAME — 31 active event(s)

┌─ Wolverhampton vs Arsenal
│ Ticker: KXEPLGAME-26FEB18WOLARS
│ Subtitle: WOL vs ARS (Feb 18)
│ Closes: Mar 4, 2026, 02:00 PM

│ ▸ YES label: Arsenal
│ Ticker: KXEPLGAME-26FEB18WOLARS-ARS
│ YES mint: EahtAm7FJfTrGdEFvsQbztsadM6ohMqyPcLk1Q6YCRDx
│ NO mint: 4kXWe1ofHoihJfSzTAT7Yecm5vsEuBFEKmrtLzuASLU8
│ Price (ask): YES $0.78 NO $0.23

│ ▸ YES label: Wolverhampton
│ Ticker: KXEPLGAME-26FEB18WOLARS-WOL
│ YES mint: CA7FMbzNTfeR7jkLzF113bBJupKwq98cixaQtc3b3frb
│ NO mint: D7ibW7tu2kvzfbDS78gF5i9UZTye7pqTP63yxYd43No3
│ Price (ask): YES $0.08 NO $0.93

│ ▸ YES label: Tie
│ Ticker: KXEPLGAME-26FEB18WOLARS-TIE
│ YES mint: 4qeSi2JVCbE9VQt1uzTJTpJSKdMFRsqWuvf3UL9fGa2P
│ NO mint: 7GA2eFoEupkSqJaeKrHgS516dipVDfmQ4ZQ2BFdZpdb3
│ Price (ask): YES $0.17 NO $0.85
└─────────────────────────────────────────────────────

┌─ [Other Events]
│ List of the other active EPL events
│ ...
└─────────────────────────────────────────────────────

Record the ticker, yesMint, and noMint values for the market you want to trade to use in the next step.

Buy a Yes/No Token

Each EPL event exposes three independent binary markets, one per possible outcome: Arsenal wins (-ARS), Wolverhampton wins (-WOL), or the match ends in a tie (-TIE).

Each market is a separate Yes/No question. Buying Yes on Arsenal is not the same as buying No on Wolverhampton. A Yes-ARS holder loses on a draw, while a No-WOL holder wins. A draw pays out Yes-TIE holders, but also No-ARS and No-WOL holders. Always pick the market that matches the exact outcome you want to speculate on.

MarketTokenArsenal winsWolverhampton winsDraw
Will Arsenal win?Yes -ARS✅ Wins❌ Loses❌ Loses
Will Arsenal win?No -ARS❌ Loses✅ Wins✅ Wins
Will Wolverhampton win?Yes -WOL❌ Loses✅ Wins❌ Loses
Will Wolverhampton win?No -WOL✅ Wins❌ Loses✅ Wins
Will the match end in a tie?Yes -TIE❌ Loses❌ Loses✅ Wins
Will the match end in a tie?No -TIE✅ Wins✅ Wins❌ Loses

Buying an outcome token is a three-step process:


  1. Request an order from DFlow's Trade API
  2. Sign the returned transaction
  3. Send the signed transaction to Solana

Create the src/buy.ts script to buy a Yes or No outcome token:

src/buy.ts
// src/buy.ts
// Buys a Yes or No outcome token using USDC as the input
import {
createSolanaRpc,
Signature,
} from '@solana/kit';
import type { OrderResponse } from './types';
import { loadWallet, fetchJson, signAndSend, waitForOrder } from './utils';

const TRADE_API = process.env.TRADE_API_URL;
const RPC_URL = process.env.QUICKNODE_RPC_URL;
const USDC_MINT = process.env.USDC_MINT;

const rpc = createSolanaRpc(RPC_URL);

// Main
async function buyOutcomeToken(
outcomeMint: string, // yesMint or noMint from market metadata
usdcAmount: number // USDC to spend (e.g. 1 = $1.00)
) {
const wallet = await loadWallet();

// USDC has 6 decimal places: $1.00 = 1_000_000 base units.
// The DFlow /order `amount` param is the INPUT quantity (USDC to spend).
const amountBaseUnits = usdcAmount * 1_000_000;

console.log(`\nSpending ${usdcAmount} USDC on outcome tokens`);
console.log(` Input (USDC): ${USDC_MINT}`);
console.log(` Output (mint): ${outcomeMint}`);
console.log(` Wallet: ${wallet.address}\n`);

// Step 1: Request an order from DFlow Trade API
const params = new URLSearchParams({
inputMint: USDC_MINT,
outputMint: outcomeMint,
amount: String(amountBaseUnits),
userPublicKey: wallet.address,
slippageBps: 'auto',
dynamicComputeUnitLimit: 'true',
prioritizationFeeLamports: '5000',
});

const order = await fetchJson<OrderResponse>(`${TRADE_API}/order?${params}`);
console.log(`Order received — mode: ${order.executionMode}, expected out: ${order.outAmount} base units`);

// Step 2: Sign and send the transaction
const signature = await signAndSend(order, wallet.keyPair, rpc);

console.log(`\nTransaction submitted: ${signature}`);
console.log(` Explorer: https://explorer.solana.com/tx/${signature}`);

// Step 3: For async orders, poll for fill confirmation
if (order.executionMode === 'async') {
const result = await waitForOrder(signature, order.lastValidBlockHeight, TRADE_API);
if (result.status === 'closed') {
console.log(`\n✅ Order filled! Received ${result.outAmount / 1_000_000} outcome tokens`);
} else if (result.status === 'expired') {
console.log(`\n⚠️ Order expired before being filled. No tokens received.`);
} else {
console.log(`\n⚠️ Order ended with status: ${result.status}`);
}
} else {
// Sync: poll signature statuses until confirmed
for (let i = 0; i < 30; i++) {
await new Promise((r) => setTimeout(r, 1_000));
const { value: statuses } = await rpc.getSignatureStatuses([signature as Signature]).send();
const s = statuses[0];
if (s?.confirmationStatus === 'confirmed' || s?.confirmationStatus === 'finalized') {
if (s.err) {
console.error(`\n❌ Transaction failed on-chain:`, s.err);
} else {
console.log(`\n✅ Sync order confirmed!`);
}
break;
}
}
}
}

const [outcomeMint, amountStr] = process.argv.slice(2);
if (!outcomeMint || !amountStr) {
console.error('Usage: npm run buy -- <outcome-mint-address> <usdc-amount>');
process.exit(1);
}

buyOutcomeToken(outcomeMint, Number(amountStr)).catch(console.error);

Run the script with the yesMint (or noMint) you recorded earlier and the amount of USDC you want to spend:

tsx --env-file=.env src/buy.ts 4qeSi2JVCbE9VQt1uzTJTpJSKdMFRsqWuvf3UL9fGa2P 1

Expected output:

Spending 1 USDC on outcome tokens
Input (USDC): EPjFW...Dt1v
Output (mint): 4qeS...Ga2P
Wallet: F6Yt...8kM9

Order received — mode: async, expected out: 11000000 base units

Transaction submitted: 3WmN...nD8s
Explorer: https://explorer.solana.com/tx/3WmND...nD8s
Status: pending
Status: closed

✅ Order filled! Received 11 outcome tokens

Track Open Positions

A user's prediction market positions live as SPL token balances in their wallet. To display them, fetch all token accounts, identify which mints are outcome tokens, and map each to its market data.

Create src/positions.ts:

src/positions.ts
// src/positions.ts
// Lists all open prediction market positions for a wallet
import { createSolanaRpc } from '@solana/kit';
import type { Market } from './types';
import { loadWallet, fetchJson, parseMintAndBalance, getWalletTokenAccounts } from './utils';

const METADATA_API = process.env.METADATA_API_URL;
const RPC_URL = process.env.QUICKNODE_RPC_URL;

const rpc = createSolanaRpc(RPC_URL);

// Main
async function getPositions(walletAddress: string) {
const tokenAccounts = await getWalletTokenAccounts(rpc, walletAddress);
const heldMints: string[] = [];
const mintToBalance: Record<string, bigint> = {};

for (const { account } of tokenAccounts) {
const { mint, amount } = parseMintAndBalance(account.data as [string, string]);
if (amount > 0n) {
heldMints.push(mint);
mintToBalance[mint] = amount;
}
}

if (heldMints.length === 0) {
console.log('No SPL tokens found in wallet.');
return;
}

console.log(`\nChecking ${heldMints.length} token(s) for prediction market positions...\n`);

const positions: { mint: string; side: 'YES' | 'NO'; market: Market; balance: bigint }[] = [];

// For each held mint, check if it is an outcome token via DFlow Metadata API
for (const mint of heldMints) {
let market: Market;
try {
market = await fetchJson<Market>(`${METADATA_API}/api/v1/market/by-mint/${mint}`);
} catch { continue; }
const allAccts = Object.values(market.accounts);
const side = allAccts.some(a => a.yesMint === String(mint)) ? 'YES' : 'NO';
positions.push({ mint, side, market, balance: mintToBalance[mint]! });
}

if (positions.length === 0) {
console.log('No prediction market positions found.');
return;
}

console.log(`Found ${positions.length} open position(s):\n`);

for (const pos of positions) {
const acct = Object.values(pos.market.accounts)[0];
console.log(`Market: ${pos.market.ticker}`);
console.log(`Title: ${pos.market.title}`);
console.log(`Side: ${pos.side}`);
console.log(`Balance: ${(Number(pos.balance) / 1_000_000).toFixed(6)} tokens`);
console.log(`Status: ${pos.market.status}${pos.market.result ? ` (result: ${pos.market.result})` : ''}`);
const isOpen = acct?.redemptionStatus === 'open';
const sideWon =
(pos.side === 'YES' && pos.market.result === 'yes') ||
(pos.side === 'NO' && pos.market.result === 'no');
const isScalar = !pos.market.result && acct?.scalarOutcomePercent != null;
const redeemable = isOpen && (sideWon || isScalar);
const redeemLabel = redeemable
? '✅ Yes'
: !pos.market.result
? '⏳ Pending (not yet determined)'
: sideWon
? '⏳ Won — redemption not open yet'
: '❌ Lost';
console.log(`Redeemable: ${redeemLabel}`);
console.log();
}
}

loadWallet()
.then(({ address: walletAddress }) => getPositions(walletAddress))
.catch(console.error);

Run the script to get market positions:

tsx --env-file=.env src/positions.ts

Expected output:

Checking 4 token(s) for prediction market positions...

Found 1 open position(s):

Market: KXEPLGAME-26FEB18WOLARS-TIE
Title: Wolverhampton vs Arsenal Winner?
Side: YES
Balance: 5.000000 tokens
Status: active
Redeemable: ⏳ Pending (not yet determined)

Once the match ends and the market is finalized, re-running the script will reflect the settled result. If you backed the correct outcome, the position will show as redeemable:

Checking 4 token(s) for prediction market positions...

Found 1 open position(s):

Market: KXEPLGAME-26FEB18WOLARS-TIE
Title: Wolverhampton vs Arsenal Winner?
Side: YES
Balance: 5.000000 tokens
Status: finalized (result: yes)
Redeemable: ✅ Yes

Redeem Winning Tokens

After a market is determined, winning outcome tokens can be redeemed for their stablecoin payout. The same /order endpoint used for buying handles redemption. DFlow detects that the input is an outcome token and builds a redemption transaction automatically.

Before attempting redemption, three conditions must be true:

  1. The market status is determined or finalized
  2. The settlement mint's redemptionStatus is open (check in market.accounts)
  3. The outcome token matches the winning side (result === "yes" → use yesMint; result === "no" → use noMint)

Create src/redeem.ts:

src/redeem.ts
import { createSolanaRpc } from '@solana/kit';
import type { Market, OrderResponse } from './types';
import { loadWallet, fetchJson, signAndSend, waitForOrder, parseMintAndBalance, getWalletTokenAccounts } from './utils';

const METADATA_API = process.env.METADATA_API_URL;
const TRADE_API = process.env.TRADE_API_URL;
const RPC_URL = process.env.QUICKNODE_RPC_URL;
const USDC_MINT = process.env.USDC_MINT;

const rpc = createSolanaRpc(RPC_URL);

// Main
async function redeemOutcomeTokens(outcomeMint: string) {
const wallet = await loadWallet();

const tokenAccounts = await getWalletTokenAccounts(rpc, wallet.address);
let rawBalance = 0n;
for (const { account } of tokenAccounts) {
const { mint, amount } = parseMintAndBalance(account.data as [string, string]);
if (mint === outcomeMint) { rawBalance = amount; break; }
}
if (rawBalance === 0n) {
console.error('No balance found for this mint in your wallet.');
process.exit(1);
}
const tokenAmount = Number(rawBalance);

// Step 1: Verify the token is redeemable
const market = await fetchJson<Market>(`${METADATA_API}/api/v1/market/by-mint/${outcomeMint}`);
const acct = Object.values(market.accounts)[0];
if (!acct) throw new Error('No account info found for this market.');

const isWinningMint =
(market.result === 'yes' && acct.yesMint === outcomeMint) ||
(market.result === 'no' && acct.noMint === outcomeMint);
const isScalar = !market.result && acct.scalarOutcomePercent !== null;

if (!isWinningMint && !isScalar) {
console.log(`\n⚠️ This outcome token cannot be redeemed.`);
console.log(` Market result: ${market.result ?? 'not yet determined'}`);
console.log(` Market status: ${market.status}`);
return;
}

if (acct.redemptionStatus !== 'open') {
console.log(`\n⚠️ Redemption window is not open yet.`);
console.log(` redemptionStatus: ${acct.redemptionStatus}`);
return;
}

console.log(`\nRedeeming ${tokenAmount} tokens from market: ${market.ticker}`);

if (isScalar && acct.scalarOutcomePercent !== null) {
const yesPct = (acct.scalarOutcomePercent / 100).toFixed(2);
const noPct = ((10_000 - acct.scalarOutcomePercent) / 100).toFixed(2);
console.log(` Scalar market — YES payout: ${yesPct}% NO payout: ${noPct}%`);
}

// Step 2: Request a redemption order (same /order endpoint as buying)
const params = new URLSearchParams({
inputMint: outcomeMint,
outputMint: USDC_MINT,
amount: String(tokenAmount),
userPublicKey: wallet.address,
});

const order = await fetchJson<OrderResponse>(`${TRADE_API}/order?${params}`);

// Step 3: Sign and send
const signature = await signAndSend(order, wallet.keyPair, rpc);

console.log(`\nTransaction submitted: ${signature}`);
console.log(` Explorer: https://explorer.solana.com/tx/${signature}`);

// Step 4: Monitor until closed
const result = await waitForOrder(signature, order.lastValidBlockHeight, TRADE_API);
if (result.status === 'closed') {
console.log(`\n✅ Redemption complete! Received ${result.outAmount / 1_000_000} USDC`);
} else {
console.log(`\n⚠️ Redemption ended with status: ${result.status}`);
}
}

const [mint] = process.argv.slice(2);
if (!mint) {
console.error('Usage: npm run redeem -- <outcome-mint-address>');
process.exit(1);
}

redeemOutcomeTokens(mint).catch(console.error);

Run it after a market has been determined and the redemption window is open:

tsx --env-file=.env src/redeem.ts 4qeSi2JVCbE9VQt1uzTJTpJSKdMFRsqWuvf3UL9fGa2P

Expected output:

Redeeming 5000000 tokens from market: KXEPLGAME-26FEB18WOLARS-TIE

Transaction submitted: 2sF5df4KwTsKx4V8Qg3GLAQFRDRtctnVUaQbB9n3cjyvyHxKKr6RPHWu38HRvMozd7HGi5JvbLw4amXMQeYvLuL5
Explorer: https://explorer.solana.com/tx/2sF5df4KwTsKx4V8Qg3GLAQFRDRtctnVUaQbB9n3cjyvyHxKKr6RPHWu38HRvMozd7HGi5JvbLw4amXMQeYvLuL5
Status: pending
Status: closed

✅ Redemption complete! Received 5 USDC

That's the complete flow!

You went from discovering a live market through the DFlow Metadata API all the way to redeeming your winnings for USDC, with buying outcome tokens and tracking positions handled entirely on Solana. Each step is just a single API call or signed transaction. No off-chain trading infrastructure required.

Production Checklist


KYC is mandatory for production apps

Any application that lets end users trade prediction markets through DFlow must integrate Proof for identity verification. Proof is DFlow's designated KYC provider and a prerequisite for going live. Kalshi's CFTC-regulated status means user verification is a compliance requirement, not an optional feature.

Before deploying a prediction market application to mainnet users, work through this checklist:

API Keys and Endpoints

  • Replace dev endpoints (dev-quote-api.dflow.net, dev-prediction-markets-api.dflow.net) with production URLs and API Keys provided by DFlow
  • Pass the API key via x-api-key header on every request
  • Apply rate-limit-aware retry logic with exponential backoff

Market Lifecycle Edge Cases

  • Markets can be suspended temporarily; check status before showing a "trade" CTA
  • closeTime marks when trading closes. Display this prominently so users don't attempt to buy after the market closes
  • result can take time to populate after a market closes; poll until status reaches determined

Frequently Asked Questions

How do DFlow's async prediction market trades work differently from a regular Solana swap?

Unlike standard atomic swaps (one transaction, immediate confirmation), prediction market orders on DFlow use an async Concurrent Liquidity Program (CLP) model. You submit one transaction expressing a "trade intent," and one or more liquidity providers fill it in separate transactions. Poll GET /order-status?signature=<txSig> until the status reaches closed to confirm your outcome tokens have been minted.

Can I use any token to buy a Yes/No outcome token, or does it have to be USDC?

You can start from any Solana spot token. The DFlow /order endpoint automatically routes your input through an intermediate swap to the market's settlement mint (USDC or CASH) before acquiring the outcome token. However, using USDC or CASH directly as your input is the fastest path. It skips the extra swap leg and saves roughly 50 ms of latency.

What happens to my outcome tokens if I backed the losing side?

Losing outcome tokens expire worthless after settlement. Only the winning side is redeemable for the full settlement value ($1.00 per token = 10,000 on DFlow's internal scale). In scalar markets, both YES and NO tokens are redeemable at proportional payouts based on the scalarOutcomePercent field. Always confirm redemptionStatus is open before attempting a redemption transaction.

What is the difference between a market's status being determined vs finalized?

determined means Kalshi has identified the winning outcome but settlement has not fully propagated yet. finalized means the market is fully settled and all positions have been paid out. DFlow marks outcome tokens as redeemable once the market reaches determined and the settlement mint's redemptionStatus is open. You should wait for redemptionStatus: "open" rather than relying solely on market status.

Do I need an API key to get started with DFlow?

Not for development. Both the dev Trade API (https://dev-quote-api.dflow.net) and the dev Metadata API (https://dev-prediction-markets-api.dflow.net) work without authentication. Rate limits apply on dev endpoints. For production traffic, you need an API key (pass it as x-api-key header).

What compliance requirements apply if I ship a prediction market app to users?

Because positions are backed by Kalshi — a CFTC-regulated exchange — any production application must integrate Proof (a KYC/identity provider) to meet Kalshi compliance requirements. DFlow's documentation specifies this as a hard requirement before going live. Failing to integrate KYC compliance before launch may violate Kalshi's terms of service.

Wrapping Up

You've built a complete end-to-end prediction market flow on Solana. Starting from the DFlow Metadata API, you navigated the Series → Events → Markets hierarchy to find live EPL contracts, fetched events and Yes/No prices, bought outcome tokens with a single signed transaction, mapped wallet balances back to market positions, and redeemed winning tokens for stablecoins after settlement.

The same patterns here apply to any Kalshi-backed market DFlow exposes: sports, economics, or politics. You now have a solid foundation to build out a full prediction market application with Solana at the core.

Resources


We ❤️ Feedback!

Let us know if you have any feedback or requests for new topics. We'd love to hear from you.

Share this guide