Building a Polymarket Copy-Trading Bot with PolyEdge SDK
This guide demonstrates how to stream pre-block Polymarket mempool trades with the PolyEdge TypeScript SDK (polyedge) and execute automated copy orders with the official Polymarket SDK (@polymarket/client).
[PolyEdge Mempool Stream] ──(tx pre-block)──> [Match Target Trader] ──> [Polymarket Order API]
1. Project Setup & Dependencies
Initialize a Node.js TypeScript project:
mkdir poly-copy-bot && cd poly-copy-bot
npm init -y
npm install @polymarket/client polyedge dotenv viem
npm install -D typescript tsx @types/node
npx tsc --init
Create a .env configuration file:
# PolyEdge API Key
POLYEDGE_API_KEY=pe_live_xxxxxxxxxxxxxxxx
# Polymarket Account Credentials
POLYMARKET_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
POLYMARKET_DEPOSIT_WALLET=0xYourPolymarketDepositWalletAddress
# Target Trader Addresses to Copy (comma-separated, lower-case)
TARGET_ADDRESSES=0x34dd4a4b70eaf79a17878f7938263c801d4dfd83,0x4bf459194025b2ca11a2db2deb9ef2a08c467021
# Sizing Mode: FIXED or PROPORTIONAL
COPY_MODE=FIXED
# FIXED Mode: Fixed USD notional per trade
FIXED_AMOUNT_USD=50
# PROPORTIONAL Mode: Ratio (0.1 = 10%) and Max Spend Ceiling (USD)
COPY_RATIO=0.1
MAX_SPEND_USD=200
# Order Type: FAK (Fill-and-Kill) or FOK (Fill-or-Kill)
ORDER_TYPE=FAK
# Slippage Tolerance (0.03 = 3%)
SLIPPAGE_TOLERANCE_PCT=0.03
2. Order Types & Sizing Strategies
Order Execution Types (OrderType)
OrderType.FAK(Fill-And-Kill / Recommended): Matches immediately against resting book liquidity. Any unfilled quantity is immediately cancelled without resting on the book.OrderType.FOK(Fill-Or-Kill): The order must fill completely at the specified price limits, or the entire order is rejected.
Sizing Modes
- Fixed (
FIXED): Commits a fixed dollar amount (e.g.$50) per signal. - Proportional (
PROPORTIONAL): Replicates a fraction (e.g.10%) of the target trader's fill notional, capped byMAX_SPEND_USD.
3. Data Models (types.ts)
Underlying transaction types are provided directly by polyedge. We only define application-level signals and configuration:
// types.ts
import type { OrderType } from "@polymarket/client";
export interface CopySignal {
txHash: string;
targetAddress: string;
role: "taker" | "maker";
assetId: string;
side: "BUY" | "SELL";
price: number;
shares: number;
notionalUsd: number;
marketQuestion: string;
}
export interface BotConfig {
polymarketPrivateKey: `0x${string}`;
polymarketDepositWallet: `0x${string}`;
copyMode: "FIXED" | "PROPORTIONAL";
fixedAmountUsd: number;
copyRatio: number;
maxSpendUsd: number;
orderType: OrderType.FAK | OrderType.FOK;
slippageTolerancePct: number;
}
4. Initialize & Connect Stream (stream.ts)
Use the polyedge SDK to provision a stream filtered by target wallet addresses and establish a connection with built-in 45s keep-alive watchdog and automatic reconnect:
// stream.ts
import { PolyEdgeClient, type PolyEdgeStream } from "polyedge";
export async function createAndConnectStream(
apiKey: string,
targetAddresses: string[]
): Promise<PolyEdgeStream> {
const client = new PolyEdgeClient({ apiKey });
// 1. Provision a stream subscribed to target addresses
const streamConfig = await client.streams.create({
name: `CopyBot-${Date.now()}`,
addresses: targetAddresses,
});
if (!streamConfig.id) {
throw new Error("Failed to create stream: ID not returned");
}
console.log(`[PolyEdge] Stream provisioned: ${streamConfig.id}`);
// 2. Connect with 45s watchdog
return client.streams.connect(streamConfig.id, {
heartbeatTimeoutMs: 45_000,
});
}
5. Trade Attribution & Matching (matcher.ts)
In Polymarket's binary market architecture, each market contains two outcome tokens. token_ids_index strictly evaluates to 0 or 1, indexing the traded token asset ID.
Inspect both the aggressor (taker) and resting limit orders (makers) to identify target trader participation:
// matcher.ts
import type { LiveTransaction } from "polyedge";
import type { CopySignal } from "./types";
export function extractCopySignals(
tx: LiveTransaction,
targetAddresses: Set<string>
): CopySignal[] {
const signals: CopySignal[] = [];
const market = tx.market;
if (!market || !market.token_ids || market.token_ids.length < 2) {
return signals;
}
const resolveAssetId = (index?: number): string | null => {
return (index === 0 || index === 1) && market.token_ids ? market.token_ids[index] : null;
};
// 1. Inspect Taker (Aggressor fill)
if (tx.taker?.user?.address) {
const takerAddr = tx.taker.user.address.toLowerCase();
if (targetAddresses.has(takerAddr)) {
const assetId = resolveAssetId(tx.taker.token_ids_index);
const shares = Number(tx.taker.shares) / 1e6;
const notionalUsd = Number(tx.taker.usdc) / 1e6;
const price = shares > 0 ? notionalUsd / shares : 0;
if (assetId && shares > 0 && price > 0 && tx.taker.side && tx.tx_hash) {
signals.push({
txHash: tx.tx_hash,
targetAddress: takerAddr,
role: "taker",
assetId,
side: tx.taker.side,
price,
shares,
notionalUsd,
marketQuestion: market.question || "",
});
}
}
}
// 2. Inspect Makers (Passive limit orders filled in match)
if (Array.isArray(tx.makers)) {
for (const maker of tx.makers) {
if (!maker.user?.address) continue;
const makerAddr = maker.user.address.toLowerCase();
if (targetAddresses.has(makerAddr)) {
const assetId = resolveAssetId(maker.token_ids_index);
const shares = Number(maker.shares) / 1e6;
const notionalUsd = Number(maker.usdc) / 1e6;
const price = shares > 0 ? notionalUsd / shares : 0;
if (assetId && shares > 0 && price > 0 && maker.side && tx.tx_hash) {
signals.push({
txHash: tx.tx_hash,
targetAddress: makerAddr,
role: "maker",
assetId,
side: maker.side,
price,
shares,
notionalUsd,
marketQuestion: market.question || "",
});
}
}
}
}
return signals;
}
6. Order Execution (executor.ts)
Initialize the official Polymarket SDK (@polymarket/client) and submit market orders with price slippage bounds:
// executor.ts
import { Client, Side as OrderSide, OrderType } from "@polymarket/client";
import { privateKeyToAccount } from "viem/accounts";
import type { BotConfig, CopySignal } from "./types";
const CLOB_API_URL = "https://clob.polymarket.com";
const POLYGON_CHAIN_ID = 137;
export async function createExecutor(config: BotConfig) {
const account = privateKeyToAccount(config.polymarketPrivateKey);
const client = new Client(
CLOB_API_URL,
POLYGON_CHAIN_ID,
account,
config.polymarketDepositWallet
);
async function executeOrder(signal: CopySignal) {
const orderUsd = config.copyMode === "FIXED"
? config.fixedAmountUsd
: Math.min(signal.notionalUsd * config.copyRatio, config.maxSpendUsd);
if (orderUsd <= 0 || signal.price <= 0) return null;
// Slippage boundaries: max price for buy, min price for sell
const maxPrice = signal.side === "BUY"
? Math.min(0.99, Number((signal.price * (1 + config.slippageTolerancePct)).toFixed(4)))
: undefined;
const minPrice = signal.side === "SELL"
? Math.max(0.01, Number((signal.price * (1 - config.slippageTolerancePct)).toFixed(4)))
: undefined;
const shares = Number((orderUsd / signal.price).toFixed(2));
if (shares <= 0) return null;
console.log(
`[Copy Order] Target: ${signal.targetAddress} (${signal.role}) | ` +
`Side: ${signal.side} | USD: $${orderUsd.toFixed(2)} | ` +
`${signal.side === "BUY" ? `MaxPrice: ${maxPrice}` : `MinPrice: ${minPrice}`}`
);
return signal.side === "BUY"
? client.placeMarketOrder({
assetId: signal.assetId,
side: OrderSide.BUY,
amount: orderUsd,
orderType: config.orderType,
...(maxPrice ? { maxPrice } : {}),
})
: client.placeMarketOrder({
assetId: signal.assetId,
side: OrderSide.SELL,
shares,
orderType: config.orderType,
...(minPrice ? { minPrice } : {}),
});
}
return { client, executeOrder };
}
7. Main Application Entry Point (index.ts)
Consume incoming mempool transactions with the SDK's async iterator (for await) and submit copy orders:
// index.ts
import "dotenv/config";
import { OrderType } from "@polymarket/client";
import { createAndConnectStream } from "./stream";
import { extractCopySignals } from "./matcher";
import { createExecutor } from "./executor";
async function main() {
const polyedgeApiKey = process.env.POLYEDGE_API_KEY;
const privateKey = process.env.POLYMARKET_PRIVATE_KEY as `0x${string}`;
const depositWallet = process.env.POLYMARKET_DEPOSIT_WALLET as `0x${string}`;
const rawTargetAddresses = process.env.TARGET_ADDRESSES || "";
if (!polyedgeApiKey || !privateKey || !depositWallet || !rawTargetAddresses) {
throw new Error("Missing required environment variables in .env");
}
const targetAddresses = rawTargetAddresses
.split(",")
.map((addr) => addr.trim().toLowerCase())
.filter(Boolean);
const targetSet = new Set(targetAddresses);
// 1. Initialize Polymarket client
const executor = await createExecutor({
polymarketPrivateKey: privateKey,
polymarketDepositWallet: depositWallet,
copyMode: process.env.COPY_MODE?.toUpperCase() === "PROPORTIONAL" ? "PROPORTIONAL" : "FIXED",
fixedAmountUsd: Number(process.env.FIXED_AMOUNT_USD || "50"),
copyRatio: Number(process.env.COPY_RATIO || "0.1"),
maxSpendUsd: Number(process.env.MAX_SPEND_USD || "200"),
orderType: process.env.ORDER_TYPE?.toUpperCase() === "FOK" ? OrderType.FOK : OrderType.FAK,
slippageTolerancePct: Number(process.env.SLIPPAGE_TOLERANCE_PCT || "0.03"),
});
// 2. Provision and connect stream via PolyEdge SDK
const stream = await createAndConnectStream(polyedgeApiKey, targetAddresses);
const processedTxHashes = new Set<string>();
process.on("SIGINT", () => {
console.log("\n[Shutdown] Closing stream and exiting...");
stream.destroy();
process.exit(0);
});
console.log(`[Listening] Waiting for matching mempool events...`);
// 3. Consume transactions via async iterator
for await (const tx of stream) {
if (!tx.tx_hash) continue;
if (processedTxHashes.has(tx.tx_hash)) continue;
processedTxHashes.add(tx.tx_hash);
// Keep sliding window of 5,000 hashes
if (processedTxHashes.size > 5000) {
const oldest = processedTxHashes.values().next().value;
if (oldest) processedTxHashes.delete(oldest);
}
const signals = extractCopySignals(tx, targetSet);
for (const signal of signals) {
try {
const result = await executor.executeOrder(signal);
console.log(`[Order Result]`, result);
} catch (err) {
console.error(`[Order Failed] Error copying ${signal.txHash}:`, err);
}
}
}
}
main().catch((err) => {
console.error("Fatal error:", err);
process.exit(1);
});
8. Run & Verify
Run the bot with tsx:
npx tsx index.ts
When a target trader matches on Polymarket, execution logs appear immediately:
[PolyEdge] Stream provisioned: 9bae9005-9ef1-4f17-8658-36ea54536836
[Listening] Waiting for matching mempool events...
[Copy Order] Target: 0x4bf459194025b2ca11a2db2deb9ef2a08c467021 (taker) | Side: BUY | USD: $50.00 | MaxPrice: 0.5047
[Order Result] { orderId: '0x...', status: 'MATCHED' }