SIWA Documentation
Sign In With Agent — v1.0
Agent? If you are an AI agent, read /skill.md instead — it contains structured instructions for wallet creation, registration, and SIWA authentication.
Getting Started
SIWA lets AI agents authenticate with off-chain services by proving ownership of an ERC-8004 identity NFT. Install the SDK, create a wallet, register onchain, and authenticate.
Installation
npm install @buildersgarden/siwaThe @buildersgarden/siwa package exposes four modules via package exports:
import { createWallet, signMessage, getAddress } from '@buildersgarden/siwa/keystore';
import { signSIWAMessage, verifySIWA } from '@buildersgarden/siwa';
import { getAgent, getReputation } from '@buildersgarden/siwa/registry';
import { readMemory, writeMemoryField } from '@buildersgarden/siwa/memory';
import { computeHMAC } from '@buildersgarden/siwa/proxy-auth';Quick Start
The fastest way to try SIWA is with the test harness:
git clone https://github.com/builders-garden/siwa
cd siwa
pnpm install
# Run the full flow: wallet creation → registration → SIWA sign-in
cd packages/siwa-testing
pnpm run devSign Up (Registration)
Register an agent by creating a wallet, building a registration file, and calling the Identity Registry contract.
import { createWallet, signTransaction, getAddress } from '@buildersgarden/siwa/keystore';
import { writeMemoryField } from '@buildersgarden/siwa/memory';
// 1. Create wallet (key goes to proxy, never returned)
const info = await createWallet();
writeMemoryField('Address', info.address);
writeMemoryField('Keystore Backend', info.backend);
// 2. Build registration JSON
const registration = {
type: "AI Agent",
name: "My Agent",
description: "An ERC-8004 registered agent",
services: [{ type: "MCP", url: "https://api.example.com/mcp" }],
active: true
};
// 3. Upload to IPFS or use data URI
const encoded = Buffer.from(JSON.stringify(registration)).toString('base64');
const agentURI = `data:application/json;base64,${encoded}`;
// 4. Register onchain (sign via proxy)
const iface = new ethers.Interface([
'function register(string agentURI) external returns (uint256 agentId)'
]);
const data = iface.encodeFunctionData('register', [agentURI]);
const { signedTx } = await signTransaction({ to: REGISTRY, data, ... });
const tx = await provider.broadcastTransaction(signedTx);Sign In (SIWA Authentication)
Authenticate with any SIWA-aware service using the challenge-response flow.
import { signSIWAMessage } from '@buildersgarden/siwa';
// 1. Request nonce from server
const { nonce, issuedAt, expirationTime } = await fetch(
'https://api.example.com/siwa/nonce',
{ method: 'POST', body: JSON.stringify({ address, agentId, agentRegistry }) }
).then(r => r.json());
// 2. Sign SIWA message (key never exposed — signs via proxy)
const { message, signature } = await signSIWAMessage({
domain: 'api.example.com',
address,
statement: 'Authenticate as a registered ERC-8004 agent.',
uri: 'https://api.example.com/siwa',
agentId,
agentRegistry,
chainId,
nonce,
issuedAt,
expirationTime
});
// 3. Submit to server for verification
const session = await fetch('https://api.example.com/siwa/verify', {
method: 'POST',
body: JSON.stringify({ message, signature })
}).then(r => r.json());
// session.token contains the JWTServer-Side Verification
On the server, use verifySIWA to validate the signature, recover the signer, and check all protocol fields.
import { verifySIWA } from '@buildersgarden/siwa';
// In your /siwa/verify endpoint handler:
const { message, signature } = req.body;
const result = verifySIWA(message, signature, {
domain: 'api.example.com', // must match your server's origin
nonce: storedNonce, // the nonce you issued earlier
});
// result contains:
// {
// address, — recovered signer address
// agentId, — ERC-8004 agent token ID
// agentRegistry, — registry contract identifier
// chainId, — chain ID from the message
// }
// IMPORTANT: also verify onchain ownership
const owner = await identityRegistry.ownerOf(result.agentId);
if (owner.toLowerCase() !== result.address.toLowerCase()) {
throw new Error('Signer does not own this agent NFT');
}
// All checks passed — issue a session token
const token = jwt.sign(result, SECRET, { expiresIn: '1h' });API Reference
@buildersgarden/siwa/keystore
Secure key storage abstraction. None of these functions return the private key.
| Function | Returns | Description |
|---|---|---|
| createWallet() | { address, backend } | Create a new wallet. Key stored in backend, never returned. |
| signMessage(msg) | { signature, address } | Sign a message. Key loaded, used, discarded. |
| signTransaction(tx) | { signedTx, address } | Sign a transaction. Same pattern. |
| signAuthorization(auth) | SignedAuthorization | EIP-7702 delegation signing. |
| getAddress() | string | Get the wallet's public address. |
| hasWallet() | boolean | Check if a wallet exists in the backend. |
With the proxy backend, all signing is delegated over HMAC-authenticated HTTP. getSigner() is not available with proxy.
@buildersgarden/siwa
SIWA protocol operations — message building, signing, and verification.
| Function | Returns | Description |
|---|---|---|
| buildSIWAMessage(fields) | string | Build a formatted SIWA message string. |
| signSIWAMessage(fields) | { message, signature } | Build and sign a SIWA message. |
| verifySIWA(msg, sig, domain, nonceValid, provider, criteria?) | SIWAVerificationResult | Verify a SIWA signature. Optional criteria param validates agent profile/reputation. |
verifySIWA accepts an optional SIWAVerifyCriteria object as the 6th argument to validate agent profile and reputation after the ownership check. When criteria are provided, the result includes the full agent profile.
| Criteria Field | Type | Description |
|---|---|---|
| mustBeActive | boolean | Require metadata.active === true. |
| requiredServices | ServiceType[] | Agent must expose all listed service types (e.g. 'MCP', 'A2A'). |
| requiredTrust | TrustModel[] | Agent must support all listed trust models. |
| minScore | number | Minimum reputation score. |
| minFeedbackCount | number | Minimum reputation feedback count. |
| reputationRegistryAddress | string | Required when using minScore or minFeedbackCount. |
| custom | (agent) => boolean | Custom validation function receiving the full AgentProfile. |
import { verifySIWA } from '@buildersgarden/siwa';
const result = await verifySIWA(
message,
signature,
'api.example.com',
(nonce) => nonceStore.consume(nonce),
provider,
{
mustBeActive: true,
requiredServices: ['MCP'],
requiredTrust: ['reputation'],
minScore: 0.5,
minFeedbackCount: 10,
reputationRegistryAddress: '0x8004BAa1...9b63',
}
);
if (result.valid) {
// result.agent contains the full AgentProfile
console.log(result.agent.metadata.services);
}@buildersgarden/siwa/registry
Read agent profiles and reputation from on-chain registries.
| Function | Returns | Description |
|---|---|---|
| getAgent(agentId, options) | AgentProfile | Read agent profile from the Identity Registry (owner, tokenURI, agentWallet, metadata). |
| getReputation(agentId, options) | ReputationSummary | Read agent reputation summary from the Reputation Registry. |
getAgent fetches and parses the agent's metadata JSON from its tokenURI. Supported URI schemes: ipfs://, data:application/json;base64,, and https://.
import { getAgent, getReputation } from '@buildersgarden/siwa/registry';
const agent = await getAgent(42, {
registryAddress: '0x8004A169...a432',
provider,
});
// agent.owner — NFT owner address
// agent.agentWallet — linked wallet (null if unset)
// agent.metadata — parsed JSON (name, services, active, ...)
const rep = await getReputation(42, {
reputationRegistryAddress: '0x8004BAa1...9b63',
provider,
tag1: 'starred', // filter by reputation tag
});
// rep.score — normalized score
// rep.count — total feedback countThe module exports typed string literals for values defined in the ERC-8004 specification. These provide autocompletion while still accepting custom strings.
| Type | Values |
|---|---|
| ServiceType | 'web' | 'A2A' | 'MCP' | 'OASF' | 'ENS' | 'DID' | 'email' |
| TrustModel | 'reputation' | 'crypto-economic' | 'tee-attestation' |
| ReputationTag | 'starred' | 'reachable' | 'ownerVerified' | 'uptime' | 'successRate' | 'responseTime' | 'blocktimeFreshness' | 'revenues' | 'tradingYield' |
@buildersgarden/siwa/memory
Helpers for reading and writing the agent's public identity state in MEMORY.md.
| Function | Description |
|---|---|
| ensureMemoryExists(path, template) | Initialize MEMORY.md from template if missing. |
| readMemory(path) | Parse MEMORY.md into key-value pairs. |
| writeMemoryField(key, value) | Write a single field to MEMORY.md. |
| hasWalletRecord(path) | Check if wallet info exists in MEMORY.md. |
| isRegistered(path) | Check if agent is registered. |
| appendToMemorySection(section, line) | Append a line to a section. |
@buildersgarden/siwa/proxy-auth
HMAC-SHA256 authentication utilities for the keyring proxy transport.
| Function | Description |
|---|---|
| computeHMAC(secret, method, path, body, timestamp) | Compute HMAC-SHA256 signature for a proxy request. |
Security Model
The agent's private key is the root of its onchain identity. SIWA's security architecture ensures the key never enters the agent process.
Keyring Proxy
All signing is delegated to a separate keyring proxy server over HMAC-authenticated HTTP. The proxy holds the encrypted key and performs all cryptographic operations.
Agent Process Keyring Proxy (port 3100)
(KEYSTORE_BACKEND=proxy) (holds encrypted key)
signMessage("hello")
|
+--> POST /sign-message
+ HMAC-SHA256 header ---> Validates HMAC + timestamp (30s)
Loads key, signs, discards
<-- Returns { signature, address }| Property | Detail |
|---|---|
| Key isolation | Private key lives in a separate OS process; never enters agent memory. |
| Transport auth | HMAC-SHA256 over method + path + body + timestamp; 30-second replay window. |
| Audit trail | Every signing request logged with timestamp, endpoint, source IP. |
| Compromise limit | Even full agent takeover can only request signatures — cannot extract key. |
Threat Model
| Threat | Mitigation |
|---|---|
| Prompt injection exfiltration | Key never in any file the agent reads into context. |
| Context window leakage | Key loaded inside function, used, and discarded — never returned. |
| File system snooping | AES-encrypted V3 JSON Keystore (scrypt KDF). |
| Log / error exposure | Signing functions return only signatures, never raw keys. |
| Accidental commit | No file in the project ever contains the plaintext key. |
MEMORY.md: Public Data Only
The agent's memory file stores only public identity state — address, agentId, registration status, session tokens. The private key is never written to MEMORY.md or any other file the agent reads.
Protocol Specification
Message Format
{domain} wants you to sign in with your Agent account:
{address}
{statement}
URI: {uri}
Version: 1
Agent ID: {agentId}
Agent Registry: {agentRegistry}
Chain ID: {chainId}
Nonce: {nonce}
Issued At: {issuedAt}
[Expiration Time: {expirationTime}]
[Not Before: {notBefore}]
[Request ID: {requestId}]Field Definitions
| Field | Required | Description |
|---|---|---|
| domain | Yes | Origin domain requesting authentication. |
| address | Yes | EIP-55 checksummed Ethereum address. |
| statement | No | Human-readable purpose string. |
| uri | Yes | RFC 3986 URI of the resource. |
| version | Yes | Must be "1". |
| agentId | Yes | ERC-721 tokenId in the Identity Registry. |
| agentRegistry | Yes | eip155:{chainId}:{registryAddress} |
| chainId | Yes | EIP-155 Chain ID. |
| nonce | Yes | Server-generated, >= 8 alphanumeric chars. |
| issuedAt | Yes | RFC 3339 datetime. |
| expirationTime | No | RFC 3339 datetime. |
| notBefore | No | RFC 3339 datetime. |
| requestId | No | Opaque request identifier. |
Authentication Flow
1. Nonce Request — Agent sends address, agentId, agentRegistry to POST /siwa/nonce. Server returns nonce + timestamps.
2. Agent Signs — Agent builds SIWA message and signs via EIP-191 personal_sign.
3. Verification — Agent submits message + signature to POST /siwa/verify.
4. Server Checks — Parse message, recover signer, verify address match, check domain binding, validate nonce + time window, call ownerOf(agentId) onchain.
5. Session — Issue JWT with address, agentId, agentRegistry, chainId.
SIWA vs SIWE
| Aspect | SIWE (EIP-4361) | SIWA |
|---|---|---|
| Purpose | Human wallet auth | Agent identity auth |
| Identity proof | Owns an Ethereum address | Owns an ERC-8004 agent NFT |
| Onchain check | None required | ownerOf(agentId) REQUIRED |
| Extra fields | None | agentId, agentRegistry |
| Signing | EIP-191 | EIP-191 (same) |
| Message prefix | ...your Ethereum account | ...your Agent account |
Contract Addresses
Mainnet
| Chain | Chain ID | Identity Registry | Reputation Registry |
|---|---|---|---|
| Base | 8453 | 0x8004A169...a432 | 0x8004BAa1...9b63 |
Testnets
| Chain | Chain ID | Identity Registry |
|---|---|---|
| Base Sepolia | 84532 | 0x8004A818...BD9e |
| ETH Sepolia | 11155111 | 0x8004a609...8847 |
| Linea Sepolia | 59141 | 0x8004aa7C...82e7 |
| Polygon Amoy | 80002 | 0x8004ad19...2898 |
Solana
| Network | Program ID |
|---|---|
| Devnet | HvF3JqhahcX7JfhbDRYYCJ7S3f6nJdrqu5yi9shyTREp |
Agent Registry String Format
{namespace}:{chainId}:{identityRegistryAddress}
Examples:
eip155:8453:0x8004A169FB4a3325136EB29fA0ceB6D2e539a432 (Base)
eip155:84532:0x8004A818BFB912233c491871b3d84c89A494BD9e (Base Sepolia)Public RPC Endpoints
| Chain | RPC URL |
|---|---|
| Base | https://mainnet.base.org |
| Base Sepolia | https://sepolia.base.org |
| ETH Sepolia | https://rpc.sepolia.org |
| Linea Sepolia | https://rpc.sepolia.linea.build |
| Polygon Amoy | https://rpc-amoy.polygon.technology |
For production use, use a provider like Alchemy or Infura with your own API key.
Explorer
View any registered agent at 8004scan.io