> ## Documentation Index
> Fetch the complete documentation index at: https://docs.testnet.dev.adipredictstreet.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Partner Trades & Reconciliation

> Cross-wallet executed-trade feed and window-aggregate reconciliation for auditor / regulator-tech integrations.

## Audience

This surface is for **partners** running off-platform copies of the trade
log — typically auditors and regulator-tech integrations. It is **not**
for retail wallet flows; for a single user's own trades use
[`GET /api/me/trades`](/concepts/portfolio/trades).

Two endpoints:

1. [`GET /api/v1/partner/trades`](#trades-feed) — incremental, cross-wallet trade feed.
2. [`GET /api/v1/partner/reconciliation/snapshot`](#reconciliation-snapshot) — window-aggregate cross-check.

***

## Authentication

Both endpoints use API-key auth.

* Header: `X-Api-Key: ps_live_<keyId>_<secret>`
* Required scope: **`partner:trades:read`**
* The scope is **read-only by design** — admin refuses to issue a key
  that mixes `partner:trades:read` with any write scope (`orders:write`,
  `vault:write`). A partner using this surface cannot place orders or
  move funds with the same key.
* Privy JWT sessions are explicitly rejected on these endpoints — they
  are partner-only.

The scope is selectable in the admin-panel API-key issuance form
(checkbox alongside `markets:read`, `events:read`, etc.).

***

## Trades feed

Cross-wallet executed-trade feed. Cursor-paginated and incremental by
modification time.

```http theme={null}
GET /api/v1/partner/trades
X-Api-Key: ps_live_<keyId>_<secret>
```

### Query parameters

| Parameter       | Type            | Required | Default | Description                                                                  |
| --------------- | --------------- | -------- | ------- | ---------------------------------------------------------------------------- |
| `modifiedSince` | ISO-8601 string | Yes      | —       | Returns trades whose `max(createdAt, settledAt)` is at or after this value.  |
| `cursor`        | string          | No       | `null`  | Opaque token from the previous response's `nextCursor`. Pass back unchanged. |
| `limit`         | integer         | No       | `50`    | Page size. **Min 1, max 300.**                                               |
| `marketId`      | string          | No       | —       | Restrict results to a single market symbol.                                  |

### Response — `200 OK`

```ts theme={null}
{
  trades: PartnerTrade[];
  nextCursor: string | null;   // null when window fully drained
}
```

`PartnerTrade`:

```ts theme={null}
{
  tradeId: string;             // upsert key
  status: 'matched' | 'settlement_pending' | 'settled' | 'settlement_failed';
  marketSymbol: string;
  marketTitle: string | null;
  eventId: string | null;
  eventTitle: string | null;
  categoryTags: string[];      // tag-id list
  matchedAt: string;           // ISO-8601, immutable
  settledAt: string | null;    // ISO-8601 once on-chain confirmed
  txHash: string | null;
  price: string;               // decimal, USDC
  quantity: string;            // decimal, outcome-token units
  takerFee: PartnerTradeFee;
  makerFee: PartnerTradeFee;   // always { amount: '0', currency: 'USDC', tokenId: null }
  taker: PartnerTradeSide;
  maker: PartnerTradeSide;
}
```

`PartnerTradeFee`:

```ts theme={null}
{
  amount: string;                          // decimal
  currency: 'USDC' | 'OUTCOME_TOKENS';
  tokenId: string | null;                  // non-null iff currency='OUTCOME_TOKENS'
}
```

`PartnerTradeSide` (identical shape on `taker` and `maker`):

```ts theme={null}
{
  side: 'BUY' | 'SELL';
  walletAddress: string;        // user EOA the order was signed from
  vaultAddress: string;         // smart-contract wallet that held collateral / outcome tokens
  orderId: string;
  orderType: 'LIMIT' | 'MARKET';
  orderTimeInForce: string;     // 'GTC' | 'IOC' | 'FOK' | 'GTD'
  orderPlacedAt: string;        // ISO-8601
  orderOriginalQty: string;
  orderRemainingQty: string;    // remaining at the time of THIS trade
}
```

### Example

```http theme={null}
GET /api/v1/partner/trades?modifiedSince=2026-05-01T00:00:00Z&limit=50
X-Api-Key: ps_live_97f73727c6cd6859_xxx
```

```jsonc theme={null}
{
  "trades": [
    {
      "tradeId": "1042",
      "status": "settled",
      "marketSymbol": "WC26-GROUP-A-ARG-MEX-1",
      "marketTitle": "Will Argentina beat Mexico?",
      "eventId": "evt_argmex_q1",
      "eventTitle": "Argentina vs Mexico",
      "categoryTags": ["world-cup", "group-stage"],
      "matchedAt": "2026-05-01T12:34:56.123456Z",
      "settledAt": "2026-05-01T12:35:12.789012Z",
      "txHash": "0xabc...",
      "price": "0.65",
      "quantity": "100.0",
      "takerFee": { "amount": "0.42", "currency": "USDC", "tokenId": null },
      "makerFee": { "amount": "0",    "currency": "USDC", "tokenId": null },
      "taker": {
        "side": "BUY",
        "walletAddress": "0x014eab...",
        "vaultAddress":  "0x3ef9a2...",
        "orderId": "ord_xyz",
        "orderType": "LIMIT",
        "orderTimeInForce": "GTC",
        "orderPlacedAt": "2026-05-01T12:30:00.000000Z",
        "orderOriginalQty": "100.0",
        "orderRemainingQty": "0"
      },
      "maker": { /* same shape */ }
    }
  ],
  "nextCursor": "eyJ0cyI6IjIwMjYtMDUtMDFUMTI6MzQ6NTYuMTIzNDU2WiIsImlkIjoiMTA0MiJ9"
}
```

### Behaviour notes

* **Idempotency.** A single trade may surface twice — once when matched
  (`status='matched'`, `settledAt: null`), again when settlement lands
  (`status='settled'`, `settledAt: <iso>`). Partners are expected to
  **upsert by `tradeId`**.
* **In-walk re-emit.** The same trade can re-emit within a single cursor
  walk if its settlement lands between page N and N+1. Same upsert rule
  applies.
* **`modifiedSince` must stay constant** across all cursor pages of a
  single drain.
* **Settlement failures are emitted** with `status='settlement_failed'`
  — partners must NOT filter these out client-side.

### Status codes

| Code  | When                                                                              |
| ----- | --------------------------------------------------------------------------------- |
| `200` | Success (incl. empty `trades: []` page)                                           |
| `400` | Malformed query (see [Errors](#error-envelope))                                   |
| `401` | Missing or invalid `X-Api-Key`                                                    |
| `403` | Authenticated but key lacks `partner:trades:read`, or JWT used instead of API-key |
| `429` | Rate limit exceeded                                                               |
| `5xx` | Internal failure (transient — retry with exponential backoff)                     |

### Rate limit

* Per API-key: **600 requests / minute** (default; can be raised per
  partner via `partner.rate_limit_per_min`).
* Per IP: **1000 requests / minute** (defence-in-depth on egress IP).

***

## Reconciliation snapshot

Window-aggregate cross-check for a partner's local copy of trades.
Returns counts, sums, and a deterministic content hash over the trade
ids in the window.

```http theme={null}
GET /api/v1/partner/reconciliation/snapshot
X-Api-Key: ps_live_<keyId>_<secret>
```

### Query parameters

| Parameter | Type            | Required | Description                                                          |
| --------- | --------------- | -------- | -------------------------------------------------------------------- |
| `from`    | ISO-8601 string | Yes      | Inclusive lower bound. Compared against `max(createdAt, settledAt)`. |
| `to`      | ISO-8601 string | Yes      | Exclusive upper bound. Window length capped at 24 hours.             |

The `from` / `to` bounds use the same `max(createdAt, settledAt)`
expression as the trade feed's `modifiedSince`. The window aligns 1:1
with trades the partner pulled via `?modifiedSince=`.

### Response — `200 OK`

```ts theme={null}
{
  window: { from: string; to: string };
  totals: {
    tradeCount: number;
    totalQuantity: string;             // decimal, outcome-token units
    totalUsdcVolume: string;           // SUM(price * quantity), USDC
    totalTakerFeesUsdc: string;        // taker fees in USDC over window
    totalTakerFeesOutcomeTokens: Array<{
      tokenId: string;
      amount: string;
    }>;                                // taker fees by outcome token
  };
  fingerprint: {
    algorithm: 'sha256-csv-trade-ids';
    value: string;                     // 64-char lowercase hex
  };
  computedAt: string;                  // ISO-8601
}
```

### Fingerprint algorithm

`sha256` of the trade ids in the window, sorted **lexicographically as
decimal strings** and joined by commas:

```python theme={null}
import hashlib
ids = sorted(str(t.tradeId) for t in trades_in_window)   # string sort
fp  = hashlib.sha256(",".join(ids).encode()).hexdigest()
```

<Warning>
  Sort as strings, **not** as integers. `["1", "10", "2"]`, not
  `["1", "2", "10"]`. The server matches the default sort order in
  Python / JavaScript / Go.
</Warning>

Empty-window canonical (no trades in `[from, to)`):
`e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`
(SHA-256 of the empty string).

### Stability

| Field                                                           | Stability                                                                                                                                                                                                             |
| --------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `tradeCount`, `totalQuantity`, `totalUsdcVolume`, `fingerprint` | **Stable** for past windows (`to <= now`). Trade timestamps are not back-dated.                                                                                                                                       |
| `totalTakerFeesUsdc`, `totalTakerFeesOutcomeTokens`             | **May increase** across calls in live windows that contain `matched` (in-flight) trades. The fee-ledger row is written at settlement time. Past windows in which every trade has reached a terminal state are stable. |

### Status codes

| Code  | When                                                                |
| ----- | ------------------------------------------------------------------- |
| `200` | Success                                                             |
| `400` | `invalid_window` (from ≥ to or missing) / `window_too_large` (>24h) |
| `401` | Missing or invalid `X-Api-Key`                                      |
| `403` | Scope missing or JWT used instead of API-key                        |
| `429` | Rate limit exceeded                                                 |
| `5xx` | Internal failure                                                    |

### Rate limit

* Per API-key: **60 requests / minute** (recon is meant for periodic
  polling, not high-frequency).
* Per IP: **200 requests / minute**.

***

## Error envelope

All errors follow the project-standard JSON shape:

```ts theme={null}
{
  code: string;
  message: string;
}
```

### Codes used by these endpoints

| HTTP | `code`                                   | Meaning                                                 |
| ---- | ---------------------------------------- | ------------------------------------------------------- |
| 400  | `invalid_modified_since`                 | `modifiedSince` not a valid ISO-8601 timestamp          |
| 400  | `invalid_cursor`                         | Cursor is malformed / not produced by us                |
| 400  | `invalid_window`                         | `from` ≥ `to` or either field missing                   |
| 400  | `window_too_large`                       | Reconciliation window > 24 hours                        |
| 401  | `api_key_required`                       | `X-Api-Key` header missing                              |
| 403  | `api_key_required_not_jwt`               | Authenticated as JWT user; this surface is API-key-only |
| 403  | `api_key_scope_missing`                  | API key valid but lacks `partner:trades:read` scope     |
| 429  | (no body code; see `Retry-After` header) | Rate limit exceeded                                     |
| 5xx  | `internal_error`                         | Transient — retry with exponential backoff              |
