Client side of the sign-in-with-x extension: answering a challenge.
When a server advertises a Sign-In-With-X challenge under
PaymentRequired.extensions["sign-in-with-x"], a client that has already
paid for the resource can prove control of its wallet instead of paying
again. authenticate/4 picks the chain to sign for, refuses challenges
that are not bound to the resource's origin, signs the CAIP-122 message
with X402.Extensions.SIWX.sign/3, and returns the SIGN-IN-WITH-X
header value. X402.Client.Finch and X402.MCP.Client drive the full
flow through their :siwx option; use this module directly with any
other transport.
Options
The :siwx option of the drivers is a keyword list:
:chain_id- Required. CAIP-2 chain to sign for, or:autoto pick the first advertisedsupportedChainsentry the signer can sign (EVM signers map toeip155:*, Solana signers tosolana:*).:address- Address placed in the proof; the signer's address when omitted. The default value isnil.:signature_scheme- OptionalsignatureSchemehint copied into the proof. The default value isnil.:domain- Expected challengedomain. Required for transports without a resource URL (MCP); for HTTP it defaults to the resource URL's host. The default value isnil.:uri- Exact expected challenge URI, including path and query. Required for automatic MCP proofs, alongside:domain. Configure it independently of the challenge and bind the transport to that trusted server. The default value isnil.
Summary
Types
Validated :siwx driver options.
A signed challenge, ready to send.
Errors returned by authenticate/4, wrapped as {:siwx, reason}.
Functions
Signs the challenge advertised in a PaymentRequired map.
Fetches the advertised challenge from a PaymentRequired map.
Validates a driver's :siwx option.
Types
@type opts() :: [ chain_id: String.t() | :auto, address: String.t() | nil, signature_scheme: String.t() | nil, domain: String.t() | nil, uri: String.t() | nil ]
Validated :siwx driver options.
A signed challenge, ready to send.
@type reason() :: :invalid_challenge | :domain_mismatch | :uri_mismatch | :unsupported_chain | :payment_cancelled | X402.Extensions.SIWX.sign_error() | X402.Extensions.SIWX.signed_encode_error()
Errors returned by authenticate/4, wrapped as {:siwx, reason}.
Functions
@spec authenticate(map(), X402.Signer.t(), keyword(), keyword()) :: {:ok, proof()} | :none | {:error, {:siwx, reason()}}
Signs the challenge advertised in a PaymentRequired map.
Returns :none when the server advertised no sign-in-with-x
challenge, {:ok, proof} with the SIGN-IN-WITH-X header value once
signed, or {:error, {:siwx, reason}}.
Before signing, the challenge is checked against the resource it was
issued for, as the spec requires: its info.domain must equal (ignoring case)
siwx_opts[:domain] when given, otherwise the host (optionally with
port) of :resource_url; and, when :resource_url is given, the origin
(scheme, host, port) of info.uri must equal the URL's origin. Failures
are reported as :domain_mismatch / :uri_mismatch. With neither a
trusted domain nor a resource URL, signing fails with :domain_mismatch.
Never derive the expected domain from the untrusted challenge.
siwx_opts[:uri] additionally requires an exact URI match, including path
and query, and a domain consistent with its host/port. The pin must be an
absolute HTTP(S) URL without userinfo or fragment. It is mandatory for
transport: :mcp; a domain alone is insufficient for automatic MCP proofs.
With chain_id: :auto the first entry of supportedChains whose family
the signer can sign is used (eip155:* needs X402.Signer.sign_message/2,
solana:* needs X402.Signer.sign_ed25519/2); no match yields
:unsupported_chain. Signing errors from X402.Extensions.SIWX.sign/3
are returned as they are.
Emits [:x402, :client, :siwx] with status: :error, :reason,
:transport, and :chain_id on failure; the drivers emit the :ok event
once they know the outcome. The chain is the selected signing chain, or
nil if origin validation or automatic selection failed before selecting
one. It is never :auto.
Options
:resource_url— the URL that returned the 402, for the origin check.:transport— atom identifying the caller in telemetry (:httpor:mcpfor the built-in drivers); defaults tonilfor direct calls.:before_sign— optional zero-arity consent callback, invoked after origin/URI checks and chain selection but before wallet signing. Only:okallows signing; other returns yield{:siwx, :payment_cancelled}. It is not called when no challenge is present.
Examples
iex> {:ok, signer} = X402.Signer.SolanaKey.new(:binary.copy(<<1>>, 32))
iex> challenge = X402.Extensions.SIWX.challenge(
...> domain: "api.example.com",
...> uri: "https://api.example.com",
...> supported_chains: [%{chain_id: "eip155:8453"}, %{chain_id: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"}]
...> )
iex> payment_required = %{"x402Version" => 2, "accepts" => [], "extensions" => %{"sign-in-with-x" => challenge}}
iex> {:ok, proof} = X402.Client.SIWX.authenticate(payment_required, signer, [chain_id: :auto],
...> resource_url: "https://api.example.com/premium")
iex> {proof.chain_id, proof.address}
{"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "AKnL4NNf3DGWZJS6cPknBuEGnVsV4A4m5tgebLHaRSZ9"}
iex> {:ok, {:spec, fields}} = X402.Extensions.SIWX.decode_signed(proof.header)
iex> fields["nonce"] == challenge["info"]["nonce"]
true
iex> X402.Client.SIWX.authenticate(%{"x402Version" => 2, "accepts" => []}, :signer, chain_id: :auto)
:none
Fetches the advertised challenge from a PaymentRequired map.
Examples
iex> challenge = %{"info" => %{"domain" => "api.example.com"}, "supportedChains" => []}
iex> payment_required = %{"x402Version" => 2, "accepts" => [], "extensions" => %{"sign-in-with-x" => challenge}}
iex> X402.Client.SIWX.fetch_challenge(payment_required)
{:ok, challenge}
iex> X402.Client.SIWX.fetch_challenge(%{"x402Version" => 2, "accepts" => []})
:error
Validates a driver's :siwx option.
nil and false disable automatic Sign-In-With-X; a keyword list is
validated against the options above. Designed for NimbleOptions custom
validation.
Examples
iex> X402.Client.SIWX.validate_opts(false)
{:ok, nil}
iex> {:ok, opts} = X402.Client.SIWX.validate_opts(chain_id: :auto)
iex> Enum.sort(opts)
[address: nil, chain_id: :auto, domain: nil, signature_scheme: nil, uri: nil]
iex> {:error, message} = X402.Client.SIWX.validate_opts(address: "0xabc")
iex> message =~ ":chain_id"
true