> For the complete documentation index, see [llms.txt](https://city-protocol.gitbook.io/docs/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://city-protocol.gitbook.io/docs/developers/city-protocol-sdk/wallet.md).

# Wallet

Wallet login, embedded wallets and server-side identity verification.

Wallet login and session management run on Privy's embedded wallets in your frontend, and `verifyPrivyWallet` from `@cityprotocol/core` verifies each identity token on your server.

## How it works

The wallet module is the identity and account-access layer of every product built on City Protocol. It authenticates users, provisions or connects their wallets and verifies each wallet-linked session across the client and server boundary, so vault interactions, access-gated flows and onchain operations all start from a wallet that your server has verified. The backend stays stateless: it verifies the identity token on every protected request and keeps no session of its own.

| Component           | Where it runs       | What it does                                                                                                                                                                |
| ------------------- | ------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Privy provider      | Frontend            | `PrivyProvider` sits at the root of the React tree, initializes the Privy session, manages login state and the login methods, and sets the embedded-wallet policy           |
| Identity token      | Frontend to backend | A short-lived JWT that Privy issues after login; it encodes the session and the linked wallet and is the only credential your backend trusts                                |
| `verifyPrivyWallet` | Backend             | Validates the identity token against Privy's API with your app credentials and returns the user's Privy `id` and `wallet.address`; it throws on an invalid or expired token |
| Wallet gate         | Frontend            | A component pattern that renders by `ready` and `authenticated` from `usePrivy` and attaches the identity token to every call to your backend                               |

Gas sponsorship sits in the same wallet layer. With sponsorship enabled in the Privy dashboard, your app covers users' transaction fees through Privy's embedded wallets, and every SDK call runs unchanged, as [Gas sponsorship with Privy](/docs/developers/city-protocol-sdk/wallet/gas-sponsorship-with-privy.md) sets out.

## The five-step handshake

The handshake runs in a fixed order across your frontend, Privy and your backend.

<figure><img src="https://3297077862-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FJxNp2waFk6hg1G4lEQZu%2Fuploads%2FPHHAcGGNFCwdSqt3CRJP%2Ffig-sdk-wallet.png?alt=media" alt="Sequence across your frontend, Privy and your backend: login with an embedded wallet for new users, the identity token, the request with a Bearer token, verification through verifyPrivyWallet and the verified wallet"><figcaption><p>The five-step wallet handshake. Privy issues the identity token in the frontend, your backend verifies it with verifyPrivyWallet on every protected request, and the verified address becomes the user's identity.</p></figcaption></figure>

{% stepper %}
{% step %}

### Connect

The user logs in through `login` from `usePrivy`, which opens the Privy modal. `loginMethods` sets the entry points: `"wallet"`, with `"email"`, `"sms"` or `"google"` added where the product supports them. With `createOnLogin` set to `"users-without-wallets"`, Privy provisions an embedded wallet for a user who has none, and `"off"` leaves every user to connect their own.
{% endstep %}

{% step %}

### Token issuance

On login, Privy issues an identity token in the frontend, scoped to the session and carrying the linked wallet address. `useIdentityToken` returns it and refreshes it automatically, so keep it out of `localStorage`.
{% endstep %}

{% step %}

### Request

The frontend attaches the token as `Authorization: Bearer <identityToken>` to every API call that needs a verified session.
{% endstep %}

{% step %}

### Verify

The backend passes the raw header to `verifyPrivyWallet` with `PRIVY_APP_ID` and `PRIVY_APP_SECRET`. The helper validates the token signature against Privy's infrastructure and returns the user object with the Privy `id` and the verified `wallet.address`, and it throws when the token is invalid or expired.
{% endstep %}

{% step %}

### Associate

The verified wallet address becomes the user's identity for every City Protocol feature: vault deposits, access-control checks and any onchain interaction that needs a known sender. The user's `CityClient` takes the Viem wallet client that Privy manages for the same account, so the onchain signer and the verified identity are always the same address.
{% endstep %}
{% endstepper %}

## Security constraints

* **Keep `PRIVY_APP_SECRET` on the server:** The secret authenticates your backend with Privy's verification infrastructure, and a copy in client code would let anyone forge verification requests. Of the Privy credentials, the client bundle carries only `VITE_PRIVY_APP_ID`, the public app identifier.
* **Send the token as `Authorization: Bearer <identityToken>`:** The standard header lets the backend extract and verify the token in one step and keeps it out of URL parameters and request bodies.
* **Resolve the wallet address on the server:** A wallet address rendered in the browser is no proof of ownership. Only the identity token, verified through `verifyPrivyWallet`, confirms that the session is authentic and the wallet is linked, so the backend takes the address from the verified token and never from the client.
* **Verify on every protected request:** Identity tokens are session-scoped. The backend calls `verifyPrivyWallet` on each request to a protected endpoint and caches no result, so revoked or expired sessions are rejected at once.

{% content-ref url="/pages/5iZrQ9mAmyvEzkmoffwn" %}
[Installation](/docs/developers/city-protocol-sdk/wallet/installation.md)
{% endcontent-ref %}

{% content-ref url="/pages/sQaiGU4DBFZ3TEOTQp3I" %}
[Environment variables](/docs/developers/city-protocol-sdk/wallet/environment-variables.md)
{% endcontent-ref %}

{% content-ref url="/pages/g1MssiKyrArHewnhtfQ6" %}
[React setup](/docs/developers/city-protocol-sdk/wallet/react-setup.md)
{% endcontent-ref %}

{% content-ref url="/pages/A2iZu05Icqnm5tN1pOEH" %}
[Client-side authentication](/docs/developers/city-protocol-sdk/wallet/client-side-authentication.md)
{% endcontent-ref %}

{% content-ref url="/pages/vE5o2DwWx8vJBZ2N4aA7" %}
[Backend verification](/docs/developers/city-protocol-sdk/wallet/backend-verification.md)
{% endcontent-ref %}

{% content-ref url="/pages/mcNRXPw9oS3K95QGkNyu" %}
[Common frontend flows](/docs/developers/city-protocol-sdk/wallet/common-frontend-flows.md)
{% endcontent-ref %}

{% content-ref url="/pages/BR9piUmYPWhJSgmH5mcx" %}
[Recommended architecture](/docs/developers/city-protocol-sdk/wallet/recommended-architecture.md)
{% endcontent-ref %}

{% content-ref url="/pages/PeecY4BaTF44nTH8Q5uD" %}
[Gas sponsorship with Privy](/docs/developers/city-protocol-sdk/wallet/gas-sponsorship-with-privy.md)
{% endcontent-ref %}
