GuidesTrading

Manage positions

Redeem, merge, split and convert outcome tokens, check balances, and call the contracts directly.

A position is a balance of one outcome's tokens. Positions live on-chain, so managing them means sending a transaction from your wallet. The SDKs do this for you through the OrderBuilder you already use for orders. Each operation costs BNB gas, so keep some BNB in the wallet that sends it.

Positions and outcome tokens explains what each operation does. This page shows how to run them.

Find your positions

Two endpoints list a wallet's positions:

EndpointReturns
Get positionsYour own positions. Needs a JWT (see Authentication).
Get positions by addressAny wallet's positions. Needs only your API key.

Each position has the market, the outcome it holds and its amount in wei. It also has valueUsd, averageBuyPriceUsd and pnlUsd. The fields the operations below need are:

Operation inputWhere to find it
conditionIdmarket.conditionId
indexSetoutcome.indexSet: 1 for Yes, 2 for No
isNegRisk, isYieldBearingmarket.isNegRisk, market.isYieldBearing
amountamount, in wei

Filter with isResolved=true to list positions in resolved markets. Positions in outcomes that lost are left out unless you add includeLost=true. Pages hold up to 100 positions. Pass the response's cursor as after to get the next page.

Approvals

Each operation moves tokens through a contract that needs your approval first. Splitting spends USDT, and merging, redeeming and converting move outcome tokens. The SDKs can set exactly the approvals one operation needs. See Approvals.

Redeem winning positions

Once a market resolves, each share of the winning outcome redeems for 1 USDT. A binary market can also resolve 50-50, which pays 0.50 USDT per share of either outcome. Redeeming burns the tokens and pays out the USDT. There is no deadline to redeem. See Resolution for when a market resolves.

This example redeems every winning position of your wallet:

// Redeem every winning position of your wallet in resolved markets.
import { ChainId, OrderBuilder } from "@predictdotfun/sdk";
import { Wallet } from "ethers";

const apiKey = "YOUR_API_KEY";
const wallet = new Wallet(process.env.WALLET_PRIVATE_KEY!);
const orderBuilder = await OrderBuilder.make(ChainId.BnbMainnet, wallet);

// positions in resolved markets. Lost positions are left out by default.
const response = await fetch(
  `https://api.predict.fun/v1/positions/${wallet.address}?isResolved=true&first=100`,
  { headers: { "x-api-key": apiKey } },
);
if (!response.ok) throw new Error(`Predict API error: ${response.status}`);
const { data: positions } = await response.json();

for (const { market, outcome, amount } of positions) {
  const result = await orderBuilder.redeemPositions({
    conditionId: market.conditionId,
    indexSet: outcome.indexSet,
    isNegRisk: market.isNegRisk,
    isYieldBearing: market.isYieldBearing,
    // negative-risk markets redeem a stated amount, the whole position here.
    amount: market.isNegRisk ? BigInt(amount) : undefined,
  });
  console.log(market.question, result.success ? "redeemed" : result.cause);
}

In a negative-risk market, you redeem a stated amount. In other markets, redeeming always redeems your whole balance of that outcome, so the amount is left out.

Merge positions

Merging turns equal amounts of Yes and No back into USDT: 1 Yes share plus 1 No share returns 1 USDT. It doesn't need the market to resolve, so it is how you exit a position you hold on both sides.

import { ChainId, OrderBuilder } from "@predictdotfun/sdk";
import { Wallet } from "ethers";

const wallet = new Wallet(process.env.WALLET_PRIVATE_KEY!);
const orderBuilder = await OrderBuilder.make(ChainId.BnbMainnet, wallet);

// merge 10 Yes and 10 No shares back into 10 USDT.
const result = await orderBuilder.mergePositions({
  conditionId: "CONDITION_ID", // the market's conditionId
  amount: 10n * 10n ** 18n, // 10 shares of each outcome, in wei
  isNegRisk: false,
  isYieldBearing: true,
});
if (!result.success) throw result.cause ?? new Error("Merge failed");

Split USDT into positions

Splitting is the reverse of merging. Each USDT you split becomes 1 Yes share and 1 No share.

import { ChainId, OrderBuilder } from "@predictdotfun/sdk";
import { Wallet } from "ethers";

const wallet = new Wallet(process.env.WALLET_PRIVATE_KEY!);
const orderBuilder = await OrderBuilder.make(ChainId.BnbMainnet, wallet);

// split 10 USDT into 10 Yes and 10 No shares.
const result = await orderBuilder.splitPositions({
  conditionId: "CONDITION_ID", // the market's conditionId
  amount: 10n * 10n ** 18n, // 10 USDT, in wei
  isNegRisk: false,
  isYieldBearing: true,
});
if (!result.success) throw result.cause ?? new Error("Split failed");

Convert positions

Converting works only in negative-risk categories, where several markets share one winner. It burns No positions and gives back the equivalent in Yes positions and USDT. For each amount you convert, you give amount No shares of each selected market, and you receive:

  • amount Yes shares of every other market in the category.
  • amount × (number of selected No positions − 1) USDT.

If the category has a conversion fee, it is taken from both the Yes shares and the USDT. A category needs more than one market to convert. See Negative-risk markets for why this holds its value.

Two inputs come from the API:

  • negRiskOnChainId: the category's field of that name, from Get categories. It is a 32-byte hex string, not the numeric id the API uses for categories and markets.
  • indexSet: a bitmask that selects which No positions to convert. For each one, set the bit at that market's questionIndex. Markets carry their questionIndex in categories and positions.
import { ChainId, OrderBuilder } from "@predictdotfun/sdk";
import { Wallet } from "ethers";

const wallet = new Wallet(process.env.WALLET_PRIVATE_KEY!);
const orderBuilder = await OrderBuilder.make(ChainId.BnbMainnet, wallet);

// convert the No positions of the questions at index 0 and 2.
const questionIndexes = [0, 2];
const indexSet = questionIndexes.reduce(
  (set, index) => set | (1n << BigInt(index)),
  0n,
);

const result = await orderBuilder.convertPositions({
  negRiskOnChainId: "NEG_RISK_ON_CHAIN_ID", // the category's negRiskOnChainId
  indexSet,
  amount: 10n * 10n ** 18n, // 10 shares of each No position, in wei
  isYieldBearing: true,
});
if (!result.success) throw result.cause ?? new Error("Convert failed");

Check your USDT balance

import { ChainId, OrderBuilder } from "@predictdotfun/sdk";
import { formatUnits, Wallet } from "ethers";

const wallet = new Wallet(process.env.WALLET_PRIVATE_KEY!);
const orderBuilder = await OrderBuilder.make(ChainId.BnbMainnet, wallet);

// the wallet's USDT balance, in wei. USDT on BNB Chain has 18 decimals.
const balance = await orderBuilder.balanceOf();
console.log(`${formatUnits(balance, 18)} USDT`);

Call the contracts directly

The OrderBuilder exposes every contract it uses. You can call any contract function through it, with the wallet you gave to OrderBuilder.make as the sender.

import { ChainId, OrderBuilder } from "@predictdotfun/sdk";
import { Wallet } from "ethers";

const wallet = new Wallet(process.env.WALLET_PRIVATE_KEY!);
const orderBuilder = await OrderBuilder.make(ChainId.BnbMainnet, wallet);

// the contracts exist only when OrderBuilder.make gets a signer.
if (!orderBuilder.contracts) throw new Error("No signer was given to OrderBuilder.make");

// each entry has the ethers `contract` and its `codec`, an ethers Interface.
const { contract: usdt } = orderBuilder.contracts.USDT;
const balance = await usdt.balanceOf(wallet.address);

The SDKs also export the contract addresses and ABIs, so you can use them with your own client:

import {
  // supported chains and contract addresses
  ChainId,
  AddressesByChainId,
  // ABIs
  CTFExchangeAbi,
  NegRiskCtfExchangeAbi,
  NegRiskAdapterAbi,
  ConditionalTokensAbi,
  YieldBearingConditionalTokensAbi,
  ERC20Abi,
  // contract types
  type CTFExchange,
  type ConditionalTokens,
  type NegRiskAdapter,
  type NegRiskCtfExchange,
  type ERC20,
} from "@predictdotfun/sdk";
console.log(AddressesByChainId[ChainId.BnbMainnet].CTF_EXCHANGE);

If you call the contracts without the SDKs, these are the functions behind each operation. The contract addresses are on Deployed contracts.

OperationStandard markets: ConditionalTokens or YieldBearingConditionalTokensNegative-risk markets: NegRiskAdapter or YieldBearingNegRiskAdapter
SplitsplitPosition(USDT, 0x0, conditionId, [1, 2], amount)splitPosition(conditionId, amount)
MergemergePositions(USDT, 0x0, conditionId, [1, 2], amount)mergePositions(conditionId, amount)
RedeemredeemPositions(USDT, 0x0, conditionId, [indexSet])redeemPositions(conditionId, [yesAmount, noAmount])
ConvertNot availableconvertPositions(negRiskOnChainId, indexSet, amount)

0x0 is the zero bytes32 parent collection, and [1, 2] are the index sets of Yes and No. To redeem one outcome in a negative-risk market, set the other outcome's amount to 0. All amounts are in wei. USDT on BNB Chain has 18 decimals.

On this page