Behaviour for client-side payment signers.
A signer produces the cryptographic signatures a payer client needs to authorize x402 payments. Implementations are structs whose module implements this behaviour; the library dispatches on the struct's module, so custom signers (KMS-backed, hardware wallets, remote signing services) can be supplied anywhere the library takes a signer.
Callback design
The reference SDKs expose two shapes: the TypeScript ClientEvmSigner
signs full EIP-712 typed data (signTypedData), because wallet-backed and
remote signers refuse raw digests, while the Go client signer signs the
precomputed EIP-712 digest from a local private key. This behaviour
supports both: sign_eip712/3 receives the precomputed 32-byte digest
(sufficient for local keys and raw-signing KMS APIs) and the full typed
data map (domain/types/primaryType/message, mirroring the EIP-712 JSON
representation) for implementations that must reconstruct the message.
Implementations return the raw 65-byte r || s || v signature. v may be
0/1 or 27/28; the dispatcher normalizes it to 27/28 as expected
by EIP-3009 contracts.
Chain families
EVM payments sign EIP-712 typed data through sign_eip712/3. Solana
(SVM) payments instead sign raw transaction message bytes with Ed25519
through the optional sign_ed25519/2 callback — a signer implements
the callbacks for the chain families it supports, and schemes report a
signer without the needed callback as {:error, :unsupported_signer}.
Built-in implementations
X402.Signer.LocalKey signs with a raw secp256k1 private key and requires
the optional ex_secp256k1 and ex_keccak dependencies.
X402.Signer.SolanaKey signs with an Ed25519 key through OTP's :crypto
(no extra dependencies).
Gas-paying EVM execution uses the separate optional sign_transaction/3
callback. It receives a type-2 transaction, not EIP-712 typed data. A
typed-data-only wallet is never asked to sign a raw transaction digest.
Summary
Types
A raw 64-byte Ed25519 signature.
A raw 65-byte r || s || v signature.
A struct whose module implements X402.Signer.
EIP-712 typed data in its JSON representation.
Callbacks
Returns the signer's payment address (for EVM, a 0x-prefixed hex address).
Signs a message with Ed25519 and returns the raw 64-byte signature.
Signs an EIP-712 digest and returns the 65-byte r || s || v signature.
Signs an arbitrary message with EIP-191 personal_sign and returns the
0x-prefixed hex encoding of the 65-byte r || s || v signature.
Signs an EIP-1559 transaction digest without broadcasting.
Functions
Returns the address of a signer, dispatching on its struct module.
Signs a message with Ed25519, dispatching on the signer's struct module.
Signs an EIP-712 digest with a signer, dispatching on its struct module.
Signs a message with EIP-191 personal_sign, dispatching on the signer's
struct module.
Signs a type-2 transaction through its dedicated callback, without sending.
Types
@type ed25519_signature() :: <<_::512>>
A raw 64-byte Ed25519 signature.
@type signature() :: <<_::520>>
A raw 65-byte r || s || v signature.
@type t() :: struct()
A struct whose module implements X402.Signer.
@type typed_data() :: map()
EIP-712 typed data in its JSON representation.
Contains the "domain", "types", "primaryType", and "message" keys.
Callbacks
Returns the signer's payment address (for EVM, a 0x-prefixed hex address).
@callback sign_ed25519(signer :: t(), message :: binary()) :: {:ok, ed25519_signature()} | {:error, term()}
Signs a message with Ed25519 and returns the raw 64-byte signature.
Used by SVM (Solana) schemes, where message is the serialized
transaction message bytes (including the version prefix). Optional —
implement it for signers that support Solana payments.
@callback sign_eip712(signer :: t(), digest :: binary(), typed_data :: typed_data()) :: {:ok, signature()} | {:error, term()}
Signs an EIP-712 digest and returns the 65-byte r || s || v signature.
digest is the precomputed 32-byte EIP-712 digest
(keccak256(0x19 0x01 || domainSeparator || structHash)). typed_data is
the full EIP-712 typed data for implementations that cannot sign raw
digests. Optional — implement it for signers that support EVM payments.
Signs an arbitrary message with EIP-191 personal_sign and returns the
0x-prefixed hex encoding of the 65-byte r || s || v signature.
The signer hashes keccak256("\x19Ethereum Signed Message:\n" <> byte_size(message) <> message) and signs the digest with its secp256k1
key. Used by the Sign-In-With-X extension (X402.Extensions.SIWX.sign/3)
for eip155:* chains. Optional — implement it for EVM signers that
support wallet authentication.
@callback sign_transaction(t(), binary(), X402.Transaction.t()) :: {:ok, signature()} | {:error, term()}
Signs an EIP-1559 transaction digest without broadcasting.
Receives the locally computed digest and complete X402.Transaction so
hardware or remote implementations can reconstruct and review the exact
intent. Returns a raw 65-byte signature, not a serialized transaction.
Implementations must not broadcast: durable execution records the signed
bytes before granting permission to send.
Functions
Returns the address of a signer, dispatching on its struct module.
Examples
iex> X402.Signer.address(:not_a_signer)
{:error, :invalid_signer}
@spec sign_ed25519(t(), binary()) :: {:ok, ed25519_signature()} | {:error, term()}
Signs a message with Ed25519, dispatching on the signer's struct module.
Returns {:error, :unsupported_signer} when the signer module does not
implement the optional sign_ed25519/2 callback, and
{:error, :invalid_signature_format} for signatures that are not
64 bytes.
Examples
iex> X402.Signer.sign_ed25519(:not_a_signer, "message")
{:error, :invalid_signer}
iex> {:ok, evm_signer} = X402.Signer.LocalKey.new("0x" <> String.duplicate("11", 32))
iex> X402.Signer.sign_ed25519(evm_signer, "message")
{:error, :unsupported_signer}
@spec sign_eip712(t(), binary(), typed_data()) :: {:ok, signature()} | {:error, term()}
Signs an EIP-712 digest with a signer, dispatching on its struct module.
Normalizes the recovery byte to 27/28 and rejects signatures that are
not 65 bytes with {:error, :invalid_signature_format}.
Examples
iex> X402.Signer.sign_eip712(:not_a_signer, <<0::256>>, %{})
{:error, :invalid_signer}
Signs a message with EIP-191 personal_sign, dispatching on the signer's
struct module.
Returns the lowercase 0x-prefixed hex encoding of the 65-byte
r || s || v signature with v normalized to 27/28. Returns
{:error, :unsupported_signer} when the signer module does not implement
the optional sign_message/2 callback and
{:error, :invalid_signature_format} when the callback returns anything
other than a 65-byte hex signature.
Examples
iex> X402.Signer.sign_message(:not_a_signer, "message")
{:error, :invalid_signer}
iex> {:ok, solana_signer} = X402.Signer.SolanaKey.new(:binary.copy(<<1>>, 32))
iex> X402.Signer.sign_message(solana_signer, "message")
{:error, :unsupported_signer}
@spec sign_transaction(t(), X402.Transaction.t()) :: {:ok, signature()} | {:error, term()}
Signs a type-2 transaction through its dedicated callback, without sending.
Computes the digest from the complete transaction and normalizes the
returned recovery byte. There is no fallback to sign_eip712/3 or
personal_sign. Execution code must also verify the recovered gas-account
address before recording or broadcasting the encoded transaction.
Examples
iex> X402.Signer.sign_transaction(:not_a_signer, %{})
{:error, :invalid_signer}