> ## Documentation index
> Fetch the complete documentation index at: https://docs.predictefy.com/llms.txt
> Use it to discover every available page before exploring further.

# Monitor a multi-venue portfolio

> Check capability first, read balances and positions per venue, and know which surface each number came from.

Positions live on venues, not on Predictefy. Reading them across venues means asking each one
what it will actually serve before asking it for anything.

## Ask what is available first

`getAccountCapabilities` is the call that comes before the others. It reports, per resource,
what this venue can do:

```sh
curl -s "$PREDICTEFY_API_URL/v1/accounts/polymarket/capabilities" \
  -H "Authorization: Bearer pk_live_YOUR_KEY"
```

It returns four resources — `balances`, `positions`, `openOrders`, `fills` — each with a
`state`, a `served` flag and `notes`. `state` is one of:

| State                 | Meaning                                                       |
| --------------------- | ------------------------------------------------------------- |
| `public`              | Readable without the account holder's credentials              |
| `owner_auth_required` | Needs the account owner's own authentication                   |
| `derived`             | Reconstructed by Predictefy rather than served by the venue    |
| `not_supported`       | The venue does not expose it at all                            |

Branch on this. `not_supported` is a correct answer about the world — do not retry it, and do not
substitute another venue's number. `derived` is real but is our reconstruction, not the venue's
statement, and should be labelled as such wherever a user sees it.

## Read positions and balances

```sh
curl -s "$PREDICTEFY_API_URL/v1/accounts/polymarket/ACCOUNT_ID/positions?limit=100" \
  -H "Authorization: Bearer pk_live_YOUR_KEY"
```

The path carries **both** the venue and the account id — `/v1/accounts/{venue}/{accountId}/…`.
The same shape serves `/balances`, `/positions`, `/open-orders` and `/fills`, each taking `limit`
and `cursor`.

A position row:

```json
{
  "venue": "polymarket",
  "accountId": "0xabc…",
  "positionId": "pos_18f2",
  "marketId": "0x7d3f…",
  "canonicalMarketId": "polymarket:0x7d3f…",
  "outcomeId": "0x91aa…",
  "side": "yes",
  "size": 250,
  "avgEntryPrice": 0.41,
  "markPrice": 0.44,
  "unrealizedPnl": 7.5,
  "realizedPnl": 0,
  "status": "open",
  "asOf": "2026-08-12T14:31:04.882Z"
}
```

Everything above `raw` is required and normalized identically across venues, so one renderer
works everywhere. `raw` carries the venue's own payload when you need something the normalized
shape does not cover.

Note both id forms: `marketId` is the venue's, `canonicalMarketId` is `{venue}:{marketId}` and is
what you use as a cross-venue key. See [Identifiers](/guides/market-ids/).

## The aggregate view

`getPortfolio` rolls this up for one address:

```sh
curl -s "$PREDICTEFY_API_URL/v1/portfolio?address=0xabc…&venues=polymarket,kalshi" \
  -H "Authorization: Bearer pk_live_YOUR_KEY"
```

It takes `address` and an optional `venues` filter, and returns `venues` keyed by venue — each
with its own `balances` and `positions` — plus `totals` carrying `markValueUsd` and a `byVenue`
breakdown, and an `asOf` for the whole snapshot.

Use `totals.markValueUsd` for a headline number and `totals.byVenue` when you need to show where
the value sits. The top-level `asOf` covers the aggregate; individual rows carry their own.

## Two surfaces, and they are not the same

This is the mistake worth avoiding.

- **`/v1/accounts/{venue}/…`** is the **hosted account** surface: what a venue reports about an
  account, read-only.
- **`/v1/exec/{venue}/positions`** is the **execution service** surface: positions derived from
  orders you placed through Predictefy. Its rows are `ExecDerivedPosition` — `venue`, `marketId`,
  `outcomeId`, `netSize` — and nothing else. It is deliberately narrow.

They answer different questions and will legitimately disagree: the account surface sees
everything on the venue, the execution surface sees only what came through us. Comparing them
and calling the difference a bug is a misreading. Pick the one that matches the question you are
answering, and say which one your UI is showing.

`/v1/exec/{venue}/balance` is likewise the execution service's view, not the venue's account
balance.

## Building the monitor

```js
async function venueSnapshot(venue, accountId) {
  const caps = await api(`/v1/accounts/${venue}/capabilities`);

  if (caps.data.positions.state === 'not_supported') {
    // Honest gap. Render it as unavailable, not as zero positions.
    return { venue, positions: null, reason: 'not_supported' };
  }

  const positions = await api(`/v1/accounts/${venue}/${accountId}/positions`, { limit: 100 });
  return {
    venue,
    positions: positions.data,
    derived: caps.data.positions.state === 'derived',
  };
}
```

An empty array means "ran and found nothing". `not_supported` means "cannot run here". Rendering
the second as the first tells the user they hold no positions on a venue where we simply cannot
look — see [Capability-honest data](/guides/honest-data/).

## Cost

Account reads are per venue per resource, so a portfolio across five venues is at least five
calls before pagination. `getPortfolio` is one call for the aggregate and is cheaper than
assembling it yourself when you only need totals. Current weights are in
[Credits & billing](/guides/credits/).

## Related

- [Accounts & funding](/guides/accounts/) — the account surface in full
- [Trading & execution](/guides/trading/) — the execution service and its own position view
- [Capability-honest data](/guides/honest-data/) — why `not_supported` is an answer
