# Use Domino with Para (/docs/build/para)



If your app signs users in with [Para](https://www.getpara.com), they can use Domino's quests, points and leaderboards without a second login. Your server checks a Para JWT and tells Domino who the user is and which wallets Para holds for them. Their Para wallets join their Domino wallets, so wallet quests count them straight away.

This is [app login](/docs/build/app-login) with Para's JWT as the session. It needs a server route, such as a [Domino-hosted app's Worker](/docs/build/hosted-app) or your own backend.

```mermaid
sequenceDiagram
    actor User
    participant Browser as Your app
    participant Server as Your server
    participant Domino
    User->>Browser: Log in with Para
    Browser->>Server: Token on /relay
    Server->>Server: Verify it
    Server->>Domino: User ID, wallets
    Domino-->>Browser: Quests
```

## Before you start [#before-you-start]

* **An app login key that vouches for accounts.** An organization admin [creates the key](/docs/build/app-login#1-create-an-integration-key) for your app's address and [turns on **Allow vouching**](/docs/build/app-login#turn-it-on). Your server reads it as `APP_IDENTITY_KEY`.
* **Your Para API key's ID.** Para sets each JWT's audience (`aud`) to the ID of the API key that issued it, so the route only accepts your app's tokens. Save it as `PARA_API_KEY_ID`.
* **Para's environment.** The route reads Para's production keys from `https://api.getpara.com`. With a beta API key, change `apiUrl` to `https://api.beta.getpara.com`.

## Connect Para [#connect-para]

### 1. Add the server route [#1-add-the-server-route]

Mount this module on `/relay/*` in your server, with your app's address as `origin`, and set `cosmos` to your app's Cosmos chain. It fetches Para's public keys and accepts the JWT as an `Authorization: Bearer` header.

<!-- example-source: apps/docs/examples/login-providers/para.ts -->

```ts
import { createParticipantBackend } from "@domino-sdk/relay/portal-proxy"
import { createRemoteJWKSet, errors, jwtVerify } from "jose"

// Your app's Cosmos chain: Para's token names no chain ID, and Domino needs one.
const cosmos = { network: "cosmos:cosmoshub-4", prefix: "cosmos" } as const

type ParaWallet = { type?: string; address?: string }

/** A wallet Domino can link, or null for a chain it doesn't take. */
function wallet({ type, address = "" }: ParaWallet) {
  if (type === "EVM") {
    return { kind: "wallet", chain: "eip155", address } as const
  }

  if (type === "SOLANA") {
    return { kind: "wallet", chain: "solana", address } as const
  }

  // Only your chain's addresses: another prefix is another Cosmos chain.
  if (
    type === "COSMOS" &&
    address.slice(0, address.lastIndexOf("1")) === cosmos.prefix
  ) {
    return {
      kind: "wallet",
      chain: "cosmos",
      network: cosmos.network,
      address,
    } as const
  }

  return null
}

/**
 * The wallets in a Para JWT: its embedded `wallets` first, then the external wallet the user
 * signed in with, at most 20.
 */
export function paraWallets(data: {
  wallets?: ParaWallet[]
  externalWallet?: ParaWallet
}) {
  return [...(data.wallets ?? []), data.externalWallet]
    .map((entry) => (entry ? wallet(entry) : null))
    .filter((account) => account !== null)
    .slice(0, 20)
}

/** Verifies the JWT from Para's `issueJwt()` against Para's public keys and your API key's ID. */
export function paraAuthenticate(config: {
  /** The `aud` of your app's tokens: your Para API key's ID. */
  apiKeyId: string
  /** `https://api.getpara.com`, or `https://api.beta.getpara.com` for Para's beta environment. */
  apiUrl: string
}) {
  const jwks = createRemoteJWKSet(
    new URL("/.well-known/jwks.json", config.apiUrl)
  )

  return async (request: Request) => {
    const token = request.headers
      .get("Authorization")
      ?.match(/^Bearer (\S+)$/)?.[1]

    if (!token) {
      return null
    }

    try {
      const { payload } = await jwtVerify<{
        data?: { wallets?: ParaWallet[]; externalWallet?: ParaWallet }
      }>(token, jwks, { audience: config.apiKeyId })

      return payload.sub
        ? { subject: payload.sub, accounts: paraWallets(payload.data ?? {}) }
        : null
    } catch (error) {
      // A forged, expired or misaddressed token is a signed-out visitor. Anything else, such as
      // the provider's keys failing to load, is a fault.
      if (
        [
          errors.JWTClaimValidationFailed,
          errors.JWTExpired,
          errors.JWSSignatureVerificationFailed,
          errors.JWSInvalid,
          errors.JWTInvalid,
          errors.JWKSNoMatchingKey,
          errors.JOSEAlgNotAllowed,
        ].some((type) => error instanceof type)
      ) {
        return null
      }

      throw error
    }
  }
}

let backend: ReturnType<typeof createParticipantBackend> | undefined

/** The `/relay/*` handler, built once so the verifier keeps its keys between requests. */
export function participant(env: {
  APP_IDENTITY_KEY: string
  PARA_API_KEY_ID: string
}) {
  backend ??= createParticipantBackend({
    baseUrl: "https://relay.domino.run",
    origin: "https://app.example.com",
    integrationKey: env.APP_IDENTITY_KEY,
    authenticate: paraAuthenticate({
      apiKeyId: env.PARA_API_KEY_ID,
      apiUrl: "https://api.getpara.com",
    }),
  })

  return backend
}
```

The route signs nobody in for a missing, expired or forged token, or one issued for another Para API key. Domino gets the Para user ID as the member's app identity.

> [!DEV] What the route sends
> - **EVM, Solana and your Cosmos chain.** Para's `data.wallets` entries of type `EVM` and `SOLANA`, and the external wallet a user logged in with. A Domino Cosmos wallet needs a chain ID, and Para's token names none, so the route vouches `COSMOS` wallets only on the chain you name, and only addresses with that chain's prefix. Other wallet types are left out, because Domino refuses the whole exchange when one account is invalid.
> - **Embedded wallets first, at most 20.** Domino attaches up to 5 wallets per chain family and refuses the rest with `limit-reached`, and an exchange carries at most 20 accounts. Para's own wallets come before the external one.
> - **Not `/sessions/verify`.** Para's verification-token endpoint confirms a session but returns no wallets, so the route reads them from the JWT.
>
> On a Domino-hosted Worker, call it from your `fetch` handler: `if (new URL(request.url).pathname.startsWith("/relay/")) return participant(env)(request)`. `participant` builds the handler on the first request and reuses it, so the provider's keys are fetched once, not on every request.

### 2. Send the JWT from the browser [#2-send-the-jwt-from-the-browser]

Give Domino's browser client a `getToken` that returns a Para JWT, and pass the client to `RelayProvider`. Each `issueJwt()` call asks Para's API for a new token and extends the Para session, so keep the token until shortly before it expires; it lasts as long as your Para session length, 30 minutes by default.

```tsx
import { createBrowserRelayClient } from "@domino-sdk/relay/browser"
import { RelayProvider } from "@domino-sdk/relay/react"
import type { ReactNode } from "react"

// From @getpara/react-sdk or @getpara/web-sdk: the Para client your app created.
declare const para: {
  isFullyLoggedIn(): Promise<boolean>
  issueJwt(): Promise<{ token: string }>
}

let issued: { token: string; expiresAt: number } | undefined

async function paraToken() {
  if (!(await para.isFullyLoggedIn())) return null

  if (!issued || issued.expiresAt < Date.now() + 60_000) {
    const { token } = await para.issueJwt()
    const { exp } = JSON.parse(atob(token.split(".")[1].replace(/-/g, "+").replace(/_/g, "/")))
    issued = { token, expiresAt: exp * 1000 }
  }

  return issued.token
}

const relay = createBrowserRelayClient({ baseUrl: "/relay", getToken: paraToken })

export function Community({ children }: { children: ReactNode }) {
  return <RelayProvider client={relay}>{children}</RelayProvider>
}
```

When the user logs in or out with Para, clear `issued` on logout and call `useSession().refresh()` so the page reads the Domino session again, as [app login](/docs/build/app-login#3-connect-the-browser) explains.

### 3. Link the embedded wallet for payouts [#3-link-the-embedded-wallet-for-payouts]

A leaderboard's [payout list](/docs/build/leaderboards#export-the-payout-list) leaves out vouched wallets, because the member never proved them to Domino. To pay prizes to the embedded EVM wallet, ask the member to sign Domino's message with it once. The signature proves the wallet, and it stays linked as if the member had linked it themselves.

Para doesn't hand out its embedded wallet as an EIP-1193 provider, so `evmSigner` doesn't fit. It signs EVM messages through its viem account instead, so wrap that in a signer:

```tsx
import type { WalletSigner } from "@domino-sdk/relay"
import { useWallets } from "@domino-sdk/relay/react"

// From @getpara/viem-v2-integration: createParaAccount(para), a viem account.
declare const account: {
  address: `0x${string}`
  signMessage(input: { message: string }): Promise<`0x${string}`>
}

const paraSigner: WalletSigner = {
  chain: "eip155",
  name: "Para",
  address: async () => account.address,
  sign: async (message) => ({ signature: await account.signMessage({ message }) }),
}

export function useProveEmbeddedWallet() {
  const { link } = useWallets()

  return () => link(paraSigner)
}
```

The message states that it authorizes no transaction or transfer. See [Bring your own wallet provider](/docs/build/wallets#bring-your-own-wallet-provider) for the other signers.

### 4. Check it [#4-check-it]

* Log in with Para and check that the app shows the member's Domino quests.
* In Console, open the member's page: **Wallets** lists the Para wallets, marked **Vouched by** your key's name.
* Prove the embedded wallet and check that it is no longer marked **Vouched by**.
* Log out of Para and check that the member's data disappears.

> [!NOTE] Checked against Para's documentation
> The route is tested against Para's documented JWT (your API key's ID as audience, the user ID as `sub`, wallets in `data.wallets`) with a throwaway key served as Para's JWKS. It has not yet been run against a live Para app. Para documents only `EVM` and `SOLANA` wallets in the token; whether Cosmos wallets appear as `COSMOS`, and in which address format, is not yet confirmed.

## Where next [#where-next]

- [Use your app's login](/docs/build/app-login) — keys, refusals and how to test the login in a browser.
- [Wallets](/docs/build/wallets) — wallet quests, limits and what operators see.
