Skip to main content
chat-relayer doesn’t use API keys. End users authenticate by signing each request with a delegate Ed25519 key (in production, derived from their wallet via the MemWal client SDK). This is the auth scheme most likely to trip up a new client integration, so this page documents it field-by-field against a known-working run.

The scheme

  1. Generate (or load) an Ed25519 keypair. The raw 32-byte public key, hex-encoded, is delegate_pubkey_hex.
  2. Build the canonical byte string exactly:
    — one role:content\n line per message in messages, in order, then a final line model:...\nowner:...\nns:... with no trailing newline.
  3. Sign those exact bytes with Ed25519 — no prehashing, no re-encoding the string first. Sign the raw UTF-8 bytes directly. The raw 64-byte signature, hex-encoded, is signature_hex.
  4. Send delegate_pubkey_hex and signature_hex as plain JSON fields alongside messages / model / owner_address / namespace.
Server-side verification lives in chat-relayer/src/relayer/src/http.rs::canonical_chat_req (builds the identical string) and src/relayer/src/auth.rs::verify_signature (decodes both hex fields, requires exactly 32 and 64 bytes respectively, rejects anything that doesn’t decode or verify with 401 Unauthorized — not a 400, so check auth first if a client gets rejected before reaching the model).
Common client bugs:
  • Hashing the canonical string before signing — Ed25519 here signs the raw message directly, which most libraries simply call “sign”, not “sign the hash of.”
  • An off-by-one on the newline — there must be no trailing newline after the final ns:<namespace> line.
  • Sending the DER/SPKI-wrapped public key instead of the raw 32-byte key (Node’s crypto.generateKeyPairSync exports DER by default — see the script below for how to strip the wrapper).
  • Sending the signature as base64 instead of hex.

Reference implementation

Worked example

Canonical bytes signed for a first message:
(rendered with line breaks above for readability — the actual signed bytes use \n, not real newlines plus a trailing one) Resulting request:
Resulting response:

owner_address is permanent

owner_address is a key into long-term, cross-session memory — it is not reset between requests, sessions, or days. Re-running a test with the same owner_address surfaces accumulated recalled_facts from every prior call, which is correct behavior (that’s the point of the cross-session memory layer), but it makes isolated test runs hard to read. Use a fresh, unique owner_address (e.g. a UUID) per test run if you want a clean before/after comparison.

Full chat-relayer API reference

Endpoint, request/response shapes, and the two automatic memory layers