Skip to main content

SIWE Authentication

Sign-In with Ethereum (SIWE) doubles your rate limits and enables wallet-based identity.

Benefitsโ€‹

FeatureAnonymousSIWE Authenticated
Daily limit100200
Per minute3060
/api/me accessโŒโœ…
Usage trackingBy IPBy wallet
API key creationโŒโœ…

Authentication Flow Overviewโ€‹

โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”                           โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”
โ”‚ Client โ”‚ โ”‚ API โ”‚
โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”ฌโ”€โ”€โ”€โ”€โ”€โ”˜
โ”‚ โ”‚
โ”‚ 1. GET /api/auth/siwe/challenge โ”‚
โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถโ”‚
โ”‚ โ”‚
โ”‚ { nonce, message } โ”‚
โ”‚ โ—€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚
โ”‚ โ”‚
โ”‚ 2. User signs message with wallet โ”‚
โ”‚ โ”‚
โ”‚ 3. POST /api/auth/siwe/verify โ”‚
โ”‚ { message, signature } โ”‚
โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถโ”‚
โ”‚ โ”‚
โ”‚ { token, address, expiresAt } โ”‚
โ”‚ โ—€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”‚
โ”‚ โ”‚
โ”‚ 4. Use token in requests โ”‚
โ”‚ Authorization: Bearer <token> โ”‚
โ”‚ โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ–ถโ”‚

Step 1: Get Challenge (Nonce)โ€‹

Request a unique challenge nonce. This prevents replay attacks.

Endpointโ€‹

GET /api/auth/siwe/challenge

cURLโ€‹

curl https://api.web3identity.com/api/auth/siwe/challenge

Responseโ€‹

{
"nonce": "8a3x9Z2bN4cD5eF6gH7iJ8kL9mN0oP1q",
"issuedAt": "2026-02-08T15:30:00Z",
"expiresAt": "2026-02-08T15:35:00Z",
"domain": "api.web3identity.com",
"uri": "https://api.web3identity.com",
"version": "1",
"chainId": 1
}
FieldDescription
nonceUnique challenge string (use within 5 minutes)
issuedAtWhen the challenge was created
expiresAtChallenge expiration (5 minutes)
domainDomain to use in SIWE message
uriURI to use in SIWE message
versionSIWE version
chainIdChain ID to use
Nonce Expiration

Nonces expire after 5 minutes and can only be used once. Always request a fresh nonce before signing.


Step 2: Create & Sign SIWE Messageโ€‹

Build a SIWE-compliant message and sign it with the user's wallet.

import { SiweMessage } from 'siwe';

// Get challenge from Step 1
const challenge = await fetch('https://api.web3identity.com/api/auth/siwe/challenge')
.then(r => r.json());

// Create SIWE message
const message = new SiweMessage({
domain: challenge.domain,
address: walletAddress,
statement: 'Sign in to Web3 Identity API for enhanced access.',
uri: challenge.uri,
version: challenge.version,
chainId: challenge.chainId,
nonce: challenge.nonce,
issuedAt: challenge.issuedAt,
expirationTime: new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString(),
});

const messageString = message.prepareMessage();

// Sign with wallet (example with viem)
const signature = await walletClient.signMessage({
account: walletAddress,
message: messageString,
});

Message Formatโ€‹

The SIWE message follows this format:

api.web3identity.com wants you to sign in with your Ethereum account:
0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045

Sign in to Web3 Identity API for enhanced access.

URI: https://api.web3identity.com
Version: 1
Chain ID: 1
Nonce: 8a3x9Z2bN4cD5eF6gH7iJ8kL9mN0oP1q
Issued At: 2026-02-08T15:30:00Z
Expiration Time: 2026-02-09T15:30:00Z

Step 3: Verify Signatureโ€‹

Submit the signed message to get an authentication token.

Endpointโ€‹

POST /api/auth/siwe/verify
Content-Type: application/json

Request Bodyโ€‹

{
"message": "api.web3identity.com wants you to sign in with your Ethereum account:\n0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045\n\nSign in to Web3 Identity API for enhanced access.\n\nURI: https://api.web3identity.com\nVersion: 1\nChain ID: 1\nNonce: 8a3x9Z2bN4cD5eF6gH7iJ8kL9mN0oP1q\nIssued At: 2026-02-08T15:30:00Z",
"signature": "0x1234567890abcdef..."
}

cURLโ€‹

curl -X POST https://api.web3identity.com/api/auth/siwe/verify \
-H "Content-Type: application/json" \
-d '{
"message": "api.web3identity.com wants you to sign in...",
"signature": "0x..."
}'

Success Responseโ€‹

{
"success": true,
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhZGRyZXNzIjoiMHhkOGRBNkJGMjY5NjRhRjlEN2VFZDllMDNFNTM0MTVEMzdBQTk2MDQ1IiwiaWF0IjoxNzA3NDA0MjAwLCJleHAiOjE3MDc0OTA2MDB9.abc123",
"address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"expiresAt": "2026-02-09T15:30:00Z"
}

Error Responsesโ€‹

Error CodeMessageCause
INVALID_NONCE"Nonce expired or already used"Get a fresh challenge
INVALID_SIGNATURE"Signature verification failed"Check message format matches exactly
INVALID_MESSAGE"Message format invalid"Ensure SIWE format is correct
EXPIRED_MESSAGE"Message has expired"Sign and verify within 5 minutes

Step 4: Use Authentication Tokenโ€‹

Include the token in the Authorization header for authenticated requests.

Header Formatโ€‹

Authorization: Bearer eyJhbGciOiJIUzI1NiIs...

Example Requestsโ€‹

# Get your profile
curl -H "Authorization: Bearer eyJ..." \
https://api.web3identity.com/api/me

# Get your usage stats
curl -H "Authorization: Bearer eyJ..." \
https://api.web3identity.com/api/me/usage

# Make authenticated API call
curl -H "Authorization: Bearer eyJ..." \
https://api.web3identity.com/api/ens/resolve/vitalik.eth

Session Managementโ€‹

Token Expirationโ€‹

  • Session duration: 24 hours
  • Token format: JWT (JSON Web Token)
  • Storage: Client-side (localStorage, httpOnly cookie, or in-memory)

Checking Session Statusโ€‹

curl -H "Authorization: Bearer eyJ..." \
https://api.web3identity.com/api/auth/session
{
"authenticated": true,
"address": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
"expiresAt": "2026-02-09T15:30:00Z",
"expiresIn": 82340,
"tier": "siwe",
"limits": {
"daily": 200,
"perMinute": 60
}
}

Token Refreshโ€‹

Tokens can be refreshed before expiration without re-signing:

POST /api/auth/siwe/refresh
Authorization: Bearer eyJ...
{
"success": true,
"token": "eyJhbGciOiJIUzI1NiIs...",
"expiresAt": "2026-02-10T15:30:00Z"
}
Refresh Strategy

Refresh tokens when they have less than 1 hour until expiration. The SDK handles this automatically.

Signing Outโ€‹

POST /api/auth/siwe/logout
Authorization: Bearer eyJ...
{
"success": true,
"message": "Session terminated"
}

Complete Code Exampleโ€‹

JavaScript (Full Flow)โ€‹

import { SiweMessage } from 'siwe';
import { createWalletClient, custom, http } from 'viem';
import { mainnet } from 'viem/chains';

const API_BASE = 'https://api.web3identity.com';

class SIWEAuth {
constructor() {
this.token = null;
this.expiresAt = null;
}

async signIn(walletClient) {
// 1. Get challenge
const challenge = await fetch(`${API_BASE}/api/auth/siwe/challenge`)
.then(r => r.json());

const [address] = await walletClient.getAddresses();

// 2. Create SIWE message
const message = new SiweMessage({
domain: challenge.domain,
address,
statement: 'Sign in to Web3 Identity API for enhanced access.',
uri: challenge.uri,
version: challenge.version,
chainId: challenge.chainId,
nonce: challenge.nonce,
issuedAt: challenge.issuedAt,
});

const messageString = message.prepareMessage();

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

// 4. Verify with API
const response = await fetch(`${API_BASE}/api/auth/siwe/verify`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: messageString, signature }),
});

if (!response.ok) {
const error = await response.json();
throw new Error(error.message || 'Verification failed');
}

const { token, expiresAt } = await response.json();
this.token = token;
this.expiresAt = new Date(expiresAt);

return { token, address, expiresAt };
}

async refreshToken() {
if (!this.token) throw new Error('No active session');

const response = await fetch(`${API_BASE}/api/auth/siwe/refresh`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${this.token}` },
});

if (!response.ok) {
throw new Error('Token refresh failed - please sign in again');
}

const { token, expiresAt } = await response.json();
this.token = token;
this.expiresAt = new Date(expiresAt);

return { token, expiresAt };
}

async fetch(endpoint, options = {}) {
// Auto-refresh if expiring soon (< 1 hour)
if (this.expiresAt && this.expiresAt - Date.now() < 3600000) {
await this.refreshToken();
}

return fetch(`${API_BASE}${endpoint}`, {
...options,
headers: {
...options.headers,
...(this.token && { 'Authorization': `Bearer ${this.token}` }),
},
});
}

async signOut() {
if (!this.token) return;

await fetch(`${API_BASE}/api/auth/siwe/logout`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${this.token}` },
});

this.token = null;
this.expiresAt = null;
}

get isAuthenticated() {
return this.token !== null && this.expiresAt > new Date();
}
}

// Usage
const auth = new SIWEAuth();

// With browser wallet
const walletClient = createWalletClient({
chain: mainnet,
transport: custom(window.ethereum),
});

await auth.signIn(walletClient);
console.log('Signed in! Token expires:', auth.expiresAt);

// Make authenticated requests
const me = await auth.fetch('/api/me').then(r => r.json());
console.log('My address:', me.address);

const profile = await auth.fetch('/api/ens/resolve/vitalik.eth').then(r => r.json());
console.log('Profile:', profile);

// Sign out when done
await auth.signOut();

Pythonโ€‹

import requests
from eth_account import Account
from eth_account.messages import encode_defunct
from datetime import datetime
import json

class SIWEAuth:
def __init__(self, private_key: str):
self.account = Account.from_key(private_key)
self.base_url = "https://api.web3identity.com"
self.token = None
self.expires_at = None

def sign_in(self) -> dict:
# 1. Get challenge
challenge = requests.get(f"{self.base_url}/api/auth/siwe/challenge").json()

# 2. Create SIWE message
message = f"""{challenge['domain']} wants you to sign in with your Ethereum account:
{self.account.address}

Sign in to Web3 Identity API for enhanced access.

URI: {challenge['uri']}
Version: {challenge['version']}
Chain ID: {challenge['chainId']}
Nonce: {challenge['nonce']}
Issued At: {challenge['issuedAt']}"""

# 3. Sign message
signable = encode_defunct(text=message)
signed = self.account.sign_message(signable)

# 4. Verify with API
response = requests.post(
f"{self.base_url}/api/auth/siwe/verify",
json={
"message": message,
"signature": signed.signature.hex()
}
)
response.raise_for_status()

data = response.json()
self.token = data["token"]
self.expires_at = datetime.fromisoformat(data["expiresAt"].replace("Z", "+00:00"))

return data

def fetch(self, endpoint: str, **kwargs) -> requests.Response:
headers = kwargs.pop("headers", {})
if self.token:
headers["Authorization"] = f"Bearer {self.token}"
return requests.get(f"{self.base_url}{endpoint}", headers=headers, **kwargs)

def sign_out(self):
if self.token:
requests.post(
f"{self.base_url}/api/auth/siwe/logout",
headers={"Authorization": f"Bearer {self.token}"}
)
self.token = None
self.expires_at = None


# Usage
auth = SIWEAuth(os.environ["WALLET_PRIVATE_KEY"])
auth.sign_in()

me = auth.fetch("/api/me").json()
print(f"Authenticated as: {me['address']}")

usage = auth.fetch("/api/me/usage").json()
print(f"Usage: {usage['used']}/{usage['limit']}")

Using the SDKโ€‹

The SDK handles the entire SIWE flow automatically:

import { ATVClient } from '@atv-eth/x402-sdk';

const client = new ATVClient({
privateKey: process.env.WALLET_KEY,
});

// One-line sign in
await client.signIn();
console.log('Authenticated:', client.isAuthenticated);

// Check status
const status = await client.sessionStatus();
console.log('Expires:', status.expiresAt);

// Make authenticated requests
const usage = await client.myUsage();
const me = await client.me();

// Sign out when done
await client.signOut();

Authenticated Endpointsโ€‹

With a valid SIWE token, you gain access to:

EndpointMethodDescription
/api/meGETYour wallet profile
/api/me/usageGETYour usage statistics
/api/auth/sessionGETSession status
/api/auth/siwe/refreshPOSTRefresh token
/api/auth/siwe/logoutPOSTTerminate session
/api/user/keysGETList your API keys
/api/user/keysPOSTCreate new API key
/api/user/keys/:idDELETERevoke an API key

Troubleshootingโ€‹

ErrorCauseSolution
INVALID_NONCENonce expired or reusedRequest fresh challenge
INVALID_SIGNATURESignature doesn't match messageVerify exact message format
EXPIRED_MESSAGEMessage issuedAt too oldSign within 5 minutes of challenge
TOKEN_EXPIRED24-hour session endedSign in again
DOMAIN_MISMATCHWrong domain in messageUse api.web3identity.com

Security Best Practicesโ€‹

  1. Always use fresh nonces โ€” Request a new challenge before each sign-in
  2. Validate tokens server-side โ€” Don't trust client-provided tokens
  3. Store tokens securely โ€” Use httpOnly cookies or secure storage
  4. Handle expiration gracefully โ€” Auto-refresh or prompt user
  5. Clear tokens on disconnect โ€” Don't leave stale sessions

Next Stepsโ€‹