Create a EOA or SCW signer
XMTP SDKs support message signing with 2 different types of Ethereum accounts: Externally Owned Accounts (EOAs) and 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 can pay (e.g. Base) |
| Security model | Single key | Policy-based, multi-layer security |
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';
const accountIdentifier: Identifier = {
identifier: '0x...', // Ethereum address as the identifier
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 |
| 137 | Polygon |
| 324 | zkSync Era |
| 480 | World Chain |
| 8453 | Base |
| 42161 | Arbitrum One |
| 59144 | Linea |
Need support for a different chain ID? Please post your request to the XMTP Technical Forums. To look up chain IDs, see ChainList.
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.
export const createSCWSigner = (
address: `0x${string}`,
walletClient: WalletClient,
chainId: bigint,
): Signer => {
return {
type: "SCW",
getIdentifier: () => ({
identifier: address.toLowerCase(),
identifierKind: "Ethereum",
}),
signMessage: async (message: string) => {
const signature = await walletClient.signMessage({
account: address,
message,
});
return toBytes(signature);
},
getChainId: () => {
return chainId;
},
};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 recovery phrase 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.

