> ## 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 and manage a smart account

> Create a smart account, execute approved calls through a relayer, and update its signers

> [!NOTE]
>
> **What you'll be able to do**
>
> * Create and fund a smart account.
> * Approve and execute transactions with one or multiple signers.
> * Update the account's signers and approval requirements.

MetalAccount is Metal's smart account. It holds tokens at its own address and
executes calls approved by its signers. The calls, nonce and expiry form an
operation. Each signer has a weight. This guide uses operations approved by
signatures whose combined weight meets the account's approval threshold. Any
relayer may submit the approved operation in a transaction and pay its fee.

This guide uses ordinary EVM signing accounts in two configurations: a single
signer with weight one and threshold one, or three signers with weight one and
threshold two. The second configuration requires any two distinct signers to
approve an operation.

## Configure the factory and signers

Install public packages with `pnpm add @metalntwx/metal-ts@0.3.2 viem@2.57.7`.
Use `relayerWallet` to submit transactions and pay gas. The wallet clients
`walletA`, `walletB` and `walletC` represent the three signers that approve
operations.

Keep the signers' keys separate and request each signature independently.
Maintain access to enough signers to meet the approval threshold. Before
discarding a temporary test signer, use the current signers to approve its
removal and set a threshold the remaining signers can meet. Verify that change
at finality before discarding the key.

On Metal Testnet, the factory recorded by `@metalntwx/metal-pay@0.3.4` is
`0x3dbED5DebA965A639bB0Fd40BE96b97922070FA8`. Verify factory code and
`implementation()` against the current network configuration before
creating an account.

```typescript theme={null}
import { encodeAbiParameters, encodeFunctionData, erc20Abi,
  getAddress, hashTypedData, parseAbi, type Hex } from "viem"

const factory = "0x3dbED5DebA965A639bB0Fd40BE96b97922070FA8" as const
const factoryAbi = parseAbi([
  "function implementation() view returns (address)",
  "function predictAccount(bytes[] signers, uint64[] weights, uint64 threshold) view returns (address)",
  "function createAccount(bytes[] signers, uint64[] weights, uint64 threshold) returns (address)",
  "event AccountCreated(address indexed account, bytes32 indexed salt)",
])
const accountAbi = parseAbi([
  "function getSignerCount() view returns (uint256)",
  "function getSigners(uint64 start, uint64 end) view returns (bytes[])",
  "function isSigner(bytes signer) view returns (bool)",
  "function signerWeight(bytes signer) view returns (uint64)",
  "function threshold() view returns (uint64)",
  "function nonces(address owner, uint192 key) view returns (uint256)",
  "function supportsExecutionMode(bytes32 mode) view returns (bool)",
  "function executeDigest(bytes32 mode, bytes executionData, uint256 nonce, uint64 validUntil) view returns (bytes32)",
  "function executeSigned(bytes32 mode, bytes executionData, uint256 nonce, uint64 validUntil, bytes signature)",
  "function addSigners(bytes[] signers)",
  "function removeSigners(bytes[] signers)",
  "function setSignerWeights(bytes[] signers, uint64[] weights)",
  "function setThreshold(uint64 threshold)",
  "event Executed(uint256 indexed nonce)",
])
if (await client.getChainId() !== 63825) throw new Error("Wrong network")
if (!await client.getCode({ address: factory })) throw new Error("Factory absent")
const implementation = await client.readContract({
  address: factory, abi: factoryAbi, functionName: "implementation",
})
if (!await client.getCode({ address: implementation })) throw new Error("Implementation absent")
const signers = [walletA, walletB, walletC].map(wallet => {
  if (!wallet.account) throw new Error("Configure each wallet client with its signing account")
  return getAddress(wallet.account.address) as Hex
})
if (new Set(signers.map(s => s.toLowerCase())).size !== 3) {
  throw new Error("Approval signers must be distinct")
}
const weights = [1n, 1n, 1n]
const threshold = 2n
const accountAddress = await client.readContract({
  address: factory, abi: factoryAbi, functionName: "predictAccount",
  args: [signers, weights, threshold],
})
```

Each ordinary signer is encoded as its 20-byte address. The factory uses the
initial signer array, including its order, to derive the account address. Save
the exact initial configuration and predicted address.

For a single-signer account, use `[signers[0]]`, `[1n]` and `1n` instead. Every
weight must be positive and the threshold must be reachable. Factory creation
is permissionless and idempotent for the exact configuration. An existing
account may have changed its signers since creation, so always inspect its
current configuration.

## Create and fund the account

Use the relayer to call `createAccount(signers, weights, threshold)`. Simulate
and estimate this call using [Estimate and record fees](https://docs.metalntwx.com/build/prepare/estimate-and-record-fees.md).
Sign it with `signMip20Contract` as in
[Prepare a transfer](https://docs.metalntwx.com/build/payments/direct-payment.md#prepare-a-transfer),
substituting the factory address, ABI, function and arguments above.
[Save the signed attempt](https://docs.metalntwx.com/build/payments/verify-and-reconcile-payments.md#save-outgoing-attempts-before-submission)
before sending it.

Verify successful execution, the receipt's block hash and finality. For a new
account, `AccountCreated` must name the predicted address. If the account
already existed, the factory returns its address without emitting another
creation event. Verify code at that address, `getSignerCount()`,
`getSigners(0, count)`, each `signerWeight` and `threshold()` against the
intended current configuration.

Fund `accountAddress` with a small MIP20 transfer using
[Direct payment](https://docs.metalntwx.com/build/payments/direct-payment.md). The MetalAccount
supplies the payment amount for its outgoing calls. Fund the relayer's signing
account separately so it can pay transaction fees from its balance in the gas
token.

## Build the operation

This operation transfers `amount` of the MIP20 `token` from the MetalAccount to
`recipient`. Verify the token address and convert the amount to base units using
the direct-payment guide. Check the MetalAccount's spendable balance before
requesting approval.

```typescript theme={null}
const mode = `0x01${"00".repeat(31)}` as Hex
const executionData = encodeAbiParameters([
  { type: "tuple[]", components: [
    { name: "target", type: "address" },
    { name: "value", type: "uint256" },
    { name: "callData", type: "bytes" },
  ] },
], [[{
  target: token,
  value: 0n,
  callData: encodeFunctionData({ abi: erc20Abi,
    functionName: "transfer", args: [recipient, amount] }),
}]])
if (!await client.readContract({ address: accountAddress, abi: accountAbi,
  functionName: "supportsExecutionMode", args: [mode] })) {
  throw new Error("Unsupported execution mode")
}
const nonce = await client.readContract({
  address: accountAddress, abi: accountAbi, functionName: "nonces",
  args: [accountAddress, 0n],
})
```

The mode is ERC-7821 batch execution with default revert behavior. Each call's
native `value` is zero because the MIP20 function transfers the token amount.
A nonce combines a 192-bit key in the high bits and a 64-bit sequence. The call
above reads the nonce for key zero. Coordinate operation preparation for each
key so requests receive distinct sequence numbers.

## Collect approvals and submit

The EIP-712 domain uses the MetalAccount as `verifyingContract`. Review the
target, decoded calldata, amount, recipient and nonce before requesting
signatures. Compare the locally computed digest with the account's
`executeDigest`.

```typescript theme={null}
const validUntil = (await client.getBlock()).timestamp + 3600n
const approval = {
  domain: { name: "MetalAccount", version: "1", chainId: 63825,
    verifyingContract: accountAddress },
  types: { Execute: [
    { name: "mode", type: "bytes32" },
    { name: "executionData", type: "bytes" },
    { name: "nonce", type: "uint256" },
    { name: "validUntil", type: "uint64" },
  ] },
  primaryType: "Execute",
  message: { mode, executionData, nonce, validUntil },
} as const
const digest = await client.readContract({ address: accountAddress,
  abi: accountAbi, functionName: "executeDigest", args: [mode, executionData, nonce, validUntil] })
if (hashTypedData(approval) !== digest) throw new Error("Approval digest mismatch")
const signatures = await Promise.all([walletA, walletB].map(wallet =>
  wallet.signTypedData({ ...approval, account: wallet.account! })))
const quorumSignature = encodeAbiParameters([
  { type: "bytes[]" }, { type: "bytes[]" },
], [[signers[0], signers[1]], signatures])
const operation = {
  address: accountAddress, abi: accountAbi, functionName: "executeSigned",
  args: [mode, executionData, nonce, validUntil, quorumSignature],
} as const
await client.simulateContract({ ...operation, account: relayerWallet.account })
```

`validUntil` is the latest block timestamp at which the approval can execute.
This example allows one hour. Save the deadline with the operation; extending
it requires new signatures.

For the single-signer configuration, encode that signer's address and signature.
Each signer contributes its weight once. To use other ERC-7913 signer types,
provide their verifier and signature encoding. A contract signer requires
ERC-1271 signature validation, including ERC-7739 domain binding when the signer
is another MetalAccount.

Estimate `operation` for the relayer, sign the outer transaction with
`signMip20Contract`, and save its bytes and hash before submission. Save the
operation bytes, nonce, deadline, approvals and expected target effect in the same durable
operation record. A changed operation requires fresh approvals.

Require `Executed(nonce)` from the MetalAccount and verify the expected result
of its calls. For this transfer, verify the token transfer from `accountAddress`
to `recipient`, the receipt's block hash and finality using
[Verify a finalized transfer](https://docs.metalntwx.com/build/payments/verify-and-reconcile-payments.md#verify-a-finalized-transfer).
Read the nonce again and require the sequence to advance once.

## Change the signers and threshold

Administration functions accept calls only from the account itself. Encode an
ERC-7821 batch whose `target` is `accountAddress` and whose calldata calls
`addSigners`, `removeSigners`, `setSignerWeights` or `setThreshold`. Approve and
submit that batch through the same `executeSigned` procedure with the required
signing weight under the account's current threshold and a fresh nonce.

To replace one signer in a two-of-three account, batch `addSigners([newSigner])`
followed by `removeSigners([oldSigner])`. A newly added signer has weight one.
Keep the threshold at two. To change weights, call `setSignerWeights` before
raising the threshold. Lower the threshold before a removal that would make
it unreachable. All calls in the batch revert together if any step fails.

After finality, read back the signer list, weights and threshold. Confirm that
the replacement can approve a new operation and the removed signer no longer
contributes weight. Signer changes preserve the account's address and funds.

## Schedule submission

An approved `executeSigned` operation runs when its transaction is included.
To submit it later, use the network
[contract scheduler](https://docs.metalntwx.com/build/contracts/schedule-a-contract-call.md), on a network
that has one; Metal Testnet does not have the scheduler yet. Set the
approval's `validUntil` to cover the intended execution window and reconcile
its nonce before submitting the same operation through another path.

## Test and recover

Simulate duplicate signatures, insufficient signing weight, changed calldata,
wrong domain, an expired approval and a direct administrative call from the relayer. They must fail
without changing the target's state. Replaying an executed nonce must also
fail. Verify both the one-signer and two-of-three configurations before changing
signer weights or approval thresholds.

After an uncertain submission, use the saved outer transaction hash and account
nonce to investigate the original operation. If the nonce has advanced, find
its execution and verify the intended result before authorizing another
payment. Follow
[Recover an uncertain submission](https://docs.metalntwx.com/build/payments/verify-and-reconcile-payments.md#recover-an-uncertain-submission)
to resolve the saved attempt.


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