Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.xandrlabs.ai/llms.txt

Use this file to discover all available pages before exploring further.

ALX Protocol uses EIP-712 typed structured data to let agents authorize KB queries off-chain. A SignedProtocolRequest binds a specific KB, a natural-language query, the requesting agent address, a nonce, an expiry timestamp, and a chain ID. Verifiers recover the signer from the signature and enforce expiry and nonce uniqueness before processing the request. All signing and verification primitives are exported from @alx/protocol/core. Import directly from there rather than from the top-level @alx/protocol package when you only need EIP-712 utilities.
import {
  buildSignedProtocolRequestDomain,
  verifySignedProtocolRequest,
  SIGNED_PROTOCOL_REQUEST_TYPES,
  NonceTracker,
  SignedRequestValidationError,
} from "@alx/protocol/core";

SignedProtocolRequest interface

interface SignedProtocolRequest {
  kbId:    string;              // 0x-prefixed 32-byte hex (bytes32)
  query:   string;              // natural-language query string
  agent:   string;              // EIP-55 checksummed agent address
  nonce:   bigint | number | string; // uint256 — unique per agent
  expiry:  bigint | number | string; // uint64 — unix timestamp (seconds)
  chainId: bigint | number | string; // uint256 — must match domain
}

buildSignedProtocolRequestDomain

Constructs the EIP-712 domain separator object. Pass the result directly to wallet.signTypedData() or verifySignedProtocolRequest().
function buildSignedProtocolRequestDomain(
  input: SignedProtocolRequestDomain
): { name: string; version: string; chainId: string; verifyingContract?: string }
input
SignedProtocolRequestDomain
required
Domain configuration for the EIP-712 envelope.

verifySignedProtocolRequest

Verifies an EIP-712 signed request end-to-end. Throws SignedRequestValidationError on any failure.
function verifySignedProtocolRequest(
  options: SignedProtocolRequestVerificationOptions
): { ok: true; signer: string; request: NormalizedRequest }
options
SignedProtocolRequestVerificationOptions
required
result
{ ok: true; signer: string; request: NormalizedRequest }
On success, returns ok: true, the checksummed signer address recovered from the signature, and the normalized request with all numeric fields converted to strings.

hashSignedProtocolRequest

Computes the EIP-712 struct hash for a request. Useful when you need the typed data hash for off-chain audit logs or as input to another protocol layer.
function hashSignedProtocolRequest(
  domain: SignedProtocolRequestDomain,
  request: SignedProtocolRequest
): string

recoverSignedProtocolRequestSigner

Recovers the signer address from a signature without performing expiry or nonce checks. Use verifySignedProtocolRequest for production verification; use this only when you need the raw recovered address.
function recoverSignedProtocolRequestSigner(
  domain: SignedProtocolRequestDomain,
  request: SignedProtocolRequest,
  signature: string
): string

normalizeSignedProtocolRequest

Validates and normalizes the numeric fields of a request to their canonical string forms. Throws MALFORMED_REQUEST if kbId is not a valid 32-byte hex string or if any numeric field cannot be converted to a non-negative BigInt.
function normalizeSignedProtocolRequest(
  request: SignedProtocolRequest
): { kbId: string; query: string; agent: string; nonce: string; expiry: string; chainId: string }

SIGNED_PROTOCOL_REQUEST_TYPES

The EIP-712 type definition for SignedProtocolRequest. Pass this as the types argument to wallet.signTypedData().
const SIGNED_PROTOCOL_REQUEST_TYPES = {
  SignedProtocolRequest: [
    { name: "kbId",    type: "bytes32"  },
    { name: "query",   type: "string"   },
    { name: "agent",   type: "address"  },
    { name: "nonce",   type: "uint256"  },
    { name: "expiry",  type: "uint64"   },
    { name: "chainId", type: "uint256"  },
  ],
};

NonceTracker

An in-memory store that tracks consumed nonces per agent address. Use one NonceTracker instance per server process lifetime and persist nonces to durable storage if you need cross-restart replay protection.
class NonceTracker {
  has(agent: string, nonce: bigint | number | string): boolean;
  consume(agent: string, nonce: bigint | number | string): void;
}
agent
string
required
The agent’s Ethereum address. Normalized to EIP-55 checksum form internally.
nonce
bigint | number | string
required
The nonce value from the signed request.
consume() throws SignedRequestValidationError with code NONCE_REUSED if the nonce has already been recorded for that agent.

SignedRequestValidationError

Thrown by all verification functions on failure. Inspect .code to determine the failure reason.
class SignedRequestValidationError extends Error {
  code:
    | "MALFORMED_REQUEST"  // kbId format wrong, invalid address, bad integer
    | "INVALID_SIGNATURE"  // signature cannot be decoded or recovered
    | "SIGNER_MISMATCH"    // recovered signer ≠ request.agent
    | "EXPIRED_REQUEST"    // now >= request.expiry
    | "CHAIN_MISMATCH"     // request.chainId ≠ expectedChainId
    | "NONCE_REUSED";      // nonce already consumed for this agent
}

Complete signing and verification example

import { ethers } from "ethers";
import {
  buildSignedProtocolRequestDomain,
  verifySignedProtocolRequest,
  SIGNED_PROTOCOL_REQUEST_TYPES,
  NonceTracker,
  SignedRequestValidationError,
  type SignedProtocolRequest,
} from "@alx/protocol/core";

const CONTRACT_ADDRESS = "0xD1F216E872a9ed4b90E364825869c2F377155B29";
const CHAIN_ID = 8453;

// ── Agent side: build and sign the request ───────────────────────────────────

const domain = buildSignedProtocolRequestDomain({
  chainId: CHAIN_ID,
  verifyingContract: CONTRACT_ADDRESS,
});

const request: SignedProtocolRequest = {
  kbId:    "0xabcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890ab",
  query:   "What are the key security invariants for input validation?",
  agent:   await wallet.getAddress(),
  nonce:   1n,
  expiry:  BigInt(Math.floor(Date.now() / 1000) + 300), // valid for 5 minutes
  chainId: CHAIN_ID,
};

const signature = await wallet.signTypedData(
  domain,
  SIGNED_PROTOCOL_REQUEST_TYPES,
  request
);

// ── Verifier side: verify the incoming request ───────────────────────────────

const nonceTracker = new NonceTracker();

try {
  const { signer } = verifySignedProtocolRequest({
    domain,
    request,
    signature,
    expectedChainId: CHAIN_ID,
    nonceTracker,
  });

  console.log("Verified. Signer:", signer);
  // signer === request.agent (checksummed)
} catch (err) {
  if (err instanceof SignedRequestValidationError) {
    console.error("Validation failed:", err.code, err.message);
    // Handle specific codes as needed
  } else {
    throw err;
  }
}

Error code reference

CodeCause
MALFORMED_REQUESTkbId is not a 32-byte hex string, address is invalid, or a numeric field is not a valid non-negative integer
INVALID_SIGNATUREThe signature bytes cannot be decoded or the ECDSA recovery fails
SIGNER_MISMATCHThe address recovered from the signature does not equal request.agent
EXPIRED_REQUESTThe current timestamp is greater than or equal to request.expiry
CHAIN_MISMATCHrequest.chainId does not equal expectedChainId
NONCE_REUSEDThe NonceTracker has already recorded this nonce for the given agent