> ## 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.

# Direct payment

> Read token balances and send a MIP20 payment

> [!NOTE]
>
> **What you'll be able to do**
>
> * Read an account's MIP20 token balance.
> * Send a direct payment to a recipient.

A token contract is a program on Metal that records how much of a particular
token each account holds. For example, the `*USDZ` contract keeps track of
accounts' `*USDZ` balances. Call this contract to check balances
and transfer tokens.

For a single demonstration transfer, start with
[Make Your First Transfer](https://docs.metalntwx.com/start/make-your-first-transfer.md).
To deploy a new MIP20 token, use the [MIP20 Factory](https://docs.metalntwx.com/build/contracts/create-a-mip20-token.md).

## Choose the token and contract call

Obtain the token's network and contract address from its issuer and save both
in the token configuration. Reads and transfers use that contract address.
Different tokens can share a name or symbol, so those labels alone cannot
identify the token.

Before using the contract, call `isMIP20(tokenAddress)` on `MIP20Factory`
to check that Metal recognises it as a MIP20 token.

> [!NOTE]
>
> MIP20 status does not establish who issued the token. Verify the issuer's
> identity separately.

On testnet, `*USDZ`'s contract is at `0x000000000000000000000000000000000000b5d5` and has 18
decimals. Read `decimals()` for each configured token instead of assuming all
tokens use the same precision.

| Contract call | Application input | Result | Signature and onchain gas |
| - | - | - | - |
| `name`, `symbol`, `decimals` | Token address | Name, symbol, and decimals | No |
| `balanceOf` | Account address | Spendable balance in base units | No |
| `transfer` | Recipient and amount | A transaction transferring the signing account's tokens | Yes |

Read requests consume endpoint RPC credit. They do not submit transactions or
incur transaction fees. `balanceOf` excludes tokens reserved in MIP20 holds.
See the [MIP20 reference](https://docs.metalntwx.com/reference/assets-and-payments/mip20.md) for hold behavior.

Install the public packages used below:

```bash theme={null}
pnpm add @metalntwx/metal-ts@0.3.2 viem@2.57.7
```

## Read metadata

Check that Metal recognises the token as MIP20 and read its metadata.
The code uses testnet `*USDZ`. For another token, substitute its verified
address. These reads require no private key.

```typescript theme={null}
import { erc20Abi, getAddress, maxUint256, parseAbi, parseUnits } from "viem";

const token = "0x000000000000000000000000000000000000b5d5" as const;
const factory = "0x0000000000000000000000000000000000000f4c" as const;
const factoryAbi = parseAbi(["function isMIP20(address token) view returns (bool)"]);
if (!await client.readContract({
  address: factory, abi: factoryAbi, functionName: "isMIP20", args: [token],
})) throw new Error("Token is not MIP20.");
const [name, symbol, decimals] = await Promise.all([
  client.readContract({ address: token, abi: erc20Abi, functionName: "name" }),
  client.readContract({ address: token, abi: erc20Abi, functionName: "symbol" }),
  client.readContract({ address: token, abi: erc20Abi, functionName: "decimals" }),
]);
```

Save the chain ID, address, and decimals with the token configuration. Use the
name and symbol for display after checking the issuer independently.

## Read an account balance

Supply `accountAddress` from the application's account record as a full EVM
address. The account can be read without access to its signing key.

```typescript theme={null}
const holder = getAddress(accountAddress);
const balance = await client.readContract({
  address: token, abi: erc20Abi, functionName: "balanceOf", args: [holder],
});
```

The result is a `bigint` in base units. Convert it to a display string with
viem's `formatUnits(balance, decimals)`. Refresh the balance before spending
because other transactions may have changed it.

## Convert a payment amount

Supply `amountText` as a decimal string from the payment instruction. Reject
excess precision before conversion so it cannot be silently rounded.

```typescript theme={null}
if (!/^(0|[1-9]\d*)(\.\d+)?$/.test(amountText)
  || (amountText.split(".")[1]?.length ?? 0) > decimals) {
  throw new Error("Invalid amount or excess decimal precision.");
}
const amount = parseUnits(amountText, decimals);
if (amount <= 0n || amount > maxUint256) throw new Error("Invalid transfer amount.");
```

Keep amounts as integers during calculation and serialize them as decimal
strings. This transfer requires a positive amount.

## Prepare a transfer

Confirm that `wallet`'s signing account matches `holder`. Set `recipientAddress`
from the validated payment instruction.

Check the deployment and screen this exact token, sender, recipient and amount
using [Receive policy](https://docs.metalntwx.com/reference/compliance/receive-policy.md). Require the documented
ReceivePolicy contract and compatible token deployment before relying on these
rules. Screening is a preflight check, as policy can change before execution.

Follow [Estimate and record fees](https://docs.metalntwx.com/build/prepare/estimate-and-record-fees.md) with
this token, recipient, and amount. Use `holder` as `sender` and the quoted `price` as
`maxFeePerGas`. Before signing, check the sender's balance in the token being transferred and
the signing account's balance in the gas token. This transaction uses `*USDZ`
as its gas token.

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

const recipient = getAddress(recipientAddress);
// This example sends USDZ and pays gas in USDZ.
// balance, amount, and maximumFee are all in USDZ base units.
// If you send another token, check its balance against amount and
// your USDZ balance against maximumFee separately.
if (balance < amount + maximumFee) throw new Error("Insufficient amount plus gas.");
const signed = await signMip20Contract({
  publicClient: client, walletClient: wallet,
  address: token, abi: erc20Abi, functionName: "transfer",
  args: [recipient, amount],
  gasToken: "0x000000000000000000000000000000000000b5d5",
  gasLimit, gasFallback: gasLimit, maxFeePerGas,
});
```

The transaction calls `transfer` on the token contract with the recipient and
amount as arguments. Sending tokens from the signing account requires no
[spending approval](https://docs.metalntwx.com/reference/assets-and-payments/mip20.md#approve-a-spender).
Save `signed.txHash` and `signed.raw` before calling `await signed.send()`.
Verify the successful transaction receipt's `Transfer` event against the token, holder,
recipient, and amount. Confirm finality before crediting the payment.

## Verify results and handle failures

Use [Verify and reconcile payments](https://docs.metalntwx.com/build/payments/verify-and-reconcile-payments.md) to
check execution, finality, and the expected transfer before crediting another
system. The guide also covers preventing duplicate credit.

A recipient's receive policy can refuse a credit. In the default `REVERT` mode,
the transfer reverts. With `QUARANTINE` enabled, execution can succeed while the
tokens move to ReceivePolicy quarantine instead of the recipient. Require the exact
recipient `Transfer` before marking the payment paid. If the transaction receipt
contains a matching [quarantine receipt](https://docs.metalntwx.com/reference/compliance/receive-policy.md), save the
full tuple and recovery record, leave the payment unpaid, and reconcile the
claim before authorizing another payment. A balance difference or successful
transaction receipt alone cannot identify the intended payment.

| Failure | Application behavior |
| - | - |
| Invalid address, malformed amount, or excess precision | Reject the input before signing. Test negative amounts, exponential notation, zero amounts, and excess decimal precision. |
| Wrong network or address that is not a MIP20 token | Stop and correct the token configuration. |
| Insufficient balance in the token being transferred or in the gas token | Obtain test funding or reject the transaction. |
| Simulation or estimation fails | Inspect the call and policy requirements. Do not bypass the failure with an arbitrary gas limit. |
| Signer refuses the request | Leave the transaction unsigned. |
| Send response or receipt wait fails | Keep the saved hash and investigate its receipt and account nonce. Do not create a second payment automatically. |
| Reverted transaction | Record failed execution and the transaction fee. Investigate the contract or policy rejection. |
| Finalized transfer to ReceivePolicy quarantine | Save the quarantine receipt. Do not credit the intended recipient or automatically send again. |

Check that an amount larger than the available balance is rejected before
submission.

For applications with interactive approval, use a controlled signer that rejects
a request to test refusal. The signer configured with the test account's private
key signs automatically.

Serialize transaction preparation and submission for each signing account to
prevent concurrent transactions from using the same nonce.


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