基于 PolyEdge SDK 构建 Polymarket 跟单机器人

本指南介绍如何使用 PolyEdge TypeScript SDK(polyedge)捕获出块前内存池Polymarket交易,并配合 Polymarket 官方 SDK(@polymarket/client)实现毫秒级自动化跟单。

[PolyEdge 内存池推流] ──(tx pre-block)──> [匹配目标交易员] ──> [Polymarket 下单接口]

1. 项目初始化与依赖

创建项目并安装依赖:

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

在项目根目录下配置 .env 文件:

# PolyEdge API Key
POLYEDGE_API_KEY=pe_live_xxxxxxxxxxxxxxxx

# Polymarket 账户配置
POLYMARKET_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
POLYMARKET_DEPOSIT_WALLET=0xYourPolymarketDepositWalletAddress

# 监控目标交易员地址(小写,逗号分隔)
TARGET_ADDRESSES=0x34dd4a4b70eaf79a17878f7938263c801d4dfd83,0x4bf459194025b2ca11a2db2deb9ef2a08c467021

# 仓位模式:FIXED (固定金额) 或 PROPORTIONAL (按比例)
COPY_MODE=FIXED

# FIXED 模式:单笔跟单金额 (USD)
FIXED_AMOUNT_USD=50

# PROPORTIONAL 模式:比例系数 (0.1 = 复制 10%) 与上限 (USD)
COPY_RATIO=0.1
MAX_SPEND_USD=200

# 订单类型:FAK (立即成交剩余撤销) 或 FOK (全额成交或完全撤销)
ORDER_TYPE=FAK

# 滑点上限 (0.03 = 3%)
SLIPPAGE_TOLERANCE_PCT=0.03

2. 订单类型与仓位模式

订单执行类型 (OrderType)

  • OrderType.FAK (Fill-And-Kill / 推荐):市价单立即与盘口撮合,无法立即成交的部分自动撤销,不挂单入册。
  • OrderType.FOK (Fill-Or-Kill):整笔订单必须全额立即成交,否则全部撤销。

仓位计算模式

  • 固定金额 (FIXED):每笔信号均按固定美元金额(如 $50)下单。
  • 等比复制 (PROPORTIONAL):按目标交易员实际成交额的固定比例(如 10%)下单,并通过 MAX_SPEND_USD 设限。

3. 数据模型定义 (types.ts)

LiveTransaction 等底层协议类型由 polyedge 导出,此处仅需定义跟单信号与本地配置接口:

// 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. 初始化与连接推流 (stream.ts)

使用 polyedge SDK 创建监控特定地址的专属推流并建立连接。SDK 内置 45 秒看门狗与断线自动恢复机制:

// stream.ts
import { PolyEdgeClient, type PolyEdgeStream } from "polyedge";

export async function createAndConnectStream(
  apiKey: string,
  targetAddresses: string[]
): Promise<PolyEdgeStream> {
  const client = new PolyEdgeClient({ apiKey });

  // 1. 创建过滤目标地址的交易流配置
  const streamConfig = await client.streams.create({
    name: `CopyBot-${Date.now()}`,
    addresses: targetAddresses,
  });

  if (!streamConfig.id) {
    throw new Error("创建交易流失败:未返回流 ID");
  }

  console.log(`[PolyEdge] 交易流创建成功: ${streamConfig.id}`);

  // 2. 连接 SSE 实时推流
  return client.streams.connect(streamConfig.id, {
    heartbeatTimeoutMs: 45_000,
  });
}

5. 交易归因与信号提取 (matcher.ts)

Polymarket 预测市场采用二元代币结算架构,每个市场包含两个 Outcome Token。token_ids_index 严格取 0 或 1,分别对应两个代币的 Asset ID。

逐一检查该笔交易的主动吃单方(taker)与被动挂单方(makers),确认目标交易员角色并计算成交价:

// 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. 检查 Taker (主动吃单)
  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. 检查 Makers (被动挂单撮合成交)
  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. 下单执行器 (executor.ts)

使用 Polymarket 官方 SDK(@polymarket/client)计算仓位与滑点保护边界,提交市价单:

// 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;

    // 滑点约束:买单设最高限价,卖单设最低限价
    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(
      `[执行跟单] 目标: ${signal.targetAddress} (${signal.role}) | ` +
      `方向: ${signal.side} | 金额: $${orderUsd.toFixed(2)} | ` +
      `${signal.side === "BUY" ? `价格上限: ${maxPrice}` : `价格下限: ${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. 入口主程序 (index.ts)

通过异步迭代器(for await)消费实时推流,提取跟单信号并执行:

// 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("缺少环境变量,请检查 .env 配置");
  }

  const targetAddresses = rawTargetAddresses
    .split(",")
    .map((addr) => addr.trim().toLowerCase())
    .filter(Boolean);
  const targetSet = new Set(targetAddresses);

  // 1. 初始化 Polymarket 下单客户端
  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. 建立 PolyEdge 交易流连接
  const stream = await createAndConnectStream(polyedgeApiKey, targetAddresses);
  const processedTxHashes = new Set<string>();

  process.on("SIGINT", () => {
    console.log("\n[退出] 正在关闭连接...");
    stream.destroy();
    process.exit(0);
  });

  console.log(`[监听中] 正在捕获出块前内存池Polymarket交易...`);

  // 3. 消费交易流事件
  for await (const tx of stream) {
    if (!tx.tx_hash) continue;
    if (processedTxHashes.has(tx.tx_hash)) continue;
    processedTxHashes.add(tx.tx_hash);

    // 维持最近 5,000 笔哈希滑动窗口
    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(`[跟单结果]`, result);
      } catch (err) {
        console.error(`[下单失败] 目标交易: ${signal.txHash}`, err);
      }
    }
  }
}

main().catch((err) => {
  console.error("运行失败:", err);
  process.exit(1);
});

8. 启动与验证

使用 tsx 启动:

npx tsx index.ts

当目标交易员触发成交时,终端输出跟单日志:

[PolyEdge] 交易流创建成功: 9bae9005-9ef1-4f17-8658-36ea54536836
[监听中] 正在捕获出块前内存池Polymarket交易...
[执行跟单] 目标: 0x4bf459194025b2ca11a2db2deb9ef2a08c467021 (taker) | 方向: BUY | 金额: $50.00 | 价格上限: 0.5047
[跟单结果] { orderId: '0x...', status: 'MATCHED' }