Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.calmtreasury.xyz/llms.txt

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

Enable identity tokens in Privy

Calm authenticates users via Privy’s identity tokens — short-lived JWTs that carry the user’s sub, linked wallet, and linked email. They are not the same as Privy access tokens. You need to enable identity tokens once per Privy app before the SDK can bootstrap a session.
1

Open the Privy dashboard

Sign in at dashboard.privy.io and pick your app.
2

Navigate to User Management → Authentication → Advanced

Privy User Authentication settings panel, Advanced tab selected
3

Toggle "Return user data in an identity token" ON

Scroll the Advanced tab until you see it. Once enabled, the user’s email and linked_accounts (including the linked wallet) ride inside the identity token — which is exactly what the Calm SDK forwards to POST /v1/session.
Return user data in an identity token toggle, enabled
4

Use getIdentityToken in your app

Pass Privy’s getIdentityToken (NOT getAccessToken) to <CalmProvider>:
const { getIdentityToken } = usePrivy();
return <CalmProvider getAccessToken={getIdentityToken}></CalmProvider>;
If this toggle is off, the SDK fails to bootstrap — the API can’t see the user’s email or wallet in the JWT claims and returns email_not_linked / wallet_not_linked.

The chain of trust

Privy verifies the user (email + wallet ownership)


Privy issues an identity-token JWT (signed by Privy's keys)


Calm verifies the JWT against Privy's JWKS at
  https://auth.privy.io/api/v1/apps/<aud>/jwks.json


Calm issues a short-lived session cookie ({ sub, wallet, app_id })


Subsequent requests attach the cookie via credentials: "include"
No partner registration, no API key, no shared secret. Anyone with a Privy app and a verified user can use Calm.

What POST /v1/session does

1

Reads the Privy identity token

From Authorization: Bearer <jwt>. The SDK calls getAccessToken() (or Privy’s getIdentityToken() in practice).
2

Self-discovers the Privy app id

From the JWT’s aud claim. No X-Privy-App-Id header needed.
Forging the aud to a different app id doesn’t work — the JWT’s signature won’t validate against that app’s JWKS.
3

Verifies the signature

Against https://auth.privy.io/api/v1/apps/<aud>/jwks.json. JWKS is cached per app id for the process lifetime.
4

Extracts the linked wallet

The first entry in linked_accounts with type: "wallet". The wallet address is lower-cased before binding.
5

Mints the session cookie

HS256-signed JWT with claims { sub, wallet, app_id }. Sent via Set-Cookie: calm_session=…; HttpOnly; SameSite=None; Secure; Partitioned.
AttributeValueWhy
HttpOnlyJavaScript in the partner app can’t read it.
SecureRequired by browsers when SameSite=None.
SameSite=NoneRequired to attach on cross-site fetches (partner → Calm).
PartitionedAvoids being categorized as a third-party tracking cookie in Safari/Chrome (CHIPS).
Max-Age3600One hour. SDK re-bootstraps when expired.
{
  "sub":   "did:privy:cl…",
  "wallet": "0xabc…",
  "app_id": "cm…",
  "iss":   "calmtreasury.xyz",
  "iat":   1717340000,
  "exp":   1717343600
}
  • Email (collected in the register form, stored server-side)
  • First/last name (passed to Bridge at register, not echoed back)
  • KYC state (proxied fresh from Bridge on every state read)
  • Virtual account details (proxied fresh from Bridge)
The cookie is identity + scope only. State is always read live.

CORS

The browser must include the cookie on cross-site fetches. That requires:
  • The API responds with Access-Control-Allow-Origin: <partner-origin> (not *)
  • The API responds with Access-Control-Allow-Credentials: true
  • The SDK sets credentials: "include" (we already do)
If you self-host or proxy the API, make sure the partner’s origin is in your allowlist.