The verifier is a Rust/Axum service on port 3002. It validates EIP-712 payment signatures for the gateway and rejects malformed signatures, wrong-chain contexts, expired/future timestamps, and replayed nonces.
- Accept
POST /verifyrequests from the gateway. - Enforce EIP-712 domain parity with gateway, web, and E2E signing code.
- Recover the signer address from the wallet signature.
- Reject chain ID mismatches before signature acceptance.
- Reject expired timestamps and future timestamps beyond allowed clock skew.
- Reject reused nonce hashes inside the configured signature window.
- Return structured
error_codevalues that the gateway maps to sanitized public errors.
Legacy authorizations use this domain:
| Field | Value |
|---|---|
name |
MicroAI Paygate |
version |
1 |
chainId |
EXPECTED_CHAIN_ID, falling back to CHAIN_ID, then 84532 |
verifyingContract |
0x0000000000000000000000000000000000000000 |
Payment type:
Payment(
address recipient,
string token,
string amount,
string nonce,
uint256 timestamp
)
Request-bound v2 authorizations use the same domain fields with version set to 2 and this type:
PaymentAuthorization(
address payer,
address recipient,
string token,
string amount,
string nonce,
uint256 timestamp,
string audience,
string method,
string resource,
string contentType,
bytes32 requestHash
)
Any change to this shape must be applied together in:
gateway/main.goverifier/src/main.rsweb/src/lib/x402-client.tssdk/typescript/src/payment.tstests/e2e.test.tsgateway/openapi.yaml- Root and service documentation
Returns:
{
"status": "healthy",
"service": "verifier",
"version": "<cargo package version>"
}Request shape:
{
"context": {
"recipient": "0x2cAF48b4BA1C58721a85dFADa5aC01C2DFa62219",
"token": "USDC",
"amount": "0.001",
"nonce": "550e8400-e29b-41d4-a716-446655440000",
"chainId": 84532,
"timestamp": 1700000000
},
"signature": "0x..."
}The verifier also accepts the staged authorizationVersion: 2 context, which adds audience, method, resource, contentType, and requestHash. V2 requests must include a top-level payer address that is also covered by the typed-data signature; the verifier rejects the request unless the recovered signer matches it. Legacy requests omit both fields until the gateway cutover.
Successful response:
{
"is_valid": true,
"recovered_address": "0x...",
"error": null
}Business rejection response:
{
"is_valid": false,
"recovered_address": null,
"error": "human-readable verifier detail",
"error_code": "invalid_signature"
}Important error codes:
| Code | Meaning |
|---|---|
invalid_signature |
Signature recovery failed or signer did not match the context. |
invalid_authorization_context |
The v2 version, binding fields, or payer are missing or malformed. |
signer_mismatch |
The v2 signature does not recover to the claimed payer. |
chain_id_mismatch |
Payment context chain does not match verifier expectation. |
timestamp_expired |
Timestamp is older than SIGNATURE_EXPIRY_SECONDS. |
timestamp_future |
Timestamp is beyond allowed future skew. |
timestamp_missing |
Timestamp field is missing or invalid. |
nonce_already_used |
Nonce hash was already accepted inside the signature window. |
Returns Prometheus text-format metrics for verifier request volume, verification latency, valid signatures, and invalid signatures by rejection reason.
| Variable | Default | Notes |
|---|---|---|
MAX_REQUEST_BODY_BYTES |
1048576 |
JSON body size limit. |
EXPECTED_CHAIN_ID |
84532 |
Preferred chain ID enforcement variable. |
CHAIN_ID |
unset | Fallback when EXPECTED_CHAIN_ID is unset. |
SIGNATURE_EXPIRY_SECONDS |
300 |
Signature freshness window and nonce retention TTL. |
PORT |
3002 |
Listen port for the verifier service. Invalid values fall back to 3002 with a warning. |
BIND_ADDRESS |
0.0.0.0 |
Network interface/address the verifier binds to. Invalid values fall back to 0.0.0.0 with a warning. |
SIGNATURE_CLOCK_SKEW_SECONDS |
60 |
Allowed future timestamp skew. |
VERIFIER_NONCE_STORE |
memory |
Use memory locally/tests or redis for shared multi-replica replay protection. |
REDIS_URL |
unset | Required when VERIFIER_NONCE_STORE=redis; accepts host:port, redis://..., or rediss://.... |
VERIFIER_NONCE_KEY_PREFIX |
microai:verifier:nonce: |
Redis key prefix for accepted nonce hashes. |
VERIFIER_REDIS_TIMEOUT_MS |
2000 |
Redis nonce-store connection and claim timeout in milliseconds. |
Nonce replay protection uses VERIFIER_NONCE_STORE.
memorykeeps accepted nonce hashes inside one verifier process. This is the default for local development and tests.redisstores accepted nonce hashes with atomicSET NX EXwrites so every verifier replica rejects the same replayed nonce. The Redis TTL isSIGNATURE_EXPIRY_SECONDS + SIGNATURE_CLOCK_SKEW_SECONDS + 1.
The verifier validates the signature before claiming a nonce, so malformed or invalid signatures do not burn nonces. If Redis is configured but unavailable, /verify returns 503 with error_code: "nonce_store_unavailable" instead of accepting a payment without shared replay protection.
cd verifier
cargo runThe service listens on 0.0.0.0:3002 by default. Override the bind address and port with BIND_ADDRESS and PORT.
The verifier exposes Prometheus metrics at GET /metrics on the configured listener port. The default port is 3002.
Example local scrape config:
scrape_configs:
- job_name: microai-verifier
static_configs:
- targets: ["localhost:<configured-port>"]
metrics_path: /metricscd verifier
cargo fmt -- --check
cargo clippy -- -D warnings
cargo testRun these checks after changing EIP-712 fields, chain ID parsing, timestamp logic, nonce replay protection, request body limits, response schemas, or dependencies.