Skip to content

fix(node): stop paying the merchant's royalty to our own treasury - #51

Merged
daochild merged 3 commits into
mainfrom
fix/node-royalty-belongs-to-merchant
Sep 2, 2026
Merged

fix(node): stop paying the merchant's royalty to our own treasury#51
daochild merged 3 commits into
mainfrom
fix/node-royalty-belongs-to-merchant

Conversation

@enot3615

@enot3615 enot3615 commented Sep 2, 2026

Copy link
Copy Markdown
Collaborator

Ports the #42 royalty fix from the Python client to the Node client, five weeks late.

The defect

When a merchant has not named an ip_creator, the Node client filled the royalty slot by reading B2BSplitter.treasury(). The comment argued for it — address(0) would supposedly strand the 1bp in the contract, which has no sweep function.

That is not what the contract does. B2BSplitter._split(), contracts/B2BSplitter.sol:238-244:

if (_ipCreator != address(0)) { ipAmt = _total * ipCreatorBps / D; }
// else: ipAmt stays 0 and is absorbed into merchantAmt below
merchantAmt = _total - treasuryAmt - ipAmt;

With address(0) the royalty is folded into the merchant's leg. Nothing is stranded. So the fallback took 1bp off every payment whose merchant had not set a creator and paid it to our treasury. This is the on-chain source of the 0.01% creator fee QA raised.

Why a test that reads two files

Python was fixed in #42 on 2026-08-27; Node was not touched. A unit test on either client alone passes the whole time — the clients only disagree with each other. node/tests/royalty-parity-with-python.test.ts therefore reads both sources and asserts they resolve the slot the same way. Verified failing on the defect (2 of 4 assertions) before being accepted.

This is the third instance of the same shape this month — one implementation corrected, its twin left alone. The second was gate/src/scope.ts against backend/aifp/scope.js (#50).

Also in this PR

  • The dead splitterTreasury() helper and its cache are removed. Leaving the machinery in place is how the defect comes back.
  • The Solana branch still routes through treasury in both clients. That is matched and deliberate — the Solana bridges do not surface an ip_creator — and the fourth assertion pins it so nobody "fixes" it for symmetry.

Checks

When a merchant has not named an ip_creator, the Node client resolved the
royalty slot by reading B2BSplitter.treasury() and passing that. The comment
justified it: address(0) "would skip the transfer and permanently strand the
1bp inside B2BSplitter — the contract has no sweep function."

The premise is false. B2BSplitter._split(), contracts/B2BSplitter.sol:238-244:

    if (_ipCreator != address(0)) { ipAmt = _total * ipCreatorBps / D; }
    // else: ipAmt stays 0 and is absorbed into merchantAmt below
    merchantAmt = _total - treasuryAmt - ipAmt;

Nothing is stranded — with address(0) the royalty folds into the merchant's
leg, which is where it belongs when nobody named a creator. So the fallback
was not rescuing a basis point, it was taking one off every such payment and
paying it to us. That is the on-chain source of the "0.01% creator fee" that
QA raised.

Python was corrected in #42 on 2026-08-27. Node was not. Same defect, same
week, twin file left alone — the third instance of that pattern this month
(gate/src/scope.ts against backend/aifp/scope.js was the second).

A unit test on either client alone passes throughout, so the guard added here
reads both files and fails when they disagree. Verified failing on the defect
before it was accepted. The dead splitterTreasury() helper and its cache are
removed with it; leaving the machinery in place is how this comes back.

The Solana branch keeps routing through treasury in both clients. That one is
matched and deliberate — the Solana bridges do not surface an ip_creator at
all — and the fourth assertion pins it so nobody "fixes" it by symmetry.

Full Node suite: 189/189.
daochild
daochild previously approved these changes Sep 2, 2026
daochild and others added 2 commits September 2, 2026 11:37
The published-files gate is right: unifiedAgent.ts ships to users, so two
different builds must not both call themselves rc.3.

rc.6 rather than rc.4 on purpose. rc.4 is claimed by #46 and rc.5 by #49, both
still open. Taking the next number above every open claim means this can merge
in any order without making anyone renumber — and a royalty fix should not wait
on a release-numbering negotiation.
@daochild
daochild merged commit e5db52b into main Sep 2, 2026
15 checks passed
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.

2 participants