# Bot Trading Guide — Buy / Sell / Redeem

Developer reference for wiring a bot against the `MidasMarket` contract. Covers the three on-chain trade flows: **buy**, **sell**, **redeem**.

The `MidasMarket` ABI is required for every example below — request the latest copy from the team along with your testnet RPC URL and a funded wallet.

### Setup <a href="#setup" id="setup"></a>

```ts
import { Contract, JsonRpcProvider, Wallet, parseUnits } from 'ethers';

const ERC20_ABI = [
  'function approve(address spender, uint256 amount) returns (bool)',
  'function allowance(address owner, address spender) view returns (uint256)',
  'function decimals() view returns (uint8)',
  'function balanceOf(address account) view returns (uint256)'
];

const ERC1155_ABI = [
  'function balanceOf(address account, uint256 id) view returns (uint256)',
  'function getTokenId(address market, uint256 outcomeIndex) view returns (uint256)'
  // NOTE: outcome shares are soulbound — setApprovalForAll / safeTransferFrom revert
  //       for user-to-user transfers. The market burns shares directly.
];

// MidasMarket — only the surface a trading bot needs (buy / sell / redeem,
// quotes, status, fee inputs, events, and custom errors).
const MIDAS_MARKET_ABI = [
  // --- Trading ---
  'function buy(uint8[] outcomes, uint256[] amounts, uint256 maxCost) payable',
  'function sell(uint8[] outcomes, uint256[] amounts, uint256 minReturn)',
  'function redeem()',

  // --- Quotes (LSLMSR base — does NOT include fees) ---
  'function getOutcomePurchaseCost(uint8[] outcomes, uint256[] amounts) view returns (uint256)',
  'function getOutcomeSaleReturn(uint8[] outcomes, uint256[] amounts) view returns (uint256)',
  'function getPrices() view returns (uint256[])',

  // --- State reads ---
  'function getStatus() view returns (uint8)', // 0=PENDING 1=CANCELLED 2=ACTIVE 3=PAUSED 4=RESOLVED 5=CLOSED 6=UPDATE_REQUIRED 7=VOIDED 8=VOIDED_CLOSED
  'function getCollateralToken() view returns (address)',
  'function getOutcomeCount() view returns (uint8)',
  'function getRedeemableAmountPerShare() view returns (uint256)',
  'function MARKET_OUTCOME() view returns (address)',

  // --- Market data (fee bps, timestamps, collateral) used by buy/sell helpers below.
  // Field order MUST match IMidasMarket.MarketData exactly — guessing the order
  // silently mis-decodes creatorFeeBps and you'll trip ReturnBelowMinimum on sell.
  'function getMarket() view returns (tuple(' +
    'address resolver,' +
    'uint40 expiresAt,' +
    'uint40 startsAt,' +
    'uint16 creatorFeeBps,' +
    'address creator,' +
    'uint40 resolvedAt,' +
    'uint16 protocolFeeBpsOverride,' +
    'uint8 outcomeCount,' +
    'uint8 winningOutcome,' +
    'uint8 status,' +
    'bool overrideProtocolFeeBps,' +
    'address collateralToken,' +
    'address resolvedBy,' +
    'uint256 initialSharesPerOutcome,' +
    'uint256 collateralAmount,' +
    'uint256 redeemableAmountPerShare,' +
    'uint256 resolverFee,' +
    'tuple(uint256 T0, uint256 alpha0Bps, uint256 T1, uint256 alpha1Bps,' +
    '      uint256 T2, uint256 alpha2Bps, uint256 c1_fp, uint256 c2_fp) alphaConfig' +
  '))',
];

const provider = new JsonRpcProvider(process.env.EVM_RPC_URL);
const wallet = new Wallet(process.env.BOT_PRIVATE_KEY!, provider);

function getMarket(address: string) {
  return new Contract(address, MIDAS_MARKET_ABI, wallet);
}

```

Required env: `EVM_RPC_URL`, `EVM_CHAIN_ID`, `BOT_PRIVATE_KEY`.

### Quickstart <a href="#quickstart" id="quickstart"></a>

End-to-end: fetch a tradeable market, quote, buy 1 share of outcome 0. Assumes the `Setup` block above has run. **For a real bot, also account for fees and the native-collateral path** — see the [Buy](/midaspredict/developers/bot-trading-guide-buy-sell-redeem.md#buy) section.

> **Share units == collateral units.** A "whole share" is `10 ** collateralDecimals`, not `1e18`. For USDC collateral that's `10n ** 6n`; for zkLTC it's `10n ** 18n`. See [Share denomination](#share-denomination).

```ts
const API_BASE = 'https://predict-testnet-api.midashand.xyz/api';

// 1. Find a tradeable market
const res = await fetch(`${API_BASE}/markets?status=open&limit=1`);
const { data } = (await res.json()) as {
  data: { items: Array<{ market: string; collateralToken: string }> };
};
const { market: marketAddress, collateralToken } = data.items[0];

// 2. Quote 1 share of outcome 0 (LSLMSR base cost — does NOT include fees)
const market = getMarket(marketAddress);
const erc20 = new Contract(collateralToken, ERC20_ABI, wallet);
const collateralDecimals: number = await erc20.decimals();
const oneShare = 10n ** BigInt(collateralDecimals); // share scale == collateral scale
const amounts = [oneShare];
const baseQuote: bigint = await market.getOutcomePurchaseCost([0], amounts);

// 3. Add fees + slippage to derive maxCost (see Buy section for fee math)
const maxCost = (baseQuote * 11_000n) / 10_000n; // ~10% headroom; tighten in production

// 4. Approve collateral, then buy. Native-collateral markets require msg.value instead.
await (await erc20.approve(marketAddress, maxCost)).wait();
const tx = await market.buy([0], amounts, maxCost);
console.log('bought:', (await tx.wait()).hash);
```

For sell/redeem, slippage handling, fee math, native collateral, and event subscriptions, see the dedicated sections below.

## Market Data <a href="#market-data" id="market-data"></a>

### Overview <a href="#overview" id="overview"></a>

Market metadata is exposed via the backend REST API. **No authentication required** for read endpoints — bots can pull markets anonymously.

Base URL: `https://predict-testnet-api.midashand.xyz/api`

What the backend gives you:

* Paginated, filterable list of markets (status, category, collateral, sort, expiry window).
* Cached metadata: title, outcomes, collateral, volume, TVL, expiry, status.

### Fetching Markets <a href="#fetching-markets" id="fetching-markets"></a>

There are two market families. Both terminate as on-chain `MidasMarket` contracts, so the [Trading](/midaspredict/developers/bot-trading-guide-buy-sell-redeem.md#trading) flows (buy / sell / redeem) are identical for both — only discovery differs.

| Family                            | Source                                                | Examples                                                      | Discovery endpoint     |
| --------------------------------- | ----------------------------------------------------- | ------------------------------------------------------------- | ---------------------- |
| **Standard markets**              | User-created via the app                              | "Will X win the election?", "Will BTC hit $100k by year-end?" | `GET /markets`         |
| **Quick markets** (daily markets) | Auto-created at fixed cycles for each supported asset | "BTC Up or Down 15m", "LTC Up or Down 1h"                     | `GET /daily-markets/*` |

A bot that wants to trade either kind hits the matching discovery endpoint, then uses the returned `marketAddress` (or `market`) for buy/sell/redeem against the contract.

#### Standard markets — `GET /markets` <a href="#standard-markets--get-markets" id="standard-markets--get-markets"></a>

Useful query params for a bot:

| Param             | Values             | Notes                                                                                                              |
| ----------------- | ------------------ | ------------------------------------------------------------------------------------------------------------------ |
| `status`          | `open` \| `closed` | `open` = ACTIVE markets (tradeable: `buy` / `sell`). `closed` = markets ready to redeem (CLOSED / VOIDED\_CLOSED). |
| `collateralToken` | hex address(es)    | Filter to markets denominated in your collateral. Repeat for multiple.                                             |
| `page` / `limit`  | int / 1–100        | Paginate. Default 20.                                                                                              |

Response:

```json
{
  "items": [ /* Market[] */ ],
  "page": 1,
  "limit": 20,
  "total": 137,
  "favoriteMarketIds": []
}
```

Bot-relevant fields on each `Market`:

| Field                                 | Type                                                                          | Use                                                                                                      |
| ------------------------------------- | ----------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------- |
| `market`                              | string (hex)                                                                  | **Contract address** — pass to `getMarket(address)` for buy/sell/redeem.                                 |
| `status`                              | string (`ACTIVE` \| `CLOSED` \| `RESOLVED` \| `VOID` \| `VOID_CLOSED` \| ...) | DB cache; re-check on-chain via `getStatus()` before trading.                                            |
| `outcomeCount`                        | int                                                                           | Number of outcomes (2 for binary).                                                                       |
| `outcomes`                            | `[{ id, title }]`                                                             | Display names per outcome index.                                                                         |
| `collateralToken`                     | string (hex)                                                                  | ERC20 to approve before `buy` (or send as `msg.value` if it's the native wrapper).                       |
| `startsAt` / `expiresAt`              | string (unix sec)                                                             | Trading window. **Trades revert before `startsAt` even when status is ACTIVE.**                          |
| `initialSharesPerOutcome`             | string                                                                        | Initial share supply seeded by creator.                                                                  |
| `tvl`, `volume_24h`, `totalVolumeUSD` | string                                                                        | Liquidity / activity signals.                                                                            |
| `alphaConfig`                         | object                                                                        | LSLMSR curve params for off-chain pricing if needed.                                                     |
| `creatorFeeBps`                       | string                                                                        | Creator fee in bps. **Required to compute `maxCost` / `minReturn` correctly** — see [Trading](#trading). |

#### Example: fetch tradeable standard markets <a href="#example-fetch-tradeable-standard-markets" id="example-fetch-tradeable-standard-markets"></a>

```ts
type MarketListItem = {
  market: string;
  status: string;
  outcomeCount: number;
  outcomes: Array<{ id: number; title: string }>;
  collateralToken: string;
  expiresAt: string;
  startsAt: string;
  creatorFeeBps: string;
  initialSharesPerOutcome: string;
};

type MarketListResponse = {
  items: MarketListItem[];
  page: number;
  limit: number;
  total: number;
};

const API_BASE = 'https://predict-testnet-api.midashand.xyz/api';

async function listOpenMarkets(collateralToken?: string): Promise<MarketListItem[]> {
  // NOTE: don't use `new URL('/markets', API_BASE)` — the WHATWG URL constructor
  // treats the second arg as an origin and silently strips any path on API_BASE
  // (e.g. `host/api`), so you'd hit `host/markets` and get a 404. Build the
  // path with a template string and use URLSearchParams for query args instead.
  const qs = new URLSearchParams({ status: 'open', limit: '100' });
  if (collateralToken) qs.set('collateralToken', collateralToken);

  const res = await fetch(`${API_BASE}/markets?${qs}`);
  if (!res.ok) throw new Error(`GET /markets failed: ${res.status}`);
  const body = (await res.json()) as { data: MarketListResponse };
  return body.data.items;
}
```

#### Quick markets <a href="#quick-markets" id="quick-markets"></a>

Quick markets are created automatically at fixed cycles (5m / 15m / 1h / 4h / 1d) for each supported asset. Each instance is a binary "Up or Down" market at a strike price locked at slot start (outcome 0 = UP, outcome 1 = DOWN).

There are two discovery endpoints depending on what the bot needs:

| Endpoint             | Use when                                                                                                                                                                                                                                   |
| -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `GET /daily-markets` | You want broader history across all lifecycle stages (upcoming, live, ended, resolved, closed). Paginated. Filter via `bucket` (shorthand) or `status` (fine-grained whitelist), with optional `window` / `startAt` / `endAt` time bounds. |

> **Tradeable check.** Always verify before trading by either:
>
> * computing `startsAt <= now < expiresAt` from the response, or
> * calling `getStatus() === 2` on-chain **and** checking `block.timestamp >= startsAt` (the contract enforces both — see [Trading > Overview](/midaspredict/developers/bot-trading-guide-buy-sell-redeem.md#overview-1)).

**`GET /daily-markets`**

Returns daily market instances across all lifecycle stages. **By default** returns the last 24 hours of `PENDING` / `CREATED` / `ACTIVE` / `RESOLVED` (incl. `CLOSED` via `markets.status` join). `FAILED` rows are always excluded. Use `bucket` for high-level lifecycle filtering, or `status` for fine-grained instance-status filtering (debug). Filters can be combined freely.

<table><thead><tr><th width="106">Param</th><th width="119">Type</th><th width="118">Default</th><th>Notes</th></tr></thead><tbody><tr><td><code>page</code></td><td>integer</td><td><code>1</code></td><td></td></tr><tr><td><code>limit</code></td><td>integer</td><td><code>10</code></td><td></td></tr><tr><td><code>bucket</code></td><td>string</td><td>—</td><td>Lifecycle-stage shorthand. <strong>Overrides <code>status</code> if both are given.</strong> Common values: <code>live</code>, <code>closed</code>.</td></tr><tr><td><code>asset</code></td><td>string</td><td>—</td><td>Comma-separated asset whitelist (e.g. <code>BTC,LTC</code>). Case-insensitive.</td></tr><tr><td><code>status</code></td><td>string</td><td>—</td><td>Comma-separated instance-status whitelist (debug only). Example: <code>ACTIVE,CLOSED</code>. Ignored if <code>bucket</code> is set.</td></tr><tr><td><code>sortBy</code></td><td>string</td><td><code>startsAt</code></td><td>Sort field.</td></tr><tr><td><code>sortOrder</code></td><td>string</td><td><code>desc</code></td><td><code>asc</code> or <code>desc</code>.</td></tr><tr><td><code>window</code></td><td>string</td><td><code>24h</code></td><td>Time window relative to NOW. Ignored if <code>startAt</code> or <code>endAt</code> is set.</td></tr><tr><td><code>startAt</code></td><td>string</td><td>—</td><td>Inclusive lower bound on <code>starts_at</code>. Overrides <code>window</code>. Accepts ISO 8601 (<code>2026-05-01T10:00:00Z</code>), epoch seconds (<code>1764763200</code>), or epoch millis (<code>1764763200000</code>).</td></tr><tr><td><code>endAt</code></td><td>string</td><td>—</td><td>Inclusive upper bound on <code>starts_at</code>. Overrides <code>window</code>. Same formats as <code>startAt</code>.</td></tr></tbody></table>

Response shape: `{ items: [...], total, page, limit }`.

**Item fields (both endpoints)**

<table><thead><tr><th width="195">Field</th><th width="211">Type</th><th>Use</th></tr></thead><tbody><tr><td><code>marketAddress</code></td><td>string (hex) | null</td><td><strong>Contract address</strong> — pass to <code>getMarket(address)</code>. <code>null</code> for instances that haven't been created on-chain yet.</td></tr><tr><td><code>asset</code></td><td>string</td><td>Underlying symbol (e.g. <code>BTC</code>).</td></tr><tr><td><code>cycleDurationMin</code></td><td>int</td><td><code>5</code> / <code>15</code> / <code>60</code> / <code>240</code> / <code>1440</code>.</td></tr><tr><td><code>strikePrice</code></td><td>string | null</td><td>Locked price at slot start. <code>null</code> until the strike is locked.</td></tr><tr><td><code>startsAt</code> / <code>expiresAt</code></td><td>ISO 8601</td><td>Trading window.</td></tr><tr><td><code>status</code></td><td><code>PENDING</code> | <code>CREATED</code> | <code>ACTIVE</code> | <code>EXPIRED</code> | <code>RESOLVED</code> | <code>CLOSED</code> | <code>FAILED</code></td><td>Computed display status. Not authoritative — re-check on-chain via <code>getStatus()</code>.</td></tr><tr><td><code>winningOutcomeIndex</code></td><td>int | null</td><td>Set when resolved (0 = UP, 1 = DOWN).</td></tr><tr><td><code>winner</code></td><td><code>'Up'</code> | <code>'Down'</code> | null</td><td>Convenience label derived from <code>winningOutcomeIndex</code> (note the casing).</td></tr><tr><td><code>settlementPrice</code></td><td>string | null</td><td>Settlement price at expiry.</td></tr><tr><td><code>market</code></td><td>object | null</td><td>Nested on-chain market snapshot (volume, TVL, holders, on-chain status, <code>volume24h</code>).</td></tr></tbody></table>

**Example: fetch all currently-tradeable quick markets**

Uses `/daily-markets?bucket=live`. The `bucket=live` filter keeps only currently-ACTIVE instances; `cycle` is **not** a server-side filter on this endpoint, so filter on `cycleDurationMin` client-side. We also drop pre-window instances (ACTIVE but `startsAt > NOW`) by checking the time bounds.

```ts
type QuickMarketItem = {
  marketAddress: string | null;
  asset: string;
  cycleDurationMin: number;
  strikePrice: string | null;
  startsAt: string;  // ISO 8601
  expiresAt: string; // ISO 8601
  inWindow?: boolean;
  status: 'PENDING' | 'CREATED' | 'ACTIVE' | 'EXPIRED' | 'RESOLVED' | 'CLOSED' | 'FAILED';
};

type DailyMarketsResponse = {
  items: QuickMarketItem[];
  total: number;
  page: number;
  limit: number;
};

async function listTradeableQuickMarkets(opts: {
  asset?: string;        // comma-separated whitelist, e.g. "BTC" or "BTC,LTC"
  cycle?: number;        // minutes: 5 / 15 / 60 / 240 / 1440 (filtered client-side)
  limit?: number;        // backend cap is 100
} = {}): Promise<QuickMarketItem[]> {
  // See `listOpenMarkets` for why we don't use `new URL('/path', API_BASE)`.
  const qs = new URLSearchParams({ bucket: 'live', limit: String(opts.limit ?? 100) });
  if (opts.asset) qs.set('asset', opts.asset);

  const res = await fetch(`${API_BASE}/daily-markets?${qs}`);
  if (!res.ok) throw new Error(`GET /daily-markets failed: ${res.status}`);
  const body = (await res.json()) as { data: DailyMarketsResponse };

  // bucket=live can include pre-window instances (ACTIVE but starts_at > NOW).
  // Keep only those currently in their trading window, and apply the cycle
  // filter client-side since the endpoint doesn't accept it.
  const now = Date.now();
  return body.data.items.filter((m) => {
    if (!m.marketAddress) return false;
    if (opts.cycle != null && m.cycleDurationMin !== opts.cycle) return false;
    return Date.parse(m.startsAt) <= now && Date.parse(m.expiresAt) > now;
  });
}

// e.g. all currently-tradeable BTC 15-minute quick markets
const liveBtc15 = await listTradeableQuickMarkets({ asset: 'BTC', cycle: 15 });
```

Once you have a `marketAddress`, the buy / sell / redeem flow in [Trading](#trading) is identical to standard markets — quick markets are just `MidasMarket` contracts with a 2-outcome (UP / DOWN) configuration. Always re-check `getStatus() === 2` and `block.timestamp >= startsAt` on-chain before submitting a trade.

## Trading <a href="#trading" id="trading"></a>

### Overview <a href="#overview-1" id="overview-1"></a>

Trading is **non-custodial**: the bot signs and submits transactions directly to the `MidasMarket` contract. The backend is for discovery only — it never holds or routes funds.

A market is an `MidasMarket` proxy with `N` outcomes (typically 2 for binary). Outcome shares are ERC1155 tokens minted on the contract returned by `MARKET_OUTCOME()`. Pricing follows an **LSLMSR-dynamic bonding curve**, so cost/return is path-dependent and must be quoted on-chain immediately before submission.

#### System config bootstrap (`GET /system/config`) <a href="#system-config-bootstrap-get-systemconfig" id="system-config-bootstrap-get-systemconfig"></a>

A bot needs a few global, slow-changing values to compute fees, detect native-collateral markets, and validate trade-size limits. Rather than reading these from the on-chain `MARKET_CONFIG_MANAGER` contract, the bot fetches them once at startup from the backend:

```ts
type SystemConfig = {
  fees: {
    protocolFeeBps: number;          // applied to base cost / sale return (unless market overrides)
    userFeeBps:     number;          // applied to creatorFee
  };
  nativeWrapper: string | null;       // address of WzkLTC etc.; null if not configured
  paused:        boolean;             // global platform pause
  collateralTokens: Array<{
    address:        string;
    symbol:         string;
    decimals:       number;
    config: {
      minTradeSizeInCollateral:   string;   // bigint string, in token wei
      maxTradeSizeInCollateral:   string;
    };
  }>;
};

async function loadSystemConfig(): Promise<SystemConfig> {
  const res = await fetch(`${API_BASE}/system/config`);
  if (!res.ok) throw new Error(`GET /system/config failed: ${res.status}`);
  const body = (await res.json()) as { data: SystemConfig };
  return body.data;
}

// Cache once at startup, refresh every 30–60 s if the bot runs long.
const sysCfg = await loadSystemConfig();
```

Every `buy` / `sell` example below assumes `sysCfg` is in scope. With this, the bot does **not** need to ship `CONFIG_MANAGER_ABI` or hold an RPC connection to read fees — only `getMarket()` (per-market) and the trade itself stay on-chain.

#### Share denomination <a href="#share-denomination" id="share-denomination"></a>

Outcome share `amounts[]` are denominated in the **same scale as the collateral token**, not in a fixed 18-decimal unit. The contract uses `precisionFactor = 10 ** IERC20Metadata(collateral).decimals()` everywhere a share is converted to collateral (see `_getPrecisionFactor` and `redeem`). One implication: with empty supplies, `MarketHelper.getSharesForCollateral(...)` returns `collateralAmount` directly (1:1).

| Collateral | "1 whole share" | `oneShare`   |
| ---------- | --------------- | ------------ |
| USDC       | 10⁶             | `10n ** 6n`  |
| zkLTC      | 10¹⁸            | `10n ** 18n` |

Always derive the unit from `IERC20.decimals()` of the market's `collateralToken` — never hardcode `1e18`. The simplest safe pattern when sizing a buy is: pick a target collateral spend, then ask `MarketHelper.getSharesForCollateralInMarket(market, outcome, collateralAmount)` for the corresponding share count.

#### Status state machine <a href="#status-state-machine" id="status-state-machine"></a>

The on-chain enum is:

<table><thead><tr><th width="91">Value</th><th width="172">Status</th><th width="200">Meaning</th><th>Trading allowed</th></tr></thead><tbody><tr><td>0</td><td><code>PENDING</code></td><td>Awaiting oracle approval</td><td>No</td></tr><tr><td>1</td><td><code>CANCELLED</code></td><td>Cancelled before activation</td><td>No</td></tr><tr><td>2</td><td><code>ACTIVE</code></td><td>Live</td><td><code>buy</code> / <code>sell</code> (also requires <code>block.timestamp >= startsAt &#x26;&#x26; &#x3C; expiresAt</code> and the platform not globally paused)</td></tr><tr><td>3</td><td><code>PAUSED</code></td><td>Per-market paused after activation</td><td>No</td></tr><tr><td>4</td><td><code>RESOLVED</code></td><td>Winning outcome set, awaiting <code>close()</code></td><td>No (intermediate — wait for CLOSED)</td></tr><tr><td>5</td><td><code>CLOSED</code></td><td>Resolution finalized</td><td><code>redeem</code></td></tr><tr><td>6</td><td><code>UPDATE_REQUIRED</code></td><td>Oracle requested metadata change</td><td>No</td></tr><tr><td>7</td><td><code>VOIDED</code></td><td>Voided after activation, awaiting <code>close()</code></td><td>No (intermediate — wait for VOIDED_CLOSED)</td></tr><tr><td>8</td><td><code>VOIDED_CLOSED</code></td><td>Void finalized</td><td><code>redeem</code> (proportional refund)</td></tr></tbody></table>

Read it via `getStatus()` before every trade — never cache.

#### Quote-then-submit pattern <a href="#quote-then-submit-pattern" id="quote-then-submit-pattern"></a>

Every `buy` and `sell` follows the same shape:

1. **Quote** with a view call (`getOutcomePurchaseCost` / `getOutcomeSaleReturn`) — this returns the **LSLMSR base cost / gross return only**.
2. **Add fees** (creator + protocol + user) — see [Fee math](#fee-math) below.
3. **Apply slippage** — set `maxCost` (buy) or `minReturn` (sell) with a buffer on the *fee-adjusted* number.
4. **Ensure ERC20 allowance for `maxCost`** (buy, ERC20 collateral) — or send `msg.value: maxCost` (buy, native-wrapper collateral). Sell burns ERC1155 directly via the market.
5. **Submit** the tx. Re-quote on every send; the curve moves with each trade.

#### Fee math <a href="#fee-math" id="fee-math"></a>

`getOutcomePurchaseCost` / `getOutcomeSaleReturn` return the LSLMSR base only. The contract takes three fees on top of that base, so the bot must replicate the calculation:

```
creatorFee  = base * creatorFeeBps  / 10_000
protocolFee = base * protocolFeeBps / 10_000   // sysCfg.fees.protocolFeeBps (or marketData.protocolFeeBpsOverride)
userFees    = creatorFee * userFeeBps / 10_000  // sysCfg.fees.userFeeBps

buy:  totalCost  = base + creatorFee + protocolFee + userFees;  require(totalCost  <= maxCost)
sell: netReturn  = base - (creatorFee + protocolFee + userFees); require(netReturn >= minReturn)
```

Sources for each input:

* `creatorFeeBps` — per-market, from `getMarket()` (or the market list response).
* Effective protocol fee — `marketData.overrideProtocolFeeBps ? marketData.protocolFeeBpsOverride : sysCfg.fees.protocolFeeBps`.
* `userFeeBps` — global, from `sysCfg.fees.userFeeBps`.
* Trade-size limits — global per collateral, from `sysCfg.collateralTokens.find(t => t.address === marketData.collateralToken).config.{min,max}TradeSizeInCollateral`. The contract requires `base >= min && base <= max` or it reverts `InvalidParameter`.
* Native-collateral detection — derived: `marketData.collateralToken.toLowerCase() === sysCfg.nativeWrapper?.toLowerCase()`. The collateral entry has no `isNative` field; only the top-level `sysCfg.nativeWrapper` address.

#### Soulbound outcome shares <a href="#soulbound-outcome-shares" id="soulbound-outcome-shares"></a>

ERC1155 outcome tokens are **non-transferable** between users. Only mint (buy), burn (sell/redeem), and market-to-user transfers (creator-share unlock) are allowed; calls to `safeTransferFrom` / `setApprovalForAll` for user-to-user transfers revert. A bot operating across multiple wallets must trade independently per wallet — it cannot move shares between them.

The outcome `tokenId` is derived as `keccak256(abi.encodePacked(block.chainid, marketAddress, outcomeIndex))`. Easiest path: just call `MARKET_OUTCOME.getTokenId(marketAddress, outcomeIndex)`.

### Buy and Sell <a href="#buy-and-sell" id="buy-and-sell"></a>

#### Buy <a href="#buy" id="buy"></a>

`buy(uint8[] outcomes, uint256[] amounts, uint256 maxCost) payable`

Mints `amounts[i]` shares of `outcomes[i]` to the caller. Pulls `totalCost <= maxCost` of the collateral token (or `msg.value` for native collateral). Reverts `CostExceedsMaximum` on slippage breach.

```ts
const BPS = 10_000n;

type FeeCfg = {
  creatorFeeBps: bigint;
  protocolFeeBps: bigint;
  userFeeBps: bigint;
};

function applyBuyFees(base: bigint, f: FeeCfg): bigint {
  const creatorFee  = (base * f.creatorFeeBps)  / BPS;
  const protocolFee = (base * f.protocolFeeBps) / BPS;
  const userFees    = (creatorFee * f.userFeeBps) / BPS;
  return base + creatorFee + protocolFee + userFees;
}

function findCollateral(sysCfg: SystemConfig, address: string) {
  const lower = address.toLowerCase();
  const t = sysCfg.collateralTokens.find(c => c.address.toLowerCase() === lower);
  if (!t) throw new Error(`collateral ${address} not in /system/config`);
  return t;
}

// Native collateral isn't a per-token flag in /system/config — it's the
// top-level `nativeWrapper` address. Derive it.
function isNativeCollateral(sysCfg: SystemConfig, collateralToken: string) {
  const wrapper = sysCfg.nativeWrapper?.toLowerCase();
  return !!wrapper && collateralToken.toLowerCase() === wrapper;
}

async function buy(
  sysCfg: SystemConfig,
  marketAddress: string,
  outcomes: number[],   // e.g. [0] for YES-only, or [0, 1] for both legs
  amounts: bigint[],    // shares per outcome, scale = 10**collateralDecimals
  slippageBps: number   // e.g. 100 = 1% on top of fee-adjusted cost
) {
  if (sysCfg.paused) throw new Error('platform paused');
  const market = getMarket(marketAddress);

  // 1. Sanity checks
  const status = Number(await market.getStatus());
  if (status !== 2) throw new Error(`market not ACTIVE (status=${status})`);

  const marketData = await market.getMarket();
  const now = Math.floor(Date.now() / 1000);
  if (now < Number(marketData.startsAt)) throw new Error('pre-window');
  if (now >= Number(marketData.expiresAt)) throw new Error('expired');

  // 2. Quote (LSLMSR base, view, no gas)
  const base: bigint = await market.getOutcomePurchaseCost(outcomes, amounts);

  // 3. Validate trade-size limits (configManager values live in sysCfg)
  const collateralInfo = findCollateral(sysCfg, marketData.collateralToken);
  const minBase = BigInt(collateralInfo.config.minTradeSizeInCollateral);
  const maxBase = BigInt(collateralInfo.config.maxTradeSizeInCollateral);
  if (base < minBase) throw new Error(`trade too small: base=${base} min=${minBase}`);
  if (base > maxBase) throw new Error(`trade too large: base=${base} max=${maxBase}`);

  // 4. Add fees + slippage (no contract reads needed)
  const protocolFeeBps: bigint = marketData.overrideProtocolFeeBps
    ? BigInt(marketData.protocolFeeBpsOverride)
    : BigInt(sysCfg.fees.protocolFeeBps);
  const fees: FeeCfg = {
    creatorFeeBps: BigInt(marketData.creatorFeeBps),
    protocolFeeBps,
    userFeeBps:    BigInt(sysCfg.fees.userFeeBps),
  };
  const totalCost = applyBuyFees(base, fees);
  const maxCost   = totalCost + (totalCost * BigInt(slippageBps)) / BPS;

  // 5. Pay path: ERC20 approve OR native msg.value
  let tx;
  if (isNativeCollateral(sysCfg, collateralInfo.address)) {
    tx = await market.buy(outcomes, amounts, maxCost, { value: maxCost });
  } else {
    const erc20 = new Contract(collateralInfo.address, ERC20_ABI, wallet);
    const current: bigint = await erc20.allowance(wallet.address, marketAddress);
    if (current < maxCost) {
      await (await erc20.approve(marketAddress, maxCost)).wait();
    }
    tx = await market.buy(outcomes, amounts, maxCost);
  }

  const receipt = await tx.wait();
  return { txHash: tx.hash, base, totalCost, maxCost, receipt };
}
```

Notes:

* `outcomes` and `amounts` must be the same length. Pass each outcome at most once per call.
* Re-quote on every send. Other trades between your quote and your tx will move the curve.
* A one-shot `approve(market, MaxUint256)` works if the bot only trades a single market. If the same wallet trades multiple markets, scoping each approval to `maxCost` per call limits exposure.
* Trade-size limits (`base` must lie in `[minTradeSizeInCollateral, maxTradeSizeInCollateral]`) come from `sysCfg.collateralTokens[].config` — refresh `sysCfg` periodically (30–60 s) so the bot picks up backend-side updates.

Emits `OutcomeTokensBought` (single-leg) or `BatchOutcomeTokensBought` (multi-leg) plus `MarketPriceChanged`.

#### Sell <a href="#sell" id="sell"></a>

`sell(uint8[] outcomes, uint256[] amounts, uint256 minReturn)`

Burns `amounts[i]` shares of `outcomes[i]` from the caller and pays out `netReturn >= minReturn` of the collateral token (after fees). Reverts `ReturnBelowMinimum` on slippage breach.

The market burns ERC1155 outcome shares directly via its own privileged `burn` path on `MARKET_OUTCOME`, so the bot does **not** need `setApprovalForAll` — and in fact couldn't grant one even if it tried (shares are soulbound).

```ts
function applySellFees(base: bigint, f: FeeCfg): bigint {
  const creatorFee  = (base * f.creatorFeeBps)  / BPS;
  const protocolFee = (base * f.protocolFeeBps) / BPS;
  const userFees    = (creatorFee * f.userFeeBps) / BPS;
  return base - (creatorFee + protocolFee + userFees);
}

async function sell(
  sysCfg: SystemConfig,
  marketAddress: string,
  outcomes: number[],
  amounts: bigint[],
  slippageBps: number
) {
  if (sysCfg.paused) throw new Error('platform paused');
  const market = getMarket(marketAddress);

  if (Number(await market.getStatus()) !== 2) throw new Error('market not ACTIVE');

  const marketData = await market.getMarket();
  const base: bigint = await market.getOutcomeSaleReturn(outcomes, amounts);
  const protocolFeeBps: bigint = BigInt(sysCfg.fees.protocolFeeBps);
  const fees: FeeCfg = {
    creatorFeeBps: BigInt(marketData.creatorFeeBps),
    protocolFeeBps,
    userFeeBps:    BigInt(sysCfg.fees.userFeeBps),
  };
  const netExpected = applySellFees(base, fees);
  const minReturn   = netExpected - (netExpected * BigInt(slippageBps)) / BPS;

  const tx = await market.sell(outcomes, amounts, minReturn);
  const receipt = await tx.wait();
  return { txHash: tx.hash, base, netExpected, minReturn, receipt };
}
```

Checking your share balance before sizing the sell:

```ts
const outcomeNftAddr: string = await market.MARKET_OUTCOME();
const nft = new Contract(outcomeNftAddr, ERC1155_ABI, provider);
const tokenId: bigint = await new Contract(outcomeNftAddr, [
  'function getTokenId(address market, uint256 outcomeIndex) view returns (uint256)'
], provider).getTokenId(marketAddress, outcomeIndex);
const balance: bigint = await nft.balanceOf(wallet.address, tokenId);
```

Emits `OutcomeTokensSold` / `BatchOutcomeTokensSold` plus `MarketPriceChanged`.

### Redeem <a href="#redeem" id="redeem"></a>

`redeem()` — no arguments. Pays the caller based on terminal status:

* `CLOSED` (5): `redeemableAmountPerShare * (shares of winningOutcome held by caller) / 10^collateralDecimals`. Losing-outcome shares are worthless and must be burned via a separate flow if at all (typically just left on-chain).
* `VOIDED_CLOSED` (8): proportional refund per `redeemableAmountPerShare` across all outcomes the caller holds.

Reverts `InvalidMarketState` if the market is not yet in `CLOSED` / `VOIDED_CLOSED` (note: `RESOLVED` and `VOIDED` are intermediate — they require a separate `close()` call, usually triggered by a keeper, before redemption opens). Reverts `NoTokensToRedeem` if the caller holds no claimable shares.

```ts
async function redeem(marketAddress: string) {
  const market = getMarket(marketAddress);

  const status = Number(await market.getStatus());
  if (status !== 5 && status !== 8) {
    throw new Error(`market not redeemable (status=${status} — wait for CLOSED/VOIDED_CLOSED)`);
  }

  const tx = await market.redeem();
  const receipt = await tx.wait();
  return { txHash: tx.hash, receipt };
}
```

Detecting redeemability for a held position:

```ts
const perShare: bigint = await market.getRedeemableAmountPerShare();
// Non-zero only after resolution / void. Combined with status (5 or 8) and your held
// outcome index vs. winningOutcome (from getMarket()), you can decide whether to call redeem.
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://midashand-1.gitbook.io/midaspredict/developers/bot-trading-guide-buy-sell-redeem.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
