Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
930abdd
docs(agentic-payments): production patterns for x402 + MPP
Eras256 Aug 15, 2026
16c2d92
docs(agentic-payments): address Copilot review on #97
Eras256 Aug 16, 2026
0174883
fix(agentic-payments): move description out of accepts in the multi-r…
Eras256 Aug 28, 2026
23c7988
fix(agentic-payments): fail closed, not open, in the MPP production p…
Eras256 Aug 28, 2026
23b0968
fix(agentic-payments): sync docs to the two follow-up code fixes
Eras256 Aug 28, 2026
2bde1e9
docs(agentic-payments): validate STELLAR_RECIPIENT format in resolveR…
Eras256 Aug 28, 2026
2a440bd
docs(agentic-payments): four -> five env vars in the Session prose
Eras256 Aug 28, 2026
8931c8f
fix(agentic-payments): facilitator default, missing currency, broken …
Eras256 Aug 28, 2026
d963836
fix(agentic-payments): correct facilitator reasoning, close the multi…
Eras256 Aug 28, 2026
5f00ce7
fix(agentic-payments): normalize path in fail-closed fallback, valida…
Eras256 Aug 28, 2026
6bfb4d1
fix(agentic-payments): stop hand-replicating route matching, isolate …
Eras256 Aug 29, 2026
a2ae0cc
Merge remote-tracking branch 'upstream/main' into agentic-payments-pr…
vaiosx01 Aug 29, 2026
8905688
fix(agentic-payments): correct the %2f comment, show handlers moved u…
Eras256 Aug 29, 2026
41e0718
fix(agentic-payments): call resolveRecipient() for x402's PAY_TO, not…
Eras256 Aug 29, 2026
e9f4d42
fix(agentic-payments): add the missing Session middleware factory, de…
Eras256 Aug 29, 2026
a57a07a
fix(agentic-payments): use || not ?? for the facilitator URL default
Eras256 Aug 29, 2026
4618db0
fix(agentic-payments): require FEE_PAYER_SECRET in Session's gate, de…
Eras256 Aug 29, 2026
d79ebb5
fix(agentic-payments): Session's channel() was missing store, feePaye…
Eras256 Aug 29, 2026
c4e5010
fix(agentic-payments): dual-intent store comment pointed at the wrong…
vaiosx01 Aug 29, 2026
c555866
fix(agentic-payments): validate MPP_CHANNEL_CONTRACT before it reache…
Eras256 Aug 31, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 19 additions & 0 deletions evals/scenarios/agentic-payments/04-multi-route-pricing.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"skills": [
"agentic-payments"
],
"query": "I have three x402-protected Express routes (GET /signals, GET /market, POST /execute), each at a different USDC price, all behind the same OZ Channels facilitator. Right now I'm calling paymentMiddleware separately for each one. Is there a cleaner way?",
"expected_behavior": [
"Switches to paymentMiddlewareFromConfig from @x402/express with a single route-keyed object (keys like \"GET /signals\") instead of one paymentMiddleware/x402ResourceServer call per route",
"States that a request whose method+path doesn't match any key falls through to next() unpriced, not a 402 or an error",
"Puts description (and mimeType, when set) at the route-config level, not nested inside accepts — PaymentOption has no description field, so nesting it there compiles but the 402 response serves it empty",
"Does not crash the process at startup when the recipient/payTo env var is unset — wraps middleware initialization in a check instead, and fails CLOSED per request: every request under the paid mount point gets a 503, never a silent 200",
"Resolves payTo through the same recipient-resolution function as mpp.md (recovering a secret key pasted where the public key belongs, rejecting a malformed G... via StrKey.isValidEd25519PublicKey), not a bare process.env.STELLAR_RECIPIENT read — payTo is echoed back unvalidated in every 402 response body, and an unrejected malformed value would reach paymentMiddlewareFromConfig() as real config instead of tripping the fail-closed fallback",
"Mounts the payment middleware under its own path prefix (e.g. app.use(\"/paid\", x402Middleware)) rather than at the app root, and does NOT try to hand-replicate @x402/core's own route matching (method uppercasing, path normalization, wildcard/:param/[param] regex compilation) to decide which unconfigured requests deserve a 503 — every request that reaches the mounted middleware is a paid route by construction, so the unconfigured fallback can respond 503 unconditionally instead of risking a case, encoding, or dynamic-route variant slipping through free",
"Moves the actual route handlers (app.get/app.post for /signals, /market, /execute) under the same /paid prefix as the middleware mount, not just the middleware itself — a handler left registered at the old top-level path never passes through the payment middleware at all and serves its content free regardless of whether PAY_TO is configured",
"Sets an explicit fallback for the facilitator URL using || (e.g. process.env.FACILITATOR_URL || \"https://channels.openzeppelin.com/x402/testnet\"), NOT ?? — ?? only falls back on null/undefined, so FACILITATOR_URL= (set to an empty string in a .env file) passes \"\" straight through, which is falsy inside @x402/core's own 'config?.url || DEFAULT_FACILITATOR_URL' check and silently lands on the library's generic x402.org fallback anyway, sending the OZ_API_KEY Bearer token to the wrong operator and, on mainnet, hitting a facilitator with no stellar:pubnet entry at all"
],
"machine_checkable": [
"Generated server passes tsc --noEmit / node --check against @x402/express, @x402/core, @x402/stellar"
]
}
21 changes: 21 additions & 0 deletions evals/scenarios/agentic-payments/05-production-hardening.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{
"skills": [
"agentic-payments"
],
"query": "My MPP charge server throws at startup in a fresh environment because STELLAR_RECIPIENT isn't set yet, which breaks CI. I also want to add Session mode later without duplicating the whole server, and I want a way for an agent to check which payment intents are actually live before it makes a request. What's the production-safe way to structure this?",
"expected_behavior": [
"Wraps recipient resolution so the process never throws at import time: recovers when a secret key (S...) was pasted where the public key belongs by deriving the public key and warning loudly, and disables the affected middleware (not the process) when the value is unset or unparseable",
"Gives Charge and Session mode separate Mppx instances, each initialized only when its own full config is present, so adding Session later is additive, not a rewrite",
"Fails CLOSED per request when an intent's own instance is null — the route middleware returns a non-200 (e.g. 503), never a plain next() that lets the route respond with its normal content unpriced",
"Exposes a runtime /info endpoint reporting each intent's true initialized state (enabled: !!instance), not a static capability list, and does not call it a health check — it reports configuration/initialization state, not whether the RPC or facilitator it depends on is currently reachable",
"For Session mode's commitmentKey, converts the stored hex (MPP_COMMITMENT_KEY) into a Stellar G... address before passing it to stellar.channel() — the library validates it as a Stellar address, not raw ed25519 bytes",
"Validates MPP_COMMITMENT_KEY against a strict regex (e.g. /^[0-9a-f]{64}$/i) on the raw string BEFORE decoding, not a length check on the decoded bytes — Buffer.from(x, 'hex') stops at the first invalid character instead of throwing, so 64 valid hex chars followed by garbage still decodes to exactly 32 bytes and would pass a decoded-length check",
"Never throws when MPP_COMMITMENT_KEY is malformed — catches it, logs the error, and leaves sessionMppx null (Session fails closed the same way an unset env var already does) without affecting chargeMppx, which must stay up regardless of Session's configuration state",
"Wires recipient and currency into stellar.channel()'s config when available, not just channel and commitmentKey — both are optional in the library's types but exist specifically so it can reject a channel that would settle to the wrong account or pay out the wrong token",
"Gives Session its own route-middleware factory (e.g. mppSessionMiddleware) mirroring the Charge one, rather than calling sessionMppx.channel(...) directly as route middleware — sessionMppx is null when Session isn't configured, and evaluating that call at route-registration time (which runs at import time) would throw immediately and crash the whole process, chargeMppx included",
"Includes FEE_PAYER_SECRET in the gate that decides whether sessionMppx is constructed, not just MPP_CHANNEL_CONTRACT/commitmentKey/recipient/MPP_SECRET_KEY — channel.Parameters' own feePayer doc comment says it's \"Required when handling close credential actions,\" so omitting it from the gate lets sessionMppx construct successfully and /info report Session as live right up until close() is actually called and throws",
"Validates MPP_CHANNEL_CONTRACT with StrKey.isValidContract() before including it in the gate and passing it to stellar.channel(), the same validate-before-use pattern as commitmentKey and feePayerSigner — channel() only validates store, not the channel address, so a typo'd contract ID still builds sessionMppx and still reports Session as live in /info, then throws deep inside the SDK on the first paid request instead of failing at boot",
"Validates FEE_PAYER_SECRET with StrKey.isValidEd25519SecretSeed() before calling Keypair.fromSecret() on it, same pattern as commitmentKey's hex regex — Keypair.fromSecret() throws synchronously on a malformed secret, and since that call sits inside the Mppx.create() branch, an unvalidated bad key throws at import time and crashes chargeMppx along with it, not just Session",
"Passes store (e.g. Store.memory() for dev, a persistent store for production) into stellar.channel()'s config for Session mode, not just for Charge — channel.Parameters declares store as required, not optional, so omitting it throws at construction time inside the same ternary, which is exactly the failure this section's 'no intent's setup may throw' rule is meant to prevent"
]
}
Loading
Loading