> ## Documentation Index
> Fetch the complete documentation index at: https://docs.metalntwx.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Route incoming payments

> Collect and forward a payment through an explicit application call

> [!NOTE]
>
> **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](https://docs.metalntwx.com/build/payments/direct-payment.md#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](https://docs.metalntwx.com/reference/compliance/receive-policy.md)
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](https://docs.metalntwx.com/build/contracts/deploy-an-application-contract.md).

```solidity theme={null}
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.31;

interface RoutingToken {
    function balanceOf(address account) external view returns (uint256);
    function transfer(address to, uint256 amount) external returns (bool);
    function transferFrom(address from, address to, uint256 amount) external returns (bool);
}

contract PaymentRouter {
    RoutingToken public immutable token;
    address public immutable sender;
    address public immutable treasury;
    uint256 public immutable maximumAmount;
    uint256 public routedTotal;
    mapping(bytes32 => bool) public paid;
    bool private entered;

    event Routed(bytes32 indexed paymentId, address indexed sender,
        address indexed treasury, uint256 amount);
    event Recovered(uint256 amount);
    error InvalidConfiguration();
    error Unauthorized();
    error InvalidPayment();
    error AlreadyPaid();
    error Reentered();
    error TransferFailed();
    error InexactTransfer();

    modifier onlySender() {
        if (msg.sender != sender) revert Unauthorized();
        _;
    }

    modifier guarded() {
        if (entered) revert Reentered();
        entered = true;
        _;
        entered = false;
    }

    constructor(address token_, address sender_, address treasury_, uint256 maximum_) {
        if (token_.code.length == 0 || sender_ == address(0)
            || sender_ == address(this) || treasury_ == address(0)
            || treasury_ == address(this) || treasury_ == sender_
            || maximum_ == 0) revert InvalidConfiguration();
        token = RoutingToken(token_);
        sender = sender_;
        treasury = treasury_;
        maximumAmount = maximum_;
    }

    function route(bytes32 paymentId, uint256 amount) external onlySender guarded {
        if (paymentId == bytes32(0) || amount == 0 || amount > maximumAmount) {
            revert InvalidPayment();
        }
        if (paid[paymentId]) revert AlreadyPaid();
        paid[paymentId] = true;
        uint256 heldBefore = token.balanceOf(address(this));
        if (!token.transferFrom(sender, address(this), amount)) revert TransferFailed();
        if (token.balanceOf(address(this)) != heldBefore + amount) revert InexactTransfer();
        uint256 treasuryBefore = token.balanceOf(treasury);
        if (!token.transfer(treasury, amount)) revert TransferFailed();
        if (token.balanceOf(treasury) != treasuryBefore + amount
            || token.balanceOf(address(this)) != heldBefore) revert InexactTransfer();
        routedTotal += amount;
        emit Routed(paymentId, sender, treasury, amount);
    }

    function recoverUnrouted() external onlySender guarded {
        uint256 amount = token.balanceOf(address(this));
        if (amount == 0) revert InvalidPayment();
        uint256 senderBefore = token.balanceOf(sender);
        if (!token.transfer(sender, amount)) revert TransferFailed();
        if (token.balanceOf(address(this)) != 0
            || token.balanceOf(sender) != senderBefore + amount) revert InexactTransfer();
        emit Recovered(amount);
    }
}
```

The balance checks reject a transfer redirected to
[quarantine](https://docs.metalntwx.com/reference/compliance/receive-policy.md#quarantine-and-receipts), 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:

| Case | Required result |
| - | - |
| Authorized sender and amount within the maximum | Exact sender-to-router and router-to-treasury transfers, one `Routed` event, ID paid and total increased |
| Another caller invokes `route` or recovery | Reverts without changing balances, allowance or payment state |
| Zero ID, zero amount or amount above the maximum | Reverts before collecting tokens |
| Reuse a successful payment ID, including with a different amount | Reverts without a second transfer |
| Insufficient allowance or sender balance | Reverts with ID unpaid and total unchanged |
| Treasury refuses in `REVERT` mode | Both transfers and every router state change roll back |
| Treasury refuses in `QUARANTINE` mode | Balance check reverts, including the quarantine receipt and escrow credit |
| Token redirects the incoming collection away from the router | Balance check reverts without a paid ID |
| Sender makes an ordinary transfer to the router | No forwarding or `Routed` event. Recovery returns it to the sender without completing a payment |
| Sender refuses recovery, by revert or quarantine | Recovery reverts and the router retains its balance until the sender accepts |
| Token attempts a reentrant route or recovery call | `onlySender` rejects it with `Unauthorized` before the reentrancy guard runs, so no second payment is made |

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](https://docs.metalntwx.com/reference/assets-and-payments/mip20.md#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](https://docs.metalntwx.com/build/prepare/estimate-and-record-fees.md) to obtain
`gasLimit`, and use the quoted `price` as `maxFeePerGas`.

```typescript theme={null}
import { parseAbi } from "viem"
import { signMip20Contract } from "@metalntwx/metal-ts"

const routerAbi = parseAbi([
  "function route(bytes32 paymentId, uint256 amount)",
  "function paid(bytes32 paymentId) view returns (bool)",
  "event Routed(bytes32 indexed paymentId, address indexed sender, address indexed treasury, uint256 amount)",
])
const call = { address: routerAddress, abi: routerAbi,
  functionName: "route", args: [paymentId, amount] } as const
await client.simulateContract({ ...call, account: wallet.account })
const signed = await signMip20Contract({
  publicClient: client, walletClient: wallet, ...call,
  gasToken: "0x000000000000000000000000000000000000b5d5",
  gasLimit, gasFallback: gasLimit, 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](https://docs.metalntwx.com/build/payments/verify-and-reconcile-payments.md#save-outgoing-attempts-before-submission)
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](https://docs.metalntwx.com/reference/network-fundamentals/settlement.md). Apply the business result once using the
[durable payment procedure](https://docs.metalntwx.com/build/payments/verify-and-reconcile-payments.md).
`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.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.