Skip to main content

Overview

Rendering a position looks like a solved problem. Take size, take price, multiply, show a number. Prediction markets break it in two places. First, a position can be finished without being closed: the event happened, the outcome is decided, and the money is still not yours. Second, losing positions go to exactly zero, which most portfolio UIs treat as a rendering bug and hide. Both produce the same support ticket: a user looking at a number that does not match what they can actually do. This guide covers modelling positions so that never happens.
Read time: about 13 minutes. Front-end or product engineering context assumed.

TL;DR

  • Positions arrive with cost basis already computed. Do not reconstruct it from fills.
  • Unrealized PnL is a mark on what you hold. Realized PnL is a cashflow model, measuring USDC that actually moved.
  • Positions have four states: open, awaiting resolution, resolved-unredeemed, redeemed.
  • A losing position settles at $0 and that is correct, not a bug. It is also a realized loss that matters for tax.
  • Use the Trade API for current holdings, the Data API for history and any public wallet.
  • Numbers are JSON strings. Render with a decimal type.

What you will do

  • Fetch positions with cost basis from the Trade API
  • Distinguish realized from unrealized correctly, and explain the difference to users
  • Implement the four-state model with the right action for each
  • Handle losing positions without hiding them
  • Pull historical views from the Data API for charts and closed positions
  • Format probabilities so they read as both a price and a chance

What you will need

Knowledge
  • Comfort with a front-end framework, and a decimal library
Tools and access
  • A Bravado API key with trade.read for the user’s own account
  • Nothing at all for public wallets, since Data API reads are open

Positions arrive with cost basis

average_entry_price is computed by a share-level FIFO engine replaying the wallet’s full history, not averaged from recent fills.
Do not recompute cost basis client-side from the trade log. Splits, merges, and redemptions are not ordinary buys and sells. A split mints a complete outcome set from collateral; a merge destroys one. Treating those as trades produces a basis that disagrees with the chain, and the disagreement grows with account age.

Realized and unrealized answer different questions

Bravado’s realized PnL is a cashflow model. It measures USDC that entered and left the wallet, net of fees, rather than inferring profit from price differences. That is why it reconciles against a bank-statement view of the account, and why the tax endpoints can be built on it. For history across a wallet’s whole life rather than current holdings:
That works on any public address, so a portfolio view of someone else’s wallet needs no permission from them.

The four-state model

This is the core of the guide.

Why two states are not enough

Collapse awaiting_resolution into “closed” and you tell the user they have money. They do not: the outcome is not final, and nothing can be done. Collapse resolved_unredeemed into “open” and you hide the one action that actually matters, claiming the collateral. The user’s money sits unclaimed because your UI never surfaced a button.
Exact field names for the resolution flags depend on the response shape; confirm against the positions reference. The four-state distinction is what matters, not the field name.
Redeeming is a normal mutating call:

Losing positions go to zero

A losing outcome settles at $0 per share. There is no partial recovery and nobody to sell to. Portfolio UIs frequently treat this as bad data and hide the row, or render it as an error. Both are wrong:
  • The loss is real and realized. Hiding it makes the portfolio total wrong.
  • It is a disposition for tax purposes, and one that looks nothing like a trade. Tooling that only reads fills misses it entirely. See Generate a tax statement.
Show it plainly, with the reason:

Historical views

The Trade API gives you current holdings. The Data API gives you everything else, and works on any public wallet: Category exposure is the most under-used of these in product terms. It shows whether a trader is broadly capable or good at exactly one thing, which is genuinely interesting to a user looking at their own record and essential to anyone evaluating a leader to copy.

Formatting

Prices are decimal probabilities, so 0.62 is 62 cents and a 62% implied chance. Rendering 62¢ (62%) teaches the model without a tooltip.
parseFloat will eventually give you a total that disagrees with Polymarket by a cent, and the user will file it as a bug against your product. Values arrive as strings specifically to prevent that. See Numeric conventions.

Handle 503 as a state

Data API statement endpoints return 503 with available: false while a wallet is still being computed. For a wallet nobody has queried before, this is expected.
Show a processing state and poll. An error toast is wrong; nothing failed.

Wrapping up

Two things separate a prediction market portfolio from a spot one: positions finish before they close, and losers go to exactly zero. Model the first with four states and the right action on each. Show the second plainly rather than hiding it. The rest is ordinary formatting work, provided you use a decimal type.

Frequently asked questions

They are in resolved_unredeemed. The outcome is final and the shares are worth a dollar each, but the collateral is not in the wallet until redeemed. Surface a redeem action rather than showing available cash.
Almost certainly splits, merges, or redemptions being treated as ordinary trades. They are not. Use the average_entry_price returned with the position.
No. A losing outcome settling at zero is a real, realized loss, and it belongs in both the portfolio total and any tax export.
collateral_value is what the position is worth at the current mark. unrealized_pnl is that value minus what you paid. One is a level, the other is a change.
Yes. Every Data API read works on any public address with no permission from the wallet owner and no funded account of your own.
PMWAS and statement endpoints compute a wallet’s full history on first request. available: false means still working. Poll rather than failing.

Resources