feat: add aggregate /.well-known/x402 and external buyer-tool compat - #806
feat: add aggregate /.well-known/x402 and external buyer-tool compat#806HananINouman wants to merge 8 commits into
Conversation
Publish AgentCash/x402scan discovery fallback on the shared storefront, surface AgentCash/Bankr buy prompts, and gate the path with flow-22 (generic x402 SDK + agent chat-completions) in release-smoke. Co-authored-by: Cursor <cursoragent@cursor.com>
Skip settle on client disconnect/write errors, classify facilitator rejections, and teach HTTP buyers Bankr chat auto-pay while agent/inference use wallet-sign plus long curl after live timeout and voucher failures. Co-authored-by: Cursor <cursoragent@cursor.com>
Publish type-specific Poncho chat prompts alongside AgentCash/Bankr and surface them in the storefront pill selector after a successful live pay test. Co-authored-by: Cursor <cursoragent@cursor.com>
Record that a Bankr App with pay:x402 + allowedHosts still hit rpc timeout on Base mainnet agent offers; drop the Sepolia digression and clarify HandleProxy settle-skip vs ForwardAuth verify-only. Co-authored-by: Cursor <cursoragent@cursor.com>
…-rc2 Brings the aggregate /.well-known/x402 discovery doc and external buyer-tool compat (Merit/AgentCash/Poncho, Bankr) into rc2. Conflict resolution — both sides were additive, both kept: - catalog.go: the static-site match/hash now folds in BOTH the widget's embedded chat-vendor.js (rc2) and the new x402.json (#806), so a stale copy of either re-applies the ConfigMap. - forwardauth_test.go: kept TestBuildResourceURL_Scheme (rc2) alongside #806's brokenPipeWriter settle-skip tests. - Widget-era test call sites updated for the new wellKnownX402JSON param. Claude-Session: https://claude.ai/code/session_01PnhCQLz7CHuDBUhWd5xF8v
…SON param The #806 merge added a wellKnownX402JSON parameter to buildStaticSiteConfigMap/staticSiteContentMatches, but four widget-era call sites in catalog_test.go and hostoffer_test.go were left at the old arity, so the package failed to build its tests. Pass "" (these tests do not exercise the aggregate x402 document). Claude-Session: https://claude.ai/code/session_01PnhCQLz7CHuDBUhWd5xF8v
…njection) CodeQL flagged four new go/log-injection alerts introduced by this PR in internal/x402/forwardauth.go: the buyer's payment payload and the facilitator's reject reason reach log.Printf unescaped, so a crafted value containing CRLF can forge additional lines in the operator's log (e.g. a fake "payment settled successfully" entry). Fix at the producers rather than at each log call, so future call sites inherit it: paymentPayloadSummary, facilitatorRejectDetail and the normalize note now return CR/LF-stripped strings, and truncateForLog sanitizes too (it carries the raw facilitator /verify body). Claude-Session: https://claude.ai/code/session_01PnhCQLz7CHuDBUhWd5xF8v
5e15e33 to
a5d6972
Compare
…-rc2
Brings the aggregate /.well-known/x402 + external buyer-tool compat work
along with its CodeQL go/log-injection fix.
Conflict resolutions (both additive — kept BOTH sides, not one over the
other):
* catalog.go — the static-site match and content hash now fold in both
the widget's embedded chat-vendor.js (rc2) and the new x402.json
(#806). Dropping either side would silently pin a stale copy of that
asset across controller upgrades via the skip-when-unchanged path.
* forwardauth_test.go — kept rc2's TestBuildResourceURL_Scheme
alongside #806's brokenPipeWriter settle-skip tests.
Also updates four widget-era test call sites in catalog_test.go and
hostoffer_test.go for #806's new wellKnownX402JSON parameter (they pass
"" — those tests do not exercise the aggregate x402 document). Without
this the package does not build its tests.
Replaces the unsigned a128bdf/dd9c5a1d/cdee3a22 with a single signed
merge; the resulting tree is byte-identical to cdee3a2.
Claude-Session: https://claude.ai/code/session_01PnhCQLz7CHuDBUhWd5xF8v
…ecks Drop chatty framing, require AgentCash/Poncho tx receipts, and teach Bankr agent buyers to reuse the full accepts[] entry to avoid unsupported_scheme. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
defaultPaidMethod flipped `type: http` offers without declared `methods`
from POST to GET. For a root-priced route that makes the advertised
operation unpayable.
renderStaticSite publishes the offer root as exactTo("/", "index.html"),
a GET-scoped Exact match, and its comment states the contract:
Method-scoped to GET: discovery documents are read-only, and a
root-priced offer (route pattern "/") advertises POST <origin>/ as
its paid resource — an unscoped Exact "/" match would shadow that
POST into the static httpd (501) instead of the payment gate.
So at the offer root GET is served by the landing page and only POST
falls through to the gate. The previous hardcoded POST was one half of
that two-sided contract; changing the advertised method without changing
the routing left discovery pointing at a 200 HTML page that can never
return 402.
Observed live on a deployed offer before this fix:
GET https://<offer-host>/ -> 200 text/html
POST https://<offer-host>/ -> 402
with /.well-known/x402, openapi.json and skill.md all advertising GET.
defaultPaidMethod now takes the paid route's path and returns POST when
that route is root-priced ("", "/" or "/*"), GET otherwise. Sub-path paid
routes are unaffected and keep the GET default that stops OpenAPI and
AgentCash clients POSTing into a GET-only upstream (405), which is what
the GET default was introduced for.
The blast radius is any paid route declared WITHOUT `methods`, not only
offers with no route table at all — primaryPaidMethod checks
len(rt.Methods) > 0. A route table whose paid catch-all omits `methods`
was affected too.
Two existing assertions encoded the old behaviour for the root-priced
case and are updated with the reason. paidmethod_root_test.go adds the
check the route-surface suite was missing: the advertised method must not
be one the static site claims first. The release smoke could not catch
this because the payment gate matches on path only, ignoring method, so a
bare GET probe returns 402 regardless of what discovery advertises.
Pre-publish testing of
|
Carries d359a14: root-priced type:http offers advertise POST again, so the advertised paid operation reaches the payment gate instead of the static index.html. Adds paidmethod_root_test.go.
|
Superseded by #811 ( Verified contained, not assumed: Closing to keep one path to |
Summary
Makes Obol Stack storefront offers work with external x402 buyers — especially Merit Systems (AgentCash / x402scan / Poncho) and Bankr — via discovery docs, verifier hardenings, type-specific buy prompts, and flow-22.
Merit discovery (AgentCash / Poncho / x402scan)
Merit products share one discovery convention:
/openapi.jsonwith per-opx-payment-info/.well-known/x402for crawlers that don’t parse OpenAPIThis PR adds aggregate public
/.well-known/x402, keeps/strengthens OpenAPIx-payment-info, defaults undeclared paid HTTP ops to GET (AgentCash-friendly), and documents both surfaces in skill.md / catalog.Why AgentCash and Poncho share a flow: both are Merit. Poncho (tryponcho.com) is consumer chat; AgentCash is the micropayment/wallet layer. Same discovery → same 402 → sign → retry. Storefront pills differ mainly by “paste into AgentCash vs Poncho.” Live Base tests: both paid our HTTP offer successfully.
Bankr (live Base mainnet)
Bankr chat / CLI / Apps are different clients. Docs mention discovery-index / Apps
allowedHosts, but allowlisting alone does not fix agents: we built a Bankr App withpay:x402+allowedHostsfor our tunnel andbankr.x402.fetchagainst bounty-radar — payment often verified, then the App failed withrpc timeout~30s while the agent was still running. HTTP through the same App/chat path usually works (~1s).bankr x402 callbankr.x402.fetch(with allowlist)rpc timeoutafter verifybankr wallet sign+ curl ≥180sTwo failure modes: (A) voucher (
validAfter=now/ bad sig → verify reject, usually no charge); (B) timeout/zombie (verify OK, client aborts ~30s; older sellers could still settle → on-chain charge). Seller side: skip settle on disconnect/write error/empty SSE; better facilitator error labels; dual CAIP-2+legacyaccepts[];PAYMENT-SIGNATURE+ v=0/1→27/28; structured post-verify upstream errors.Prompts we publish: HTTP → Bankr chat auto-pay; agent/inference → forbid chat/Apps/
x402 call, teachbankr wallet sign+curl --max-time 300with pastvalidAfterandstream:true.Storefront / CI / docs
buyprompts→/api/services.json)flow-22-external-buyer-compat.sh+ generic x402 SDK buyer; release-smoke hookdocs/observability.md,CLAUDE.mdpitfall 23, buy-x402 skill noteTest plan
GET <tunnel>/.well-known/x402+ OpenAPIx-payment-infoon paid opsgo test ./internal/buyprompts/ ./internal/x402/ ./internal/serviceoffercontroller/Poncho chat, agent offer
Bankr wallet+cursor curl, agent offer
Agentcash(skill/cli/mcp) connected to cursor , agent offer
Bankr chat, http offer