Skip to main content
The Sign-In-With-X (SIWX) extension implements CAIP-122 for chain-agnostic wallet authentication. It allows clients to prove control of a wallet that previously paid for a resource, enabling access without requiring repurchase.

Overview

SIWX solves a key problem in x402: repeat access to purchased content. Without SIWX, clients must pay every time they request a resource. With SIWX:
  • For Buyers: Sign in with your wallet to access content you’ve already paid for
  • For Sellers: Grant access to returning customers without requiring repayment
  • Chain-Agnostic: Works with EVM (Ethereum, Base, etc.) and Solana wallets
  • Standards-Based: Built on CAIP-122, EIP-4361 (SIWE), and Sign-In-With-Solana

How It Works

  1. Server returns 402 with sign-in-with-x extension containing challenge parameters
  2. Client signs the CAIP-122 message with their wallet
  3. Client sends signed proof in SIGN-IN-WITH-X HTTP header
  4. Server verifies signature and grants access if wallet has previous payment
This is a Server ↔ Client extension. The Facilitator is not involved in the authentication flow.

Server Usage

The easiest way to implement SIWX is using the provided hooks, which handle all the complexity automatically:
import {
  declareSIWxExtension,
  siwxResourceServerExtension,
  createSIWxSettleHook,
  createSIWxRequestHook,
  InMemorySIWxStorage,
} from '@x402/extensions/sign-in-with-x';
import { x402ResourceServer } from '@x402/core/server';
import { x402HTTPResourceServer } from '@x402/core/http';
import { HTTPFacilitatorClient } from '@x402/core/http';
import { ExactEvmScheme } from '@x402/evm/exact/server';

const NETWORK = 'eip155:8453'; // Base mainnet
const payTo = '0xYourAddress';

// Storage for tracking paid addresses
const storage = new InMemorySIWxStorage();

const facilitatorClient = new HTTPFacilitatorClient({
  url: 'https://x402.org/facilitator'
});

// 1. Register extension for time-based field refreshment
const resourceServer = new x402ResourceServer(facilitatorClient)
  .register(NETWORK, new ExactEvmScheme())
  .registerExtension(siwxResourceServerExtension)  // Refreshes nonce/timestamps
  .onAfterSettle(createSIWxSettleHook({ storage }));  // Records payments

// 2. Declare SIWX support in routes
const routes = {
  'GET /data': {
    accepts: [{
      scheme: 'exact',
      price: '$0.01',
      network: NETWORK,
      payTo
    }],
    extensions: declareSIWxExtension({
      statement: 'Sign in to access your purchased content',
    }),
  },
};

// 3. Verify incoming SIWX proofs
const httpServer = new x402HTTPResourceServer(resourceServer, routes)
  .onProtectedRequest(createSIWxRequestHook({ storage }));  // Grants access if paid
The hooks automatically:
  • siwxResourceServerExtension: Derives network from accepts, domain/uri from request URL, refreshes nonce/issuedAt/expirationTime per request
  • createSIWxSettleHook: Records payment when settlement succeeds
  • createSIWxRequestHook: Validates and verifies SIWX proofs, grants access if wallet has paid

Smart Wallet Support (EIP-1271 / EIP-6492)

By default, only EOA (Externally Owned Account) signatures are verified. To support smart contract wallets (like Coinbase Smart Wallet, Safe, etc.), pass publicClient.verifyMessage from viem:
import { createPublicClient, http } from 'viem';
import { base } from 'viem/chains';

const publicClient = createPublicClient({
  chain: base,
  transport: http()
});

const httpServer = new x402HTTPResourceServer(resourceServer, routes)
  .onProtectedRequest(createSIWxRequestHook({
    storage,
    verifyOptions: { evmVerifier: publicClient.verifyMessage },
  }));
This enables:
  • EIP-1271: Verification of deployed smart contract wallets
  • EIP-6492: Verification of counterfactual (not-yet-deployed) wallets
Note: Smart wallet verification requires RPC calls, while EOA verification is purely local.

Manual Usage (Advanced)

For custom implementations, you can use the low-level functions directly:
import {
  declareSIWxExtension,
  parseSIWxHeader,
  validateSIWxMessage,
  verifySIWxSignature,
  SIGN_IN_WITH_X,
} from '@x402/extensions/sign-in-with-x';

// 1. Declare in PaymentRequired response
const extensions = {
  [SIGN_IN_WITH_X]: declareSIWxExtension({
    domain: 'api.example.com',
    resourceUri: 'https://api.example.com/data',
    network: 'eip155:8453',
    statement: 'Sign in to access your purchased content',
  }),
};

// 2. Verify incoming proof
async function handleRequest(request: Request) {
  const header = request.headers.get('SIGN-IN-WITH-X');
  if (!header) return; // No auth provided

  // Parse the header
  const payload = parseSIWxHeader(header);

  // Validate message fields (expiry, nonce, domain, etc.)
  const validation = await validateSIWxMessage(
    payload,
    'https://api.example.com/data'
  );
  if (!validation.valid) {
    return { error: validation.error };
  }

  // Verify signature and recover address
  const verification = await verifySIWxSignature(payload);
  if (!verification.valid) {
    return { error: verification.error };
  }

  // verification.address is the verified wallet
  // Check if this wallet has paid before
  const hasPaid = await checkPaymentHistory(verification.address);
  if (hasPaid) {
    // Grant access without payment
  }
}

Client Usage

The easiest way to use SIWX as a client is with the provided hook:
import { createSIWxClientHook } from '@x402/extensions/sign-in-with-x';
import { x402HTTPClient } from '@x402/fetch';
import { x402Client } from '@x402/core/client';
import { privateKeyToAccount } from 'viem/accounts';

const signer = privateKeyToAccount(process.env.EVM_PRIVATE_KEY as `0x${string}`);
const client = new x402Client();

// Configure client with SIWX hook - automatically tries SIWX auth before payment
const httpClient = new x402HTTPClient(client)
  .onPaymentRequired(createSIWxClientHook(signer));

// Requests automatically use SIWX auth when server supports it
const response = await httpClient.fetch('https://api.example.com/data');
The client hook automatically:
  • Detects SIWX support in 402 responses
  • Matches your wallet’s chain with server’s supportedChains
  • Signs and sends the authentication proof
  • Falls back to payment if SIWX auth fails

Manual Usage (Advanced)

For custom implementations:
import {
  createSIWxPayload,
  encodeSIWxHeader,
} from '@x402/extensions/sign-in-with-x';

// 1. Get extension and network from 402 response
const paymentRequired = await response.json();
const extension = paymentRequired.extensions['sign-in-with-x'];
const paymentNetwork = paymentRequired.accepts[0].network; // e.g., "eip155:8453"

// 2. Find matching chain in supportedChains
const matchingChain = extension.supportedChains.find(
  chain => chain.chainId === paymentNetwork
);

if (!matchingChain) {
  throw new Error('Chain not supported');
}

// 3. Build complete info with selected chain
const completeInfo = {
  ...extension.info,
  chainId: matchingChain.chainId,
  type: matchingChain.type,
};

// 4. Create signed payload
const payload = await createSIWxPayload(completeInfo, signer);

// 5. Encode and send
const header = encodeSIWxHeader(payload);
const response = await fetch(url, {
  headers: { 'SIGN-IN-WITH-X': header }
});

Multi-Chain Support

Servers can support multiple chains (e.g., both EVM and Solana) by including multiple entries in supportedChains:
const routes = {
  'GET /data': {
    accepts: [
      {
        scheme: 'exact',
        price: '$0.01',
        network: 'eip155:8453', // Base
        payTo: '0xYourAddress'
      },
      {
        scheme: 'exact',
        price: '$0.01',
        network: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', // Solana mainnet
        payTo: 'YourSolanaAddress'
      }
    ],
    extensions: declareSIWxExtension({
      network: ['eip155:8453', 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp'],
      statement: 'Sign in to access your purchased content',
    }),
  },
};
Clients match their wallet’s chainId against supportedChains and use the first matching entry. The same nonce is shared across all chains, preventing replay attacks when authenticating with different wallets.

Supported Chains

EVM (Ethereum, Base, Polygon, etc.)

  • Chain ID Format: eip155:* (e.g., eip155:8453 for Base)
  • Signature Type: eip191
  • Signature Schemes:
    • eip191 (EOA - default)
    • eip1271 (smart contract wallet)
    • eip6492 (counterfactual wallet)
  • Message Format: EIP-4361 (SIWE)

Solana

  • Chain ID Format: solana:* (e.g., solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp for mainnet)
  • Signature Type: ed25519
  • Signature Scheme: siws
  • Message Format: Sign-In With Solana

API Reference

declareSIWxExtension(options?)

Creates the extension object for servers to include in PaymentRequired. Most fields are derived automatically from request context when using siwxResourceServerExtension.
declareSIWxExtension({
  // All fields optional - derived from context if omitted
  domain?: string;                     // Server domain (derived from request URL)
  resourceUri?: string;                // Full resource URI (derived from request URL)
  network?: string | string[];         // CAIP-2 network(s) (derived from accepts[].network)
  statement?: string;                  // Human-readable purpose
  version?: string;                    // CAIP-122 version (default: "1")
  expirationSeconds?: number;          // Challenge TTL in seconds
})

parseSIWxHeader(header)

Parses a base64-encoded SIGN-IN-WITH-X header into a payload object.

validateSIWxMessage(payload, resourceUri, options?)

Validates message fields (expiry, domain binding, nonce, etc.).
validateSIWxMessage(payload, resourceUri, {
  maxAge?: number;                    // Max age for issuedAt (default: 5 min)
  checkNonce?: (nonce) => boolean;    // Custom nonce validation
})
// Returns: { valid: boolean; error?: string }

verifySIWxSignature(payload, options?)

Verifies the cryptographic signature and recovers the signer address.
verifySIWxSignature(payload, {
  evmVerifier?: EVMMessageVerifier;  // For smart wallet support
})
// Returns: { valid: boolean; address?: string; error?: string }

createSIWxPayload(serverInfo, signer)

Client helper that creates and signs a complete payload.

encodeSIWxHeader(payload)

Encodes a payload as base64 for the SIGN-IN-WITH-X header.

Storage Interface

Implement SIWxStorage to track which wallets have paid:
interface SIWxStorage {
  hasPaid(address: string, resourceUri: string): Promise<boolean>;
  recordPayment(address: string, resourceUri: string): Promise<void>;
}
The package includes InMemorySIWxStorage for development. For production, implement persistent storage (database, Redis, etc.).

Security Considerations

  • Domain Binding: The domain field prevents signature reuse across different services
  • Nonce Uniqueness: Each challenge MUST have a unique nonce to prevent replay attacks
  • Temporal Bounds: The issuedAt, expirationTime, and notBefore fields constrain signature validity windows
  • Chain-Specific Verification: Signatures are verified using chain-appropriate algorithms, preventing cross-chain signature reuse
  • Smart Wallet Support: EIP-1271 and EIP-6492 verification requires an RPC call to the wallet contract

Troubleshooting

Signature Verification Fails

Problem: verifySIWxSignature returns valid: false. Solutions:
  • Ensure the message was signed with the correct wallet
  • Check that the signature scheme matches the wallet type
  • For smart wallets, enable evmVerifier option with a viem public client
  • Verify the chain ID matches between client and server

Message Validation Fails

Problem: validateSIWxMessage returns valid: false. Solutions:
  • Check that issuedAt is recent (within maxAge, default 5 minutes)
  • Verify expirationTime hasn’t passed
  • Ensure domain matches the server’s domain
  • Confirm uri matches the resource URI

Client Hook Not Working

Problem: SIWX authentication not being attempted. Solutions:
  • Verify server is declaring SIWX extension in 402 response
  • Check that client’s wallet chain matches one of the supportedChains
  • Ensure signer is properly configured for the wallet type