Skip to main content
What you’ll be able to do
  • Accept or reject a payment using application rules.
  • Forward an accepted payment to its configured treasury in the same transaction.
  • Recover an accidental direct transfer without recording a routed payment.
An application can route a payment to one destination or split it among several recipients. This guide routes each payment to a treasury in one contract call. The sender approves the router to spend the payment amount, then calls route. The router checks the sender, amount and payment ID, collects the tokens, and forwards them to the treasury in one transaction. If either transfer fails to deliver the full amount, the entire call reverts.

Prepare the route

Choose a sender account you control and a treasury address different from the sender’s. Verify the token address and decimals using Read metadata. Convert the payment amount and the maximum permitted amount to the token’s base units. The target network must expose the ReceivePolicy genesis contract at 0x0000000000000000000000000000000000000f4d and the current MIP20 interfaces. Check nonempty code at that address before testing these policy outcomes. Also, the treasury’s receive policy must accept the router as the sender. The incoming transfer names the original sender, but the forwarded transfer names the router. Check both policies before submitting.

Implement the router

PaymentRouter collects the payment and forwards it to the configured treasury. The application assigns a nonzero bytes32 payment ID and stores it with the amount before asking the sender to submit. The router records completed IDs so each ID can be processed once. Use the Solidity compiler, Foundry project and deployment procedure from Deploy an application contract.
The balance checks reject a transfer redirected to quarantine, even though the token returns true. Reverting the router call also removes the quarantine receipt and restores the allowance, balances and payment ID. A treasury refusal in REVERT mode propagates its error and has the same rollback effect. This router serves one sender. recoverUnrouted returns any balance in the configured token to that sender without increasing routedTotal or marking an ID paid. Other people must not use this contract as a deposit account because it keeps no depositor ledger. It cannot recover unrelated tokens. Retain the sender’s signing access for as long as this router may hold funds.

Test and deploy

Run in-process Foundry tests before deploying. A local token fixture must model ordinary ERC-20 transfers, allowance consumption, policy refusal in REVERT mode and successful quarantine to a separate escrow balance. It must not call recipient code during a transfer. Test these observable outcomes: Deploy PaymentRouter with the verified token, sender, treasury and maximum amount using the deployment guide. Verify its code, constructor getters and canonical deployment finality. Save the source, ABI, compiler settings, address and transaction hash in the deployment record. Local fixture results do not establish that the deployed Metal route works.

Approve and route a payment

From the configured sender’s signing account, approve the router for the exact amount using Approve a spender. Save and verify that separate transaction before collecting. The router grants no token approvals of its own. Load routerAddress, paymentId and the base-unit amount from the saved payment instruction. Use the sender’s signing account through wallet. Read sender, token, treasury, maximumAmount and paid(paymentId) first to check the deployment and reconcile an ID that already succeeded. Estimate this exact route(paymentId, amount) call through Estimate and record fees to obtain gasLimit, and use the quoted price as maxFeePerGas.
The signing account’s balance in the gas token pays the transaction fee. This transaction uses USDZ as its gas token, so the sender needs that balance in addition to the payment amount. Save the signed attempt before calling await signed.send().

Verify and recover

Require successful execution and all three events from their expected contracts:
  • The token’s Transfer(sender, routerAddress, amount).
  • The token’s Transfer(routerAddress, treasury, amount).
  • The router’s Routed(paymentId, sender, treasury, amount).
Match the saved call and payment ID, then check the transaction receipt’s canonical block and finality. Apply the business result once using the durable payment procedure. paid(paymentId) prevents replay onchain but does not replace event verification or the application’s own record. A reverted route leaves the ID unused and the payment amount with the sender. Record the transaction fee and correct the failed rule before retrying the same payment. For an uncertain submission, recover the saved hash before sending again. If an ordinary transfer was sent by mistake, reconcile its finalized token event and call recoverUnrouted() from the configured sender. Verify Recovered, the token transfer back to the sender and finality. Ensure the sender’s receive policy accepts the router first.