Mint and use the Bearer JWT every Ava Protocol REST endpoint requires — the EIP-191 sign-in flow for users plus the out-of-band CLI mint for server tooling.
Every endpoint except client.health.check() requires a Bearer JWT on the Authorization header. The SDK manages this for you once you've supplied a token — set it once on the Client and every subsequent request carries it automatically. This page covers how you obtain that token.
There are two production-supported paths. Both produce the same Bearer JWT and use the same EOAWallet.authKey cache shape; the difference is who signs.
For the per-method reference see Auth, buildAuthMessage(), and signAuthMessage().
The connected wallet signs a canonical message; the SDK exchanges it for a JWT and sets it on the transport in one call. Use this for any user-driven action.
import { Client, buildAuthMessage } from "@avaprotocol/sdk-js";
const client = new Client({ baseUrl: process.env.NEXT_PUBLIC_AVS_REST_URL! });
// 1. Build the canonical message client-side. Pure — no SDK network call.
const { message } = buildAuthMessage({ ownerAddress });
// 2. Ask the connected wallet to sign it (wagmi / viem / window.ethereum).
const signature = await walletClient.signMessage({ message, account: ownerAddress });
// 3. Exchange the signature for a JWT. The SDK stores it on the transport
// automatically — every subsequent client.* call carries it.
const { token } = await client.auth.exchange({ message, signature, ownerAddress });
// Persist `token` server-side (NextAuth session, cookie, EOAWallet.authKey row)
// so future server actions can call client.setToken(token) instead of re-prompting.
The signed message looks like this (AUTH_TEMPLATE in the SDK):
Please sign the below text for ownership verification.
URI: https://app.avaprotocol.org
Chain ID: 11155111
Version: 4
Issued At: 2026-05-27T22:14:00.000Z
Expire At: 2026-05-28T22:14:00.000Z
Wallet: 0x804e49e8C4eDb560AE7c48B554f6d2e27Bb81557
Chain ID defaults to Chains.EigenLayerAuth (Sepolia, 11_155_111). Override via the chainId option on buildAuthMessage if you want a different value in the signed payload — but see Multi-chain behaviour below before designing the UX around per-chain sign prompts.
For background jobs, webhooks, CI smoke tests, or anything that runs without a connected wallet, mint a long-lived JWT once via the aggregator's CLI on the gateway host and treat it as a credential. The CLI signs with the gateway's JWT signing secret directly — no wallet signature, no SDK round-trip.
./ap create-api-key \
--config=config/gateway.yaml \
--role=admin \
--subject=0x804e49e8C4eDb560AE7c48B554f6d2e27Bb81557 \
--chain-id=11155111
Output is a JWT string. Store it as a deploy-time secret and pass it on Client construction:
const client = new Client({
baseUrl: process.env.AVS_REST_URL!,
token: process.env.AVS_API_KEY,
});
CLI-minted tokens have a 10-year expiry — they're effectively long-lived credentials. Rotate by minting a new one and replacing the secret; the gateway doesn't track or revoke previously-issued JWTs.
The CLI is also the only way to mint a JWT for an EOA you don't have a signature from — there is no SDK-level "admin mint for arbitrary subject" call.
JWTs minted via client.auth.exchange() expire 24 hours after issuance (the message's Expire At field controls this; default is issuedAt + 24h). The transport doesn't refresh them automatically — when a token expires or is otherwise rejected, the next request returns:
401 Unauthorized
{
"type": "about:blank",
"title": "Authentication required",
"code": "AUTH_REQUIRED",
...
}
The SDK throws AuthRequiredError for these. Catch it and re-run client.auth.exchange(...) (re-prompt the user to sign) instead of retrying with the stale token.
import { Client, AuthRequiredError } from "@avaprotocol/sdk-js";
try {
const workflows = await client.workflows.list();
} catch (err) {
if (err instanceof AuthRequiredError) {
// Token missing / expired — kick the user back to the sign-in flow.
await reauthenticate();
return retry();
}
throw err;
}
To deliberately discard the current token without making any other state change:
client.auth.clear();
The signed message embeds a single Chain ID and the JWT's aud claim carries that exact value. In practice today, the gateway routes per-request by body.chainId / inputVariables.settings.chain_id and only falls back to aud as a default — so one Sepolia-signed JWT works for workflow calls on all four supported chains. This means your sign-in UX can prompt the user once per session, not once per chain switch.
This is a property of the current gateway, not the JWT itself. If a future server change tightens aud enforcement, you'll need one signature per chain (same cardinality v3 forced, minus the silent admin-mint fallback). Verify against your target gateway before committing to a one-prompt-per-session sign-in flow:
// After sign-in with chainId=11155111, confirm cross-chain reuse works.
await client.wallets.list(); // implicit aud chain
await client.workflows.simulate({ chainId: 1, ... }); // mainnet
await client.workflows.simulate({ chainId: 8453, ... }); // base
await client.workflows.simulate({ chainId: 84532, ... }); // base-sepolia
If any call returns a 403 or Problem.code === "AUTH_AUDIENCE_MISMATCH", the gateway is enforcing strict aud — fall back to minting one JWT per (eoa, chain).
When you have a private key in hand (CI tests, scripts, never browser code), the SDK ships a one-shot helper that builds the message, signs it, and exchanges it in one call:
import { Client } from "@avaprotocol/sdk-js";
const client = new Client({ baseUrl: process.env.AVS_REST_URL! });
await client.auth.exchangeWithKey(process.env.TEST_PRIVATE_KEY!);
// Token is set; client.workflows.* / wallets.* / etc. all carry it.
Use this only when the private key is part of the runtime context (test fixtures, server-side admin scripts). Production browser code uses the Browser flow — the SDK never sees the user's private key.
A few v3 auth patterns no longer exist; if you see code reaching for them, it's leftover from the gRPC SDK:
client.authWithAPIKey({ message, apiKey }) — gone. v4 has no API-key-mints-user-JWT path; that's the admin / server flow via the CLI now.client.getSignatureFormat(eoa) — gone. Build the message client-side via buildAuthMessage({ownerAddress}); no server round-trip.{ authKey } option on resource methods — gone. The Client transport carries the Bearer token; methods don't take it explicitly.aud JWT from a browser signature. The CLI's --chain-id=0 flag does this server-side; consult the Auth model section of the studio v4 upgrade plan if you're migrating a multi-chain consumer.