Developer Guide

Polymarket 接入文档

Base URL:https://polymarket.gzshujiu.com/api

友商接入(服务端签名 · 三请求头)

APPSECRET 永远不能进浏览器。 只放在友商服务器(或受控 BFF)上用于签名。
1

获取 APPID / APPKEY / APPSECRET

登录后打开 开发者控制台 创建应用。 平台会为应用生成 APPKEY(公钥)APPSECRET(私钥种子) 一对: 你在服务端用 APPSECRET 对请求做签名,平台用已登记的 APPKEY 验签;平台只保存 APPKEY,不保存 APPSECRET,APPSECRET 须由你方服务器自行保管。APPSECRET 仅在创建成功时展示一次,请务必复制并安全保存。

2

友商服务端计算 Ed25519 签名

待签消息为 UTF-8 字符串 APPID + "\n" + 商户用户 ID(中间一个换行符);用 sk_ 种子推导私钥签名;X-Signature 为 64 字节签名的 base64url(无填充)。详见 签名与多语言示例

3

调用业务 API

每个请求的 HTTP 头中须携带:X-App-Id(与控制台 APPID 一致)、X-Uid(商户用户 ID)、X-Signature(上一步)。

然后调用 /trades/*/positions/* 等。管理站用户可使用 Authorization: Bearer(HS256)。

密钥遗失:控制台「重新生成密钥」—— APPID 不变,旧私钥签出的请求全部失效。

流程图(总览)

时序图

服务端验签(Polymarket 内部)

签名规范与多语言示例

与后端 internal/partnerauth.SignedMessage 一致:待签内容为 APPID + "\n" + 用户 ID 的 UTF-8 字节。 从 sk_ 解析 32 字节 seed → ed25519.NewKeyFromSeed(Go)或等价 API。

请求头

要求
X-App-Id等于控制台的 APPID
X-Uid商户用户 ID(与签名中 UID 一致),非空,≤128 字符
X-Signature对上述消息的 Ed25519 签名,base64url

Go

package main

import (
  "crypto/ed25519"
  "encoding/base64"
  "strings"
)

func main() {
  appID := "<你的 APPID>"
  uid := "user_123"
  appSecret := "sk_..." // sk_ + base64url(32-byte seed)
  seed, _ := base64.RawURLEncoding.DecodeString(strings.TrimPrefix(appSecret, "sk_"))
  priv := ed25519.NewKeyFromSeed(seed)
  msg := []byte(appID + "\n" + uid)
  sig := ed25519.Sign(priv, msg)
  sigB64 := base64.RawURLEncoding.EncodeToString(sig)
  _ = sigB64
}

Node.js(@noble/ed25519)

import { sign } from '@noble/ed25519';

const appId = '<你的 APPID>';
const uid = 'user_123';
const seed = Uint8Array.from(Buffer.from('sk_....'.slice(3), 'base64url'));
const msg = new TextEncoder().encode(`${appId}\n${uid}`);
const sig = await sign(msg, seed);
const sigB64 = Buffer.from(sig).toString('base64url');

Python 3

import base64
from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey

app_id = "<你的 APPID>"
uid = "user_123"
app_secret = "sk_..."  # sk_ + base64url seed
seed = base64.urlsafe_b64decode(app_secret.removeprefix("sk_") + "==")[:32]
priv = Ed25519PrivateKey.from_private_bytes(seed)
msg = f"{app_id}\n{uid}".encode("utf-8")
sig_b64 = base64.urlsafe_b64encode(priv.sign(msg)).decode().rstrip("=")
print(sig_b64)

Java(JDK Ed25519)

// 伪代码:Base64URL 解码 sk_ 去掉前缀得 32B seed → Ed25519PrivateKey
// 对 UTF-8(APPID + "\n" + 用户ID) 签名 → Base64URL 无填充 → X-Signature
💡 将下文 测试向量 复制到你方环境,签出同字符串即表示实现正确。

测试向量(可复现)

由仓库 go run ./scripts/gen_partner_signature_vectors 生成。

APPIDapp_doc_vector_01
APPKEYak_sGaWp6jdFNlgrLwAmWDCkT-oy0P816_MHQQtREUXmlE
APPSECRETsk_ASNFZ4mrze_-3LqYdlQyEBEiM0RVZneImaq7zN3u_wA
X-Uiddemo_user_001
消息(UTF-8)app_doc_vector_01 + 换行 + demo_user_001
X-SignaturePLTcR4vRdyxdG5v94aobkU9ClRTfNSSAsjt25DS2y-2wcHXtIJyjAHBwgHwInAzXZBfKWiJgJ1t9QC6dI15kAQ
仅在平台登记了相同 APPID→APPKEY 时验签通过;控制台新建应用会得到不同密钥。

📈 交易流程

① 获取市场列表

GET /markets?status=ACTIVE
// status 可选:PENDING | ACTIVE | PAUSED | SETTLED

② 试算(强烈建议先 preview)

POST /trades/preview
X-App-Id: <APPID>
X-Uid: <商户用户ID>
X-Signature: <base64url Ed25519>
# 或使用管理站:Authorization: Bearer <HS256>

{
  "marketId": "市场UUID",
  "side": "YES",      // YES 或 NO
  "tickets": 10       // 购买票数
}

// 响应
{
  "currentPrice": 0.5,
  "netCost": 5.25,       // 实际扣款(含5%手续费)
  "priceImpact": 0.002,  // 价格影响
  "newYesPrice": 0.502,
  "maxTickets": 490
}

③ 正式下单

POST /trades/buy
X-App-Id: <APPID>
X-Uid: <商户用户ID>
X-Signature: <base64url Ed25519>
# 或 Authorization: Bearer <HS256>

{
  "marketId": "市场UUID",
  "side": "YES",
  "tickets": 10
}

// 响应
{
  "trade": {
    "id": "交易UUID",
    "netCost": "5.25",
    "tickets": "10",
    "side": "YES"
  },
  "newBalance": 994.75,
  "yesPrice": 0.502,
  "noPrice": 0.498
}
💡 下单成功后服务端会自动通过 WebSocket 向该市场所有订阅者广播 price_updatetrade_feed 事件。

🔌 WebSocket 实时推送

连接端点

wss://polymarket.gzshujiu.com/api/ws

最小接入示例(浏览器)

const ws = new WebSocket('wss://polymarket.gzshujiu.com/api/ws');

ws.addEventListener('open', () => {
  // 订阅指定市场
  ws.send(JSON.stringify({
    type: 'subscribe_market',
    marketId: '市场UUID'
  }));
  // 同时订阅 BTC 实时价格
  ws.send(JSON.stringify({ type: 'subscribe_btc' }));
});

ws.addEventListener('message', (ev) => {
  const msg = JSON.parse(ev.data);
  switch (msg.type) {
    case 'price_update':
      // { yesPrice, noPrice, yesPool, noPool, maxYesTickets, maxNoTickets }
      updatePriceDisplay(msg);
      break;
    case 'trade_feed':
      // { side, tickets, price, grossCost, timestamp }
      addTradeToFeed(msg);
      break;
    case 'market_settled':
      // { marketId, result: 'YES'|'NO', totalPayout }
      showSettlementResult(msg);
      break;
    case 'btc_price':
      // { price, timestamp }
      updateBtcBanner(msg);
      break;
  }
});

// 心跳保活(每25秒)
setInterval(() => ws.send(JSON.stringify({ type: 'ping' })), 25000);

全部客户端消息类型

type附加字段说明
subscribe_marketmarketId订阅市场行情,回 subscribed
unsubscribe_marketmarketId取消订阅
subscribe_btc订阅BTC实时价格
unsubscribe_btc取消BTC订阅
ping心跳,回 pong

📊 市场状态与生命周期

状态说明

statusstatusCode中文isTradable说明
PENDING0待开盘falsestartTime 未到
ACTIVE1交易中true可正常下单
PAUSED2封盘结算中false停止下单,等待派奖
SETTLED3已结算false已派奖完毕
💡 推荐使用 GET /markets/{id}/status 获取状态,它会根据当前时间自动纠正(比如到了 settlementTime 但还未手动结算,会返回 PAUSED 而不是 ACTIVE)。直接用 isTradable 字段控制下单按钮最简单。

⚠️ 错误码速查

HTTPmessage含义 / 处理建议
400Insufficient balance余额不足,提示用户充值
400Market is not active市场不可交易,刷新市场状态
400Exceeds maximum allowed tickets超过最大可购票数,降低数量
400YES side is frozen...该侧价格触 99% 上限,引导用户买对侧
400X-App-Id, X-Uid…(缺一)三伙伴请求头须同时发送
401Missing authentication…未带三头且未带 Bearer
401invalid signature / unknown APPID签名或 APPID 错误
401Invalid or expired tokenHS256 管理员 token 无效或过期
403Insufficient permissions非管理员调用了管理员接口
404Market not found市场 ID 不存在

📖 完整接口参考