Skip to content

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

FeatureWallet (EOA)Smart Wallet (SCW)
On-chain formNative accountSmart contract
AuthenticationECDSA private keyProgrammable (e.g. Passkeys)
RecoveryRecovery / Seed phraseProgrammable (multisig, trusted guardians)
Gas paymentUser pays native gasRelayer can pay (e.g. Base)
Security modelSingle keyPolicy-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.

Browser
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 IDNetwork
0Signifies an EOA
1Ethereum Mainnet
10Optimism
137Polygon
324zkSync Era
480World Chain
8453Base
42161Arbitrum One
59144Linea

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 Uint8Array format, 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.

Browser
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:

  1. Download the offline version of the converter to prevent potential security risks
  2. Enter your recovery phrase in the "BIP39 Mnemonic" field
  3. Set "Coin" to "ETH - Ethereum"
  4. Under "Derivation Path", select "BIP44"
  5. Find your wallet address in the "Derived Addresses" section
  6. 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.