Back to Blog
Building Copy Trading Pipeline for Polymarket
Code Strategy7 min read

Building Copy Trading Pipeline for Polymarket

Copy trading a wallet on Polymarket is straightforward in principle: every trade a wallet makes is public, on-chain, and available through a free API. The harder question is how to relay those trades into your own bot without inheriting the source wallet's exact position sizing, without double-processing the same fill twice, and without losing visibility into what actually got copied and why.

This walks through one way to do that: a Code Strategy that polls a wallet, a Data Webhook that stores what it finds, and a Code Reaction that validates and forwards the trade to a live bot.

The straight-line way (and why we don't do it)

The simplest possible setup: a Code Strategy polls the target wallet's trades and fires straight into a Bot Webhook attached to your Prediction Signal Bot. One hop. Done.

It works. It's also fragile in exactly the ways that matter when real money is moving:

  • No dedupe — reconnects, retries, or a strategy re-run can replay the same trade twice.
  • No sizing control — you inherit whatever size the wallet you're copying used, which might be way outside what you're comfortable risking.
  • No audit trail — if something trades that shouldn't have, you're reconstructing what happened from the bot's signal log alone.
  • No kill switch — the strategy is the trade. There's no seam where you could pause, inspect, or override a specific class of signal without disabling the whole thing.

The setup we're actually using: Strategy → Data Webhook → Code Reaction → Bot Webhook

Same idea, one more hop, a lot more control:

Code Strategy (polls Polymarket, dedupes) 
   → Data Webhook (just stores/forwards, doesn't trade) 
   → Code Reaction (validates, clamps size, decides) 
   → Bot Webhook (Prediction Signal Bot executes)

The Data Webhook in the middle isn't decoration. It's a real seam in the pipeline:

  • Validation happens separately from polling. The Reaction re-checks every field before anything trades — a bad response from Polymarket's API never gets a free pass just because the strategy fetched it.
  • You get a size clamp for free. The Reaction can cap position size regardless of what the source wallet actually staked, so a whale's six-figure bet doesn't silently become your six-figure bet.
  • You can pause the logic without pausing the feed. Deactivate the Reaction and the strategy keeps recording trades into the Data Webhook — nothing trades, but you're not losing data either.
  • You get a real audit trail. Every relayed trade shows up in the Data Webhook's log independently of the bot's own signal log. If a trade looks wrong, you can trace it hop by hop instead of guessing.
  • The pieces are swappable. Want to copy the same wallet into two different bots with different sizing rules? Add a second Reaction on the same Data Webhook. Straight-to-bot doesn't give you that.

Basically: one extra hop buys you a validation layer, a safety clamp, and a debugging trail. Worth it.

Step 1 — find a wallet worth copying

Polymarket's leaderboard is public and sorted by profit:

https://polymarket.com/leaderboard/overall/monthly/profit

Grab a proxy wallet address from there (or wherever you already track good traders). That address is the only "secret" input this whole pipeline needs, and it's not even a secret — wallet addresses are public.

Step 2 — the Code Strategy that watches the wallet

This runs on a schedule, polls Polymarket's public trades endpoint for the wallet you picked, and forwards anything new to a Data Webhook. It never trades directly — it doesn't have to, and keeping it dumb is the point.

// === Config ===

// Polymarket proxy wallet to copy — the only thing you actually need to change
const WATCH_WALLET = "0xTHE_WALLET_YOU_WANT_TO_COPY";
// Your Sigrex Data Webhook URL
const DATA_WEBHOOK_URL = "https://hook.sigrex.io/data/<your-data-webhook-id>";
// Optional — only needed if you turned on key protection for the Data Webhook
const WEBHOOK_KEY = $.Env.HOOK_KEY;
// Keep well under the 5000ms run limit
const MAX_NEW_TRADES_PER_RUN = 1;

if (!WATCH_WALLET || !DATA_WEBHOOK_URL) return;

const state = (await $.Storage.get()) || {
  seenIds: [],
  lastTimestamp: 0,
  bootstrapped: false,
};

// 1. Pull the wallet's recent fills, newest first (API default)
const res = await $.Http.get(
  `https://data-api.polymarket.com/trades?user=${WATCH_WALLET}&limit=20`
);
if (res.status !== 200) return;

let trades;
try {
  trades = JSON.parse(res.text);
} catch {
  return;
}
if (!Array.isArray(trades) || trades.length === 0) return;

// 2. Oldest-first, so we relay in the order they actually happened
trades.sort((a, b) => a.timestamp - b.timestamp);

// short dedupe fingerprint: tx hash + outcome index + timestamp
const fingerprint = (t) =>
  `${t.transactionHash.slice(0, 18)}:${t.outcomeIndex}:${t.timestamp}`;

// 2b. First run ever: baseline the wallet's current trades as "seen" and
// copy nothing. Otherwise the first run would blast out its entire recent
// history instead of only genuinely new activity going forward.
if (!state.bootstrapped) {
  state.seenIds = trades.map(fingerprint);
  state.lastTimestamp = trades[trades.length - 1].timestamp;
  state.bootstrapped = true;
  await $.Storage.set(state);
  return;
}

const unseen = trades.filter((t) => !state.seenIds.includes(fingerprint(t)));
if (unseen.length === 0) return;

// 3. Forward at most MAX_NEW_TRADES_PER_RUN this run; the rest get picked
//    up on the next scheduled run.
const toSend = unseen.slice(0, MAX_NEW_TRADES_PER_RUN);

for (const t of toSend) {
  const id = fingerprint(t);

  const payload = {
    slug: t.slug,
    outcome: t.outcome,
    side: t.side, // BUY | SELL
    size: Math.round(t.size * t.price * 100) / 100, // approx USDC value of the fill
    id,
    debug: {
      source: "copy-trade",
      wallet: WATCH_WALLET,
      txHash: t.transactionHash,
      price: t.price,
    },
  };

  const headers = { "Content-Type": "application/json" };
  if (WEBHOOK_KEY) headers["X-Key"] = WEBHOOK_KEY;

  await $.Http.post(DATA_WEBHOOK_URL, payload, headers);

  state.seenIds.push(id);
  state.lastTimestamp = Math.max(state.lastTimestamp, t.timestamp);
}

// 4. Keep storage under the cap: drop oldest fingerprints first (FIFO)
while (
  JSON.stringify(state).length > $.Storage.byteLimit &&
  state.seenIds.length > 0
) {
  state.seenIds.shift();
}

await $.Storage.set(state);

Worth calling out:

  • Dedupe is a fingerprint, not the whole trade object. Truncated tx hash + outcome index + timestamp is enough to be unique, and short enough that you can store hundreds of them without touching Sigrex's storage cap.
  • Overflow handling is FIFO by actual byte size, not a hardcoded count — it checks JSON.stringify(state).length against $.Storage.byteLimit and drops the oldest fingerprints until it fits, so it self-adjusts if your fingerprint format ever changes.
  • The bootstrap step matters. Without it, the very first run would treat the wallet's entire recent trade history as "new" and copy all of it at once.

Step 3 — the Code Reaction that actually places the order

This is where the safety layer lives. It receives whatever the Data Webhook stored, validates it from scratch, clamps the size, and only then relays a real signal to the Bot Webhook.

// === Config ===

// Your Prediction Signal Bot's Bot Webhook (different URL from the Data Webhook)
const BOT_WEBHOOK_URL = "https://hook.sigrex.io/bot/<your-prediction-bot-webhook-hash>";
// Optional — only needed if the bot webhook has key protection enabled
const BOT_WEBHOOK_KEY = $.Env.BOT_HOOK_KEY;
// Safety clamp — caps how much any single copied trade can size into,
// regardless of what the source wallet actually staked
const MAX_SIZE_USDC = 50;

let payload;
try {
  payload = JSON.parse($.Request.data);
} catch {
  return;
}

// Only act on payloads that actually came from our copy-trade strategy
if (payload?.debug?.source !== "copy-trade") return;

const { slug, outcome, side, size, id } = payload;

// Fail closed on anything malformed — never forward a guess
if (typeof slug !== "string" || !slug) return;
if (typeof outcome !== "string" || !outcome) return;
if (side !== "BUY" && side !== "SELL") return;
if (typeof size !== "number" || !(size > 0)) return;

const botPayload = {
  slug,
  outcome,
  side,
  size: Math.min(size, MAX_SIZE_USDC),
  id,
  debug: payload.debug,
};

if (BOT_WEBHOOK_KEY) botPayload.key = BOT_WEBHOOK_KEY;

await $.Http.post(BOT_WEBHOOK_URL, botPayload, {
  "Content-Type": "application/json",
});

Two details that trip people up:

  • Key protection lives in different places on each side. The Data Webhook checks an X-Key header (that's what the strategy sends). The Bot Webhook expects its key inside the JSON body as key. Mixing these up is the most common reason a "working" pipeline silently does nothing.
  • The size clamp is the whole point of the extra hop. Copying trades 1:1 by dollar amount means a whale's bet size becomes your bet size. MAX_SIZE_USDC caps that without touching anything upstream — change it any time without redeploying the strategy.

Wiring it up

  1. Create a Data Webhook in Sigrex, point the Code Strategy at its URL.
  2. Attach a Code Reaction to that Data Webhook with the second script above.
  3. Create a Prediction Signal Bot, connect your Polymarket wallet credentials, and set it up to listen on a Bot Webhook — that URL is what goes into the Reaction.
  4. When configuring the bot, use slug_from_signal enabled rather than a fixed market. The wallet you're copying isn't limited to one market, so the bot needs to read the market from each incoming signal instead of always trading the same one. A fixed slug would only work if you were copying a single market, not a whole wallet.
  5. Leave the bot inactive first. Let a real trade flow through the whole chain and check it lands correctly in the signal log before flipping it live.

Without step 3 and 4, the Reaction has nothing to post to — the Bot Webhook only does something once a Prediction Signal Bot is actually attached to it and listening.

Every env var here is optional

Nothing above requires a secret to get running:

VariableWhere it's usedRequired?
HOOK_KEYCode Strategy → Data WebhookOnly if you turned on key protection for the Data Webhook
BOT_HOOK_KEYCode Reaction → Bot WebhookOnly if you turned on key protection for the Bot Webhook

If you haven't enabled key protection on either webhook, skip both — the scripts already guard for that (if (WEBHOOK_KEY) ..., if (BOT_WEBHOOK_KEY) ...) and just won't send a key. Key protection is still worth turning on if either webhook URL could leak, since anyone with the raw URL could otherwise post fake trades into your pipeline.

The honest tradeoff

Straight strategy-to-bot is fewer moving parts and marginally lower latency. For a wallet you trust completely and a size you're fully comfortable auto-copying at face value, that might be enough.

The extra hop through a Data Webhook and Code Reaction costs you one more network round trip. In exchange you get a validation layer that doesn't trust anything blindly, a size clamp that protects you from a whale's bet size becoming your bet size, and an audit trail that exists independently of the bot itself. For anything involving real funds copying a wallet you don't control, that trade is worth making.

Related posts

Connecting External APIs Securely with $.Http
Code Strategy·2 min read

Connecting External APIs Securely with $.Http

Standard JavaScript runtimes like Node.js or browsers rely on global functions like fetch or axios to interact with external web services. However, in secure algorithmic environments, accessing unmanaged networking interfaces can introduce significant latency, security risks, or memory overhead. In Code Strategies, external network communication is handled through the built-in $.Http object. $.Http provides a streamlined, secure utility for sending HTTP requests, managing caching automatically

2026-07-23
Time Gates in Code Strategies
Code Strategy·3 min read

Time Gates in Code Strategies

When building automated trading algorithms, controlling when your strategy evaluates the market is just as important as defining what it does. Because Code Strategies run continuously in an infinite loop—executing one iteration immediately after another—performing a full market evaluation or making external network requests on every single pass can lead to rate-limiting issues, noisy signal processing, and degraded performance. This is where Time Gates come in. A Time Gate creates a controlle

2026-07-23
How to Use $.Storage in Code Strategies
Code Strategy·2 min read

How to Use $.Storage in Code Strategies

When building continuous algorithmic trading strategies, keeping track of historical context across loop iterations is essential. Because Code Strategies execute in an isolated runtime environment, standard in-memory variables reset between runs. To maintain continuity—such as tracking open position sizes, counting trade entries, storing indicator states, or logging timestamp markers—your strategy needs a reliable way to persist data. This is where $.Storage comes in. What is $.Storage? $.S

2026-07-23