Monitor a multi-venue portfolio
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
Section titled “Ask what is available first”getAccountCapabilities is the call that comes before the others. It reports, per resource,
what this venue can do:
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
Section titled “Read positions and balances”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:
{ "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.
The aggregate view
Section titled “The aggregate view”getPortfolio rolls this up for one address:
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
Section titled “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}/positionsis the execution service surface: positions derived from orders you placed through Predictefy. Its rows areExecDerivedPosition—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
Section titled “Building the monitor”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.
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.
Related
Section titled “Related”- Accounts & funding — the account surface in full
- Trading & execution — the execution service and its own position view
- Capability-honest data — why
not_supportedis an answer