> ## Documentation Index
> Fetch the complete documentation index at: https://pond.dflow.net/llms.txt
> Use this file to discover all available pages before exploring further.

# Prediction Market Quickstart

> End-to-end recipe for building a Kalshi prediction market UI with DFlow

<Info>
  During development, you can use the [developer endpoints](/get-started/endpoints) without an API key. For production use, [request an API key](/get-started/api-key) for higher rate limits.
</Info>

<Steps>
  <Step title="Set up">
    Imports, env config, and the wallet/connection used across every step.

    <AccordionGroup>
      <Accordion title="Imports, env, and wallet">
        ```typescript theme={null}
        import "dotenv/config";

        import { TOKEN_2022_PROGRAM_ID, TOKEN_PROGRAM_ID } from "@solana/spl-token";
        import { Connection, Keypair, VersionedTransaction } from "@solana/web3.js";
        import bs58 from "bs58";

        const DFLOW_METADATA_API_URL = process.env.DFLOW_METADATA_API_URL ?? "https://dev-prediction-markets-api.dflow.net";
        const DFLOW_TRADE_API_URL = process.env.DFLOW_TRADE_API_URL ?? "https://dev-quote-api.dflow.net";
        const DFLOW_PROOF_API_URL = process.env.DFLOW_PROOF_API_URL ?? "https://proof.dflow.net";
        const DFLOW_API_KEY = process.env.DFLOW_API_KEY; // optional; not needed for dev endpoints
        const DFLOW_SETTLEMENT_MINT = process.env.DFLOW_SETTLEMENT_MINT ?? "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"; // USDC (markets also support CASH)
        const SOLANA_RPC_URL = process.env.SOLANA_RPC_URL ?? "https://api.mainnet-beta.solana.com";

        const connection = new Connection(SOLANA_RPC_URL, "confirmed");
        const keypair = Keypair.fromSecretKey(
          bs58.decode(process.env.SOLANA_PRIVATE_KEY ?? "")
        );

        const headers: HeadersInit = {};
        if (DFLOW_API_KEY) headers["x-api-key"] = DFLOW_API_KEY;
        ```
      </Accordion>
    </AccordionGroup>
  </Step>

  <Step title="Discover Kalshi markets">
    Use the Metadata API to fetch Kalshi events. Filter by series ticker, category, or tag to find relevant markets.

    <AccordionGroup>
      <Accordion title="Discover Kalshi markets">
        ```typescript theme={null}
        // Fetch active Kalshi BTC markets (series ticker prefix: KXBTC)
        const response = await fetch(
          `${DFLOW_METADATA_API_URL}/api/v1/events?withNestedMarkets=true&seriesTickers=KXBTC&status=active`,
          { headers }
        );

        const data = await response.json();
        const events = data.events;

        // Pick a market to trade
        const targetEvent = events[0];
        const targetMarket = targetEvent?.markets?.[0];

        const { yesMint, noMint } = targetMarket.accounts[DFLOW_SETTLEMENT_MINT];
        ```
      </Accordion>
    </AccordionGroup>

    [Full filtering options.](/prediction-markets/recipes/find-markets)
  </Step>

  <Step title="Verify KYC">
    Kalshi requires KYC before a wallet can receive outcome tokens. Check verification status using the Proof API before submitting a trade.

    <AccordionGroup>
      <Accordion title="Verify KYC">
        ```typescript theme={null}
        const { verified } = await fetch(
          `${DFLOW_PROOF_API_URL}/verify/${keypair.publicKey.toBase58()}`
        ).then((r) => r.json());

        if (!verified) {
          // Redirect the user to complete KYC at https://dflow.net/proof
          // then send them back to your app via a redirect_uri
          throw new Error("KYC verification required");
        }
        ```
      </Accordion>
    </AccordionGroup>

    <Note>
      **Unverified users can still get quotes.** Omit `userPublicKey` from the `/order` request to return pricing without requiring verification. This lets users browse and preview markets before completing KYC.
    </Note>

    <Warning>
      KYC alone is not sufficient. You must also implement **geoblocking** to restrict access in jurisdictions where prediction markets are not permitted. [Prediction Market Compliance.](/prediction-markets/prediction-market-compliance)
    </Warning>

    Cache the `/verify` response on your backend with a short TTL rather than calling it on every trade. [Full Proof integration guide.](/prediction-markets/kyc)
  </Step>

  <Step title="Buy YES or NO outcome tokens">
    Trading is a single `/order` call. Set `inputMint` to USDC (or CASH) and `outputMint` to either `yesMint` or `noMint`.

    <AccordionGroup>
      <Accordion title="Buy YES or NO outcome tokens">
        ```typescript theme={null}
        const params = new URLSearchParams({
          userPublicKey: keypair.publicKey.toBase58(),
          inputMint: DFLOW_SETTLEMENT_MINT,  // USDC or CASH depending on the market
          outputMint: yesMint,               // or noMint for NO
          amount: "100000",                  // 0.1 units (6 decimals)
        });

        const order = await fetch(`${DFLOW_TRADE_API_URL}/order?${params}`, { headers }).then((r) => r.json());
        ```
      </Accordion>
    </AccordionGroup>

    <Tip>
      You can trade from any token, not just USDC. The `/order` endpoint automatically routes through the settlement mint. Using USDC directly is the lowest latency trade path.
    </Tip>
  </Step>

  <Step title="Sign and send the transaction">
    <AccordionGroup>
      <Accordion title="Sign and send the transaction">
        ```typescript theme={null}
        if (!order.transaction) {
          throw new Error(`Order request failed: ${JSON.stringify(order)}`);
        }

        const txBuffer = Buffer.from(order.transaction, "base64");
        const transaction = VersionedTransaction.deserialize(txBuffer);

        transaction.sign([keypair]);
        const signature = await connection.sendRawTransaction(transaction.serialize(), {
          skipPreflight: true,
          maxRetries: 2,
        });
        console.log(`  Tx: https://orbmarkets.io/tx/${signature}`);

        await connection.confirmTransaction({
          signature,
          blockhash: transaction.message.recentBlockhash,
          lastValidBlockHeight: order.lastValidBlockHeight,
        });
        ```
      </Accordion>
    </AccordionGroup>
  </Step>

  <Step title="Track positions">
    After trading, the user holds YES or NO outcome tokens. Use `filter_outcome_mints` to identify which wallet mints are outcome tokens.

    <AccordionGroup>
      <Accordion title="Track positions">
        ```typescript theme={null}
        const [splAccounts, token2022Accounts] = await Promise.all([
          connection.getParsedTokenAccountsByOwner(keypair.publicKey, { programId: TOKEN_PROGRAM_ID }),
          connection.getParsedTokenAccountsByOwner(keypair.publicKey, { programId: TOKEN_2022_PROGRAM_ID }),
        ]);

        const allMints = [...splAccounts.value, ...token2022Accounts.value]
          .map((a) => a.account.data.parsed.info)
          .filter((info) => Number(info.tokenAmount.amount) > 0)
          .map((info) => info.mint);

        // Step 1: filter wallet mints down to outcome tokens
        const filterRes = await fetch(`${DFLOW_METADATA_API_URL}/api/v1/filter_outcome_mints`, {
          method: "POST",
          headers: { ...headers, "Content-Type": "application/json" },
          body: JSON.stringify({ addresses: allMints }),
        }).then((r) => r.json());

        const outcomeMints: string[] = filterRes.outcomeMints ?? [];

        if (outcomeMints.length === 0) {
          console.log("No outcome tokens in wallet.");
        } else {
          // Step 2: fetch full market objects for each outcome mint
          const batchRes = await fetch(`${DFLOW_METADATA_API_URL}/api/v1/markets/batch`, {
            method: "POST",
            headers: { ...headers, "Content-Type": "application/json" },
            body: JSON.stringify({ mints: outcomeMints }),
          }).then((r) => r.json());

          batchRes.markets.forEach((market: any) => {
            const accounts = market.accounts[DFLOW_SETTLEMENT_MINT];
            const position = allMints.includes(accounts.yesMint) ? "YES"
              : allMints.includes(accounts.noMint) ? "NO"
              : "NONE";
            console.log(market.ticker, position);
          });
        }
        ```
      </Accordion>
    </AccordionGroup>

    [Full portfolio view with balances and market metadata.](/prediction-markets/recipes/track-positions)
  </Step>

  <Step title="Redeem after settlement">
    Once the market is determined, check `market.result` and redeem by selling the outcome token back to USDC. For Kalshi markets, also verify `redemptionStatus === "open"` before redeeming.

    <AccordionGroup>
      <Accordion title="Redeem after settlement">
        ```typescript theme={null}
        const market = await fetch(
          `${DFLOW_METADATA_API_URL}/api/v1/market/by-mint/${yesMint}`,
          { headers }
        ).then((r) => r.json());

        const isDetermined = market.status === "determined" || market.status === "finalized";
        const settlementAccount = market.accounts?.[DFLOW_SETTLEMENT_MINT];
        const isRedemptionOpen = settlementAccount?.redemptionStatus === "open";

        if (!isDetermined) {
          console.log("Not yet determined:", market.status);
        } else if (!isRedemptionOpen) {
          console.log("Redemption not yet funded");
        } else {
          const result = market.result; // "yes" or "no"
          const redeemMint = result === "yes" ? settlementAccount.yesMint : settlementAccount.noMint;

          // Sell outcome token → USDC (same /order pattern as buying)
          const redeemParams = new URLSearchParams({
            userPublicKey: keypair.publicKey.toBase58(),
            inputMint: redeemMint,
            outputMint: DFLOW_SETTLEMENT_MINT,
            amount: userOutcomeTokenBalance,
          });

          const redeemOrder = await fetch(`${DFLOW_TRADE_API_URL}/order?${redeemParams}`, { headers }).then((r) => r.json());
          // Sign and send using the same pattern as Step 4
        }
        ```
      </Accordion>
    </AccordionGroup>

    <Note>
      Some Kalshi markets have scalar (non-binary) outcomes where both YES and NO tokens are partially redeemable. [Full redemption flow.](/prediction-markets/recipes/redeem-outcome-tokens)
    </Note>
  </Step>
</Steps>

## Related Resources

<CardGroup cols={2}>
  <Card title="Find Markets" href="/prediction-markets/recipes/find-markets" icon="search">
    Advanced market discovery: filter by category, tag, and status.
  </Card>

  <Card title="Track User Positions" href="/prediction-markets/recipes/track-positions" icon="wallet">
    Full portfolio view with balances and market metadata.
  </Card>

  <Card title="Monitor Market Lifecycle" href="/prediction-markets/recipes/monitor-market-lifecycle" icon="clock">
    Track status changes and know when redemption opens.
  </Card>

  <Card title="Redeem Outcome Tokens" href="/prediction-markets/recipes/redeem-outcome-tokens" icon="check-circle">
    Complete redemption flow including scalar outcome payouts.
  </Card>
</CardGroup>

## API Routes

* [`GET /api/v1/events`](/resources/metadata-api/events/events): fetch Kalshi prediction market events and nested markets
* [`POST /api/v1/filter_outcome_mints`](/resources/metadata-api/markets/filter-outcome-mints): identify outcome token mints in a wallet
* [`GET /order`](/resources/trading-api/order/order): get a quote and signed transaction for any trade
