Skip to content

chore: reconcile main into dev so v1.3 work cannot drop the payment fixes - #43

Closed
enot3615 wants to merge 4 commits into
devfrom
chore/reconcile-main-into-dev
Closed

chore: reconcile main into dev so v1.3 work cannot drop the payment fixes#43
enot3615 wants to merge 4 commits into
devfrom
chore/reconcile-main-into-dev

Conversation

@enot3615

@enot3615 enot3615 commented Aug 27, 2026

Copy link
Copy Markdown
Collaborator

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:

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

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 — 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, dev carries aifinpay-agent 1.5.0 and @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.

enot3615 and others added 4 commits August 27, 2026 07:52
…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
@enot3615

Copy link
Copy Markdown
Collaborator Author

Closing — this reconciles main → dev, which was the right direction only while
dev was assumed to be the integration branch. It is not.

Superseded by #47, which goes the other way and makes main the single trunk.

The fact that settled it: dev does not have the #42 royalty fix —
_splitter_treasury(pm[ is still in python/aifinpay/unified_agent.py there,
and absent on main. Anything built on dev inherits taking the merchant's
royalty, which disqualifies it as the base for the v1.3 execution work.

Supporting: dev has never produced a release (no unique tags, one feature since
2026-08-15), ci.yml triggers on bare push:/pull_request: with no branch
filter so dev buys no extra safety, there is no publish automation making
either branch mechanically special, main requires 4 status checks to dev's 3,
and 7 of 11 open PRs already target main.

@enot3615 enot3615 closed this Aug 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant