Skip to content
OpenSouk

Accept an MPP charge

Serve a sale the buyer pays before you ever answer

On the MPP charge rail the buyer pays first and the payment is attributed second. There is no 402, no retry, and no facilitator call of yours — the buyer transfers USDC straight to the split contract and then submits the transfer to us itself. What is left for you is a decision: whether to serve the resource, and on what evidence.

Both rails end in the same contract, pay out with the same arithmetic and create the same escrow record. What differs is who submits the payment and where the buyer's money sits when the split runs; x402 and MPP is that comparison. Nothing is deployed on a public chain yet — see Quickstart.

What you will build

A path that takes a transaction hash from a buyer, verifies on-chain that the transfer really landed in the split contract, and serves the resource. Two facts shape it:

  • Nothing price-checks the transfer. No contract and no service compares what the buyer sent against what you charge — there is no quote here and no authorisation. The amount is yours to check.
  • Your merchant net arrives after the buyer's payment. The split runs when our worker submits it, on a sweep timer rather than in the request.

This rail has no agent-less path: the submission requires a buyer agent id owned by the paying wallet.

Before you start

  • An active merchant identity with an active product — Register your merchant identity and List a product.
  • cast, to resolve the split contract and to read the transfer's receipt.
  • The USDC address the protocol prices in — step 1 reads it off the split contract.

Prompt mode

Show the prompt
Add an MPP-charge acceptance path to this service on Base mainnet.

The buyer pays BEFORE contacting us: it sends a plain ERC-20 USDC transfer to the protocol's
SplitRouter and then hands us the transaction hash. There is no x402 handshake, no 402, and
no facilitator call on our side.

Given a transaction hash from a buyer, verify ALL of the following against the chain before
serving anything, because nothing else does:
  - the transaction succeeded (receipt status 1);
  - it contains an ERC-20 Transfer log emitted by the USDC contract we price in;
  - that log's recipient is the SplitRouter address, read live from the
    ProtocolAddressRegistry under keccak256("SPLIT_ROUTER"). Do not hardcode it;
  - the transferred value is at least our price in USDC base units;
  - the transaction has enough confirmations for our own risk tolerance.

Treat the hash as single-use: record it and refuse a second submission of the same hash, or
one buyer's payment buys unlimited resources.

Do not wait for the split to happen before serving. The protocol splits the pooled transfer
asynchronously, on a sweep timer, after the buyer submits it for attestation.

Manual mode

Resolve the split contract and quote the charge

The recipient a buyer must pay is the split contract, and it is the same address the x402 rail uses.

export RPC=https://mainnet.base.org
export CHAIN_ID=$(cast chain-id --rpc-url "$RPC")
export PAR=<ProtocolAddressRegistry — no deployment yet, see the notice on /quickstart>
 
export SPLIT_ROUTER=$(cast call "$PAR" 'getAddress(bytes32)(address)' \
  "$(cast keccak SPLIT_ROUTER)" --rpc-url "$RPC")
export USDC=$(cast call "$SPLIT_ROUTER" 'usdc()(address)' --rpc-url "$RPC")

Read usdc() off the split contract rather than configuring it separately: that is the exact token the protocol verifies a charge transfer against, so a transfer in any other token cannot be attested no matter what you accepted.

Quoting the charge is yours to arrange. The protocol's buyer tooling takes the recipient out of the attribution token rather than from any response of yours, and reads only the first entry of an x402 accepts array — so a second accepts entry advertising a charge is not a handshake it will follow.

Verify the transfer yourself

Given $TX from the buyer:

cast receipt "$TX" --rpc-url "$RPC" --json | jq --arg usdc "$USDC" \
  --arg to "$SPLIT_ROUTER" --arg topic "$(cast keccak 'Transfer(address,address,uint256)')" '
  { status: .status,
    paid: [ .logs[]
            | select((.address | ascii_downcase) == ($usdc | ascii_downcase))
            | select(.topics[0] == $topic)
            | select((.topics[2] | sub("^0x000000000000000000000000"; "0x") | ascii_downcase)
                     == ($to | ascii_downcase))
            | { from: .topics[1], value: .data } ] }'

Four properties, all in that receipt: the status is success, the log came from the USDC contract, its recipient is the split contract, and its value is at least your price. from is the payer — the wallet the protocol will hold to the attribution — and the value to record against the hash.

Choose your own confirmation depth

The protocol will not attest a transfer until it is deep enough: our default is 30 confirmations, counted as blocks built on top of the including block, roughly a minute on Base. That is a reorg-risk decision rather than a constant.

Your depth is a separate decision, protecting a different thing — the resource you are about to serve. Serving at zero confirmations is serving against a transaction that can still disappear; serving at our depth means the buyer waits about a minute after paying.

Record the hash and serve

Store the hash, the payer and the amount before serving, and refuse a hash you have already served. The protocol's own record is idempotent on that hash — one attestation per transaction, and the on-chain split consumes the reference exactly once — but nothing tells your service that a hash has been spent. That bookkeeping is yours.

Let the buyer do the rest

The buyer signs a ChargeCommitment — see Identity and commitments — and POSTs the hash, the attribution token, its buyer agent id and that signature to the facilitator's /mpp/charge. We verify the transfer on-chain, check that the signature recovers to the payer, check that the buyer agent id is owned by that payer, check that you and your product are active, and only then sign an attestation. Our worker sweeps about once a minute and submits the split.

You do not call /mpp/charge and you cannot call it for the buyer: the signature it requires can only come from the paying wallet. Merchant routes documents it field by field.

Verify

Watch for the split, not for the transfer: the transfer only proves the money reached the router.

cast logs --rpc-url "$RPC" --address "$SPLIT_ROUTER" --from-block "$FROM_BLOCK" \
  'ChargeSettled(bytes32 indexed paymentRef, uint256 indexed escrowId,
                 uint256 indexed buyerAgentId, address payer, uint256 amount,
                 uint256 immediateCommissionAmt, uint256 heldBackAmt,
                 uint256 platformFeeAmt, uint256 cashbackAmt, uint256 merchantAmt)' \
  "$TX"

The charge's payment reference is the transfer's transaction hash and the event's first indexed topic, so the hash the buyer gave you is also the filter that finds your settlement. merchantAmt is your net. Expect roughly a sweep interval between the buyer's submission and the event, and see Escrow and router for the other nine fields. Your /v1/merchant/{id}/stats counters move once the split is indexed.

Errors and retries

What happensWhyWhat to do
The buyer reports charge transfer not verifiedThe transfer is not confirmed deeply enough yet, or does not match: wrong token, wrong recipient, reverted transactionNothing on your side. The buyer waits for depth and resubmits the same hash
The buyer reports buyerAgentId is not owned by the transfer senderThe paying wallet does not own the identity it claimedNothing on your side
charge cannot be attested: product is not activeYou deactivated the product between the buyer's transfer and its attestationThe buyer's USDC is already in the router and cannot be split. It needs manual recovery by an admin
charge cannot be attested: merchant is not activeYour merchant flag was cleared, or you were suspended, in the same windowThe same
A split reverts after attestationAny settlement-time revert on this railThe funds stay pooled in the router until an admin recovers them. They are not returned automatically

The two bold rows are the reason to keep a product active while charges are in flight. On the x402 rail an inactive product simply fails the payment and nothing moves. Here the buyer has already paid and you have already served, so deactivating turns a completed sale into stranded money that needs a human. Deactivate on a quiet endpoint, not on a busy one. A replay of an already-attested charge stays a success even so: deactivation cannot retroactively invalidate a charge that went through.

Next steps