构建Polymarket复制交易机器人

本指南详细介绍了如何构建一个Polymarket交易机器人,该机器人能够监控目标钱包的实时交易并镜像其买单。文章涵盖了Polymarket开发基础、Quicknode端点设置、CLOB客户端初始化、通过数据API和WebSocket检测交易、头寸跟踪、风险限制以及一个仅限买入的复制交易流程。

本指南中的代码尚未经过审计。强烈建议你在部署到生产环境之前对任何代码进行审计。

概述

不久前,押注现实世界事件意味着赌场或体育博彩,它们需要完整的身份验证,即便如此,你也只能限于狭窄的市场范围。如今,像 Polymarket 这样的链上预测市场让你可以交易各种结果,从选举结果到某人在直播期间说某个特定单词的次数,或者某种资产在未来 15 分钟内的价格。没有账户表格,只有智能合约和公共区块链账本。

在本指南中,我们将向你展示如何构建一个 Polymarket 交易机器人。具体来说,我们将构建一个机器人,它会监控目标钱包,实时检测其交易,并镜像买单。

让我们开始吧!

你将做什么

  • 了解面向开发者的 Polymarket
  • 创建一个 Quicknode 端点
  • 设置机器人代码库和环境
  • 使用 EOA 凭据初始化 Polymarket CLOB 客户端
  • 使用 Data API 检测目标钱包交易
  • 添加 WebSocket 监控以实现更快更新
  • 跟踪头寸并执行简单的风险限制
  • 为演示目的运行一个只买入的复制流程

你将需要什么

  • 一个在 Polygon 主网上存有 USDC.e 和 POL 资金的 EOA 钱包
  • TypeScript + Node.js
  • Polymarket CLOB SDK
  • 一个 Quicknode 端点
  • Polymarket 复制交易机器人代码在此处

架构

在编码之前的 TLDR:

  • Polymarket CLOB:你的机器人签署订单,API 处理订单流,结算在链上保持非托管。
  • Data API (发现):从你想要复制的钱包中找到新交易。
  • WebSocket (实时):一旦订阅,提供更快的市场/用户更新。
  • 执行器:确定复制订单的规模,验证检查,并提交它。
  • 风险 + 头寸:跟踪风险敞口并阻止超出你限制的交易。
  • 演示防护:本教程仅限买入流程(卖出交易被跳过)。

项目先决条件:获取 Quicknode 端点

欢迎你使用公共节点或管理自己的基础设施;但是,如果你想要更快的响应时间,可以将繁重的工作交给我们。在此处注册一个免费的 Quicknode 账户并创建一个 Polygon Mainnet 端点。将你的 HTTP 和 WSS URL 保存在 .env 文件中备用。

quicknode endpoint

克隆仓库

克隆 Quicknode 示例仓库并进入项目目录:

git clone https://github.com/quiknode-labs/qn-guide-examples.git
cd qn-guide-examples/defi/polymarket-copy-bot

步骤 1:安装依赖

Polymarket 的 SDK 目前在此设置中预期使用 ethers v5。

npm install

步骤 2:配置环境变量

使用这个最小的 .env 文件进行 EOA 模式配置:

## Required
TARGET_WALLET=0xTARGET_WALLET_TO_COPY
PRIVATE_KEY=0xYOUR_PRIVATE_KEY
RPC_URL=https://polygon-mainnet.quiknode.pro/YOUR_KEY

## Optional geo token (if provided by Polymarket for your account/region)
POLYMARKET_GEO_TOKEN=

## Trading
POSITION_MULTIPLIER=0.1
MAX_TRADE_SIZE=100
MIN_TRADE_SIZE=1
SLIPPAGE_TOLERANCE=0.02
ORDER_TYPE=FOK

## Risk (0 disables the cap)
MAX_SESSION_NOTIONAL=0
MAX_PER_MARKET_NOTIONAL=0

## Monitoring
USE_WEBSOCKET=true
USE_USER_CHANNEL=false
POLL_INTERVAL=2000
WS_ASSET_IDS=
WS_MARKET_IDS=

## Optional gas floor overrides for Polygon approval/order txs
MIN_PRIORITY_FEE_GWEI=30
MIN_MAX_FEE_GWEI=60

注意:

  • 由于我们使用私钥而不是 Polymarket 代理钱包,因此本指南中机器人始终以 EOA 模式运行(signatureType=0)。
  • 机器人在启动时从 PRIVATE_KEY 派生/生成用户 API 凭据。
  • 首次以 EOA 模式运行时可能会触发 USDC.e/CTF 支出者的批准交易。
  • 演示时从小规模开始(MAX_TRADE_SIZE=5 或更低)。

步骤 3:初始化 CLOB 客户端 (EOA)

机器人代码在 TradeExecutor 内部通过先派生后创建的备用逻辑完成此操作。

代码位于 src/trader.ts

private async deriveAndReinitApiKeys(funderAddress: string): Promise<void> {
  console.log(`   Generating API credentials programmatically...`);
  let creds = await this.clobClient.deriveApiKey().catch(() => null);
  if (!creds || this.isApiError(creds)) {
    creds = await this.clobClient.createApiKey();
  }

  const apiKey = (creds as any)?.apiKey || (creds as any)?.key;
  if (this.isApiError(creds) || !apiKey || !creds?.secret || !creds?.passphrase) {
    const errMsg = this.getApiErrorMessage(creds);
    throw new Error(`Could not create/derive API key: ${errMsg}`);
  }

  this.clobClient = new ClobClient(
    'https://clob.polymarket.com',
    137,
    this.wallet,
    {
      key: apiKey,
      secret: creds.secret,
      passphrase: creds.passphrase,
    },
    0,
    funderAddress,
    config.polymarketGeoToken || undefined
  );
}

步骤 4:REST 轮询以发现交易

使用 Data API 检测来自目标钱包的新交易。 Data API 是此机器人中的发现层(它发现要复制的内容)。

代码位于 src/monitor.ts

const response = await axios.get(
  'https://data-api.polymarket.com/activity',
  {
    params: {
      user: config.targetWallet.toLowerCase(),
      type: 'TRADE',
      limit: 100,
      sortBy: 'TIMESTAMP',
      sortDirection: 'DESC',
      start: startSeconds,
    },
    headers: {
      'Accept': 'application/json',
    },
  }
);

步骤 5:WebSocket 监控(市场 + 用户通道)

Polymarket 公开 marketuser WebSocket 通道。

  • market 通道通过 assets_ids 订阅。
  • user 通道通过 markets 订阅并需要认证。
  • 机器人懒惰连接:一旦存在至少一个订阅,WebSocket 就会启动。

代码位于 src/websocket-monitor.ts

private buildWsAuth(): { apikey: string; apiKey: string; secret: string; passphrase: string } | undefined {
  if (!this.auth) return undefined;
  return {
    apikey: this.auth.apiKey,
    apiKey: this.auth.apiKey,
    secret: this.auth.secret,
    passphrase: this.auth.passphrase,
  };
}

private sendInitialSubscribe(): void {
  if (!this.ws) return;

  const payload: any = { type: this.channel };
  if (this.channel === 'market') {
    payload.assets_ids = Array.from(this.subscribedAssets);
  } else {
    payload.markets = Array.from(this.subscribedMarkets);
    payload.auth = this.buildWsAuth();
  }

  if ((payload.assets_ids && payload.assets_ids.length) || (payload.markets && payload.markets.length)) {
    this.ws.send(JSON.stringify(payload));
  }
}

附加资源:

步骤 6:头寸跟踪

头寸在启动时加载(尽力而为),并从成功的填充中更新。

代码位于 src/positions.ts

recordFill(params: {
  trade: Trade;
  notional: number;
  shares: number;
  price: number;
  side: 'BUY' | 'SELL';
}): void {
  const { trade, notional, shares, price, side } = params;
  const key = trade.tokenId;
  const existing = this.positions.get(key);

  const sign = side === 'BUY' ? 1 : -1;
  const deltaShares = shares * sign;
  const deltaNotional = notional * sign;

  const nextShares = (existing?.shares || 0) + deltaShares;
  const nextNotional = (existing?.notional || 0) + deltaNotional;
  const avgPrice = nextShares !== 0 ? Math.abs(nextNotional / nextShares) : 0;

  const updated: PositionState = {
    tokenId: trade.tokenId,
    market: trade.market,
    outcome: trade.outcome,
    shares: Math.max(0, nextShares),
    notional: Math.max(0, nextNotional),
    avgPrice: nextShares !== 0 ? avgPrice : 0,
    lastUpdated: Date.now(),
  };

  this.positions.set(key, updated);
}

步骤 7:风险控制(简单上限)

机器人在执行前强制执行最大会话名义金额和每个市场最大名义金额

代码位于 src/risk-manager.ts

checkTrade(trade: Trade, copyNotional: number): RiskCheckResult {
  if (copyNotional <= 0) {
    return { allowed: false, reason: 'Copy notional is <= 0' };
  }

  if (config.risk.maxSessionNotional > 0) {
    const nextSession = this.sessionNotional + copyNotional;
    if (nextSession > config.risk.maxSessionNotional) {
      return {
        allowed: false,
        reason: `Session notional cap exceeded (${nextSession.toFixed(2)} > ${config.risk.maxSessionNotional})`,
      };
    }
  }

  if (config.risk.maxPerMarketNotional > 0) {
    const current = this.positions.getNotional(trade.tokenId);
    const next = current + copyNotional;
    if (next > config.risk.maxPerMarketNotional) {
      return {
        allowed: false,
        reason: `Per-market notional cap exceeded (${next.toFixed(2)} > ${config.risk.maxPerMarketNotional})`,
      };
    }
  }

  return { allowed: true };
}

机器人中的运行时防护检查:

const copyNotional = this.executor.calculateCopySize(trade.size);
const riskCheck = this.risk.checkTrade(trade, copyNotional);
if (!riskCheck.allowed) {
  console.log(`⚠️  Risk check blocked trade: ${riskCheck.reason}`);
  return;
}

单笔交易生命周期集中在一处(src/index.ts 中的 handleNewTrade):

if (trade.side === 'SELL') {
  console.log('⚠️  Skipping SELL trade (BUY-only safeguard enabled)');
  return;
}

if (this.wsMonitor) {
  await this.wsMonitor.subscribeToMarket(trade.tokenId);
}

const copyNotional = this.executor.calculateCopySize(trade.size);
const riskCheck = this.risk.checkTrade(trade, copyNotional);
if (!riskCheck.allowed) {
  console.log(`⚠️  Risk check blocked trade: ${riskCheck.reason}`);
  return;
}

try {
  const result = await this.executor.executeCopyTrade(trade, copyNotional);
  this.risk.recordFill({
    trade,
    notional: result.copyNotional,
    shares: result.copyShares,
    price: result.price,
    side: result.side,
  });
  this.stats.tradesCopied++;
  this.stats.totalVolume += result.copyNotional;
  console.log(`✅ Successfully copied trade!`);
} catch (error: any) {
  this.stats.tradesFailed++;
  console.log(`❌ Failed to copy trade`);
  if (error?.message) {
    console.log(`   Reason: ${error.message}`);
  }
}

重试策略(来自 src/trader.ts 中的 isRetryableError):

if (responseStatus === 401 || errorMsg.includes('unauthorized') || responseData.includes('unauthorized')) {
  return false;
}
if (responseStatus === 403 || errorMsg.includes('cloudflare') || responseData.includes('blocked')) {
  return false;
}
if (errorMsg.includes('network') || errorMsg.includes('timeout') || errorMsg.includes('econnreset')) {
  return true;
}
if (errorMsg.includes('rate limit') || responseData.includes('rate limit')) {
  return true;
}
if (errorMsg.includes('502') || errorMsg.includes('503') || errorMsg.includes('504')) {
  return true;
}
if (errorMsg.includes('insufficient') || responseData.includes('allowance')) {
  return false;
}
if (errorMsg.includes('invalid') || responseData.includes('bad request')) {
  return false;
}
if (errorMsg.includes('duplicate') || responseData.includes('duplicate')) {
  return false;
}

运行机器人

确保 .env 值已设置,然后启动机器人:

npm start

启动输出示例:

🤖 Polymarket Copy Trading Bot
================================
Target wallet: 0x...
Order type: FOK
WebSocket: Enabled
Auth mode: EOA (signature type 0)
================================

ℹ️  API credentials will be derived/generated from PRIVATE_KEY at startup
✅ Configuration validated
🔧 Initializing trader...
✅ Trader initialized
ℹ️  WebSocket waiting for first subscription before connecting
🚀 Bot started! Monitoring via: WebSocket + REST API

检测到交易

当目标钱包进行新交易时,机器人会检测到它,评估风险,并尝试下达复制订单。

🎯 New trade detected: BUY 12.5 USDC @ 0.62
   Time: 2026-02-17T14:03:11.000Z

==================================================
🎯 NEW TRADE DETECTED
   Time: 2026-02-17T14:03:11.000Z
   Market: 0xabc123...
   Side: BUY YES
   Size: 12.5 USDC @ 0.620
   Token ID: 1234567890...
==================================================
📈 Executing copy trade (FOK):
   Market: 0xabc123...
   Side: BUY
   Original size: 12.5 USDC
   Token ID: 1234567890...
   Copy notional: 1.25 USDC
   Balance/allowance check passed
   Market price: 0.6300
   Copy shares: 1.9841
✅ FOK order executed: 0xorderid...
✅ Successfully copied trade!
📊 Session Stats: 1/1 copied, 0 failed

当检测到目标钱包进行卖出交易时,此演示会故意跳过它:

⚠️  Skipping SELL trade (BUY-only safeguard enabled)

最终思考

你现在拥有了一个可工作的复制交易机器人,它能够监控目标钱包,强制执行风险限制,并以 EOA 模式执行。这是一个快速演示实现,所以请保持交易规模较小,并将其视为继续学习的基础。

订阅我们的 Quicknode 新闻通讯 以获取更多关于区块链基础设施的指南。你还可以在 TwitterDiscord 上与我们联系。

我们 ❤️ 反馈!

告诉我们你是否有任何反馈或对新主题的请求。我们很乐意听取你的意见。

  • 原文链接: quicknode.com/guides/def...
  • 登链社区 AI 助手,为大家转译优秀英文文章,如有翻译不通的地方,还请包涵~
点赞 0
收藏 0
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
QuickNode
QuickNode
江湖只有他的大名,没有他的介绍。