name: polymarket-profile description: Polymarket address profiler — input any 0x address, get a complete trading profile with PnL, win rate, positions, category breakdown, and top trades. All data from public APIs, no local database needed. allowed-tools: Bash(curl:) Bash(node:) Read Write Edit metadata: {"version":"0.2.0","openclaw":{"skillKey":"polymarket-profile","homepage":"https://leolabs.me","requires":{"anyBins":["curl","node"]}}}
Polymarket Address Profile
Generate a complete trading profile for any Polymarket address. All data comes from public APIs in real-time — no local database or API key required.
Trigger
User provides a Polymarket identity and wants analysis, profile, or trading overview.
Input Resolution
The skill requires a 0x proxy wallet address. Users may provide:
| Input Type | Example | How to handle |
|---|---|---|
| 0x address | 0x63ce3421... |
Use directly |
| Profile URL | polymarket.com/profile/Theo4 |
Extract username from URL, then resolve via leaderboard lookup (see below) |
| Username | Theo4 |
Resolve via leaderboard lookup (see below) |
When user provides a username or URL (not 0x address)
Step 1: Try leaderboard lookup — Search lb-api.polymarket.com/profit to resolve username → address.
# Paginate through the leaderboard searching for the username
# Start with offset=0, increment by 500 until found or no more results
curl -s "https://lb-api.polymarket.com/profit?window=all&limit=500&offset=0"
Response is an array of objects with name, pseudonym, and proxyWallet fields. Search for a case-insensitive match on name or pseudonym.
- Found → use the
proxyWalletas the 0x address, continue to Step 1 of execution - Not found after 3 pages (1500 users) → the account is likely unranked, fall back to Step 2
Step 2: Manual fallback (only if leaderboard lookup fails)
"This username isn't in the Polymarket leaderboard (only ranked users can be auto-resolved). You can find the 0x address by:
- Open the profile page on Polymarket
- Click the address/wallet icon near the username — it copies the 0x address
- Or check the browser URL — some profile pages show the address"
NOTE: Leaderboard lookup covers all users with a PnL ranking (tens of thousands). Only very new or inactive accounts with zero trading history won't be found.
API Endpoints
All endpoints are public, no authentication needed.
| Endpoint | Base URL | Purpose |
|---|---|---|
| LB API | https://lb-api.polymarket.com |
PnL, volume, rankings |
| Data API | https://data-api.polymarket.com |
Positions, activity, trades |
| CLOB API | https://clob.polymarket.com |
Orderbook, market prices |
| Gamma API | https://gamma-api.polymarket.com |
Market metadata, events, categories |
Execution Steps
Run steps 1-4 in parallel where possible to minimize latency.
Step 1: PnL Snapshot
curl -s "https://lb-api.polymarket.com/profit?window=all&address={ADDRESS}"
Response is an array. Extract [0].amount for total PnL, [0].name for username.
NOTE: lb-api only returns amount (total PnL). It does NOT return invested/numTrades/numWins. Those must be computed from positions (Step 2) and activity (Step 3).
Also fetch time-windowed PnL for trend:
curl -s "https://lb-api.polymarket.com/profit?window=7d&address={ADDRESS}"
curl -s "https://lb-api.polymarket.com/profit?window=30d&address={ADDRESS}"
Step 2: Current Positions
curl -s "https://data-api.polymarket.com/positions?user={ADDRESS}&sizeThreshold=0&limit=100&offset=0"
Paginate with offset parameter: if response returns exactly 100 records, fetch next page with offset=100, offset=200, etc. until fewer than 100 returned.
Each position object contains:
| Field | Description |
|---|---|
title |
Market question |
outcome |
"Yes" or "No" |
size |
Number of shares held |
avgPrice |
Average entry price |
curPrice |
Current market price |
initialValue |
Total cost (size × avgPrice) |
currentValue |
Current value (size × curPrice) |
cashPnl |
Realized + unrealized PnL |
percentPnl |
PnL as percentage |
totalBought |
Total shares ever bought |
realizedPnl |
PnL from closed portions |
redeemable |
Can claim settlement winnings |
endDate |
Market expiry date |
eventSlug |
Event identifier for Gamma API |
Extract:
- Number of open positions:
redeemable == falseANDcurrentValue > 0 - Largest position by
currentValue - Total portfolio value: sum of
currentValuefor open positions
Win Rate Calculation
Classify positions into three buckets:
| Bucket | Condition | Meaning |
|---|---|---|
| Won | redeemable == true AND currentValue > 0 |
Market settled in user's favor, awaiting redemption |
| Lost | redeemable == true AND currentValue == 0 |
Market settled against user |
| Open | redeemable == false AND currentValue > 0 |
Market not yet settled |
Win Rate = Won / (Won + Lost)
IMPORTANT: Do NOT use cashPnl > 0 to determine wins. Winning positions have currentValue > 0 (shares worth $1) even if cashPnl appears negative due to partial sells. The redeemable flag is the definitive settlement indicator.
Fallback: When Positions Are Empty or Incomplete
For inactive accounts, the positions API may return very few records (settled positions get cleaned up). If Won + Lost < 3, fall back to activity-based estimation:
- From activity, group all REDEEM records by
slug(market) - From activity, group all TRADE records by
slug - Markets with REDEEM volume > 0 → Won
- Markets with TRADE volume > 0 but no REDEEM → Lost (invested but no payout)
- Label Win Rate as "estimated from activity" when using this fallback
Step 3: Activity History
curl -s "https://data-api.polymarket.com/activity?user={ADDRESS}&limit=500"
IMPORTANT: You MUST paginate to get complete data. Large accounts have 10,000+ records. 500 records is NOT enough for an accurate profile.
Activity Pagination (REQUIRED)
Loop:
1. First request: /activity?user={ADDRESS}&limit=500
2. Get `timestamp` of the LAST record in the response
3. Next request: /activity?user={ADDRESS}&limit=500&end={last_timestamp}
4. Repeat until response returns fewer than 500 records (= last page)
5. Merge all results
Inform user of progress for large accounts: "Fetching activity... page X (Y records so far)"
Each activity object contains: type, size, usdcSize, price, side, title, slug, timestamp, outcome.
Classify by type field:
| Type | What it means |
|---|---|
| TRADE | Bought or sold shares (check side field: "BUY" or "SELL") |
| SPLIT | Created YES+NO pairs from USDC (market-neutral entry) |
| MERGE | Combined YES+NO back to USDC (exit / arbitrage capture) |
| REDEEM | Claimed winnings after market settlement |
| CONVERSION | Converted between YES/NO (special market operation) |
| REBATE | Fee rebate from maker orders |
Count and sum usdcSize for each type. Also track unique markets (slug) for diversity analysis.
Step 4: Market Categories
For each position, fetch market metadata via Gamma API using the eventSlug from positions:
curl -s "https://gamma-api.polymarket.com/events?slug={eventSlug}"
Or batch multiple slugs. Each event has a category field and a tags array (each tag has a label field).
Category Mapping
Gamma API categories are legacy naming. Map to Polymarket's frontend categories:
| Gamma category / tag label | Display Category |
|---|---|
Sports, NBA Playoffs, Chess, Esports, any sports team name |
Sports |
Crypto, NFTs, Bitcoin, Ethereum |
Crypto |
US-current-affairs, Elections, any president/congress/party keyword |
Politics |
Ukraine & Russia, Iran, any war/military/invasion keyword |
Geopolitics |
Business, any GDP/oil/fed/rate keyword |
Finance |
Pop-Culture, Art, Coronavirus |
Culture |
| temperature/weather/celsius keyword in title | Weather |
| AI/tech/SpaceX keyword in title | Tech |
| Musk/tweet keyword in title | Musk/Tweets |
| No match | Other |
Priority: Use Gamma category field first. If it's missing or too generic (All), fall back to tag labels. If still unclear, infer from market title keywords.
NOTE: Gamma API may return empty [] for old/settled markets. This is expected — fall back to keyword inference from market titles in activity data.
Optimization: Collect all unique eventSlug values from positions first, then batch fetch from Gamma (avoid one API call per position). Group by slug to avoid duplicates.
Compute the percentage distribution by volume invested.
Step 5: Strategy Pattern Detection
Analyze data from Steps 1-4 to classify the address into a trading pattern. Use multiple dimensions — no single metric is sufficient.
Dimensions to compute
From activity data:
trade_count: total TRADE recordssplit_count/split_volume: SPLIT records and USDC volumemerge_count/merge_volume: MERGE records and USDC volumeredeem_volume: total REDEEM volumeunique_markets: number of distinctslugvaluesavg_trade_size: total trade volume / trade counttrade_frequency: trade count / active days (first to last timestamp)
From positions data:
open_count: open positions (not redeemable)settled_count: won + lostconcentration: top 3 positions as % of total invested
Pattern Classification
Evaluate in this order (first match wins):
| Pattern | Conditions | Description |
|---|---|---|
| SPLIT Arbitrage | split_volume > trade_volume × 0.2 AND merge_volume > 0 |
Enters via SPLIT (creates YES+NO pairs), sells one side or MERGEs back. Capital-efficient, market-neutral entry. |
| Market Maker | trade_count > 500 AND unique_markets < 15 AND avg_trade_size < $20 |
High-frequency small trades concentrated in few markets. Provides liquidity, earns spread. |
| Whale / Concentrated | concentration > 60% AND unique_markets < 10 |
Heavy capital in a few markets. High-conviction directional bets. |
| Diversified | unique_markets > 50 |
Spread across many markets. Portfolio approach, lower per-market risk. |
| Small Trader | trade_count < 50 AND total_invested < $500 |
Limited activity. New or casual user. |
| Mixed | None of the above | Combination of strategies, no dominant pattern. |
Output format
🎯 Strategy Pattern: {pattern_name}
{1-2 sentence explanation based on actual numbers}
Key metrics: {trade_count} trades across {unique_markets} markets, avg ${avg_trade_size}/trade
Do NOT reveal specific thresholds used for classification. Just state the pattern name and explain it in plain language using the trader's actual data.
Step 6: Top Trades
From positions data (not activity), use cashPnl field for settled positions:
- Top 5 Wins: Settled positions with highest positive
cashPnl - Top 5 Losses: Settled positions with most negative
cashPnl
For each: market title, outcome (Yes/No), entry price (avgPrice), PnL amount.
NOTE: cashPnl from positions is approximate (affected by partial sells). This is acceptable for v0.1. Do NOT attempt to match individual BUY→SELL/REDEEM from activity — that requires complex logic and is not worth the accuracy gain for a profile overview.
Fallback: When Positions Data Is Sparse
If positions API returns very few settled records, estimate Top Trades from activity:
- Group TRADE records by
slug, sumusdcSizeper market (= total invested) - Group REDEEM records by
slug, sumusdcSizeper market (= total payout) - PnL per market ≈ REDEEM volume - TRADE BUY volume
- Sort by PnL for top wins/losses. Label as "estimated from activity".
Step 7: Assemble Profile
Output the complete profile in this format:
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Polymarket Profile: {ADDRESS_SHORT}
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
📊 Overview
Address: {ADDRESS}
Total PnL: ${totalProfit} (invested ${invested})
Win Rate: {wins}/{total} ({winRate})
7d PnL: ${pnl_7d}
30d PnL: ${pnl_30d}
📈 Current Positions ({count})
Largest: {market_name} — {direction} ${size} @ ${avg_price}
Portfolio: ${total_value}
| # | Market | Direction | Size | Avg Price | Current | PnL |
|---|--------|-----------|------|-----------|---------|-----|
| 1 | ... | YES/NO | $xx | $0.xx | $0.xx | +$x |
...
📂 Category Distribution
| Category | Positions | Volume | % |
|----------|-----------|--------|---|
| Crypto | 12 | $5,000 | 45% |
| Politics | 5 | $2,000 | 22% |
...
📋 Activity Summary
| Type | Count | Volume |
|------------|-------|--------|
| TRADE BUY | xxx | $xxx |
| TRADE SELL | xxx | $xxx |
| SPLIT | xxx | $xxx |
| MERGE | xxx | $xxx |
| REDEEM | xxx | $xxx |
🏆 Top Wins
1. {market} — {direction} +${pnl} (entry ${price} → ${exit})
2. ...
3. ...
💀 Top Losses
1. {market} — {direction} -${pnl} (entry ${price} → ${exit})
2. ...
3. ...
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Generated: {timestamp} | Data: Polymarket Public APIs
Powered by Leo Labs — leolabs.me
━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
Important Notes
- PnL from lb-api is the ground truth. Use it as the primary source. Position-level
cashPnlmay differ due to SPLIT/MERGE accounting. - Activity pagination: use
endtimestamp, neveroffset. Do NOT deduplicate — same txHash with multiple records means multiple fills in one transaction. - Always paginate ALL activity records. Partial data produces inaccurate profiles. Show pagination progress to the user (e.g. "Fetching activity... page 5, 2500 records so far").
- Category mapping: use
eventSlugfrom positions to query Gamma API. Category comes from the event'stagsorcategoryfield. If Gamma is slow, infer from market title keywords (BTC/ETH/crypto → Crypto, Trump/election → Politics, NBA/FIFA → Sports, temperature/weather → Weather). - All monetary values in USD, round to 2 decimal places.
- Address format: APIs accept both checksummed and lowercase. Normalize to lowercase.
- lb-api may require proxy in some regions. data-api and gamma-api work globally.
- Rate limits: No documented limits, but keep requests reasonable (<10 concurrent). Add 500ms delay between sequential pagination calls. If a request returns empty or non-JSON, retry once after 2 seconds.
- lb-api 7d/30d: May return empty array
[]for inactive accounts. Display as "N/A" instead of $0.