Create a EOA or SCW signer
XMTP SDKs support message signing with 2 different types of Ethereum accounts: Externally Owned Accounts (EOAs) and ERC-1271 compatible Smart Contract Wallets (SCWs).
Smart contract wallets have addresses that are unique to the chain on which they are deployed, while EOAs share the same address across multiple EVM-compatible chains.
All XMTP clients require a signer object (or instance) that provides a method for signing messages on behalf of the account.
Wallet comparison
| Feature | Wallet (EOA) | Smart Wallet (SCW) |
|---|---|---|
| On-chain form | Native account | Smart contract |
| Authentication | ECDSA private key | Programmable (e.g. Passkeys) |
| Recovery | Recovery / Seed phrase | Programmable (multisig, trusted guardians) |
| Gas payment | User pays native gas | Relayer (e.g. Base) can pay |
| Security model | Single key | Policy-based, multi-layer security |
How do I know if I have a SCW?
Your wallet is most likely a smart contract wallet if it shows several of these traits:
- It lets you sign in with email, social logins, or passkeys rather than a seed phrase
- It never reveals a private key or recovery phrase
- Transaction fees are sponsored or relayed (you can transact without holding native gas tokens)
- A block explorer for the relevant chain (e.g., Basescan, Abstract Explorer) displays a "Contract" tab for your wallet address
The most reliable way to verify is by calling eth_getCode(address) on any Ethereum-compatible RPC endpoint. A return value of undefined means the address is an EOA, while any non-empty bytecode confirms it is a smart contract account.
import { createPublicClient, http } from 'viem';
import { mainnet } from 'viem/chains';
// RPC endpoint for the target chain
// Update this to match your chain
const chainRpcUrl = 'https://base-mainnet.g.alchemy.com/v2';
// Alchemy API key
// https://www.alchemy.com/docs/create-an-api-key
const apiKey = 'secret';
// Create a client to interact with the blockchain
const client = createPublicClient({
chain: mainnet,
transport: http(`${chainRpcUrl}/${apiKey}`),
});
// The address to check
const address = '0x...';
// Retrieve the bytecode deployed at the address.
const code = await client.getCode({ address });
// EOAs have no bytecode, while smart contracts do.
if (code === undefined) {
console.log('This address is an EOA (no contract code).');
} else {
console.log('This address has contract code (smart contract account).');
}Create an Externally Owned Account signer
The EOA signer must have 3 properties: the account type, a function that returns the account identifier, and a function that signs messages.
import type { Signer, Identifier } from '@xmtp/browser-sdk';
import { IdentifierKind } from '@xmtp/browser-sdk';
const accountIdentifier: Identifier = {
identifier: '0x...', // Ethereum address as the identifier
identifierKind: IdentifierKind.Ethereum, // Specifies the identity type
};
const signer: Signer = {
type: 'EOA',
getIdentifier: () => accountIdentifier,
signMessage: async (message: string): Uint8Array => {
// typically, signing methods return a hex string
// this string must be converted to bytes and returned in this function
},
};Create a Smart Contract Wallet signer
The SCW signer has the same 3 required properties as the EOA signer, but also requires a function that returns the chain ID of the blockchain being used and an optional function that returns the block number to verify signatures against. If a function is not provided to retrieve the block number, the latest block number will be used.
Here is a list of supported chain IDs for SCWs:
| Chain ID | Network |
|---|---|
| 0 | Signifies an EOA |
| 1 | Ethereum Mainnet |
| 10 | Optimism |
| 100 | Gnosis |
| 137 | Polygon |
| 232 | Lens |
| 324 | zkSync Era |
| 480 | World |
| 2741 | Abstract |
| 8453 | Base |
| 42161 | Arbitrum One |
| 59144 | Linea |
To add SCW support for a new EVM chain, add the chain ID and a public RPC endpoint to chain_urls_default.json.
The details of creating an SCW signer are highly dependent on the wallet provider and the library you're using to interact with it. Here are some general guidelines to consider:
-
Wallet provider integration: Different wallet providers (Safe, Argent, Rainbow, etc.) have different methods for signing messages. See the wallet provider documentation for more details.
-
Library selection: Choose a library that supports your wallet provider (e.g., viem, ethers.js, web3.js). Each library has its own API for interacting with wallets. See the library documentation for more details.
-
Add an Ethereum-specific prefix: Before signing, Ethereum requires a specific prefix to be added to the message. To learn more, see ERC-191: Signed Data Standard. Libraries and wallet providers might add the prefix for you, so make sure you don't add the prefix twice.
-
Hash the prefixed message with Keccak-256: The prefixed message is hashed using the Keccak-256 algorithm, which is Ethereum's standard hashing algorithm. This step creates a fixed-length representation of the message, ensuring consistency and security. Note that some wallet providers might handle this hashing internally.
-
Sign the replay-safe hash: The replay-safe hash is signed using the private key of the SCW. This generates a cryptographic signature that proves ownership of the wallet and ensures the integrity of the message.
-
Convert the signature to a Uint8Array: The resulting signature is converted to a
Uint8Arrayformat, which is required by the XMTP SDK for compatibility and further processing.
The code snippets below are examples only and will need to be adapted based on your specific wallet provider and library.
import { IdentifierKind } from '@xmtp/browser-sdk';
export const createSCWSigner = (
address: `0x${string}`,
walletClient: WalletClient,
chainId: bigint,
): Signer => {
return {
type: "SCW",
getIdentifier: () => ({
identifier: address.toLowerCase(),
identifierKind: IdentifierKind.Ethereum,
}),
signMessage: async (message: string) => {
const signature = await walletClient.signMessage({
account: address,
message,
});
return toBytes(signature);
},
getChainId: () => {
return chainId;
},
};Add and test a custom EVM chain
To test an EVM-compatible chain that is not part of the supported chain list, check out libxmtp locally and add it for testing:
- Add the chain URL (see here). This is the only change needed to register a new chain, as it maps a chain ID to an RPC endpoint for smart contract wallet verification.
- Rebuild the
xmtp_idRust crate. Since the chain URLs are embedded at compile time, a rebuild is required for the new URL to take effect. - Start the local backend using Docker (
dev/docker/up).
You can then test using your own client code or a local version of xmtp.chat configured to allow your chain (see this PR for reference).
When testing with the local xmtp.chat instance:
- Select Smart contract wallet as your login method, then select the appropriate chain ID.
- Ensure the required browser extension is installed. Based on the extension you’re using, select either WalletConnect or Browser Injected (the latter requires the extension to populate the
window.ethereumnamespace). - Select the Local network to connect to your locally running XMTP backend.
Retrieve your wallet's private key
MetaMask users can export their account's private key to use with other applications. If your wallet provides a Secret Recovery Phrase (SRP) based on BIP39 instead, you can convert it to a private key using a BIP39 mnemonic converter. Follow these steps to convert your recovery phrase using Ian Coleman's BIP39 Converter:
- Download the offline version of the converter to prevent potential security risks
- Enter your recovery phrase in the "BIP39 Mnemonic" field
- Set "Coin" to "ETH - Ethereum"
- Under "Derivation Path", select "BIP44"
- Find your wallet address in the "Derived Addresses" section
- Copy the corresponding "Private Key"
Common errors
AssociationError.ChainIdMismatch
Wrong chain id. Initially added with 0 but now signing from 1
This error occurs when there's a mismatch between the chain ID used when initially creating an XMTP identity and the chain ID being used for subsequent signing operations. The chain ID verification prevents cross-chain signature attacks on smart contract wallets. Double-check your chain configuration to ensure the chain ID is properly set (when using SCWs) and consistent across your application.
NotFound.InboxIdForAddress
inbox id for address 0 not found
This typically happens when trying to create a DM with an address that hasn't been registered or associated with an XMTP inbox ID yet. You can only message wallet addresses that have been registered on the XMTP network. Registration occurs when a wallet is used with an XMTP-based chat app or one of our Client SDKs.
This error may also occur if your client is connected to a different XMTP network environment than the target address. For example, your client might be using the dev network while the target address was registered on production.
SignatureError.Invalid
Signature validation failed
This error occurs when attempting to connect a Smart Contract Wallet (SCW) using an incorrect chain. For example, selecting "Ethereum" as the chain for an SCW that is deployed on "Base". The error can also occur when trying to connect an Externally Owned Account (EOA) with a SCW signature setting.

