본문으로 건너뛰기

폴리마켓 복사 거래 봇 구축하기

업데이트 날짜:
2026년 2월 20일

읽는 데 9분

이 가이드에 포함된 코드는 아직 검토를 거치지 않았습니다. 코드를 운영 환경에 배포하기 전에 반드시 검토할 것을 강력히 권장합니다.

개요

얼마 전까지만 해도 현실 세계의 사건에 베팅하려면 신원 확인 절차를 모두 거쳐야 하는 카지노나 스포츠북을 이용해야 했고, 설령 그렇게 해도 베팅할 수 있는 시장은 극히 제한적이었습니다. 하지만 오늘날 Polymarket과 같은 온체인 예측 시장을 통해서는 선거 결과부터 라이브 스트리밍 중 특정 단어가 몇 번 언급될지, 혹은 향후 15분 내 자산 가격이 어떻게 될지에 이르기까지 모든 것에 대한 결과를 거래할 수 있습니다. 계정 등록 양식은 필요 없으며, 스마트 계약과 공개 블록체인 원장만 있으면 됩니다.

이 가이드에서는 Polymarket 거래 봇을 만드는 방법을 안내해 드리겠습니다. 구체적으로, 대상 지갑을 모니터링하고, 해당 지갑의 거래 내역을 실시간으로 감지한 뒤, 매수 주문을 그대로 따라 하는 봇을 만들 것입니다.

그럼 시작해 볼까요!

주요 업무


  • 개발자를 위한 Polymarket 알아보기
  • Quicknode 엔드포인트 생성
  • 봇 코드베이스 및 환경 설정
  • EOA 자격 증명을 사용하여 Polymarket CLOB 클라이언트를 초기화합니다.
  • Data API를 사용하여 대상 지갑 간 거래를 감지하기
  • 더 빠른 업데이트를 위해 WebSocket 모니터링 기능을 추가하세요
  • 포지션을 추적하고 간단한 위험 한도를 적용합니다
  • 데모 목적으로 ‘매수’ 전용 복사 흐름을 실행합니다.

준비물


건축

코딩에 들어가기 전 요약:


  • Polymarket CLOB: 사용자의 봇이 주문을 체결하고, API가 주문 흐름을 처리하며, 결제는 온체인에서 비수탁 방식으로 이루어집니다.
  • 데이터 API (검색): 복사하려는 지갑에서 새로운 거래 내역을 찾아줍니다.
  • WebSocket (실시간): 구독 시 시장 및 사용자 정보를 더 빠르게 업데이트해 줍니다.
  • Executor: 복사된 주문을 적절한 크기로 조정하고, 유효성 검사를 수행한 후 제출합니다.
  • 위험 + 포지션: 노출 규모를 추적하고 한도를 초과하는 거래를 차단하세요.
  • 데모 가드레일: 이 튜토리얼에서는 매수(BUY) 주문만 처리합니다(매도(SELL) 주문은 건너뜁니다).

프로젝트 전제 조건: Quicknode 엔드포인트 확보하기

공개 노드를 사용하시거나 자체 인프라를 관리하셔도 무방합니다. 하지만 더 빠른 응답 시간을 원하신다면, 번거로운 작업은 저희에게 맡기시면 됩니다. 여기에서 Quicknode 무료 계정을 등록하세요 그리고 다음을 생성합니다. 폴리곤 메인넷 엔드포인트. 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는 이 설정에서 이더 v5를 사용하도록 되어 있습니다.

npm 설치

2단계: 환경 변수 설정

이 미니멀한 것을 사용하세요 .env EOA 모드의 경우:

# 필수
TARGET_WALLET=0xTARGET_WALLET_TO_COPY
PRIVATE_KEY=0x사용자_개인_키
RPC_URL=https://polygon-mainnet.quiknode.pro/YOUR_KEY

# 선택 사항: 지오 토큰 (Polymarket에서 귀하의 계정/지역에 대해 제공된 경우)
POLYMARKET_GEO_TOKEN=

# 거래
포지션 배율=0.1
최대 거래 규모=100
최소 거래 규모=1
슬리피지 허용치=0.02
주문 유형=FOK

# 리스크 (0은 상한선을 비활성화합니다)
최대 세션 명목 금액=0
MAX_PER_MARKET_NOTIONAL=0

# 모니터링
USE_WEBSOCKET=true
사용자 채널 사용=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 폴링

데이터 API를 사용하여 대상 지갑에서 발생한 새로운 거래를 감지합니다. 데이터 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.
  • 사용자 채널 구독은 다음을 통해 가능합니다 시장 그리고 인증이 필요합니다.
  • 이 봇은 지연 연결 방식을 사용합니다. 즉, 구독이 하나 이상 존재하면 웹소켓이 한 번 시작됩니다.

코드는 다음 위치에 있습니다. 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 에서 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 에서 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 시작

시작 시 출력 예시:

🤖 폴리마켓 복사 거래 봇
================================
대상 지갑: 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 뉴스레터를 구독해 주세요. 트위터와 디스코드에서도 저희와 소통하실 수 있습니다.

여러분의 피드백을 ❤️ 환영합니다!

의견이나 다루었으면 하는 새로운 주제가 있다면 알려주세요. 여러분의 의견을 기다리고 있습니다.

이 가이드를 공유하세요