PMWallets
EN

用 Webhook 接收 Polymarket 交易提醒:接收、验签、去重

Webhook 把你关注的 Polymarket 交易者的每一笔成交变成发往你自己服务器的一个 HTTPS 请求 —— 用来在自己的群里发提醒、做看板,或者给不想一直挂着 WebSocket 的机器人用。下面讲怎样正确接收。

什么时候 Webhook 比 WebSocket 合适

WebSocket 需要一个一直连着的进程。Webhook 只需要一个 HTTPS 端点:一个 serverless 函数、一个小网站,或者一个转发到 Telegram、Discord、Slack 的中转。PMWallets 的 Webhook 至少投递一次、会重试,而 WebSocket 是尽力投递,所以当一笔都不能漏、多一点延迟无所谓时,Webhook 更合适。两者送来的成交对象完全相同。

注册并保管好密钥

在 PMWallets 推送页上为账户注册一个 HTTPS 端点,订阅交易者时选 webhook 通道。签名密钥只在注册时显示一次 —— 当场存好。端点必须是公网地址:localhost 和内网地址会被拒绝,也不跟随重定向。

对原始请求体验签

每个请求都带 x-pmw-signature:用你的密钥对原始请求体做 HMAC-SHA256,十六进制编码。要在解析之前对原始字节验签;解析再重新序列化 JSON 会改变字节,签名就全都对不上了。然后把这笔成交可靠地存下来 —— 写进数据库或持久化队列 —— 存好之后才返回 2xx:2xx 告诉 PMWallets 这次投递已完成,先回了 2xx 又丢掉的成交不会再发。存储失败就返回错误,它会被重试。

typescript
import { createHmac, timingSafeEqual } from "node:crypto";

app.post("/hook", express.raw({ type: "application/json" }), async (req, res) => {
  const sent = Buffer.from(req.header("x-pmw-signature") ?? "", "hex");
  const mine = createHmac("sha256", process.env.PMW_SECRET).update(req.body).digest();

  // compare the RAW body, before any JSON parsing: re-serialising changes the bytes
  if (sent.length !== mine.length || !timingSafeEqual(sent, mine)) return res.sendStatus(401);

  // store it durably BEFORE answering: a 2xx means "done" and we stop retrying,
  // so a fill acknowledged and then lost is gone for good. Anything else is retried.
  const fill = JSON.parse(req.body.toString());
  try { await saveOnce(fill.eventId, fill); } catch { return res.sendStatus(503); }
  res.sendStatus(200);
});

要预期会有重复

投递是至少一次。PMWallets 等待 5 秒的 2xx,最多尝试 3 次;发送和记录之间如果崩溃,也可能重放一次投递。把处理逻辑做成幂等:按 eventId 存储,已经存过的就忽略。

端点坏掉时

一个端点连续失败超过 24 小时、且连续失败至少 20 次,就会被停用,并发邮件通知你。只走 Webhook 的订阅随之暂停、不计费,直到你重新启用。停用期间错过的成交,可以从补发接口或交易历史导出取回。

常见问题

能在某个 Polymarket 钱包每次交易时收到通知吗?

能。在 PMWallets 上用 webhook 通道订阅这个交易者,每笔成交都会 POST 到你的端点,通常在出块后一两秒内。每个交易者每小时 $0.01,或包含在套餐里。

怎样验证 PMWallets 的 Webhook?

用你的签名密钥对原始请求体计算 HMAC-SHA256,与 x-pmw-signature 头里的十六进制值做常量时间比较。

为什么同一笔 Polymarket 成交我收到了两次?

Webhook 至少投递一次,重试或重放会让一笔成交重复。按 eventId 去重,它在每次重试中都不变。

相关页面