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

# Prediction markets

> Predict YES or NO on real-world events. Discover a market, open a proposal, predict, sell, and claim winnings.

Prediction markets let your bot take a YES/NO position on a real-world event: a game, an election, a
price level. Markets are **Polymarket-backed**: you browse Polymarket events, pick a market and a
side, and the position lives as a **proposal inside a group**, exactly like spot and perps. See
[Groups & proposals](/concepts/groups-and-proposals) for the shared model.

The lifecycle is: **discover an event → create a proposal (your opening prediction) → predict more or sell →
the market resolves → claim winnings.** Everything is funded from your USDC balance, and sells and
claims deliver back to it.

<Note>
  Every monetary value in and out is a **full-precision plain decimal string in USD** (`"10.00"`, not
  `10` and not `10000000`). That covers request fields like `amountUsd` and response fields like
  `totalCostUsd`. See [Money & precision](/concepts/money-and-precision).
</Note>

## How a market is identified

You reference a market with three things, and you never send a title. The backend resolves
`eventTitle` / `marketTitle` and the outcome token for you:

| Field      | What it is                                                | Example                                                 |
| ---------- | --------------------------------------------------------- | ------------------------------------------------------- |
| `eventId`  | The **event slug** (the question or matchup).             | `"presidential-election-winner-2028"`                   |
| `marketId` | The **market slug**, a specific market inside that event. | `"will-jd-vance-win-the-2028-us-presidential-election"` |
| `isYes`    | Side: `true` buys **YES**, `false` buys **NO**.           | `true`                                                  |

<Warning>
  `isYes` is a **boolean**, not a string. `isYes: true` is the YES side; `isYes: false` is the NO
  side. A missing or non-boolean `isYes` is rejected with a `400`.
</Warning>

<Tip>
  Get `eventId` and `marketId` from the event-read endpoints below. Use the event's `eventTicker` as
  `eventId` and the chosen market's `marketTicker` as `marketId` (both are slugs). The side within
  the market is chosen with `isYes`.
</Tip>

<Tip>
  **Porting a bot from Polymarket's API?** The identifiers are Polymarket's own, so you can pass
  what your bot already holds: `eventId` accepts an event slug or Polymarket's numeric event id, and
  `marketId` accepts a market slug, Polymarket's numeric market id, or the market's `conditionId`
  (`0x…`). Slugs are what our event-read endpoints return.
</Tip>

Proposals also carry an `outcomeMint`: the venue's outcome-token id for the chosen side (a long
numeric string), resolved server-side. It is an opaque identifier, not something you ever send.
Legacy proposals may hold older identifier formats there and in `eventId` / `marketId`.

## Setup

```bash theme={null}
export MURMO_API_KEY="murmo_your_key_here"
export MURMO_BASE="https://api.alpha-labs.trade"
```

All calls send `Authorization: Bearer $MURMO_API_KEY`. See [Authentication](/authentication).

<Note>
  **Entering** a prediction (creating a proposal, predicting on one) is restricted in some regions
  and returns `403` with code `GEO_RESTRICTED` there. Selling, closing, and claiming are **never**
  geo-restricted: you can always exit or redeem a position you hold. See
  [Eligibility & geo restrictions](/concepts/geo-restrictions).
</Note>

## Discovering events

Event reads are venue-native passthroughs, **normalized to a stable shape**: slugs in the
`...Ticker` fields, prices as human dollar strings (`"0.29"`), times as epoch seconds. Each endpoint
returns `{ "data": ... }` around the payload shown below.

### Browse

`GET /api/v1/predictions/events/browse` returns the trending feed by default. Optional query params:

* `seriesTickers`: a category tag slug such as `politics`, `sports`, `crypto`. Comma-separated
  values are accepted but only the **first** one is applied.
* `limit`: default `20`, max `50`.
* `cursor`: numeric offset for the next page.
* `status`: legacy filter, ignored.
* `category` (with optional `tag`, `page`): when present, switches to the **paginated category
  browse**. `category` is a category label or slug (`Politics` or `politics`), `tag` narrows it, and
  `page` is a zero-based page index. The response becomes `{ "events": [...], "hasMore": true }`.

<CodeGroup>
  ```bash curl theme={null}
  # Trending
  curl "$MURMO_BASE/api/v1/predictions/events/browse" \
    -H "Authorization: Bearer $MURMO_API_KEY"

  # Scoped to a category tag
  curl "$MURMO_BASE/api/v1/predictions/events/browse?seriesTickers=politics&limit=20" \
    -H "Authorization: Bearer $MURMO_API_KEY"

  # Paginated category browse
  curl "$MURMO_BASE/api/v1/predictions/events/browse?category=Politics&page=0" \
    -H "Authorization: Bearer $MURMO_API_KEY"
  ```

  ```javascript JavaScript theme={null}
  const res = await fetch(
    `${process.env.MURMO_BASE}/api/v1/predictions/events/browse?seriesTickers=politics&limit=20`,
    { headers: { Authorization: `Bearer ${process.env.MURMO_API_KEY}` } },
  );
  const { data } = await res.json(); // array of normalized events
  ```

  ```python Python theme={null}
  import requests, os

  base, key = os.environ["MURMO_BASE"], os.environ["MURMO_API_KEY"]
  res = requests.get(
      f"{base}/api/v1/predictions/events/browse",
      headers={"Authorization": f"Bearer {key}"},
      params={"seriesTickers": "politics", "limit": 20},
  )
  data = res.json()["data"]  # array of normalized events
  ```
</CodeGroup>

```json Response (one event, trimmed to one market) theme={null}
{
  "data": [
    {
      "eventTicker": "presidential-election-winner-2028",
      "seriesTicker": "politics",
      "title": "Presidential Election Winner 2028",
      "subTitle": "",
      "category": "Politics",
      "totalVolume": 512834021.47,
      "totalMarketCount": 24,
      "activeMarketCount": 21,
      "tags": ["Politics", "Elections"],
      "settlementSources": [
        { "name": "Resolution source", "url": "https://www.archives.gov/electoral-college" }
      ],
      "status": "active",
      "isNew": false,
      "startDate": 1738368000,
      "eventImageUrl": "https://polymarket-upload.s3.us-east-2.amazonaws.com/presidential-election-winner-2028.png",
      "markets": [
        {
          "marketTicker": "will-jd-vance-win-the-2028-us-presidential-election",
          "eventTicker": "presidential-election-winner-2028",
          "yesSubTitle": "JD Vance",
          "noSubTitle": "No",
          "status": "active",
          "result": "",
          "yesBidDollars": "0.28",
          "yesAskDollars": "0.29",
          "noBidDollars": "0.71",
          "noAskDollars": "0.72",
          "lastPriceDollars": "0.28",
          "volume": 149553201.88,
          "openTime": 1738368000,
          "closeTime": 1857168000,
          "expirationTime": 1857168000,
          "rulesPrimary": "This market will resolve to \"Yes\" if JD Vance wins the 2028 US presidential election.",
          "rulesSecondary": null,
          "imageUrl": "https://polymarket-upload.s3.us-east-2.amazonaws.com/jd-vance.png",
          "takerFeeBps": 0,
          "builderFeeBps": 0,
          "yesTokenId": "216549008739...881356",
          "noTokenId": "740121509217...003414",
          "negRisk": true,
          "conditionId": "0x26d06d9c6303c11bf7388cff707e4dac836e0362f6f63d8cbf4234713d8b21aa"
        }
      ]
    }
  ]
}
```

What the fields mean:

* `eventTicker` / `marketTicker` are the slugs you send back as `eventId` / `marketId`.
* `seriesTicker` is the event's primary category tag slug (also what `seriesTickers` filters on).
* `yesSubTitle` is the market's row label in a grouped event ("JD Vance"); it is plain `Yes` on a
  standalone YES/NO market.
* Prices (`yesBidDollars`, `yesAskDollars`, `noBidDollars`, `noAskDollars`, `lastPriceDollars`) are
  dollar strings between `"0.0"` and `"1.0"`; the NO side is the complement of YES. Buys fill near
  the ask of your side, sells near the bid.
* Market `status` is `active`, `inactive`, `closed`, or `finalized` (resolved, redeemable); `result`
  is `""` until resolution, then `yes`, `no`, or `invalid`.
* `takerFeeBps` / `builderFeeBps` describe the venue fee applied at match on this market; `0` means
  the market currently charges none. The fee actually charged on each of your fills is reported on
  the trade as `venueFeeUsd`.
* `yesTokenId` / `noTokenId` are the venue outcome-token ids (the chosen one becomes the proposal's
  `outcomeMint`).
* `openTime`, `closeTime`, `expirationTime`, `startDate` are epoch seconds; `volume` and
  `totalVolume` are plain numbers.
* `negRisk` and `conditionId` are venue-technical metadata; you can ignore them.
* Some fields exist for shape compatibility and are always empty, `null`, or fixed on the current
  venue: `subTitle`, `competition`, `competitionScope`, `strikeDate`, `strikePeriod`, `strikeType`,
  `floorStrike`, `capStrike`, `openInterest`, `milestoneId`, `isTrending`, `isClosing`,
  `liveStatus`, `liveDetails`, `backgroundColor`, `imageScale`, and `canCloseEarly` (always
  `false`).

### Search

`GET /api/v1/predictions/events/search` fuzzy-searches events. **`q` is required** (a missing or
blank `q` is a `400`). `limit` defaults to `20`, max `50`. `sort`, `order`, and `cursor` are
accepted but ignored: results are a single page and the response `cursor` is always `0`.

```bash theme={null}
curl "$MURMO_BASE/api/v1/predictions/events/search?q=presidential%20election&limit=10" \
  -H "Authorization: Bearer $MURMO_API_KEY"
```

```json Response (shape) theme={null}
{ "data": { "cursor": 0, "events": [ ... ] } }
```

### Live

`GET /api/v1/predictions/events/live` returns high-activity events, **ordered by 24h volume**. Optional:
`limit` (default `20`, max `50`), `cursor` (numeric offset), `category` and `subcategory` (both
category tag slugs; `subcategory` wins when both are sent). `competition` is accepted but ignored.

```bash theme={null}
curl "$MURMO_BASE/api/v1/predictions/events/live?category=sports" \
  -H "Authorization: Bearer $MURMO_API_KEY"
```

```json Response (shape) theme={null}
{ "data": { "events": [ ... ], "nextCursor": 20, "availableSports": [] } }
```

`nextCursor` is `null` on the last page.

### Event by slug

`GET /api/v1/predictions/events/{slug}` returns one event by its slug, with its markets filtered for
liquidity. An unknown slug is a `404` with code `EVENT_NOT_FOUND`. Older identifier formats from
legacy proposals also resolve here.

```bash theme={null}
curl "$MURMO_BASE/api/v1/predictions/events/presidential-election-winner-2028" \
  -H "Authorization: Bearer $MURMO_API_KEY"
```

### Related events

`GET /api/v1/predictions/events/{slug}/related` returns other events sharing this event's primary category
tag, up to 10. **`seriesTicker` must be sent** (a missing or blank value is a `400`), but its value
is unused on the current venue; any non-empty string works.

```bash theme={null}
curl "$MURMO_BASE/api/v1/predictions/events/presidential-election-winner-2028/related?seriesTicker=politics" \
  -H "Authorization: Bearer $MURMO_API_KEY"
```

## Creating a proposal (with the opening prediction)

`POST /api/v1/predictions/proposals` opens a proposal **and** places the creator's first prediction in one
call. **Group leaders only** (non-leaders get a `403`), and geo-restricted (see above).

The body is [`CreatePredictionProposalBody`](/api-reference): `groupId`, `eventId`, `marketId`,
`isYes`, `amountUsd`, and optional `reason` (max 500 characters). All five non-`reason` fields are
required.

Buys must clear two minimums:

* **\$4** flat minimum (`400`, code `AMOUNT_BELOW_MINIMUM`).
* The market's **venue minimum**, typically 5 contracts at the current price (`400`, code
  `MIN_ORDER_SIZE`). The error message includes the market's computed dollar minimum, so you can
  retry with a valid amount.

One proposal per outcome per group: while an `ACTIVE` or still-settling (`PENDING`) proposal for the
same outcome exists in the group, creating another is a `400`.

The response is `{ proposal, trade, pending, actionJobId }` and fills are **async-optimistic**:

* `pending: false`: the buy filled synchronously. `trade` is populated and `proposal.status` is
  `ACTIVE`.
* `pending: true`: your funds are committed and the buy is settling in the background. `trade` is
  `null`, `proposal.status` is `PENDING`, and `actionJobId` identifies the settlement job. The
  proposal flips to `ACTIVE` on the confirmed fill (typically under a minute) or to `FAILED` with
  your funds automatically returned. Poll `GET /proposals/{id}` to observe the flip.

<Warning>
  Never read `data.trade` unconditionally: it is `null` whenever `pending` is `true`. Branch on
  `pending` (or null-check `trade`) as in the examples below.
</Warning>

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST "$MURMO_BASE/api/v1/predictions/proposals" \
    -H "Authorization: Bearer $MURMO_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{
      "groupId": "1a9d212a-de67-45f4-88f5-6e9f41e78dc0",
      "eventId": "presidential-election-winner-2028",
      "marketId": "will-jd-vance-win-the-2028-us-presidential-election",
      "isYes": true,
      "amountUsd": "10.00",
      "reason": "Vance value"
    }'
  ```

  ```javascript JavaScript theme={null}
  const res = await fetch(`${process.env.MURMO_BASE}/api/v1/predictions/proposals`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.MURMO_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      groupId: "1a9d212a-de67-45f4-88f5-6e9f41e78dc0",
      eventId: "presidential-election-winner-2028",
      marketId: "will-jd-vance-win-the-2028-us-presidential-election",
      isYes: true,
      amountUsd: "10.00",
      reason: "Vance value",
    }),
  });
  const { data } = await res.json();

  let proposal = data.proposal;
  if (data.pending) {
    // Funds are committed; the buy is settling in the background. The proposal
    // flips to ACTIVE (filled) or FAILED (funds returned), typically under a minute.
    while (proposal.status === "PENDING") {
      await new Promise((r) => setTimeout(r, 3000));
      const poll = await fetch(
        `${process.env.MURMO_BASE}/api/v1/predictions/proposals/${proposal.id}`,
        { headers: { Authorization: `Bearer ${process.env.MURMO_API_KEY}` } },
      );
      proposal = (await poll.json()).data.proposal;
    }
  }
  console.log(proposal.id, proposal.status); // ACTIVE (or FAILED)
  if (data.trade) console.log(data.trade.tradeType, data.trade.totalCostUsd);
  ```

  ```python Python theme={null}
  import time

  res = requests.post(
      f"{base}/api/v1/predictions/proposals",
      headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
      json={
          "groupId": "1a9d212a-de67-45f4-88f5-6e9f41e78dc0",
          "eventId": "presidential-election-winner-2028",
          "marketId": "will-jd-vance-win-the-2028-us-presidential-election",
          "isYes": True,
          "amountUsd": "10.00",
          "reason": "Vance value",
      },
  )
  data = res.json()["data"]

  proposal = data["proposal"]
  if data["pending"]:
      # Funds are committed; the buy is settling in the background. The proposal
      # flips to ACTIVE (filled) or FAILED (funds returned), typically under a minute.
      while proposal["status"] == "PENDING":
          time.sleep(3)
          poll = requests.get(
              f"{base}/api/v1/predictions/proposals/{proposal['id']}",
              headers={"Authorization": f"Bearer {key}"},
          )
          proposal = poll.json()["data"]["proposal"]
  print(proposal["id"], proposal["status"])  # ACTIVE (or FAILED)
  if data["trade"]:
      print(data["trade"]["tradeType"], data["trade"]["totalCostUsd"])
  ```
</CodeGroup>

```json Response (synchronous fill) theme={null}
{
  "data": {
    "proposal": {
      "id": "f0c1...e9",
      "groupId": "1a9d212a-de67-45f4-88f5-6e9f41e78dc0",
      "createdById": "user_abc",
      "eventId": "presidential-election-winner-2028",
      "marketId": "will-jd-vance-win-the-2028-us-presidential-election",
      "outcomeMint": "216549008739...881356",
      "eventTitle": "Presidential Election Winner 2028",
      "marketTitle": "JD Vance",
      "eventImageUrl": "https://polymarket-upload.s3.us-east-2.amazonaws.com/presidential-election-winner-2028.png",
      "isYes": true,
      "status": "ACTIVE",
      "result": null,
      "didWin": null,
      "reason": "Vance value",
      "resolvedAt": null,
      "createdAt": "2026-07-06T18:04:11.000Z",
      "updatedAt": "2026-07-06T18:04:11.000Z"
    },
    "trade": {
      "id": "trd_001",
      "proposalId": "f0c1...e9",
      "groupId": "1a9d212a-de67-45f4-88f5-6e9f41e78dc0",
      "walletAddress": "5xY...addr",
      "tradeType": "BUY",
      "tokenAmount": "34.482758",
      "tokenDecimals": 6,
      "pricePerTokenUsd": "0.29",
      "totalCostUsd": "10",
      "venueFeeUsd": "0.284",
      "pnlPct": null,
      "pnlUsd": null,
      "txSignature": "4nP...ref",
      "createdAt": "2026-07-06T18:04:11.000Z"
    },
    "pending": false,
    "actionJobId": null
  }
}
```

```json Response (asynchronous fill) theme={null}
{
  "data": {
    "proposal": {
      "id": "f0c1...e9",
      "status": "PENDING",
      "eventId": "presidential-election-winner-2028",
      "marketId": "will-jd-vance-win-the-2028-us-presidential-election",
      "isYes": true,
      "result": null,
      "didWin": null
    },
    "trade": null,
    "pending": true,
    "actionJobId": "9f2c...41"
  }
}
```

<Note>
  `tokenAmount` is the count of **outcome contracts** you hold, already humanized using
  `tokenDecimals` (it is **not** named `...Raw`). Every `...Usd` field (`pricePerTokenUsd`,
  `totalCostUsd`, `venueFeeUsd`) is a human decimal string. On an opening BUY, `pnlPct` / `pnlUsd`
  are `null`.
</Note>

## Predicting more

`POST /api/v1/predictions/proposals/{id}/predict` adds to your position on an existing proposal. Body is
just `{ "amountUsd": "..." }` (required, a human USD string). Any member of the group can predict.
Geo-restricted like create, and the same two buy minimums apply (`AMOUNT_BELOW_MINIMUM`,
`MIN_ORDER_SIZE`).

The response is the same async-optimistic envelope, minus the `proposal` key:
`{ trade, pending, actionJobId }`. When `pending` is `true`, the buy settles in the background:
your position on `GET /proposals/{id}` grows once the fill confirms, and if the buy cannot complete
the funds are returned to your USDC balance. Predictions are only accepted while the proposal is
`ACTIVE`; anything else is a `409` with code `PROPOSAL_INACTIVE`. A proposal from the retired
legacy venue answers `409` `PROPOSAL_SELL_ONLY` — existing positions there can still sell and
claim, but no new predictions can enter.

<CodeGroup>
  ```bash curl theme={null}
  curl -X POST "$MURMO_BASE/api/v1/predictions/proposals/f0c1...e9/predict" \
    -H "Authorization: Bearer $MURMO_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{ "amountUsd": "5.00" }'
  ```

  ```javascript JavaScript theme={null}
  const res = await fetch(
    `${process.env.MURMO_BASE}/api/v1/predictions/proposals/${proposalId}/predict`,
    {
      method: "POST",
      headers: {
        Authorization: `Bearer ${process.env.MURMO_API_KEY}`,
        "Content-Type": "application/json",
      },
      body: JSON.stringify({ amountUsd: "5.00" }),
    },
  );
  const { data } = await res.json();
  if (data.pending) {
    console.log("Buy settling, job:", data.actionJobId);
  } else {
    console.log(data.trade.tokenAmount, "contracts at", data.trade.pricePerTokenUsd);
  }
  ```

  ```python Python theme={null}
  res = requests.post(
      f"{base}/api/v1/predictions/proposals/{proposal_id}/predict",
      headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
      json={"amountUsd": "5.00"},
  )
  data = res.json()["data"]
  if data["pending"]:
      print("Buy settling, job:", data["actionJobId"])
  else:
      print(data["trade"]["tokenAmount"], "contracts at", data["trade"]["pricePerTokenUsd"])
  ```
</CodeGroup>

```json Response (synchronous fill) theme={null}
{
  "data": {
    "trade": {
      "id": "trd_002",
      "proposalId": "f0c1...e9",
      "tradeType": "BUY",
      "tokenAmount": "16.666666",
      "tokenDecimals": 6,
      "pricePerTokenUsd": "0.3",
      "totalCostUsd": "5",
      "venueFeeUsd": "0.14",
      "pnlPct": null,
      "pnlUsd": null,
      "txSignature": "5aB...ref",
      "createdAt": "2026-07-06T18:30:00.000Z"
    },
    "pending": false,
    "actionJobId": null
  }
}
```

## Selling (cashing out)

`POST /api/v1/predictions/proposals/{id}/sell` sells part or all of **your** position. Never
geo-restricted. The body ([`AmountOrMaxBody`](/api-reference)) takes **exactly one** of:

| Field       | Effect                                                                                                                                |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| `amountUsd` | The desired USD value to sell. The server converts it to a contract amount **at the current price** and **caps it at your position**. |
| `max: true` | Sell your **entire** position on this proposal. When `max` is true, `amountUsd` is ignored.                                           |

<Warning>
  `amountUsd` on a sell is an **approximate target**, not a guarantee. The server sizes the sell from
  the current price, and the realized USD is determined at execution: it can differ from `amountUsd`
  as the price moves, and is capped at what you actually hold. To exit cleanly, prefer `max: true`.
</Warning>

Sells are sized against the venue's per-order minimum (typically 5 contracts):

* A partial sell **below** the minimum is floored **up** to the minimum, so you may sell slightly
  more than requested.
* A partial sell that would leave behind an unsellable sub-minimum remainder is extended to a
  **full exit** instead of stranding contracts you could never sell.
* A position **entirely below** the minimum cannot be sold (`400`, code `POSITION_BELOW_MIN`). No
  money moves; the position settles automatically when the market resolves.

Other sell failures: nothing to sell is a `422` (`NOTHING_TO_SELL`); no live market price to size
the sell is a `422` (`NO_MARKET_PRICE`, safe to retry).

<CodeGroup>
  ```bash Sell an amount theme={null}
  curl -X POST "$MURMO_BASE/api/v1/predictions/proposals/f0c1...e9/sell" \
    -H "Authorization: Bearer $MURMO_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{ "amountUsd": "5.00" }'
  ```

  ```bash Sell the whole position theme={null}
  curl -X POST "$MURMO_BASE/api/v1/predictions/proposals/f0c1...e9/sell" \
    -H "Authorization: Bearer $MURMO_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{ "max": true }'
  ```

  ```javascript JavaScript theme={null}
  // Approximate target
  await fetch(`${process.env.MURMO_BASE}/api/v1/predictions/proposals/${id}/sell`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.MURMO_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ amountUsd: "5.00" }),
  });

  // Exit fully
  await fetch(`${process.env.MURMO_BASE}/api/v1/predictions/proposals/${id}/sell`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${process.env.MURMO_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ max: true }),
  });
  ```

  ```python Python theme={null}
  # Approximate target
  requests.post(
      f"{base}/api/v1/predictions/proposals/{proposal_id}/sell",
      headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
      json={"amountUsd": "5.00"},
  )

  # Exit fully
  requests.post(
      f"{base}/api/v1/predictions/proposals/{proposal_id}/sell",
      headers={"Authorization": f"Bearer {key}", "Content-Type": "application/json"},
      json={"max": True},
  )
  ```
</CodeGroup>

```json Response theme={null}
{
  "data": {
    "trade": {
      "id": "trd_003",
      "proposalId": "f0c1...e9",
      "tradeType": "SELL",
      "tokenAmount": "15.625",
      "tokenDecimals": 6,
      "pricePerTokenUsd": "0.32",
      "totalCostUsd": "5",
      "venueFeeUsd": "0.136",
      "pnlPct": "4.38",
      "pnlUsd": "0.204062",
      "txSignature": "6cD...ref",
      "createdAt": "2026-07-06T19:00:00.000Z"
    }
  }
}
```

<Note>
  On a SELL, `pnlPct` and `pnlUsd` are populated (here `"4.38"` means **+4.38%**) and are **net of
  venue fees** on both the entry and the exit; `totalCostUsd` is the gross fill value before the
  fee. Percentages are decimal strings expressed as a percent, see
  [Money & precision](/concepts/money-and-precision#percentages).
</Note>

## Closing a proposal

`POST /api/v1/predictions/proposals/{id}/close` is **creator-only** and never geo-restricted. It
sells the creator's entire remaining position and moves the proposal into a sell-only (`CLOSED`)
state so members can exit but not add. Closing with nothing left to sell is a `422`
(`NOTHING_TO_SELL`); closing a proposal that is not `ACTIVE` is a `409` (`PROPOSAL_INACTIVE`).

```bash theme={null}
curl -X POST "$MURMO_BASE/api/v1/predictions/proposals/f0c1...e9/close" \
  -H "Authorization: Bearer $MURMO_API_KEY"
```

```json Response theme={null}
{
  "data": {
    "proposal": { "id": "f0c1...e9", "status": "CLOSED", "result": null, "didWin": null },
    "trade": {
      "id": "trd_004",
      "tradeType": "SELL",
      "totalCostUsd": "11.37",
      "venueFeeUsd": "0.309205",
      "pnlPct": "2.74",
      "pnlUsd": "0.2945"
    }
  }
}
```

## Claiming winnings

When the market resolves, the proposal's `status` becomes `RESOLVED`, `result` is set, and `didWin`
tells you the outcome. `POST /api/v1/predictions/proposals/{id}/claim` redeems what the resolution
owes you, with **no minimum**:

* **Winning** outcomes redeem at **\$1 per contract**.
* On an **INVALID** result (a 50/50 resolution), **both** sides redeem at **\$0.50 per contract**.
  `didWin` is `false` for both sides on INVALID, so do not treat `didWin: false` alone as "nothing
  to claim"; check `result` too.

Claiming is **idempotent**. Winnings may also be claimed for you server-side after resolution; in
that case (or on a repeat call) the endpoint returns the already-recorded CLAIM trade again instead
of failing. When there is genuinely nothing claimable, it is a `422` with code `NOT_CLAIMABLE`.

```bash theme={null}
curl -X POST "$MURMO_BASE/api/v1/predictions/proposals/f0c1...e9/claim" \
  -H "Authorization: Bearer $MURMO_API_KEY"
```

```json Response theme={null}
{
  "data": {
    "trade": {
      "id": "trd_005",
      "proposalId": "f0c1...e9",
      "tradeType": "CLAIM",
      "tokenAmount": "35.524424",
      "tokenDecimals": 6,
      "pricePerTokenUsd": "1",
      "totalCostUsd": "35.524424",
      "venueFeeUsd": "0",
      "pnlPct": "230.03",
      "pnlUsd": "24.760362",
      "txSignature": "7eF...ref",
      "createdAt": "2028-11-08T23:30:00.000Z"
    },
    "amountClaimedUsd": "35.524424"
  }
}
```

<Note>
  The claim response carries the trade **plus** a top-level `amountClaimedUsd`, the USD redeemed, as
  a full-precision decimal string. On a winning CLAIM, `pricePerTokenUsd` is `"1"`; on an INVALID
  claim it is `"0.5"`. Claims charge no venue fee (`venueFeeUsd` is `"0"`).
</Note>

## Proposal statuses

| Status     | Meaning                                                               | Visibility    |
| ---------- | --------------------------------------------------------------------- | ------------- |
| `PENDING`  | The opening buy is settling asynchronously; funds are committed.      | Creator only. |
| `ACTIVE`   | Open: members can predict, sell, and the creator can close.           | All members.  |
| `CLOSED`   | Sell-only: members can exit but not add.                              | All members.  |
| `RESOLVED` | The market resolved; `result` / `didWin` are set and claims are open. | All members.  |
| `FAILED`   | The opening buy could not complete; the funds were returned.          | Creator only. |

`PENDING` and `FAILED` proposals are private to their creator: other members get a `404` on the
detail endpoint, and the default (unfiltered) list feed hides `FAILED` entirely and shows `PENDING`
only to its creator.

## The full lifecycle

<Steps>
  <Step title="Discover an event">
    Browse or search the event-read endpoints to find an `eventId` (event slug) and a `marketId`
    (market slug) you want, and decide your side (`isYes`). Prices in these normalized payloads are
    already human dollar strings.
  </Step>

  <Step title="Create the proposal (opening prediction)">
    `POST /predictions/proposals` with `groupId`, `eventId`, `marketId`, `isYes`, and `amountUsd`
    (at least \$4 and the market's venue minimum). Leaders only. If `pending` is true, poll the
    proposal until it flips to `ACTIVE` (filled) or `FAILED` (funds returned).
  </Step>

  <Step title="Predict more or sell">
    Add with `POST .../{id}/predict` (`amountUsd`, may also return `pending: true`). Trim or exit
    with `POST .../{id}/sell`: `amountUsd` for an approximate target, or `max: true` to close your
    whole position.
  </Step>

  <Step title="Resolution">
    When the market resolves, the proposal's `status` flips to `RESOLVED` and `result` / `didWin`
    are filled in. (A creator can also `close` a proposal early to put it in sell-only mode.)
  </Step>

  <Step title="Claim">
    If you won (`didWin: true`), or the result is `INVALID` (both sides redeem at \$0.50),
    `POST .../{id}/claim` redeems your contracts for USDC and returns the CLAIM trade plus
    `amountClaimedUsd`. Repeat calls return the same recorded claim.
  </Step>
</Steps>

## Reading proposals and positions

These reads are **members-only**: every per-group prediction read asserts group membership, so a
non-member gets `403 NOT_GROUP_MEMBER`. See [Errors](/concepts/errors).

### List a group's proposals

`GET /api/v1/predictions/proposals?groupId=...` returns the group's prediction proposals, each with
your position. Optional `status` filter, one of `PENDING`, `ACTIVE`, `CLOSED`, `RESOLVED`, `FAILED`
(an unknown value is a `400`). `data` is an **array** of
[`PredictionWithPosition`](/api-reference). Remember the visibility rules: `PENDING` and `FAILED`
rows only ever appear for their creator.

```bash theme={null}
curl "$MURMO_BASE/api/v1/predictions/proposals?groupId=1a9d212a-...&status=ACTIVE" \
  -H "Authorization: Bearer $MURMO_API_KEY"
```

Each `PredictionWithPosition` item:

| Field                  | Type                 | Notes                                                                                             |
| ---------------------- | -------------------- | ------------------------------------------------------------------------------------------------- |
| `proposal`             | `PredictionProposal` | The proposal metadata (see table below).                                                          |
| `market`               | object \| null       | The normalized venue market (same shape as `markets[]` in event payloads); `null` if unavailable. |
| `userPosition`         | `PredictionPosition` | **Your** stake on this proposal.                                                                  |
| `remainingTokenAmount` | string \| null       | Your per-proposal FIFO remaining contracts, humanized.                                            |
| `createdBy`            | `UserRef`            | Who created the proposal.                                                                         |

### One proposal's detail

`GET /api/v1/predictions/proposals/{id}` returns `{ proposal, position }`. **`position` is `null`**
when you hold nothing on this proposal. A non-member gets `403`; an unknown id gets `404`, and so
does someone else's `PENDING` or `FAILED` proposal.

```bash theme={null}
curl "$MURMO_BASE/api/v1/predictions/proposals/f0c1...e9" \
  -H "Authorization: Bearer $MURMO_API_KEY"
```

```json Response theme={null}
{
  "data": {
    "proposal": { "id": "f0c1...e9", "isYes": true, "status": "ACTIVE", "marketTitle": "JD Vance" },
    "position": {
      "tokenAmount": "51.149424",
      "tokenDecimals": 6,
      "totalCostUsd": "15",
      "avgCostBasisUsd": "0.293259",
      "avgExitPriceUsd": null,
      "unrealizedPnlPct": "9.11",
      "unrealizedPnlUsd": "1.367816",
      "realizedPnlPct": "0",
      "realizedPnlUsd": "0",
      "currentValueUsd": "16.367816",
      "currentPriceUsd": "0.32"
    }
  }
}
```

<Note>
  The list endpoint nests your stake under **`userPosition`**, while the single-proposal detail
  endpoint nests it under **`position`** (and it's `null` when you hold nothing). Both are the same
  `PredictionPosition` shape.
</Note>

## Fees and PnL

Three rules cover every money field on trades and positions:

* `totalCostUsd` on a trade is the **gross** fill value (`tokenAmount × pricePerTokenUsd`).
* `venueFeeUsd` is the venue fee charged at match on that fill: on top of `totalCostUsd` on buys,
  deducted from delivery on sells. It is `"0"` when the market charges no taker fee, and on all
  claims (redemption is always fee-free). The fee rate varies by market category.
* `pnlPct` / `pnlUsd` on trades, and **every** PnL field on positions, are **net of venue fees**.

## Field reference

### `PredictionProposal`

| Field                     | Type              | Notes                                                                                                                    |
| ------------------------- | ----------------- | ------------------------------------------------------------------------------------------------------------------------ |
| `id`                      | string            | Proposal id.                                                                                                             |
| `groupId`                 | string            | Group the proposal belongs to.                                                                                           |
| `createdById`             | string            | Creator (a group leader).                                                                                                |
| `eventId`                 | string            | Event slug (legacy proposals may hold older identifier formats).                                                         |
| `marketId`                | string            | Market slug (the market within the event; the side is `isYes`).                                                          |
| `outcomeMint`             | string            | Venue outcome-token id for the chosen side, a long numeric string, resolved for you (older formats on legacy proposals). |
| `eventTitle`              | string \| null    | Resolved event title (never sent by you).                                                                                |
| `marketTitle`             | string \| null    | Resolved market title.                                                                                                   |
| `eventImageUrl`           | string \| null    | Event image.                                                                                                             |
| `isYes`                   | boolean           | `true` = YES side, `false` = NO side.                                                                                    |
| `status`                  | string            | `PENDING`, `ACTIVE`, `CLOSED`, `RESOLVED`, or `FAILED` (see [Proposal statuses](#proposal-statuses)).                    |
| `result`                  | string \| null    | Resolution outcome once settled: `YES`, `NO`, `INVALID` (50/50, both sides redeem at \$0.50), or `CANCELLED`.            |
| `didWin`                  | boolean \| null   | `null` until resolved; `false` for **both** sides on `INVALID` even though both can claim.                               |
| `reason`                  | string \| null    | Optional note from the creator (max 500 chars).                                                                          |
| `resolvedAt`              | date-time \| null | When it resolved.                                                                                                        |
| `createdAt` / `updatedAt` | date-time         | Timestamps.                                                                                                              |

### `PredictionTrade`

| Field              | Type                    | Notes                                                                                 |
| ------------------ | ----------------------- | ------------------------------------------------------------------------------------- |
| `id`               | string                  | Trade id.                                                                             |
| `proposalId`       | string                  | Parent proposal.                                                                      |
| `groupId`          | string                  | Group id.                                                                             |
| `walletAddress`    | string                  | Address the trade is recorded against.                                                |
| `tradeType`        | string                  | `BUY`, `SELL`, `CLAIM`, or `LOST`.                                                    |
| `tokenAmount`      | `DecimalString`         | Outcome contracts, **humanized** with `tokenDecimals` (not raw).                      |
| `tokenDecimals`    | integer                 | Decimals for the outcome token.                                                       |
| `pricePerTokenUsd` | `MoneyString`           | Per-contract price in USD. `"1"` on a winning CLAIM, `"0.5"` on an INVALID claim.     |
| `totalCostUsd`     | `MoneyString`           | **Gross** fill value (`tokenAmount × pricePerTokenUsd`), before the venue fee.        |
| `venueFeeUsd`      | `MoneyString`           | Venue fee charged at match on this fill; `"0"` on fee-free trades (including claims). |
| `pnlPct`           | `DecimalString` \| null | Percent PnL, **net of venue fees** (null on buys).                                    |
| `pnlUsd`           | `MoneyString` \| null   | USD PnL, **net of venue fees** (null on buys).                                        |
| `txSignature`      | string \| null          | Settlement reference for the fill, when present.                                      |
| `createdAt`        | date-time               | When the trade executed.                                                              |

### `PredictionPosition`

Your stake on a proposal. Every `...Usd` value is a `MoneyString`; every percent is a
`DecimalString`. **All PnL fields are net of venue fees.**

| Field                                   | Type                            | Notes                                                            |
| --------------------------------------- | ------------------------------- | ---------------------------------------------------------------- |
| `tokenAmount`                           | `DecimalString`                 | Outcome contracts held (humanized).                              |
| `tokenDecimals`                         | integer                         | Decimals for the outcome token.                                  |
| `totalCostUsd`                          | `MoneyString`                   | Total cost basis in USD.                                         |
| `avgCostBasisUsd`                       | `MoneyString`                   | Average entry price per contract.                                |
| `avgExitPriceUsd`                       | `MoneyString` \| null           | Average realized exit price (`null` before any exit).            |
| `unrealizedPnlPct` / `unrealizedPnlUsd` | `DecimalString` / `MoneyString` | Open PnL, net of venue fees.                                     |
| `realizedPnlPct` / `realizedPnlUsd`     | `DecimalString` / `MoneyString` | Closed PnL, net of venue fees (`"0"` before any exit).           |
| `currentValueUsd`                       | `MoneyString`                   | Mark-to-market value now.                                        |
| `currentPriceUsd`                       | `MoneyString` \| null           | Live per-contract mark; `null` when the market has no live book. |

## Errors you'll see

Business errors carry a stable `code`; branch on the code, not the message. Codes marked
*retryable* are safe to resend identically (no funds moved). See [Errors](/concepts/errors) for the
error shapes and the full catalog.

| Status | Codes                                                                                                                                                                                                      | When                                                                                                                                                                                                                                                                                                                        |
| ------ | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | `AMOUNT_BELOW_MINIMUM`, `MIN_ORDER_SIZE`, `POSITION_BELOW_MIN`, `MARKET_NOT_ACTIVE`, `VALIDATION_ERROR`                                                                                                    | Buy under \$4; buy under the market's venue minimum (the message includes the computed dollar minimum); position too small to sell; market not accepting orders; bad input (missing `q` / `groupId` / `amountUsd`, non-boolean `isYes`, bad money string, unknown `status` value, duplicate proposal for the same outcome). |
| `401`  |                                                                                                                                                                                                            | Missing or invalid API key.                                                                                                                                                                                                                                                                                                 |
| `403`  | `GEO_RESTRICTED`, `NOT_GROUP_MEMBER`, `NOT_GROUP_LEADER`                                                                                                                                                   | Create/predict from a restricted region; not a member of the group; not a leader on create (close is creator-only).                                                                                                                                                                                                         |
| `404`  | `PROPOSAL_NOT_FOUND`, `EVENT_NOT_FOUND`, `MARKET_NOT_FOUND`                                                                                                                                                | Unknown proposal id (or someone else's PENDING/FAILED proposal); unknown event slug; unknown market slug.                                                                                                                                                                                                                   |
| `409`  | `PROPOSAL_INACTIVE`, `PROPOSAL_SELL_ONLY`, `REQUEST_IN_PROGRESS` *(retryable)*                                                                                                                             | Proposal not open for that action; legacy-venue proposal accepts sells/claims but no new predictions; an identical operation is already in flight, retry shortly.                                                                                                                                                           |
| `422`  | `NOTHING_TO_SELL`, `NOT_CLAIMABLE`, `NO_POSITION`, `INSUFFICIENT_FUNDS`, `NO_MARKET_PRICE` *(retryable)*, `NO_LIQUIDITY` *(retryable)*, `ORDER_NOT_FILLED` *(retryable)*, `FUNDING_REFUNDED` *(retryable)* | Nothing to sell; nothing claimable; no position; not enough USDC; no live price to size the order; the book cannot fill the size right now; the order did not fill and the funds were released; the buy's funding failed and the money was returned, safe to retry.                                                         |

## Where to next

<CardGroup cols={2}>
  <Card title="Prediction agent" icon="robot" href="/examples/prediction-agent">
    Find an event, predict, and claim in one script.
  </Card>

  <Card title="Money & precision" icon="coins" href="/concepts/money-and-precision">
    Why every amount is a decimal string, and how to parse it.
  </Card>

  <Card title="Groups & proposals" icon="users" href="/concepts/groups-and-proposals">
    The shared social model behind every proposal.
  </Card>

  <Card title="Errors" icon="triangle-exclamation" href="/concepts/errors">
    The two error shapes and stable codes.
  </Card>

  <Card title="API Reference" icon="code" href="/api-reference">
    Every Predictions endpoint, parameter, and schema.
  </Card>
</CardGroup>
