feat(core): add FacilitatorExtension.enrichSettleResponse hook#2339
feat(core): add FacilitatorExtension.enrichSettleResponse hook#2339javierpmateos wants to merge 1 commit into
Conversation
|
@javierpmateos is attempting to deploy a commit to the Coinbase Team on Vercel. A member of the Team first needs to authorize it. |
26ff814 to
98b14a6
Compare
|
@javierpmateos do you have an extension spec to add to the PR? It would go in 'specs/extensions` I would think. In my view this PR could cover two paths:
Given #1802 it looks like you're targeting #2 but it should be clear in a spec. |
98b14a6 to
9a2ba01
Compare
|
Thanks @alftom ... good call. I've added the extension spec at To clarify: this targets path #2 — a self-contained payload signed by the facilitator, relayed by the resource server to the client via a separate The two extensions compose but stay independent: offer-receipt proves delivery (server-signed), facilitator-attestation proves settlement (facilitator-signed). Different signer, different claim, different privacy surface — as @jithinraj recommended in #1802. |
feat(core): add FacilitatorExtension hooks for facilitator-side extension data
Summary
Refs #1802. Adds
enrichSettleResponseto theFacilitatorExtensioninterface and wires itinto
x402Facilitator.settle()so that facilitator-registered extensions can inject signed orderived data into
SettleResponse.extensionsbefore the response is returned to the resourceserver.
Background
The Gap
Today's x402 extension flow covers the resource server side:
ResourceServerExtension.enrichPaymentRequiredResponseÔÇö injects data into 402 responsesResourceServerExtension.enrichSettlementResponseÔÇö injects data into the final settlement responseBut when the resource server calls the facilitator's
/settleendpoint, the facilitator has nostandardized way to populate
SettleResponse.extensions. The field exists in both the TypeScriptand Go SDKs (see PR #1808 which added it to Go), but there are no hooks to fill it on the
facilitator side.
This was identified by @alftom in issue #1802:
This PR is the infrastructure fix. The first consumer will be the
facilitator-attestationextension (separate PR), which uses this hook to sign and return EIP-712 attestations.
The Flow (before and after)
Before:
After:
Relationship to offer-receipt (PR #935)
PR #935 added
ResourceServerExtension.enrichSettlementResponsefor the resource server boundary.This PR adds the symmetric hook at the facilitator boundary, completing the extension pipeline:
Relationship to Go SDK parity (PR #1808)
PR #1808 added the
Extensionsfield to Go'sSettleResponse. This PR adds the hook mechanismthat populates that field on the TypeScript facilitator side.
Changes
packages/core/src/types/extensions.tsFacilitatorSettleResultContextinterface (exported) ÔÇö the context passed to the hook,containing
paymentPayload,requirements, andresult: SettleResponse.enrichSettleResponse?: (context: FacilitatorSettleResultContext) => Promise<unknown>to
FacilitatorExtension.packages/core/src/facilitator/x402Facilitator.tssettle(): afterafterSettleHookscomplete, iterate registered extensions and callenrichSettleResponsefor each. Extension data is merged intosettleResult.extensions[key].try/catch; errors are logged withconsole.errorand settlement continues ÔÇö a failing extension MUST NOT break the payment.packages/core/test/unit/facilitator/x402Facilitator.extensions.test.ts(new)12 new unit tests covering the contract described in the Tests section below. All 468 existing
@x402/coretests continue to pass.Design Decisions
No
declarationparameter (unlikeResourceServerExtension)ResourceServerExtension.enrichSettlementResponse(declaration, context)takes adeclarationbecause the resource server has per-route extension declarations. Facilitator extensions are
global (registered once on the facilitator instance) so there is no per-request declaration.
The hook takes only
context.FacilitatorSettleResultContextdefined inextensions.tsThe context type is defined alongside the interface that uses it (in
types/extensions.ts),following the existing precedent where
SettleResultContextis defined inx402ResourceServer.tsand re-exported through
extensions.ts. The localFacilitatorSettleResultContextinx402Facilitator.ts(used for lifecycle hooks) is structurally compatible and remains unchanged.Hook placement: after
afterSettleHooks, beforereturnExtension enrichment runs after all lifecycle hooks complete. This ensures the lifecycle hook
pipeline (e.g., audit logging in
onAfterSettle) sees the un-enriched response, while theenriched response (with signed extension data) is what gets returned to the caller.
enrichSettleResponseis NOT called when settlement throwsIf the scheme facilitator throws (e.g., on-chain tx failure), control jumps to the
catchblockand extension hooks are skipped. Extensions only run on successful settlements.
enrichSettleResponseIS called when scheme returnssuccess: falseIf the scheme returns a
SettleResponsewithsuccess: falsewithout throwing, the hook runs.This is consistent with how
afterSettleHooksbehave. Extension implementations that only wantto run on successful settlements should guard with
if (!ctx.result.success) return undefined;.Tests
registerExtensionstores extension by keyenrichSettleResponsecalled with correct context on successenrichSettleResponsereturn value appears inSettleResponse.extensions[key]extensionsenrichSettleResponseextensionsunchangedenrichSettleResponsenot calledextensionsfield survives JSON round-triptxHashandnetworkfrom settlement resultundefineddoes not create emptyextensions[key]success: falseresult: hook called, extension can inspect and skipChecklist
FacilitatorExtension.enrichSettleResponsehookFacilitatorSettleResultContextcontext type (exported)x402Facilitator.settle()after successful settlementextensionsfield preserved through Zod parse (existingsettleResponseSchemaalready handles this)@x402/coretests pass (468 total, 0 regressions)@x402/corebuilds cleanly (TypeScript + DTS)registerExtensionunaffectedoffer-receiptextensionfacilitator-attestationextension (separate PR)What This Enables
Once this PR merges, a facilitator can produce signed attestations like:
The resource server receives the enriched
SettleResponse(viaHTTPFacilitatorClient.settle),and the
extensions["facilitator-attestation"]data flows through to the client via the existingPAYMENT-RESPONSEheader.