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/siwa

The @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 dev

Sign 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 JWT

Server-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.

FunctionReturnsDescription
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)SignedAuthorizationEIP-7702 delegation signing.
getAddress()stringGet the wallet's public address.
hasWallet()booleanCheck 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.

FunctionReturnsDescription
buildSIWAMessage(fields)stringBuild a formatted SIWA message string.
signSIWAMessage(fields){ message, signature }Build and sign a SIWA message.
verifySIWA(msg, sig, domain, nonceValid, provider, criteria?)SIWAVerificationResultVerify 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 FieldTypeDescription
mustBeActivebooleanRequire metadata.active === true.
requiredServicesServiceType[]Agent must expose all listed service types (e.g. 'MCP', 'A2A').
requiredTrustTrustModel[]Agent must support all listed trust models.
minScorenumberMinimum reputation score.
minFeedbackCountnumberMinimum reputation feedback count.
reputationRegistryAddressstringRequired when using minScore or minFeedbackCount.
custom(agent) => booleanCustom 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.

FunctionReturnsDescription
getAgent(agentId, options)AgentProfileRead agent profile from the Identity Registry (owner, tokenURI, agentWallet, metadata).
getReputation(agentId, options)ReputationSummaryRead 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 count

The module exports typed string literals for values defined in the ERC-8004 specification. These provide autocompletion while still accepting custom strings.

TypeValues
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.

FunctionDescription
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.

FunctionDescription
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 }
PropertyDetail
Key isolationPrivate key lives in a separate OS process; never enters agent memory.
Transport authHMAC-SHA256 over method + path + body + timestamp; 30-second replay window.
Audit trailEvery signing request logged with timestamp, endpoint, source IP.
Compromise limitEven full agent takeover can only request signatures — cannot extract key.

Threat Model

ThreatMitigation
Prompt injection exfiltrationKey never in any file the agent reads into context.
Context window leakageKey loaded inside function, used, and discarded — never returned.
File system snoopingAES-encrypted V3 JSON Keystore (scrypt KDF).
Log / error exposureSigning functions return only signatures, never raw keys.
Accidental commitNo 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

FieldRequiredDescription
domainYesOrigin domain requesting authentication.
addressYesEIP-55 checksummed Ethereum address.
statementNoHuman-readable purpose string.
uriYesRFC 3986 URI of the resource.
versionYesMust be "1".
agentIdYesERC-721 tokenId in the Identity Registry.
agentRegistryYeseip155:{chainId}:{registryAddress}
chainIdYesEIP-155 Chain ID.
nonceYesServer-generated, >= 8 alphanumeric chars.
issuedAtYesRFC 3339 datetime.
expirationTimeNoRFC 3339 datetime.
notBeforeNoRFC 3339 datetime.
requestIdNoOpaque 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

AspectSIWE (EIP-4361)SIWA
PurposeHuman wallet authAgent identity auth
Identity proofOwns an Ethereum addressOwns an ERC-8004 agent NFT
Onchain checkNone requiredownerOf(agentId) REQUIRED
Extra fieldsNoneagentId, agentRegistry
SigningEIP-191EIP-191 (same)
Message prefix...your Ethereum account...your Agent account

Contract Addresses

Mainnet

ChainChain IDIdentity RegistryReputation Registry
Base84530x8004A169...a4320x8004BAa1...9b63

Testnets

ChainChain IDIdentity Registry
Base Sepolia845320x8004A818...BD9e
ETH Sepolia111551110x8004a609...8847
Linea Sepolia591410x8004aa7C...82e7
Polygon Amoy800020x8004ad19...2898

Solana

NetworkProgram ID
DevnetHvF3JqhahcX7JfhbDRYYCJ7S3f6nJdrqu5yi9shyTREp

Agent Registry String Format

{namespace}:{chainId}:{identityRegistryAddress}

Examples:
  eip155:8453:0x8004A169FB4a3325136EB29fA0ceB6D2e539a432    (Base)
  eip155:84532:0x8004A818BFB912233c491871b3d84c89A494BD9e   (Base Sepolia)

Public RPC Endpoints

ChainRPC URL
Basehttps://mainnet.base.org
Base Sepoliahttps://sepolia.base.org
ETH Sepoliahttps://rpc.sepolia.org
Linea Sepoliahttps://rpc.sepolia.linea.build
Polygon Amoyhttps://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