Repository navigation
chore: sync upstream main - #413
Merged
Merged
Conversation
…xample (x402-foundation#2876) Adds service_name / tags to both RouteConfigs (parity with the TypeScript advanced bazaar example) and documents the resource-block service metadata and its indexer caps in the README. Without these fields a discovered service is catalogued under its raw domain with no name, tags, or icon.
…ry) (x402-foundation#2941) * docs: add NEAR x402 Facilitator to the facilitators table * site: add NEAR x402 Facilitator ecosystem partner entry * listing: drop ecosystem-page entry (page deprecated per maintainer)
The repo moved from coinbase/x402 to x402-foundation/x402; the console guide's clone command still pointed at the old location.
* add facilitator faq * minor fixes
…02-foundation#2957) HTTPFacilitatorClient's `createAuthHeaders` callback must return an object keyed by facilitator path (`verify`/`settle`/`supported`). Returning a flat headers object silently indexed to `undefined` and dropped auth on every request, surfacing as an unexplained 401. Throw a clear error on the wrong shape and document the expected shape. Fixes x402-foundation#2762
…x402-foundation#2937) * align ts/go * add py * add changeset
…it rejection (x402-foundation#2974) * fix: add request timeouts to HTTPFacilitatorClient and guard eager init rejection verify(), settle(), and getSupported() previously called fetch() with no deadline, so a facilitator that accepted a connection but never completed the response hung the operation indefinitely — and, through the eagerly created initPromise shared by the Hono/Express/Fastify/Next middlewares, wedged every protected route until restart. - Add FacilitatorConfig.timeoutMs (default 30s, matching the Go and Python facilitator clients), applied via a fresh AbortSignal.timeout() per HTTP attempt, covering response headers and body consumption. - Reject with a typed FacilitatorTimeoutError (a FacilitatorResponseError subclass the middlewares already surface as 502) instead of a raw DOMException; settle() timeouts are documented as indeterminate. - Attach a rejection handler to the eager initPromise in all four middlewares so an early init failure cannot become an unhandled rejection; the first protected request still observes it and retries. Closes x402-foundation#2961 * fix: surface deadline aborts from error-body reads and validate timer-safe timeoutMs Review follow-ups on x402-foundation#2974: - getSupported() swallowed a deadline abort raised while reading a non-2xx error body, reporting it as a generic HTTP failure — which middleware surfaces as a 500 instead of the typed 502, and which the 429 path would even retry after the deadline had fired. Abort/timeout rejections from response.text() are now rethrown so the FacilitatorTimeoutError conversion applies; statusText remains the fallback for other body-read failures. - timeoutMs now requires a positive safe integer of at most 2^31 - 1: AbortSignal.timeout() throws ERR_OUT_OF_RANGE on non-integers at request time, and larger values overflow Node's 32-bit timers into an effective ~1ms deadline while reporting the configured duration.
* fix enrichPaymentPayload * same for go * same for py
…dation#2956) * fix(go/http): add Cache-Control: no-store to 402 responses * add cache-control policy for payment HTTP responses --------- Co-authored-by: Philippe d'Argent <pdargent@icloud.com>
…2-foundation#2990) * fix(ts/py): add cache control, no-store for 402, private for 200 * add changeset
…foundation#3024) Three inaccuracies that mislead anyone following the add-a-default-asset runbook verbatim. 1. The Python snippet used NetworkConfig(...)/AssetInfo(...) constructor syntax with AssetTransferMethod.PERMIT2. AssetTransferMethod does not exist anywhere in the Python package, so uncommenting that line raises NameError; the real value is the literal string "permit2". Every one of the ~30 entries in NETWORK_CONFIGS is a plain dict literal with snake_case string keys. Rewritten to match. 2. The Go snippet used ChainID: big.NewInt(YOUR_CHAIN_ID) while every real entry uses a named constant from the ChainID* var block, so the doc silently omitted a required step. Now shows the constant declaration. 3. Six comments referenced DEFAULT_ASSET.md; the file is DEFAULT_ASSETS.md. The TypeScript one also placed it in a nonexistent exact/server/ directory; it lives at the repository root. Comment- and markdown-only: no behaviour change, so no changelog fragments per CONTRIBUTING.md. Co-authored-by: Claude <noreply@anthropic.com>
…lt stablecoin (x402-foundation#3025) Celo had no entry in the default-asset registries, so a resource server on Celo could not write price: "$0.01" — dollar-string pricing resolves through the per-chain default-stablecoin map and had to fall back to an explicit TokenAmount with amountInAtomicUnits. Registry-only change: no new logic, no new files outside changelog fragments. Network stays an open-ended CAIP-2 string and EVM schemes register on the wildcard eip155:*, so nothing gated Celo other than the missing map entries. Default asset (verified on-chain via forno.celo.org and forno.celo-sepolia.celo-testnet.org): Celo Mainnet eip155:42220 USDC 0xcebA9300f2b948710d2653dD7B07f33A8B32118C Celo Sepolia eip155:11142220 USDC 0x01C5C0122039549AD1493B8220cABEdD739BC44E Both: EIP-712 domain name "USDC", version "2", 6 decimals, EIP-3009 transferWithAuthorization present (authorizationState returns cleanly). Each domain separator was recomputed from the committed name/version/chainId/address and byte-matched against the contract's own DOMAIN_SEPARATOR(): 42220 0xb2ce31d2838445fa765a491f550e7c78ac7280ab0f3bc9d6063a86df9c3fb578 11142220 0x23f491197bb8c5ea4fe8dd4c2293b600f073553f9814015e7a5eb57724df9578 Asset selection rationale (DEFAULT_ASSETS.md section 4): - USDC over USDT. Both are EIP-3009-capable 6-decimal tokens on Celo, but USDT's version() reverts on-chain, so its EIP-712 version cannot be read the way this table's contract assumes. USDT (0x48065fbBE25f71C9282ddf5e1cD6D6A887483D5e) remains fully usable via an explicit TokenAmount. - cUSD / USDm (0x765DE816845861e75A25fCA122bb6898B8B1282a) is not viable: Mento StableTokenV2, 18 decimals, EIP-2612 only, authorizationState reverts. It would force the Permit2 path. - Alfajores is excluded — its RPC is unreachable and Circle no longer lists a USDC there. Celo Sepolia is the live testnet. Also adds the v1 legacy network name "celo" so that getEvmChainIdV1("celo") resolves to 42220 instead of throwing. The production Celo facilitator advertises {"x402Version": 1, "scheme": "exact", "network": "celo"} in its /supported response, which no SDK could previously resolve. Mainnet only; the v1 name space has no Celo testnet name. Decimals are 6 on both networks, so evm/gen/decimals.ts needs no regeneration. Co-authored-by: Claude <noreply@anthropic.com>
…ion#3026) Generated-By: mintlify-agent Mintlify-Source: dashboard-editor Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
Returning a response after payment reconciliation fails is misleading, while dropping it without closing its body leaks resources.
…undation#3027) * don't clobber client builder codes * address review * fixes * scope only to builder codes * caps on both sides * fix go lint
…#3031) Registers Flare mainnet (eip155:14) with USD0 as the default stablecoin across the TypeScript, Go and Python SDKs, plus the v1 legacy network maps and the support-table docs. Follows DEFAULT_ASSETS.md; no paywall regeneration needed (6 decimals). EIP-712 domain parameters were verified by RECOMPUTATION rather than read and trusted: hashDomain({name, version: "1", chainId: 14, verifyingContract}) computed from the values as written in each of the three SDK files equals the token's on-chain DOMAIN_SEPARATOR 0x4c53d754dcb2f406ecf83695856d8c10b13296a318d9a481043412bf253d43ae. EIP-3009 support confirmed on-chain: transferWithAuthorization with a junk signature reverts "TetherToken: invalid signature" (the function exists and validates), and authorizationState(address,bytes32) returns cleanly.
…402-foundation#3033) The recovery retry previously created a fresh payment payload from a server-controlled PaymentRequired without re-running abort/approval hooks, so a corrective 402 could escalate amount/asset/payTo unnoticed.
* fix: match dotAll for line terminators in wildcard regex route * chore: add changeset versions * fix: format * fix: format/lint * fix: clean up narrative comments
…oundation#3032) * fix(python): verify Transfer event after exact/eip3009 settle Parity with Go x402-foundation#2727 and open TS x402-foundation#2385: do not treat receipt.status alone as proof the expected ERC-20 Transfer occurred. * style(python): ruff format eip3009 transfer-event verify Address maintainer review on x402-foundation#3032 (lint-python / formatting). * style(python): address review on transfer-event settle comments Shorten facilitator comment; drop TransactionReceipt field commentary per maintainer feedback on x402-foundation#3032.
…402-foundation#3034) * fix(mcp): plumb spend policies through createx402MCPClient factory The documented factory previously built a bare x402Client and dropped policies/selector. Route through fromConfig so integrators can bound spend. * docs(mcp): drop old-implementation comments in factory helper Document current fromConfig/policies behaviour only, per x402-foundation#3034 review.
Generated-By: mintlify-agent Mintlify-Source: dashboard-editor Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
…x402-foundation#2385) * fix(evm): verify Transfer event in receipt after exact/eip3009 settle settleEIP3009 currently treats receipt.status === "success" as proof of a successful transfer. The receipt's status only tells us the tx did not revert; it does not tell us that the expected ERC-20 Transfer was emitted from the expected token contract with the expected (from, to, value). The EVM batch-settlement facilitator and the Stellar exact facilitator already perform this kind of receipt-shape check (parseEventLogs over receipt.logs, filtered by token address, matched against the expected event arguments). This PR brings exact/eip3009 in line with that pattern. Behavior: a tx whose status is success but whose receipt logs do not contain a matching ERC-20 Transfer is now reported as a settlement failure with the new ErrTransferEventMismatch error code (wire string invalid_exact_evm_transfer_event_mismatch) instead of being reported as a successful settle. The happy path is unchanged. * address review and align Transfer check with go/py --------- Co-authored-by: Philippe d'Argent <pdargent@icloud.com>
…tion#3039) * fix: reject external / in bazaar discovery schema (SSRF) * chore: clean up spec wording * fix: format/lint
Generated-By: mintlify-agent Mintlify-Source: dashboard-editor Co-authored-by: mintlify[bot] <109931778+mintlify[bot]@users.noreply.github.com>
Select the supported interpreter for test and release preparation. Keep customer metadata, lock and library source unchanged. Signed-off-by: epistemedeus <epistemedeus@gmail.com> Co-authored-by: Cursor Agent <cursoragent@cursor.com>
…n#3789) These files predate the x402 Foundation handover and are no longer maintained. Co-authored-by: phdargen <29732335+phdargen@users.noreply.github.com>
…402-foundation#3739) Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…402-foundation#3697) * fix(svm): fail batch verify closed when afterVerify store calls fail persistSnapshot and the client-voucher baseline read ran outside afterVerify's try. Core logs a throwing afterVerify hook and returns the facilitator's valid result, so a store failure there let the request through with no reservation and no operation record: the handler ran, the commit then failed with "missing reservation", and the request id was never fenced. Move both calls inside the try so they abort verify, and make the operation release in the catch best-effort so its failure cannot turn the abort back into a pass. * fix(svm): catch synchronous operation release failures in afterVerify A custom BatchOperationStore can throw from release() before returning a promise, which .catch() never sees: the hook threw and core passed verify without a reservation. Await the release inside try/catch, and assert in the test that release is attempted for both a rejected and a thrown failure.
Keep the original null body after settlement instead of constructing an invalid Response with an empty Buffer. Add wrapper regressions and a patch changeset. Closes x402-foundation#3732.
(cherry picked from commit 0e07cd4)
✅ Heimdall Review Status
|
| */ | ||
| private normalizeDecodedPath(path: string): string { | ||
| const pathWithoutQuery = path.split(/[?#]/)[0]; | ||
| return pathWithoutQuery.replace(/\/+/g, "/").replace(/(.+?)\/+$/, "$1") || "/"; |
| }) | ||
| .join("/"); | ||
|
|
||
| return normalized.replace(/\/+/g, "/").replace(/(.+?)\/+$/, "$1"); |
Comment on lines
+80
to
+97
| app.get("/admin/payments", async (_req, res) => { | ||
| try { | ||
| const payments = await lifecycle.listAuthorizedPayments(); | ||
| res.json( | ||
| payments.map(payment => ({ | ||
| paymentInfoHash: payment.paymentInfoHash, | ||
| capturableAmount: payment.capturableAmount, | ||
| refundableAmount: payment.refundableAmount, | ||
| collectTransaction: payment.collectTransaction, | ||
| createdAt: payment.createdAt, | ||
| })), | ||
| ); | ||
| } catch (error) { | ||
| res.status(500).json({ | ||
| error: error instanceof Error ? error.message : "Unknown error", | ||
| }); | ||
| } | ||
| }); |
Comment on lines
+194
to
+218
| app.post("/verify", async (req, res) => { | ||
| try { | ||
| const { paymentPayload, paymentRequirements } = req.body as { | ||
| paymentPayload: PaymentPayload; | ||
| paymentRequirements: PaymentRequirements; | ||
| }; | ||
|
|
||
| if (!paymentPayload || !paymentRequirements) { | ||
| return res.status(400).json({ | ||
| error: "Missing paymentPayload or paymentRequirements", | ||
| }); | ||
| } | ||
|
|
||
| const response: VerifyResponse = await facilitator.verify( | ||
| paymentPayload, | ||
| paymentRequirements, | ||
| ); | ||
| res.json(response); | ||
| } catch (error) { | ||
| console.error("Verify error:", error); | ||
| res.status(500).json({ | ||
| error: error instanceof Error ? error.message : "Unknown error", | ||
| }); | ||
| } | ||
| }); |
Comment on lines
+108
to
+133
| app.post("/verify", async (req, res) => { | ||
| try { | ||
| const { paymentPayload, paymentRequirements } = req.body as { | ||
| paymentPayload: PaymentPayload; | ||
| paymentRequirements: PaymentRequirements; | ||
| }; | ||
|
|
||
| if (!paymentPayload || !paymentRequirements) { | ||
| return res.status(400).json({ | ||
| error: "Missing paymentPayload or paymentRequirements", | ||
| }); | ||
| } | ||
|
|
||
| const response: VerifyResponse = await facilitator.verify( | ||
| paymentPayload, | ||
| paymentRequirements, | ||
| ); | ||
|
|
||
| res.json(response); | ||
| } catch (error) { | ||
| console.error("Verify error:", error); | ||
| res.status(500).json({ | ||
| error: error instanceof Error ? error.message : "Unknown error", | ||
| }); | ||
| } | ||
| }); |
| ): Promise<ResolvedSwigAccount> { | ||
| const fromEnv = process.env.SWIG_ACCOUNT_ADDRESS; | ||
| if (fromEnv) { | ||
| console.log(`ℹ️ Using existing Swig account ${fromEnv}`); |
| } else { | ||
| console.log("\n✅ Using existing Swig smart wallet:"); | ||
| } | ||
| console.log(` SWIG_ACCOUNT_ADDRESS=${resolved.address}`); |
| } | ||
|
|
||
| function mdCell(value: string): string { | ||
| return value.replace(/\|/g, "\\|").replace(/\r?\n/g, " "); |
Comment on lines
+46
to
+55
| paymentMiddleware( | ||
| payToSvm, | ||
| { | ||
| "GET /protected-svm": { | ||
| price: "$0.001", | ||
| network: svmNetwork, | ||
| }, | ||
| }, | ||
| facilitatorConfig | ||
| ), |
Comment on lines
+33
to
+42
| paymentMiddleware( | ||
| payToEvm, | ||
| { | ||
| "GET /protected": { | ||
| price: "$0.001", | ||
| network: evmNetwork, | ||
| }, | ||
| }, | ||
| facilitatorConfig | ||
| ), |
| count, rate, elapsed | ||
| ); | ||
| } | ||
| let mut salt = [0u8; 32]; |
| // entries lead so a downstream cap on array length (e.g. builder-code's | ||
| // MAX_SERVICE_CODES) truncates excess server entries rather than the client's. | ||
| func mergeSlicesUnique(client, server []interface{}) []interface{} { | ||
| merged := make([]interface{}, 0, len(client)+len(server)) |
| // entries lead so a downstream cap on array length (e.g. builder-code's | ||
| // MAX_SERVICE_CODES) truncates excess server entries rather than the client's. | ||
| func mergeSlicesUnique(client, server []interface{}) []interface{} { | ||
| merged := make([]interface{}, 0, len(client)+len(server)) |
| return nil, err | ||
| } | ||
|
|
||
| suffix := make([]byte, 0, cborLength+2+1+len(markerBytes)) |
| payload types.PaymentPayload, | ||
| _ types.PaymentRequired, | ||
| ) (types.PaymentPayload, error) { | ||
| extensions := make(map[string]interface{}, len(payload.Extensions)+1) |
| return payload, x402.NewVerifyError(ErrUnauthenticatedAuthorizerRequest, payer, "") | ||
| } | ||
|
|
||
| signed := make(map[string]interface{}, len(payload.Payload)+2) |
| fmt.Printf(" Path: %s\n", c.Request().URL.Path) | ||
| fmt.Printf(" Method: %s\n", c.Request().Method) | ||
| fmt.Printf(" Error: %v\n", err) | ||
| fmt.Printf(" Headers: %v\n", c.Request().Header) |
| fmt.Printf(" Path: %s\n", c.Request.URL.Path) | ||
| fmt.Printf(" Method: %s\n", c.Request.Method) | ||
| fmt.Printf(" Error: %v\n", err) | ||
| fmt.Printf(" Headers: %v\n", c.Request.Header) |
| fmt.Printf(" Path: %s\n", r.URL.Path) | ||
| fmt.Printf(" Method: %s\n", r.Method) | ||
| fmt.Printf(" Error: %v\n", err) | ||
| fmt.Printf(" Headers: %v\n", r.Header) |
| // /supported advertises one of the signer's addresses per response. | ||
| CaptureAuthorizers: evmSigner.GetAddresses(), | ||
| FeeRecipient: feeRecipient, | ||
| MinFeeBps: uint16(minFeeBps), |
ethanoroshiba
approved these changes
Oct 15, 2026
| */ | ||
| export function priceToNano(price: string): bigint { | ||
| return parseDecimalAmount( | ||
| price.replace(/^\$/, "").replace(/\s*(USD|USDT)\s*$/i, ""), |
| /UNKNOWN_ACCOUNT|does ?n.t exist|doesn't exist|does not exist/i.test(errorText(e)); | ||
|
|
||
| const isAccessKeyMissing = (e: unknown): boolean => | ||
| /UNKNOWN_ACCESS_KEY|access key .* (not found|does not exist|doesn't exist)/i.test(errorText(e)); |
|
Review Error for ethanoroshiba @ 2026-10-15 17:19:31 UTC |
ethanoroshiba
approved these changes
Oct 15, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Resync with upstream