Purpose
The indexer keeps the backend in sync with onchain state so the relayer can serve the account existence check and owner-token lookup from PostgreSQL instead of onchain RPC calls. It does not feed delegate-key auth resolution, which follows a separate priority chain.
Why it exists
Delegate-key authentication does not walk AccountRegistry. A cold key is verified against the signed x-account-id, or against MEMWAL_ACCOUNT_ID when that env is set. The indexer is what fills the accounts table the existence check and owner-token lookup read.
How it works
The indexer is a standalone Rust service (services/indexer) that:
- Connects to the same PostgreSQL database as the relayer
- Reads Sui checkpoints from a remote checkpoint store through the Sui checkpoint-ingestion framework
- Filters for
AccountCreatedevents from the Walrus Memory package - Inserts
account_id → ownermappings into theaccountstable - Records its checkpoint watermark through the framework so it can resume after restarts
Auth resolution flow
When the relayer receives a signed request, it resolves the delegate key's account in this order:
- PostgreSQL cache (
delegate_key_cache), populated after a successful resolution - Signed
x-account-id, orMEMWAL_ACCOUNT_IDwhen the header is absent, one account object, not a registry walk - Otherwise
401. A cold key with no account id is not looked up onchain
After a successful resolution, the mapping is cached in delegate_key_cache. The indexed accounts table is not part of this chain; the indexer fills it for the existence check and owner-token lookup instead.
Configuration
The indexer reads these environment variables:
| Variable | Required | Default | Description |
|---|---|---|---|
DATABASE_URL | Yes | PostgreSQL connection string | |
MEMWAL_PACKAGE_ID | Yes | Walrus Memory contract package ID to filter events |
It also takes 2 command-line flags:
| Flag | Description |
|---|---|
--remote-store-url | URL of the remote checkpoint store to read checkpoints from |
--first-checkpoint | Checkpoint to start from. Use one at or before the checkpoint that published your Walrus Memory package, so the indexer sees every AccountCreated event. |
Running
Source: indexer/purpose.md$ cd services/indexer
$ cargo run --release -- --remote-store-url <CHECKPOINT_STORE_URL> --first-checkpoint <CHECKPOINT>
The indexer is recommended for production but optional for development. If the indexer stops or falls behind, the accounts table lags onchain state, so the existence check and owner-token lookup that read it will not see recently created accounts until it catches up; delegate-key auth resolution is unaffected, because the indexed accounts table is not part of that chain (a cold key still needs a signed x-account-id or MEMWAL_ACCOUNT_ID, otherwise it gets a 401).
Which routes read the accounts table?
Three places in the relayer read the accounts table, and all of them use the same db.find_account_by_owner helper:
GET /api/accounts/{owner}/exists: A public, unauthenticated endpoint that returns{ "exists": bool }. It checks registry presence only, not onchainactivestatus. The indexer only decodesAccountCreatedevents, so it never removes or updates a row after an account is deactivated.POST /v1/owner-tokens: Rejects the request with400when the owner has no indexed account, instead of minting a token for a nonexistent account.- Owner-token reads on
/v1/owners/{owner}/{namespaces,memories,agents}: The read API's auth middleware,auth::verify_read_api_auth, resolves the token's owner to an account through the table and returns401when the account no longer resolves.
None of them takes part in delegate-key resolution for signed requests. Because the table never records deactivation, owner-token issuance and owner-token reads both verify onchain that the account is still active. The existence endpoint does not.
When the owner-token flow checks the accounts table
POST /v1/owner-tokens runs its gates in this order: address format validation, configuration check, per-owner rate limit, account lookup in the accounts table, an onchain check that the account is still active, and then mint. The table lookup comes after the cheaper gates because it costs a database round trip, so only requests that clear them reach the accounts table. The relayer lowercases the owner address before the lookup, because the indexer always stores the accounts.owner column as lowercase hex.
The onchain check covers what the table cannot show:
- If the account has been deactivated, the relayer returns
403withMemWalAccount has been deactivated. - If the account object is not found onchain or belongs to a different owner, the relayer returns
400withowner has no MemWalAccount. - If the onchain verification itself fails, the relayer returns
503instead of minting.
Owner-token reads run the same active check, so a token minted before deactivation stops working once the account is deactivated, even before the token expires.
What happens before an account is indexed
Until the indexer inserts a row for a newly created account, the relayer treats the owner as having no account. The lookup resolves to no account, so GET /api/accounts/{owner}/exists reports that the owner has no account, and POST /v1/owner-tokens fails with 400 and the message owner has no MemWalAccount. Both routes return the account after the indexer observes the AccountCreated event and inserts the row.