Signing Requests
Learn how to sign HTTP requests with ERC-8128.
Overview
ERC-8128 signing creates cryptographic proof that an HTTP request was authorized by a specific Ethereum address. The signature covers the request components (method, URL, headers, body) and includes timing/replay protection.
With a signed request, servers can:
- Authenticate users by their Ethereum address (no passwords or API keys)
- Verify request integrity (tampering detection)
- Prevent replay attacks (via nonces)
Basic Signing
The simplest way to sign a request using signRequest:
import { signRequest } from '@slicekit/erc8128'
const signedRequest = await signRequest(
'https://api.example.com/resource',
signer
)This uses all defaults:
- Request-bound — signs authority, method, path, query, body digest
- Non-replayable — auto-generated nonce
- 60-second TTL
- Label: "eth"
Creating a Signer
Implement the EthHttpSigner interface by providing a chain ID, address, and signMessage function. This example uses viem's privateKeyToAccount:
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 (message) => {
return account.signMessage({ message: { raw: message } })
},
}Using createSignerClient
For repeated requests, use createSignerClient to bind the signer:
import { createSignerClient } from '@slicekit/erc8128'
const client = createSignerClient(signer, {
ttlSeconds: 120,
})
// Use client.fetch
const response = await client.fetch('https://api.example.com/orders', {
method: 'POST',
body: JSON.stringify({ amount: '100' }),
})
// Override options per-call
const response = await client.fetch(
'https://api.example.com/status',
{ method: 'GET' },
{ replay: 'replayable' }
)Signing Options
Customize TTL, timestamps, nonce generation, and other signature parameters via SignOptions.
const signedRequest = await signRequest(
'https://api.example.com/resource',
signer,
{ ttlSeconds: 300 } // Valid for 5 minutes
)Binding Modes
Request-bound (default) signs all applicable components for maximum security. Class-bound signs only the components you explicitly specify.
// Signs all applicable components for maximum security
const signedRequest = await signRequest(
'https://api.example.com/search?q=test',
{ method: 'POST', body: '{"data": true}' },
signer,
{ binding: 'request-bound' }
)
// Signature covers: @authority, @method, @path, @query, content-digestClass-Bound
Class-bound signatures authorize a class of requests rather than a single concrete request. By signing only specific components, a single signature can apply to multiple requests that match the signed components.
This is useful when:
- You want a single signature to work across multiple endpoints on the same domain
- You're optimizing for performance in high-frequency scenarios
- You're building delegation or session-like patterns
When using binding: 'class-bound', you must provide the components array explicitly. The library always includes @authority (required by the spec).
Additional Components
Sign extra headers alongside the default request-bound set:
const signedRequest = await signRequest(
request,
signer,
{ components: ['x-idempotency-key', 'x-request-id'] }
)
// Signature covers: @authority, @method, @path, content-digest,
// x-idempotency-key, x-request-idReplay Modes
Non-replayable (default) includes a unique nonce per request. Replayable omits the nonce — use only for safe, idempotent operations.
// Every request gets a unique nonce
const signedRequest = await signRequest(
'https://api.example.com/resource',
signer,
{ replay: 'non-replayable' }
)Replayable signatures trade single-use guarantees for reduced overhead (no nonce generation, no server-side nonce state). They still provide authentication and request integrity (if request-bound), comparable to bearer credentials like JWTs — but without requiring server-issued tokens. The verifier must explicitly opt in to accepting replayable signatures and must implement early invalidation mechanisms. See Replayable Signatures for the full tradeoff analysis.
Content-Digest
Automatic (Default)
Content-Digest is computed automatically for request-bound requests with a body:
const signedRequest = await signRequest(
'https://api.example.com/data',
{
method: 'POST',
body: JSON.stringify({ data: 'value' }),
},
signer
)
// Content-Digest header added automaticallyManual Control
Disable automatic Content-Digest computation when you want to handle it yourself or skip it entirely.
const signedRequest = await signRequest(
request,
signer,
{ contentDigest: 'off' } // Don't add Content-Digest
)Error Handling
Catch Erc8128Error to handle signing failures with structured error codes.
import { signRequest, Erc8128Error } from '@slicekit/erc8128'
try {
const signedRequest = await signRequest(input, signer)
} catch (error) {
if (error instanceof Erc8128Error) {
console.error(`Signing failed: ${error.code} - ${error.message}`)
}
throw error
}Common errors:
CRYPTO_UNAVAILABLE— No WebCrypto (older Node.js or restricted environment)UNSUPPORTED_REQUEST— Can't sign this request (e.g., invalid URL)BODY_READ_FAILED— Couldn't read request body for Content-Digest