chore: reconcile main into dev so v1.3 work cannot drop the payment fixes - #43
chore: reconcile main into dev so v1.3 work cannot drop the payment fixes#43enot3615 wants to merge 4 commits into
Conversation
…om (#39) An external QA pass on the Raters integration read our 402 and reported it as a protocol defect: "no merchant address, no Polygon/USDC, no quote/order ID, no expiry, no receipt instructions". Verdict BLOCKER. Most of that report traces to the middleware not being in the request path at all on that deployment, which is a separate matter. But this part was a fair reading of a real gap: the 402 does not carry those fields AND never said where they were, so "missing" was the only conclusion available. They cannot be inlined. accepted_chains is derived per merchant from Object.keys(merchant.pay_to) (backend/routes/aifp.js:608); accepted_assets drops POL whenever there is no live POL rate (:550); order_id and expiry belong to a quote that does not exist yet. All of it changes without this resource changing, and a gate running on the partner's own host holds none of that state. A static challenge listing them would sometimes promise a settlement the quote refuses, which is worse than not listing them. So the 402 gains one line naming the endpoint that does have them: settlement_terms_from: "POST https://api.aifinpay.io/v1/quote — returns accepted_chains, accepted_assets, amount, order_id and expiry" Two things the QA report asserted that are NOT our protocol, recorded here because the same reading will recur: * There is no free quota. No "first 100 free", no grace, no trial — grep of the manifest, llms.txt, the gate README and the frontend finds no such promise anywhere. In AIFP-1 request #1 gets the 402. `min_requests` is the minimum PREPAID batch ($0.10 / unit price), a different thing entirely. * Chains are named strings — "polygon", "amoy" — not numeric chain IDs. There is no 137 in this API to return. tests/challenge.test.ts pins the exact key set of the 402, so it failed on this change. That is the test working: the protocol surface should not grow by accident. Updated deliberately, with the reason next to it. 15 files, 94 tests pass. Refs AIFINP-209 Claude-Session: https://claude.ai/code/session_01We1vVZLdj2vYtYaj7fjahX Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…#41) A partner registered 52 resources, 8 of them pages, deployed, and served every page 200. Their tester filed it as a BLOCKER against our protocol. The protocol was fine; the README documented AIFP_MERCHANT_ID and AIFP_MERCHANT_SECRET and stopped. No Next.js example, no matcher, no mention that shouldCharge is opt-in, nothing about robots.txt. AIFINP-209. All four failures are silent, and in all four the dashboard shows paywall_enabled: true while the truth is otherwise: * A path missing from config.matcher is never seen by the middleware. The resource is registered, priced, enabled — enforcement zero, forever, and nothing reports it. * shouldCharge omitted charges everything. core.ts is `if (options.shouldCharge)`, no default. On an API that is usually right; on a PAGE it puts a 402 in front of your own readers and Googlebot, which is worse than having no gate at all. * A predicate that throws charges the request. A reader who assumes the opposite writes one that fails open and serves crawlers free. * robots.txt Disallow stops a well-behaved crawler before it reaches the paywall. No amount of correct SDK configuration fixes that, and merchants who arrive from "block the AI scrapers" already have the line in place. You cannot forbid a crawler and bill it at the same time. The new §1b is a working middleware.ts with the matcher covering pages, the predicate passed, and the robots.txt consequence stated as the business decision it is: monetising AI traffic means stop blocking, start charging. There is a test on it. Documentation usually does not deserve one — this does, because the failure it prevents is invisible. Nobody files a bug for revenue that never arrived, which is why a partner's QA found this and we did not. Verified it fails against the README this replaces: 4 of 4. 98 tests pass across 16 files. Refs AIFINP-209 Claude-Session: https://claude.ai/code/session_01We1vVZLdj2vYtYaj7fjahX Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…f-hosted origin (#42) * fix(sdk): stop taking the merchant's royalty, and let MCP reach a self-hosted origin Three things an external E2E run found on 2026-08-27, after the payment had already succeeded. AIFINP-211. ## 1. The royalty slot defaulted to our own treasury python/aifinpay/unified_agent.py substituted the splitter's treasury() whenever a payment method carried no ip_creator, justified in the code as: Passing address(0) would skip the transfer and permanently strand the 1bp inside B2BSplitter — no sweep function. B2BSplitter._split does not do that: if (_ipCreator != address(0)) { ipAmt = ...; } // else: ipAmt stays 0 and is absorbed into merchantAmt below merchantAmt = _total - treasuryAmt - ipAmt; ... if (ipAmt > 0) { transfer to _ipCreator } With address(0) nothing is stranded — the merchant keeps it and no transfer is attempted. The premise was wrong and the consequence was that 0.01% of every unattributed payment moved from the merchant to us, silently, while /v1/quote published a 99/1/0 split. Observed on-chain: tx 0x6b853876… — merchant 98.99%, treasury 1.00%, and 0.01% paid to 0xD31d82…3c8e, our own Safe. The SDK ignored settlement_call.args .ip_creator = 0x000…000 and used the fallback. ## 2. MCP never exposed gatewayOrigins @aifinpay/agent has supported it since parseGatewayUrl existed. This wrapper did not pass it, so a self-hosted merchant was unreachable: payable_fetch reached the 402 and refused with "dev.ratersapp.com is not a known AiFinPay gateway (allowed: https://gateway.aifinpay.io)". Now AIFINPAY_GATEWAY_ORIGINS. Validated rather than trusted: a bare origin, https only, and a plain hostname. WHATWG URL accepts "*" in a hostname, so "https://*.example.com" parses and its origin round-trips — it would be stored, match nothing, and read as "I allowed this host". My first version of the validator let it through; the test caught it. Unset stays undefined rather than [] — those differ downstream, where [] means "no origin is payable at all". ## 3. MCP could not resolve anything behind a proxy safe-fetch resolves a hostname and refuses if any answer is private — an SSRF guard. Behind an HTTP proxy the client does not resolve at all, so lookup() fails with EAI_AGAIN and every host is refused as "cannot resolve": the guard misfiring on the environment rather than on a threat. Now AIFINPAY_TRUSTED_HOSTS, and deliberately NOT a proxy-detection switch. "We seem to be behind a proxy, disable the check" turns one environment quirk into a blanket SSRF bypass, which is the vulnerability that file exists to prevent. An operator names hosts one at a time, matched EXACTLY — no suffix matching, because "example.com" trusting "evil-example.com" is how an allowlist stops meaning anything. The resolution failure now names the variable, so the next person meets a misconfiguration instead of a broken SDK. Tests keep both of these allowlists: the tempting fix for either is a global switch. Verified the python tests fail against the treasury fallback (3 of 4). 74 tests pass across 6 files. Refs AIFINP-211, AIFINP-210 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01We1vVZLdj2vYtYaj7fjahX * chore(release): aifinpay-agent 1.5.0, @aifinpay/mcp 2.0.0-rc.3 The version gate is right to insist. aifinpay-agent 1.5.0 is a minor, not a patch: the royalty fallback changes where 0.01% of every unattributed payment goes, so an agent that upgrades builds a different transaction. A patch bump would have said the opposite. @aifinpay/mcp 2.0.0-rc.3 adds AIFINPAY_GATEWAY_ORIGINS and AIFINPAY_TRUSTED_HOSTS, both unset by default — no behaviour changes for anyone who does not set them. Refs AIFINP-211 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01We1vVZLdj2vYtYaj7fjahX --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ixes The v1.3 audit (2026-08-27) lists this as P0: "Reconcile SDK main/dev and preserve hotfix #42 before any v1.3 execution work." Measured before the merge: main 9662fec #39, #41, #42 dev c80206a neither — and still carrying the treasury fallback grep '_splitter_treasury(pm[' dev → 1 grep '_splitter_treasury(pm[' main → 0 So a v1.3 settlement PR built on dev would have reintroduced the royalty defect #42 fixed: 0.01% of every unattributed payment routed to our own treasury instead of the merchant. A correct new router wrapped around stale transaction-building code, which is the exact risk the audit names. Clean merge. The two dev-only commits add node/internal-tokenlist/, a new package with no file overlap with anything on main. After this, dev carries aifinpay-agent 1.5.0 and @aifinpay/mcp 2.0.0-rc.3 — the versions actually published — so the v1.3 execution work has a reconciled baseline to build from rather than a branch that predates the fixes. Refs AIFINP-211 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01We1vVZLdj2vYtYaj7fjahX
|
Closing — this reconciles Superseded by #47, which goes the other way and makes The fact that settled it: Supporting: |
The v1.3 audit lists this as P0: "Reconcile SDK main/dev and preserve hotfix #42 before any v1.3 execution work."
Measured before the merge:
A v1.3 settlement PR built on
devwould have reintroduced the royalty defect #42 fixed: 0.01% of every unattributed payment routed to our own treasury instead of the merchant. A correct new router wrapped around stale transaction-building code — the exact risk the audit names.Clean merge
The two dev-only commits add
node/internal-tokenlist/, a new package with no file overlap with anything on main. No conflicts.After this,
devcarriesaifinpay-agent 1.5.0and@aifinpay/mcp 2.0.0-rc.3— the versions actually published — so v1.3 execution work builds from a reconciled baseline rather than a branch that predates the fixes.Refs AIFINP-211.