- name
- siwa-server
- description
- > Use when this capability is needed.
# SIWA Server-Side Verification
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](https://siwa.id/docs).
---
## Quick Start
### 1. Install
```bash
npm install @buildersgarden/siwa viem
```
### 2. Set Environment Variables
```typescript
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"
};
}
```
---
## Framework Middleware
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.
---
## Complete Server Implementation
### Express.js
```typescript
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.status(400).json({ error: "Missing required fields" });
}
const nonce = randomBytes(16).toString("hex");
const issuedAt = new Date().toISOString();
const expirationTime = new Date(Date.now() + 10 * 60 * 1000).toISOString();
// Store nonce with expiration
const key = `${address}:${agentId}:${agentRegistry}`;
nonceStore.set(key, { nonce, expires: Date.now() + 10 * 60 * 1000 });
const chainId = parseChainId(agentRegistry);
res.json({ nonce, issuedAt, expirationTime, chainId });
});
// ─── Verify Endpoint ─────────────────────────────────────────────────
app.post("/api/siwa/verify", async (req, res) => {
const { message, signature } = req.body;
if (!message || !signature) {
return res.status(400).json({ error: "Missing message or signature" });
}
try {
// 1. Parse the SIWA message and resolve the client for this chain
const fields = parseSIWAMessage(message);
const chainId = parseChainId(fields.agentRegistry);
if (!chainId) {
return res.status(400).json({ error: "Invalid agentRegistry format" });
}
const client = resolver.getClient(chainId);
// 2. Verify nonce was issued by us
const key = `${fields.address}:${fields.agentId}:${fields.agentRegistry}`;
const stored = nonceStore.get(key);
if (!stored) {
return res.status(401).json({ error: "Invalid or expired nonce" });
}
if (stored.nonce !== fields.nonce) {
return res.status(401).json({ error: "Nonce mismatch" });
}
if (Date.now() > stored.expires) {
nonceStore.delete(key);
return res.status(401).json({ error: "Nonce expired" });
}
// 3. Verify signature and onchain registration
const result = await verifySIWA(
message,
signature,
process.env.DOMAIN || "localhost",
(nonce) => {
// Validate nonce was issued by us and consume it
if (stored.nonce !== nonce) return false;
nonceStore.delete(key);
return true;
},
client,
);
if (!result.valid) {
return res.status(401).json({ error: result.error });
}
// 4. Create receipt for authenticated API calls
const { receipt } = createReceipt({
address: result.address,
agentId: result.agentId,
agentRegistry: result.agentRegistry,
chainId: result.chainId,
verified: result.verified,
}, {
secret: SIWA_SECRET,
ttl: 3600_000, // 1 hour in ms
});
res.json({
success: true,
address: result.address,
agentId: result.agentId,
verified: result.verified,
receipt,
});
} catch (error: any) {
res.status(400).json({ error: error.message });
}
});
// ─── Protected Endpoint (ERC-8128) ───────────────────────────────────
app.post("/api/agent-action", async (req, res) => {
try {
// Verify the ERC-8128 signed request
const result = await verifyAuthenticatedRequest(req, {
receiptSecret: SIWA_SECRET,
});
if (!result.valid) {
return res.status(401).json({ error: result.error });
}
// Access verified agent info
const { address, agentId } = result.agent;
// Process the action
const { action, params } = req.body;
res.json({
success: true,
agent: { address, agentId, verified },
result: `Processed ${action} for agent #${agentId}`,
});
} catch (error: any) {
res.status(401).json({ error: error.message });
}
});
app.listen(3000, () => {
console.log("SIWA server running on http://localhost:3000");
});
```
### Next.js App Router
**lib/siwa-resolver.ts** (shared module)
```typescript
import { createClientResolver, createMemorySIWANonceStore } from "@buildersgarden/siwa";
export const resolver = createClientResolver();
export const nonceStore = createMemorySIWANonceStore();
```
**app/api/siwa/nonce/route.ts**
```typescript
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 NextResponse.json(result, { status: 403 });
}
return NextResponse.json({
nonce: result.nonce,
issuedAt: result.issuedAt,
expirationTime: result.expirationTime,
chainId,
});
}
```
**app/api/siwa/verify/route.ts**
```typescript
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 });
}
const client = resolver.getClient(chainId);
const result = await verifySIWA(
message,
signature,
process.env.NEXT_PUBLIC_DOMAIN!,
{ nonceStore },
client,
);
if (!result.valid) {
return NextResponse.json({ error: result.error }, { status: 401 });
}
// Create receipt
const { receipt } = createReceipt({
address: result.address,
agentId: result.agentId,
agentRegistry: result.agentRegistry,
chainId: result.chainId,
verified: result.verified,
}, {
secret: SIWA_SECRET,
ttl: 3600_000, // 1 hour in ms
});
return NextResponse.json({
success: true,
address: result.address,
agentId: result.agentId,
verified: result.verified,
receipt,
});
} catch (error: any) {
return NextResponse.json({ error: error.message }, { status: 400 });
}
}
```
**app/api/protected/route.ts**
```typescript
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,
});
if (!result.valid) {
return NextResponse.json({ error: result.error }, { status: 401 });
}
const body = await req.json();
return NextResponse.json({
success: true,
agent: result.agent,
received: body,
});
}
```
---
## SDK Wrappers for Express & Next.js
The SDK provides pre-built middleware for common frameworks:
### Express Middleware
```typescript
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,
});
});
```
### Next.js Wrapper
```typescript
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 };
```
### Express
```typescript
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({
Ver en GitHub