Skip to main content
The capture adapter for Circle webhook v2 notifications — Wallets transactions and Contracts event logs. Unlike an in-process adapter, this source is external: Circle POSTs signed notifications to an HTTPS endpoint, at-least-once and unordered. The package provides the pieces that physics requires — it hosts no HTTP server of its own, so the pieces embed into whatever server receives the POSTs. Zero runtime dependencies beyond @usehaia/trace-core.
Status: early. The full pipeline is implemented — verification, envelope validation, deduplication, and a default circle.* event vocabulary — and exercised end to end by replay tests. It has been run against live Arc testnet traffic, and the shipped fixtures include captured deliveries alongside synthetic ones; no capture of a failed transaction exists yet.

Install

Handle a delivery

createWebhookHandler composes the pieces and returns the HTTP decision; the surrounding server only relays it:
createFileEventWriter appends to the path it is given and does not create directories — that is createRunEventWriter’s job. Create the parent directory yourself, or the first delivery fails with ENOENT, the handler answers 500, and Circle refuses to even verify the endpoint when the subscription is created.
The decision contract: One handler serves any number of route paths: Circle requires a unique URL per subscription, but a delivery’s source is identified by notification_type, not by the URL it arrived on.

The event vocabulary

Events are recorded in the default circle.* vocabulary (see normalizeNotification). Terminal wallet transactions become circle.transaction.{inbound|outbound}.{complete|failed} — with CONFIRMED and COMPLETE folded together, since chains with instant finality can skip CONFIRMED. Monitored contract events become circle.contract.{payment_created|withdrawal|refund}. Payloads are strictly allowlisted, occurred_at is the fact’s own time from the payload rather than the delivery time (a transaction’s updateDate, a contract event’s firstConfirmDate), and context_id is the escrow contract’s address. A first verified delivery also records one session-level trace.attached attestation, so “no events” can never be mistaken for “the receiver was never wired up”. Pass your own normalize to use a different vocabulary. The pieces below are what the handler composes — usable directly if you need a different pipeline.

Verify a notification

Every v2 notification is signed (ECDSA P-256 / SHA-256) over the raw request body. Verification must run on the bytes exactly as received — re-serializing parsed JSON can change them and break a valid signature.

Validate the envelope and deduplicate

Circle delivers at-least-once (retries reuse the same notificationId) and unordered — so deduplicate before writing, and key any processing off the state inside the payload, never off arrival order.
The in-memory store is bounded and process-local. Two instances behind a load balancer, or one instance after a cold start, will each accept the same redelivered notification. A deployment that is not a single long-lived process should back the one-method DedupeStore interface with shared storage.