Oracle07

Re-screening & webhooks

A grade is only as good as the day it was issued, so CompliR keeps re-grading. Watched addresses follow the cadence the analyst chose, USDC blocklist events trigger an immediate re-screen, and every tier change reaches your webhook with the full signed verdict.

Cadence windows

The analyst proposes nextRescreenInSeconds; code clamps it into the window for the tier. After a tier change the ceiling halves for the next cycle. The fallback (no model) always picks the ceiling.

TierMinMaxAfter a tier change
HIGH5 min1 hmax 30 min
MEDIUM15 min24 hmax 12 h
LOW1 h7 dmax 3.5 d

Source: RESCREEN_WINDOW_SECONDS in src/lib/analyst/guards.ts.

Watch an address

http
POST /api/v1/watch                         # 0.05 USDC / 30 days via x402 · settled after the handler succeeds
{ "address": "0x…", "webhookUrl": "https://treasury.example/complir", "clientId": "treasury-7", "payeeBook": [ … ] }

→ 200  X-CompliR-Watch-Id: wat_…
{ "id": "wat_…", "address": "0x…", "webhookUrl": "…", "clientId": "treasury-7",
  "lastTier": null, "nextRescreenAt": 1760003600, "expiresAt": 1762592000, "createdAt": 1760000000 }
  • 30 days per purchase. Re-registering the same address and client upserts the watch and extends it.
  • Payee book travels with the watch, so lookalike and payee-change checks keep running on re-screens.
  • Charged only after success: the handler runs first, then x402 settles; a failed registration is never charged.
  • Schedule. The watcher runs every 10 minutes as a Vercel cron (/api/internal/watcher, bearer CRON_SECRET) or every 60 s as worker/watcher.ts. A due watch is re-screened with kind: RESCREEN; a failed re-screen is retried in 10 minutes and keeps the last tier.

The tier.changed webhook

http
POST <your webhookUrl>
content-type: application/json
x-complir-event: tier.changed              # 8 s timeout; delivery is best-effort, the ledger entry is not

{
  "type": "tier.changed",
  "address": "0x…",
  "from": "LOW",                           # null on the first grade of a watch
  "to": "MEDIUM",
  "verdict": { "score": 37, "tier": "MEDIUM", "recommendedLimits": { … }, "reasons": [ … ], "rationale": "…",
               "eddRequested": [ … ], "nextRescreenAt": "…", "attestation": { … }, "decisionId": "dec_…", "screenedAt": "…" },
  "registryTx": null                       # tx hash once the registry write lands
}

Before the POST, a TIER_CHANGE entry is appended to the ledger with from, to, decisionId, watchId and the trigger. Verify verdict.attestation exactly as you would a screen response; the evidence bundle is omitted from the webhook body and available at GET /api/v1/decision/:decisionId.

Blocklist-event reactor

Arc's USDC is Circle's FiatToken: Blacklisted(address) and UnBlacklisted(address) logs surface at the proxy 0x3600…0000 (the reactor also subscribes to Blocklisted/UnBlocklisted in case a future upgrade emits those). A tick polls new blocks and, for every affected address that is watched, on the Trust Map, or screened by anyone in the last 30 days (EVENTS_SCREENED_DAYS), runs a real re-screen with clientId: "events" and pushes tier.changed to every watch on it. Everything else is logged as ignored.

Schedule
Vercel cron every 5 min (/api/internal/events) · worker/events.ts every 20 s (EVENTS_TICK_MS)
Budget
EVENTS_MAX_BLOCKS=500 per tick · EVENTS_RPC_BUDGET=12 calls · cursor only advances through contiguously fetched blocks
Lag
a lag above EVENTS_MAX_LAG=5000 blocks skips ahead: this is a live reactor, not an indexer; the cadence watcher is the baseline
Visibility
GET /api/v1/network/events lists the last events seen, whether a re-screen ran, tierFrom → tierTo, webhooks delivered, latency

Simulate a blocklist event

sh
# Blacklist drill: run the exact reactor handler as if USDC had just emitted Blacklisted(addr).
# The subject is a real sanctioned wallet (Ronin Bridge exploiter, OFAC SDN + JP MOF) that USDC on Arc really blacklists,
# so the re-screen reads live chain facts - no overrides, no synthetic evidence.
curl -s -X POST http://localhost:3000/api/internal/events -H "Authorization: Bearer $CRON_SECRET" \
  -H 'content-type: application/json' -d '{ "simulate": "0x098b716b8aaf21512996dc57eb0615e2383e2f96" }'
# The reverse event only exercises the UnBlacklisted path: the re-screen still reads the chain, so the tier
# stays HIGH until Circle really un-blacklists the address.
curl -s -X POST … -d '{ "simulate": "0x098b716b8aaf21512996dc57eb0615e2383e2f96", "kind": "UNBLOCKLISTED" }'

The drill runs the identical handler and is marked simulated: true in the event log; only the event is simulated. The re-screen reads the chain and the sanctions lists, so the verdict is the real one for that address. Trust Map re-grades are a third loop: recipients of 25 USDC or more are re-graded every 24 hours by the tailer. See the trust model for what these loops cannot see.