ERC-8128: Ethereum HTTP Signatures
SkillSecuritySign and verify HTTP requests with Ethereum wallets using ERC-8128. Use when building authenticated APIs that need wallet-based auth, making signed requests to ERC-8128 endpoints, implementing request verification in servers, or working with agent-to-server authentication. Covers both the @slicekit/erc8128 JS library and the erc8128 CLI.
Available today. Use it from your connected AI after setup.
No other account needed.
Connect ahel once, and every AI you use reads what you have installed.
Then ask your AI: use the ERC-8128: Ethereum HTTP Signatures skill
What this skill tells your AI
The instructions your AI receives, as published by slice-so/erc8128 in skill/SKILL.md and read by ahel’s review.
ERC-8128 extends RFC 9421 (HTTP Message Signatures) with Ethereum wallet signing. It enables HTTP authentication using existing Ethereum keys—no new credentials needed.
📚 Full documentation: erc8128.slice.so
When to Use
- API authentication — Wallets already onchain can authenticate to your backend
- Agent auth — Bots and agents sign requests with their operational keys
- Replay protection — Signatures include nonces and expiration
- Request integrity — Sign URL, method, headers, and body
Packages
| Package | Purpose |
|---|---|
@slicekit/erc8128 | JS library for signing and verifying |
@slicekit/erc8128-cli | CLI for signed requests (erc8128 curl) |
Library: @slicekit/erc8128
Sign requests
import { createSignerClient } from '@slicekit/erc8128'
import type { EthHttpSigner } from '@slicekit/erc8128'
import { privateKeyToAccount } from 'viem/accounts'
const account = privateKeyToAccount('0x...')
const signer: EthHttpSigner = {
chainId: 1,
address: account.address,
signMessage: async (msg) => account.signMessage({ message: { raw: msg } }),
}
const client = createSignerClient(signer)
// Sign and send
const response = await client.fetch('https://api.example.com/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: '100' }),
})
// Sign only (returns new Request with signature headers)
const signedRequest = await client.signRequest('https://api.example.com/orders')
Verify requests
import { createVerifierClient } from '@slicekit/erc8128'
import type { NonceStore } from '@slicekit/erc8128'
import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'
// NonceStore interface for replay protection
const nonceStore: NonceStore = {
consume: async (key: string, ttlSeconds: number): Promise<boolean> => {
// Return true if nonce was successfully consumed (first use)
// Return false if nonce was already used (replay attempt)
}
}
const publicClient = createPublicClient({ chain: mainnet, transport: http() })
const verifier = createVerifierClient({
verifyMessage: publicClient.verifyMessage,
nonceStore
})
const result = await verifier.verifyRequest({ request: request })
if (result.ok) {
console.log(`Authenticated: ${result.address} on chain ${result.chainId}`)
} else {
console.log(`Failed: ${result.reason}`)
}
Sign options
| Option | Type | Default | Description |
|---|---|---|---|
binding | "request-bound" | "class-bound" | "request-bound" | What to sign |
replay | "non-replayable" | "replayable" | "non-replayable" | Include nonce |
ttlSeconds | number | 60 | Signature validity |
components | string[] | — | Additional components to sign |
contentDigest | "auto" | "recompute" | "require" | "off" | "auto" | Content-Digest handling |
request-bound: Signs @authority, @method, @path, @query (if present), and content-digest (if body present). Each request is unique.
class-bound: Signs only the components you explicitly specify. Reusable across similar requests. Requires components array.
📖 See Request Binding for details.
Verify policy
| Option | Type | Default | Description |
|---|---|---|---|
maxValiditySec | number | 300 | Max allowed TTL |
clockSkewSec | number | 0 | Allowed clock drift |
replayable | boolean | false | Allow nonce-less signatures |
classBoundPolicies | string[] | string[][] | — | Accepted class-bound component sets |
📖 See Verifying Requests and VerifyPolicy for full options.
CLI: erc8128 curl
For CLI usage, see references/cli.md.
Quick examples:
# GET with keystore
erc8128 curl --keystore ./key.json https://api.example.com/data
# POST with JSON
erc8128 curl -X POST \
-H "Content-Type: application/json" \
-d '{"foo":"bar"}' \
--keyfile ~/.keys/bot.key \
https://api.example.com/submit
# Dry run (sign only)
erc8128 curl --dry-run -d @body.json --keyfile ~/.keys/bot.key https://api.example.com
📖 See CLI Guide for full documentation.
Common Patterns
Express middleware
import { createVerifierClient } from '@slicekit/erc8128'
import type { NonceStore } from '@slicekit/erc8128'
import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'
const publicClient = createPublicClient({ chain: mainnet, transport: http() })
// Implement NonceStore (Redis example)
const nonceStore: NonceStore = {
consume: async (key, ttlSeconds) => {
const result = await redis.set(key, '1', 'EX', ttlSeconds, 'NX')
return result === 'OK'
}
}
const verifier = createVerifierClient({
verifyMessage: publicClient.verifyMessage,
nonceStore
})
async function erc8128Auth(req, res, next) {
// Convert Express req to a Fetch API Request (use your own helper or a library like node-fetch)
const fetchReq = toFetchRequest(req)
const result = await verifier.verifyRequest({ request: fetchReq })
if (!result.ok) {
return res.status(401).json({ error: result.reason })
}
req.auth = { address: result.address, chainId: result.chainId }
next()
}
Agent signing (with key file)
import { createSignerClient } from '@slicekit/erc8128'
import type { EthHttpSigner } from '@slicekit/erc8128'
import { privateKeyToAccount } from 'viem/accounts'
import { readFileSync } from 'fs'
const key = readFileSync(process.env.KEYFILE, 'utf8').trim()
const account = privateKeyToAccount(key as `0x${string}`)
const signer: EthHttpSigner = {
chainId: Number(process.env.CHAIN_ID) || 1,
address: account.address,
signMessage: async (msg) => account.signMessage({ message: { raw: msg } }),
}
const client = createSignerClient(signer)
// Use client.fetch() for all authenticated requests
Common verify failure reasons
| Reason | Meaning |
|---|---|
missing_headers | Required Signature / Signature-Input headers not found |
expired | Signature TTL has elapsed |
replay | Nonce already consumed (replay attempt) |
bad_keyid | keyid doesn't match erc8128:<chainId>:<address> format |
bad_signature_check | Signature doesn't match the claimed address |
digest_mismatch | Body was modified after signing |
📖 See VerifyFailReason for the full list of 23 failure reasons.
Key Management
For agents and automated systems:
| Method | Security | Use Case |
|---|---|---|
--keystore --interactive | High | Encrypted JSON keystore, password prompted interactively |
--keystore --password | High | Encrypted JSON keystore, password via flag or ETH_KEYSTORE_PASSWORD env |
--keyfile | Medium | Unencrypted key file, file permissions for protection |
ETH_PRIVATE_KEY | Low | Environment variable, avoid in production |
| Signing service | High | Delegate to external service (SIWA, AWAL) |
Documentation
- Full docs: erc8128.slice.so
- Quick Start: erc8128.slice.so/getting-started/quick-start
- Concepts: erc8128.slice.so/concepts/overview
- API Reference: erc8128.slice.so/api/signRequest
- ERC-8128 Spec: GitHub
Signals
- GitHub stars
- 22
- Forks
- 1
- Last commit
- Aug 2026
Advanced
- Catalog kind
- skill
- Gateway key
erc8128- Source
- github.com/slice-so/erc8128