
Viem Siwe
- 4 installs
- Updated January 26, 2026
- melonask/viem-siwe-skills
Helps with ai & agent building tasks.
About
viem-siwe is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- viem-siwe
- AI & Agent Building
- AI-coding skill
Viem Siwe by the numbers
- 4 all-time installs (skills.sh)
- Ranked #13,372 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 4, 2026 (Skillselion catalog sync)
npx skills add https://github.com/melonask/viem-siwe-skills --skill viem-siweAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 4 |
|---|---|
| Last updated | January 26, 2026 |
| Repository | melonask/viem-siwe-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
Viem SIWE
This skill provides expertise in implementing Sign-In with Ethereum (SIWE) adhering to EIP-4361 using viem.
Reference Implementation
For a complete, copy-pasteable implementation of a SIWE auth module, refer to references/implementation.md.
This implementation includes:
siwe.ts: Core logic for nonce generation, message creation, parsing, and verification.index.ts: Public API for the auth module.
API Documentation
For detailed API documentation of viem's SIWE utilities (createSiweMessage, verifySiweMessage, etc.), refer to references/api-docs.md.
Critical Implementation Details
Nonce Management
- Always generate a unique nonce for every login attempt.
- Store nonces with an expiration (TTL) on the backend.
- Verify and consume the nonce upon signature validation to prevent replay attacks.
Message Verification
- Verify Domain: Ensure the
domainin the message matches the host to prevent phishing. - Verify Chain ID: Ensure the
chainIdmatches the expected network. - Check Expiration: Respect
expirationTimeandnotBeforefields.
Smart Contract Wallets (ERC-1271)
When verifying signatures from smart contract wallets:
- Use a
PublicClientinstance inverifySiweMessage. - Do not rely solely on
verifyMessagewhich only works for EOAs.
Viem SIWE Skills
This skill provides a complete, production-ready implementation of Sign-In with Ethereum (SIWE) using viem v2.
Installation
To add this skill to your project, run:
npx skills add melonask/viem-siwe-skillsFeatures
- EIP-4361 Compliant: Full support for the SIWE standard.
- Nonce Management: Secure generation and consumption of nonces to prevent replay attacks.
- ERC-1271 Support: Verified signature checks for both EOAs and Smart Contract Wallets.
- Session Handling: Patterns for creating and managing authenticated sessions.
Structure
SKILL.md: Main entry point with usage triggers and high-level guidance.references/implementation.md: The core auth module implementation (siwe.tsandindex.ts).references/api-docs.md: Reference documentation forviemSIWE utilities.
Security Best Practices Included
1. Strict Nonce Verification: Fails authentication if the nonce is missing, expired, or already used. 2. Domain/Chain ID Binding: Ensures signatures are only valid for the intended host and network. 3. Time-based Validation: Respects issuedAt, notBefore, and expirationTime.
API Documentation
verifySiweMessage
Verifies EIP-4361 formatted message was signed.
Usage
import { account, walletClient, publicClient } from './client'
import { message } from './message'
const signature = await walletClient.signMessage({ account, message })
const valid = await publicClient.verifySiweMessage({
message,
signature,
})Parameters
message:string- EIP-4361 formatted message to be verified.signature:Hex- The signature that was generated by signing the message.address:Address(optional) - Ethereum address to check against.blockNumber:number(optional) - For Smart Contract Accounts, block number to check deployment.blockTag:string(optional) - For Smart Contract Accounts.domain:string(optional) - RFC 3986 authority to check against.nonce:string(optional) - Random string to check against.scheme:string(optional) - URI scheme to check against.time:Date(optional) - Current time to check expiration.
createSiweMessage
Creates EIP-4361 formatted message.
Usage
import { createSiweMessage } from 'viem/siwe'
const message = createSiweMessage({
address: '0xA0Cf798816D4b9b9866b5330EEa46a18382f251e',
chainId: 1,
domain: 'example.com',
nonce: 'foobarbaz',
uri: 'https://example.com/path',
version: '1',
})Parameters
address:Address- The Ethereum address performing the signing.chainId:number- The EIP-155 Chain ID.domain:string- RFC 3986 authority requesting the signing.nonce:string- Random string for replay protection.uri:string- RFC 3986 URI referring to the resource.version:'1'- SIWE Message version.expirationTime:Date(optional)issuedAt:Date(optional)notBefore:Date(optional)requestId:string(optional)resources:string[](optional)scheme:string(optional)statement:string(optional)
generateSiweNonce
Generates random EIP-4361 nonce.
Usage
import { generateSiweNonce } from 'viem/siwe'
const nonce = generateSiweNonce()parseSiweMessage
Parses EIP-4361 formatted message into message fields object.
Usage
import { parseSiweMessage } from 'viem/siwe'
const fields = parseSiweMessage(message)validateSiweMessage
Validates EIP-4361 message.
Usage
import { validateSiweMessage } from 'viem/siwe'
const valid = validateSiweMessage({
address: '0xd...',
message: parsedMessage
})Reference Implementation
siwe.ts
/**
* Sign-In with Ethereum (SIWE) implementation using viem
*/
import { verifyMessage, type Address, type Hex, type PublicClient } from "viem";
import {
createSiweMessage,
generateSiweNonce,
parseSiweMessage,
validateSiweMessage,
type SiweMessage,
} from "viem/siwe";
// ============================================================================
// Types
// ============================================================================
export interface SiweSessionData {
address: Address;
chainId: number;
domain: string;
nonce: string;
issuedAt: string;
expirationTime?: string;
notBefore?: string;
requestId?: string;
resources?: string[];
}
export interface SiweVerifyParams {
message: string;
signature: Hex;
expectedDomain?: string;
expectedChainId?: number;
}
export interface SiweSession {
address: Address;
chainId: number;
isValid: boolean;
expiresAt?: Date;
data: SiweSessionData;
}
export interface NonceResult {
nonce: string;
issuedAt: string;
expiresAt: string;
}
// ============================================================================
// Nonce Management
// ============================================================================
// In-memory nonce store (replace with Redis/database in production)
const nonceStore = new Map<string, { nonce: string; createdAt: number }>();
const NONCE_TTL = 5 * 60 * 1000; // 5 minutes
/**
* Generate a new SIWE nonce
*/
export function generateNonce(): NonceResult {
const nonce = generateSiweNonce();
const issuedAt = new Date().toISOString();
const expiresAt = new Date(Date.now() + NONCE_TTL).toISOString();
// Store nonce
nonceStore.set(nonce, { nonce, createdAt: Date.now() });
// Clean up expired nonces
cleanupExpiredNonces();
return { nonce, issuedAt, expiresAt };
}
/**
* Verify a nonce is valid and unused
*/
export function verifyNonce(nonce: string): boolean {
const stored = nonceStore.get(nonce);
if (!stored) return false;
// Check if expired
if (Date.now() - stored.createdAt > NONCE_TTL) {
nonceStore.delete(nonce);
return false;
}
return true;
}
/**
* Consume a nonce (mark as used)
*/
export function consumeNonce(nonce: string): boolean {
if (!verifyNonce(nonce)) return false;
nonceStore.delete(nonce);
return true;
}
/**
* Clean up expired nonces
*/
function cleanupExpiredNonces(): void {
const now = Date.now();
for (const [nonce, data] of nonceStore.entries()) {
if (now - data.createdAt > NONCE_TTL) {
nonceStore.delete(nonce);
}
}
}
// ============================================================================
// Message Creation
// ============================================================================
export interface CreateMessageParams {
address: Address;
chainId: number;
domain: string;
uri: string;
nonce?: string;
statement?: string;
expirationTime?: Date;
notBefore?: Date;
requestId?: string;
resources?: string[];
}
/**
* Create a SIWE message
*/
export function createMessage(params: CreateMessageParams): string {
const nonce = params.nonce ?? generateSiweNonce();
return createSiweMessage({
address: params.address,
chainId: params.chainId,
domain: params.domain,
uri: params.uri,
nonce,
version: "1",
statement: params.statement,
expirationTime: params.expirationTime,
notBefore: params.notBefore,
requestId: params.requestId,
resources: params.resources,
});
}
// ============================================================================
// Message Parsing
// ============================================================================
/**
* Parse a SIWE message string into components
*/
export function parseMessage(message: string): SiweMessage | null {
try {
return parseSiweMessage(message) as SiweMessage;
} catch {
return null;
}
}
// ============================================================================
// Signature Verification
// ============================================================================
/**
* Verify a SIWE signature
*/
export async function verifySiweSignature(
params: SiweVerifyParams,
client?: PublicClient,
): Promise<SiweSession> {
const { message, signature, expectedDomain, expectedChainId } = params;
// Parse the message
const parsed = parseMessage(message);
if (!parsed) {
return {
address: "0x0" as Address,
chainId: 0,
isValid: false,
data: {} as SiweSessionData,
};
}
// Validate message fields
const isMessageValid =
parsed.address &&
validateSiweMessage({
address: parsed.address,
message: parsed as SiweMessage,
});
if (!isMessageValid) {
return {
address: parsed.address ?? ("0x0" as Address),
chainId: parsed.chainId ?? 0,
isValid: false,
data: extractSessionData(parsed),
};
}
// Verify domain if specified
if (expectedDomain && parsed.domain !== expectedDomain) {
return {
address: parsed.address ?? ("0x0" as Address),
chainId: parsed.chainId ?? 0,
isValid: false,
data: extractSessionData(parsed),
};
}
// Verify chain ID if specified
if (expectedChainId && parsed.chainId !== expectedChainId) {
return {
address: parsed.address ?? ("0x0" as Address),
chainId: parsed.chainId ?? 0,
isValid: false,
data: extractSessionData(parsed),
};
}
// Verify the nonce
if (parsed.nonce && !consumeNonce(parsed.nonce)) {
return {
address: parsed.address ?? ("0x0" as Address),
chainId: parsed.chainId ?? 0,
isValid: false,
data: extractSessionData(parsed),
};
}
// Verify the signature
let isSignatureValid = false;
if (client) {
// Use public client for smart contract wallet verification (ERC-1271)
isSignatureValid = await client.verifyMessage({
address: parsed.address!,
message,
signature,
});
} else {
// Basic signature verification for EOA
isSignatureValid = await verifyMessage({
address: parsed.address!,
message,
signature,
});
}
// Calculate expiration
let expiresAt: Date | undefined;
if (parsed.expirationTime) {
expiresAt = new Date(parsed.expirationTime);
}
return {
address: parsed.address!,
chainId: parsed.chainId!,
isValid: isSignatureValid,
expiresAt,
data: extractSessionData(parsed),
};
}
/**
* Extract session data from parsed message
*/
function extractSessionData(parsed: SiweMessage): SiweSessionData {
return {
address: parsed.address ?? ("0x0" as Address),
chainId: parsed.chainId ?? 0,
domain: parsed.domain ?? "",
nonce: parsed.nonce ?? "",
issuedAt: parsed.issuedAt?.toISOString() ?? new Date().toISOString(),
expirationTime: parsed.expirationTime?.toISOString(),
notBefore: parsed.notBefore?.toISOString(),
requestId: parsed.requestId,
resources: parsed.resources,
};
}
// ============================================================================
// Session Management
// ============================================================================
// In-memory session store (replace with Redis/database in production)
const sessionStore = new Map<string, SiweSession>();
/**
* Create a session from verified SIWE
*/
export function createSession(session: SiweSession): string {
// Generate session ID
const sessionId = crypto.randomUUID();
// Store session
sessionStore.set(sessionId, session);
return sessionId;
}
/**
* Get session by ID
*/
export function getSession(sessionId: string): SiweSession | null {
const session = sessionStore.get(sessionId);
if (!session) return null;
// Check expiration
if (session.expiresAt && new Date() > session.expiresAt) {
sessionStore.delete(sessionId);
return null;
}
return session;
}
/**
* Delete session (logout)
*/
export function deleteSession(sessionId: string): boolean {
return sessionStore.delete(sessionId);
}
/**
* Clear all sessions for an address
*/
export function clearSessionsForAddress(address: Address): number {
let cleared = 0;
for (const [id, session] of sessionStore.entries()) {
if (session.address.toLowerCase() === address.toLowerCase()) {
sessionStore.delete(id);
cleared++;
}
}
return cleared;
}index.ts
/**
* Sign-In with Ethereum implementation for secure authentication
*/
// Export all SIWE functions
export * from "./siwe";
// ============================================================================
// Auth Module API
// ============================================================================
import type { PublicClient } from "viem";
import {
generateNonce as _generateNonce,
verifySiweSignature,
createMessage as _createMessage,
createSession,
getSession,
deleteSession,
type SiweSession,
type NonceResult,
type CreateMessageParams,
type SiweVerifyParams,
} from "./siwe";
/**
* Auth module providing SIWE authentication
*/
export const auth = {
/**
* Generate a new nonce for SIWE
*/
nonce(): NonceResult {
return _generateNonce();
},
/**
* Create a SIWE message for signing
*/
createMessage(params: CreateMessageParams): string {
return _createMessage(params);
},
/**
* Verify a signed SIWE message
*/
async verify(
params: SiweVerifyParams,
client?: PublicClient,
): Promise<{ session: SiweSession; sessionId?: string }> {
const session = await verifySiweSignature(params, client);
if (session.isValid) {
const sessionId = createSession(session);
return { session, sessionId };
}
return { session };
},
/**
* Get current session info
*/
me(sessionId: string): SiweSession | null {
return getSession(sessionId);
},
/**
* Logout (delete session)
*/
logout(sessionId: string): boolean {
return deleteSession(sessionId);
},
};
// ============================================================================
// Types Re-export
// ============================================================================
export type {
SiweSession,
SiweSessionData,
SiweVerifyParams,
CreateMessageParams,
NonceResult,
} from "./siwe";