User Data
Endpoints for querying your profile, orders, trades, positions, fiat sessions, notifications, referrals, and rebates. All endpoints require authentication.
Profile
GET /v1/user/profile
Returns the authenticated user's profile including wallet addresses, display name, and account settings.
Response
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"email": "user@example.com",
"display_name": "trader1",
"wallet_address": "0xabc...",
"linked_wallets": ["0xabc...", "0xdef..."],
"role": "user",
"created_at": "2024-01-15T10:30:00Z"
}
Orders (Enhanced)
GET /v1/user/orders/v2
Scope: read:account
Query your orders with advanced filters and pagination.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
status | string | Comma-separated: open,filled,cancelled |
side | string | BUY or SELL |
outcome | int | Outcome index filter |
since | string | ISO 8601 timestamp (orders after this) |
sort | string | created_at or updated_at (default) |
limit | int | Max results (default 50, max 200) |
offset | int | Skip N results |
Response
{
"orders": [
{
"id": "...",
"order_hash": "0x...",
"market_id": "...",
"side": "BUY",
"price": "0.50000000",
"status": "open",
"remaining": "1000000",
"filled_amount": "0",
"time_in_force": "GTC",
"post_only": false,
"created_at": "2024-03-01T00:00:00Z"
}
],
"total": 150,
"limit": 50,
"offset": 0
}
Trades (Enhanced)
GET /v1/user/trades/v2
Scope: read:account
Query your trade history with filters.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
market_id | UUID | Filter by market |
side | string | BUY or SELL (taker side) |
since | string | ISO 8601 timestamp (created after this) |
limit | int | Max results (default 50, max 100) |
offset | int | Skip N results |
Response
{
"trades": [
{
"id": "...",
"market_id": "...",
"taker_order_id": "...",
"maker_order_id": "...",
"price": "0.50000000",
"amount": "1000000",
"taker_side": "BUY",
"outcome_index": 0,
"role": "taker",
"party_order_id": "...",
"party_side": "BUY",
"party_outcome_index": 0,
"notional_usdc_micro": "1000000",
"token_quantity_atomic": "2000000",
"settlement_status": "pending",
"matched_at": "2024-03-01T12:30:00Z"
}
],
"total": 42,
"limit": 50,
"offset": 0
}
Canonical Party Amounts (Both Trade Versions)
GET /v1/user/trades and /v1/user/trades/v2 expose the same authenticated
party projection:
| Field | Type | Meaning |
|---|---|---|
party_order_id | UUID or null | The viewer's exact order in this match |
party_side | BUY, SELL, or null | The viewer's economic side |
party_outcome_index | int or null | The viewer's outcome, not necessarily the taker's |
notional_usdc_micro | string or null | Positive matched USDC amount in 1e-6 units |
token_quantity_atomic | string or null | Positive gross outcome-token amount in 1e-6 units, before fees |
The five fields are available together or explicitly null together. V2's
additional role is taker, maker, or null on the same boundary; V1 retains
its legacy role. Old backend versions omit the additions: treat missing or
contradictory evidence as unavailable, never as zero or a legacy-price fallback.
Standard BUY/SELL matches share the same USDC/token pair across the two parties.
For complementary BUY/BUY matches, both sides are BUY, their outcomes are
opposite, and each has its own USDC contribution against the gross minted token
quantity. Explicitly allowed self-trades retain one row with taker precedence;
history pagination does not invent a second maker row.
The pair describes one matched fragment. It is not a limit-order value,
order-wide average, fee amount, net token delivery, settlement confirmation, or
realized profit. V2 exposes its separate settlement_status; V1 does not, and
its legacy tx_signature is not confirmation. A price comparison should use
100 × notional_usdc_micro / token_quantity_atomic, with exact integer amount
parsing, and attribute the row by party_order_id. Client/server clock comparison
and the legacy price are not attribution or accounting evidence.
The web indication comparison displays the latest observed fragment for the exact submitted order as Last fill. Its bounded history lookup is not a complete order aggregate. Missing canonical fields withhold the comparison.
Legacy fields remain unchanged for compatibility: V1 quantity and V2
amount use the taker order's maker denomination (BUY collateral, SELL tokens),
while legacy outcome and price do not express every party's view. V1's historical
maker-side flip is wrong for complementary BUY/BUY rows. Do not use these fields
instead of the canonical projection.
Activity
GET /v1/user/activity
Scope: read:account
Returns { activity, total, limit, offset } for matched fills and order
placements, cancellations and expirations. Deposits and withdrawals are not
rows in this endpoint: the portfolio merges it with the separate
GET /v1/user/fund-movements stream.
| Parameter | Type | Description |
|---|---|---|
market_id | UUID | Filter by market |
type | string | fill, placed, cancelled, or expired; omitted means all |
limit | int | Default 50; effective maximum 100 |
offset | int | Skip N results; default 0 |
Fill rows expose the same canonical party and amount fields described above,
plus party_outcome_name, the persisted market label at the validated party
index. These six fields are available together or explicitly null together.
Invalid identity, amount evidence or outcome labels withhold the group. Non-fill
rows have all six null. Legacy fields, including role, retain their historical
behavior; they are not fallback accounting evidence.
The two amount strings use 1e-6 units: USDC and gross outcome tokens. Standard and complementary fills use the same viewer projection as trade history; self-trades retain one row with taker precedence. A placement, cancellation or expiration does not represent matched volume or an executed cash movement.
The web tables label Gross tokens and Matched USDC. Matches are unsigned
and neutral, not red/green cash debits/credits. CSV exports preserve exact
six-decimal matched values; missing amounts are blank. Values too large for
safe numeric display remain unavailable on screen without losing exact export
values. This endpoint has no settlement status, and a tx_signature is not
confirmation. Matched amounts are before fees, not settlement or realized profit.
Positions
GET /v1/user/positions
Returns all positions for the authenticated user across all markets.
Response
[
{
"id": "...",
"market_id": "...",
"outcome_index": 0,
"quantity": "5000000",
"average_entry_price": "0.45000000",
"realized_pnl": "0",
"claimed": false
}
]
Redeemable Positions
GET /v1/user/redeemable
Returns positions in resolved markets that can be claimed (redeemed for USDC).
Response
[
{
"market_id": "...",
"outcome_index": 0,
"quantity": "5000000",
"payout": "5000000",
"resolution": "YES",
"resolved_at": "2024-03-15T18:00:00Z"
}
]
Claim Winnings
POST /v1/user/positions/:marketId/claim
Initiates an on-chain redemption of winning positions in a resolved market. This triggers a transaction on Polygon to transfer USDC to your wallet.
Response
{
"message": "claim initiated",
"market_id": "...",
"tx_hash": "0x..."
}
Mark Position Claimed
POST /v1/user/positions/:marketId/mark-claimed
Marks a position as claimed after an external on-chain redemption (e.g., if you called the contract directly).
Response
{
"message": "position marked as claimed"
}
Fiat Sessions
List Sessions
GET /v1/user/fiat/sessions
Returns all on-ramp and off-ramp sessions for the authenticated user.
Get Session
GET /v1/user/fiat/sessions/:id
Returns details of a specific fiat session.
Create On-Ramp Session
POST /v1/user/fiat/onramp/sessions
Creates a new fiat-to-crypto on-ramp session (deposit USDC).
Complete On-Ramp Session
POST /v1/user/fiat/onramp/sessions/:id/complete
Marks an on-ramp session as complete after payment confirmation.
Create Off-Ramp Session
POST /v1/user/fiat/offramp/sessions
Creates a new crypto-to-fiat off-ramp session (withdraw to bank).
Durable Off-Ramp Transfer
GET /v1/user/fiat/offramp/sessions/:id/transfer
POST /v1/user/fiat/offramp/sessions/:id/transfer/authorization
POST /v1/user/fiat/offramp/sessions/:id/transfer/submit
POST /v1/user/fiat/offramp/sessions/:id/cancel
Coinbase is launched with an opaque, session-specific partnerUserRef, so a
concurrent cash-out cannot be mistaken for this session. The backend creates
one transfer intent after Coinbase locks the destination and amount. The user's linked wallet signs the exact EIP-3009
authorization, and the signature is persisted before the backend relays it to
Polygon. 4rho never fabricates or signs the customer's USDC authorization; the
dedicated cash-out EOA only pays gas for the outer relay transaction and shares
no nonce stream with another role. The exact unsigned envelope is persisted
before signing, and exact signed bytes plus their computable hash are persisted
before broadcast. Crashes and RPC timeouts rebroadcast the same bytes and
reconcile by the authorization nonce.
Return-page mounts call only GET .../transfer. Its transfer_state is
awaiting_provider, waiting_for_wallet, submission_pending, submitted_on_chain,
provider_funding_reconciling, confirmed, provider_processing, complete, expired, or
recoverable_error. provider_funding_reconciling means Coinbase supplied a transaction that
4rho is independently verifying, or its reported hash conflicts with another durable cash-out and
is quarantined for operator review; it does not claim the saved user relay was submitted. When can_submit is
false, clients must not show or open a send action. The backend reconciles a
missing or replaced transaction hash from the authorization nonce and requires
32 confirmations before treating the transfer as confirmed.
{
"session_id": "9f3c…",
"intent_id": "6a7e…",
"transfer_state": "submitted_on_chain",
"can_submit": false,
"message": "Transfer submitted. Do not send again while on-chain evidence is settling.",
"authorization": { "nonce": "0x…", "digest": "0x…", "signature": "0x…" },
"tx_evidence": {
"submitted_tx_hash": "0x…",
"effective_tx_hash": "0x…",
"confirmations": 4,
"required_confirmations": 32
}
}
Authorization and relay-retry mutations both include the returned intent_id.
They are safe under repeated delivery. Cancellation is allowed only before a
wallet authorization is persisted.
Refresh Launch Token
POST /v1/user/fiat/onramp/sessions/:id/launch-token
POST /v1/user/fiat/offramp/sessions/:id/launch-token
Mints a fresh Coinbase sessionToken + provider launch URL for an existing session. Use when the original launch URL has expired and the user wants to resume the flow without creating a brand-new session.
{
"session_id": "9f3c…",
"status": "initiated",
"launch_url": "https://pay.coinbase.com/buy/select-asset?sessionToken=…"
}
Get Off-Ramp Transaction
GET /v1/user/fiat/offramp/sessions/:id/transaction
Polls Coinbase for the off-ramp transaction details. While Coinbase hasn't observed a transaction yet, returns the pending shape:
{
"session_id": "9f3c…",
"status": "awaiting_quote",
"message": "Awaiting transaction"
}
Once Coinbase issues the transaction, returns the observed shape with the
backend-owned transfer intent. The client must use the nested transfer
authorization contract rather than constructing a free-form ERC-20 send:
{
"session_id": "9f3c…",
"status": "created",
"to_address": "0xCoinbase…",
"asset": "USDC",
"network": "polygon",
"sell_amount": { "value": "100.00", "currency": "USDC" },
"tx_hash": "",
"created_at": "2026-05-22T10:00:00Z",
"transfer": {
"intent_id": "6a7e…",
"transfer_state": "waiting_for_wallet",
"can_submit": true,
"authorization": { "nonce": "0x…", "digest": "0x…" }
}
}
Rebates
GET /v1/user/rebates
Returns accrued fee rebates for the authenticated user, newest first.
Optional query params: page, per_page (max 100), status.
accrued means owed, not paid. There is no payout rail yet, so no rebate has
been settled and no other status is written.
Response
{
"rebates": [
{
"id": "...",
"user_id": "...",
"trade_id": "...",
"market_id": "...",
"rebate_bps": 25,
"rebate_amount_usdc": "12.500000",
"status": "accrued",
"credited_at": null,
"created_at": "2026-03-01T12:00:00Z"
}
],
"total": 1,
"page": 1,
"per_page": 20,
"summary": {
"accrued": { "total_usdc": "12.5", "count": 1 }
}
}
Referrals
Get Referral Info
GET /v1/user/referral
Returns the user's referral code and referral status.
Response
{
"referral_code": "ABC123",
"referrals_count": 5,
"is_referred": true,
"referred_by": "XYZ789"
}
Get Referral Earnings
GET /v1/user/referral/earnings
Returns earnings generated from referrals.
Applying a Referral Code
Referrals are signup-only. The redemption path is the optional referral_code field on POST /v1/auth/privy (or a 4rho_referral httpOnly cookie set by visiting /refer/[code]); both attach the referral atomically inside the signup handler. There is no post-signup POST /v1/user/referral/redeem endpoint - it was retired with the #3778 Privy-only migration. A user can only ever be linked to one referrer.
Notifications
List Notifications
GET /v1/notifications
Returns notifications for the authenticated user (order fills, market resolutions, etc.) in (created_at DESC, id DESC) order. The response also carries the user's TOTAL unread count so a bell-badge consumer doesn't need a second round-trip on each inbox open.
Query Parameters
| Parameter | Type | Description |
|---|---|---|
cursor | string | Opaque pagination token returned as next_cursor on a prior response. |
limit | int | Page size (default 20, max 100). |
Response
{
"items": [
{
"id": "...",
"type": "order_filled",
"title": "Order filled",
"body": "Your BUY order was filled at 0.52",
"read_at": null,
"created_at": "2024-03-01T14:30:00Z"
}
],
"next_cursor": null,
"unread_count": 3
}
Unread Count
GET /v1/notifications/unread-count
Lightweight count-only endpoint for bell-badge polling (Redis-cached, no list payload).
{ "unread_count": 3 }
Mark Notification Read
POST /v1/notifications/:id/read
Marks a single notification as read.
Mark All Notifications Read
POST /v1/notifications/mark-all-read
Marks every unread notification as read. Response includes the number of rows flipped:
{ "marked": 3 }
Achievements
GET /v1/user/achievements
Scope: read:account
Returns the achievement chips granted to the authenticated user. New eligibility is computed and granted on each call - there's no background worker, so the response is always up to date.
Response
[
{ "id": "joined", "granted_at": "2026-01-15T10:30:00Z" },
{ "id": "first_trade", "granted_at": "2026-01-15T11:02:14Z" },
{ "id": "markets_traded_10", "granted_at": "2026-02-03T18:44:00Z" },
{ "id": "markets_traded_100", "granted_at": "2026-04-21T09:12:00Z" },
{ "id": "sharp", "granted_at": "2026-05-10T22:00:00Z" },
{ "id": "top_5pct_nfl", "granted_at": "2026-05-17T14:00:00Z" }
]
Possible id values: joined, first_trade, markets_traded_10, markets_traded_100, sharp (≥ 55% win rate over ≥ 20 closed positions), top_5pct_nfl (top 5% of NFL traders by P&L over a 30d window, min 5 closed positions in that category).
The endpoint always returns a JSON array - [] for users with no chips, never null.
Rolling 24h P&L
GET /v1/user/portfolio/pnl-24h
Scope: read:account
Returns the user's available USDC balance plus their rolling 24-hour P&L delta computed from cached position marks. Powers the Pro mode StatusBar. Response is memoized per-user for 30s.
Response
{
"available_balance_usdc": "1245.67",
"pnl_24h_usdc": "+42.31",
"pnl_24h_pct": 3.51,
"computed_at": "2026-05-22T10:00:00Z"
}
available_balance_usdc is null when the balance is unknown (no chain reader wired, or the on-chain read failed); a "0.00" is an observed zero, never a placeholder.
pnl_24h_usdc is signed ("+42.31" / "-12.04") and rounded to 2dp as a string to avoid float-precision drift in the UI. pnl_24h_pct is 0 when the baseline (positions at now - 24h) is zero, never Inf or NaN.
Responsible Gaming
Self-imposed reality-check intervals, session-timeout warnings, and trading timeouts. The timeout endpoints back the cooldown self-service UI; when an active timeout exists every order-placement and amend route returns 423 Locked + Retry-After + code TRADING_TIMEOUT_ACTIVE until it expires.
Get Preferences
GET /v1/user/rg/preferences
Scope: read:account
{
"reality_check_interval_minutes": 30,
"session_timeout_minutes": 120
}
Both fields are nullable - null means the feature is off for the user.
Update Preferences
PUT /v1/user/rg/preferences
Scope: manage:account
Allowed values:
reality_check_interval_minutes∈{15, 30, 60, 120}ornullsession_timeout_minutes∈{60, 120, 240, 480}ornull
{
"reality_check_interval_minutes": 60,
"session_timeout_minutes": null
}
Returns the same shape as GET. Out-of-range values return 400 INVALID_INPUT.
Get Active Trading Timeout
GET /v1/user/rg/timeout
Scope: read:account
Returns the active trading-timeout row, or literal null when none is active.
{
"id": "9f3c…",
"started_at": "2026-05-22T10:00:00Z",
"ends_at": "2026-05-23T10:00:00Z",
"duration_label": "24h"
}
Start a Trading Timeout
POST /v1/user/rg/timeout
Scope: manage:account
Body:
{ "duration_label": "24h" }
duration_label ∈ "24h" | "7d" | "30d" | "custom". When "custom", supply custom_hours (integer hours). Returns 201 with the timeout row, or 409 TIMEOUT_ALREADY_ACTIVE if one is still in effect.
Cancel the Active Timeout
POST /v1/user/rg/timeout/cancel
Scope: manage:account
Sets cancelled_at = NOW() on the active row. Returns 404 NO_ACTIVE_TIMEOUT when nothing is active.
Legacy Endpoints
The original order and trade endpoints are still available at:
GET /v1/user/orders- Original order listGET /v1/user/trades- Trade list withmarket_id,limit, andoffset; includes the canonical party fields above
Use the /v2 versions for advanced filtering and pagination.