logo

SIWE Authentication

Step-by-step Sign-In with Ethereum flow — nonce, message, sign, verify, and use the session token

SIWE (EIP-4361) provides session-based wallet authentication. Instead of signing every request, you sign a challenge message once and receive a session token that lasts 12 hours.

The @namefi/api-client handles SIWE automatically when using type: 'EIP712'. This guide covers the manual flow for custom integrations or raw HTTP usage.

Overview

┌─────────┐         ┌─────────────┐
│  Client  │         │  Namefi API  │
└────┬─────┘         └──────┬──────┘
     │  1. GET /siwe/nonce   │
     │──────────────────────►│
     │     { nonce }         │
     │◄──────────────────────│
     │                       │
     │  2. GET /siwe/message │
     │──────────────────────►│
     │  { message,           │
     │    messageString }    │
     │◄──────────────────────│
     │                       │
     │  3. personal_sign     │
     │  (local wallet)       │
     │                       │
     │  4. POST /siwe/verify │
     │──────────────────────►│
     │  { token, session }   │
     │◄──────────────────────│
     │                       │
     │  5. Authenticated     │
     │  requests with        │
     │  x-namefi-siwe-token  │
     │──────────────────────►│

Step 1: Get a nonce

Request a replay-protected nonce for your wallet address. The nonce expires after 5 minutes.

const nonceResponse = await fetch(
  'https://backend.astra.namefi.io/v-next/siwe/nonce?signerAddress=0xYourAddress',
);
const { valid, nonce } = await nonceResponse.json();

if (!valid) {
  throw new Error('Failed to get nonce');
}

Request: GET /v-next/siwe/nonce

ParameterTypeDescription
signerAddressstring (query)Checksummed Ethereum address

Response:

{ "valid": true, "nonce": "a1b2c3d4..." }

Step 2: Prepare the SIWE message

Send the nonce back to get a canonical SIWE message. The server constructs the message with proper domain, URI, and expiration fields.

const messageResponse = await fetch(
  `https://backend.astra.namefi.io/v-next/siwe/message?signerAddress=0xYourAddress&nonce=${nonce}&chainId=1`,
);
const { valid, message, messageString } = await messageResponse.json();

if (!valid) {
  throw new Error('Failed to prepare message');
}

Request: GET /v-next/siwe/message

ParameterTypeDescription
signerAddressstring (query)Checksummed Ethereum address
noncestring (query)Nonce from step 1
chainIdnumber (query, optional)EIP-155 chain ID to bind the session to

Response:

{
  "valid": true,
  "message": {
    "address": "0xYourAddress",
    "chainId": 1,
    "domain": "astra.namefi.io",
    "uri": "https://astra.namefi.io",
    "version": "1",
    "nonce": "a1b2c3d4...",
    "issuedAt": "2025-01-15T10:30:00.000Z",
    "expirationTime": "2025-01-15T22:30:00.000Z",
    "statement": "Sign in to Namefi API"
  },
  "messageString": "astra.namefi.io wants you to sign in with your Ethereum account:\n0xYourAddress\n\nSign in to Namefi API\n\nURI: https://astra.namefi.io\nVersion: 1\n..."
}

The message object is the structured EIP-4361 payload. The messageString is the canonical string representation you sign.

Allowed chains

Use GET /v-next/siwe/allowed-chains to discover which chain IDs the server accepts:

const chainsResponse = await fetch(
  'https://backend.astra.namefi.io/v-next/siwe/allowed-chains',
);
const allowedChains = await chainsResponse.json();
// Production: [1, 8453]  (Mainnet, Base)
// Development: [11155111, 46630]  (Sepolia, Namefi Testnet)

If you omit chainId in step 2, the server uses its default chain.

Step 3: Sign the message

Sign the messageString with personal_sign using your wallet. This produces a standard Ethereum message signature.

With viem (local account)

import { mnemonicToAccount } from 'viem/accounts';

const account = mnemonicToAccount(process.env.MNEMONIC!);
const signature = await account.signMessage({ message: messageString });

With WalletConnect (remote wallet)

const signature = await provider.request({
  method: 'personal_sign',
  params: [messageString, '0xYourAddress'],
});

With ethers.js

const signature = await signer.signMessage(messageString);

Step 4: Verify the signature

Submit the signature along with the structured message object and address. The server verifies the signature, consumes the nonce, and returns a session token.

const verifyResponse = await fetch(
  'https://backend.astra.namefi.io/v-next/siwe/verify',
  {
    method: 'POST',
    headers: { 'content-type': 'application/json' },
    body: JSON.stringify({
      signature,
      message,       // The structured message object from step 2, not the string
      address: '0xYourAddress',
    }),
  },
);

const result = await verifyResponse.json();

if (!result.valid) {
  throw new Error(`Verification failed: ${result.error}`);
}

const { token, session } = result;
console.log('Session token:', token);
console.log('Expires in:', session.maxAgeSeconds, 'seconds');

Request: POST /v-next/siwe/verify

FieldTypeDescription
signaturestringHex-encoded signature (0x...)
messageobjectThe structured SIWE message object from step 2
addressstringExpected signer address (checksummed)

Response:

{
  "valid": true,
  "recoveredAddress": "0xYourAddress",
  "token": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
  "session": {
    "address": "0xYourAddress",
    "chainId": 1,
    "createdAt": "2025-01-15T10:30:00.000Z",
    "maxAgeSeconds": 43200
  }
}

Step 5: Use the session token

Attach the token as the x-namefi-siwe-token header on authenticated requests:

const response = await fetch(
  'https://backend.astra.namefi.io/v-next/user/domains',
  {
    headers: {
      'x-namefi-siwe-token': token,
    },
  },
);

const domains = await response.json();

Session lifecycle

PropertyValue
Token lifetime12 hours (43200 seconds)
Nonce lifetime5 minutes (300 seconds)
Token formatUUID v4
Header namex-namefi-siwe-token
Refresh skew30 seconds (client refreshes 30s before expiry)

The token is opaque — store it and reuse it for all requests until it expires. When it expires, repeat the full flow (steps 1-4) to get a new token.

Full example

import { mnemonicToAccount } from 'viem/accounts';

const BASE_URL = 'https://backend.astra.namefi.io/v-next';
const account = mnemonicToAccount(process.env.MNEMONIC!);
const address = account.address;

// 1. Get nonce
const { nonce } = await fetch(
  `${BASE_URL}/siwe/nonce?signerAddress=${address}`,
).then((r) => r.json());

// 2. Prepare message
const { message, messageString } = await fetch(
  `${BASE_URL}/siwe/message?signerAddress=${address}&nonce=${nonce}&chainId=1`,
).then((r) => r.json());

// 3. Sign
const signature = await account.signMessage({ message: messageString });

// 4. Verify and get token
const { token } = await fetch(`${BASE_URL}/siwe/verify`, {
  method: 'POST',
  headers: { 'content-type': 'application/json' },
  body: JSON.stringify({ signature, message, address }),
}).then((r) => r.json());

// 5. Make authenticated requests
const domains = await fetch(`${BASE_URL}/user/domains`, {
  headers: { 'x-namefi-siwe-token': token },
}).then((r) => r.json());

console.log('Your domains:', domains);

Using SIWE with the client library

When using @namefi/api-client with type: 'EIP712', SIWE is handled automatically. The client runs steps 1-4 behind the scenes on the first request that needs session auth, caches the token, and refreshes it before expiry:

import { createNamefiClient, type EIP712Signer } from '@namefi/api-client';

const client = createNamefiClient({
  authentication: {
    signer: myEip712Signer,
    type: 'EIP712',
  },
  siwe: {
    chainId: 1,
  },
  logger: true,
});

// SIWE bootstrap happens automatically when needed
const domains = await client.user.getDomains();

See the namefi-api-skills repository for a complete CLI-based WalletConnect + SIWE implementation.

On this page