Alerts and signed webhooks
Alerts and signed webhooks
Section titled “Alerts and signed webhooks”Create a strategy profile (via the create_strategy_profile MCP tool or POST /api/me/profiles) with match criteria and delivery channels. After each listings run the matcher scores new/changed inventory against every active profile and delivers deduped alerts.
Criteria and channels
Section titled “Criteria and channels”{ "name": "BRRRR 85+ under 400k", "strategy": "brrrr", "min_percentile": 85, "max_price": 400000, "min_confidence": 60, "bbox": [-87.75, 41.70, -87.50, 41.85], "channels": { "telegram": true, "webhook": { "url": "https://your-agent.example.com/pm-alerts", "secret": "whsec_…" } }}Matching is deduped on profile + listing + price-bucket (one alert per meaningful price move), digested to at most one push per profile per day carrying the top matches. Every alert row and payload carries provenance: the score snapshot and model_versions that triggered it (5.4.4).
Payload
Section titled “Payload”{ "profile": { "id": "prof_…", "name": "BRRRR 85+ under 400k" }, "strategy": "brrrr", "as_of": "2026-07-10T13:00:00.000Z", "model_versions": { "model": "p3s-2026.07", "underwrite": "p3s-2026.07", "serving": "p5s8-2026.07" }, "matches": [ { "listing_id": "rc123", "address": "…", "zip": "60637", "price": 315000, "url": "…", "score_snapshot": { "brrrr": 88, "flip": 61, "hold": 54, "arv": 402000, "arv_method": "renovated_comps", "arv_confidence": 71 } } ]}Verifying the signature
Section titled “Verifying the signature”Each POST carries two headers:
X-PM-Timestamp— unix seconds when the request was signed.X-PM-Signature—sha256=<hex>where<hex> = HMAC-SHA256(secret, timestamp + "." + rawBody).
Verify with a constant-time compare over the raw request body:
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyPlantedWebhook(req, rawBody, secret) { const ts = req.headers["x-pm-timestamp"]; const sig = (req.headers["x-pm-signature"] || "").replace(/^sha256=/, ""); // replay protection: reject anything older/newer than 5 minutes if (!ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false; const expected = createHmac("sha256", secret).update(ts + "." + rawBody).digest("hex"); const a = Buffer.from(sig, "hex"), b = Buffer.from(expected, "hex"); return a.length === b.length && timingSafeEqual(a, b);}Delivery semantics
Section titled “Delivery semantics”- Timeout 5s per attempt. Retries ×3 with exponential backoff (200ms, 400ms, 800ms) inline.
- After 3 failures the alert is dead-lettered (
dead_lettercolumn holds the last diagnostic); it is not silently dropped. - Replay protection is your responsibility: reject timestamps outside ±5 minutes and treat
(listing_id, price)as idempotent.
Scale note
Section titled “Scale note”Delivery runs inline in the Worker’s scheduled run today (Workers + KV + D1, no Durable Objects binding required). The durable upgrade path is Cloudflare Queues for ret/backoff fan-out — the payload and signature contract above do not change when that lands.
