Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
직접 명령은 검토 Prompt를 거치지 않습니다. 실행하기 전에 소스를 확인하세요.
npx skills add https://github.com/tomevault-io/tomes --skill siwa-server명령은 한 줄로 유지됩니다. 복사하기 전에 가로로 스크롤해 전체 내용을 확인하세요.
로컬 사본을 원하시나요? SkillsMP에서 현재 제공할 수 있는 파일을 다운로드하세요.
SKILL.md 표시 중
SOC 직업 분류 기준
| name | siwa-server |
| description | > Use when this capability is needed. |
This guide covers server-side SIWA verification for backends and APIs that need to authenticate agents. No wallet or signing required — only verification.
For full API reference and advanced options, see https://siwa.id/docs.
npm install @buildersgarden/siwa viem
import { parseSIWAMessage, verifySIWA, createClientResolver, parseChainId } from "@buildersgarden/siwa";
// Dynamic client resolver — supports all chains, no hardcoding needed
const resolver = createClientResolver();
async function verifyAgent(message: string, signature: string) {
const fields = parseSIWAMessage(message);
const chainId = parseChainId(fields.agentRegistry);
const client = resolver.getClient(chainId!);
const result = await verifySIWA(
message,
signature,
"api.example.com",
(nonce) => validateAndConsumeNonce(nonce),
client,
);
if (!result.valid) {
throw new Error(result.error);
}
return {
address: result.address,
agentId: result.agentId,
verified: result.verified, // "onchain" | "offline"
};
}
The SDK provides pre-built middleware that handles SIWA sign-in (nonce + verify), ERC-8128 request verification, receipts, and CORS — all in a few lines.
import express from "express";
import { randomBytes } from "crypto";
import { parseSIWAMessage, verifySIWA, createClientResolver, parseChainId } from "@buildersgarden/siwa";
import { createReceipt, verifyReceipt } from "@buildersgarden/siwa/receipt";
import { verifyAuthenticatedRequest } from "@buildersgarden/siwa/erc8128";
const app = express();
app.use(express.json());
// Dynamic client resolver — supports all chains, no hardcoding needed
const resolver = createClientResolver();
// In-memory nonce store (use Redis in production)
const nonceStore = new Map<string, { nonce: string; expires: number }>();
const SIWA_SECRET = process.env.SIWA_SECRET || "change-me-in-production";
// ─── Nonce Endpoint ──────────────────────────────────────────────────
app.post("/api/siwa/nonce", (req, res) => {
const { address, agentId, agentRegistry } = req.body;
if (!address || agentId === undefined || !agentRegistry) {
return res.().({ : });
}
nonce = ().();
issuedAt = ().();
expirationTime = (.() + * * ).();
key = ;
nonceStore.(key, { nonce, : .() + * * });
chainId = (agentRegistry);
res.({ nonce, issuedAt, expirationTime, chainId });
});
app.(, (req, res) => {
{ message, signature } = req.;
(!message || !signature) {
res.().({ : });
}
{
fields = (message);
chainId = (fields.);
(!chainId) {
res.().({ : });
}
client = resolver.(chainId);
key = ;
stored = nonceStore.(key);
(!stored) {
res.().({ : });
}
(stored. !== fields.) {
res.().({ : });
}
(.() > stored.) {
nonceStore.(key);
res.().({ : });
}
result = (
message,
signature,
process.. || ,
{
(stored. !== nonce) ;
nonceStore.(key);
;
},
client,
);
(!result.) {
res.().({ : result. });
}
{ receipt } = ({
: result.,
: result.,
: result.,
: result.,
: result.,
}, {
: ,
: ,
});
res.({
: ,
: result.,
: result.,
: result.,
receipt,
});
} (: ) {
res.().({ : error. });
}
});
app.(, (req, res) => {
{
result = (req, {
: ,
});
(!result.) {
res.().({ : result. });
}
{ address, agentId } = result.;
{ action, params } = req.;
res.({
: ,
: { address, agentId, verified },
: ,
});
} (: ) {
res.().({ : error. });
}
});
app.(, {
.();
});
lib/siwa-resolver.ts (shared module)
import { createClientResolver, createMemorySIWANonceStore } from "@buildersgarden/siwa";
export const resolver = createClientResolver();
export const nonceStore = createMemorySIWANonceStore();
app/api/siwa/nonce/route.ts
import { NextResponse } from "next/server";
import { createSIWANonce, parseChainId } from "@buildersgarden/siwa";
import { resolver, nonceStore } from "@/lib/siwa-resolver";
export async function POST(req: Request) {
const { address, agentId, agentRegistry } = await req.json();
if (!address || agentId === undefined || !agentRegistry) {
return NextResponse.json({ error: "Missing required fields" }, { status: 400 });
}
const chainId = parseChainId(agentRegistry);
if (!chainId) {
return NextResponse.json({ error: "Invalid agentRegistry format" }, { status: 400 });
}
const client = resolver.getClient(chainId);
const result = await createSIWANonce(
{ address, agentId, agentRegistry },
client,
{ nonceStore },
);
if (result.status !== "nonce_issued") {
return .(result, { : });
}
.({
: result.,
: result.,
: result.,
chainId,
});
}
app/api/siwa/verify/route.ts
import { NextResponse } from "next/server";
import { parseSIWAMessage, verifySIWA, parseChainId } from "@buildersgarden/siwa";
import { createReceipt } from "@buildersgarden/siwa/receipt";
import { resolver, nonceStore } from "@/lib/siwa-resolver";
const SIWA_SECRET = process.env.SIWA_SECRET!;
export async function POST(req: Request) {
const { message, signature } = await req.json();
if (!message || !signature) {
return NextResponse.json({ error: "Missing message or signature" }, { status: 400 });
}
try {
const fields = parseSIWAMessage(message);
const chainId = parseChainId(fields.agentRegistry);
if (!chainId) {
return NextResponse.json({ error: "Invalid agentRegistry format" }, { status: 400 });
}
client = resolver.(chainId);
result = (
message,
signature,
process..!,
{ nonceStore },
client,
);
(!result.) {
.({ : result. }, { : });
}
{ receipt } = ({
: result.,
: result.,
: result.,
: result.,
: result.,
}, {
: ,
: ,
});
.({
: ,
: result.,
: result.,
: result.,
receipt,
});
} (: ) {
.({ : error. }, { : });
}
}
app/api/protected/route.ts
import { NextResponse } from "next/server";
import { verifyAuthenticatedRequest } from "@buildersgarden/siwa/erc8128";
const SIWA_SECRET = process.env.SIWA_SECRET!;
export async function GET(req: Request) {
const result = await verifyAuthenticatedRequest(req, {
receiptSecret: SIWA_SECRET,
});
if (!result.valid) {
return NextResponse.json({ error: result.error }, { status: 401 });
}
return NextResponse.json({
message: `Hello Agent #${result.agent.agentId}!`,
agent: result.agent,
});
}
export async function POST(req: Request) {
const result = await verifyAuthenticatedRequest(req, {
receiptSecret: SIWA_SECRET,
});
(!result.) {
.({ : result. }, { : });
}
body = req.();
.({
: ,
: result.,
: body,
});
}
The SDK provides pre-built middleware for common frameworks:
import express from "express";
import { siwaMiddleware, siwaJsonParser, siwaCors } from "@buildersgarden/siwa/express";
const app = express();
// Apply SIWA middleware to protected routes — no hardcoded chain needed
app.use("/api/protected", siwaMiddleware({
receiptSecret: process.env.SIWA_SECRET!,
}));
app.get("/api/protected/data", (req, res) => {
// req.agent contains verified agent info
const { address, agentId, verified } = req.agent;
res.json({
message: `Hello Agent #${agentId}!`,
address,
verified,
});
});
import { withSiwa, siwaOptions } from "@buildersgarden/siwa/next";
export const POST = withSiwa(async (agent, req) => {
const body = await req.json();
return { agent: { address: agent.address, agentId: agent.agentId }, received: body };
}, {
receiptSecret: process.env.SIWA_SECRET!,
allowedSignerTypes: ['eoa', 'sca'],
});
export { siwaOptions as OPTIONS };
import express from "express";
import { siwaMiddleware, siwaJsonParser, siwaCors } from "@buildersgarden/siwa/express";
const app = express();
app.use(siwaJsonParser());
app.use(siwaCors());
app.get("/api/protected", siwaMiddleware({
receiptSecret: process.env.SIWA_SECRET!,
}), (req, res) => {
res.json({ agent: req.agent });
});
import Fastify from "fastify";
import { siwaPlugin, siwaAuth } from "@buildersgarden/siwa/fastify";
const fastify = Fastify();
await fastify.register(siwaPlugin);
fastify.post("/api/protected", {
preHandler: siwaAuth({
receiptSecret: process.env.SIWA_SECRET!,
allowedSignerTypes: ['eoa'],
}),
}, async (req) => {
return { agent: req.agent };
});
await fastify.listen({ port: 3000 });
import { Hono } from "hono";
import { siwaMiddleware, siwaCors } from "@buildersgarden/siwa/hono";
const app = new Hono();
app.use("*", siwaCors());
app.post("/api/protected", siwaMiddleware({
receiptSecret: process.env.SIWA_SECRET!,
}), (c) => {
return c.json({ agent: c.get("agent") });
});
export default app;
Add pay-per-request or pay-once monetization to any SIWA-protected endpoint. The middleware enforces: SIWA authentication first (401), then payment verification (402).
import { createFacilitatorClient, type X402Config } from "@buildersgarden/siwa/x402";
const facilitator = createFacilitatorClient({
url: "https://api.cdp.coinbase.com/platform/v2/x402",
});
const x402: X402Config = {
facilitator,
resource: { url: "/api/premium", description: "Premium data" },
accepts: [{
scheme: "exact",
network: "eip155:84532",
amount: "1000000", // 1 USDC (6 decimals)
asset: "0x036CbD53842c5426634e7929541eC2318f3dCF7e",
payTo: "0xYourAddress",
maxTimeoutSeconds: 60,
}],
};
import { createMemoryX402SessionStore } from "@buildersgarden/siwa/x402";
const x402WithSession: X402Config = {
...x402,
session: {
store: createMemoryX402SessionStore(),
ttl: 3_600_000, // 1 hour
},
};
Next.js:
export const POST = withSiwa(async (agent, req, payment) => {
return { agent, txHash: payment?.txHash };
}, { x402 });
export const OPTIONS = () => siwaOptions({ x402: true });
Express:
app.post("/api/premium", siwaMiddleware({ x402 }), (req, res) => {
res.json({ agent: req.agent, txHash: req.payment?.txHash });
});
app.use(siwaCors({ x402: true }));
Hono:
app.use("*", siwaCors({ x402: true }));
app.post("/api/premium", siwaMiddleware({ x402 }), (c) => {
return c.json({ agent: c.get("agent"), txHash: c.get("payment")?.txHash });
});
Fastify:
await fastify.register(siwaPlugin, { x402: true });
fastify.post("/api/premium", { preHandler: siwaAuth({ x402 }) }, async (req) => {
return { agent: req.agent, txHash: req.payment?.txHash };
});
verifySIWA(
message: string, // Full SIWA message string
signature: string, // EIP-191 signature hex
expectedDomain: string, // Must match message domain
nonceValid: NonceValidator, // Nonce validation (see below)
client: PublicClient, // viem client for onchain checks
criteria?: SIWAVerifyCriteria, // Optional verification criteria
)
// NonceValidator: callback, stateless token, or nonce store
type NonceValidator =
| ((nonce: string) => boolean | Promise<boolean>)
| { nonceToken: string; secret: string }
| { nonceStore: SIWANonceStore };
import { createSIWANonce, verifySIWA } from "@buildersgarden/siwa";
import { createMemorySIWANonceStore } from "@buildersgarden/siwa/nonce-store";
const nonceStore = createMemorySIWANonceStore();
// Issue nonce
const nonce = await createSIWANonce(params, client, { nonceStore });
// Verify — nonceStore consumes the nonce automatically
const result = await verifySIWA(
message, signature, "example.com",
{ nonceStore },
client,
{ allowedSignerTypes: ['eoa'] },
);
Available stores: createMemorySIWANonceStore() (single-process), createRedisSIWANonceStore(redis) (multi-instance), createKVSIWANonceStore(kv) (Cloudflare Workers).
Servers can challenge agents at sign-in or during authenticated requests to prove they are AI agents.
import { createSIWANonce } from "@buildersgarden/siwa";
const result = await createSIWANonce(
{ address, agentId, agentRegistry },
client,
{
secret: SIWA_SECRET,
captchaPolicy: async ({ address }) => {
const known = await db.agents.exists(address);
return known ? null : 'medium';
},
captchaOptions: { secret: SIWA_SECRET },
},
);
if (result.status === 'captcha_required') {
return res.json(result); // Agent solves and resubmits
}
export const POST = withSiwa(handler, {
captchaPolicy: () => Math.random() < 0.05 ? 'easy' : null,
captchaOptions: { secret: process.env.SIWA_SECRET! },
});
| Level | Time Limit | Constraints |
|---|---|---|
easy | 30s | Line count + ASCII sum of first chars |
medium | 20s | + word count |
hard | 15s | + character at specific position |
extreme | 10s | + total character count |
For authenticated API calls, agents sign HTTP requests with ERC-8128:
import { verifyAuthenticatedRequest } from "@buildersgarden/siwa/erc8128";
async function handleRequest(req: Request) {
const result = await verifyAuthenticatedRequest(req, {
receiptSecret: process.env.SIWA_SECRET!,
// Optional: nonce store for replay protection
nonceStore: myNonceStore,
});
if (!result.valid) {
return new Response(JSON.stringify({ error: result.error }), {
status: 401,
});
}
// result.agent contains:
// - address: string
// - agentId: number
// - agentRegistry: string
// - chainId: number
// - signerType?: 'eoa' | 'sca'
return new Response(JSON.stringify({ agent: result.agent }));
}
createMemorySIWANonceStore() (single-process), createRedisSIWANonceStore(redis) (multi-instance), or createKVSIWANonceStore(kv) (Cloudflare Workers)SIWANonceStore interface (just issue + consume)@buildersgarden/siwa)| Export | Description |
|---|---|
signSIWAMessage(fields, signer) | Sign a SIWA authentication message |
parseSIWAMessage(message) | Parse SIWA message string to fields |
verifySIWA(message, signature, domain, nonceValid, client, criteria?) | Verify signature + onchain registration. nonceValid accepts callback, { nonceToken, secret }, or { nonceStore }. |
createSIWANonce(params, client, options?) | Issue nonce with optional { nonceStore } for server-side tracking |
buildSIWAMessage(fields) | Build SIWA message from fields |
@buildersgarden/siwa/receipt)| Export | Description |
|---|---|
createReceipt(payload, options) | Create HMAC-signed receipt. Options: { secret, ttl? } (ttl in ms, default 30min). Returns { receipt, expiresAt }. |
verifyReceipt(receipt, secret) | Verify and decode receipt. Returns ReceiptPayload or null. |
DEFAULT_RECEIPT_TTL | Default receipt validity: 30 minutes (1800000 ms) |
@buildersgarden/siwa/erc8128)| Export | Description |
|---|---|
verifyAuthenticatedRequest(req, options) | Verify ERC-8128 signed HTTP request. Options: VerifyOptions. Returns AuthResult. |
VerifyOptions | Type: { receiptSecret, rpcUrl?, verifyOnchain?, publicClient?, nonceStore?, allowedSignerTypes? } |
AuthResult | Type: { valid: true, agent: SiwaAgent } | { valid: false, error: string } |
SiwaAgent | Type: { address, agentId, agentRegistry, chainId, signerType? } |
@buildersgarden/siwa/client-resolver)| Export | Description |
|---|---|
createClientResolver(options?) | Create a resolver that lazily creates and caches PublicClient per chain. Options: { rpcOverrides?, allowedChainIds? }. |
parseChainId(agentRegistry) | Extract chain ID from eip155:{chainId}:{address} format. Returns number | null. |
ClientResolver | Interface: { getClient(chainId), isSupported(chainId), supportedChainIds() } |
ClientResolverOptions | Type: { rpcOverrides?: Record<number, string>, allowedChainIds?: number[] } |
@buildersgarden/siwa/nonce-store)| Export | Description |
|---|---|
SIWANonceStore | Interface: { issue(nonce, ttlMs), consume(nonce) } |
createMemorySIWANonceStore() | In-memory store with TTL expiry (single-process) |
createRedisSIWANonceStore(redis, prefix?) | Redis-backed store. Default prefix: "siwa:nonce:" |
createKVSIWANonceStore(kv, prefix?) | Cloudflare Workers KV store |
RedisLikeClient | Interface for ioredis / node-redis |
KVNamespaceLike | Interface for Cloudflare KV bindings |
@buildersgarden/siwa/express)| Export | Description |
|---|---|
siwaMiddleware(options?) | Auth middleware. Sets req.agent (and req.payment when x402). Options: { receiptSecret?, rpcUrl?, verifyOnchain?, publicClient?, allowedSignerTypes?, x402?: X402Config } |
siwaJsonParser() | JSON parser with rawBody capture for Content-Digest verification |
siwaCors(options?) | CORS middleware with SIWA headers |
@buildersgarden/siwa/next)| Export | Description |
|---|---|
withSiwa(handler, options?) | Wrap route handler with SIWA auth. Handler: (agent, req, payment?) => object | Response. Options: { receiptSecret?, rpcUrl?, verifyOnchain?, allowedSignerTypes?, x402?: X402Config } |
siwaOptions(corsOpts?) | Return 204 OPTIONS response with CORS headers. Pass { x402: true } to include payment headers. |
corsJson(data, init?) | JSON Response with CORS headers. init: { status?: number } |
corsHeaders() | Returns CORS headers object |
@buildersgarden/siwa/fastify)| Export | Description |
|---|---|
siwaPlugin | Fastify plugin: CORS with SIWA headers. Uses @fastify/cors if available. |
siwaAuth(options?) | preHandler hook: verifies ERC-8128 + receipt. Sets req.agent. Options: { receiptSecret?, rpcUrl?, verifyOnchain?, publicClient?, allowedSignerTypes? } |
@buildersgarden/siwa/hono)| Export | Description |
|---|---|
siwaMiddleware(options?) | Auth middleware. Sets c.set("agent", agent). Options: { receiptSecret?, rpcUrl?, verifyOnchain?, publicClient?, allowedSignerTypes? } |
siwaCors(options?) | CORS middleware with SIWA headers + OPTIONS preflight |
# Required
SIWA_SECRET=your-32-byte-random-secret
# Optional — override RPC endpoints per chain (createClientResolver checks these)
RPC_URL_84532=https://sepolia.base.org
RPC_URL_8453=https://mainnet.base.org
RPC_URL_11155111=https://rpc.sepolia.org
# Optional
DOMAIN=api.example.com
Note:
createClientResolver()includes built-in RPC endpoints for all supported chains (Base, Base Sepolia, ETH Sepolia, Linea Sepolia, Polygon Amoy). SetRPC_URL_{chainId}environment variables only when you need to override the defaults (e.g., for a private RPC provider).
Converted and distributed by TomeVault — claim your Tome and manage your conversions.