get_trader_profile
Retrieve a trader's on-chain profile including first activity date, recent CTF events, and USDC flows. Useful for analyzing trader behavior on Polymarket.
Instructions
Get a trader's on-chain profile from the Traders subgraph: when they first appeared, their recent CTF events (splits, merges, transfers), and USDC flows.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| address | Yes | Ethereum address of the trader | |
| eventLimit | No | Number of recent events to return |
Implementation Reference
- src/index.ts:840-920 (registration)Tool registration for 'get_trader_profile' using server.registerTool(...) with name 'get_trader_profile'. Defines description and inputSchema (address, eventLimit).
// Tool 19: get_trader_profile // --------------------------------------------------------------------------- server.registerTool( "get_trader_profile", { description: "Get a trader's on-chain profile from the Traders subgraph: when they first appeared, their recent CTF events (splits, merges, transfers), and USDC flows.", inputSchema: { address: z.string().describe("Ethereum address of the trader"), eventLimit: z.number().min(1).max(100).default(20).describe("Number of recent events to return"), }, }, async ({ address, eventLimit }) => { try { const addr = address.toLowerCase(); const tradersQuery = `{ trader(id: "${addr}") { id firstSeenBlock firstSeenTimestamp ctfEvents(first: ${eventLimit}, orderBy: timestamp, orderDirection: desc) { id eventType conditionId amounts blockNumber timestamp } usdcTransfers(first: ${eventLimit}, orderBy: timestamp, orderDirection: desc) { id from to amount isInbound blockNumber timestamp } } }`; const obQuery = `{ makerFills: orderFilledEvents(first: ${eventLimit}, orderBy: timestamp, orderDirection: desc, where: { maker: "${addr}" }) { id maker taker price side fee makerAmountFilled takerAmountFilled timestamp } takerFills: orderFilledEvents(first: ${eventLimit}, orderBy: timestamp, orderDirection: desc, where: { taker: "${addr}" }) { id maker taker price side fee makerAmountFilled takerAmountFilled timestamp } account(id: "${addr}") { id collateralVolume numTrades } }`; const [tradersData, obData] = await Promise.all([ querySubgraph(SUBGRAPHS.traders.ipfsHash, tradersQuery), querySubgraph(SUBGRAPHS.orderbook.ipfsHash, obQuery).catch(() => null), ]); const od = obData as { makerFills?: Array<{ id: string; timestamp: string }>; takerFills?: Array<{ id: string; timestamp: string }>; account?: { id: string; collateralVolume: string; numTrades: string }; } | null; // Deduplicate and merge fills const seen = new Set<string>(); const allFills = [ ...(od?.makerFills ?? []).map((e) => ({ ...e, role: "maker" })), ...(od?.takerFills ?? []).map((e) => ({ ...e, role: "taker" })), ] .filter((e) => { if (seen.has(e.id)) return false; seen.add(e.id); return true; }) .sort((a, b) => parseInt(b.timestamp) - parseInt(a.timestamp)); const td = tradersData as { trader?: { ctfEvents?: unknown[] } }; const hasCTFEvents = (td.trader?.ctfEvents?.length ?? 0) > 0; const hasOBFills = allFills.length > 0; const obVolume = parseFloat(od?.account?.collateralVolume || "0"); let entryType: string; if (hasCTFEvents && hasOBFills) entryType = "hybrid (split collateral + orderbook buys)"; else if (!hasCTFEvents && hasOBFills) entryType = "⚠ orderbook-only — no split collateral detected"; else if (hasCTFEvents && !hasOBFills) entryType = "split-collateral only"; else entryType = "no activity detected"; return textResult({ ...(tradersData as object), orderbookFills: allFills, orderbookAccount: od?.account ?? null, entryType, ...(obVolume > 0 && !hasCTFEvents ? { pnlWarning: `⚠ wallet entered entirely via orderbook buys ($${obVolume.toFixed(2)} OB volume) — P&L from Slimmed/Beefy subgraphs is unreliable` } : {}), }); } catch (error) { return errorResult(error); } } ); - src/index.ts:852-920 (handler)Handler function for get_trader_profile. Queries the Traders subgraph for trader info (firstSeenBlock, firstSeenTimestamp, CTF events, USDC transfers), cross-references with the Orderbook subgraph for fills and account volume, deduplicates, and returns a combined profile with entryType classification.
async ({ address, eventLimit }) => { try { const addr = address.toLowerCase(); const tradersQuery = `{ trader(id: "${addr}") { id firstSeenBlock firstSeenTimestamp ctfEvents(first: ${eventLimit}, orderBy: timestamp, orderDirection: desc) { id eventType conditionId amounts blockNumber timestamp } usdcTransfers(first: ${eventLimit}, orderBy: timestamp, orderDirection: desc) { id from to amount isInbound blockNumber timestamp } } }`; const obQuery = `{ makerFills: orderFilledEvents(first: ${eventLimit}, orderBy: timestamp, orderDirection: desc, where: { maker: "${addr}" }) { id maker taker price side fee makerAmountFilled takerAmountFilled timestamp } takerFills: orderFilledEvents(first: ${eventLimit}, orderBy: timestamp, orderDirection: desc, where: { taker: "${addr}" }) { id maker taker price side fee makerAmountFilled takerAmountFilled timestamp } account(id: "${addr}") { id collateralVolume numTrades } }`; const [tradersData, obData] = await Promise.all([ querySubgraph(SUBGRAPHS.traders.ipfsHash, tradersQuery), querySubgraph(SUBGRAPHS.orderbook.ipfsHash, obQuery).catch(() => null), ]); const od = obData as { makerFills?: Array<{ id: string; timestamp: string }>; takerFills?: Array<{ id: string; timestamp: string }>; account?: { id: string; collateralVolume: string; numTrades: string }; } | null; // Deduplicate and merge fills const seen = new Set<string>(); const allFills = [ ...(od?.makerFills ?? []).map((e) => ({ ...e, role: "maker" })), ...(od?.takerFills ?? []).map((e) => ({ ...e, role: "taker" })), ] .filter((e) => { if (seen.has(e.id)) return false; seen.add(e.id); return true; }) .sort((a, b) => parseInt(b.timestamp) - parseInt(a.timestamp)); const td = tradersData as { trader?: { ctfEvents?: unknown[] } }; const hasCTFEvents = (td.trader?.ctfEvents?.length ?? 0) > 0; const hasOBFills = allFills.length > 0; const obVolume = parseFloat(od?.account?.collateralVolume || "0"); let entryType: string; if (hasCTFEvents && hasOBFills) entryType = "hybrid (split collateral + orderbook buys)"; else if (!hasCTFEvents && hasOBFills) entryType = "⚠ orderbook-only — no split collateral detected"; else if (hasCTFEvents && !hasOBFills) entryType = "split-collateral only"; else entryType = "no activity detected"; return textResult({ ...(tradersData as object), orderbookFills: allFills, orderbookAccount: od?.account ?? null, entryType, ...(obVolume > 0 && !hasCTFEvents ? { pnlWarning: `⚠ wallet entered entirely via orderbook buys ($${obVolume.toFixed(2)} OB volume) — P&L from Slimmed/Beefy subgraphs is unreliable` } : {}), }); } catch (error) { return errorResult(error); } } ); - src/index.ts:844-851 (schema)Input schema for get_trader_profile: address (string, required) and eventLimit (number, min 1, max 100, default 20). Uses Zod for validation.
{ description: "Get a trader's on-chain profile from the Traders subgraph: when they first appeared, their recent CTF events (splits, merges, transfers), and USDC flows.", inputSchema: { address: z.string().describe("Ethereum address of the trader"), eventLimit: z.number().min(1).max(100).default(20).describe("Number of recent events to return"), }, }, - src/subgraphs.ts:94-104 (helper)Subgraph configuration for 'traders' subgraph used by get_trader_profile. Defines the IPFS hash (QmfT4YQwFfAi77hrC2JH3JiPF7C4nEn27UQRGNpSpUupqn) and key entities (Trader, CTFEvent, USDCTransfer).
traders: { name: "Traders", ipfsHash: "QmfT4YQwFfAi77hrC2JH3JiPF7C4nEn27UQRGNpSpUupqn", description: "Per-trader event log indexing every CTF interaction and USDC flow. Each Trader has a derived list of CTFEvents (splits, merges, transfers, resolutions, redemptions) and USDCTransfers (inbound/outbound with amounts). Best for: building trader profiles, tracking when a wallet first appeared, reconstructing a trader's full on-chain history, and analyzing USDC deposit/withdrawal patterns.", keyEntities: [ "Trader (address, firstSeenBlock, firstSeenTimestamp)", "CTFEvent (eventType, conditionId, amounts, timestamp) — immutable, @derivedFrom trader", "USDCTransfer (from, to, amount, isInbound, timestamp) — immutable, @derivedFrom trader", ], }, - src/graphClient.ts:12-59 (helper)The querySubgraph helper function used by the handler to make GraphQL queries to subgraphs via The Graph's hosted service.
export async function querySubgraph( ipfsHash: string, query: string, variables?: Record<string, unknown> ): Promise<unknown> { const apiKey = process.env.GRAPH_API_KEY; if (!apiKey) { throw new GraphClientError( "GRAPH_API_KEY environment variable is required. " + "Get one at https://thegraph.com/studio/apikeys/" ); } const url = `https://gateway.thegraph.com/api/${apiKey}/deployments/id/${ipfsHash}`; const body: Record<string, unknown> = { query }; if (variables && Object.keys(variables).length > 0) { body.variables = variables; } const response = await fetch(url, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(body), }); if (!response.ok) { throw new GraphClientError( `Graph API returned HTTP ${response.status}: ${response.statusText}`, response.status ); } const json = (await response.json()) as { data?: unknown; errors?: unknown[]; }; if (json.errors && json.errors.length > 0) { throw new GraphClientError( `GraphQL errors: ${JSON.stringify(json.errors)}`, undefined, json.errors ); } return json.data; }