Skill: limone-eth/erc8004-agent (version 0.0.2)

--- name: 8004-agent version: 0.0.1 description: > 8004 Agent Skill for registering AI agents on the ERC-8004 Trustless Agents standard and authenticating them via SIWA (Sign In With Agent). Use this skill when an agent needs to: (1) create or manage an Ethereum wallet for onchain identity, (2) register on the ERC-8004 Identity Registry as an NFT-based agent identity (SIGN UP), (3) authenticate with a server by proving ownership of an ERC-8004 identity using a signed challenge (SIGN IN / SIWA), (4) build or update an ERC-8004 registration file (metadata JSON with endpoints, trust models, services), (5) upload agent metadata to IPFS or base64 data URI, (6) look up or verify an agent's onchain registration. The agent persists public identity state in MEMORY.md. Private keys are held in a separate keyring proxy server — the agent can request signatures but never access the key itself. Triggers on: ERC-8004, trustless agents, agent registration, SIWA, Sign In With Agent, agent identity NFT, Agent0 SDK, agent wallet, agent keystore, keyring proxy. --- # 8004 Agent Skill v0.0.1 Register AI agents onchain (ERC-8004) and authenticate them via **SIWA (Sign In With Agent)**. ## Overview ERC-8004 ("Trustless Agents") provides three onchain registries deployed as per-chain singletons: - **Identity Registry** — ERC-721 NFTs. Each agent gets a unique `agentId` (tokenId) and an `agentURI` pointing to a JSON registration file. - **Reputation Registry** — Feedback signals (score, tags) from clients to agents. - **Validation Registry** — Third-party validator attestations (zkML, TEE, staked re-execution). **SIWA (Sign In With Agent)** is a challenge-response authentication protocol (inspired by SIWE / EIP-4361) where an agent proves ownership of an ERC-8004 identity by signing a structured message. See [references/siwa-spec.md](references/siwa-spec.md). --- ## Security Architecture > **Full details**: [references/security-model.md](references/security-model.md) The agent's private key is the root of its onchain identity. It must be protected against prompt injection, accidental exposure, and file system snooping. ### Principle: The private key NEVER enters the agent process All signing is delegated to a **keyring proxy server** — a separate process that holds the encrypted private key and exposes only HMAC-authenticated signing endpoints. The agent can request signatures but can never extract the key, even under full compromise (arbitrary code execution via prompt injection). ``` Agent Process Keyring Proxy Server (port 3100) (auto-detected from (holds encrypted private key) KEYRING_PROXY_URL) createWallet() | +--> POST /create-wallet + HMAC-SHA256 header ---> Generates key, encrypts to disk <-- Returns { address } only signMessage("hello") | +--> POST /sign-message + HMAC-SHA256 header ---> Validates HMAC + timestamp (30s window) Loads key, signs, discards key <-- Returns { signature, address } ``` **Why this is secure:** | 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 is logged with timestamp, endpoint, source IP, success/failure | | **Compromise limit** | Even full agent takeover can only request signatures — cannot extract the key | **Environment variables:** | Variable | Used by | Purpose | |---|---|---| | `KEYRING_PROXY_URL` | Agent | Proxy server URL — private (e.g. `http://keyring-proxy:3100`) or public | | `KEYRING_PROXY_SECRET` | Both | HMAC shared secret | | `KEYRING_PROXY_PORT` | Proxy server | Listen port (default: 3100) | | `AGENT_PRIVATE_KEY` | Proxy server | Hex-encoded private key (0x...) — use an existing wallet instead of generating one | | `KEYSTORE_PASSWORD` | Proxy server | Password for the encrypted-file keystore (not needed with `AGENT_PRIVATE_KEY`) | > **Auto-detection**: When `KEYRING_PROXY_URL` is set, `KEYSTORE_BACKEND` automatically defaults to `proxy` — no need to set it manually. When `AGENT_PRIVATE_KEY` is set on the proxy server, `KEYSTORE_BACKEND` defaults to `env`. > > The proxy server stores keys using an AES-encrypted V3 JSON Keystore (scrypt KDF) by default. To use an existing wallet instead, set `AGENT_PRIVATE_KEY` on the proxy server — the key is held in memory at runtime (no encrypted file needed). ### Keystore API The `@buildersgarden/siwa/keystore` module exposes ONLY these operations — none return the private key: ``` createWallet() → { address, backend } // Creates key, returns ONLY address signMessage(msg) → { signature, address } // Signs via proxy, key never exposed signTransaction(tx) → { signedTx, address } // Same pattern signAuthorization(auth) → SignedAuthorization // EIP-7702 delegation signing getAddress() → string // Public address only hasWallet() → boolean ``` > `getSigner()` is **not available** with the proxy backend — use `signMessage()` / `signTransaction()` instead. ### IMPORTANT: Install and use the SDK The agent MUST use the `@buildersgarden/siwa` SDK to interact with the keyring proxy. The SDK handles HMAC authentication automatically. **Step 1 — Install the SDK** (if not already available in your environment): ```bash npm install @buildersgarden/siwa ``` **Step 2 — Set environment variables:** ``` KEYRING_PROXY_URL=https://your-keyring-proxy.up.railway.app KEYRING_PROXY_SECRET=<your-shared-secret> ``` **Step 3 — Use the SDK functions** (never call the proxy HTTP endpoints directly): ```typescript import { createWallet, signMessage, getAddress } from '@buildersgarden/siwa/keystore'; const info = await createWallet(); // SDK handles HMAC auth internally const { signature } = await signMessage(msg); // SDK handles HMAC auth internally const address = await getAddress(); // SDK handles HMAC auth internally ``` The SDK reads `KEYRING_PROXY_URL` and `KEYRING_PROXY_SECRET` from environment variables and constructs the correct HMAC headers automatically. ### Fallback: Manual HMAC authentication (without SDK) If you absolutely cannot install the SDK (e.g. non-Node.js environment, restricted runtime), you can call the proxy HTTP endpoints directly using the HMAC protocol described below. **Prefer the SDK whenever possible.** **Headers required on every request** (except `GET /health`): | Header | Value | |---|---| | `Content-Type` | `application/json` | | `X-Keyring-Timestamp` | Current time as Unix epoch **milliseconds** (e.g. `1738792800000`) | | `X-Keyring-Signature` | HMAC-SHA256 hex digest of the payload string (see below) | **HMAC payload format** — a single string with four parts separated by newlines (`\n`): ``` {METHOD}\n{PATH}\n{TIMESTAMP}\n{BODY} ``` | Part | Value | |---|---| | `METHOD` | HTTP method, uppercase (always `POST`) | | `PATH` | Endpoint path (e.g. `/create-wallet`, `/sign-message`) | | `TIMESTAMP` | Same value as the `X-Keyring-Timestamp` header | | `BODY` | The raw JSON request body string (e.g. `{}` or `{"message":"hello"}`) | **Compute the signature:** ``` HMAC-SHA256(secret, "POST\n/create-wallet\n1738792800000\n{}") → hex digest ``` **Timestamp window:** The server rejects requests where the timestamp differs from server time by more than **30 seconds**. **Example — create a wallet (Node.js without SDK):** ```typescript import crypto from 'crypto'; const PROXY_URL = process.env.KEYRING_PROXY_URL; const [REDACTED_SECRET]; async function proxyRequest(path: string, body: Record<string, unknown> = {}) { const bodyStr = JSON.stringify(body); const timestamp = Date.now().toString(); const payload = `POST\n${path}\n${timestamp}\n${bodyStr}`; const signature = crypto.createHmac('sha256', SECRET).update(payload).digest('hex'); const res = await fetch(`${PROXY_URL}${path}`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'X-Keyring-Timestamp': timestamp, 'X-Keyring-Signature': signature, }, body: bodyStr, }); if (!res.ok) throw new Error(`${path} failed (${res.status}): ${await res.text()}`); return res.json(); } // Usage const wallet = await proxyRequest('/create-wallet'); // { address, backend } const addr = await proxyRequest('/get-address'); // { address } const sig = await proxyRequest('/sign-message', { message: 'hello' }); // { signature, address } ``` **Example — create a wallet (Python):** ```python import hmac, hashlib, json, time, requests, os PROXY_URL = os.environ["KEYRING_PROXY_URL"] [REDACTED_SECRET]"KEYRING_PROXY_SECRET"] def proxy_request(path, body=None): if body is None: body = {} body_str = json.dumps(body, separators=(",", ":")) timestamp = str(int(time.time() * 1000)) payload = f"POST\n{path}\n{timestamp}\n{body_str}" signature = hmac.new(SECRET.encode(), payload.encode(), hashlib.sha256).hexdigest() resp = requests.post( f"{PROXY_URL}{path}", headers={ "Content-Type": "application/json", "X-Keyring-Timestamp": timestamp, "X-Keyring-Signature": signature, }, data=body_str, ) resp.raise_for_status() return resp.json() wallet = proxy_request("/create-wallet") # {"address": "0x...", "backend": "..."} sig = proxy_request("/sign-message", {"message": "hello"}) # {"signature": "0x...", "address": "0x..."} ``` **Available endpoints:** | Endpoint | Body | Response | |---|---|---| | `POST /create-wallet` | `{}` | `{ address, backend }` | | `POST /has-wallet` | `{}` | `{ hasWallet: boolean }` | | `POST /get-address` | `{}` | `{ address }` | | `POST /sign-message` | `{ message: string }` | `{ signature, address }` | | `POST /sign-transaction` | `{ tx: { to, data, nonce, chainId, type, maxFeePerGas, ... } }` | `{ signedTx, address }` | | `POST /sign-authorization` | `{ auth: { chainId, address, nonce } }` | `{ signedAuthorization }` | | `GET /health` | — | `{ status: "ok", backend }` (no auth required) | ### MEMORY.md: Public Data Only MEMORY.md stores the agent's public identity state — **never the private key**: ```markdown ## Wallet - **Address**: `0x1234...abcd` <- public - **Keystore Backend**: `proxy` <- which backend holds the key - **Created At**: `2026-02-04T...` ## Registration - **Status**: `registered` - **Agent ID**: `42` - **Agent Registry**: `eip155:84532:0x8004AA63...` ... ``` **Lifecycle rules**: 1. **Before any action** — Read MEMORY.md. If wallet exists, skip creation. If registered, skip re-registration. 2. **After wallet creation** — Write address + backend info to MEMORY.md. Private key goes to proxy keystore only. 3. **After registration** — Write agentId, agentRegistry, agentURI, chainId to MEMORY.md. 4. **After SIWA sign-in** — Append session token under Sessions. **Template**: [assets/MEMORY.md.template](assets/MEMORY.md.template) --- ## Deploying the Keyring Proxy Before signing anything, the agent needs a running **keyring proxy** — the separate process that holds the private key and performs all cryptographic operations. ### Option A — Railway (Recommended) Deploy with one click using the Railway template: [![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/siwa-keyring-proxy?referralCode=ZUrs1W) This deploys a single `keyring-proxy` service built from [REDACTED_SECRET]. Set these environment variables in Railway: | Variable | Required | Description | |---|---|---| | `KEYRING_PROXY_SECRET` | Yes | Shared HMAC-SHA256 secret. Must match your agent. | | `KEYSTORE_PASSWORD` | Conditional | Password for the encrypted-file keystore (default backend). | | `AGENT_PRIVATE_KEY` | Conditional | Hex-encoded private key (0x...) to use an existing wallet instead. | After deployment, note the proxy URL (e.g. `https://your-keyring-proxy.up.railway.app`). Set it as `KEYRING_PROXY_URL` on your agent. > Full deployment guide with architecture details, OpenClaw gateway setup, and verification steps: [https://siwa.builders.garden/docs/deploy](https://siwa.builders.garden/docs/deploy) ### Option B — Docker (Self-hosted) ```bash docker build -f packages/keyring-proxy/Dockerfile -t keyring-proxy . docker run -p 3100:3100 \ -e KEYRING_PROXY_SECRET=your-secret \ -e KEYSTORE_PASSWORD=your-password \ keyring-proxy ``` ### Option C — Local Development ```bash cd packages/siwa-testing pnpm run proxy ``` Once the proxy is running, set these environment variables on the agent: ``` KEYRING_PROXY_URL=http://localhost:3100 # or your Railway URL KEYRING_PROXY_SECRET=your-shared-secret ``` The `proxy` keystore backend is auto-detected when `KEYRING_PROXY_URL` is set — no need to set `KEYSTORE_BACKEND` manually. --- ## Workflow: SIGN UP (Agent Registration) ### Step 0: Check MEMORY.md + Keystore ```typescript import { hasWallet } from '@buildersgarden/siwa/keystore'; import { ensureMemoryExists, hasWalletRecord, isRegistered } from '@buildersgarden/siwa/memory'; ensureMemoryExists('./MEMORY.md', './assets/MEMORY.md.template'); if (await hasWallet() && isRegistered('./MEMORY.md')) { // Already registered — skip to SIGN IN or update } if (await hasWallet() && hasWalletRecord('./MEMORY.md')) { // Wallet exists — skip to Step 2 } // Otherwise proceed to Step 1 ``` ### Step 1: Create Wallet (key goes to proxy, address goes to MEMORY.md) ```typescript import { createWallet } from '@buildersgarden/siwa/keystore'; import { writeMemoryField } from '@buildersgarden/siwa/memory'; const info = await createWallet(); // <- key created in proxy, NEVER returned // Write ONLY public data to MEMORY.md writeMemoryField('Address', info.address); writeMemoryField('Keystore Backend', info.backend); if (info.keystorePath) writeMemoryField('Keystore Path', info.keystorePath); writeMemoryField('Created At', new Date().toISOString()); ``` ### Step 1b: Fund the Wallet (REQUIRED before registration) **STOP — the wallet must have ETH for gas before any onchain transaction can succeed.** Registration, URI updates, and metadata changes all require gas fees. After creating the wallet, you MUST: 1. **Show the user the wallet address and the target chain** so they can send funds: - Address: the value returned by `createWallet()` or `getAddress()` - Chain: the chain where the agent will register (e.g. Base Sepolia chainId `84532`, Base mainnet chainId `8453`) 2. **Tell the user to send ETH** (or the chain's native gas token) to that address on that chain. 3. **Wait for the user to confirm** they have funded the wallet before proceeding to registration. For testnets, suggest using a faucet: - **Base Sepolia**: [Base Sepolia faucet](https://www.alchemy.com/faucets/base-sepolia) or bridge from ETH Sepolia - **ETH Sepolia**: [Sepolia faucet](https://www.alchemy.com/faucets/ethereum-sepolia) > **Do NOT attempt to register or send any onchain transaction until the wallet is funded.** The transaction will fail with an insufficient funds error. Example message to show the user: ``` Wallet created successfully! Address: 0x1234...abcd Chain: Base Sepolia (chainId 84532) Please send some ETH to this address on Base Sepolia to cover gas fees. You can use a faucet: https://www.alchemy.com/faucets/base-sepolia Let me know once the wallet is funded and I'll proceed with registration. ``` ### Step 2: Build the Registration File Create a JSON file following the ERC-8004 schema. Use [assets/registration-template.json](assets/registration-template.json) as a starting point. Required fields: `type`, `name`, `description`, `image`, `services`, `active`. After building, update MEMORY.md profile: ```typescript writeMemoryField('Name', registrationFile.name); writeMemoryField('Description', registrationFile.description); ``` ### Step 3: Upload Metadata **Option A — IPFS (Pinata, recommended):** ```typescript const res = await fetch('https://api.pinata.cloud/pinning/pinJSONToIPFS', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${process.env.PINATA_JWT}` }, body: JSON.stringify({ pinataContent: registrationFile }) }); const { IpfsHash } = await res.json(); const agentURI = `ipfs://${IpfsHash}`; ``` **Option B — Base64 data URI:** ```typescript const encoded = Buffer.from(JSON.stringify(registrationFile)).toString('base64'); const agentURI = `data:application/json;base64,${encoded}`; ``` ### Step 4: Register Onchain (signed via proxy) With the proxy backend, the agent builds the transaction and delegates signing to the proxy: ```typescript import { signTransaction, getAddress } from '@buildersgarden/siwa/keystore'; import { writeMemoryField } from '@buildersgarden/siwa/memory'; const provider = new ethers.JsonRpcProvider(process.env.RPC_URL); const address = await getAddress(); const IDENTITY_REGISTRY_ABI = [ 'function register(string agentURI) external returns (uint256 agentId)', 'event Registered(uint256 indexed agentId, string agentURI, address indexed owner)' ]; // Build the transaction const iface = new ethers.Interface(IDENTITY_REGISTRY_ABI); const data = iface.encodeFunctionData('register', [agentURI]); const nonce = await provider.getTransactionCount(address); const feeData = await provider.getFeeData(); const txReq = { to: REGISTRY_ADDRESS, data, nonce, chainId, type: 2, maxFeePerGas: feeData.maxFeePerGas, maxPriorityFeePerGas: feeData.maxPriorityFeePerGas, gasLimit: (await provider.estimateGas({ to: REGISTRY_ADDRESS, data, from: address })) * 120n / 100n, }; // Sign via proxy — key never enters this process const { signedTx } = await signTransaction(txReq); const txResponse = await provider.broadcastTransaction(signedTx); const receipt = await txResponse.wait(); // Parse event for agentId for (const log of receipt.logs) { try { const parsed = iface.parseLog({ topics: log.topics as string[], data: log.data }); if (parsed?.name === 'Registered') { const agentId = parsed.args.agentId.toString(); const agentRegistry = `eip155:${chainId}:${REGISTRY_ADDRESS}`; // Persist PUBLIC results to MEMORY.md writeMemoryField('Status', 'registered'); writeMemoryField('Agent ID', agentId); writeMemoryField('Agent Registry', agentRegistry); writeMemoryField('Agent URI', agentURI); writeMemoryField('Chain ID', chainId.toString()); writeMemoryField('Registered At', new Date().toISOString()); } } catch { /* skip non-matching logs */ } } ``` See [references/contract-addresses.md](references/contract-addresses.md) for deployed addresses per chain. ### Alternative: Agent0 SDK ```typescript import { SDK } from 'agent0-sdk'; import { readMemory } from '@buildersgarden/siwa/memory'; // Note: Agent0 SDK takes a private key string. If using the SDK, // you'll need a non-proxy backend or load the key within a narrow scope. // Prefer the signTransaction() approach above for proxy integration. ``` ### Alternative: create-8004-agent CLI ```bash npx create-8004-agent ``` After `npm run register`, update MEMORY.md with the output agentId. --- ## Workflow: SIGN IN (SIWA — Sign In With Agent) Full spec: [references/siwa-spec.md](references/siwa-spec.md) ### Step 0: Read Public Identity from MEMORY.md ```typescript import { readMemory, isRegistered } from '@buildersgarden/siwa/memory'; const memory = readMemory('./MEMORY.md'); if (!isRegistered()) { throw new Error('Agent not registered. Run SIGN UP workflow first.'); } const address = memory['Address']; const agentId = parseInt(memory['Agent ID']); const agentRegistry = memory['Agent Registry']; const chainId = parseInt(memory['Chain ID']); ``` ### Step 1: Request Nonce from Server ```typescript const nonceRes = await fetch('https://api.targetservice.com/siwa/nonce', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ address, agentId, agentRegistry }) }); const { nonce, issuedAt, expirationTime } = await nonceRes.json(); ``` ### Step 2: Sign via Proxy (key never exposed) ```typescript import { signSIWAMessage } from '@buildersgarden/siwa/siwa'; // signSIWAMessage internally calls keystore.signMessage() // which delegates to the keyring proxy — the key never enters this process. const { message, signature } = await signSIWAMessage({ domain: 'api.targetservice.com', address, statement: 'Authenticate as a registered ERC-8004 agent.', uri: 'https://api.targetservice.com/siwa', agentId, agentRegistry, chainId, nonce, issuedAt, expirationTime }); ``` ### Step 3: Submit and Persist Session ```typescript import { appendToMemorySection } from '@buildersgarden/siwa/memory'; const verifyRes = await fetch('https://api.targetservice.com/siwa/verify', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ message, signature }) }); const session = await verifyRes.json(); if (session.success) { appendToMemorySection('Sessions', `- **${agentId}@api.targetservice.com**: \`${session.token}\` (exp: ${expirationTime || 'none'})` ); } ``` ### SIWA 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}] ``` ### Server-Side Verification The server MUST: 1. Recover signer from signature (EIP-191) 2. Match recovered address to message address 3. Validate domain binding, nonce, time window 4. **Call `ownerOf(agentId)` onchain** to confirm signer owns the agent NFT 5. *(Optional)* Evaluate `SIWAVerifyCriteria` — activity status, required services, trust models, reputation score 6. Issue session token `verifySIWA()` in `@buildersgarden/siwa/siwa` accepts an optional `criteria` parameter (6th argument) to enforce requirements after the ownership check: ```typescript import { verifySIWA } from '@buildersgarden/siwa/siwa'; const result = await verifySIWA(message, signature, domain, nonceValid, provider, { mustBeActive: true, // agent metadata.active must be true requiredServices: ['MCP'], // ServiceType values from ERC-8004 requiredTrust: ['reputation'], // TrustModel values from ERC-8004 minScore: 0.5, // minimum reputation score minFeedbackCount: 10, // minimum feedback count reputationRegistryAddress: '0x8004BAa1...9b63', }); // result.agent contains the full AgentProfile when criteria are provided ``` See the test server's `verifySIWARequest()` for a full reference implementation. | Endpoint | Method | Description | |---|---|---| | `/siwa/nonce` | POST | Generate and return a nonce | | `/siwa/verify` | POST | Accept `{ message, signature }`, verify, return session/JWT | --- ## MEMORY.md Quick Reference | Section | When Written | Key Fields | |---|---|---| | **Wallet** | Step 1 of SIGN UP | Address, Keystore Backend, Created At | | **Registration** | Step 4 of SIGN UP | Status, Agent ID, Agent Registry, Agent URI, Chain ID | | **Agent Profile** | Step 2 of SIGN UP | Name, Description, Image | | **Services** | After adding endpoints | One line per service | | **Sessions** | After each SIWA sign-in | Token, domain, expiry per session | | **Notes** | Any time | Free-form (funding tx, faucet used, etc.) | **What is NOT in MEMORY.md**: Private keys, keystore passwords, mnemonic phrases. --- ## Reference Files - **[references/security-model.md](references/security-model.md)** — Threat model, keystore architecture, prompt injection defense - **[references/siwa-spec.md](references/siwa-spec.md)** — Full SIWA protocol specification (message ABNF, field definitions, security considerations) - **[references/contract-addresses.md](references/contract-addresses.md)** — Deployed registry addresses per chain, ABI fragments - **[references/registration-guide.md](references/registration-guide.md)** — Detailed registration file schema, endpoint types, update flows ## Core Library (`@buildersgarden/siwa` package) - **`@buildersgarden/siwa/keystore`** — Secure key storage abstraction with keyring proxy support - **`@buildersgarden/siwa/memory`** — MEMORY.md read/write helpers (public data only) - **`@buildersgarden/siwa/siwa`** — SIWA message building, signing (via keystore), and server-side verification (with optional criteria) - **`@buildersgarden/siwa/registry`** — Read agent profiles (`getAgent`) and reputation (`getReputation`) from on-chain registries. Exports ERC-8004 typed values: `ServiceType`, `TrustModel`, `ReputationTag` - **`@buildersgarden/siwa/proxy-auth`** — HMAC-SHA256 authentication utilities for the keyring proxy ## Assets - **[assets/MEMORY.md.template](assets/MEMORY.md.template)** — Template for the agent's public identity memory file - **[assets/registration-template.json](assets/registration-template.json)** — Starter registration file template

path: package.json
content: name: @siwa/skill
private: yes
version: 0.0.1

path: CLAUDE.md
content: # 8004 Agent Skill v0.0.1

You are an ERC-8004 agent with access to the 8004 Agent Skill. This skill lets you create and manage an Ethereum wallet, register as an onchain agent, and authenticate with services using the SIWA protocol.

## Prerequisites: Deploy the Keyring Proxy

Before using any signing capabilities, a **keyring proxy** must be running — it holds the private key and performs all cryptographic operations. The agent never accesses the key directly.

**Recommended: Deploy to Railway with one click:**

[![Deploy on Railway](https://railway.com/button.svg)](https://railway.com/deploy/siwa-keyring-proxy?referralCode=ZUrs1W)

Set `KEYRING_PROXY_SECRET` (shared HMAC secret) and either `KEYSTORE_PASSWORD` (for encrypted-file backend) or `AGENT_PRIVATE_KEY` (to use an existing wallet). After deployment, configure your agent with:

```
KEYRING_PROXY_URL=https://your-keyring-proxy.up.railway.app
KEYRING_PROXY_SECRET=<your-shared-secret>
```

For full deployment instructions (Docker, local dev, OpenClaw gateway): [https://siwa.builders.garden/docs/deploy](https://siwa.builders.garden/docs/deploy)

## Prerequisites: Install the SDK

Before using any capabilities, you MUST install the SIWA SDK in your environment:

```bash
npm install @buildersgarden/siwa
```

This gives you access to `@buildersgarden/siwa/keystore` (wallet creation, signing), `@buildersgarden/siwa/siwa` (SIWA authentication), `@buildersgarden/siwa/memory` (MEMORY.md helpers), and `@buildersgarden/siwa/registry` (onchain lookups). The SDK handles all HMAC proxy authentication internally — you never need to craft raw HTTP calls to the proxy.

## Your Capabilities

You can perform these actions by running commands in the `siwa/packages/siwa-testing` directory, **or** by using the SDK directly in code:

### 1. Create a Wallet
Creates an Ethereum wallet. The private key is stored securely in the keyring proxy — you never see it.
```bash
cd /home/node/.openclaw/workspace/siwa/packages/siwa-testing && pnpm run agent create-wallet
```

### 2. Fund the Wallet (REQUIRED before registration)
After creating a wallet, you **MUST stop and ask the user to fund it** before attempting registration or any onchain transaction. Show the user:
- The wallet **address** (from `create-wallet` or `status` output)
- The **chain** where they need to send gas (e.g. Base Sepolia, chainId 84532)
- A **faucet link** for testnets (e.g. https://www.alchemy.com/faucets/base-sepolia)

**Do NOT proceed to registration until the user confirms the wallet is funded.** Onchain transactions will fail without gas.

### 3. Check Status
Shows your current wallet address, registration state, and keystore backend.
```bash
cd /home/node/.openclaw/workspace/siwa/packages/siwa-testing && pnpm run agent status
```

### 4. Register as an Agent
Registers your wallet as an ERC-8004 agent identity (mock mode for testing, or live onchain).
```bash
cd /home/node/.openclaw/workspace/siwa/packages/siwa-testing && pnpm run agent register
```

### 5. Sign In (SIWA Authentication)
Proves ownership of your ERC-8004 identity by signing a structured message and receiving a JWT from the server.
```bash
cd /home/node/.openclaw/workspace/siwa/packages/siwa-testing && pnpm run agent sign-in
```

### 6. Full Flow (All Steps)
Runs wallet creation → registration → SIWA sign-in → authenticated API call, all sequentially.
```bash
cd /home/node/.openclaw/workspace/siwa/packages/siwa-testing && pnpm run agent:flow
```

### 7. Run Proxy Tests
Validates that the keyring proxy is working correctly (7 tests).
```bash
cd /home/node/.openclaw/workspace/siwa/packages/siwa-testing && pnpm run agent test-proxy
```

### 8. Reset State
Clears MEMORY.md and keystore state to start fresh.
```bash
cd /home/node/.openclaw/workspace/siwa/packages/siwa-testing && pnpm run reset
```

## Important Notes

- **Private keys are NEVER accessible to you.** All signing is delegated to the keyring proxy over HMAC-authenticated HTTP. This is by design — even if you are compromised, the key cannot be extracted.
- **NEVER call the keyring proxy HTTP endpoints directly.** Always use the SDK functions (`createWallet()`, `signMessage()`, etc. from `@buildersgarden/siwa/keystore`) or the CLI commands listed above. The proxy uses a specific HMAC-SHA256 authentication protocol that the SDK handles internally — hand-crafting HTTP requests to the proxy will fail. If you cannot install the SDK, see the "Fallback: Manual HMAC authentication" section in SKILL.md for the exact protocol specification.
- **MEMORY.md** in `siwa/packages/siwa-testing/` contains your public identity state (address, agentId, registration status). Read it to know your current state.
- **Proxy backend is auto-detected** from `KEYRING_PROXY_URL` — no need to set `KEYSTORE_BACKEND` manually.
- If a command fails with "Cannot find module", run `cd /home/node/.openclaw/workspace/siwa/packages/siwa-testing && pnpm install` first.

## Architecture

```
You (OpenClaw Agent)          Keyring Proxy (keyring-proxy:3100)     SIWA Server (siwa-server:3000)
  |                             |                                      |
  +-- signMessage() ----------> | Signs with real private key          |
  |   (via proxy backend)       | Returns signature only               |
  |                             |                                      |
  +-- SIWA sign-in -------------|------------------------------------> |
  |   (signed challenge)        |                                      | Verifies signature
  |                             |                                      | Returns JWT
```

## Reference

- Full skill documentation: `siwa/SKILL.md`
- Security model: `siwa/references/security-model.md`
- SIWA protocol spec: `siwa/references/siwa-spec.md`


path: assets/registration-template.json
content: {
  "type": "https://eips.ethereum.org/EIPS/eip-8004#registration-v1",
  "name": "AGENT_NAME",
  "description": "AGENT_DESCRIPTION",
  "image": "https://example.com/agent-image.png",
  "services": [
    {
      "name": "A2A",
      "endpoint": "https://YOUR_DOMAIN/.well-known/agent-card.json",
      "version": "0.3.0"
    },
    {
      "name": "MCP",
      "endpoint": "https://YOUR_DOMAIN/mcp",
      "version": "2025-06-18"
    }
  ],
  "x402Support": false,
  "active": true,
  "registrations": [],
  "supportedTrust": [
    "reputation"
  ]
}


path: assets/MEMORY.template.md
content: # Agent Identity Memory

> This file contains **public** agent identity data only. The private key is stored
> securely in the keystore backend (OS keychain or encrypted file) — never here.

## Wallet

- **Address**: `<NOT SET>`
- **Keystore Backend**: `<NOT SET>`
- **Keystore Path**: `<NOT SET>`
- **Created At**: `<NOT SET>`

## Registration

- **Status**: `unregistered`
- **Agent ID**: `<NOT SET>`
- **Agent Registry**: `<NOT SET>`
- **Agent URI**: `<NOT SET>`
- **Chain ID**: `<NOT SET>`
- **Registered At**: `<NOT SET>`

## Agent Profile

- **Name**: `<NOT SET>`
- **Description**: `<NOT SET>`
- **Image**: `<NOT SET>`

## Services

<!-- One line per service endpoint after registration -->

## Sessions

<!-- SIWA session tokens recorded here after successful sign-ins -->

## Notes

<!-- Free-form notes: funding tx hash, faucet used, etc. -->


path: references/registration-guide.md
content: # ERC-8004 Registration Guide

> Part of the 8004 Agent Skill v0.0.1

## Registration File Schema (v1)

The `agentURI` onchain resolves to this JSON file. It follows the ERC-8004 registration-v1 schema.

### Required Fields

| Field | Type | Description |
|---|---|---|
| `type` | string | MUST be `"https://eips.ethereum.org/EIPS/eip-8004#registration-v1"` |
| `name` | string | Human-readable agent name |
| `description` | string | What the agent does, pricing, interaction methods |
| `image` | string | URL to agent logo/avatar |
| `services` | array | List of service endpoint objects |
| `active` | boolean | Whether the agent is currently active |

### Optional Fields

| Field | Type | Description |
|---|---|---|
| `x402Support` | boolean | Whether the agent supports x402 micropayments |
| `registrations` | array | List of onchain registrations (populated after minting) |
| `supportedTrust` | array | Trust models: `"reputation"`, `"crypto-economic"`, `"tee-attestation"` |

### Service Endpoint Object

Each entry in the `services` array:

```json
{
  "name": "<protocol-name>",
  "endpoint": "<uri-or-address>",
  "version": "<optional-version-string>"
}
```

**Recognized service names:**

| Name | Endpoint Format | Version Example |
|---|---|---|
| `web` | `https://web.agent.com/` | — |
| `A2A` | `https://agent.com/.well-known/agent-card.json` | `"0.3.0"` |
| `MCP` | `https://mcp.agent.com/` | `"2025-06-18"` |
| `OASF` | `ipfs://{cid}` or URL | `"0.8"` |
| `ENS` | `myagent.eth` | `"v1"` |
| `DID` | `did:method:foobar` | `"v1"` |
| `email` | `[REDACTED_SECRET]` | — |

Custom service names are allowed. The `version` field is RECOMMENDED but not required.

### OASF Endpoint Extensions

OASF entries support optional `skills` and `domains` arrays for taxonomy tagging:

```json
{
  "name": "OASF",
  "endpoint": "https://github.com/agntcy/oasf/",
  "version": "v0.8.0",
  "skills": ["data_engineering/data_transformation_pipeline", "natural_language_processing/summarization"],
  "domains": ["finance_and_business/investment_services"]
}
```

### Registration Object

Each entry in the `registrations` array:

```json
{
  "agentId": 42,
  "agentRegistry": "eip155:84532:0x8004A818BFB912233c491871b3d84c89A494BD9e"
}
```

An agent MAY have multiple registrations across different chains.

## Onchain Metadata Keys

Set via `setMetadata(agentId, key, value)`. The key `agentWallet` is reserved and MUST be set via `setAgentWallet()` with an EIP-712 signature proof.

Common metadata patterns:
- Custom metadata can store agent version, category, capabilities hash, or any key-value data.
- Metadata values are `bytes` — encode strings as UTF-8 bytes.

## Endpoint Domain Verification

An agent MAY prove control of an HTTPS endpoint-domain by publishing:

```
https://{endpoint-domain}/.well-known/agent-registration.json
```

This file MUST contain at least a `registrations` array with entries matching the onchain agent. If the endpoint domain is the same domain serving the agent's primary registration file, this check is redundant.

## Update Flow

To update agent metadata after initial registration:

1. Modify the registration JSON.
2. Re-upload to IPFS (new CID) or update the hosted file.
3. Call `setAgentURI(agentId, newURI)` onchain.

With Agent0 SDK:

```typescript
const agent = await sdk.loadAgent('84532:42');
agent.updateInfo(undefined, 'Updated description', undefined);
await agent.setMCP('https://new-mcp.agent.com/');
await agent.registerIPFS(); // re-uploads and updates onchain
```

## Transfer Flow

Agent NFTs are standard ERC-721 and can be transferred:

```typescript
const registry = new ethers.Contract(registryAddress, ['function transferFrom(address,address,uint256)'], signer);
await registry.transferFrom(currentOwner, newOwner, agentId);
```

**Important**: On transfer, the `agentWallet` metadata is automatically cleared and must be re-verified by the new owner.

## Wallet Requirements

- The registering address becomes the NFT owner.
- Fund with testnet ETH for gas fees (use chain faucets).
- For Base Sepolia: use the Base Sepolia faucet or bridge from ETH Sepolia.
- Private key is needed for: registration, URI updates, metadata changes, SIWA signing, giving feedback.

## Agent0 SDK Chains & Config

```typescript
// Supported chain IDs for SDK initialization
const SUPPORTED_CHAINS = {
  ETH_SEPOLIA: 11155111,
  BASE_SEPOLIA: 84532,
  POLYGON_AMOY: 80002
};

// Multi-chain agent ID format: "chainId:agentId"
// e.g., "84532:42" for Base Sepolia agent #42
```


path: references/siwa-spec.md
content: # SIWA — Sign In With Agent

> Part of the 8004 Agent Skill v0.0.1

**Protocol Version 1.0 — Authentication protocol for ERC-8004 registered agents**

SIWA enables ERC-8004 agents to authenticate with off-chain services by signing a structured plaintext message with their Ethereum private key. Inspired by SIWE (EIP-4361), SIWA extends the pattern to include ERC-8004 identity fields (`agentId`, `agentRegistry`) and verifies onchain ownership of the agent NFT.

## Message Format (ABNF)

```abnf
siwa-message = domain %s" wants you to sign in with your Agent account:" LF
               address LF LF
               [ statement LF ] LF
               %s"URI: " uri LF
               %s"Version: " version LF
               %s"Agent ID: " agent-id LF
               %s"Agent Registry: " agent-registry LF
               %s"Chain ID: " chain-id LF
               %s"Nonce: " nonce LF
               %s"Issued At: " issued-at
               [ LF %s"Expiration Time: " expiration-time ]
               [ LF %s"Not Before: " not-before ]
               [ LF %s"Request ID: " request-id ]

domain          = authority          ; RFC 3986 authority (host[:port])
address         = "0x" 40HEXDIG     ; EIP-55 checksummed Ethereum address
statement       = *( reserved / unreserved / " " )  ; no LF
uri             = URI               ; RFC 3986
version         = "1"               ; fixed for this spec version
agent-id        = 1*DIGIT           ; ERC-721 tokenId on the Identity Registry
agent-registry  = namespace ":" chain-id-ref ":" registry-address
                                    ; e.g. "eip155:84532:0x8004AA63..."
namespace       = "eip155"          ; EVM namespace
chain-id-ref    = 1*DIGIT           ; EIP-155 chain ID
registry-address= "0x" 40HEXDIG    ; Identity Registry contract address
chain-id        = 1*DIGIT           ; EIP-155 chain ID for the signing session
nonce           = 8*ALPHANUM        ; server-generated, ≥ 8 alphanumeric chars
issued-at       = date-time         ; RFC 3339
expiration-time = date-time         ; RFC 3339 (OPTIONAL)
not-before      = date-time         ; RFC 3339 (OPTIONAL)
request-id      = *VCHAR            ; system-specific identifier (OPTIONAL)
```

## Field Definitions

| Field | Required | Description |
|---|---|---|
| `domain` | YES | Origin domain requesting authentication. MUST match the server's actual origin. |
| `address` | YES | Ethereum address performing the signing. MUST be EIP-55 checksummed. |
| `statement` | NO | Human-readable purpose string. |
| `uri` | YES | RFC 3986 URI of the resource being authenticated to. |
| `version` | YES | MUST be `"1"`. |
| `agentId` | YES | The ERC-721 tokenId of the agent in the Identity Registry. |
| `agentRegistry` | YES | Colon-separated string: `eip155:{chainId}:{identityRegistryAddress}`. |
| `chainId` | YES | EIP-155 Chain ID the session is bound to. |
| `nonce` | YES | Server-generated random string, ≥ 8 alphanumeric characters. Must be single-use. |
| `issuedAt` | YES | RFC 3339 datetime when the message was created. |
| `expirationTime` | NO | RFC 3339 datetime after which the message is invalid. |
| `notBefore` | NO | RFC 3339 datetime before which the message is invalid. |
| `requestId` | NO | Opaque identifier for the sign-in request. |

## Example Message

```
api.myplatform.com wants you to sign in with your Agent account:
0x742d35Cc6634C0532925a3b844Bc9e7595f0bEb0

Authenticate as a registered ERC-8004 agent.

URI: https://api.myplatform.com/siwa
Version: 1
Agent ID: 42
Agent Registry: eip155:84532:0x8004A818BFB912233c491871b3d84c89A494BD9e
Chain ID: 84532
Nonce: kX9f2mPqR7wL
Issued At: 2025-09-01T12:00:00Z
Expiration Time: 2025-09-01T12:10:00Z
```

## Authentication Flow

### 1. Nonce Request

```
POST /siwa/nonce
Content-Type: application/json

{
  "address": [REDACTED_SECRET],
  "agentId": 42,
  "agentRegistry": "eip155:84532:0x8004A818BFB912233c491871b3d84c89A494BD9e"
}
```

Server response:

```json
{
  "nonce": "kX9f2mPqR7wL",
  "issuedAt": "2025-09-01T12:00:00Z",
  "expirationTime": "2025-09-01T12:10:00Z"
}
```

The server MUST store the nonce associated with the address and mark it as consumed after one use.

### 2. Agent Signs Message

The agent constructs the full SIWA message string from the fields and signs using EIP-191 `personal_sign` (prefix `"\x19Ethereum Signed Message:\n" + len(message) + message`).

```typescript
const message = buildSIWAMessage({
  domain: 'api.myplatform.com',
  address: wallet.address,
  statement: 'Authenticate as a registered ERC-8004 agent.',
  uri: 'https://api.myplatform.com/siwa',
  version: '1',
  agentId: 42,
  agentRegistry: 'eip155:84532:0x8004A818BFB912233c491871b3d84c89A494BD9e',
  chainId: 84532,
  nonce: 'kX9f2mPqR7wL',
  issuedAt: '2025-09-01T12:00:00Z',
  expirationTime: '2025-09-01T12:10:00Z'
});

const signature = await wallet.signMessage(message);
```

### 3. Verification Request

```
POST /siwa/verify
Content-Type: application/json

{
  "message": "<full SIWA message string>",
  "signature": "0x..."
}
```

### 4. Server Verification Steps

The server MUST perform ALL of the following:

1. **Parse** — Validate the message conforms to the SIWA ABNF format.
2. **Recover** — Use `ecrecover` (via `ethers.verifyMessage`) to recover the signer address from the signature.
3. **Address match** — Confirm recovered address matches the `address` field in the message.
4. **Domain binding** — Confirm the `domain` field matches the server's own origin.
5. **Nonce** — Confirm the `nonce` was issued by this server and has not been consumed.
6. **Time window** — If `expirationTime` is present, confirm `now < expirationTime`. If `notBefore` is present, confirm `now >= notBefore`.
7. **Onchain ownership** — Call `ownerOf(agentId)` on the Identity Registry at the address specified in `agentRegistry`. Confirm the returned owner matches the recovered signer address.
8. **Consume nonce** — Mark the nonce as used to prevent replay.

If all checks pass, issue a session token (JWT or equivalent) containing at minimum: `address`, `agentId`, `agentRegistry`, `chainId`, `iat`, `exp`.

### 5. Server Response

Success:

```json
{
  "success": true,
  "token": "eyJhbGciOiJIUzI1NiIs...",
  "agent": {
    "agentId": 42,
    "agentRegistry": "eip155:84532:0x8004A818BFB912233c491871b3d84c89A494BD9e",
    "address": [REDACTED_SECRET]
  }
}
```

Failure:

```json
{
  "success": false,
  "error": "Signature does not match agent owner"
}
```

## Smart Contract Wallet Support

For smart contract wallets (e.g., ERC-4337 accounts), verification SHOULD fall back to ERC-1271 `isValidSignature(hash, signature)` if `ecrecover` does not match. This mirrors the ERC-8004 spec's support for EIP-1271 alongside EIP-712.

## Security Considerations

- **Replay protection**: Nonces MUST be single-use and server-generated. Servers SHOULD expire unused nonces after a short TTL (e.g., 5–10 minutes).
- **Domain binding**: The `domain` field MUST match the requesting origin to prevent phishing. Agents SHOULD verify the domain before signing.
- **Onchain verification is mandatory**: Unlike SIWE, SIWA REQUIRES the server to verify onchain ownership. This is the key differentiator — proving the signer is a registered ERC-8004 agent.
- **Key management**: Agent private keys SHOULD be stored in secure enclaves, environment variables, or TEEs. Never expose keys in client-side code.
- **Transfer handling**: If an agent NFT is transferred, the previous owner's SIWA sessions become invalid (ownership check fails). Servers SHOULD set reasonable session TTLs.
- **agentWallet vs owner**: The `agentWallet` metadata key in ERC-8004 is for payment routing. SIWA authenticates the **owner** (the address that holds the NFT). Servers MAY optionally also accept signatures from the `agentWallet` address if the use case warrants it.

## Comparison with SIWE (EIP-4361)

| 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 standard | EIP-191 | EIP-191 (same) |
| Contract wallets | ERC-1271 | ERC-1271 (same) |
| Message prefix | "wants you to sign in with your Ethereum account" | "wants you to sign in with your Agent account" |


path: references/contract-addresses.md
content: # ERC-8004 Contract Addresses & ABIs

> Part of the 8004 Agent Skill v0.0.1

## Deployed Registry Addresses

### EVM Chains (Mainnets)

| Chain | Chain ID | Identity Registry | Reputation Registry |
|---|---|---|---|
| Base | `8453` | [REDACTED_SECRET] | [REDACTED_SECRET] |

### EVM Chains (Testnets)

| Chain | Chain ID | Identity Registry | Reputation Registry |
|---|---|---|---|
| Base Sepolia | `84532` | [REDACTED_SECRET] | [REDACTED_SECRET] |
| ETH Sepolia | `11155111` | [REDACTED_SECRET] | — |
| Linea Sepolia | `59141` | [REDACTED_SECRET] | — |
| Polygon Amoy | `80002` | [REDACTED_SECRET] | — |

### Solana

| Network | Program ID |
|---|---|
| Devnet | [REDACTED_SECRET] |

## Agent Registry String Format

The `agentRegistry` identifier follows this format:

```
{namespace}:{chainId}:{identityRegistryAddress}
```

Examples:
- Base: `eip155:8453:0x8004A169FB4a3325136EB29fA0ceB6D2e539a432`
- Base Sepolia: `eip155:84532:0x8004A818BFB912233c491871b3d84c89A494BD9e`
- ETH Sepolia: `eip155:[REDACTED_SECRET]`
- Polygon Amoy: `eip155:80002:0x8004ad19E14B9e0654f73353e8a0B600D46C2898`

## Identity Registry ABI (Key Functions)

```json
[
  "function register(string agentURI) external returns (uint256 agentId)",
  "function register(string agentURI, tuple(string metadataKey, bytes metadataValue)[] metadata) external returns (uint256 agentId)",
  "function register() external returns (uint256 agentId)",
  "function setAgentURI(uint256 agentId, string newURI) external",
  "function tokenURI(uint256 tokenId) external view returns (string)",
  "function ownerOf(uint256 tokenId) external view returns (address)",
  "function getMetadata(uint256 agentId, string metadataKey) external view returns (bytes)",
  "function setMetadata(uint256 agentId, string metadataKey, bytes metadataValue) external",
  "function getAgentWallet(uint256 agentId) external view returns (address)",
  "function setAgentWallet(uint256 agentId, address newWallet, uint256 deadline, bytes signature) external",
  "function unsetAgentWallet(uint256 agentId) external",
  "event Registered(uint256 indexed agentId, string agentURI, address indexed owner)",
  "event URIUpdated(uint256 indexed agentId, string newURI, address indexed updatedBy)",
  "event MetadataSet(uint256 indexed agentId, string indexed indexedMetadataKey, string metadataKey, bytes metadataValue)"
]
```

## Reputation Registry ABI (Key Functions)

```json
[
  "function giveFeedback(uint256 agentId, int128 value, uint8 valueDecimals, string tag1, string tag2, string endpoint, string feedbackURI, bytes32 feedbackHash) external",
  "function revokeFeedback(uint256 agentId, uint64 feedbackIndex) external",
  "function appendResponse(uint256 agentId, address clientAddress, uint64 feedbackIndex, string responseURI, bytes32 responseHash) external",
  "function getSummary(uint256 agentId, address[] clientAddresses, string tag1, string tag2) external view returns (uint64 count, int128 summaryValue, uint8 summaryValueDecimals)",
  "function readFeedback(uint256 agentId, address clientAddress, uint64 feedbackIndex) external view returns (int128 value, uint8 valueDecimals, string tag1, string tag2, bool isRevoked)",
  "function getClients(uint256 agentId) external view returns (address[])",
  "function getLastIndex(uint256 agentId, address clientAddress) external view returns (uint64)"
]
```

## Validation Registry ABI (Key Functions)

```json
[
  "function validationRequest(address validatorAddress, uint256 agentId, string requestURI, bytes32 requestHash) external",
  "function validationResponse(bytes32 requestHash, uint8 response, string responseURI, bytes32 responseHash, string tag) external",
  "function getValidationStatus(bytes32 requestHash) external view returns (address validatorAddress, uint256 agentId, uint8 response, bytes32 responseHash, string tag, uint256 lastUpdate)",
  "function getSummary(uint256 agentId, address[] validatorAddresses, string tag) external view returns (uint64 count, uint8 averageResponse)",
  "function getAgentValidations(uint256 agentId) external view returns (bytes32[])",
  "function getValidatorRequests(address validatorAddress) external view returns (bytes32[])"
]
```

## RPC Endpoints (Public)

| Chain | Public RPC |
|---|---|
| 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.

## Agent0 SDK Default Subgraph URLs

The Agent0 SDK auto-configures subgraph endpoints per chain for indexed queries (search, feedback aggregation). No manual configuration needed when using the SDK.

## 8004scan Explorer

View any registered agent at: `https://www.8004scan.io/`

After registration, agents are browsable by agentId and chain.


path: references/security-model.md
content: # Security Model

> Part of the 8004 Agent Skill v0.0.1

How ERC-8004 agent identity keys are stored and protected against prompt injection and accidental exposure.

## Threat Model

### The core problem

An AI agent that processes untrusted input (messages from other agents, web content, user prompts) is vulnerable to **prompt injection**: adversarial instructions embedded in the input that manipulate the agent's behavior.

If the private key is stored in a plaintext file (like a `.env` or a markdown memo), a prompt injection can instruct the agent to:

1. Read the file containing the private key
2. Exfiltrate it (embed in an HTTP request, return it in a response, encode it in an outbound message)

The key is then compromised, and the attacker controls the agent's onchain identity.

### What we defend against

| Threat | Description | Mitigation |
|---|---|---|
| **Prompt injection exfiltration** | Malicious input instructs the agent to read and leak the private key | Key is never in any file the agent reads into context |
| **Context window leakage** | Key appears in the agent's working memory / LLM context | Key is loaded inside a function, used, and discarded — never returned |
| **File system snooping** | Another process reads the key from disk | OS keychain uses encrypted storage with access controls; V3 keystore is AES-encrypted |
| **Log / error exposure** | Key appears in stack traces, console output, or error messages | Signing functions return only signatures, never raw keys |
| **Accidental commit** | Key is committed to version control | No file in the project ever contains the plaintext key |

### What we do NOT defend against

- A fully compromised host (root access) — nothing can protect keys on a fully owned machine short of hardware HSMs
- Malicious code that the agent itself executes (if the agent runs arbitrary code, it can call the keystore API)
- Side-channel attacks on the signing process

For the highest security in production, use a **hardware wallet** or **TEE-based signer** (see ERC-8004's Validation Registry trust models).

## Architecture: Keystore Backends

The `@buildersgarden/siwa/keystore` module provides three storage backends. In production, the **keyring proxy** is the primary backend — all other backends are used either internally by the proxy server or for local development.

### Keyring Proxy (`proxy`) — Production Default

**The recommended backend. Private key never enters the agent process.** See the "Keyring Proxy" section below for full details.

### Encrypted JSON Keystore (`encrypted-file`)

**The Ethereum-native approach. Used internally by the proxy server. Also works for local development.**

Uses the same encrypted format as MetaMask, Geth, and MyEtherWallet:

- **KDF**: scrypt (N=131072, r=8, p=1) derives a 256-bit key from the password
- **Cipher**: AES-128-CTR encrypts the private key
- **MAC**: Keccak-256 integrity check
- **File on disk**: `agent-keystore.json` — contains only ciphertext, never the raw key

This is built into ethers.js:

```typescript
// Encrypt (on wallet creation)
const json = await wallet.encrypt(password);
fs.writeFileSync('./agent-keystore.json', json, { mode: 0o600 });

// Decrypt (on signing)
const wallet = await ethers.Wallet.fromEncryptedJson(json, password);
```

**How it resists prompt injection**: Even if an injection reads `agent-keystore.json`, it gets AES-encrypted ciphertext like:

```json
{"address":"88a5c2d9...","Crypto":{"ciphertext":"10adcc8bcaf49474c6710460e0dc97...","kdf":"scrypt",...}}
```

This is useless without the password. The password can itself be stored in the OS keychain (layered defense) or derived from machine-specific factors.

**File permissions**: Created with `chmod 600` (owner-only read/write).

### Environment Variable (`env`)

**For CI/CD and testing only. Not recommended for production.**

### Keyring Proxy (`proxy`)

**Process isolation — private key never enters the agent process.**

The agent delegates all signing to a separate **keyring proxy server** over HMAC-authenticated HTTP. The proxy server holds the real keystore (any of the backends above) and performs all cryptographic operations. Even full agent compromise (arbitrary code execution) cannot extract the key — only request signatures.

| Property | Detail |
|---|---|
| **Transport** | HMAC-SHA256 authenticated HTTP (30s replay window) |
| **Key isolation** | Private key lives in a separate process; never enters agent memory |
| **Audit** | Every signing request is logged with timestamp, path, source IP |
| **Limitation** | `getSigner()` is not available — use `signMessage()` / `signTransaction()` |

**Architecture:**

```
Agent Process                     Keyring Proxy Server (port 3100)
(auto-detected from               (encrypted-file or env backend)
 KEYRING_PROXY_URL)

signMessage("hello")
  |
  +--> POST /sign-message
       + HMAC-SHA256 header  ---> Validates HMAC + timestamp
                                  Loads key from real keystore
                                  Signs message, discards key
                              <-- Returns { signature, address }
```

**How it resists prompt injection**: The private key exists in a completely separate OS process. Even if the agent process is fully compromised (arbitrary code execution via prompt injection), the attacker can only request signatures — they cannot extract the key. The proxy also maintains an audit log of every signing request.

**Env vars:**

| Variable | Used by | Purpose |
|---|---|---|
| `KEYRING_PROXY_URL` | Agent | Proxy server URL (e.g. `http://localhost:3100`) |
| `KEYRING_PROXY_SECRET` | Both | HMAC shared secret |
| `KEYRING_PROXY_PORT` | Proxy server | Listen port (default: 3100) |

**Deployment:** Deploy the keyring proxy to Railway with one click using the [Railway template](https://railway.com/deploy/siwa-keyring-proxy?referralCode=ZUrs1W), or run it via Docker / locally. Full deployment guide: [https://siwa.builders.garden/docs/deploy](https://siwa.builders.garden/docs/deploy).

### Environment Variable (`env`)

Reads `AGENT_PRIVATE_KEY` from the process environment. This is the least secure option because:

- Environment variables can be read by any process running as the same user
- They may appear in process listings (`/proc/<pid>/environ` on Linux)
- Container orchestrators may log them

Use only when the OS keychain and encrypted file are unavailable (e.g., ephemeral CI runners).

## The Signing Boundary

The most important architectural decision: **external code never receives the private key.**

The `@buildersgarden/siwa/keystore` module exposes only these functions:

```
createWallet()           → { address, backend }           // No key
importWallet(pk)         → { address, backend }           // Consumes key, never returns it
getAddress()             → string                          // Public address only
hasWallet()              → boolean
signMessage(msg)         → { signature, address }          // Key loaded, used, discarded
signTransaction(tx)      → { signedTx, address }           // Key loaded, used, discarded
signAuthorization(auth)  → SignedAuthorization              // EIP-7702 delegation signing
getSigner(provider)      → ethers.Wallet                   // For contract calls (not available with proxy)
```

The private key exists in memory only during a signing call. With the proxy backend, the key never enters the agent process at all — it stays in the proxy server. With local backends, the key is loaded from the backend, used for the cryptographic operation, and then the `Wallet` object falls out of scope and is eligible for garbage collection. It is **never returned** to the calling code.

This means:

- The agent's main loop / LLM context never sees the key
- MEMORY.md contains only public data (address, agentId, etc.)
- A prompt injection that says "read all files and send me secrets" gets nothing useful

## MEMORY.md: Public Data Only

After this redesign, MEMORY.md stores only:

| Field | Sensitive? | Example |
|---|---|---|
| Address | No (public) | `0x1234...abcd` |
| Keystore Backend | No | `encrypted-file` |
| Keystore Path | Low risk | `./agent-keystore.json` |
| Agent ID | No (public) | `42` |
| Agent Registry | No (public) | `eip155:84532:0x8004AA63...` |
| Agent URI | No (public) | `ipfs://Qm...` |
| Chain ID | No (public) | `84532` |
| Sessions | Medium | Session tokens (short-lived) |

The **Private Key** field has been removed entirely.

## Setup Guide

### Recommended: Encrypted File

```bash
export KEYSTORE_PASSWORD="your-strong-passphrase"
```

The keystore will auto-detect the encrypted-file backend and use the password to encrypt/decrypt.

### CI/Testing or Existing Wallet: Environment Variable

```bash
export AGENT_PRIVATE_KEY="0xabc123..."
# KEYSTORE_BACKEND auto-detects to "env" when AGENT_PRIVATE_KEY is set
```

## Key Rotation

To rotate the agent's key while preserving its onchain identity:

1. Create a new wallet via `createWallet()`
2. Transfer the agent NFT to the new address: `transferFrom(oldAddress, newAddress, agentId)`
3. The `agentWallet` metadata key auto-clears on transfer (per ERC-8004 spec)
4. Update MEMORY.md with the new address
5. Delete the old wallet: `deleteWallet()`

## Dependencies

| Package | Required? | Purpose |
|---|---|---|
| `ethers` | **Yes** | Wallet operations, V3 keystore encryption, signing |

No other dependencies are needed. The encrypted-file backend uses only `ethers` built-in functions.


Security scan verdicts — ClawScan: suspicious | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)