Skip to content

chore: sync upstream main - #413

Merged
CarsonRoscoe merged 580 commits into
mainfrom
chore/sync-upstream-main
Oct 15, 2026
Merged

CarsonRoscoe merged 580 commits into
mainfrom
chore/sync-upstream-main

Conversation

@CarsonRoscoe

Copy link
Copy Markdown

Resync with upstream

rascal-3 and others added 30 commits July 24, 2026 11:56
…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.
…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
…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>
epistemedeus and others added 8 commits October 13, 2026 13:44
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.
@cb-heimdall

cb-heimdall commented Oct 15, 2026 •

Copy link
Copy Markdown

✅ Heimdall Review Status

Requirement Status More Info
Reviews ✅ 1/1
Denominator calculation
Show calculation
1 if user is bot 0
1 if user is external 0
2 if repo is sensitive 0
From .codeflow.yml 1
Additional review requirements
Show calculation
Max 0
0
From CODEOWNERS 0
Global minimum 0
Max 1
1
1 if commit is unverified 0
Sum 1

*/
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",
});
}
});
Comment thread e2e/scripts/swig-setup.ts
): Promise<ResolvedSwigAccount> {
const fromEnv = process.env.SWIG_ACCOUNT_ADDRESS;
if (fromEnv) {
console.log(`ℹ️ Using existing Swig account ${fromEnv}`);
Comment thread e2e/scripts/swig-setup.ts
} 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];
Comment thread go/client.go
// 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))
Comment thread go/client.go
// 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),
Comment thread examples/go/facilitator/auth-capture/main.go Dismissed
@CarsonRoscoe CarsonRoscoe changed the title Chore/sync upstream main chore: sync upstream main 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));
@cb-heimdall

Copy link
Copy Markdown

Review Error for ethanoroshiba @ 2026-10-15 17:19:31 UTC
User failed mfa authentication, see go/mfa-help

@CarsonRoscoe
CarsonRoscoe merged commit 0e6e8c5 into main Oct 15, 2026
25 of 27 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Development

Successfully merging this pull request may close these issues.