@wzrd_sol/facilitator-gate
v0.1.1
Published
Facilitator-side x402 trust gate. Drop the free TWZRD settle decision into a facilitator's onBeforeSettle (or faremeter handleSettle) so wash-flagged / block-rated sellers are refused at settle time. Fail-open.
Maintainers
Readme
@wzrd_sol/facilitator-gate
Facilitator-side x402 trust gate. Drop the free TWZRD settle decision into a facilitator's settle path so a wash-flagged / block-rated seller's settlement is refused at the moment money would move.
Why this layer
The TWZRD trust signal is most valuable at exactly one instant: the moment before a settlement executes. Buyer-side gates (@wzrd_sol/plugin-trustgate) are advisory and skippable. A receipt is issued after settlement. This package puts the decision in the settle path, facilitator-side, where it is load-bearing — return { abort: true, reason } and the settlement does not happen.
It is the TypeScript counterpart to the Python on_before_settle.py reference in twzrd-agent-intel, adapted to the verified @x402/core facilitator hook contract.
Zero runtime dependencies
The @x402/core and faremeter types are structurally mirrored, not imported — so this drops into any facilitator without pinning their SDK version. The only network call is the free preflight POST /v1/intel/preflight.
Install
npm install @wzrd_sol/facilitator-gateUsage
Canonical @x402/core facilitator (Daydreams + any spec-compliant facilitator)
The verified @x402/core contract: onBeforeSettle(ctx) => Promise<void | { abort: true, reason }>. Register on the instance (the createFacilitator({ hooks }) wrapper narrows the type and won't accept the abort channel — register directly):
import { createFacilitator } from "@daydreamsai/facilitator";
import { makeOnBeforeSettle } from "@wzrd_sol/facilitator-gate";
const facilitator = createFacilitator({ /* ...your config... */ });
// Register the gate directly on the instance (full typed abort channel):
facilitator.onBeforeSettle(
makeOnBeforeSettle({
failClosed: false, // default: a gate outage proceeds (never brick settles)
// shadow: true, // observe-only: report via onVerdict, never abort (see ROLLOUT.md)
onVerdict: (v, ctx) =>
console.log(
`[twzrd] ${v.sellerWallet} -> ${v.decision} (trust ${v.trustScore}); ` +
`block=${v.block} resource=${ctx.requirements.resource ?? ""}`,
),
}),
);
// A wash-flagged seller now returns { abort: true, reason } -> settle refused.Run it for real:
examples/quickstart/wires this hook into a realonBeforeSettleflow and drives a real on-chain x402 settlement. Rolling out? Start in shadow mode - seeROLLOUT.md.
faremeter (FacilitatorHandler.handleSettle)
faremeter has no lifecycle hooks; the seam is the handler. Wrap handleSettle:
import { wrapHandleSettle } from "@wzrd_sol/facilitator-gate";
// `baseHandler` is your existing faremeter FacilitatorHandler.
const handler = {
...baseHandler,
handleSettle: wrapHandleSettle(baseHandler.handleSettle.bind(baseHandler), {
failClosed: false, // gate outage => delegate to inner (never brick)
// shadow: true, // observe-only: report, always delegate (see ROLLOUT.md)
queriedPubkey: process.env.WZRD_CONSUMER_PUBKEY, // payer pubkey for attribution
onVerdict: (v, requirements) =>
console.log(
`[twzrd] ${v.sellerWallet} -> ${v.decision} (trust ${v.trustScore}); ` +
`block=${v.block} payTo=${requirements.payTo ?? ""}`,
),
}),
};
// On block: returns x402SettleResponse { success: false, errorReason: "trust_gate_blocked" }
// and never calls the inner handler.Any other settle path (e.g. OpenFacilitator)
OpenFacilitator exposes no facilitator hook — wrap the /settle controller and use the raw decision:
import { evaluateSettleGate } from "@wzrd_sol/facilitator-gate";
// in your POST /settle handler, before calling facilitator.settle(...):
const verdict = await evaluateSettleGate(
{
sellerWallet: requirements.payTo, // the x402 recipient
priceUsdc: Number(requirements.maxAmountRequired) || undefined,
resourceName: requirements.resource,
},
{
failClosed: false, // outage => proceed (never brick)
queriedPubkey: process.env.WZRD_CONSUMER_PUBKEY, // payer pubkey for attribution
},
);
console.log(
`[twzrd] ${verdict.sellerWallet} -> ${verdict.decision} ` +
`(trust ${verdict.trustScore}); block=${verdict.block} available=${verdict.gateAvailable}`,
);
if (verdict.block) {
return res.status(402).json({ error: "trust_gate_blocked", reason: verdict.reason });
}
// else proceed to settle(...)Facilitator → seam → abort mechanism
Verified June 2026 against source (see "Sources").
| Facilitator | Pre-settle seam | This package | Abort mechanism |
|---|---|---|---|
| Daydreams / any @x402/core v2+ | onBeforeSettle hook (canonical) | makeOnBeforeSettle | return { abort: true, reason } |
| faremeter @0.22.0 | FacilitatorHandler.handleSettle (pluggable handler, no hook) | wrapHandleSettle | return SettleResponse { success: false } |
| OpenFacilitator @1.0.0 | none — wrap /settle controller | evaluateSettleGate | return HTTP 402, don't call settle() |
| PayAI hosted | none (closed) | n/a — not injectable by a third party | — |
Decision semantics (matched to on_before_settle.py)
- Block when the preflight returns
decision === "block"(ortrust_score < minScoreif you set one). - Fail-open by default: a gate outage (timeout, non-2xx, unparseable body, network error) → proceed with
gateAvailable: false. A trust-service hiccup never halts an adopting facilitator's settlements. SetfailClosed: trueto block on any outage. - Cloudflare UA gotcha: the prod edge returns 403 to the default fetch/undici User-Agent. This package sends
twzrd-facilitator-gate/0.1so the POST reaches the service — without it, every call falsely reads as "unavailable" and (fail-open) silently proceeds. Override withuserAgent. - Shadow mode (
shadow: true): evaluate and report viaonVerdict, but never abort. Run observe-only before enforcing.
Config
| Option | Default | Meaning |
|---|---|---|
| intelBase | https://intel.twzrd.xyz | Preflight host |
| failClosed | false | Block on a gate outage (strict) |
| minScore | 0 | Also block below this score (sharp edge: unknown sellers score ~45) |
| timeoutMs | 5000 | Preflight timeout |
| userAgent | twzrd-facilitator-gate/0.1 | UA sent to the preflight |
| queriedPubkey | — | Consumer/payer pubkey for funnel attribution |
| shadow | false | Observe-only (hook/wrapper opts) |
| onVerdict | — | Per-evaluation callback (metrics/shadow) |
Status & honest scope
- Tested offline (19 tests, mock fetch): decision logic, both adapters, fail-open/closed, the UA header, shadow mode.
- Hook signature is verified against
@x402/[email protected]'s published.d.ts— but the abort has not been exercised against a live running facilitator. Run a facilitator in shadow mode first, confirm the verdicts, then enforce. - No live facilitator adoption yet. This package existing is the precondition for a facilitator operator to turn on screened settlement — it does not by itself produce a blocked settlement in the wild.
- The runnable
examples/quickstart/demo shows an ALLOW, by design - not a block. Its default seller is wash-shaped (captive 91.4%) but not wash-proven: the advisory preflight this package calls blocks it on shape, while the server-side enforcement settle gate correctly allows it (no circular-flow wash). The two layers are meant to disagree on this seller. To observe a settle-path block you would need a circular-flow seller; the only known one in the corpus is the controlled FTNMk rig. The demo does not fake a block.
Ingest the corpus
The gate's decisions are backed by the only live cross-facilitator x402 payer corpus on Solana. A facilitator running this gate can also pull and ingest the corpus as a feed (no auth, paginated, wash-discounted):
curl -s 'https://intel.twzrd.xyz/v1/intel/corpus_feed?limit=5' | jq .Browser demo: https://twzrd.xyz/demo/ingest/ · schema: https://intel.twzrd.xyz/docs · ingestion pilots: https://twzrd.xyz/grants (email [email protected]).
Adopt
- Run it for real:
examples/quickstart/- wiresmakeOnBeforeSettleinto a realonBeforeSettleflow and drives a real on-chain x402 settlement (guarded behindWZRD_I_UNDERSTAND_SPEND=1). - Roll it out safely:
ROLLOUT.md- the shadow -> enforce -> (optional) fail-closed procedure, with the exact config diff at each step.
Develop
npm install
npm test # offline (mock fetch, no chain/facilitator deps)
npm run typecheck
npm run buildSources
@x402/coreonBeforeSettlecontract:@x402/[email protected]/dist/cjs/facilitator/index.d.ts; specx402-foundation/x402docs/advanced-concepts/lifecycle-hooks.mdx- Daydreams wrapper:
daydreamsai/facilitatorpackages/core/src/factory.ts - faremeter handler:
faremeter/faremeterpackages/types/src/facilitator.ts - TWZRD reference:
twzrd-agent-intelverifier/on_before_settle.py,docs/proposals/openfacilitator-onbeforesettle.md
License
MIT
