PMWallets
中文

Polymarket trade alerts by webhook: receive, verify, deduplicate

A webhook turns every fill of a Polymarket trader you follow into an HTTPS request to your own server — for alerts in your own chat, a dashboard, or a bot that should not hold a WebSocket open. This is how to receive them correctly.

When a webhook beats a WebSocket

A WebSocket needs a process that stays connected. A webhook only needs an HTTPS endpoint: a serverless function, a small web app, or a relay into Telegram, Discord or Slack. PMWallets delivers webhooks at least once with retries, while its WebSocket is best effort, so a webhook is the better choice when no fill may be missed and a little extra delay does not matter. Both carry the same fill object.

Register and keep the secret

Register one HTTPS endpoint per account on the PMWallets feed page and subscribe to traders with the webhook channel. The signing secret is shown once, when you register — store it then. Endpoints must be public: localhost and private addresses are refused, and redirects are not followed.

Verify the signature on the raw body

Each request carries x-pmw-signature: an HMAC-SHA256 of the raw request body keyed with your secret, hex-encoded. Verify it against the raw bytes before parsing; parsing and re-serialising the JSON changes the bytes and every signature then fails. Then store the fill durably — a database row or a persistent queue — and only then answer 2xx: a 2xx tells PMWallets the delivery is done, so a fill acknowledged and then lost is not sent again. If storing fails, answer an error and it will be retried.

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

Expect duplicates

Delivery is at least once. PMWallets waits 5 seconds for a 2xx and tries up to 3 times, and a crash between sending and recording can replay a delivery. Make the handler idempotent: store by eventId and ignore one you have already stored.

When the endpoint breaks

An endpoint that has been failing for more than 24 hours, with at least 20 failed deliveries in a row, is switched off and you are emailed. Subscriptions that deliver only by webhook are then paused and not charged until you turn it back on. Fills missed while it was off can be fetched from the replay endpoint or a trade-history export.

Questions

Can I get a notification every time a Polymarket wallet trades?

Yes. Subscribe to the trader on PMWallets with the webhook channel and each fill is POSTed to your endpoint, typically within a second or two of the block. It costs $0.01 per trader per hour, or is included in a plan.

How do I verify a PMWallets webhook?

Compute HMAC-SHA256 of the raw request body with your signing secret and compare it, in constant time, with the hex value in the x-pmw-signature header.

Why did I receive the same Polymarket fill twice?

Webhooks are delivered at least once, so a retry or a replay can repeat a fill. Deduplicate on eventId, which stays the same across every retry.

Related