# How to read Polymarket positions and activity by wallet

> Polymarket splits its API across three hosts, and wallet positions live on only one of them. The Data API takes an address and returns what that wallet holds — at 150 requests per 10 seconds, counted per IP.

- Source: https://papaproxy.net/blog/polymarket-data-api-positions.php
- Published: 2026-08-08
- Author: Alex Young
- Category: API & data · PapaProxy.net Blog

---

## Key takeaways

- Wallet positions live only on `data-api.polymarket.com`; Gamma serves market metadata and the CLOB serves books, prices and your own authenticated ledger.
- Query the account's **User Profile Address**, which depends on the account type — EOA, `POLY_PROXY`, `GNOSIS_SAFE` or `POLY_1271`; for a plain EOA the funder is the EOA itself. The wrong address returns an empty array rather than an error.
- `/positions` defaults to `sizeThreshold=1.0` and `limit=100` (max 500, `offset` up to 10,000) — set the threshold to 0, page through `offset`, and read `outcomeIndex` alongside `size`.
- `/activity` accepts twelve types, not six: DEPOSIT, WITHDRAWAL, YIELD, MAKER_REBATE, TAKER_REBATE and REFERRAL_REWARD join the trading ones, deposits and withdrawals need `excludeDepositsWithdrawals=false`, `start`/`end` are epoch seconds, and `offset` past 5,000 is rejected with a 400.
- Limits are IP-based: `/positions` and `/closed-positions` allow 150 req/10s, `/trades` 200, and the Data API overall 1,000. Documentation describes queueing, but our August 2026 test found immediate 429s without gradual slowdown.
- Reading is fully public; L1/L2 authentication — with the signature type and funder address matched to the account type — is needed only for trading and for your own ledger.

## Where the Data API lives

The base URL is `https://data-api.polymarket.com`, separate from Gamma (markets and events) and from the CLOB (order books and trading). The mental model that saves the most time: Gamma tells you what exists, the CLOB tells you what it costs, and the Data API tells you who is holding it.

The data-api positions endpoint is a plain GET that needs no key, no signature and no account — the Data API documents its routes as public:

TextCopy code

```text
GET https://data-api.polymarket.com/positions?user=0x...&sizeThreshold=50&limit=500
```

The user positions endpoint takes `user` as its only required parameter and layers useful filters on top. `market` accepts one or more condition IDs as a comma-separated list, `eventId` does the same for events and is mutually exclusive with `market`. `sizeThreshold` sets the minimum position size to include and defaults to 1.0, which quietly hides dust — set it to 0 if you need everything. `redeemable` and `mergeable` narrow the response to positions in those states, and pagination runs on `limit` (default 100, maximum 500) plus `offset` (maximum 10,000). Sorting is available through `sortBy` — CURRENT, INITIAL, TOKENS, CASHPNL, PERCENTPNL, TITLE, RESOLVING, PRICE or AVGPRICE, defaulting to TOKENS — with `sortDirection` as ASC or DESC.

A positions endpoint example returns objects with `proxyWallet`, `conditionId`, `asset`, `size`, `avgPrice`, `initialValue`, `currentValue`, `cashPnl`, `percentPnl`, `realizedPnl`, `curPrice`, `redeemable`, `mergeable`, `negativeRisk`, plus market context — `title`, `slug`, `outcome`, `outcomeIndex`, `oppositeOutcome`, `endDate`.

Two fields deserve attention before you build on them. The `user` parameter is documented as a **User Profile Address**, and which address that is depends on the account type: Polymarket supports EOA accounts alongside `POLY_PROXY`, `GNOSIS_SAFE` and `POLY_1271` signature types, and for a plain EOA the funder is the EOA itself. So "always query the proxy" is the wrong rule — the right one is to query the address that holds the position for that account type, which is also what the response echoes back in `proxyWallet`. Querying a signing key that isn't the profile address returns an empty array rather than an error, so log which address you asked for. And `outcomeIndex` matters as much as `size`: the same market has a Yes and a No side, so a position is only meaningful together with the outcome it belongs to.

PythonCopy code

```python
import requests

DATA = "https://data-api.polymarket.com"
PAGE = 500                                    # documented maximum

def positions(profile_address, min_size=0):
    out, offset = [], 0
    while offset <= 10000:                    # documented offset ceiling
        r = requests.get(f"{DATA}/positions",
                         params={"user": profile_address, "sizeThreshold": min_size,
                                 "limit": PAGE, "offset": offset,
                                 "sortBy": "CURRENT", "sortDirection": "DESC"},
                         timeout=10)
        r.raise_for_status()
        page = r.json()
        out.extend(page)
        if len(page) < PAGE:                  # short page means this was the last one
            break
        offset += PAGE
    return out

for p in positions("0x0000000000000000000000000000000000000000")[:5]:
    print(f"{p['title'][:45]:<45} {p['outcome']:<4} "
          f"size={p['size']:>10.2f} value=${p['currentValue']:>10.2f} pnl=${p['cashPnl']:>9.2f}")
```

What to look at: `sizeThreshold=0` overrides the default of 1.0, so dust isn't filtered out silently, and the loop pages through `offset` instead of stopping at the first 500 — a wallet with a wide book would otherwise be truncated without any error. The loop stops on a short page and refuses to walk past the documented `offset` ceiling of 10,000, which is the real limit on how deep this endpoint goes. The call returns an empty list for a wallet with nothing open — the same shape you'd get from querying an address that isn't the account's profile address.

## Getting the list of wallets

Positions are useless without knowing whose positions to read, and that's what the leaderboard surface is for. The Data API exposes a leaderboard of top traders ranked by profit or volume over a time window, alongside a builder leaderboard covering apps that route order flow through Polymarket's builder programme. The leaderboard API documentation is thinner than the rest of the surface, and shapes here have moved more than once — check the current response against your parser rather than trusting a field list copied from a blog post, this one included.

The operational pattern is the same as on any venue with this pairing: rankings move slowly, positions move constantly. Fetch the leaderboard on a slow schedule, cache the wallet list, and spend your request budget on the position calls that actually change. If you need a portfolio total rather than a position breakdown, `/value` returns the aggregate value for a wallet in one call — considerably cheaper than pulling every position and summing it yourself.

## Reading trades and activity

Positions tell you where a wallet stands now; the activity endpoint tells you how it got there. `GET /activity?user=0x...` returns on-chain activity ordered by timestamp, newest first, and its `type` filter accepts twelve values: TRADE, SPLIT, MERGE, REDEEM, REWARD, CONVERSION, DEPOSIT, WITHDRAWAL, YIELD, MAKER_REBATE, TAKER_REBATE and REFERRAL_REWARD. That list is worth reading twice, because only the first of them is a trade: splits and merges move between complementary outcome tokens, redemptions settle resolved markets, rebates and yield are income rather than activity, and treating all of them as trades will inflate any volume metric you build.

Three parameters catch people out. `start` and `end` are expressed in **seconds**, not milliseconds — the opposite convention from most exchange APIs. Deposits and withdrawals are excluded by default: `excludeDepositsWithdrawals` defaults to `true` and that default wins even when you name DEPOSIT or WITHDRAWAL in `type`, so you must pass `false` explicitly to see them. And history is windowed by default — omitting `start` or passing `0` gives roughly the last three years, while passing a positive epoch such as `1` reaches full history. `side` filters BUY or SELL but applies only to trades, so combining it with a REDEEM filter returns nothing.

Pagination here behaves differently from `/positions`, and the difference matters. The `offset` ceiling is 5,000, and requests past it are rejected with a 400 rather than silently clamped; to read deeper you page inside `start`/`end` windows, each of which carries its own offset budget. Ordering is stable in both directions, so pages compose without gaps or repeats. Sorting runs on `sortBy` with TIMESTAMP, TOKENS or CASH.

For fills specifically, `/trades` returns trades by wallet address with optional filters, and it carries its own tighter budget of 200 requests per 10 seconds. From the market side rather than the wallet side, the holders endpoint — `GET /holders?market=...` — returns the largest holders of a given market's outcome tokens, which is a direct way to discover new wallets worth watching without walking a leaderboard at all.

## Positions on the other two APIs

A recurring search is for a positions endpoint on CLOB or a positions endpoint on Gamma, and the honest answer is that as of August 2026 neither is where wallet positions live. That isn't a gap in your reading — it's how the surface is divided.

The CLOB at `https://clob.polymarket.com` is the trading and market-data surface: order books through `/book` and `/books`, prices through `/price`, `/midpoint` and `/prices-history`, and your own ledger through `/data/orders` and `/data/trades`. Those last two are account-scoped and authenticated, so they show what *you* did, not what an arbitrary wallet holds. Gamma at `https://gamma-api.polymarket.com` is the metadata surface — events, markets, tags, search — and answers "what is this market and what are its condition IDs", which is exactly what you need to translate a `conditionId` from a positions response into a human-readable market.

So the practical shape of a tracker is three hosts doing three jobs: the Data API for wallet state, Gamma for market context, the CLOB for live prices. Each has its own rate-limit budget, which is a feature rather than a nuisance — enriching positions with market metadata draws on Gamma's allowance, not the one your position polling is spending.

## Authenticating for account endpoints

Everything above is public. Authentication enters only when you place orders or read your own ledger, and it comes in two levels. L1 is a signature from your private key that creates or derives an API key; L2 is that API key, used as HMAC headers on subsequent requests. The step that trips people up is the pairing of signer and funder, because they are not always the same address: Polymarket supports several account types — a plain EOA alongside the `POLY_PROXY`, `GNOSIS_SAFE` and `POLY_1271` signature types — and the client has to be told both which signature type applies and which address actually holds the funds. For an EOA account the funder is the EOA itself; for the proxy and Safe types it isn't. Get that pairing wrong and orders fail validation even though the key itself is valid.

Two things follow. First, nothing in a wallet-tracking pipeline needs credentials at all — if you're only reading positions, activity and trades by address, an unauthenticated client is the correct design. Second, when you do add trading, the API key endpoints carry their own limit of 100 requests per 10 seconds, and CLOB orders and cancellations are additionally governed by per-signer token-bucket limits that sit outside the IP-based numbers below.

## Rate limits and what they mean for scale

Polymarket's limits are IP-based and enforced through Cloudflare on sliding windows rather than fixed ones. The documentation says excess requests are delayed and queued, but [our August 2026 test found immediate 429 responses with no gradual slowdown](/blog/polymarket-api-rate-limits.php#our-test-what-the-limits-actually-do). Monitor both latency and status codes, and maintain your own per-endpoint request counters instead of assuming one signal will always arrive first.

The documented Data API budgets:

| Endpoint | Limit |
| --- | --- |
| General | 1,000 req / 10s |
| `/positions` | 150 req / 10s |
| `/closed-positions` | 150 req / 10s |
| `/trades` | 200 req / 10s |

For comparison, Gamma's general allowance is 4,000 per 10 seconds and the CLOB's is 9,000, with a global ceiling of 15,000 across everything.

One position call per wallet turns 150 per 10 seconds into arithmetic. Planning at about 70% of the limit to stay clear of throttling, one address supports roughly 105 wallet reads per 10-second cycle:

| Wallets watched | Refresh every 10s | every 30s | every 60s |
| --- | --- | --- | --- |
| 100 | 1 | 1 | 1 |
| 500 | 5 | 2 | 1 |
| 1,000 | 10 | 4 | 2 |
| 5,000 | 48 | 16 | 8 |

These are budget calculations from the published limits, not measurements — treat them as a planning starting point and verify against your own traffic.

Before adding capacity, spend the software levers, because most trackers are wasteful in the same three ways. Batch by market instead of by wallet where you can: `/holders` answers "who holds this market" in one call, replacing dozens of per-wallet reads. Use `/value` when you need a portfolio total rather than every line item. And tier your polling — wallets with large live exposure deserve a short interval, dormant ones a long one, and a uniform timer spends most of its budget confirming that nothing changed.

Past those, the constraint is that the budget belongs to the IP address rather than the account, so a wide tracker either slows down or spreads across addresses. That's the part our [proxies for crypto projects](/crypto-proxy.php) cover: dedicated IPv4 addresses, static for the full plan term, each subject to its own documented IP-based allowance — real-world scaling should still be verified under your target workload rather than assumed linear. Isolating workers also prevents one job's rate-limit response or backlog from slowing another's.
