Users
Users
List operator users and retrieve a user's Portfolio as JSON, read-only inspection HTML, or launch-capable HTML.
Path
GET /api/operator/users
Required scope: users:read
Query params
All list query parameters are optional.
| Parameter | Type | Requirement | Values/default |
|---|---|---|---|
status | string | Optional | active, suspended, or disabled. |
search | string | Optional | Case-insensitive match against external user ID or display name. |
external_user_id | string | Optional | Exact external user ID. |
created_from | datetime | Optional | Inclusive lower bound; any JavaScript-parseable date/time. |
created_to | datetime | Optional | Inclusive upper bound; any JavaScript-parseable date/time. |
sort | string | Optional | created_at_desc (default), created_at_asc, or external_user_id_asc. |
page | positive integer | Optional | Default 1. |
page_size | positive integer | Optional | Default 50, maximum 100. |
Response fields
idexternal_user_idpolynion_user_iddisplay_namestatusmetadatalast_seen_atcreated_atupdated_at
Portfolio JSON data
GET /api/operator/users/portfolio
Required scope: users:read
Use this read-only endpoint when your backend needs the Portfolio data without HTML or launch links. It does not create or update the operator user, and it does not create launch tokens.
This JSON endpoint is unchanged by the HTML product selector: it remains the Polynion Market Portfolio contract below and does not gain Polyflight product or ticket pagination controls.
Query params
| Parameter | Type | Requirement | Values/default |
|---|---|---|---|
external_user_id | string | Required | Stable operator-side user ID. |
trade_page | positive integer | Optional | Default 1, maximum 10000. |
trade_page_size | positive integer | Optional | Default 20, maximum 100. |
balance_page | positive integer | Optional | Default 1, maximum 10000. |
balance_page_size | positive integer | Optional | Default 20, maximum 100. |
trade_sort_by | string | Optional | createdAt (default), sharesDelta, or creditsDelta. |
trade_sort_direction | string | Optional | desc (default) or asc. |
Example
curl -sS \
-H "Authorization: Bearer ${OPERATOR_API_KEY}" \
"https://your-beacon-origin.example.com/api/operator/users/portfolio?external_user_id=operator-user-123&trade_page=1&trade_page_size=20&balance_page=1&balance_page_size=20"
Response
{
"operator_user": {
"id": 42,
"external_user_id": "operator-user-123",
"polynion_user_id": 987,
"display_name": "Rina",
"status": "active"
},
"generated_at": "2026-07-10T12:00:00.000Z",
"currency_code": "IDR",
"wallet": {
"provider": "rest_v1",
"currency_code": "IDR",
"balance_minor": 250000,
"available": true,
"refreshed_at": "2026-07-10T12:00:00.000Z"
},
"portfolio_value": {
"positions_count": 0,
"total_value_minor": 0,
"realized_pnl_minor": 0,
"unrealized_pnl_minor": 0
},
"metrics": {
"markets_predicted": 0,
"win_rate_pct": 0,
"lifetime_volume_minor": 0
},
"positions": [],
"open_positions": [],
"resolved_positions": [],
"trade_history": {
"items": [],
"pagination": {
"page": 1,
"page_size": 20,
"total": 0,
"total_pages": 1
},
"sort": {
"by": "createdAt",
"direction": "desc"
}
},
"balance_mutations": {
"items": [
{
"id": 4321,
"market": {
"id": 123,
"slug": "example-market",
"title": "Will this market resolve Yes?",
"translations": {}
},
"operation_type": "trade",
"direction": "debit",
"status": "succeeded",
"amount_minor": -50000,
"currency_code": "IDR",
"created_at": "2026-07-10T11:59:00.000Z"
}
],
"pagination": {
"page": 1,
"page_size": 20,
"total": 1,
"total_pages": 1
}
}
}
Response details:
positionscontains every operator-visible position used for Portfolio totals.open_positionsandresolved_positionsare convenience subsets of the same objects.- Position objects include market identity/status, labels/translations, shares, redeemed shares, BPS prices, raw
yes_net_credits/no_net_credits, and operator-currency cost/value/PnL fields. - Use the explicit per-outcome cost fields:
*_gross_cost_minor,*_fee_minor, and*_net_cost_minor. Gross cost is processed Mini App buy gross debits minus gross sell proceeds before fee. Fee is always non-negative. Net cost aggregates each trade after its fee: buy net is gross debit minus fee, while sell net is gross proceeds minus fee and remains negative in cost accounting. Because buy and sell fees move the signed net in opposite directions, mixed-flow aggregate net cost is not simply aggregate gross cost minus total fee. A negative gross/net cost means cumulative sell proceeds exceed buy costs. metrics.lifetime_volume_minoris the absolute sum of historical gross costs before fee across the operator user's processed Mini App trades. It uses stored operator-currency amounts and does not reconvert Credits at the current FX rate.*_resolution_payout_minorprefers succeeded wallet-credit amounts for processed auto-redemptions, falling back to the stored item amount when no succeeded credit is found. Current or pending winning shares use projected payout rules: Credits flooring, current FX, and the operator FX fee. Stored processed amounts are not revalued at current FX, but their presence alone does not prove reconciliation.*_resolved_pnl_minoris resolution payout minus gross cost; a losing outcome's PnL is the negative gross cost. Open-only and resolved-only PnL fields arenulloutside their applicable market state.yes_cost_minor/no_cost_minorremain backward-compatible legacy current-FX conversions. New integrations should use the explicit gross/fee/net fields.- Explicit cost fields use historical processed-order money where available; positions/trades without matching historical order evidence fall back to current-FX conversions. Portfolio PnL is therefore a position-oriented view, not the confirmed-wallet-cash report from Market PnL.
- Trade items include market context, side/outcome, decimal
shares_delta, legacy convertedamount_minor,gross_cost_minor,fee_minor,net_cost_minor, rawfee_credits_total, status, and timestamp. Trade cost fields use the same sign convention as positions: buys are positive costs; sells are negative proceeds. For sells,gross_cost_minoris proceeds before fee andnet_cost_minoris the smaller proceeds amount actually credited after fee. balance_mutationsis a bounded, latest-first (created_at DESC, then operation ID descending) projection of balance operations for this operator user. Items expose only operation ID, market identity/title/translations, derivedoperation_type, direction, status, signedamount_minor, currency, and timestamp.operation_typeistradewhen the operation has an order andresolutionwhen it has a redemption item. No separate persisted type is required.balance_mutations.items[].amount_minoris the direction-signed operation amount:debitandcredit_reversalare negative;creditanddebit_reversalare positive. Checkstatus: planned, failed, or unknown operations do not prove an actual balance change. Currency comes fromcurrency_code.- All
*_minorfields usecurrency_code. Share fields use decimal shares. BPS fields use basis points. Fields ending in_creditsremain Polynion Credits. wallet.available: falsemeans the balance could not be refreshed. Checkwallet.unavailable_reason; do not treatbalance_minor: nullas zero.- Responses include
Cache-Control: no-storeandVary: Authorization. 404 OPERATOR_USER_NOT_FOUNDmeans the external user id does not exist for the authenticated operator. This endpoint never creates it.
Portfolio positions, Market trade history, cost totals, and metrics apply current operator market eligibility (ownership, geo, visibility, and resolution horizon). Hidden or otherwise ineligible markets can disappear from these views. Use the dedicated trade/report endpoints for historical reconciliation rather than treating a Portfolio total as an all-history cash ledger.
The JSON balance projection retains its legacy behavior: it does not accept a product filter and can include unlinked/Polyflight operations as operation_type: "unknown" with market: null. Do not infer Opinion-only cash from it. HTML views separate the two products; use Balance operations for explicit product-filtered JSON reporting.
HTML products and ticket accounting
Both HTML endpoints below embed a top-level Polynion Market / Polyflight selector. product selects the initial product; it is optional and defaults to opinion (Polynion Market), preserving the existing Market view. Product and section switching happens locally inside the returned HTML, without browser authentication requests or operator API fetches.
For product: "polyflight", the same tab values select Open Tickets (open), Resolved Tickets (closed), Ticket History (history), and Balance Mutation (balance):
- Open Tickets shows admitted, active tickets. Failed or unconfirmed purchase attempts are not active tickets and do not inflate active stake.
- Resolved Tickets separates the terminal game result (
won,lost, orrefunded) from wallet delivery state. A final win with an owed payout is still a resolved win; a loss acknowledgement can have a zero wallet amount. - Ticket History shows the user's admitted tickets only. Failed or unconfirmed purchase attempts do not appear in any ticket list or ticket count; their operations appear only in Balance Mutation.
- Ticket tables show exact gross stake, fee, net stake, and potential payout. Potential payout is a ticket's stored possible outcome amount, not a confirmed credit.
- The summary shows active stake, lifetime stake, confirmed credits, pending credits, and realized result PnL. Active stake is native IDR committed to active tickets, not a fictional resale value or Market portfolio valuation. Realized result PnL describes terminal ticket results separately from whether their wallet credits have been delivered.
- Balance Mutation shows native-IDR wallet operations and distinguishes confirmed effects from pending or unknown effects. An unknown outcome is not proof that a debit or credit succeeded; neither a ticket result nor a pending obligation should be treated as a confirmed wallet balance change.
Polyflight money remains native IDR: minor-unit amounts are exact decimal integer strings, not floating-point JSON numbers. Preserve that precision, including values above JavaScript's safe integer range. Stake, fee, net stake, potential payout, and historical wallet amounts are stored historical amounts, not current-FX conversions or estimates of resale value.
Summary formulas are independent of the displayed page: active stake sums purchased ticket stakes; lifetime stake sums all admitted ticket stakes; confirmed credits sum paid win/refund obligations; pending credits sum win/refund obligations not yet paid. Realized result PnL sums a won ticket's authoritative payout minus its gross stake, subtracts lost stakes, and assigns zero to refunded tickets. A zero-amount loss acknowledgement contributes no credit. Market FX-provider unavailability is shown in the Market section without blocking native-IDR Polyflight history; database failures remain errors.
ticket_page and ticket_page_size independently page each of Open Tickets, Resolved Tickets, and Ticket History; balance_page and balance_page_size independently page each product's wallet list. Existing trade pagination and sorting continue to apply to Polynion Market.
Ticket pagination accepts only whole, safe positive integers: digit strings in GET, and numeric integers or digit strings in POST. Fractional values, malformed strings, booleans, and arrays return 400 INVALID_TICKET_PAGE or 400 INVALID_TICKET_PAGE_SIZE before user lookup, host resolution, or launch effects. Valid values above the configured page/page-size limit are capped at that limit.
Historical Polyflight data remains readable when the product is disabled. Disabled or otherwise unavailable products have no launch CTA. Both HTML views use user-facing projections: operator terms, share BPS, and private wallet or proof evidence are not exposed.
Internal Portfolio HTML inspection
GET /api/operator/users/portfolio-view
Required scope: users:read
Use this operator-internal endpoint when a backend needs ready-to-render HTML for inspecting an existing operator user's Portfolio. This is not a public or user-side launch flow. It is strictly read-only:
- identifies an existing operator user by required
external_user_id - never creates or updates an operator user
- never resolves a Mini App launch host
- never creates launch tokens or launch URLs
- renders no launch CTA links or buttons; the Market Open table omits the entire
Actioncolumn - adds a distinct
Market IDheader and plain integer cell to Market Open, Resolved, Trade History, and Balance Mutation - keeps market titles, Market IDs, and Polyflight flight and ticket IDs as plain text, never launch links
Query params
| Parameter | Type | Requirement | Values/default |
|---|---|---|---|
external_user_id | string | Required | Stable operator-side user ID owned by the authenticated operator. |
product | string | Optional | opinion (default, Polynion Market) or polyflight; selects the initial embedded product. |
tab | string | Optional | open (default), closed, history, or balance. |
trade_page | positive integer | Optional | Default 1, maximum 10000. |
trade_page_size | positive integer | Optional | Default 20, maximum 100. |
ticket_page | positive integer | Optional | Default 1, maximum 10000; applies independently to each Polyflight ticket list. |
ticket_page_size | positive integer | Optional | Default 20, maximum 100; applies independently to each Polyflight ticket list. |
balance_page | positive integer | Optional | Default 1, maximum 10000. |
balance_page_size | positive integer | Optional | Default 20, maximum 100. |
trade_sort_by | string | Optional | createdAt (default), sharesDelta, or creditsDelta. |
trade_sort_direction | string | Optional | desc (default) or asc. |
locale | string | Optional | en, id, es, or zh-CN; defaults to the operator locale. |
Example
curl -sS -G \
-H "Authorization: Bearer ${OPERATOR_API_KEY}" \
--data-urlencode "external_user_id=operator-user-123" \
--data-urlencode "product=polyflight" \
--data-urlencode "tab=history" \
--data-urlencode "ticket_page=1" \
--data-urlencode "ticket_page_size=20" \
"https://your-beacon-origin.example.com/api/operator/users/portfolio-view" \
-o portfolio-inspection.html
Response headers:
Content-Type: text/html; charset=utf-8
Cache-Control: no-store
X-Robots-Tag: noindex, nofollow
Vary: Authorization
Errors and integration rules
400 INVALID_EXTERNAL_USER_IDmeansexternal_user_idis missing, blank, or invalid.404 OPERATOR_USER_NOT_FOUNDmeans that external user does not exist for the authenticated operator. The endpoint does not create it.- Invalid enum or pagination controls return
400 INVALID_<QUERY_PARAMETER>, for exampleINVALID_PRODUCT,INVALID_TAB,INVALID_LOCALE, orINVALID_TICKET_PAGE_SIZE. - Call this endpoint from a trusted backend only. Never expose an operator API key in browser code.
- Do not cache the returned HTML. Stream it to an internal operator surface or render it in a sandboxed frame.
- Do not use this endpoint to launch the Mini App. Use the launch-capable POST endpoint below only when short-lived Market Trade or Polyflight flight launch links are required.
Launch-capable Portfolio HTML view
POST /api/operator/users/portfolio-view
Required scopes: users:read and launch:write
This endpoint returns text/html, not JSON. Use it when your backend needs a ready-to-render, view-only Portfolio view for one operator user. It includes the embedded Polynion Market / Polyflight selector described above. The default Polynion Market view keeps the same main sections as the Mini App Portfolio page:
OpenResolvedTrade HistoryBalance Mutation- balance, portfolio value, and portfolio metrics
The rendered view is intentionally not a general navigation surface:
- market titles are plain text, not links
- trade-history market titles are plain text, not links
- open-position
Tradebuttons are single-use Mini App launch links that redirect to the matching market after launch - resolved positions show both the user's bought outcome and the market's winning
Resolution; redemption is automatic, so the Resolved table has noActioncolumn - Open, Resolved, and Trade History show gross cost, fee, and net cost in the authenticated operator's configured currency; resolved PnL uses gross cost
- Balance Mutation shows Timestamp, Market, Type, Status, and Amount. Amount is the signed user balance effect in operator-currency minor units: debits and credit reversals are red/negative; credits and debit reversals are green/positive.
- Balance Mutation timestamps retain their ISO instant in
<time datetime>markup. The inline client script rendersDD Month YYYY, HH:mm:ssin the viewing browser's local timezone; the initial no-script fallback is deterministic UTC text and is not presented as browser-local.
The Polyflight sections use the native-IDR accounting described above. Flight and ticket IDs remain plain text. Flight launch links are generated only when Polyflight is enabled for the authenticated IDR operator; unavailable-product CTAs are absent, while historical tickets remain readable. Each Polyflight link is single-use, uses the same short TTL as POST /api/operator/launch, and is bound to the intended flight through launch metadata: product: "polyflight", the operator's enabledProducts, and the flight-specific redirectPath. Existing Market Trade links and sections retain their behavior.
Request body
{
"launch_parameters": {
"external_user_id": "operator-user-123",
"display_name": "Rina",
"locale": "id",
"metadata": {}
},
"product": "opinion",
"tab": "open",
"trade_page": 1,
"trade_page_size": 20,
"ticket_page": 1,
"ticket_page_size": 20,
"balance_page": 1,
"balance_page_size": 20,
"trade_sort_by": "createdAt",
"trade_sort_direction": "desc"
}
launch_parameters is required and uses the same fields as POST /api/operator/launch:
launch_parameters field | Type | Requirement | Notes |
|---|---|---|---|
external_user_id | string | Required | Stable operator-side user ID. The endpoint upserts this operator user before rendering. |
display_name | string | Optional | Latest display-name snapshot. |
locale | string | Optional | Default Portfolio locale. Use en, id, es, or zh-CN. |
host | string | Optional | Must be active and allowed for the operator. Omit unless the Provider assigned one. |
metadata | object | Optional | Merged into operator-user metadata. |
View options:
| Field | Type | Requirement | Values/default |
|---|---|---|---|
product | string | Optional | opinion (default, Polynion Market) or polyflight; selects the initial embedded product. |
tab | string | Optional | open (default), closed, history, or balance. |
trade_page | positive integer | Optional | Default 1, maximum 10000. |
trade_page_size | positive integer | Optional | Default 20, maximum 100. |
ticket_page | positive integer | Optional | Default 1, maximum 10000; applies independently to each Polyflight ticket list. |
ticket_page_size | positive integer | Optional | Default 20, maximum 100; applies independently to each Polyflight ticket list. |
balance_page | positive integer | Optional | Default 1, maximum 10000. |
balance_page_size | positive integer | Optional | Default 20, maximum 100. |
trade_sort_by | string | Optional | createdAt (default), sharesDelta, or creditsDelta. |
trade_sort_direction | string | Optional | desc (default) or asc. |
locale | string | Optional | Overrides launch_parameters.locale for the rendered document only. |
Example
curl -sS \
-X POST \
-H "Authorization: Bearer ${OPERATOR_API_KEY}" \
-H "Content-Type: application/json" \
-d '{
"launch_parameters": {
"external_user_id": "operator-user-123",
"display_name": "Rina",
"locale": "id",
"metadata": {}
},
"product": "polyflight",
"tab": "history",
"ticket_page": 1,
"ticket_page_size": 20
}' \
"https://your-beacon-origin.example.com/api/operator/users/portfolio-view" \
-o portfolio.html
Response headers include:
Content-Type: text/html; charset=utf-8
Cache-Control: no-store
X-Robots-Tag: noindex, nofollow
Vary: Authorization
Integration rules
- Call this endpoint from your backend only. Never expose an operator API key in browser code.
- Stream or inject the returned HTML into your server-rendered page.
- Do not cache the returned HTML; balances, positions, and embedded launch links are user-specific and time-sensitive.
- Market
Tradelinks and enabled-IDR Polyflight flight links contain single-use launch tokens with the same short TTL asPOST /api/operator/launch; refresh the Portfolio HTML when the page is stale. Product switching itself is local and does not make browser authentication or operator API requests. 400 INVALID_LAUNCH_PARAMETERSmeans the requiredlaunch_parametersobject is missing or invalid.400 INVALID_OPERATOR_HOSTmeanslaunch_parameters.hostdoes not belong to the operator.
Dashboard Playground behavior
The Operator Dashboard API Playground keeps the exact response available in Raw and renders Preview only inside a sandboxed iframe srcdoc. Preview injects a restrictive CSP, does not grant allow-same-origin, cannot access dashboard cookies or the parent DOM, and permits available Market Trade and Polyflight flight links only as normal new tabs. The embedded product selector works locally within that frame. This preview is an operator troubleshooting aid; backend integrations must continue to call the endpoint server-side.
