Skip to content
MiroSharkPublic

About

The affiliation layer for the agent economy. Turn your x402 endpoint affiliate-ready in one line.

Topics

Resources

Security policy

Stars

8 stars

Watchers

0 watching

Forks

Latest commit

 

History

61 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

x402aff - Split every payment. Builder-code affiliation for x402: give the apps that send you paying users a cut, enforced on-chain at settlement. One line on the client, no facilitator of your own to run. 10% default builder cut, 1 line on the buyer's client, 0 contracts to run, 7 mainnet fork tests.

Get it →   Star x402aff on GitHub   x402aff on PyPI   x402aff on npm   Live claims dashboard at miroshark.xyz/x402aff   Integration docs

The kit in four numbers: 10% default builder cut (1000 bps, set anywhere from 0 to 10000), 1 line on the buyer's client, 0 contracts to run, 7 mainnet-fork tests (5 of them adversarial).

PyPI npm CI License: MIT Dashboard


Give the apps that send you paying users a cut - enforced on-chain, at settlement, with no facilitator of your own to run.

If you sell an API behind an x402 paywall, this splits each payment so the builder whose app drove it earns a share (default 10%). The cut lands in an ownerless, immutable 0xSplits contract at settlement, so once it's there nobody - including you - can redirect it. That's what makes it a credible offer rather than a promise.

Off-chain affiliate promises vs. an on-chain split. Before: you track referrals in a database, pay out monthly, and can change the rate or pause payouts; builders cannot verify a thing. After, with x402aff: the payment lands in an ownerless 0xSplits contract, split at settlement in the same USDC transfer, recipients and ratio baked into the address, and anyone can trigger the payout.

No settler, no self-run facilitator, no contract you write or audit: the stock CDP facilitator settles (gas sponsored), an audited 0xSplits contract splits, and the builder adds one line to their x402 client.

Validated on Base mainnet and reproducible on a mainnet fork - see the Foundry proof in fork-test/, and a live seller's claims dashboard at miroshark.xyz/x402aff.


How it works

How payment works: 01 Request, the buyer's app calls your route with its builder code. 02 402, your API answers with payTo set to the (you, builder) split address. 03 Settle, the CDP facilitator settles the gasless USDC payment into the split and writes a/s/w on-chain. 04 Payout, anyone calls distribute: 10% to the builder, 90% to you.

Base Builder Codes put three tags on a paid request: a (you, the API), s (the builder that drove it), w (the facilitator). The kit turns s into money with one move: set the route's payTo to a per-(you, builder) 0xSplits split.

  buyer's app          YOUR /run                    CDP facilitator        Base
  (sends its code) ──▶ 402: payTo = the split ──▶ settles USDC into it ──▶ split
                                                    (writes a/s/w)          holds funds
                                                                              │
                              distribute (anyone) ◀───────────────────────────┘
                                     │
                        ┌────────────┴────────────┐
                     10% builder                90% you

Payouts aren't atomic: money settles into the split on its own, but releasing it (distribute) is a separate permissionless call - you, a keeper, or the builder can trigger it.


Get started - pick your language

Both ship a one-object Affiliation facade and resolve the identical split address (the TS encoding is asserted byte-for-byte against Python). Get an app code at base.dev → Settings → Builder Codes, then:

TypeScript / Node → ts/ · guide: ts/README.md

npm install x402aff viem     # viem is a peer dependency
import { Affiliation } from "x402aff";

const aff = new Affiliation({ appCode: "bc_yourcode", sellerPayout: "0x…" });

// `aff.payTo` is a drop-in x402 DynamicPayTo callback for express/hono/next
// middleware: `{ payTo: aff.payTo, extensions: aff.extensions }`. Or resolve it
// yourself: `const payTo = await aff.payToFor(req.headers)`.
const extensions = aff.extensions;               // declares your `a`
// payouts: const { calls, balanceUnits } = await aff.release("bc_alice");

Only dependency: viem, kept as a peer so you share the one your app already has. Or just vendor ts/src/affiliation.ts - it is one file, and it ships in the published tarball too.

Python → python/ · guide: INTEGRATION.md

pip install x402aff              # add 'x402aff[cdp]' for the CDP discovery path
from x402aff import Affiliation

aff = Affiliation(app_code="bc_yourcode", seller_payout=YOUR_WALLET)

# on your x402 route:
PaymentOption(..., pay_to=aff.pay_to)         # per-request split, from the header
RouteConfig(..., extensions=aff.extensions)   # declares your app code `a`

# release payouts, later (permissionless):
calls, balance = aff.release("bc_alice")

aff.pay_to is a drop-in x402 DynamicPayTo callback; aff.pay_to_for(headers) is the sync form for other frameworks.

Neither ever throws: an unknown or unresolvable code falls back to your wallet, so the payment always works, it just isn't split. The buyer side is one line - Python ships BuilderCodeClientExtension (x402aff.buyer_client), TS uses the official @x402/extensions/builder-code. Stamping s by hand instead? marked_service_codes() / markedServiceCodes() build the array for you.


Options

Everything a seller needs, nothing to operate: change the cut, more than two recipients, release payouts, claims dashboard, find every kit payment, any language.

Change the cut. Basis points, default 1000 (10%), range 0..10000 (0 = attribution only). Set X402_BUILDER_SHARE_BPS=1500 (both SDKs) or pass builder_share_bps=1500 (Python) / builderShareBps: 1500 (TS). The ratio is baked into the split address, so changing it opens a new split per builder - old funds stay safe at the old ratio.

More than two recipients (platform fee, partner, …). Any recipients whose allocations sum to 10000: build a SplitPlan directly and address-prediction + distribute just work.

Release payouts. aff.pending() (or python3 -m x402aff.monitor as a CLI) discovers every builder who paid you straight from CDP's index - no local ledger - and shows which splits are ready, with the cast commands.

Claims dashboard (aff.splits_payload())

One call returns every per-builder split for your seller, ready to serialize behind a GET /splits route: split address, codes + share, live balance, deployed state, and a permissionless [deploy?, distribute] claim. It reconstructs from the seller wallet the facade already holds, so it covers undeployed splits too - a counterfactual address can't be recovered seller-side from the chain otherwise.

See one running: miroshark.xyz/x402aff serves this payload live from a Base-mainnet seller. Every split, balance, and claim on that page comes from aff.splits_payload(), and the claim button is the permissionless distribute - anyone can trigger it, and the funds still go only to the split's baked-in recipients.

@app.get("/splits")                       # Python (Flask)
def splits(): return aff.splits_payload()
app.get("/splits", async (_req, res) =>   // TS (Express) - pass your CDP SQL runner
  res.json(await aff.splitsPayload(cdpQuery)));

Deliberately no per-split payment count: getting one alongside the received USDC amount means joining base.events, which trips the CDP SQL API's leaf-scan limit (measured 94.44 GiB against a 93.13 GiB cap) and 400s. For just the count, queries.sql #5c reads the attribution table alone - no join, confirmed working. #5b records why the amount can't come cheaply.

Discover every kit payment (any seller)

The buyer extension stamps a hardcoded, shared marker (x402aff) as a second s, so one query finds every kit-routed payment across all sellers - undeployed splits included, no address reconstruction, and it never changes a payout (the split pays the primary code):

SELECT DISTINCT transaction_hash
FROM base.transaction_attributions
WHERE builder_code = 'x402aff' AND action = 1;

Run it in the CDP SQL Playground (no auth) or POST https://api.cdp.coinbase.com/platform/v2/data/query/run with a CDP JWT. Join those txs to the USDC Transfer to recover each split and read its claimable balance - queries.sql #5 / #5b.

Any other language (Go, Rust, …)

Three view-calls against two contracts - port ts/src/affiliation.ts or python/x402aff/affiliation.py:

  1. code → payout - payoutAddress(uint256) on the registry 0x000000BC7E6457e610fe52Dcc0ca5b3ce59C8E80 (token id = the code's ASCII bytes as a big-endian int).
  2. build the Split tuple - recipients = [builderPayout, seller], allocations = [bps, 10000 - bps], totalAllocation = 10000, incentive = 0.
  3. tuple → address - isDeployed(Split, 0x0, 0x0) on the factory 0x8E8eB0cC6AE34A38B67D5Cf91ACa38f60bc3Ecf4 returns your payTo.

Good to know

  • Mainnet + CDP only. The registry and factory the kit reads exist only on Base mainnet, and attribution is written by the CDP facilitator.
  • Use a paid RPC. Set X402_BASE_RPC - the public one 429s, and a failed resolve silently falls back to your wallet (unsplit).
  • Tell builders how their code resolved. Echo PayTo.status (resolved, unregistered, invalid, error, none) in the X-Builder-Code-Status response header (STATUS_HEADER). Without it, an unminted or mistyped code looks exactly like a working one. A code shown on base.dev only resolves once it is minted on the registry (isRegistered(string) returns true).
  • Distribute costs a few cents of gas (the buyer's payment is gasless). Payouts land ~2 base units light: a split keeps 1 unit warm and floors each share.
  • s is a self-asserted tag, not signed proof of who drove a payment. Resolving it says where money goes (the code's registered payout), not who is entitled to it - a routing opt-in, the right level for an affiliate program.
  • CDP SDK stamps cdp_sdk_server / cdp_sdk_client as service codes on every settlement. That is SDK telemetry, not your builder identity. Pass builderCode as a. Discovery drops cdp_sdk* the same way it drops cdp_facil*.
  • No keys, no custody. payTo is just an address in the 402, settlement is the buyer's gasless CDP payment, and distribute is permissionless. Nothing here needs a private key.

Full trust model, edge cases, and caveats: INTEGRATION.md.


Security

Money in a split can only reach two addresses. Attacks on a funded split and their results: trigger distribute as distributor (pays out, you get 0), tampered struct (reverts), hijacking split at the funded address (impossible), updateSplit or setPaused (reverts, ownerless), front-run the deploy (still pays builder and seller).

The kit deploys no contracts of its own. It only reads two canonical third-party contracts and lets the stock CDP facilitator settle into them - no new on-chain attack surface, and the money is held by code that is already audited.

  • 0xSplits PushSplit (factory 0x8E8e…Ecf4) holds and splits each payment. Splits V2 was audited by Zach Obront ahead of its May 2024 launch (report). Every split is created ownerless (owner = 0) and immutable.
  • Base Builder Codes registry (0x000000BC…C8E80) maps a code to the payout its owner registered. Read-only, so a builder's cut can only go where that builder said.

What the split guarantees - proven against live mainnet

Money in a split can only ever reach the two baked-in recipients at the baked ratio, no matter who calls what. Not asserted - tested: fork-test/ forks Base mainnet and runs 7 tests, including 5 adversarial ones in SplitAbuse.t.sol:

Attack tried against a funded split Result
Trigger distribute yourself, name yourself the distributor builder 10% / seller 90%; you get 0 (distributionIncentive = 0)
distribute with a tampered struct (swap recipient / flip ratio / add a skim) reverts - the wallet hash-checks the struct
Deploy a hijacking split that pays you, at the funded address impossible - different params → different CREATE2 address
updateSplit / setPaused to redirect or freeze (seller or attacker) reverts - the split is ownerless
Front-run the deterministic deploy same address, same recipients; still pays builder/seller

Two locks make that hold: the address is CREATE2 over its params, so funds only accrue at the address for exactly those recipients; and distribute re-validates the struct against the hash stored at creation, so even there the money can't move elsewhere.


What's in the box

Path What it is
ts/ · python/ Start here - the Affiliation facades.
docs/INTEGRATION.md Integration deep-dive (money path, trust model, wiring, caveats).
fork-test/ Foundry proof against live Base contracts on a mainnet fork.
skills/x402aff/ Agent skill that teaches a coding agent to wire the kit into an x402 seller.
python/x402aff/{resolver,split,push_split,payto,distribute,monitor}.py The primitives the facades wrap.
cd python && pytest            # Python unit tests
cd ts && npm test              # TypeScript tests
cd fork-test && forge test     # mainnet-fork proof

References


A clay arcade cabinet spilling lime coins

Give builders a reason to send you buyers.

MIT licensed. Python and TypeScript. Live on Base mainnet.
www.x402aff.xyz


Built by Aaron Elijah Mars, founder of Aeon and MiroShark · @aaronjmars

About

The affiliation layer for the agent economy. Turn your x402 endpoint affiliate-ready in one line.

Topics

Resources

Security policy

Stars

8 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages