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

# Create a MIP20 token

> Token creation and ownership through MIP20Factory

> [!NOTE]
>
> **What you'll be able to do**
>
> * Create a MIP20 token on Metal Testnet.
> * Verify its address, owner, initial supply, and the initial holder's balance.

`MIP20Factory.deployMIP20` creates a token with the name, symbol, decimals,
initial supply and initial holder you choose. The account that calls it is the
MIP20 deployer. It becomes the token owner and can create additional supply
through the token's [`mint` function](https://docs.metalntwx.com/reference/assets-and-payments/mip20#factory-deployed-metaltoken).

This guide creates 100 units of a test token with six decimals, then verifies
the token configuration and delivery to its initial holder.

## Prepare the creation request

| Input | Source and purpose |
| - | - |
| `METAL_RPC_URL` | The full testnet endpoint URL from the [console setup](https://docs.metalntwx.com/start/connect-to-testnet.md#2-create-an-rpc-endpoint) |
| `PRIVATE_KEY` | A dedicated test account's 32-byte, `0x`-prefixed private key. This account becomes the token owner |
| `RECIPIENT_ADDRESS` | The intended initial holder of the supply. Use a test account you control |
| Name, symbol, decimals, supply | Application token configuration. The test token uses `Integration Test Token`, `ITT`, 6 decimals, and 100 tokens |

The factory address is `0x0000000000000000000000000000000000000f4c`, and the
`*USDZ` address is `0x000000000000000000000000000000000000b5d5`. Both are listed
in [Contract Addresses](https://docs.metalntwx.com/reference/contract-addresses.md).

Install the public packages in the application directory. For a new project,
run `pnpm init` first:

```bash theme={null}
pnpm add viem@2.57.7 @metalntwx/metal-ts@0.3.2
pnpm add -D typescript@6 tsx@4 @types/node@22 --allow-build=esbuild
```

The build allowance enables the `esbuild` dependency used by `tsx`.

## Define the creation inputs

Set the initial supply and define the factory interface and arguments. The
code uses `client` from [Connect to Metal](https://docs.metalntwx.com/build/prepare/connect-to-metal.md)
and `account` and `wallet` from
[Estimate and record fees](https://docs.metalntwx.com/build/prepare/estimate-and-record-fees.md):

```typescript theme={null}
import { erc20Abi, getAddress, parseAbi, parseEventLogs, parseUnits, zeroAddress } from "viem";
import { signMip20Contract } from "@metalntwx/metal-ts";

const publicClient = client;
const walletClient = wallet;
const initialHolder = getAddress(process.env.RECIPIENT_ADDRESS!);
if (initialHolder === zeroAddress) throw new Error("Choose a nonzero initial holder.");
const factory = "0x0000000000000000000000000000000000000f4c" as const;
const gasToken = "0x000000000000000000000000000000000000b5d5" as const;
const factoryAbi = parseAbi([
  "function deployMIP20(string name_, string symbol_, uint8 decimals_, uint256 initialSupply, address initialHolder) returns (address token)",
  "function isMIP20(address token) view returns (bool)",
  "event MIP20Deployed(address indexed token, address indexed deployer)",
]);
const name = "Integration Test Token";
const symbol = "ITT";
const decimals = 6;
const initialSupply = parseUnits("100", decimals);
const args = [name, symbol, decimals, initialSupply, initialHolder] as const;
```

With six decimals, the initial supply of 100 tokens is `100000000` base units.
The mint credits the initial holder if its receive policy accepts the tokens.
If the policy refuses them, the entire initial supply goes to quarantine,
including when the refusal mode is `REVERT`. See
[ReceivePolicy](https://docs.metalntwx.com/reference/compliance/receive-policy.md) for recipient rules and recovery.

## Call the factory

Follow [Estimate and record fees](https://docs.metalntwx.com/build/prepare/estimate-and-record-fees.md) to
estimate `deployMIP20` with the arguments above. Omit fee fields during
estimation and check that the signing account's balance in the gas token covers
the maximum gas cost. Sign the factory call with the resulting `gasLimit` and
`maxFeePerGas`:

```typescript theme={null}
const signed = await signMip20Contract({
  publicClient, walletClient, address: factory, abi: factoryAbi,
  functionName: "deployMIP20", args, gasToken, maxFeePerGas,
  gasLimit, gasFallback: gasLimit,
});
```

Save `signed.txHash`, `signed.raw`, and the requested configuration before
broadcasting with `await signed.send()`. If the submission result is uncertain,
investigate that saved transaction before signing again. A new factory call
would create another token.

## Extract the created address

Read the transaction receipt by the saved hash and check that execution
succeeded. Extract the token address from `MIP20Deployed`, checking that the
factory emitted it and that its deployer matches the signing account:

```typescript theme={null}
const receipt = await publicClient.waitForTransactionReceipt({
  hash: signed.txHash, timeout: 60_000,
});
if (receipt.status !== "success") throw new Error("Creation reverted.");
const deployments = parseEventLogs({
  abi: factoryAbi, eventName: "MIP20Deployed",
  logs: receipt.logs.filter((log) => log.address.toLowerCase() === factory),
});
if (deployments.length !== 1) throw new Error("Expected one deployment event.");
const { token, deployer } = deployments[0].args;
if (deployer.toLowerCase() !== account.address.toLowerCase()) {
  throw new Error("Unexpected deployer.");
}
```

## Verify and save the configuration

Confirm [finality and the canonical receipt block](https://docs.metalntwx.com/reference/network-fundamentals/settlement.md).
Read the token state at `receipt.blockNumber` to check the configuration at the
finalized creation block. Start with the initial supply:

```typescript theme={null}
const supply = await publicClient.readContract({
  address: token, abi: erc20Abi, functionName: "totalSupply",
  blockNumber: receipt.blockNumber,
});
if (supply !== initialSupply) throw new Error("Unexpected initial supply.");
```

Before saving the token as verified, check every field:

| Read at the creation block | Expected result |
| - | - |
| Code at the created address | Nonempty bytecode |
| Factory `isMIP20(token)` | `true` |
| Token `name()`, `symbol()`, `decimals()` | Requested name, symbol, and precision |
| Token `totalSupply()` | Requested supply in base units |
| Token `balanceOf(initialHolder)` | Requested initial supply for direct delivery, or zero with a matching quarantine receipt |
| Token `owner()` | The signing account |

Add `owner()` to the standard ERC-20 ABI:

```typescript theme={null}
const tokenAbi = [...erc20Abi, ...parseAbi([
  "function owner() view returns (address)",
])] as const;
const owner = await publicClient.readContract({
  address: token, abi: tokenAbi, functionName: "owner",
  blockNumber: receipt.blockNumber,
});
if (owner.toLowerCase() !== account.address.toLowerCase()) {
  throw new Error("Unexpected token owner.");
}
```

Check the table's remaining getters with the same `readContract` pattern.
For `isMIP20`, call the factory with `args: [token]`. For `balanceOf`, call
the token with `args: [initialHolder]`. Read the bytecode with
`publicClient.getCode({ address: token, blockNumber: receipt.blockNumber })`.
Stop if the metadata, owner, MIP20 status or supply differs from the request.

Verify delivery by matching the token's
`Transfer(zeroAddress, initialHolder, initialSupply)` event and the initial
holder's balance. If the mint went to quarantine, require a matching
[quarantine receipt](https://docs.metalntwx.com/reference/compliance/receive-policy.md#read-and-recover-a-quarantined-credit)
for this token, the zero-address originator, intended initial holder and full
supply. Save the complete receipt, including its recovery authority, and recover
that supply through ReceivePolicy.

Save the chain ID, token address, decimals, owner, initial holder, requested
configuration, deployment transaction hash and delivery or quarantine status in
the deployment record. Retain the created address as soon as the deployment
event is verified, so an interrupted delivery check can resume from that address
and hash.

## Recover a pending creation

If submission or receipt polling times out, look up the saved transaction with
`publicClient.waitForTransactionReceipt({ hash: savedHash })`. Once its receipt
is available, repeat address extraction and verification against the original
signing account, initial holder, and token configuration.

Check the endpoint and saved transaction's status before rebroadcasting. To
submit the same signed transaction again, use
`publicClient.sendRawTransaction({ serializedTransaction: savedRaw })`.
The [TypeScript SDK](https://docs.metalntwx.com/reference/typescript-sdk.md#signmip20contract) describes this
signing and submission procedure.

If estimation fails, check the signing account's balance in the gas token and the
requested arguments. If execution reverts, creation rolls back and a
transaction fee can still be charged.

## Mint additional supply

The factory-deployed token exposes `mint(address to, uint256 amount)`.
Its current owner can call this function on the token address to increase
the supply. Convert the amount using the token's decimals, estimate and pay gas,
then verify the receipt, finality and resulting supply increase. Match either
the destination credit or its quarantine receipt. If the mint was quarantined,
recover the existing supply through ReceivePolicy using that receipt.

The owner can transfer ownership using `transferOwnership(address newOwner)`.
The new owner then controls minting. Verify the resulting
`owner()` value before updating the application's authority records.


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