メインコンテンツへスキップ

Polymarket用コピートレードボットの構築

更新日:
2026年2月20日

読了時間:9分

このガイドに記載されているコードは監査を受けていません。本番環境にデプロイする前に、必ずコードの監査を行うことを強くお勧めします。

概要

つい最近まで、現実世界の出来事への賭けといえば、厳格な本人確認が求められるカジノやスポーツブックに限られており、たとえ利用できたとしても、対象となる市場はごく限られたものばかりでした。しかし今日では、Polymarketのようなオンチェーン予測市場を利用すれば、選挙結果から、ライブ配信中に特定の単語が何回発せられるか、あるいは今後15分間の資産価格の推移に至るまで、あらゆる事象の結果を取引できるようになりました。 アカウント登録フォームは不要で、スマートコントラクトとパブリックブロックチェーンの台帳だけで済みます。

このガイドでは、Polymarket用の取引ボットの作り方を解説します。具体的には、対象のウォレットを監視し、その取引をリアルタイムで検知して、買い注文をミラーリングするボットを作成します。

さあ、始めましょう!

主な業務内容


  • 開発者向け「Polymarket」について
  • Quicknodeエンドポイントを作成する
  • ボットのコードベースと環境を設定する
  • EOAの認証情報を使用してPolymarket CLOBクライアントを初期化する
  • Data API を使用して、ターゲットウォレットとの取引を検出する
  • WebSocketの監視機能を追加して、更新を高速化
  • ポジションを追跡し、シンプルなリスク制限を適用する
  • デモ用に「購入」のみのコピーフローを実行する

必要なもの


建築

コーディングを始める前に、要点をまとめておくと:


  • Polymarket CLOB:ボットが注文を出し、APIが注文フローを処理し、決済は引き続きオンチェーンで非カストディアルに行われます。
  • データ API(ディスカバリー):コピーしたいウォレットから新しい取引を検索します。
  • WebSocket(リアルタイム):購読すると、市場情報やユーザー情報の更新がより迅速に行われます。
  • Executor:コピーされた注文のサイズを調整し、チェックを検証して、送信します。
  • リスク+ポジション:エクスポージャーを把握し、設定した上限を超える取引を阻止します。
  • デモ用ガードレール:このチュートリアルでは「買い」のみの取引を行います(「売り」の取引は省略されます)。

プロジェクトの前提条件:Quicknodeエンドポイントを取得する

パブリックノードをご利用いただくことも、独自のインフラを管理していただくことも可能です。ただし、より高速な応答時間を希望される場合は、面倒な作業は弊社にお任せください。 こちらからQuicknodeの無料アカウントに登録してください そして、 Polygon メインネット エンドポイント。HTTP および WSS の URL を手元に用意しておいてください。 .env ファイル。

quicknodeエンドポイント

リポジトリをクローンする

Quicknodeガイドのサンプルリポジトリをクローンし、プロジェクトディレクトリに移動します:

git 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モードの場合:

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

# オプションのジオトークン(Polymarketからアカウントまたは地域ごとに提供されている場合)
POLYMARKET_GEO_TOKEN=

# 取引
POSITION_MULTIPLIER=0.1
MAX_TRADE_SIZE=100
MIN_TRADE_SIZE=1
スリッページ許容値=0.02
注文種別=FOK

# リスク(0に設定すると上限が無効になります)
MAX_SESSION_NOTIONAL=0
MAX_PER_MARKET_NOTIONAL=0

# モニタリング
USE_WEBSOCKET=true
USE_USER_CHANNEL=false
POLL_INTERVAL=2000
WS_ASSET_IDS=
WS_MARKET_IDS=

# Polygonの承認/注文トランザクションに対するオプションのガス料金下限のオーバーライド
MIN_PRIORITY_FEE_GWEI=30
MIN_MAX_FEE_GWEI=60

注:


  • このボットは常にEOAを実行します(signatureType=0) このガイドでは、Polymarketのプロキシウォレットではなく、自身の秘密鍵を使用しているためです。
  • このボットは、以下の場所からユーザーのAPI認証情報を取得・生成します。 PRIVATE_KEY 起動時に。
  • EOAモードでの初回実行により、USDC.e/CTFの利用者に対して承認トランザクションがトリガーされる場合があります。
  • デモ用には、まずは小さいサイズから始めてみてください(MAX_TRADE_SIZE=5 以下)。

ステップ 3: CLOB クライアント(EOA)の初期化

ボットのコードは内部で次のように処理しています TradeExecutor 「derive-first」を使用し、その後フォールバックを作成します。

コードは 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 の監視(マーケットおよびユーザーチャネル)

ポリマーケットが明らかにする 市場 そして ユーザー WebSocketチャネル。

  • 市場 チャンネルは以下で登録できます assets_ids.
  • ユーザー チャンネルは以下で登録できます 市場 また、認証が必要です。
  • このボットは遅延接続方式を採用しています。つまり、少なくとも1つのサブスクリプションが存在すると、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;
}

単一取引のライフサイクルを一か所で把握(handleNewTrade in src/index.ts):

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}`);
}
}

再試行ポリシー(出典: isRetryableError in src/trader.ts):

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 コピートレードボット
================================
対象ウォレット: 0x...
注文タイプ:FOK
WebSocket:有効
認証モード:EOA (署名 タイプ 0)
================================

ℹ️ API 認証情報は、起動時に PRIVATE_KEY から導出/生成されます
✅ 設定が検証されました
🔧 トレーダーを初期化中...
✅ トレーダーが初期化されました
ℹ️ WebSocket待機中 最初の 接続前に最初のサブスクリプションを待機中
🚀 ボットが起動しました 監視方法:WebSocket + REST API

取引が検出されました

対象のウォレットが新しい取引を実行すると、ボットはそれを検知し、リスクを評価した上で、コピー注文を実行しようとします。

🎯 新しい取引が検出されました:買い 12.5 USDC @ 0.62
時間: 2026-02-17T14:03:11.000Z

==================================================
🎯 新しい取引が検出されました
日時: 2026-02-17T14:03:11.000Z
市場: 0xabc123...
ポジション:買い YES
数量: 12.5 USDC @ 0.620
トークンID: 1234567890...
==================================================
📈 コピートレードの実行 (FOK):
マーケット: 0xabc123...
取引方向:買い
元のサイズ: 12.5 USDC
トークンID: 1234567890...
名目金額: 1.25 USDC
残高/許容額の確認に合格
市場価格: 0.6300
コピーシェア: 1.9841
✅ FOK注文が約定:0xorderid...
✅ 取引のコピーに成功しました!
📊 セッション統計: 1/1をコピー、 0 失敗

対象のウォレットから「売り」取引が検出された場合、このデモでは意図的にそれをスキップします:

⚠️  売り注文をスキップ (「買い」限定の安全策が有効)

まとめ

これで、対象のウォレットを監視し、リスク制限を適用し、EOAモードで取引を実行できるコピートレードボットが完成しました。これは簡単なデモ実装ですので、規模は小さく抑え、今後の学習の土台として活用してください。

ブロックチェーンインフラに関するガイドをもっとご覧になりたい方は、Quicknodeのニュースレターにご登録ください。また、TwitterDiscordでも私たちとつながることができます。

皆様からのフィードバックを心よりお待ちしております!❤️

ご意見や新しいトピックに関するご要望などがありましたら、ぜひお知らせください。皆様からのご連絡を心よりお待ちしております。

このガイドをシェアする