SIWE Authentication
Sign-In with Ethereum (SIWE) doubles your rate limits and enables wallet-based identity.
Benefitsโ
| Feature | Anonymous | SIWE Authenticated |
|---|---|---|
| Daily limit | 100 | 200 |
| Per minute | 30 | 60 |
/api/me access | โ | โ |
| Usage tracking | By IP | By 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
}
| Field | Description |
|---|---|
nonce | Unique challenge string (use within 5 minutes) |
issuedAt | When the challenge was created |
expiresAt | Challenge expiration (5 minutes) |
domain | Domain to use in SIWE message |
uri | URI to use in SIWE message |
version | SIWE version |
chainId | Chain 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.
Using siwe Library (Recommended)โ
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 Code | Message | Cause |
|---|---|---|
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:
| Endpoint | Method | Description |
|---|---|---|
/api/me | GET | Your wallet profile |
/api/me/usage | GET | Your usage statistics |
/api/auth/session | GET | Session status |
/api/auth/siwe/refresh | POST | Refresh token |
/api/auth/siwe/logout | POST | Terminate session |
/api/user/keys | GET | List your API keys |
/api/user/keys | POST | Create new API key |
/api/user/keys/:id | DELETE | Revoke an API key |
Troubleshootingโ
| Error | Cause | Solution |
|---|---|---|
INVALID_NONCE | Nonce expired or reused | Request fresh challenge |
INVALID_SIGNATURE | Signature doesn't match message | Verify exact message format |
EXPIRED_MESSAGE | Message issuedAt too old | Sign within 5 minutes of challenge |
TOKEN_EXPIRED | 24-hour session ended | Sign in again |
DOMAIN_MISMATCH | Wrong domain in message | Use api.web3identity.com |
Security Best Practicesโ
- Always use fresh nonces โ Request a new challenge before each sign-in
- Validate tokens server-side โ Don't trust client-provided tokens
- Store tokens securely โ Use httpOnly cookies or secure storage
- Handle expiration gracefully โ Auto-refresh or prompt user
- Clear tokens on disconnect โ Don't leave stale sessions
Next Stepsโ
- SIWE Tutorial โ Step-by-step integration guide
- API Keys โ Higher limits with prepaid credits
- Rate Limits โ Understanding your limits