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

ParameterTypeDescription
statusstringComma-separated: open,filled,cancelled
sidestringBUY or SELL
outcomeintOutcome index filter
sincestringISO 8601 timestamp (orders after this)
sortstringcreated_at or updated_at (default)
limitintMax results (default 50, max 200)
offsetintSkip 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

ParameterTypeDescription
market_idUUIDFilter by market
sidestringBUY or SELL (taker side)
sincestringISO 8601 timestamp (created after this)
limitintMax results (default 50, max 100)
offsetintSkip 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:

FieldTypeMeaning
party_order_idUUID or nullThe viewer's exact order in this match
party_sideBUY, SELL, or nullThe viewer's economic side
party_outcome_indexint or nullThe viewer's outcome, not necessarily the taker's
notional_usdc_microstring or nullPositive matched USDC amount in 1e-6 units
token_quantity_atomicstring or nullPositive 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.

ParameterTypeDescription
market_idUUIDFilter by market
typestringfill, placed, cancelled, or expired; omitted means all
limitintDefault 50; effective maximum 100
offsetintSkip 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

ParameterTypeDescription
cursorstringOpaque pagination token returned as next_cursor on a prior response.
limitintPage 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} or null
  • session_timeout_minutes{60, 120, 240, 480} or null
{
  "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 list
  • GET /v1/user/trades - Trade list with market_id, limit, and offset; includes the canonical party fields above

Use the /v2 versions for advanced filtering and pagination.