Skip to content

Signer

To connect to XMTP, your app needs a signer, an object that identifies a user and can sign messages on their behalf. Ethereum EOA signatures use ECDSA on the secp256k1 curve. The SDK also supports smart contract wallet verification and P-256 passkey signatures.

There are four ways to create a signer:

  • Key pair: Generate or provide a secp256k1 private key directly. No wallet or blockchain needed.
  • Externally Owned Account (EOA): Use an Ethereum wallet.
  • Smart Contract Wallet (SCW): Use an ERC-1271 compatible smart contract wallet.
  • Passkey: Use a P-256 key and the passkey signature fields.

All four use the same signer interface, so the rest of your XMTP integration is identical regardless of which you choose.

Use Client.generateKeys() (SDKClient.generateKeys() in Kotlin and Swift) to create a LocalSigningKey. It derives an Ethereum address, signs ERC-191 text, and can be passed directly to Client.create() or SDKClient.create(). No additional crypto package is needed. Generating a key does not contact a backend.

const
const signer: LocalSigningKey
signer
= await
class Client

An XMTP client. Its inputs and results are public values; the binding Client and the target transport stay private to the package.

Client
.
ClientMembers.generateKeys(): Promise<LocalSigningKey>
generateKeys
();

Save the private key securely before creating a persistent account. toHex() returns lowercase hex with a 0x prefix; toBytes() returns a copy of the 32 private-key bytes. LocalSigningKey.fromHex() and fromBytes() restore the same identity. Hex imports require 0x followed by exactly 64 hexadecimal digits. Invalid lengths, invalid hex, zero, and out-of-range private keys fail with InvalidInput. toBytes() and toHex() are synchronous on Swift and Kotlin, and asynchronous on Node and Browser. Generation, import, and signing remain asynchronous on every platform.

SDKByte type
Browser and NodeUint8Array
KotlinByteArray
SwiftData
const
const signer: LocalSigningKey
signer
= await
class LocalSigningKey
LocalSigningKey
.
LocalSigningKey.fromHex(key: string): Promise<LocalSigningKey>
fromHex
(
saved: string
saved
);
const
const keyBytes: Uint8Array<ArrayBufferLike>
keyBytes
= await
const signer: LocalSigningKey
signer
.
LocalSigningKey.toBytes(): Promise<Uint8Array>
toBytes
();
const
const keyHex: string
keyHex
= await
const signer: LocalSigningKey
signer
.
LocalSigningKey.toHex(): Promise<string>
toHex
();

The exported value controls the account. Use the Keychain, an encrypted keystore, or another secret store. Keep it separate from the database encryption key. toString() and normal debug output do not export the private key; use toHex() explicitly.

Connect your wallet or key provider to the signer interface. These EOA examples call an app function that signs SigningRequest.text as ERC-191 text and returns raw recoverable ECDSA signature bytes.

const
const signer: Signer
signer
:
(alias) interface Signer
import Signer
Signer
= {
Signer.identity(): Promise<PublicIdentity>
identity
: async () => ({
identifier: string
identifier
:
address: string
address
,
kind: PublicIdentityKind
kind
: "ethereum" }),
Signer.kind(): Promise<SignerKind>
kind
: async () => ({
kind: "eoa"
kind
: "eoa" }),
Signer.sign(request: SigningRequest): Promise<Signature>
sign
: async (
request: SigningRequest
request
) => ({
kind: "ecdsa"
kind
: "ecdsa",
value: Uint8Array<ArrayBufferLike>
value
: await
signText: (text: string) => Promise<Uint8Array>
signText
(
request: SigningRequest
request
.
text: string
text
),
}),
};
PartBrowser, NodeKotlin, Swift
Identityidentity(): Promise<PublicIdentity>identity() returns PublicIdentity
Kindkind(): Promise<SignerKind>kind() returns SignerKind
Signsign(request): Promise<Signature>sign(request) returns Signature

All three methods are async. PublicIdentity has identifier and kind. Ethereum identities use the ethereum kind and an Ethereum address. Passkey identities use the passkey kind and a public key encoded as hex.

SignerKind selects EOA, SCW, or passkey. An SCW kind carries a required chain ID and an optional block number. JavaScript uses bigint; Kotlin uses ULong; Swift uses UInt64. The EOA kind carries neither field.

Return the matching Signature variant with raw bytes. An EOA returns ecdsa with 65-byte [R || S || V] data. Convert a wallet library’s hex result to bytes when needed. An SCW returns its bytes, address, chain ID, and optional block number. A passkey returns its signature, public key, authenticator data, and client data JSON.

Most Ethereum wallet providers add the ERC-191 prefix and hash the message. If you add it again, verification fails. Check the wallet provider behavior.

An SCW signer uses the same interface. Its SignerKind and Signature carry the same chain ID. The signature can include a block number for historical verification. The backend operator must configure the RPC endpoint for that chain.

To add a different identity kind, see Extend the identity model.

ErrorCause
InvalidInputThe signature variant or SCW chain ID does not match the signer kind, or signature validation fails
Signer (code SignerFailed)An identity, kind, or sign callback failed
ChainNotAcceptedThe backend configuration does not accept the SCW chain