Skip to main content

API Reference

The Rust relayer exposes these routes. The route table lives in services/server/src/main.rs.

See also:

Authentication​

The routes below require signed headers, except the MCP transports, which use bearer authentication. The SDK handles the signing automatically.

Not every /api/* route works this way. Account existence checks need no authentication, the security-delete families use their own challenge and bearer flows, and /api/admin/* requires an ADMIN_API_KEY. Those families stay out of scope here.

Required headers​

HeaderDescription
x-public-keyHex-encoded Ed25519 public key (32 bytes)
x-signatureHex-encoded Ed25519 signature (64 bytes)
x-timestampUnix timestamp in seconds (5-minute validity window)
x-nonceUUID v4 nonce. The relayer records it in Redis for replay protection

Optional headers​

HeaderDescription
x-account-idMemWalAccount object ID hint. Official SDKs always send it and include it in the canonical signature
x-seal-sessionBase64-encoded Seal SessionKey for relayer-managed decrypt flows. The TypeScript and Python SDKs use it
x-delegate-keyLegacy delegate private key credential for relayer-managed decrypt flows. Deprecated; use x-seal-session where supported

Signature format​

The signed message is:

Source: relayer/api-reference.md
{timestamp}.{method}.{path_and_query}.{body_sha256}.{nonce}.{account_id}

For GET requests, body_sha256 is the SHA-256 of an empty byte string. If a raw client omits x-account-id, it must sign the empty string in the final account_id position. Official SDKs send x-account-id. A cold delegate key with no account id is rejected with 401. The relayer does not search AccountRegistry. A key already in the relayer's delegate-key cache, or a server with MEMWAL_ACCOUNT_ID set, can still authenticate without the header.

The relayer verifies the Ed25519 signature, then resolves the owner by looking up the public key in onchain MemWalAccount.delegate_keys.

Public routes​

These routes require no authentication.

GET /health​

Service liveness check. status is "ok" when the relayer process is up. HTTP 200 means the process is running, not that writes are accepted.

writes is "ok", "degraded", or "paused".

  • "paused" when WRITES_PAUSED is set (1 / true / yes). That flag is write-path admission, not a health-only signal: POST /api/remember, /api/remember/manual, /api/remember/bulk, and /api/analyze then return HTTP 503 with {"error":"writes are paused"}. /health itself stays HTTP 200 with status: "ok" and writes: "paused", so clients can distinguish an intentional pause from an integrator bug. Reads (recall, restore, remember job status) stay available.
  • "degraded" when recent durable writes (last 15 minutes) have failed at least three times and none have landed. The relayer still accepts and queues a write (write_ready stays true; HTTP 200), but Walrus is not storing it, expect the job to fail rather than queue more. A single success in that window keeps "ok" even if other writes failed: this is a total-outage detector, not a per-request verdict. memwal_health prints writes=degraded when this is set.
  • "ok" otherwise (including a quiet window with no finished writes).

write_ready is true when the encryption sidecar process answered its own /health and Postgres can accept writes (cached a few seconds). Postgres is considered not writable when Neon cluster size (pg_cluster_size, not pg_database_size of this database) is at or within 1MB of neon.max_cluster_size. If pg_cluster_size() is missing (no neon extension), the probe falls back to sum(pg_database_size) against that GUC. Self-hosted Postgres without the GUC keeps the sidecar-only check. Probe errors and timeouts fail open (write_ready stays true) so CI is not blocked; timeouts log at warn. A sidecar outage or a disk/project-size write outage can still return HTTP 200 with write_ready: false. Use writes, not write_ready, for the pause or Walrus-outage signal. write_ready: true is not a guarantee that remember or analyze succeed.

Response:

Source: relayer/api-reference.md
{
"status": "ok",
"version": "0.1.0",
"relayerVersion": "0.1.0",
"apiVersion": "1.0.0",
"minSupportedSdk": {
"typescript": "0.0.4",
"python": "0.1.0",
"mcp": "0.0.1"
},
"featureFlags": {
"auth.accountBoundNonce": true,
"auth.sealSessionHeader": true,
"runtime.versionEndpoint": true
},
"deprecations": [],
"build": {},
"mode": "production",
"prompt_versions": {
"extract": "extract.v1",
"ask": "ask.v1"
},
"write_ready": true,
"writes": "ok"
}

GET /version​

Stable relayer/API compatibility metadata.

Response: the compatibility object documented in Versioning and Compatibility.

GET /config​

Public deployment parameters that the SDK reads to build a Seal SessionKey client-side. The endpoint returns no secrets.

Response:

Source: relayer/api-reference.md
{
"packageId": "0x...",
"network": "testnet",
"suiRpcUrl": "https://fullnode.testnet.sui.io",
"suiGrpcUrl": "https://fullnode.testnet.sui.io",
"suiTransport": "grpc",
"rateLimitDisabled": false
}

This example shows the fields a client needs to select a transport and build a session key. A deployment can return more, so read fields by name rather than assuming the shape is exhaustive.

suiGrpcUrl is the preferred gRPC endpoint, and suiTransport names the transport the relayer prefers for Sui reads. Newer SDKs read both; a client might fall back to suiRpcUrl while a deployment migrates. rateLimitDisabled mirrors the server's benchmark-bypass setting so benchmark scripts can pre-flight the configuration.

GET /metrics​

Prometheus metrics for scraping. See Observability for the exported series and how to wire a scraper.

POST /sponsor​

Proxy to the Seal/Walrus sidecar's /sponsor endpoint for sponsored transactions. The request must include authTimestamp, a UUID-v4 authNonce, and authSignature: a Sui personal-message signature over the sender, transaction-kind hash, timestamp, and nonce. Only one allowlisted Walrus Memory account call qualifies for sponsorship.

POST /sponsor/execute​

Proxy to the sidecar's /sponsor/execute endpoint. sender must match the short-lived, one-time Redis binding that the authenticated /sponsor call creates.

Protected routes​

Every route below requires the signed headers described in Authentication.

GET /api/whoami​

Return the account identity the caller's delegate key resolves to. Takes no request body.

Authentication already resolves the account before any handler runs, so this route just hands back what the middleware computed. Returning account_id is safe here precisely because the route is authenticated. The caller has proven it holds a delegate key registered against this account, so it only ever learns about itself. The public GET /api/accounts/:owner/exists route deliberately withholds it.

The motivating use is rebuilding local credentials: a client that holds a working delegate key but has lost the surrounding metadata (an interrupted sign-in, a wiped config file) needs account_id, owner, and package_id to write a usable credentials file, and the key alone proves entitlement to all three.

Response:

Source: relayer/api-reference.md
{
"account_id": "0x...",
"owner": "0x...",
"package_id": "0x..."
}

The caller sends x-account-id on every network. Authentication does not scan AccountRegistry to discover it. A request with no account id returns 401 unless that delegate key is already in the relayer's cache or the server has MEMWAL_ACCOUNT_ID set. A client that has lost its account id cannot recover it from this route.

POST /api/remember​

Submit text as an encrypted memory job. The relayer returns after creating a background job; embedding, Seal encryption, Walrus upload, and vector indexing continue asynchronously.

Request:

Source: relayer/api-reference.md
{
"text": "User prefers dark mode",
"namespace": "demo"
}

namespace defaults to "default" if omitted and is limited to 255 UTF-8 bytes.

Response: 202 Accepted

Source: relayer/api-reference.md
{
"job_id": "uuid",
"status": "running"
}

GET /api/remember/:job_id​

Poll a remember job. status is one of pending, running, uploaded, done, or failed. Failed jobs include an error message. Unknown job IDs and jobs that belong to another owner return 404, so callers cannot enumerate job IDs.

Response:

Source: relayer/api-reference.md
{
"job_id": "uuid",
"status": "done",
"owner": "0x...",
"namespace": "demo",
"blob_id": "walrus-blob-id"
}

POST /api/remember/bulk​

Submit up to 20 memories in one request. job_ids[i] corresponds to items[i]. This route accepts request bodies up to 2 MB.

Request:

Source: relayer/api-reference.md
{
"items": [
{ "text": "User prefers dark mode", "namespace": "demo" },
{ "text": "User works in TypeScript", "namespace": "demo" }
]
}

Response: 202 Accepted

Source: relayer/api-reference.md
{
"job_ids": ["uuid-1", "uuid-2"],
"total": 2,
"status": "running"
}

POST /api/remember/bulk/status​

Poll a batch of remember jobs. Unknown job IDs come back with status not_found, and failed items include an error message.

Request:

Source: relayer/api-reference.md
{
"job_ids": ["uuid-1", "uuid-2"]
}

Response:

Source: relayer/api-reference.md
{
"results": [
{ "job_id": "uuid-1", "status": "done", "blob_id": "walrus-blob-id" },
{ "job_id": "uuid-2", "status": "running" }
]
}

POST /api/recall​

Search for memories matching a natural language query. Returns decrypted plaintext results.

Request:

Source: relayer/api-reference.md
{
"query": "What do we know about this user?",
"limit": 10,
"namespace": "demo",
"scoring_weights": {
"semantic": 1.0,
"recency": 0.3,
"recency_half_life_days": 30,
"importance": 0.2
}
}

limit defaults to 10; the server caps it at 100. namespace defaults to "default". scoring_weights takes an optional object; omit it to keep the plain cosine-distance order.

sort is optional: "relevance" (the cosine order, and the behaviour when omitted) or "recent" (the newest among the semantic matches). An explicit sort, "relevance" included, is the order, and the relayer ignores scoring_weights for that request.

deadline_ms is optional: how long the caller waits for this response, in milliseconds. When set, the relayer stops about one second before it, counting from when the request arrived, and answers 504 with the step that was still running, so the caller learns where the recall stalled instead of timing out blind. A short deadline still gets at least 2 seconds of work; if the whole deadline went on authentication before the recall started, the relayer answers at once with stage auth. Values above 600000 are capped. Omit it to let the recall run to completion. The TypeScript SDK sends 14000, a second under the 15s it aborts recall() at: the relayer's own margin runs from arrival, so it covers the reply's trip back but not the connect the caller's timer already started on.

Source: relayer/api-reference.md
{
"error": "Recall timed out after 14001ms during walrus_download",
"message": "Recall timed out after 14001ms during walrus_download",
"code": "RECALL_TIMEOUT",
"stage": "walrus_download",
"elapsed_ms": 14001
}

stage is one of auth, embed, vector_search, walrus_download, or seal_decrypt.

Scoring weights​

The optional scoring_weights object turns on composite ranking. The same object works on /api/recall, /api/recall/manual, and /api/ask.

FieldDefaultDescription
semantic1.0Weight for cosine similarity between the query and each memory
recency0Weight for how recently the relayer indexed each memory
recency_half_life_days30Half-life in days for the recency decay
importance0Weight for the per-fact importance score that the extractor assigns at save time

Response:

Source: relayer/api-reference.md
{
"results": [
{
"blob_id": "walrus-blob-id",
"text": "User prefers dark mode",
"distance": 0.15,
"score": 0.91
}
],
"total": 1,
"dropped_count": 0
}

score only appears when scoring_weights sets a nonzero recency or importance weight and sort is omitted. A request that sets only the semantic weight keeps the plain cosine order, and the relayer omits score. dropped_count only appears when at least one match dropped out because its blob download or decryption failed; the relayer omits those matches from results.

POST /api/remember/manual​

Register a client-encrypted payload. The client sends Seal-encrypted data (base64) and a precomputed embedding vector. The relayer uploads the encrypted bytes to Walrus and stores the vector mapping.

Request:

Source: relayer/api-reference.md
{
"encrypted_data": "base64-encoded-seal-encrypted-bytes",
"vector": [0.01, -0.02, ...],
"namespace": "demo"
}

Response:

Source: relayer/api-reference.md
{
"id": "uuid",
"blob_id": "walrus-blob-id",
"owner": "0x...",
"namespace": "demo"
}

POST /api/recall/manual​

Search with a precomputed query vector. Returns index hits only; the client handles downloading and decrypting. The request accepts the same optional scoring_weights object as POST /api/recall, and the server applies the same limit cap of 100.

Request:

Source: relayer/api-reference.md
{
"vector": [0.01, -0.02, ...],
"limit": 10,
"namespace": "demo"
}

Response:

Source: relayer/api-reference.md
{
"results": [
{
"blob_id": "walrus-blob-id",
"distance": 0.15,
"created_at": "2026-07-23T12:00:00Z",
"importance": 0.5
}
],
"total": 1
}

created_at is the time the relayer indexed the entry. importance carries the per-fact importance score that the extractor assigns at save time.

POST /api/analyze​

Extract facts from text using an LLM, then enqueue each fact as a separate memory job.

Request:

Source: relayer/api-reference.md
{
"text": "I live in Hanoi and prefer dark mode.",
"namespace": "demo",
"occurred_at": "2026-07-01T00:00:00Z"
}

occurred_at is an optional RFC 3339 timestamp that anchors the extracted facts in time, for example when you import older conversations. The extractor writes the date into the fact text itself; the relayer stores no separate event-time metadata and cannot filter or rank by event time.

Response: 202 Accepted

Source: relayer/api-reference.md
{
"job_ids": ["uuid-1", "uuid-2"],
"facts": [
{ "text": "User lives in Hanoi", "id": "uuid-1", "job_id": "uuid-1" },
{ "text": "User prefers dark mode", "id": "uuid-2", "job_id": "uuid-2" }
],
"fact_count": 2,
"status": "pending",
"owner": "0x..."
}

POST /api/embed​

Return an embedding vector for text without storing a memory. Uses the same model and width as remember, recall, and analyze. Requires signed-header auth.

Request:

Source: relayer/api-reference.md
{
"text": "hello world"
}

text must be non-empty and at most 64 KiB.

Response:

Source: relayer/api-reference.md
{
"vector": [0.01, -0.02]
}

POST /api/ask​

Recall memories, inject them into an LLM prompt, and return an AI-generated answer with the context used.

Request:

Source: relayer/api-reference.md
{
"question": "What do you know about my preferences?",
"limit": 5,
"namespace": "demo"
}

limit defaults to 5 and caps at 100. namespace defaults to "default". The request accepts the same optional scoring_weights object as POST /api/recall.

Response:

Source: relayer/api-reference.md
{
"answer": "Based on your memories, you prefer dark mode and live in Hanoi.",
"memories_used": 2,
"memories": [
{
"blob_id": "walrus-blob-id",
"text": "User prefers dark mode",
"distance": 0.12
}
]
}

POST /api/restore​

Rebuild missing vector entries for one namespace. Queries onchain blobs by owner and namespace, downloads from Walrus, decrypts, re-embeds, and re-indexes only the entries missing from the index.

Request:

Source: relayer/api-reference.md
{
"namespace": "demo",
"limit": 10
}

limit defaults to 10 and caps the onchain query itself, newest blobs first. Raise it to cover a larger namespace.

Response:

Source: relayer/api-reference.md
{
"restored": 3,
"skipped": 7,
"failed": 0,
"total": 10,
"namespace": "demo",
"owner": "0x...",
"truncated": false
}

skipped is onchain blobs already in the local success index. failed is permanent decrypt/UTF-8 failures on this onchain page (negative-cache hits plus new permanent failures this call), so it never exceeds total. Transient download, decrypt, or embed errors are not counted in failed; when a page yields only those, truncated is true so the caller retries.

truncated=true means this restore is known-retryable-incomplete: more missing blobs than limit allowed this call to restore, or the sidecar's owner-wide candidate fetch hit its cap and raising limit can still expand that fetch (limit < 20). Once the sidecar cap is saturated (limit >= 20, cap pinned at 100), truncation follows this call's missing-blob page length, not onchain total. A fully restored namespace does not loop. truncated=false is not proof the sidecar saw every onchain blob; blobs beyond the owner-wide sidecar candidate cap can still be missing. WALM-451 tracks a sourceCapped field for that case (WALM-451). Relayers older than WALM-319 omit truncated; SDKs default it to false.

POST /api/forget​

Delete every vector index row for one namespace. The Walrus blobs persist, so a later POST /api/restore call can re-index them. The relayer resolves the owner from the signed headers and only deletes that owner's rows.

Request:

Source: relayer/api-reference.md
{
"namespace": "demo"
}

namespace defaults to "default".

Response:

Source: relayer/api-reference.md
{
"deleted": 12,
"namespace": "demo",
"owner": "0x..."
}

deleted is the number of index rows the relayer removed.

POST /api/stats​

Return the memory count and stored byte total for one namespace, scoped to the authenticated owner.

Request:

Source: relayer/api-reference.md
{
"namespace": "demo"
}

namespace defaults to "default".

Response:

Source: relayer/api-reference.md
{
"memory_count": 42,
"storage_bytes": 1048576,
"namespace": "demo",
"owner": "0x..."
}

MCP Transports​

The relayer also proxies Model Context Protocol traffic to its Node sidecar. GET, POST, DELETE, and OPTIONS on /api/mcp serve the Streamable HTTP transport, and GET /api/mcp/sse plus POST /api/mcp/messages serve the legacy SSE transport. These routes use bearer authentication instead of signed headers. See Reference for transport details and client configuration.