# Get account balance Source: https://docs.privy.io/api-reference/accounts/balance get /v1/accounts/{account_id}/balance Get the balance of an account, aggregated across all wallets and supported chains. # Create account Source: https://docs.privy.io/api-reference/accounts/create post /v1/accounts Creates a new account with associated wallets. # Get account Source: https://docs.privy.io/api-reference/accounts/get get /v1/accounts/{account_id} Get an account by account ID. # List all accounts Source: https://docs.privy.io/api-reference/accounts/list get /v1/accounts List all accounts in your app. # Update account Source: https://docs.privy.io/api-reference/accounts/update patch /v1/accounts/{account_id} Update an account by account ID. Supports updating the display name and adding new wallets. # Create aggregation Source: https://docs.privy.io/api-reference/aggregations/create post /v1/aggregations Create a new aggregation to track and measure metrics over a time window. ### SDK methods Learn more about creating aggregations and using them in policies [here](/controls/policies/stateful-policies). # Delete aggregation Source: https://docs.privy.io/api-reference/aggregations/delete delete /v1/aggregations/{aggregation_id} Delete an aggregation by aggregation ID. # Get aggregation Source: https://docs.privy.io/api-reference/aggregations/get get /v1/aggregations/{aggregation_id} Get an aggregation by aggregation ID. # Add to allowlist Source: https://docs.privy.io/api-reference/apps/add-to-allowlist post /v1/apps/{app_id}/allowlist Add a new entry to the allowlist for an app. The allowlist must be enabled. ### SDK methods Learn more about managing your allowlist using our SDKs [here](/user-management/users/managing-users/allowlist#adding-to-the-allow-list). # List allowlist entries Source: https://docs.privy.io/api-reference/apps/list-allowlist get /v1/apps/{app_id}/allowlist Get all allowlist entries for an app. Returns the list of users allowed to access the app when the allowlist is enabled. ### SDK methods Learn more about managing your allowlist using our SDKs [here](/user-management/users/managing-users/allowlist#getting-the-allow-list). # Remove from allowlist Source: https://docs.privy.io/api-reference/apps/remove-from-allowlist delete /v1/apps/{app_id}/allowlist Remove an entry from the allowlist for an app. The allowlist must be enabled. ### SDK methods Learn more about managing your allowlist using our SDKs [here](/user-management/users/managing-users/allowlist#removing-from-the-allow-list). # Authorization signatures Source: https://docs.privy.io/api-reference/authorization-signatures Securing Privy API requests with authorization signatures ## Overview [Owners](/controls/overview) provide an additional layer of security for actions taken by your app’s wallets. This primitive helps ensure that only actions explicitly authorized by your server are executed on user wallets. When you specify an owner of a resource, all requests to update that resource must be signed with the associated key (user, authorization key, or key quorum). Requests to take actions with a wallet must also be signed by the wallet’s owner. This security measure verifies that each request comes from your authorized backend systems and helps prevent unauthorized operations. Authorization signatures are an important security measure and we strongly recommend registering authorization keys for all production resources. Learn more about [owners](/controls/authorization-keys/owners/overview) and [authorization signatures](/controls/authorization-keys/keys/overview). ### When are they necessary? Authorization signatures are necessary in the following cases. #### Updating wallets and policies All critical resources, such as wallets and policies, have an `owner_id` field, which indicates the authorization key or quorum whose signatures are required in order to modify the given resource. This means, if the `owner_id` is set, authorization signatures are required for all `PATCH` and `DELETE` requests to the resource. This includes: * `PATCH /v1/wallets/[wallet_id]` * `PATCH /v1/policies/[policy_id]` * `DELETE /v1/policies/[policy_id]` Signatures from the wallet's owner are required to take actions on a wallet by default. If an `owner_id` is set, authorization signatures are required for: * `POST /v1/wallets//rpc` #### Executing actions with wallets Executing actions with wallets requires authorization signature(s) from the wallet's owner, if the wallet has an owner. This includes: * `POST /v1/wallets/[wallet_id]/rpc` #### Updating key quorums Though key quorums do not have owners, updating or deleting a key quorum requires a satisfying set of signatures from the *existing* key quorum that meet the authorization threshold. This includes: * `PATCH /v1/key_quorums/[key_quorum_id]` * `DELETE /v1/key_quorum/[key_quorum_id]` Signatures from the wallet's owner are required to take actions on a wallet by default. If an `owner_id` is set, authorization signatures are required for: ## Usage At a high-level, the flow of using authorization signatures is as follows: Get the private keys that you will use to sign your request. This might be retrieved from the private keys you saved locally (app owners and key quorum owners) or requested from the Privy API using a user JWT (user owners). Construct the request that you intend to make to Privy. This might be updating or deleting wallets, updating or deleting policies, updating or deleting key quorums, or taking actions with wallets. Format and sign the request with your private key(s). Finally, when making your request to Privy, include the authorization signature(s) as a string in the `privy-authorization-signature` header. ### Getting authorization keys To start, get the private keys for the authorization key(s) that owner your resource. If the owner of your resource is an authorization key, get the private key(s) that you saved locally when creating your owner in the Privy API or Dashboard. Privy does not save these private key(s) and cannot help you recover them. If the owner of your resource is a user, request a time-bound user key to take actions with or update the resource. Follow the guide below to learn how to request user keys given a user's access token. Request keys for user owners. Request a user key with a user's access token to sign requests to the Privy API. ### Signing requests Next, sign the request with your authorization key(s). Make sure to correctly format your request before signing the request. If the owner of your resource is a key quorum, make sure to sign the request with enough authorization keys to meet the authorization threshold. Follow this guide for instructions on how to correctly sign your request: Learn how to sign requests to the Privy API. ### Setting required headers Once you have collected your authorization signature(s), set the following header on your request to the Privy API. The authorization signature. If multiple signatures are required, include them as a comma-delimited string. A Unix timestamp in milliseconds (e.g., `1773679531000`) indicating when the request expires. Privy rejects requests where this value is in the past, helping prevent replay attacks. This header is included in the [signature payload](/controls/authorization-keys/using-owners/sign/overview#signature-payload) and must match the value used when computing the authorization signature. If you are using Privy's SDKs, the appropriate authorization signature and request expiry headers are added automatically to your requests. # Add items to a condition set Source: https://docs.privy.io/api-reference/condition-sets/condition-set-items/create post /v1/condition_sets/{condition_set_id}/condition_set_items Add new items to a condition set. Can add up to 100 items at once. # Delete an item from a condition set Source: https://docs.privy.io/api-reference/condition-sets/condition-set-items/delete delete /v1/condition_sets/{condition_set_id}/condition_set_items/{condition_set_item_id} Delete an item from a condition set by condition set ID and item ID. # Get an item from a condition set Source: https://docs.privy.io/api-reference/condition-sets/condition-set-items/get get /v1/condition_sets/{condition_set_id}/condition_set_items/{condition_set_item_id} Get an item from a condition set by condition set ID and item ID. # Get all items from a condition set Source: https://docs.privy.io/api-reference/condition-sets/condition-set-items/get-all get /v1/condition_sets/{condition_set_id}/condition_set_items Get all items in a condition set with pagination support. # Update items in a condition set Source: https://docs.privy.io/api-reference/condition-sets/condition-set-items/update put /v1/condition_sets/{condition_set_id}/condition_set_items Replace all items in a condition set by condition set ID. Can add up to 100 items at once. # Create condition set Source: https://docs.privy.io/api-reference/condition-sets/create post /v1/condition_sets Create a new condition set. You must provide either "owner" or "owner_id" (but not both) to specify ownership. # Delete condition set Source: https://docs.privy.io/api-reference/condition-sets/delete delete /v1/condition_sets/{condition_set_id} Delete a condition set by condition set ID. # Get condition set Source: https://docs.privy.io/api-reference/condition-sets/get get /v1/condition_sets/{condition_set_id} Get a condition set by condition set ID. # Update condition set Source: https://docs.privy.io/api-reference/condition-sets/update patch /v1/condition_sets/{condition_set_id} Update a condition set by condition set ID. # Create crypto deposit account Source: https://docs.privy.io/api-reference/crypto-deposits/create post /v1/wallets/{wallet_id}/deposit_accounts/crypto Create a deposit address to send crypto to to convert it into a target asset. # Create fiat deposit account Source: https://docs.privy.io/api-reference/fiat/deposit-accounts/create post /v1/wallets/{wallet_id}/deposit_accounts/fiat Creates a Bridge Virtual Account linked to a wallet. Fiat sent to the returned deposit instructions will be converted to the specified crypto asset and delivered to the wallet. # Get fiat deposit account Source: https://docs.privy.io/api-reference/fiat/deposit-accounts/get get /v1/wallets/{wallet_id}/deposit_accounts/fiat/{deposit_account_id} Returns a single fiat deposit account linked to a wallet. # List fiat deposit accounts Source: https://docs.privy.io/api-reference/fiat/deposit-accounts/list get /v1/wallets/{wallet_id}/deposit_accounts/fiat Returns a list of fiat deposit accounts linked to a wallet. # Create external fiat account Source: https://docs.privy.io/api-reference/fiat/external-fiat-accounts/create post /v1/users/{user_id}/external_fiat_accounts Creates an external fiat account linked to a user for use in offramp transfers. # Delete external fiat account Source: https://docs.privy.io/api-reference/fiat/external-fiat-accounts/delete delete /v1/users/{user_id}/external_fiat_accounts/{account_id} Deletes an external fiat account linked to a user. # Get external fiat account Source: https://docs.privy.io/api-reference/fiat/external-fiat-accounts/get get /v1/users/{user_id}/external_fiat_accounts/{account_id} Returns a single external fiat account linked to a user. # List external fiat accounts Source: https://docs.privy.io/api-reference/fiat/external-fiat-accounts/list get /v1/users/{user_id}/external_fiat_accounts Returns a list of external fiat accounts linked to a user. # Create external fiat account Source: https://docs.privy.io/api-reference/fiat/external-fiat-accounts/organizations/create post /v1/organizations/{organization_id}/external_fiat_accounts Creates an external fiat account linked to an organization for use in offramp transfers. # Delete external fiat account Source: https://docs.privy.io/api-reference/fiat/external-fiat-accounts/organizations/delete delete /v1/organizations/{organization_id}/external_fiat_accounts/{account_id} Deletes an external fiat account linked to an organization. # Get external fiat account Source: https://docs.privy.io/api-reference/fiat/external-fiat-accounts/organizations/get get /v1/organizations/{organization_id}/external_fiat_accounts/{account_id} Returns a single external fiat account linked to an organization. # List external fiat accounts Source: https://docs.privy.io/api-reference/fiat/external-fiat-accounts/organizations/list get /v1/organizations/{organization_id}/external_fiat_accounts Returns a list of external fiat accounts linked to an organization. # Get KYB status Source: https://docs.privy.io/api-reference/fiat/kyb/get get /v1/organizations/{organization_id}/kyb Returns KYB status for all providers the organization has initiated KYB with. # Initiate KYB verification Source: https://docs.privy.io/api-reference/fiat/kyb/links post /v1/organizations/{organization_id}/kyb/links Generates a hosted KYB link for the organization and returns the current KYB status snapshot. # Initiate KYB terms of service Source: https://docs.privy.io/api-reference/fiat/kyb/tos post /v1/organizations/{organization_id}/kyb/tos Generates a Bridge terms-of-service acceptance link for the organization. # Get KYC status Source: https://docs.privy.io/api-reference/fiat/kyc-server/get get /v1/users/{user_id}/kyc Returns KYC status for all providers the user has initiated KYC with. # Initiate KYC verification Source: https://docs.privy.io/api-reference/fiat/kyc-server/links post /v1/users/{user_id}/kyc/links Generates a hosted KYC link for the user and returns the current KYC status snapshot. # Initiate KYC terms of service Source: https://docs.privy.io/api-reference/fiat/kyc-server/tos post /v1/users/{user_id}/kyc/tos Generates a Bridge terms-of-service acceptance link for the user. # Idempotency keys Source: https://docs.privy.io/api-reference/idempotency-keys Making Privy API requests idempotent with idempotency keys Idempotency keys prevent duplicate execution of API requests. Privy processes a request with a given idempotency key only once within a 24-hour window, preventing duplicated transactions. ## Required headers Include the following header with REST API requests: A unique identifier for the request, up to 256 characters. Privy recommends V4 UUIDs. ## When to use them Use idempotency keys for: * Any `POST` request that triggers state changes or transactions * Scenarios where network issues might cause request retries * Critical operations where duplicate execution would cause problems Privy treats idempotency keys as optional, but apps should include them for all state-changing operations in production. ## How idempotency works Privy receives a request with a new idempotency key. It processes the request normally and stores both the request details and response for 24 hours. The app sends another request with the same idempotency key within 24 hours: * **Matching body:** Privy returns the stored response without re-executing the operation * **Different body:** Privy returns a 400 error indicating invalid use of the key After 24 hours, idempotency keys expire. Privy processes requests with an expired key as new requests. Changing any part of the request body while reusing an idempotency key results in an error. Each unique operation requires its own idempotency key. ## Error replay behavior Replay behavior varies by endpoint group: | Endpoint group | Endpoints | 4xx replay | 5xx replay | | ------------------ | --------------------------------- | ---------- | ----------------------- | | **Wallet actions** | `/transfer`, `/swap`, `/earn` | Cached | Deleted (retry allowed) | | **RPC** | `/rpc` (all chains) | Cached | Cached | | **Import** | `/wallets/import/init`, `/submit` | Cached | Cached | | **Wallet create** | `/wallets` | Cached | Cached | For RPC, import, and wallet create endpoints, Privy permanently caches a 5xx response against the idempotency key for its 24-hour lifetime. Generate a new key to retry after a server error. **Policy violation exception:** If any endpoint returns a `POLICY_VIOLATION` error, Privy deletes the idempotency record regardless of status code. The app can retry with the same key after resolving the policy issue. ## Generating idempotency keys Generate a unique, random string for each distinct operation. Use V4 UUIDs for best results. ```ts JavaScript/TypeScript theme={"system"} import {v4 as uuidv4} from 'uuid'; // Generate idempotency key const idempotencyKey = uuidv4(); ``` ## Examples ```ts @privy-io/node theme={"system"} import {PrivyClient} from '@privy-io/node'; import {v4 as uuidv4} from 'uuid'; const client = new PrivyClient({appId: '$PRIVY_APP_ID', appSecret: '$PRIVY_APP_SECRET'}); // Generate idempotency key const idempotencyKey = uuidv4(); const res = await client .wallets() .ethereum() .sendTransaction('$WALLET_ID', { idempotency_key: idempotencyKey, caip2: 'eip155:8453', params: { transaction: { to: '0xE3070d3e4309afA3bC9a6b057685743CF42da77C', value: '0x2386F26FC10000', chain_id: 8453 } } }); ``` ```ts TypeScript/JavaScript theme={"system"} import axios from 'axios'; import {v4 as uuidv4} from 'uuid'; // Generate idempotency key const idempotencyKey = uuidv4(); const response = await axios.post( 'https://auth.privy.io/api/v1/wallets/y5ofctvacjiv53u4hmnqi0e5/rpc', { caip2: 'eip155:8453', method: 'eth_sendTransaction', params: { transaction: { to: '0xE3070d3e4309afA3bC9a6b057685743CF42da77C', value: '0x2386F26FC10000', chainId: 8453 } } }, { headers: { 'privy-app-id': 'insert-your-app-id', 'privy-idempotency-key': idempotencyKey, Authorization: 'Bearer insert-your-api-key' } } ); ``` Store the idempotency key alongside transaction records for critical operations. Retry behavior differs by endpoint. See [error replay behavior](#error-replay-behavior) to determine when to generate a new key. # Authorize intent Source: https://docs.privy.io/api-reference/intents/authorize post /v1/intents/{intent_id}/authorize Authorize a pending intent by providing a signature. Can be called by the wallet owner (via user token) or with the app secret. # Create rule Source: https://docs.privy.io/api-reference/intents/create-rule post /v1/intents/policies/{policy_id}/rules Create an intent to add a rule to a policy. The intent must be authorized by the policy owner before it can be executed. # Delete rule Source: https://docs.privy.io/api-reference/intents/delete-rule delete /v1/intents/policies/{policy_id}/rules/{rule_id} Create an intent to delete a rule from a policy. The intent must be authorized by the policy owner before it can be executed. # Get intent Source: https://docs.privy.io/api-reference/intents/get get /v1/intents/{intent_id} Retrieve an intent by ID. Returns its current status, authorization details, and execution result when applicable. Requests authenticated with an app secret can retrieve any intent for the app. Requests authenticated with a user token can retrieve only intents that the authenticated user created, must approve, or has signed. Unrelated intents return a 404 response. # List intents Source: https://docs.privy.io/api-reference/intents/list get /v1/intents List intents for an app. Returns a paginated list with each intent's current status and details. Requests authenticated with an app secret can retrieve all intents for the app. Requests authenticated with a user token return only intents that the authenticated user created, must approve, or has signed. Query parameters only narrow this scoped result set. # Reject intent Source: https://docs.privy.io/api-reference/intents/reject post /v1/intents/{intent_id}/reject Reject a pending intent, preventing it from being executed. Can be called by the intent creator (via user token) or with the app secret. # Create RPC transaction Source: https://docs.privy.io/api-reference/intents/rpc post /v1/intents/wallets/{wallet_id}/rpc Create an intent to execute an RPC method on a wallet. The intent must be authorized by either the wallet owner or signers before it can be executed. # Create transfer action Source: https://docs.privy.io/api-reference/intents/transfer post /v1/intents/wallets/{wallet_id}/transfer Create an intent to execute a token transfer via a wallet. The intent must be authorized by either the wallet owner or signers before it can be executed. # Update key quorum Source: https://docs.privy.io/api-reference/intents/update-key-quorum patch /v1/intents/key_quorums/{key_quorum_id} Create an intent to update a key quorum. The intent must be authorized by the key quorum members before it can be executed. # Update policy Source: https://docs.privy.io/api-reference/intents/update-policy patch /v1/intents/policies/{policy_id} Create an intent to update a policy. The intent must be authorized by the policy owner before it can be executed. # Update policy rule Source: https://docs.privy.io/api-reference/intents/update-rule patch /v1/intents/policies/{policy_id}/rules/{rule_id} Create an intent to update a rule on a policy. The intent must be authorized by the policy owner before it can be executed. # Update wallet Source: https://docs.privy.io/api-reference/intents/update-wallet patch /v1/intents/wallets/{wallet_id} Create an intent to update a wallet. The intent must be authorized by the wallet owner before it can be executed. # Introduction Source: https://docs.privy.io/api-reference/introduction Getting started with the Privy REST API Privy offers low-level APIs you can use to interact with wallets and user objects directly. This means APIs to interface with the following resources: * **Users**: create user objects with appropriate linked accounts and pregenerate wallets for them. * **Wallets**: create, update and use wallets across blockchains. * **Authorization keys**: create and manage authorization keys to manage wallets. * **Policies**: create and manage policies tied to wallets. * **Webhooks**: subscribe to Privy webhooks and react to events in your app. Read more about direct API access below. For experimental APIs that are part of Privy's supported Labs program, see [Privy Labs APIs](/api-reference/labs/overview). ## Base URL All requests to the Privy API must be made to the following base URL: ``` https://api.privy.io ``` HTTPS is required for all requests. HTTP requests will be rejected. ## Authentication All API endpoints require authentication using Basic Auth and a Privy App ID header. Include the following headers with every request: Basic Auth header with your app ID as the username and your app secret as the password. Your Privy app ID as a string. Requests missing either of these headers will be rejected by Privy's middleware. Your Privy app ID and app secret can be found in the [**App settings** > **Basics**](https://dashboard.privy.io/apps?page=settings\&tab=basics) tab for your app. ## Examples ```javascript theme={"system"} fetch('https://api.privy.io/v1/wallets', { method: 'GET', headers: { 'Authorization': `Basic ${btoa('insert-your-app-id' + ':' + 'insert-your-app-secret')}`, 'privy-app-id': 'insert-your-app-id', 'Content-Type': 'application/json' } }) .then(response => response.json()) .then(data => console.log(data)); ``` ```bash theme={"system"} curl -X GET "https://api.privy.io/v1/wallets" \ --user "insert-your-app-id:insert-your-app-secret" \ -H "privy-app-id: insert-your-app-id" \ -H "Content-Type: application/json" ``` ## Rate limits Privy rate limits REST API endpoints to ensure fair usage and system stability. When you encounter a rate limit (HTTP 429 response), implement retry logic with exponential backoff to handle these gracefully. Learn best practices for handling rate limits, including batching, caching, and retry strategies in our [optimizing your setup](/recipes/dashboard/optimizing#handling-rate-limits) guide. # Create key quorum Source: https://docs.privy.io/api-reference/key-quorums/create post /v1/key_quorums Create a new key quorum. ### SDK methods Learn more about creating key quorums using our SDKs [here](/controls/key-quorum/create). # Delete key quorum Source: https://docs.privy.io/api-reference/key-quorums/delete delete /v1/key_quorums/{key_quorum_id} Delete a key quorum by key quorum ID. # Get key quorum Source: https://docs.privy.io/api-reference/key-quorums/get get /v1/key_quorums/{key_quorum_id} Get a key quorum by ID. # Update key quorum Source: https://docs.privy.io/api-reference/key-quorums/update patch /v1/key_quorums/{key_quorum_id} Update a key quorum by key quorum ID. # Advanced swap (Solana) Source: https://docs.privy.io/api-reference/labs/advanced-swap post /v1/wallets/{wallet_id}/experimental/solana/advanced-swap Execute a low-latency synchronous Solana token swap with an embedded wallet. This is a [Privy Labs API](/api-reference/labs/overview). It is subject to change as Privy iterates on the design with partners. Privy provides at least three months of notice before removing a documented Labs API. ### Overview The advanced swap endpoint performs a synchronous Solana token swap in a single request: Privy fetches a quote, signs the transaction via the enclave, and submits it to the network. Wallet data preparation is parallelized with the swap quote fetching to minimize latency. Unlike the standard [swap endpoint](/api-reference/wallets/swap/tokens), this endpoint returns the signed transaction directly rather than creating an asynchronous wallet action. To learn more about the design of this endpoint, see [our blog post on reducing trading latency](https://privy.io/blog/reducing-trading-latency-on-privy). ### Prerequisites * The wallet must be a **Solana** embedded wallet * Your app must be approved for access to this Labs endpoint *** ### Considerations This endpoint behaves similarly to [`optimistic_broadcast`](/api-reference/wallets/solana/sign-and-send-transaction) on the standard Solana sign-and-send endpoint: * The response is returned as soon as the transaction is signed and submitted, it does not wait for onchain confirmation. * The `submission_status` field indicates whether the network acknowledged the transaction, not whether it confirmed onchain. Monitor the returned `transaction_hash` via Solana RPC for final status. * The `signed_transaction` field contains the fully signed transaction (base64-encoded), which can be rebroadcast to any Solana RPC endpoint for redundancy or improved landing rates. * Your app is responsible for checking transaction validity and rebroadcasting if needed. # Privy labs APIs Source: https://docs.privy.io/api-reference/labs/overview Learn what Privy Labs APIs are and what expectations apply to documented experimental endpoints. Privy Labs APIs are experimental APIs for new use cases that Privy believes are interesting or are showing emerging value across customer integrations. Privy uses Labs to collaborate closely with developers while these areas evolve quickly. This makes it possible to explore new patterns with real feedback before an API graduates into the core Privy API. The expectations on this page apply to Labs APIs that are documented in this section of the API reference. ## What to expect 1. **Labs APIs are experiments.** A Labs API may be deprecated as Privy learns what works. Privy provides at least **three months of notice** before removing a documented Labs API. 2. **Documented Labs APIs are supported.** If your team has a question about a documented Labs API, Privy provides support and follow-up. 3. **Successful Labs APIs graduate to the main Privy API without a re-integration.** When a Labs API graduates, the existing Labs paths continue to work, so your app does not need to move to a new URL. # Create organization Source: https://docs.privy.io/api-reference/organizations/create post /v1/organizations Create an organization in an app. # Delete organization Source: https://docs.privy.io/api-reference/organizations/delete delete /v1/organizations/{organization_id} Delete an organization by ID. # Get organization Source: https://docs.privy.io/api-reference/organizations/get get /v1/organizations/{organization_id} Get an organization by ID. # List organizations Source: https://docs.privy.io/api-reference/organizations/get-all get /v1/organizations List organizations in an app. # Update organization Source: https://docs.privy.io/api-reference/organizations/update patch /v1/organizations/{organization_id} Update an organization by ID. # Create policy Source: https://docs.privy.io/api-reference/policies/create post /v1/policies Create a new policy. ### SDK methods Learn more about creating policies using our SDKs [here](/controls/policies/create-a-policy). # Delete policy Source: https://docs.privy.io/api-reference/policies/delete delete /v1/policies/{policy_id} Delete a policy by policy ID. # Get policy Source: https://docs.privy.io/api-reference/policies/get get /v1/policies/{policy_id} Get a policy by policy ID. ### SDK methods Learn more about getting policies using our SDKs [here](/controls/policies/get-a-policy). # Add a rule to a policy Source: https://docs.privy.io/api-reference/policies/rules/create post /v1/policies/{policy_id}/rules Create a new rule for a policy. # Delete a rule from a policy Source: https://docs.privy.io/api-reference/policies/rules/delete delete /v1/policies/{policy_id}/rules/{rule_id} Delete a rule by policy ID and rule ID. # Get a rule from a policy Source: https://docs.privy.io/api-reference/policies/rules/get get /v1/policies/{policy_id}/rules/{rule_id} Get a rule by policy ID and rule ID. # Update a rule in a policy Source: https://docs.privy.io/api-reference/policies/rules/update patch /v1/policies/{policy_id}/rules/{rule_id} Update a rule by policy ID and rule ID. # Update policy Source: https://docs.privy.io/api-reference/policies/update patch /v1/policies/{policy_id} Update a policy by policy ID. ### SDK methods Learn more about updating policies using our SDKs [here](/controls/policies/update-a-policy). # Request expiry Source: https://docs.privy.io/api-reference/request-expiry Prevent replay attacks and delayed execution with request expiry timestamps The `privy-request-expiry` header allows your app to set a deadline for when an API request must be processed. Privy rejects requests where the expiry timestamp has passed, helping prevent replay attacks and the delayed presentation of previously signed requests. ## Required headers When using request expiry with the REST API, include the following header with your request: A Unix timestamp in milliseconds representing the deadline by which the request must be processed (e.g., `1773679531000`). ## When is it necessary? The `privy-request-expiry` header is optional for all endpoints where authorization signatures are accepted, but strongly recommended for: * Requests that include [authorization signatures](/api-reference/authorization-signatures), to limit the window in which a signed request can be used * State-changing operations where delayed execution could be problematic * Security-sensitive operations where replay attacks are a concern ## How request expiry works When making a request, include the `privy-request-expiry` header with a Unix timestamp in milliseconds representing the deadline for the request. If the request requires an [authorization signature](/api-reference/authorization-signatures), the `privy-request-expiry` header must be included in the [signature payload](/controls/authorization-keys/using-owners/sign/overview#signature-payload) under the `headers` field. The value signed must match the header value sent with the request. When Privy receives the request, it checks the `privy-request-expiry` value against the current server time. If the expiry timestamp is in the past, the request is rejected with a [`request_expired`](/basics/troubleshooting/error-handling/api-errors#request-expired) error. The expiry value must be a Unix timestamp in **milliseconds**, not seconds. Using seconds will result in a timestamp that appears to be far in the past, and the request will be rejected. ## Request expiry in Privy SDKs If you are using any of Privy's SDKs below, a default expiry of 15 minutes is set if you don't specify one. Intents endpoints default to a 72-hour expiry instead. If you'd like to override the default value, you can configure the default expiry globally when constructing the client, or override it on a per-request basis: ```typescript theme={"system"} import {PrivyClient} from '@privy-io/node'; // Configure custom default expiries globally (in milliseconds) const privy = new PrivyClient({ appId: 'your-app-id', appSecret: 'your-app-secret', requestExpiry: { defaultMs: 10 * 60 * 1000, // 10 minutes for standard calls defaultIntentMs: 24 * 60 * 60 * 1000 // 24 hours for intents endpoints } }); const walletId = 'your-wallet-id'; // Uses the configured default expiry (10 minutes) const responseWithDefaultExpiry = await privy.wallets().ethereum().signMessage(walletId, { message: 'Hello, world!' }); // Override the expiry for a specific request const responseWith5MinExpiry = await privy .wallets() .ethereum() .signMessage(walletId, { message: 'Hello, world!', request_expiry: privy.getRequestExpiry(5 * 60 * 1000) // 5 minutes }); ``` ## Including in authorization signatures When a request includes both a `privy-request-expiry` header and an authorization signature, the expiry must be included in the signature payload. This ensures that the expiry cannot be tampered with after signing. ```json theme={"system"} { "version": 1, "method": "POST", "url": "https://auth.privy.io/api/v1/wallets//rpc", "body": { "method": "personal_sign", "params": { "message": "Hello, world!" } }, "headers": { "privy-app-id": "insert-your-app-id", "privy-request-expiry": "1773679531000" } } ``` See [authorization signatures](/api-reference/authorization-signatures) for the full signature payload specification. ## Error handling If a request is received after its expiry timestamp, Privy returns a `request_expired` error. See the [API error codes](/basics/troubleshooting/error-handling/api-errors#request-expired) page for details and troubleshooting steps. # Get transaction by external ID Source: https://docs.privy.io/api-reference/transactions/external-id get /v1/transactions List transactions by reference ID. Use this endpoint to look up transactions by a developer-provided `reference_id`. This is useful for reconciling transactions initiated via [`eth_sendTransaction`](/api-reference/wallets/ethereum/eth-send-transaction) or [`signAndSendTransaction`](/api-reference/wallets/solana/sign-and-send-transaction) with your internal records. To set a `reference_id` on a transaction, pass it when calling [`eth_sendTransaction`](/api-reference/wallets/ethereum/eth-send-transaction) (EVM) or [`signAndSendTransaction`](/api-reference/wallets/solana/sign-and-send-transaction) (Solana). The `reference_id` must be unique per transaction and can be up to 64 characters. # Get gas spend Source: https://docs.privy.io/api-reference/transactions/gas-spend get /v1/apps/gas_spend Get aggregated Privy gas credits charged for a set of wallets over a time range. Maximum 100 wallet IDs and 30-day range per request. Learn more about querying gas spend [here](/wallets/gas-and-asset-management/gas/gas-spend). # Get transaction Source: https://docs.privy.io/api-reference/transactions/get get /v1/transactions/{transaction_id} Get a transaction by transaction ID. ### SDK methods Learn more about fetching transactions using our SDKs [here](/wallets/gas-and-asset-management/assets/fetch-a-transaction). In August 2025 we migrated transactions to a new data store. As part of this migration, we changed the format of transaction IDs from CUID2 to UUIDv4. You may continue using the CUID2 for your existing transactions, but we encourage migration to the new UUID, as it will avoid a very slight latency increase due to an extra lookup for mapping from the legacy ID to the new ID. # Create user Source: https://docs.privy.io/api-reference/users/create post /v1/users Create a new user with linked accounts. Optionally pre-generate embedded wallets for the user. ### SDK methods Learn more about creating users using our SDKs [here](/user-management/migrating-users-to-privy/create-or-import-a-user). # Add custom metadata Source: https://docs.privy.io/api-reference/users/custom-metadata/create post /v1/users/{user_id}/custom_metadata Adds custom metadata to a user by user ID. ### SDK methods Learn more about custom metadata using our SDKs [here](/user-management/users/custom-metadata). # Update custom metadata Source: https://docs.privy.io/api-reference/users/custom-metadata/update patch /v1/users/{user_id}/custom_metadata Partially updates custom metadata for a user by user ID. Only top-level keys provided in the request are updated; unspecified keys are preserved. ### SDK methods Learn more about custom metadata using our SDKs [here](/user-management/users/custom-metadata). # Delete user Source: https://docs.privy.io/api-reference/users/delete delete /v1/users/{user_id} Delete a user by user ID. ### SDK methods Learn more about deleting users using our SDKs [here](/user-management/users/managing-users/deleting-users). # Get user by ID Source: https://docs.privy.io/api-reference/users/get get /v1/users/{user_id} Get a user by user ID. ### SDK methods Learn more about querying users using our SDKs [here](/user-management/users/managing-users/querying-users). This endpoint is heavily rate limited. If you're looking to get information about an authenticated user, consider using [identity tokens](/user-management/users/identity-tokens) as a more efficient way to access user data. # Get users Source: https://docs.privy.io/api-reference/users/get-all get /v1/users Get all users in your app. ### SDK methods Learn more about querying users using our SDKs [here](/user-management/users/managing-users/querying-users). # Get user by custom auth ID Source: https://docs.privy.io/api-reference/users/get-by-custom-auth post /v1/users/custom_auth/id Looks up a user by their custom auth ID. # Get user by Discord username Source: https://docs.privy.io/api-reference/users/get-by-discord-username post /v1/users/discord/username Looks up a user by their Discord username. # Get user by email address Source: https://docs.privy.io/api-reference/users/get-by-email-address post /v1/users/email/address Looks up a user by their email address. # Get user by Farcaster ID Source: https://docs.privy.io/api-reference/users/get-by-farcaster-id post /v1/users/farcaster/fid Looks up a user by their Farcaster ID. # Get user by GitHub username Source: https://docs.privy.io/api-reference/users/get-by-github-username post /v1/users/github/username Looks up a user by their Github username. # Get user by Instagram username Source: https://docs.privy.io/api-reference/users/get-by-instagram-username post /v1/users/instagram/username Looks up a user by their Instagram username. # Get user by phone number Source: https://docs.privy.io/api-reference/users/get-by-phone-number post /v1/users/phone/number Looks up a user by their phone number. # Get user by smart wallet address Source: https://docs.privy.io/api-reference/users/get-by-smart-wallet-address post /v1/users/smart_wallet/address Looks up a user by their smart wallet address. # Get user by Spotify subject Source: https://docs.privy.io/api-reference/users/get-by-spotify-subject post /v1/users/spotify/subject Looks up a user by their Spotify subject (user ID). # Get user by Telegram user ID Source: https://docs.privy.io/api-reference/users/get-by-telegram-user-id post /v1/users/telegram/telegram_user_id Looks up a user by their Telegram user ID. # Get user by Telegram username Source: https://docs.privy.io/api-reference/users/get-by-telegram-username post /v1/users/telegram/username Looks up a user by their Telegram username. # Get user by Twitch username Source: https://docs.privy.io/api-reference/users/get-by-twitch-username post /v1/users/twitch/username Looks up a user by their Twitch username. # Get user by Twitter subject Source: https://docs.privy.io/api-reference/users/get-by-twitter-subject post /v1/users/twitter/subject Looks up a user by their Twitter subject. # Get user by Twitter username Source: https://docs.privy.io/api-reference/users/get-by-twitter-username post /v1/users/twitter/username Looks up a user by their Twitter username. # Get user by wallet address Source: https://docs.privy.io/api-reference/users/get-by-wallet-address post /v1/users/wallet/address Looks up a user by their wallet address. # Pregenerate wallets Source: https://docs.privy.io/api-reference/users/pregenerate-wallets post /v1/users/{user_id}/wallets Creates an embedded wallet for an existing user. ### SDK methods Learn more about pregenerating wallets using our SDKs [here](/recipes/pregenerate-wallets). # Refresh Twitter account Source: https://docs.privy.io/api-reference/users/refresh-twitter-account post /v1/users/twitter/refresh Refresh the cached profile data for a user's linked Twitter account, including their username, display name, and profile picture. ```sh theme={"system"} curl --request POST \ --url https://api.privy.io/v1/users/twitter/refresh \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "subject": "1234567890987654321" }' ``` ```json 200 theme={"system"} { "type": "twitter_oauth", "subject": "1234567890987654321", "username": "new_handle", "name": "Updated Name", "profile_picture_url": "https://pbs.twimg.com/profile_images/.../avatar.jpg", "verified_at": 1755000000, "first_verified_at": 1755000000, "latest_verified_at": 1755000000 } ``` ```json 404 theme={"system"} { "error": "No Twitter account found with the provided subject for this app" } ``` ```json 429 theme={"system"} { "error": "Twitter account refreshes are limited to once per day", "code": "too_many_requests" } ``` Privy refreshes a user's Twitter profile data whenever they log in or re-authorize their Twitter account. Use this endpoint to refresh that data for users with an existing session, who will not re-authenticate soon. Privy only overwrites a field when Twitter returns a value for it. If Twitter omits `username`, `name`, or `profile_picture_url`, the existing value on the linked account is preserved rather than set to `null`. ### Body The Twitter user ID of the account to refresh, as stored on the `subject` field of the user's `twitter_oauth` linked account. This is Twitter's stable numeric identifier for the account, not the account's username. ### Returns The refreshed `twitter_oauth` linked account. This endpoint does not return the full user object. Available options: `twitter_oauth` The Twitter user ID of the account. This value is stable and does not change when the user changes their username. The user's Twitter username. The user's display name on Twitter. A URL for the user's Twitter profile picture. Unix timestamp, in seconds, of when the user linked their Twitter account to their Privy account. Unix timestamp, in seconds, of when the user first linked their Twitter account. Unix timestamp, in seconds, of when the user most recently linked their Twitter account. ### Errors Two distinct conditions share each of the `404` and `429` statuses. Your app should branch on the `error` message, not the status alone. None of these responses include a `Retry-After` header. | Status | Error | Description | | ------ | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | `404` | `No Twitter account found with the provided subject for this app` | No user in the app has a `twitter_oauth` account with the given `subject`. | | `404` | `Twitter user no longer exists or has been suspended` | Twitter no longer serves a profile for the account. Your app should stop retrying this `subject`. | | `429` | `Twitter account refreshes are limited to once per day` | Privy's own limit. The linked account was refreshed or otherwise modified within the last 24 hours. Retry after 24 hours. | | `429` | `Twitter API rate limit exceeded, try again later` | Twitter rate limited Privy's request. The linked account was not modified, so your app can retry shortly. | | `500` | `Unable to fetch profile from Twitter API` | Privy could not reach Twitter, or Twitter returned an unexpected response. | | `500` | `Twitter API authentication failed` | Privy's Twitter credentials were rejected. Retrying will not help; contact Privy support. | Both `429` responses include the error code `too_many_requests`. The `404` and `500` responses do not include an error code. # Search users Source: https://docs.privy.io/api-reference/users/search post /v1/users/search Search users by search term, emails, phone numbers, or wallet addresses. ### SDK methods Learn more about querying users using our SDKs [here](/user-management/users/managing-users/querying-users). # Get wallet action by external ID Source: https://docs.privy.io/api-reference/wallets/actions/external-id get /v1/actions Look up a wallet action by `reference_id` across every wallet in the app. Returns an empty list if no action matches. Use `?include=steps` to include step-level details. Use this endpoint to look up a wallet action by a developer-provided `reference_id`. This is useful for reconciling actions initiated via [transfer](/api-reference/wallets/transfer/index), [swap](/api-reference/wallets/swap/quote) or [earn](/api-reference/wallets/earn/deposit) with your internal records. Unlike [get wallet action](/api-reference/wallets/actions/get) and [list all wallet actions](/api-reference/wallets/actions/list), this endpoint searches across every wallet in your app, so you do not need to know which wallet performed the action. To set a `reference_id` on a wallet action, pass it in the request body when creating the action. The `reference_id` must be unique per app and can be up to 64 characters. See [Set a reference ID](/wallets/actions/reference-id) for examples. If no action matches the given `reference_id`, the endpoint returns `200` with an empty list rather than a `404`. Pass `?include=steps` to expand step-level details in the response. # Get wallet action Source: https://docs.privy.io/api-reference/wallets/actions/get get /v1/wallets/{wallet_id}/actions/{action_id} Get the current status of a wallet action by its ID. # List all wallet actions Source: https://docs.privy.io/api-reference/wallets/actions/list get /v1/wallets/{wallet_id}/actions List all wallet actions for a wallet. # Archive wallet Source: https://docs.privy.io/api-reference/wallets/archive post /v1/wallets/{wallet_id}/archive Archives a wallet, preventing it from being used in any write or signing operations. Archived wallets are hidden from list endpoints by default. Returns 404 if the wallet does not exist or is already archived. # Authenticate Source: https://docs.privy.io/api-reference/wallets/authenticate post /v1/wallets/authenticate Exchange a user JWT for a session key authorized to act on the user's wallets. Returns the encrypted authorization key and the list of wallets it can access. Directly managing user authorization keys via the API is an advanced setting. We recommend using Privy's SDKs, which internally manage user authorization keys if applicable. This endpoint is used to create an ephemeral signing key for signing requests to [take actions](/api-reference/wallets/ethereum/eth-send-transaction) with a user's wallet. The returned key is encrypted using Hybrid Public Key Encryption (HPKE), with the following configuration: The response `authorization_key` is ciphertext and must be decrypted. # Create wallets in batch Source: https://docs.privy.io/api-reference/wallets/batch-create post /v1/wallets/batch Creates multiple wallets in a single request. ### Batch behavior This endpoint creates multiple wallets in a single request. Each wallet creation is processed independently, so a failure for one wallet does not affect the others. If the request body is valid, the endpoint returns HTTP 200 with a `results` array containing the success or failure status for each wallet. Request-level errors (invalid body, authentication failure, rate limiting) return standard HTTP error codes before any wallets are processed. # Create wallet Source: https://docs.privy.io/api-reference/wallets/create post /v1/wallets Creates a new wallet on the requested chain and for the requested owner. Prior to creating a wallet, we strongly recommend you learn about wallet [controls](/controls/overview) and [policies](/controls/policies/overview) to understand the right configuration for your wallets. ### SDK methods Learn more about creating wallets using our SDKs [here](/wallets/wallets/create/create-a-wallet). **Idempotency on errors:** On 4xx or 5xx, Privy caches the response and replays it for the same key. Generate a new key to retry after a server error. Policy violations are an exception and allow same-key retries. # Create custodial wallets Source: https://docs.privy.io/api-reference/wallets/custodial-wallets post /v1/custodial_wallets Create a new wallet custodied by a third-party provider. # Deposit into vault Source: https://docs.privy.io/api-reference/wallets/earn/deposit post /v1/wallets/{wallet_id}/earn/ethereum/deposit Deposit assets into an ERC-4626 yield vault. If your app has gas sponsorship configured, usage of the `/earn/ethereum/deposit` endpoint will be [gas-sponsored by default](/wallets/actions/overview#gas-management). There is no need to specify additional parameters for sponsorship. **Idempotency on errors:** This endpoint returns `pending` immediately. Replaying the same key returns the cached response. On a synchronous 5xx, Privy deletes the record so the same key retries fresh. Action outcomes (`rejected`, `failed`, `succeeded`) arrive via [webhooks](/wallets/actions/webhooks) and [polling](/wallets/actions/status). Policy violations always allow fresh retries. # Earn fee collect Source: https://docs.privy.io/api-reference/wallets/earn/fees-collect post /v1/wallets/{wallet_id}/earn/ethereum/fees/collect Collect accumulated performance fees from an Aave vault. If your app has gas sponsorship configured, usage of the `/earn/ethereum/fees/collect` endpoint will be [gas-sponsored by default](/wallets/actions/overview#gas-management). There is no need to specify additional parameters for sponsorship. # Get incentive rewards Source: https://docs.privy.io/api-reference/wallets/earn/get-incentive-rewards get /v1/wallets/{wallet_id}/earn/ethereum/incentive/claim Retrieve all incentive rewards for a wallet on a given chain, with claimed and claimable amounts per token. # Get Ethereum vault position Source: https://docs.privy.io/api-reference/wallets/earn/get-position get /v1/wallets/{wallet_id}/earn/ethereum/vaults Retrieve a wallet's current position in a specific Ethereum vault. Returns the vault shares and asset values. # Get Ethereum vault details Source: https://docs.privy.io/api-reference/wallets/earn/get-vault-details get /v1/earn/ethereum/vaults/{vault_id} Retrieve detailed information about an Ethereum vault, including current APY and liquidity. # Claim reward incentives Source: https://docs.privy.io/api-reference/wallets/earn/incentive-claim post /v1/wallets/{wallet_id}/earn/ethereum/incentive/claim Claim reward incentives for a wallet on a given chain. Ifear your app has gas sponsorship configured, usage of the `/earn/ethereum/incentive/claim` endpoint will be [gas-sponsored by default](/wallets/actions/overview#gas-management). There is no need to specify additional parameters for sponsorship. **Idempotency on errors:** This endpoint returns `pending` immediately. Replaying the same key returns the cached response. On a synchronous 5xx, Privy deletes the record so the same key retries fresh. Action outcomes (`rejected`, `failed`, `succeeded`) arrive via [webhooks](/wallets/actions/webhooks) and [polling](/wallets/actions/status). Policy violations always allow fresh retries. # Withdraw from vault Source: https://docs.privy.io/api-reference/wallets/earn/withdraw post /v1/wallets/{wallet_id}/earn/ethereum/withdraw Withdraw assets from an ERC-4626 yield vault. If your app has gas sponsorship configured, usage of the `/earn/ethereum/withdraw` endpoint will be [gas-sponsored by default](/wallets/actions/overview#gas-management). There is no need to specify additional parameters for sponsorship. **Idempotency on errors:** This endpoint returns `pending` immediately. Replaying the same key returns the cached response. On a synchronous 5xx, Privy deletes the record so the same key retries fresh. Action outcomes (`rejected`, `failed`, `succeeded`) arrive via [webhooks](/wallets/actions/webhooks) and [polling](/wallets/actions/status). Policy violations always allow fresh retries. # Assign wallet entity Source: https://docs.privy.io/api-reference/wallets/entity post /v1/wallets/{wallet_id}/entity Assign a user or organization to a wallet. You can assign up to 150 wallets to an organization. If the organization is already at this limit, this endpoint returns a `409` response with the error code `wallet_entity_limit_exceeded`. # eth_sendTransaction Source: https://docs.privy.io/api-reference/wallets/ethereum/eth-send-transaction post /v1/wallets/{wallet_id}/rpc Sign and send a transaction using the eth_sendTransaction method. ### SDK methods Learn more about sending transactions using our SDKs [here](/wallets/using-wallets/ethereum/send-a-transaction). *** **Idempotency on errors:** On 4xx or 5xx, Privy caches the response and replays it for the same key. Generate a new key to retry after a server error. Policy violations are an exception and allow same-key retries. ```sh Without sponsorship theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "eth_sendTransaction", "caip2": "eip155:11155111", "chain_type": "ethereum", "params": { "transaction": { "to": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "value": "0x2386F26FC10000" } } }' ``` ```sh With sponsorship theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "eth_sendTransaction", "caip2": "eip155:11155111", "chain_type": "ethereum", "sponsor": true, "params": { "transaction": { "to": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "value": "0x2386F26FC10000" } } }' ``` ```json Without sponsorship theme={"system"} { "method": "eth_sendTransaction", "data": { "hash": "0xfc3a736ab2e34e13be2b0b11b39dbc0232a2e755a11aa5a9219890d3b2c6c7d8", "caip2": "eip155:11155111", "transaction_id": "y90vpg3bnkjxhw541c2zc6a9", "transaction_request": { "to": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "value": "0x2386F26FC10000", "chain_id": 11155111, "type": 2, "nonce": 5, "max_fee_per_gas": "0xF4834", "max_priority_fee_per_gas": "0xF4240", "gas_limit": "0xC350" } } } ``` ```json With sponsorship theme={"system"} { "method": "eth_sendTransaction", "data": { "hash": "", "user_operation_hash": "0x8f3a4e7d2c1b9a5e6f4d3c2b1a9e8f7d6c5b4a3e2d1c0b9a8f7e6d5c4b3a2e1d", "caip2": "eip155:11155111", "transaction_id": "y90vpg3bnkjxhw541c2zc6a9" } } ``` The wallet RPC endpoint is a synchronous endpoint, and a successful response indicates that the transaction has been broadcasted to the network. Transactions may get broadcasted but still fail to be confirmed by the network. The endpoint does not wait for confirmation or retry if the transaction fails to be confirmed. To handle these scenarios, see our guide on [speeding up transactions](/recipes/speeding-up-transactions). ### Headers ID of your Privy app. Request authorization signature. If multiple signatures are required, they should be comma separated. Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters ID of the wallet to get. ### Body Available options: `eth_sendTransaction` Available options: `0`, `1`, `2`, `4`. Applies to EIP-7702 type-4 transactions. The delegated contract address. Optional parameter to enable gas sponsorship for this transaction. [Learn more.](/wallets/gas-and-asset-management/gas/overview) Options for [user pays](/wallets/gas-and-asset-management/gas/setup#user-pays) gas sponsorship, in which the wallet covers gas with a stablecoin balance instead of the app's gas credits. Requires `sponsor: true` and an app configured for user pays mode. Not supported on Tempo. The token the wallet pays gas with, such as `usdc` or `usdt`. Must be enabled for this chain in the dashboard. See the [supported chains and tokens](/wallets/gas-and-asset-management/gas/setup#user-pays). Optional developer-provided reference ID for transaction reconciliation. Must be unique per transaction and up to 64 characters. Use this to correlate transactions with your own internal records. The `reference_id` is included in [transaction webhook](/api-reference/webhooks/transaction/broadcasted) payloads and can be used to [look up transactions](/api-reference/transactions/external-id). Available options: `ethereum` ### Returns Available options: `eth_sendTransaction` The transaction hash. For paymaster-sponsored transactions, returns an empty string until the transaction confirms on-chain. The user operation hash. Only present for standard EVM paymaster-sponsored transactions. The full transaction object that was signed and broadcast. Contains the resolved transaction fields including `to`, `value`, `chain_id`, `nonce`, `gas_limit`, and fee parameters. The developer-provided reference ID, if one was provided in the request. # eth_sign7702Authorization Source: https://docs.privy.io/api-reference/wallets/ethereum/eth-sign-7702-authorization post /v1/wallets/{wallet_id}/rpc Signs an EIP-7702 authorization struct using the wallet's private key. ### SDK methods Learn more about signing EIP-7702 authorizations using our SDKs [here](/wallets/using-wallets/ethereum/sign-7702-authorization). *** **Idempotency on errors:** On 4xx or 5xx, Privy caches the response and replays it for the same key. Generate a new key to retry after a server error. Policy violations are an exception and allow same-key retries. ```sh cURL theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "eth_sign7702Authorization", "params": { "contract": "0x1234567890abcdef1234567890abcdef12345678", "chain_id": 1, "nonce": 0 } }' ``` ```json 200 theme={"system"} { "method": "eth_sign7702Authorization", "data": { "authorization": { "contract": "0x1234567890abcdef1234567890abcdef12345678", "chain_id": 1, "nonce": 0, "r": "0x0db9c7bd881045cbba28c347de6cc32a653e15d7f6f2f1cec21d645f402a6419", "s": "0x6e877eb45d3041f8d2ab1a76f57f408b63894cfc6f339d8f584bd26efceae308", "y_parity": 1 } } } ``` ### Headers ID of your Privy app. Request authorization signature. If multiple signatures are required, they should be comma separated. Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters ID of the wallet to get. ### Body The RPC method to execute. Must be `eth_sign7702Authorization`. The parameters for signing the EIP-7702 authorization. The address of the smart contract that the EOA will delegate to. Must be a valid Ethereum address in hex format. The chain ID where this authorization will be valid. The nonce for the authorization. If not provided, we will fetch the current nonce for the wallet. ### Response The RPC method that was executed. Will be `eth_sign7702Authorization`. The response data containing the signed authorization. The signed EIP-7702 authorization object. The address of the smart contract that the EOA delegates to. The chain ID where this authorization is valid. The nonce for the authorization. The r component of the ECDSA signature. The s component of the ECDSA signature. The recovery parameter (0 or 1) for the signature. # eth_signTransaction Source: https://docs.privy.io/api-reference/wallets/ethereum/eth-sign-transaction post /v1/wallets/{wallet_id}/rpc Sign a transaction using the eth_signTransaction method. ### SDK methods Learn more about signing transactions using our SDKs [here](/wallets/using-wallets/ethereum/sign-a-transaction). *** **Idempotency on errors:** On 4xx or 5xx, Privy caches the response and replays it for the same key. Generate a new key to retry after a server error. Policy violations are an exception and allow same-key retries. ```sh cURL theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "eth_signTransaction", "params": { "transaction": { "to": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "value": "0x2386F26FC10000", "chain_id": 11155111, "data": "0x", "gas_limit": 50000, "nonce": 0, "max_fee_per_gas": 1000308, "max_priority_fee_per_gas": "1000000" } } }' ``` ```json 200 theme={"system"} { "method": "eth_signTransaction", "data": { "signed_transaction": "0x02f870830138de80830f4240830f437480940b81418147df37155d643b5cb65ba6c8cb7aba76872000000000000480c080a05c11a2166ec56189d993dec477477d962ce0d4c466ab7ed8982110621ec87a57a003c796590c0c62eac30acd412f2aa0e8ad740c4ded86fb64d3326ee4c0ea804c", "encoding": "rlp" } } ``` ### Headers ID of your Privy app. Request authorization signature. If multiple signatures are required, they should be comma separated. Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters ID of the wallet to get. ### Body Available options: `eth_signTransaction` Available options: `0`, `1`, `2`, `4`. Applies to EIP-7702 type-4 transactions. The delegated contract address. ### Response Available options: `eth_signTransaction` Available options: `rlp` # eth_signUserOperation Source: https://docs.privy.io/api-reference/wallets/ethereum/eth-sign-user-operation post /v1/wallets/{wallet_id}/rpc Sign a user operation using the eth_signUserOperation method. This method is currently only supported for these smart contract addresses: `0x69007702764179f14F51cdce752f4f775d74E139`, `0x00000000000002377B26b1EdA7b0BC371C60DD4f`, `0xd6CEDDe84be40893d153Be9d467CD6aD37875b28`, and `0x63c0c19a282a1B52b07dD5a65b58948A07DAE32B`. *** **Idempotency on errors:** On 4xx or 5xx, Privy caches the response and replays it for the same key. Generate a new key to retry after a server error. Policy violations are an exception and allow same-key retries. ```sh cURL theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "eth_signUserOperation", "params": { "contract": "0x69007702764179f14F51cdce752f4f775d74E139", "user_operation": { "sender": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "nonce": "0x0", "call_data": "0x", "call_gas_limit": "0x30d40", "verification_gas_limit": "0x30d40", "pre_verification_gas": "0x5208", "max_fee_per_gas": "0xf4240", "max_priority_fee_per_gas": "0xf4240", "paymaster": "0x0000000000000000000000000000000000000000", "paymaster_data": "0x", "paymaster_verification_gas_limit": "0x0", "paymaster_post_op_gas_limit": "0x0" }, "chain_id": "11155111" } }' ``` ```json 200 theme={"system"} { "method": "eth_signUserOperation", "data": { "signature": "0x1754782aea15e96189c3a85a7b7ac2f6339f6f4f3b29b1d3200a4c9907ef53e4776a84387583896b0a074cbc6de1a1c2a1eb53aba199da6ada8c99b0266171c41b", "encoding": "hex" } } ``` ### Headers ID of your Privy app. Request authorization signature. If multiple signatures are required, they should be comma separated. Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters ID of the wallet to get. ### Body Available options: `eth_signUserOperation` The smart contract address for the user operation. Currently supports `0x69007702764179f14F51cdce752f4f775d74E139`, `0x00000000000002377B26b1EdA7b0BC371C60DD4f`, `0xd6CEDDe84be40893d153Be9d467CD6aD37875b28`, and `0x63c0c19a282a1B52b07dD5a65b58948A07DAE32B`. The account making the operation. Anti-replay parameter; also used as the salt for first-time account creation. The account factory address, if deploying a new account. Additional data for the account factory. The data to pass to the sender during the main execution call. The amount of gas to allocate the main execution call. The amount of gas to allocate for the verification step. Extra gas to pay the bundler. Maximum fee per gas (similar to EIP-1559 max\_fee\_per\_gas). Maximum priority fee per gas (similar to EIP-1559 max\_priority\_fee\_per\_gas). Address of paymaster sponsoring the transaction, zero for self-sponsored. Extra data to send to the paymaster. The amount of gas to allocate for the paymaster validation code. The amount of gas to allocate for the paymaster post-operation code. The chain ID for the user operation. ### Response Available options: `eth_signUserOperation` The hex-encoded signature of the user operation. Available options: `hex` # eth_signTypedData_v4 Source: https://docs.privy.io/api-reference/wallets/ethereum/eth-signtypeddata-v4 post /v1/wallets/{wallet_id}/rpc Sign a message using the eth_signTypedData_v4 method. ### SDK methods Learn more about signing typed data using our SDKs [here](/wallets/using-wallets/ethereum/sign-typed-data). *** **Idempotency on errors:** On 4xx or 5xx, Privy caches the response and replays it for the same key. Generate a new key to retry after a server error. Policy violations are an exception and allow same-key retries. ```sh cURL theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "eth_signTypedData_v4", "params": { "typed_data": { "types": { "EIP712Domain": [ { "name": "name", "type": "string" }, { "name": "version", "type": "string" }, { "name": "chainId", "type": "uint160" }, { "name": "verifyingContract", "type": "address" } ], "Person": [ { "name": "name", "type": "string" }, { "name": "wallet", "type": "address" } ], "Mail": [ { "name": "from", "type": "Person" }, { "name": "to", "type": "Person" }, { "name": "contents", "type": "string" } ] }, "message": { "from": { "name": "Alice", "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" }, "to": { "name": "Bob", "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" }, "contents": "Hello, Bob!" }, "primary_type": "Mail", "domain": { "name": "DApp Mail", "version": "1", "chainId": "0x3e8", "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" } } } }' ``` ```json 200 theme={"system"} { "method": "eth_signTypedData_v4", "data": { "signature": "0x1754782aea15e96189c3a85a7b7ac2f6339f6f4f3b29b1d3200a4c9907ef53e4776a84387583896b0a074cbc6de1a1c2a1eb53aba199da6ada8c99b0266171c41b", "encoding": "hex" } } ``` ### Headers ID of your Privy app. Request authorization signature. If multiple signatures are required, they should be comma separated. Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters ID of the wallet to get. ### Body Available options: `eth_signTypedData_v4` A CAIP-2 chain ID specifying which chain to sign on (e.g. `eip155:1`). Required when using `signature_options`. Options controlling signature production. Required for ERC-1271 signing with EIP-7702 delegated (gas-sponsored) wallets. Learn more in the [ERC-1271 signatures guide](/recipes/evm/erc-1271-signatures). The type of cryptographic signature to produce. Use `erc1271` for ERC-1271 compliant signatures for smart account wallets, or `ecdsa` for standard ECDSA signatures. Available options: `ecdsa`, `erc1271` ### Response Available options: `eth_signTypedData_v4` Available options: `utf-8`, `hex` # personal_sign Source: https://docs.privy.io/api-reference/wallets/ethereum/personal-sign post /v1/wallets/{wallet_id}/rpc Sign a message using the personal_sign method. ### SDK methods Learn more about signing messages using our SDKs [here](/wallets/using-wallets/ethereum/sign-a-message). *** **Idempotency on errors:** On 4xx or 5xx, Privy caches the response and replays it for the same key. Generate a new key to retry after a server error. Policy violations are an exception and allow same-key retries. ```sh cURL theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "personal_sign", "params": { "message": "Hello from Privy!", "encoding": "utf-8" } }' ``` ```json 200 theme={"system"} { "method": "personal_sign", "data": { "signature": "0x0db9c7bd881045cbba28c347de6cc32a653e15d7f6f2f1cec21d645f402a64196e877eb45d3041f8d2ab1a76f57f408b63894cfc6f339d8f584bd26efceae3081c", "encoding": "hex" } } ``` ### Headers ID of your Privy app. Request authorization signature. If multiple signatures are required, they should be comma separated. Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters ID of the wallet to get. ### Body Available options: `personal_sign` Available options: `utf-8`, `hex` A CAIP-2 chain ID specifying which chain to sign on (e.g. `eip155:1`). Required when using `signature_options`. Options controlling signature production. Required for ERC-1271 signing with EIP-7702 delegated (gas-sponsored) wallets. Learn more in the [ERC-1271 signatures guide](/recipes/evm/erc-1271-signatures). The type of cryptographic signature to produce. Use `erc1271` for ERC-1271 compliant signatures for smart account wallets, or `ecdsa` for standard ECDSA signatures. Available options: `ecdsa`, `erc1271` ### Response Available options: `personal_sign` Available options: `utf-8`, `hex` # secp256k1_sign Source: https://docs.privy.io/api-reference/wallets/ethereum/secp256k1-sign post /v1/wallets/{wallet_id}/rpc Sign a hash using the secp256k1 method. ### SDK methods Learn more about signing raw hashes using our SDKs [here](/wallets/using-wallets/ethereum/sign-a-raw-hash). *** **Idempotency on errors:** On 4xx or 5xx, Privy caches the response and replays it for the same key. Generate a new key to retry after a server error. Policy violations are an exception and allow same-key retries. ```sh cURL theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "secp256k1_sign", "params": { "hash": "0x12345678", } }' ``` ```json 200 theme={"system"} { "method": "secp256k1_sign", "data": { "signature": "0x0db9c7bd881045cbba28c347de6cc32a653e15d7f6f2f1cec21d645f402a64196e877eb45d3041f8d2ab1a76f57f408b63894cfc6f339d8f584bd26efceae3081c", "encoding": "hex" } } ``` ### Headers ID of your Privy app. Request authorization signature. If multiple signatures are required, they should be comma separated. Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters ID of the wallet to get. ### Body Available options: `secp256k1_sign` ### Response Available options: `secp256k1_sign` Available options: `hex` # wallet_sendCalls Source: https://docs.privy.io/api-reference/wallets/ethereum/wallet-send-calls post /v1/wallets/{wallet_id}/rpc Send a batch of calls using the wallet_sendCalls method. ### SDK methods Learn more about sending transactions using our SDKs [here](/wallets/using-wallets/ethereum/send-a-transaction). *** **Idempotency on errors:** On 4xx or 5xx, Privy caches the response and replays it for the same key. Generate a new key to retry after a server error. Policy violations are an exception and allow same-key retries. ```sh Without sponsorship theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "wallet_sendCalls", "caip2": "eip155:84532", "chain_type": "ethereum", "params": { "calls": [ { "to": "0xB8644175b78da1971C7278Ead8ff70Ce65E0981b", "value": "0x00000001" }, { "to": "0xB8644175b78da1971C7278Ead8ff70Ce65E0981b", "value": "0x00000002" } ] } }' ``` ```sh With sponsorship theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "wallet_sendCalls", "caip2": "eip155:84532", "chain_type": "ethereum", "sponsor": true, "params": { "calls": [ { "to": "0xB8644175b78da1971C7278Ead8ff70Ce65E0981b", "value": "0x00000001" }, { "to": "0xB8644175b78da1971C7278Ead8ff70Ce65E0981b", "value": "0x00000002" } ] } }' ``` ```json Response theme={"system"} { "method": "wallet_sendCalls", "data": { "transaction_id": "b4966a89-8983-4b1b-a93a-b104799527f5", "caip2": "eip155:84532" } } ``` The wallet RPC endpoint is a synchronous endpoint, and a successful response indicates that the transaction has been broadcasted to the network. Transactions may get broadcasted but still fail to be confirmed by the network. The endpoint does not wait for confirmation or retry if the transaction fails to be confirmed. To handle these scenarios, see our guide on [speeding up transactions](/recipes/speeding-up-transactions). ### Headers ID of your Privy app. Request authorization signature. If multiple signatures are required, they should be comma separated. Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters ID of the wallet to get. ### Body Available options: `wallet_sendCalls` An array of call objects to execute in a batch. The recipient address for the call. The value to send in the call in wei as a hexadecimal string. The encoded calldata for the call. Optional parameter to enable gas sponsorship for this transaction. [Learn more.](/wallets/gas-and-asset-management/gas/overview) Options for [user pays](/wallets/gas-and-asset-management/gas/setup#user-pays) gas sponsorship, in which the wallet covers gas with a stablecoin balance instead of the app's gas credits. Requires `sponsor: true` and an app configured for user pays mode. Not supported on Tempo. The token the wallet pays gas with, such as `usdc` or `usdt`. Must be enabled for this chain in the dashboard. See the [supported chains and tokens](/wallets/gas-and-asset-management/gas/setup#user-pays). Available options: `ethereum` ### Returns Available options: `wallet_sendCalls` A unique identifier for the batch transaction. # Export wallet Source: https://docs.privy.io/api-reference/wallets/export post /v1/wallets/{wallet_id}/export Export a wallet's private key. ### SDK methods Learn more about exporting wallets using our SDKs [here](/wallets/wallets/export). *** This endpoint exports a wallet's private key using Hybrid Public Key Encryption (HPKE). The following HPKE configuration is supported: * KEM (Key Encapsulation Mechanism): DHKEM\_P256\_HKDF\_SHA256 * KDF (Key Derivation Function): HKDF\_SHA256 * AEAD (Authenticated Encryption with Associated Data): CHACHA20\_POLY1305 * Mode: BASE # Get wallet Source: https://docs.privy.io/api-reference/wallets/get get /v1/wallets/{wallet_id} Get a wallet by wallet ID. ### SDK methods Learn more about getting wallets using our SDKs [here](/wallets/wallets/get-a-wallet/get-wallet-by-id). # Get wallets Source: https://docs.privy.io/api-reference/wallets/get-all get /v1/wallets Get all wallets in your app. ### SDK methods Learn more about getting wallets using our SDKs [here](/wallets/wallets/get-a-wallet/get-all-wallets). # Get balance Source: https://docs.privy.io/api-reference/wallets/get-balance get /v1/wallets/{wallet_id}/balance Get the balance of a wallet by wallet ID. ### SDK methods Learn more about fetching wallet balances using our SDKs [here](/wallets/gas-and-asset-management/assets/fetch-balance). # Get wallet by address Source: https://docs.privy.io/api-reference/wallets/get-by-address post /v1/wallets/address Look up a wallet by its blockchain address. Returns the wallet object if found. # Get transactions Source: https://docs.privy.io/api-reference/wallets/get-transactions get /v1/wallets/{wallet_id}/transactions Get incoming and outgoing transactions of a wallet by wallet ID. # Initialize import Source: https://docs.privy.io/api-reference/wallets/import/init post /v1/wallets/import/init Initialize a wallet import. Complete by submitting the import. ### SDK methods Learn more about importing wallets using our SDKs [here](/wallets/wallets/import-a-wallet/private-key). **Idempotency on errors:** On 4xx or 5xx, Privy caches the response and replays it for the same key. Generate a new key to retry after a server error. Policy violations are an exception and allow same-key retries. # Submit import Source: https://docs.privy.io/api-reference/wallets/import/submit post /v1/wallets/import/submit Submit a wallet import request. ### SDK methods Learn more about importing wallets using our SDKs [here](/wallets/wallets/import-a-wallet/private-key). **Idempotency on errors:** On 4xx or 5xx, Privy caches the response and replays it for the same key. Generate a new key to retry after a server error. Policy violations are an exception and allow same-key retries. # Create a payout Source: https://docs.privy.io/api-reference/wallets/payout/create post /v1/wallets/{wallet_id}/payout/fiat Create a payout from a wallet that lands as a fiat currency in a destination bank account. # Raw sign Source: https://docs.privy.io/api-reference/wallets/raw-sign post /v1/wallets/{wallet_id}/raw_sign Sign a raw hash along the blockchain's cryptographic curve using the wallet's private key. ### SDK methods Learn more about signing with other chains using our SDKs [here](/wallets/using-wallets/other-chains). To see how to integrate a chain with raw signing, see the [Tier 1 integration guide](/recipes/tier-1-wallet-integration). # signAndSendTransaction Source: https://docs.privy.io/api-reference/wallets/solana/sign-and-send-transaction post /v1/wallets/{wallet_id}/rpc Sign and send transaction with a Solana wallet using the signAndSendTransaction method. ### SDK methods Learn more about sending transactions using our SDKs [here](/wallets/using-wallets/solana/send-a-transaction). *** The wallet RPC endpoint is a synchronous endpoint, and a successful response indicates that the transaction has been broadcasted to the network. Transactions may get broadcasted but still fail to be confirmed by the network. The endpoint does not wait for confirmation or retry if the transaction fails to be confirmed. To handle these scenarios, see our guide on [speeding up transactions](/recipes/speeding-up-transactions). **Idempotency on errors:** On 4xx or 5xx, Privy caches the response and replays it for the same key. Generate a new key to retry after a server error. Policy violations are an exception and allow same-key retries. ```sh cURL theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "signAndSendTransaction", "caip2": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", "sponsor": true, "params": { "transaction": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDRpb0mdmKftapwzzqUtlcDnuWbw8vwlyiyuWyyieQFKESezu52HWNss0SAcb60ftz7DSpgTwUmfUSl1CYHJ91GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAScgJ7J0AXFr1azCEvB1Y5zpiF4eXR+yTW0UB7am+E/MBAgIAAQwCAAAAQEIPAAAAAAA=", "encoding": "base64" } } ' ``` ```json 200 theme={"system"} { "method": "signAndSendTransaction", "data": { "hash": "22VS6wqrbeaN21ku3pjEjfnrWgk1deiFBSB1kZzS8ivr2G8wYmpdnV3W7oxpjFPGkt5bhvZvK1QBzuCfUPUYYFQq", "signed_transaction": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDRpb0mdmKftapwzzqUtlcDnuWbw8vwlyiyuWyyieQFKESezu52HWNss0SAcb60ftz7DSpgTwUmfUSl1CYHJ91GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAScgJ7J0AXFr1azCEvB1Y5zpiF4eXR+yTW0UB7am+E/MBAgIAAQwCAAAAQEIPAAAAAAA=", "caip2": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", "transaction_id": "nyorsf87s9d08jimesv3n8yq" } } ``` ### Headers ID of your Privy app. Request authorization signature. If multiple signatures are required, they should be comma separated. Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters ID of the wallet to get. ### Body Available options: `signAndSendTransaction` Available options: `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` (Solana Mainnet), `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` (Solana Devnet), `solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z` (Solana Testnet) Base64 encoded serialized transaction to sign. Available options: `base64` Optional parameter to enable gas sponsorship for this transaction. [Learn more.](/wallets/gas-and-asset-management/gas/overview) Optional developer-provided reference ID for transaction reconciliation. Must be unique per transaction and up to 64 characters. Use this to correlate transactions with your own internal records. The `reference_id` is included in [transaction webhook](/api-reference/webhooks/transaction/broadcasted) payloads and can be used to [look up transactions](/api-reference/transactions/external-id). Optional. If set to true, the signed transaction will be immediately returned after the enclave signs, and submission to the network will happen asynchronously. Useful if you want to minimize E2E latency, or if you want to submit to multiple endpoints to get the best transaction landing time possible. Note that enabling this option also disables network preflight checks to minimize latency, which means an invalid transaction will not return an error in the API response and will not fire webhooks. You are responsible for rebroadcasting and validating the transaction, which can be done against a Solana RPC endpoint using the `signed_transaction` from the response. **Lowest latency configuration**: For minimum E2E latency, combine `optimistic_broadcast` with the `x-privy-skip-simulation: true` request header. This skips both simulation and broadcast confirmation, returning the signed transaction as fast as possible. Your application is then responsible for simulating (if desired) and broadcasting the transaction independently. ### Returns Available options: `signAndSendTransaction` Transaction hash of the signed and sent transaction. CAIP-2 chain ID of the network where the transaction was sent. Optional Privy-assigned transaction ID. The developer-provided reference ID, if one was provided in the request. The signed transaction payload, base64-encoded. This can be used to verify the signature or to rebroadcast the transaction via your own RPC endpoint. # signMessage Source: https://docs.privy.io/api-reference/wallets/solana/sign-message post /v1/wallets/{wallet_id}/rpc Sign a message with a Solana wallet using the signMessage method. ### SDK methods Learn more about signing messages using our SDKs [here](/wallets/using-wallets/solana/sign-a-message). **Idempotency on errors:** On 4xx or 5xx, Privy caches the response and replays it for the same key. Generate a new key to retry after a server error. Policy violations are an exception and allow same-key retries. ```sh cURL theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "signMessage", "params": { "message": "aGVsbG8sIFByaXZ5IQ=", "encoding": "base64" } } ' ``` ```json 200 theme={"system"} { "method": "signMessage", "data": { "signature": "76wpEsq9FS4QOInePQUY3b4GCXdVwLv+nNp4NnI+EPTAPVwvXCjzjUW/gD6Vuh4KaD+7p2X4MaTu6xYu0rMTAA==", "encoding": "base64" } } ``` ### Headers ID of your Privy app. Request authorization signature. If multiple signatures are required, they should be comma separated. Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters ID of the wallet to get. ### Body Available options: `signMessage` Base64 encoded message to sign. Available options: `base64` ### Response Available options: `signMessage` Available options: `base64` # signTransaction Source: https://docs.privy.io/api-reference/wallets/solana/sign-transaction post /v1/wallets/{wallet_id}/rpc Sign a transaction with a Solana wallet using the signTransaction method. ### SDK methods Learn more about signing transactions using our SDKs [here](/wallets/using-wallets/solana/sign-a-transaction). **Idempotency on errors:** On 4xx or 5xx, Privy caches the response and replays it for the same key. Generate a new key to retry after a server error. Policy violations are an exception and allow same-key retries. ```sh cURL theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "signTransaction", "params": { "transaction": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDRpb0mdmKftapwzzqUtlcDnuWbw8vwlyiyuWyyieQFKESezu52HWNss0SAcb60ftz7DSpgTwUmfUSl1CYHJ91GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAScgJ7J0AXFr1azCEvB1Y5zpiF4eXR+yTW0UB7am+E/MBAgIAAQwCAAAAQEIPAAAAAAA=", "encoding": "base64" } } ' ``` ```json 200 theme={"system"} { "method": "signTransaction", "data": { "signed_transaction": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDRpb0mdmKftapwzzqUtlcDnuWbw8vwlyiyuWyyieQFKESezu52HWNss0SAcb60ftz7DSpgTwUmfUSl1CYHJ91GAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAScgJ7J0AXFr1azCEvB1Y5zpiF4eXR", "encoding": "base64" } } ``` ### Headers ID of your Privy app. Request authorization signature. If multiple signatures are required, they should be comma separated. Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters ID of the wallet to get. ### Body Available options: `signTransaction` Base64 encoded serialized transaction to sign. Available options: `base64` ### Response Available options: `signTransaction` Available options: `base64` # claimStaticDeposit Source: https://docs.privy.io/api-reference/wallets/spark/claim-static-deposit post /v1/wallets/{wallet_id}/rpc Claims funds sent to the static BTC deposit address. **Idempotency on errors:** On 4xx or 5xx, Privy caches the response and replays it for the same key. Generate a new key to retry after a server error. Policy violations are an exception and allow same-key retries. ```sh theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "claimStaticDeposit", "network": "MAINNET", "params": { "credit_amount_sats": 9901, "signature": "3045022100c4b5c728cdaf2ca0d9ffa2a5689eefa51d286bf32fbfb678071735a44e83132f0220173e59115579e705689dd0d3e819b87255e86ed4629637c85f761c0960869c7a", "transaction_id": "cff576f1ebebda2ddf812f06f656a6668f08f13d56290b4468327607f4d68acb", "output_index": 0 } }' ``` ```json 200 theme={"system"} { "method": "claimStaticDeposit", "data": { "transfer_id": "22a62e0b-6eae-4d84-8f15-2a69a6440b74" } } ``` ### Headers ID of your Privy app. Request authorization signature. If multiple signatures are required, they should be comma separated. Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters ID of the wallet to get. These wallet methods are modeled after the [Spark Wallet SDK](https://github.com/buildonspark/spark/tree/main/sdks/js/packages/spark-sdk). For more information about this wallet method, check out the [Spark Wallet documentation](https://docs.spark.money/wallet/introduction). ### Body Available options: `claimStaticDeposit` Available options: `MAINNET`, `REGTEST` Required parameters to claim the deposit. Amount of native tokens (in satoshis) being claimed from the UTXO. A signature authorizing the claim, signed by the address that received the deposit. The transaction ID (hash) of the deposit transaction containing the UTXO. The index of the UTXO output in the transaction. ### Returns Always `"claimStaticDeposit"` Information about the claimed deposit. A unique identifier representing the successful internal transfer of claimed funds to the wallet. # createLightningInvoice Source: https://docs.privy.io/api-reference/wallets/spark/create-lightning-invoice post /v1/wallets/{wallet_id}/rpc Creates a Lightning invoice for the given wallet, allowing funds to be received via the Lightning Network. **Idempotency on errors:** On 4xx or 5xx, Privy caches the response and replays it for the same key. Generate a new key to retry after a server error. Policy violations are an exception and allow same-key retries. ```sh theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "createLightningInvoice", "network": "MAINNET", "params": { "amount_sats": 20 } }' ``` ```json 200 theme={"system"} { "method": "createLightningInvoice", "data": { "id": "SparkLightningReceiveRequest:019842c4-c50f-cd96-0000-612d71a61547", "created_at": "2025-07-25T18:07:28.527107+00:00", "updated_at": "2025-07-25T18:07:28.527107+00:00", "network": "MAINNET", "invoice": { "encodedInvoice": "lnbc...", "bitcoinNetwork": "MAINNET", "paymentHash": "f59c86aaafa9920f410d1567d9ee38bf526c932691c23c7bac6d8519682a6d76", "amount": { "originalValue": 20000, "originalUnit": "MILLISATOSHI", "preferredCurrencyUnit": "USD", "preferredCurrencyValueRounded": 2, "preferredCurrencyValueApprox": 2.3228803716608595 }, "createdAt": "2025-07-25T18:07:28.484042+00:00", "expiresAt": "2025-08-24T18:07:28.484042+00:00", "memo": null }, "status": "INVOICE_CREATED", "typename": "LightningReceiveRequest", "payment_preimage": null, "receiver_identity_public_key": null } } ``` ### Headers ID of your Privy app. Request authorization signature. If multiple signatures are required, they should be comma separated. Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters ID of the wallet to get. These wallet methods are modeled after the [Spark Wallet SDK](https://github.com/buildonspark/spark/tree/main/sdks/js/packages/spark-sdk). For more information about this wallet method, check out the [Spark Wallet documentation](https://docs.spark.money/wallet/introduction). ### Body Available options: `createLightningInvoice` Available options: `MAINNET`, `REGTEST` Parameters for the invoice to be created. The amount of sats to be received. ### Returns Always `"createLightningInvoice"` Details about the created Lightning invoice. Unique ID of the Lightning receive request. Timestamp of invoice creation. Timestamp of last update. The Bitcoin network this invoice is valid for. Encoded invoice details. Bolt11-encoded Lightning invoice string. The Bitcoin network. The hash of the preimage for this payment request. Amount details. Timestamp of invoice creation. Timestamp of invoice expiration. Optional memo included with the invoice. Current status of the invoice. Possible value: `"INVOICE_CREATED"`. Public key of the receiving node. # getBalance Source: https://docs.privy.io/api-reference/wallets/spark/get-balance post /v1/wallets/{wallet_id}/rpc Retrieve the balance and token holdings of a Spark wallet. Claims any pending transfers. ```sh theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "getBalance", "network": "MAINNET" }' ``` ```json 200 theme={"system"} { "method": "getBalance", "data": { "balance": "239170", "token_balances": { "btknrt1x9helfvakyz8y53lzwt2wjen7d30ft6skpu69eydvndqt5uxsr4q0zvugn": { "balance": "999999910", "token_metadata": { "raw_token_identifier": "316f9fa59db10472523f1396a74b33f362f4af50b079a2e48d64da05d38680ea", "token_public_key": "025bd027cd332a40e21f16cb6e9c6aee9ac11e3dff9508081b64fa8b27658b18b6", "token_name": "Merica", "token_ticker": "USA", "decimals": 6, "max_supply": "21000000000000" } } }, "encoding": "hex" } } ``` ### Headers ID of your Privy app. Request authorization signature. If multiple signatures are required, they should be comma separated. Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters ID of the wallet to get. These wallet methods are modeled after the [Spark Wallet SDK](https://github.com/buildonspark/spark/tree/main/sdks/js/packages/spark-sdk). For more information about this wallet method, check out the [Spark Wallet documentation](https://docs.spark.money/wallet/introduction). ### Body Available options: `getBalance` Available options: `MAINNET`, `REGTEST` ### Returns Available options: `getBalance` The balance and token holdings of the wallet. Native Spark balance in satoshis. A mapping of token Spark addresses to token data. ``` ``` # getStaticDepositAddress Source: https://docs.privy.io/api-reference/wallets/spark/get-static-deposit-address post /v1/wallets/{wallet_id}/rpc Returns a static deposit address that can be used to deposit tokens from BTC into Spark. ```sh theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "getStaticDepositAddress", "network": "MAINNET" }' ``` ```json 200 theme={"system"} { "method": "getStaticDepositAddress", "data": { "address": "bcrt1ppvq36yzcycqfcgcl34samllm75zkqjgdkfsqj8hkgdh9pnse5czqj0zh9r" } } ``` ### Headers ID of your Privy app. Request authorization signature. If multiple signatures are required, they should be comma separated. Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters ID of the wallet to get. These wallet methods are modeled after the [Spark Wallet SDK](https://github.com/buildonspark/spark/tree/main/sdks/js/packages/spark-sdk). For more information about this wallet method, check out the [Spark Wallet documentation](https://docs.spark.money/wallet/introduction). ### Body Available options: `getStaticDepositAddress` Available options: `MAINNET`, `REGTEST` ### Returns Always `"getStaticDepositAddress"` The static deposit address for the wallet. A BTC address for depositing native tokens. # getClaimStaticDepositQuote Source: https://docs.privy.io/api-reference/wallets/spark/get-static-deposit-quote post /v1/wallets/{wallet_id}/rpc Retrieve the quote needed to claim BTC sent to a static deposit address. ```sh theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "getClaimStaticDepositQuote", "network": "REGTEST", "params": { "transaction_id": "448f305caf15ec10b2a8286fde6c31ebe2eb30e2018b22a8f7630d3fa2753e49" } }' ``` ```json 200 theme={"system"} { "method": "getClaimStaticDepositQuote", "data": { "credit_amount_sats": 9901, "signature": "304402206e61d688bc498b8cd95f798c82c6f087c71d56ef822b04bc268240dba7be8705022077609ae2349b5233455821cd8db7dc36ac9e123b62f983bd1da371034e786dfd", "transaction_id": "448f305caf15ec10b2a8286fde6c31ebe2eb30e2018b22a8f7630d3fa2753e49", "output_index": 1, "network": "REGTEST" } } ``` ### Headers ID of your Privy app. Request authorization signature. If multiple signatures are required, they should be comma separated. Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters ID of the wallet to get. These wallet methods are modeled after the [Spark Wallet SDK](https://github.com/buildonspark/spark/tree/main/sdks/js/packages/spark-sdk). For more information about this wallet method, check out the [Spark Wallet documentation](https://docs.spark.money/wallet/introduction). ### Body Must be set to getClaimStaticDepositQuote. Blockchain network to use. Options: `MAINNET`, `REGTEST`. The transaction hash of the BTC sent to the static deposit address. ### Returns Contains the BTC claim quote. The amount of sats credited to the wallet after claiming. The hash of the Bitcoin transaction containing the deposit. The index of the output within the transaction that is being claimed. The network used (e.g. `REGTEST` or `MAINNET`). # getWithdrawalFeeQuote Source: https://docs.privy.io/api-reference/wallets/spark/get-withdrawal-fee-quote post /v1/wallets/{wallet_id}/rpc Get a fee quote for withdrawing BTC from a Spark wallet to a Bitcoin L1 address. ```sh theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "getWithdrawalFeeQuote", "network": "MAINNET", "params": { "amount_sats": 10000, "onchain_address": "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4" } }' ``` ```json 200 theme={"system"} { "method": "getWithdrawalFeeQuote", "data": { "id": "5c1a2b3d-4e5f-6a7b-8c9d-0e1f2a3b4c5d", "created_at": "2025-07-24T16:43:12.509Z", "updated_at": "2025-07-24T16:43:12.509Z", "network": "MAINNET", "total_amount": { "original_value": 10000, "original_unit": "SATOSHI" }, "user_fee_fast": { "original_value": 200, "original_unit": "SATOSHI" }, "user_fee_medium": { "original_value": 150, "original_unit": "SATOSHI" }, "user_fee_slow": { "original_value": 100, "original_unit": "SATOSHI" }, "l1_broadcast_fee_fast": { "original_value": 50, "original_unit": "SATOSHI" }, "l1_broadcast_fee_medium": { "original_value": 40, "original_unit": "SATOSHI" }, "l1_broadcast_fee_slow": { "original_value": 30, "original_unit": "SATOSHI" }, "expires_at": "2025-07-24T17:43:12.509Z" } } ``` ### Headers ID of your Privy app. Request authorization signature. If multiple signatures are required, they should be comma separated. Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters ID of the wallet to get. These wallet methods are modeled after the [Spark Wallet SDK](https://github.com/buildonspark/spark/tree/main/sdks/js/packages/spark-sdk). For more information about this wallet method, check out the [Spark Wallet documentation](https://docs.spark.money/wallet/introduction). ### Body Must be set to `getWithdrawalFeeQuote`. Blockchain network to use. Options: `MAINNET`, `REGTEST`. The amount of satoshis to withdraw. The Bitcoin L1 address to withdraw to. ### Returns Available options: `getWithdrawalFeeQuote` A `CoopExitFeeQuote` object with the following fields: The unique identifier of this fee quote. The date and time when the quote was first created. The date and time when the quote was last updated. The network the coop exit fee quote is on. The total amount of all nodes swapped for the coop exit quote. The user fee (excluding L1 broadcast fee) for fast exit speed. The user fee (excluding L1 broadcast fee) for medium exit speed. The user fee (excluding L1 broadcast fee) for slow exit speed. The L1 broadcast fee for fast exit speed. The L1 broadcast fee for medium exit speed. The L1 broadcast fee for slow exit speed. The time when the fee quote expires. # payLightningInvoice Source: https://docs.privy.io/api-reference/wallets/spark/pay-lightning-invoice post /v1/wallets/{wallet_id}/rpc Pay a Lightning Network invoice from a Spark wallet. **Idempotency on errors:** On 4xx or 5xx, Privy caches the response and replays it for the same key. Generate a new key to retry after a server error. Policy violations are an exception and allow same-key retries. ```sh curl --request POST \ theme={"system"} --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "payLightningInvoice", "network": "MAINNET", "params": { "invoice": "lnbc1230n1p...", "max_fee_sats": 5, "prefer_spark": true, "amount_sats_to_send": 5000 } }' ``` ```json 200 theme={"system"} { "method": "payLightningInvoice", "data": { "id": "2fb0b49e-0aef-4726-a348-2dd0a9432c12", "created_at": "2025-07-24T16:43:12.509Z", "updated_at": "2025-07-24T16:43:12.509Z", "network": "MAINNET", "encoded_invoice": "lnbc1230n1p...", "fee": { "original_value": 3, "original_unit": "SATOSHI" }, "status": "SUCCESS", "typename": "LightningSendRequest" } } ``` ### Headers ID of your Privy app. Request authorization signature. If multiple signatures are required, they should be comma separated. Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters ID of the wallet to get. These wallet methods are modeled after the [Spark Wallet SDK](https://github.com/buildonspark/spark/tree/main/sdks/js/packages/spark-sdk). For more information about this wallet method, check out the [Spark Wallet documentation](https://docs.spark.money/wallet/introduction). ### Body Available options: `payLightningInvoice` Available options: `MAINNET`, `REGTEST` The BOLT11 Lightning invoice to pay. Maximum fee (in sats) the payer is willing to pay. Whether to prefer paying on Spark. If true, will return a Transfer object. Defaults to false. Amount to pay in sats. Required only for zero-amount invoices. ### Returns Available options: `payLightningInvoice` If `prefer_spark` is false, a `LightningSendRequest` object with the following fields: If `prefer_spark` is true, the response will be a `Transfer` object with the following fields: Each item contains transfer metadata and encrypted leaf information. A mapping from signer identifier to public share # signMessageWithIdentityKey Source: https://docs.privy.io/api-reference/wallets/spark/sign-message-with-identity-key post /v1/wallets/{wallet_id}/rpc Sign a message using the Spark wallet's identity key. **Idempotency on errors:** On 4xx or 5xx, Privy caches the response and replays it for the same key. Generate a new key to retry after a server error. Policy violations are an exception and allow same-key retries. ```sh curl --request POST \ theme={"system"} --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "signMessageWithIdentityKey", "network": "MAINNET", "params": { "message": "Hello, Spark!", "compact": true } }' ``` ```json 200 theme={"system"} { "method": "signMessageWithIdentityKey", "data": { "signature": "304402201a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f80902201a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192a3b4c5d6e7f809" } } ``` ### Headers ID of your Privy app. Request authorization signature. If multiple signatures are required, they should be comma separated. Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters ID of the wallet to get. These wallet methods are modeled after the [Spark Wallet SDK](https://github.com/buildonspark/spark/tree/main/sdks/js/packages/spark-sdk). For more information about this wallet method, check out the [Spark Wallet documentation](https://docs.spark.money/wallet/introduction). ### Body Available options: `signMessageWithIdentityKey` Available options: `MAINNET`, `REGTEST` The message to sign with the identity key. Whether to use compact signature format. Defaults to false. ### Returns Available options: `signMessageWithIdentityKey` The signature response object The signature of the message from the wallet's identity key. # transfer Source: https://docs.privy.io/api-reference/wallets/spark/transfer post /v1/wallets/{wallet_id}/rpc Transfer satoshis from a Spark wallet to another Spark address. **Idempotency on errors:** On 4xx or 5xx, Privy caches the response and replays it for the same key. Generate a new key to retry after a server error. Policy violations are an exception and allow same-key retries. ```sh curl --request POST \ theme={"system"} --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "transfer", "network": "MAINNET", "params": { "receiver_spark_address": "spark1pgss8z35rpycv4duqdk5u3sclhjnztjunv5yajlwk69tyv5fsvwwe9mgwmxfkx", "amount_sats": 16 } }' ``` ```json 200 theme={"system"} { "method": "transfer", "data": { "id": "01983eb7-4008-7b73-b269-c0bd86955a1b", "sender_identity_public_key": "027bca218b2853d5ff2e9b174370f58107a23df98a759e968c523253c1702dd485", "receiver_identity_public_key": "038a3418498655bc036d4e4618fde5312e5c9b284ecbeeb68ab23289831cec9768", "status": "TRANSFER_STATUS_SENDER_KEY_TWEAKED", "total_value": 16, "expiry_time": "1970-01-01T00:00:00.000Z", "leaves": [ { "leaf": { "id": "01983e38-b2f8-7649-a910-b2be1424de1d", "tree_id": "01983e36-e645-7a34-ab95-a6faec210992", "value": 16, "parent_node_id": "01983e38-b2e2-7edd-9b6b-a784545aa28c", "node_tx": "", "refund_tx": "", "vout": 0, "verifying_public_key": "", "owner_identity_public_key": "", "signing_keyshare": { "owner_identifiers": ["...", "..."], "threshold": 2, "public_key": "", "public_shares": { "...": "", "...": "" }, "updated_time": "2025-07-24T23:14:14.598Z" }, "status": "TRANSFER_LOCKED", "network": "MAINNET" }, "secret_cipher": "", "signature": "", "intermediate_refund_tx": "" } ], "created_time": "2025-07-24T23:14:14.276Z", "updated_time": "2025-07-24T23:14:14.618Z", "type": "TRANSFER", "transfer_direction": "OUTGOING", "encoding": "hex" } } ``` ### Headers ID of your Privy app. Request authorization signature. If multiple signatures are required, they should be comma separated. Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters ID of the wallet to get. These wallet methods are modeled after the [Spark Wallet SDK](https://github.com/buildonspark/spark/tree/main/sdks/js/packages/spark-sdk). For more information about this wallet method, check out the [Spark Wallet documentation](https://docs.spark.money/wallet/introduction). ### Body Available options: `transfer` Available options: `MAINNET`, `REGTEST` The Spark address of the recipient. The amount to send in satoshis. ### Returns Available options: `transfer` The returned Transfer object Each item contains transfer metadata and encrypted leaf information. A mapping from signer identifier to public share # transferTokens Source: https://docs.privy.io/api-reference/wallets/spark/transfer-tokens post /v1/wallets/{wallet_id}/rpc Transfer a specified amount of Spark tokens to another Spark address. **Idempotency on errors:** On 4xx or 5xx, Privy caches the response and replays it for the same key. Generate a new key to retry after a server error. Policy violations are an exception and allow same-key retries. ```sh theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "transferTokens", "network": "MAINNET", "params": { "token_identifier": "btknrt1x9helfvakyz8y53lzwt2wjen7d30ft6skpu69eydvndqt5uxsr4q0zvugn", "token_amount": 10, "receiver_spark_address": "spark1pgss8z35rpycv4duqdk5u3sclhjnztjunv5yajlwk69tyv5fsvwwe9mgwmxfkx" } }' ``` ```json 200 theme={"system"} { "method": "transferTokens", "data": { "id": "22c469d0a956b188f7dc058e43515a2c4e675d75edc302b75805d9c5dccaeb6b", } } ``` ### Headers ID of your Privy app. Request authorization signature. If multiple signatures are required, they should be comma separated. Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters ID of the wallet to get. These wallet methods are modeled after the [Spark Wallet SDK](https://github.com/buildonspark/spark/tree/main/sdks/js/packages/spark-sdk). For more information about this wallet method, check out the [Spark Wallet documentation](https://docs.spark.money/wallet/introduction). ### Body Available options: `transferTokens` Available options: `MAINNET`, `REGTEST` Parameters for the token transfer. Spark token address (starts with `btkn`). Number of tokens to send Spark address to send the tokens to. ### Returns Always `"transferTokens"` The result of the token transfer. Transaction hash of the token transfer. ``` ``` # withdraw Source: https://docs.privy.io/api-reference/wallets/spark/withdraw post /v1/wallets/{wallet_id}/rpc Withdraw BTC from a Spark wallet to a Bitcoin L1 address (cooperative exit). **Idempotency on errors:** On 4xx or 5xx, Privy caches the response and replays it for the same key. Generate a new key to retry after a server error. Policy violations are an exception and allow same-key retries. ```sh theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "withdraw", "network": "MAINNET", "params": { "onchain_address": "bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4", "exit_speed": "FAST", "amount_sats": 10000, "fee_quote_id": "5c1a...", "fee_amount_sats": 250, "deduct_fee_from_withdrawal_amount": false } }' ``` ```json 200 theme={"system"} { "method": "withdraw", "data": { "id": "9b1f8a2c-3d4e-4f5a-8b6c-7d8e9f0a1b2c", "created_at": "2025-07-24T16:43:12.509Z", "updated_at": "2025-07-24T16:43:12.509Z", "network": "MAINNET", "fee": { "original_value": 200, "original_unit": "SATOSHI" }, "l1_broadcast_fee": { "original_value": 50, "original_unit": "SATOSHI" }, "status": "PENDING", "expires_at": "2025-07-24T17:43:12.509Z", "coop_exit_txid": "448f305caf15ec10b2a8286fde6c31ebe2eb30e2018b22a8f7630d3fa2753e49", "fee_quote_id": "5c1a...", "exit_speed": "FAST" } } ``` ### Headers ID of your Privy app. Request authorization signature. If multiple signatures are required, they should be comma separated. Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters ID of the wallet to get. These wallet methods are modeled after the [Spark Wallet SDK](https://github.com/buildonspark/spark/tree/main/sdks/js/packages/spark-sdk). For more information about this wallet method, check out the [Spark Wallet documentation](https://docs.spark.money/wallet/introduction). ### Body Must be set to `withdraw`. Blockchain network to use. Options: `MAINNET`, `REGTEST`. The Bitcoin L1 address to withdraw to. The exit speed for the withdrawal. Options: `FAST`, `MEDIUM`, `SLOW`. The amount of satoshis to withdraw. Withdraws the full balance if not specified. The fee amount in satoshis, from a prior `getWithdrawalFeeQuote` call. The fee quote ID from a prior `getWithdrawalFeeQuote` call. Whether to deduct the fee from the withdrawal amount. Defaults to false. ### Returns Available options: `withdraw` A `CoopExitRequest` object with the following fields: The unique identifier of this coop exit request. The date and time when the request was first created. The date and time when the request was last updated. The network the coop exit request is on. The fee the user pays for the coop exit, not including the L1 broadcast fee. The L1 broadcast fee the user pays for the coop exit. The status of this coop exit request. The time when the coop exit request expires and the UTXOs are released. The transaction id of the coop exit transaction. The fee quote ID used for this coop exit. The exit speed used for this coop exit. Options: `FAST`, `MEDIUM`, `SLOW`. # Get swap quote Source: https://docs.privy.io/api-reference/wallets/swap/quote post /v1/wallets/{wallet_id}/swap/quote Get a price quote for swapping tokens within a wallet. ### Prerequisites * Swaps must be [enabled in the Privy Dashboard](/wallets/actions/swap/setup) * [Gas sponsorship](/wallets/gas-and-asset-management/gas/setup) must be configured for your app *** Swap quotes reflect real-time market conditions and can change quickly. Fetch a fresh quote before executing a swap to ensure your app displays accurate pricing. # Swap tokens Source: https://docs.privy.io/api-reference/wallets/swap/tokens post /v1/wallets/{wallet_id}/swap Execute a token swap within a wallet. ### Prerequisites * Swaps must be [enabled in the Privy Dashboard](/wallets/actions/swap/setup) * [Gas sponsorship](/wallets/gas-and-asset-management/gas/setup) must be configured for your app *** Privy automates token approvals and transaction submission. The response is a wallet action that starts in `pending` status and can be polled for confirmation. *** **Idempotency on errors:** This endpoint returns `pending` immediately. Replaying the same key returns the cached response. On a synchronous 5xx, Privy deletes the record so the same key retries fresh. Action outcomes (`rejected`, `failed`, `succeeded`) arrive via [webhooks](/wallets/actions/webhooks) and [polling](/wallets/actions/status). Policy violations always allow fresh retries. # eth_sendTransaction Source: https://docs.privy.io/api-reference/wallets/tempo/eth-send-transaction post /v1/wallets/{wallet_id}/rpc Sign and send a Tempo transaction using the eth_sendTransaction method with type 118. ### SDK methods Learn more about sending Tempo transactions using our SDKs [here](/wallets/using-wallets/tempo/send-a-transaction). *** **Idempotency on errors:** On 4xx or 5xx, Privy caches the response and replays it for the same key. Generate a new key to retry after a server error. Policy violations are an exception and allow same-key retries. ```sh Without sponsorship theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "eth_sendTransaction", "caip2": "eip155:4217", "chain_type": "ethereum", "params": { "transaction": { "type": 118, "calls": [ { "to": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "value": "0x2386F26FC10000" } ] } } }' ``` ```sh With sponsorship theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "eth_sendTransaction", "caip2": "eip155:4217", "chain_type": "ethereum", "sponsor": true, "params": { "transaction": { "type": 118, "calls": [ { "to": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "value": "0x2386F26FC10000" } ] } } }' ``` ```json Without sponsorship theme={"system"} { "method": "eth_sendTransaction", "data": { "hash": "0xfc3a736ab2e34e13be2b0b11b39dbc0232a2e755a11aa5a9219890d3b2c6c7d8", "caip2": "eip155:4217", "transaction_id": "y90vpg3bnkjxhw541c2zc6a9", "transaction_request": { "chain_id": 4217, "type": 118, "calls": [ { "to": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "value": "0x2386F26FC10000" } ] } } } ``` ```json With sponsorship theme={"system"} { "method": "eth_sendTransaction", "data": { "hash": "0xfc3a736ab2e34e13be2b0b11b39dbc0232a2e755a11aa5a9219890d3b2c6c7d8", "caip2": "eip155:4217", "transaction_id": "y90vpg3bnkjxhw541c2zc6a9" } } ``` The wallet RPC endpoint is a synchronous endpoint, and a successful response indicates that the transaction has been broadcasted to the network. Transactions may get broadcasted but still fail to be confirmed by the network. The endpoint does not wait for confirmation or retry if the transaction fails to be confirmed. To handle these scenarios, see our guide on [speeding up transactions](/recipes/speeding-up-transactions). ### Headers ID of your Privy app. Request authorization signature. If multiple signatures are required, they should be comma separated. Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters ID of the wallet to get. ### Body Available options: `eth_sendTransaction` The CAIP-2 chain identifier for the Tempo network. Must be `118` for Tempo transactions. The calls to execute atomically. The target address for the call. The calldata for the call. The value to send with the call in wei. Address of the supported TIP-20 token used to pay Tempo fees. If omitted, Tempo's [fee-token preference rules](https://docs.tempo.xyz/protocol/fees/spec-fee#fee-lifecycle) apply. Tempo has no native gas token. The Tempo 2D nonce key. Unix timestamp in seconds after which Tempo will reject the transaction. Unix timestamp in seconds before which Tempo will reject the transaction. A fee payer signature for sponsored Tempo transactions. To have Privy populate this field, pass `sponsor: true` at the request root. Account abstraction authorizations for Tempo transactions. Enable gas sponsorship for this transaction. [Learn more.](/wallets/gas-and-asset-management/gas/overview) Optional developer-provided reference ID for transaction reconciliation. Must be unique per transaction and up to 64 characters. Use this to correlate transactions with your own internal records. The `reference_id` is included in [transaction webhook](/api-reference/webhooks/transaction/broadcasted) payloads and can be used to [look up transactions](/api-reference/transactions/external-id). Available options: `ethereum` ### Returns Available options: `eth_sendTransaction` The broadcast transaction hash. The full transaction object that was signed and broadcast. The developer-provided reference ID, if one was provided in the request. # eth_signTransaction Source: https://docs.privy.io/api-reference/wallets/tempo/eth-sign-transaction post /v1/wallets/{wallet_id}/rpc Sign a Tempo transaction using the eth_signTransaction method with type 118. ### SDK methods Learn more about signing Tempo transactions using our SDKs [here](/wallets/using-wallets/tempo/sign-a-transaction). **Idempotency on errors:** On 4xx or 5xx, Privy caches the response and replays it for the same key. Generate a new key to retry after a server error. Policy violations are an exception and allow same-key retries. ```sh cURL theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "eth_signTransaction", "params": { "transaction": { "type": 118, "chain_id": 4217, "calls": [ { "to": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "value": "0x2386F26FC10000" } ] } } }' ``` ```json 200 theme={"system"} { "method": "eth_signTransaction", "data": { "signed_transaction": "0x76f870830138de80830f4240830f437480940b81418147df37155d643b5cb65ba6c8cb7aba76872000000000000480c080a05c11a2166ec56189d993dec477477d962ce0d4c466ab7ed8982110621ec87a57a003c796590c0c62eac30acd412f2aa0e8ad740c4ded86fb64d3326ee4c0ea804c", "encoding": "rlp" } } ``` ### Headers ID of your Privy app. Request authorization signature. If multiple signatures are required, they should be comma separated. Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters ID of the wallet to get. ### Body Available options: `eth_signTransaction` Must be `118` for Tempo transactions. The calls to include in the transaction. The target address for the call. The calldata for the call. The value to send with the call in wei. Address of the supported TIP-20 token used to pay Tempo fees. If omitted, Tempo's [fee-token preference rules](https://docs.tempo.xyz/protocol/fees/spec-fee#fee-lifecycle) apply. Tempo has no native gas token. The Tempo 2D nonce key. Unix timestamp in seconds after which Tempo will reject the transaction. Unix timestamp in seconds before which Tempo will reject the transaction. A fee payer signature for sponsored Tempo transactions. Account abstraction authorizations for Tempo transactions. ### Response Available options: `eth_signTransaction` Available options: `rlp` # Transfer Source: https://docs.privy.io/api-reference/wallets/transfer/index post /v1/wallets/{wallet_id}/transfer Transfer tokens from a wallet to a destination address. If your app has gas sponsorship configured, usage of the `/transfer` endpoint will be [gas-sponsored by default](/wallets/actions/overview#gas-management). There is no need to specify additional parameters for sponsorship. Cross-chain and cross-asset transfers accept an optional `fee_configuration` parameter to control fee distribution between Privy and your app. Use the [quote endpoint](/api-reference/wallets/transfer/quote) to preview fees before executing a transfer. **Idempotency on errors:** This endpoint returns `pending` immediately. Replaying the same key returns the cached response. On a synchronous 5xx, Privy deletes the record so the same key retries fresh. Action outcomes (`rejected`, `failed`, `succeeded`) arrive via [webhooks](/wallets/actions/webhooks) and [polling](/wallets/actions/status). Policy violations always allow fresh retries. # Quote transfer fees Source: https://docs.privy.io/api-reference/wallets/transfer/quote post /v1/wallets/{wallet_id}/transfer/quote Get a fee estimate and expected output amount before executing a transfer ```sh cURL theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/transfer/quote \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "source": { "asset": "usdc", "amount": "10.0", "chain": "base" }, "destination": { "address": "0xRecipientAddress", "chain": "arbitrum" }, "fee_configuration": { "type": "total_fee_bps", "value": 80 } }' ``` ```json 200 theme={"system"} { "source": { "asset": "usdc", "amount": "10.0", "chain": "base" }, "destination": { "address": "0xRecipientAddress", "chain": "arbitrum" }, "estimated_output_amount": "9.94", "estimated_fees": [ { "type": "relayer", "amount": "0.02" }, { "type": "developer", "recipient": "0x1234567890abcdef1234567890abcdef12345678", "amount": "0.04" } ], "expires_at": 1715200000 } ``` # Update wallet Source: https://docs.privy.io/api-reference/wallets/update patch /v1/wallets/{wallet_id} Update a wallet's policies or authorization key configuration. # Intent authorized Source: https://docs.privy.io/api-reference/webhooks/intents/authorized webhook intent.authorized Fired when a user authorizes (signs) an intent. # Intent created Source: https://docs.privy.io/api-reference/webhooks/intents/created webhook intent.created Fired when a new intent is created. # Intent executed Source: https://docs.privy.io/api-reference/webhooks/intents/executed webhook intent.executed Fired when an intent is successfully executed (2xx response from enclave). # Intent failed Source: https://docs.privy.io/api-reference/webhooks/intents/failed webhook intent.failed Fired when an intent execution fails (non-2xx response from enclave). # Intent rejected Source: https://docs.privy.io/api-reference/webhooks/intents/rejected webhook intent.rejected Fired when an intent is rejected. # MFA disabled Source: https://docs.privy.io/api-reference/webhooks/mfa/disabled webhook mfa.disabled Fired when a user disables multi-factor authentication. # MFA enabled Source: https://docs.privy.io/api-reference/webhooks/mfa/enabled webhook mfa.enabled Fired when a user enables multi-factor authentication. # Webhooks overview Source: https://docs.privy.io/api-reference/webhooks/overview Set up webhooks to receive real-time notifications when users take actions in your application Webhooks allow you to specify a backend endpoint that Privy will call with a signed payload whenever a user makes an action in your application, such as logging in, or linking a new account. As soon as you register an endpoint, Privy will start sending subscribed events in near real-time. The following functionality exists for [wallets reconstituted server-side](/wallets/wallets/create/create-a-wallet). More on [Privy architecture here](/security/wallet-infrastructure/architecture) Webhooks can be tested at no cost in development environments. To enable webhooks in production, upgrade to the Enterprise plan in the Privy Dashboard. images/Webhooks.png ## Registering an endpoint 1. In your backend, create a new endpoint that will accept **POST** requests from Privy When creating your endpoint to receive webhook events, always verify the payload signature by following our [webhook signing key documentation](/api-reference/webhooks/overview#webhook-signing-key). 2. In the dashboard, go to the **Configuration > Webhooks** page 3. Add your new endpoint as the destination URL and select any event types you'd like to be notified for. The URL must begin with `https://`. You can specify which user events your webhook endpoint will be notified about. The options are as follows: ### User events | Event Name | Type | Action | | ------------------------ | ------------------------- | ------------------------------------------------------------------------------ | | User created | user.created | A user was created in the application. | | User authenticated | user.authenticated | A user successfully logged into the application. | | User linked account | user.linked\_account | A user successfully linked a new login method. | | User unlinked account | user.unlinked\_account | A user successfully unlinked an existing login method. | | User updated account | user.updated\_account | A user successfully updated the email or phone number linked to their account. | | User transferred account | user.transferred\_account | A user successfully transferred their account to a new account. | | Wallet created for user | user.wallet\_created | A wallet (embedded or smart wallet) was successfully created for a user. | | MFA enabled | mfa.enabled | A user enabled MFA for their account. | | MFA disabled | mfa.disabled | A user disabled MFA for their account. | ### Wallet events | Event Name | Type | Action | | --------------------- | --------------------------- | ---------------------------------------------------------- | | Wallet archived | wallet.archived | A wallet was archived via the API. | | Wallet restored | wallet.restored | A wallet was restored from archive. | | Funds deposited | wallet.funds\_deposited | Funds were deposited into a user's embedded wallet. | | Funds withdrawn | wallet.funds\_withdrawn | Funds were withdrawn from a user's embedded wallet. | | Private key exported | wallet.private\_key\_export | A user exported their private key from an embedded wallet. | | Seed phrase exported | wallet.seed\_phrase\_export | A user exported their seed phrase from an embedded wallet. | | Wallet recovery setup | wallet.recovery\_setup | A user set up wallet recovery for their embedded wallet. | | Wallet recovered | wallet.recovered | A user successfully recovered their embedded wallet. | ### Transaction events | Event Name | Type | Action | | ------------------------- | ------------------------------- | ---------------------------------------------------------------------------- | | Transaction broadcasted | transaction.broadcasted | A transaction was submitted to the network and is awaiting confirmation. | | Transaction confirmed | transaction.confirmed | A transaction was confirmed on-chain. | | Transaction failed | transaction.failed | A transaction failed to be submitted to the network. | | Execution reverted | transaction.execution\_reverted | A transaction was confirmed on-chain but execution reverted. | | Transaction replaced | transaction.replaced | A transaction was replaced by another transaction (e.g. speed-up or cancel). | | Transaction still pending | transaction.still\_pending | A transaction remained pending for an extended period. | | Provider error | transaction.provider\_error | A provider error occurred while submitting a transaction. | ### Wallet action events | Event Name | Type | Action | | -------------------- | ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | | Swap | wallet\_action.swap.\* | A token swap was created, succeeded, was rejected, or failed. | | Transfer | wallet\_action.transfer.\* | A token transfer was created, succeeded, was rejected, or failed. | | Earn deposit | wallet\_action.earn\_deposit.\* | A vault deposit was created, succeeded, was rejected, or failed. See [earn webhooks](/wallets/actions/earn/webhooks). | | Earn withdrawal | wallet\_action.earn\_withdraw.\* | A vault withdrawal was created, succeeded, was rejected, or failed. See [earn webhooks](/wallets/actions/earn/webhooks). | | Earn incentive claim | wallet\_action.earn\_incentive\_claim.\* | An incentive claim was created, succeeded, was rejected, or failed. See [earn webhooks](/wallets/actions/earn/webhooks). | ### Usage charge events Usage charge events are only published for accounts on postpaid billing. See [usage webhooks](/wallets/gas-and-asset-management/usage-billing/usage-webhooks) for the payload and reconciliation guidance. To set up postpaid billing, contact your Privy account manager or [sales@privy.io](mailto:sales@privy.io). | Event Name | Type | Action | | -------------------------------- | -------------------------------- | ----------------------------------------------------------- | | Gas sponsorship charge recorded | usage.gas\_sponsorship.recorded | Privy recorded a charge for sponsored network gas. | | Cross-chain swap charge recorded | usage.cross\_chain\_fee.recorded | Privy recorded a charge for a cross-chain transfer or swap. | ### Intent events | Event Name | Type | Action | | ----------------- | ----------------- | -------------------------------------------------------------------------------- | | Intent created | intent.created | An [intent](/controls/dashboard/overview) was proposed and is awaiting approval. | | Intent authorized | intent.authorized | A team member authorized an intent. | | Intent rejected | intent.rejected | An intent was rejected by a team member. | | Intent executed | intent.executed | An intent was fully approved and the action completed successfully. | | Intent failed | intent.failed | An intent was fully approved but the action failed during execution. | ### User operation events | Event Name | Type | Action | | ------------------------ | ------------------------- | ------------------------------------------ | | User operation completed | user\_operation.completed | An ERC-4337 UserOperation landed on-chain. | **That's it! You've successfully configured webhooks for your app.** 🎉 ## Testing the webhook setup During development and testing, you can quickly verify your webhook endpoint is working correctly. Here's a simple step-by-step guide: #### Step 1: Expose your local endpoint If you're testing locally, you'll need to expose your local server to the internet. Use a tool like [ngrok](https://ngrok.com/) or [Cloudflare Tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/): ```bash theme={"system"} # Using ngrok (after installing: https://ngrok.com/download) ngrok http 3000 # Or using Cloudflare Tunnel (after installing: https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/install-and-setup/installation/) cloudflared tunnel --url http://localhost:3000 ``` Copy the public URL (e.g., `https://abc123.ngrok.io` or `https://abc123.trycloudflare.com`). #### Step 2: Create your webhook endpoint Create a simple endpoint in your backend that accepts POST requests: ```tsx theme={"system"} import {NextRequest, NextResponse} from 'next/server'; export async function POST(req: NextRequest) { const body = await req.json(); // Log the webhook payload console.log('Webhook received:', body); // Return 200 to acknowledge receipt return NextResponse.json({received: true}, {status: 200}); } ``` #### Step 3: Register your endpoint in the dashboard 1. Go to **Configuration > Webhooks** in the Privy Dashboard 2. Add your public URL (from Step 1) as the webhook endpoint 3. Select the event types you want to test #### Step 4: Test your endpoint Click the **"Test webhook"** button in the dashboard. This will send a test webhook (`privy.test`) to your endpoint with the following payload: ```json theme={"system"} { "type": "privy.test", "message": "Hello, World!" } ``` #### Step 5: Verify receipt Check your server logs to confirm you received the webhook. You should see: * The webhook payload in your console/logs * A successful 200 response returned to Privy For quick testing, you can also use services like [webhook.site](https://webhook.site/) or [RequestBin](https://requestbin.com/) to get a temporary endpoint URL without setting up your own server. ## Webhook delivery Privy sends webhooks to your configured endpoint via Svix. The webhook system operates on an **at least once** delivery basis with automatic retries if the endpoint does not successfully respond. Redundant webhook deliveries can be identified using the `idempotency_key` field where available, ensuring your application can safely handle duplicate events. ### Retry behavior Your endpoint must return a 2xx response for the webhook delivery to be considered successful. Anything else is considered an error response, and will be retried based on the following schedule, where each period is started following the failure of the preceding attempt: * Immediately * 5 seconds * 5 minutes * 30 minutes * 2 hours * 5 hours * 10 hours * 10 hours (in addition to the previous) After the final attempt, the message will be marked as a failure, and must be manually retried from the dashboard. If all attempts to your endpoint fail for 5 consecutive days, your endpoint will be automatically disabled. ### Static IPs Webhooks will be delivered from the following list of IP addresses: ``` 44.228.126.217 50.112.21.217 52.24.126.164 54.148.139.208 2600:1f24:64:8000::/56 ``` ## Webhook signing key The webhook signing key is necessary to verify that the payloads sent to your endpoint are from Privy. Follow the steps below in order to set up webhook verification in your backend. Webhook payloads must be verified before they are trusted and used on your server. This is done by verifying a signature sent with your webhook. Privy uses [`svix`](https://www.svix.com/) for webhooks infrastructure. Your endpoint must return a 2xx (status code 200-299) response for the webhook to be marked as delivered. Any other statuses (including 3xx) are considered failed deliveries. Your endpoint will be automatically disabled after 5 consecutive days of delivery failures ### Verify a webhook with the SDK Use the **`PrivyClient`**'s **`webhooks().verify`** method to verify an incoming webhook. Pass in the request body and svix headers. As an example, for a Next.js API request: ```tsx theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID, appSecret: process.env.PRIVY_APP_SECRET, webhookSigningSecret: process.env.PRIVY_WEBHOOK_SIGNING_SECRET }); // req is an input of type `NextApiRequest`. // `headers` must include `svix-id`, `svix-timestamp`, and `svix-signature`. const verifiedPayload = privy.webhooks().verify({ payload: req.body, headers: req.headers }); ``` If the webhook payload is valid, the method returns the verified payload. If the webhook payload is invalid, the method throws an `InvalidWebhookError`. Use the **`PrivyClient`**'s **`Webhooks.Verify`** method to verify an incoming webhook. Pass in the raw request body bytes and the request headers: ```go theme={"system"} package main import ( "io" "log" "net/http" "os" privy "github.com/privy-io/go-sdk" ) var client = privy.NewPrivyClient(privy.PrivyClientOptions{ AppID: os.Getenv("PRIVY_APP_ID"), AppSecret: os.Getenv("PRIVY_APP_SECRET"), WebhookSigningSecret: os.Getenv("PRIVY_WEBHOOK_SIGNING_SECRET"), }) func webhookHandler(w http.ResponseWriter, r *http.Request) { body, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "failed to read body", http.StatusBadRequest) return } payload, err := client.Webhooks.Verify(privy.VerifyInput{ Payload: body, Headers: r.Header, }) if err != nil { http.Error(w, "invalid webhook", http.StatusUnauthorized) return } log.Printf("verified webhook payload: %+v", payload) w.WriteHeader(http.StatusOK) } ``` If the webhook payload is valid, the method returns the verified `*WebhookPayload`. If the webhook payload is invalid, the method returns an `*InvalidWebhookError`. Use the **`PrivyClient`**'s **`webhooks.unsafe_unwrap`** method to parse an incoming webhook payload into a typed event. Pass in the raw request body as a string: ```ruby theme={"system"} # Verify the Svix signature at your HTTP layer (e.g. with the `svix` gem) # before calling unsafe_unwrap. The Ruby SDK does not yet have a built-in # verifier — this is on the roadmap. event = client.webhooks.unsafe_unwrap(raw_payload_string) case event when Privy::Models::TransactionConfirmedWebhookPayload puts(event.transaction_hash) when Privy::Models::UserCreatedWebhookPayload puts(event.user.id) end ``` ### Manual verification In order to verify an incoming webhook, please refer to svix's [manual verification guide](https://docs.svix.com/receiving/verifying-payloads/how-manual) or [library verification guide](https://docs.svix.com/receiving/verifying-payloads/how). ## Webhook events reference Events for user creation, authentication, and account linking. Includes [`user.created`](/api-reference/webhooks/user/created), [`user.authenticated`](/api-reference/webhooks/user/authenticated), and more. Events for deposits, withdrawals, and wallet security. Includes [`wallet.funds_deposited`](/api-reference/webhooks/wallet/funds_deposited), [`wallet.funds_withdrawn`](/api-reference/webhooks/wallet/funds_withdrawn), and more. Events for transaction lifecycle. Includes [`transaction.confirmed`](/api-reference/webhooks/transaction/confirmed), [`transaction.failed`](/api-reference/webhooks/transaction/failed), and more. Events for swaps, transfers, and earn actions. Includes `wallet_action.swap.*`, `wallet_action.transfer.*`, and more. Events for multi-factor authentication. Includes [`mfa.enabled`](/api-reference/webhooks/mfa/enabled) and [`mfa.disabled`](/api-reference/webhooks/mfa/disabled). Events for the intent approval lifecycle. Includes [`intent.created`](/api-reference/webhooks/intents/created), [`intent.authorized`](/api-reference/webhooks/intents/authorized), and [`intent.rejected`](/api-reference/webhooks/intents/rejected). Events for ERC-4337 UserOperation lifecycle. Includes [`user_operation.completed`](/api-reference/webhooks/user-operation/completed). # Transaction broadcasted Source: https://docs.privy.io/api-reference/webhooks/transaction/broadcasted webhook transaction.broadcasted Fired when a transaction is successfully broadcast to the blockchain network. If a `reference_id` was provided when the transaction was created via [`eth_sendTransaction`](/api-reference/wallets/ethereum/eth-send-transaction) or [`signAndSendTransaction`](/api-reference/wallets/solana/sign-and-send-transaction), it is included in the webhook payload for reconciliation. # Transaction confirmed Source: https://docs.privy.io/api-reference/webhooks/transaction/confirmed webhook transaction.confirmed Fired when a transaction is confirmed on-chain. If a `reference_id` was provided when the transaction was created via [`eth_sendTransaction`](/api-reference/wallets/ethereum/eth-send-transaction) or [`signAndSendTransaction`](/api-reference/wallets/solana/sign-and-send-transaction), it is included in the webhook payload for reconciliation. # Transaction execution reverted Source: https://docs.privy.io/api-reference/webhooks/transaction/execution_reverted webhook transaction.execution_reverted Fired when a transaction execution reverts on-chain. If a `reference_id` was provided when the transaction was created via [`eth_sendTransaction`](/api-reference/wallets/ethereum/eth-send-transaction) or [`signAndSendTransaction`](/api-reference/wallets/solana/sign-and-send-transaction), it is included in the webhook payload for reconciliation. # Transaction failed Source: https://docs.privy.io/api-reference/webhooks/transaction/failed webhook transaction.failed Fired when a transaction fails. If a `reference_id` was provided when the transaction was created via [`eth_sendTransaction`](/api-reference/wallets/ethereum/eth-send-transaction) or [`signAndSendTransaction`](/api-reference/wallets/solana/sign-and-send-transaction), it is included in the webhook payload for reconciliation. # Transaction provider error Source: https://docs.privy.io/api-reference/webhooks/transaction/provider_error webhook transaction.provider_error Fired when there is an error from the transaction provider. If a `reference_id` was provided when the transaction was created via [`eth_sendTransaction`](/api-reference/wallets/ethereum/eth-send-transaction) or [`signAndSendTransaction`](/api-reference/wallets/solana/sign-and-send-transaction), it is included in the webhook payload for reconciliation. # Transaction replaced Source: https://docs.privy.io/api-reference/webhooks/transaction/replaced webhook transaction.replaced Fired when a transaction is replaced by another transaction (e.g., speed-up or cancellation). If a `reference_id` was provided when the transaction was created via [`eth_sendTransaction`](/api-reference/wallets/ethereum/eth-send-transaction) or [`signAndSendTransaction`](/api-reference/wallets/solana/sign-and-send-transaction), it is included in the webhook payload for reconciliation. # Transaction still pending Source: https://docs.privy.io/api-reference/webhooks/transaction/still_pending webhook transaction.still_pending Fired when a transaction has been pending for an extended period without confirmation. If a `reference_id` was provided when the transaction was created via [`eth_sendTransaction`](/api-reference/wallets/ethereum/eth-send-transaction) or [`signAndSendTransaction`](/api-reference/wallets/solana/sign-and-send-transaction), it is included in the webhook payload for reconciliation. # Cross-chain swap charge recorded Source: https://docs.privy.io/api-reference/webhooks/usage/cross_chain_fee_recorded webhook usage.cross_chain_fee.recorded Fired when Privy records its fee on a cross-chain transfer or swap for a wallet action. # Gas sponsorship charge recorded Source: https://docs.privy.io/api-reference/webhooks/usage/gas_sponsorship_recorded webhook usage.gas_sponsorship.recorded Fired when Privy records a sponsored-gas charge for a wallet action. # User authenticated Source: https://docs.privy.io/api-reference/webhooks/user/authenticated webhook user.authenticated Fired when a user successfully authenticates (logs in) to your app. # User created Source: https://docs.privy.io/api-reference/webhooks/user/created webhook user.created Fired when a new user is created in your app. # User deleted Source: https://docs.privy.io/api-reference/webhooks/user/deleted webhook user.deleted Fired when a user is deleted from your app, for any reason (manual deletion or account merge). # User linked account Source: https://docs.privy.io/api-reference/webhooks/user/linked_account webhook user.linked_account Fired when a user links a new account (email, phone, wallet, social login, etc.) to their profile. # User transferred account Source: https://docs.privy.io/api-reference/webhooks/user/transferred_account webhook user.transferred_account Fired when an account is transferred from one user to another. # User unlinked account Source: https://docs.privy.io/api-reference/webhooks/user/unlinked_account webhook user.unlinked_account Fired when a user unlinks an account from their profile. # User updated account Source: https://docs.privy.io/api-reference/webhooks/user/updated_account webhook user.updated_account Fired when a user updates one of their linked accounts. # User wallet created Source: https://docs.privy.io/api-reference/webhooks/user/wallet_created webhook user.wallet_created Fired when an embedded wallet is created for a user. # Earn deposit created Source: https://docs.privy.io/api-reference/webhooks/wallet-action/earn-deposit/created webhook wallet_action.earn_deposit.created Fired when a wallet action is created and execution has begun. The action is now pending. # Earn deposit failed Source: https://docs.privy.io/api-reference/webhooks/wallet-action/earn-deposit/failed webhook wallet_action.earn_deposit.failed Fired when a wallet action fails after at least one transaction was broadcast. There may be onchain state changes; inspect the steps for details. # Earn deposit rejected Source: https://docs.privy.io/api-reference/webhooks/wallet-action/earn-deposit/rejected webhook wallet_action.earn_deposit.rejected Fired when a wallet action fails before any onchain effects (e.g. policy rejection, simulation failure). Safe to retry. # Earn deposit succeeded Source: https://docs.privy.io/api-reference/webhooks/wallet-action/earn-deposit/succeeded webhook wallet_action.earn_deposit.succeeded Fired when all steps of a wallet action have been confirmed onchain. The action is complete. # Earn fee collect created Source: https://docs.privy.io/api-reference/webhooks/wallet-action/earn-fee-collect/created webhook wallet_action.earn_fee_collect.created Fired when a wallet action is created and execution has begun. The action is now pending. # Earn fee collect failed Source: https://docs.privy.io/api-reference/webhooks/wallet-action/earn-fee-collect/failed webhook wallet_action.earn_fee_collect.failed Fired when a wallet action fails after at least one transaction was broadcast. There may be onchain state changes; inspect the steps for details. # Earn fee collect rejected Source: https://docs.privy.io/api-reference/webhooks/wallet-action/earn-fee-collect/rejected webhook wallet_action.earn_fee_collect.rejected Fired when a wallet action fails before any onchain effects (e.g. policy rejection, simulation failure). Safe to retry. # Earn fee collect succeeded Source: https://docs.privy.io/api-reference/webhooks/wallet-action/earn-fee-collect/succeeded webhook wallet_action.earn_fee_collect.succeeded Fired when all steps of a wallet action have been confirmed onchain. The action is complete. # Earn reward incentive claim created Source: https://docs.privy.io/api-reference/webhooks/wallet-action/earn-incentive-claim/created webhook wallet_action.earn_incentive_claim.created Fired when a wallet action is created and execution has begun. The action is now pending. # Earn reward incentive claim failed Source: https://docs.privy.io/api-reference/webhooks/wallet-action/earn-incentive-claim/failed webhook wallet_action.earn_incentive_claim.failed Fired when a wallet action fails after at least one transaction was broadcast. There may be onchain state changes; inspect the steps for details. # Earn reward incentive claim rejected Source: https://docs.privy.io/api-reference/webhooks/wallet-action/earn-incentive-claim/rejected webhook wallet_action.earn_incentive_claim.rejected Fired when a wallet action fails before any onchain effects (e.g. policy rejection, simulation failure). Safe to retry. # Earn reward incentive claim succeeded Source: https://docs.privy.io/api-reference/webhooks/wallet-action/earn-incentive-claim/succeeded webhook wallet_action.earn_incentive_claim.succeeded Fired when all steps of a wallet action have been confirmed onchain. The action is complete. # Earn withdraw created Source: https://docs.privy.io/api-reference/webhooks/wallet-action/earn-withdraw/created webhook wallet_action.earn_withdraw.created Fired when a wallet action is created and execution has begun. The action is now pending. # Earn withdraw failed Source: https://docs.privy.io/api-reference/webhooks/wallet-action/earn-withdraw/failed webhook wallet_action.earn_withdraw.failed Fired when a wallet action fails after at least one transaction was broadcast. There may be onchain state changes; inspect the steps for details. # Earn withdraw rejected Source: https://docs.privy.io/api-reference/webhooks/wallet-action/earn-withdraw/rejected webhook wallet_action.earn_withdraw.rejected Fired when a wallet action fails before any onchain effects (e.g. policy rejection, simulation failure). Safe to retry. # Earn withdraw succeeded Source: https://docs.privy.io/api-reference/webhooks/wallet-action/earn-withdraw/succeeded webhook wallet_action.earn_withdraw.succeeded Fired when all steps of a wallet action have been confirmed onchain. The action is complete. # Swap created Source: https://docs.privy.io/api-reference/webhooks/wallet-action/swap/created webhook wallet_action.swap.created Fired when a wallet action is created and execution has begun. The action is now pending. # Swap failed Source: https://docs.privy.io/api-reference/webhooks/wallet-action/swap/failed webhook wallet_action.swap.failed Fired when a wallet action fails after at least one transaction was broadcast. There may be onchain state changes; inspect the steps for details. # Swap rejected Source: https://docs.privy.io/api-reference/webhooks/wallet-action/swap/rejected webhook wallet_action.swap.rejected Fired when a wallet action fails before any onchain effects (e.g. policy rejection, simulation failure). Safe to retry. # Swap succeeded Source: https://docs.privy.io/api-reference/webhooks/wallet-action/swap/succeeded webhook wallet_action.swap.succeeded Fired when all steps of a wallet action have been confirmed onchain. The action is complete. # Transfer created Source: https://docs.privy.io/api-reference/webhooks/wallet-action/transfer/created webhook wallet_action.transfer.created Fired when a wallet action is created and execution has begun. The action is now pending. # Transfer failed Source: https://docs.privy.io/api-reference/webhooks/wallet-action/transfer/failed webhook wallet_action.transfer.failed Fired when a wallet action fails after at least one transaction was broadcast. There may be onchain state changes; inspect the steps for details. # Transfer rejected Source: https://docs.privy.io/api-reference/webhooks/wallet-action/transfer/rejected webhook wallet_action.transfer.rejected Fired when a wallet action fails before any onchain effects (e.g. policy rejection, simulation failure). Safe to retry. # Transfer succeeded Source: https://docs.privy.io/api-reference/webhooks/wallet-action/transfer/succeeded webhook wallet_action.transfer.succeeded Fired when all steps of a wallet action have been confirmed onchain. The action is complete. # Wallet archived Source: https://docs.privy.io/api-reference/webhooks/wallet/archived webhook wallet.archived Fired when a wallet is archived. # Funds deposited Source: https://docs.privy.io/api-reference/webhooks/wallet/funds_deposited webhook wallet.funds_deposited Fired when funds are deposited into a wallet. # Funds withdrawn Source: https://docs.privy.io/api-reference/webhooks/wallet/funds_withdrawn webhook wallet.funds_withdrawn Fired when funds are withdrawn from a wallet. # Private key export Source: https://docs.privy.io/api-reference/webhooks/wallet/private_key_export webhook wallet.private_key_export Fired when a user exports the private key of their embedded wallet. # Wallet recovered Source: https://docs.privy.io/api-reference/webhooks/wallet/recovered webhook wallet.recovered Fired when a user recovers their embedded wallet. # Wallet recovery setup Source: https://docs.privy.io/api-reference/webhooks/wallet/recovery_setup webhook wallet.recovery_setup Fired when a user sets up recovery for their embedded wallet. # Wallet restored Source: https://docs.privy.io/api-reference/webhooks/wallet/restored webhook wallet.restored Fired when a wallet is restored from archive. # Overview Source: https://docs.privy.io/authentication/overview Understand how Privy authenticates wallet access using user authentication and API authentication. Privy's wallet system supports granular controls on who can access wallets and what actions different users can perform. To enforce these controls, Privy's API must verify the identity of the party requesting a wallet action, ensuring that only authorized actions are executed by the system. This process is known as **authentication**. Privy authenticates individual users to provision access to wallets. images/auth-splash.png *** ## Individual user authentication Privy is a powerful toolkit for progressive authentication of individual end users. Fine-grained control over onboarding flows and wallet connections helps improve conversion and craft better UX. Your app can authenticate users across web2 and web3 accounts, using either an existing authentication provider or Privy's authentication system. ### Using Privy as the authentication provider If your app doesn't have an existing authentication provider, or needs a single provider for authentication and embedded wallets, your app can use Privy's authentication system, which supports both web2 and web3 accounts. Privy's client-side SDKs offers a variety of authentication methods, including email, SMS, passkey, socials (Google, Apple, Twitter, Farcaster, etc.), any OAuth system, and Ethereum and Solana wallets. ### Using an existing authentication provider If your app already has an authentication provider, Privy integrates with your app's [existing authentication system](/authentication/user-authentication/jwt-based-auth/overview). This includes any OIDC compliant authentication system, including OAuth 2.0, Auth0, Firebase, AWS Cognito, and more. Your app can integrate an existing authentication provider with Privy via the REST API or any of Privy's client-side SDKs. ## Authentication security levels Privy's login methods fall into two categories based on their trust model: ### Delegated authentication A third party controls the credential. Account access depends on that provider remaining available and cooperative. Examples: Google, Apple, Twitter, Discord, Telegram, email OTP, SMS ### Direct authentication The user owns the credential outright. No third party can revoke or suspend access. Examples: passkeys (WebAuthn), hardware keys (YubiKey), TOTP authenticator apps Account access is wallet access. If your app uses a delegated login method as the primary authenticator, Privy recommends requiring MFA with either a passkey or authenticator app. [Set up MFA →](/authentication/user-authentication/mfa/overview) | [Security checklist →](/security/implementation-guide/security-checklist) *** ## Get started Authenticate users using just their email address and a one-time passcode. Authenticate users with their externally owned Ethereum or Solana wallets. Require passkey or authenticator app MFA to protect delegated logins. Allow your users to sign into your Farcaster Mini App seamlessly with Privy. # Access tokens Source: https://docs.privy.io/authentication/user-authentication/access-tokens Verify Privy access tokens on your backend to authenticate API requests from your frontend When a user logs in to your app and becomes **authenticated**, Privy issues the user an app **access token**. This token is signed by Privy and cannot be spoofed. When your frontend makes a request to your backend, you should include the current user's access token in the request. This allows your server to determine whether the requesting user is truly authenticated or not. Looking to access user data? Check out our [Identity tokens](/user-management/users/identity-tokens#identity-tokens). *** ## Access token format Privy access tokens are [JSON Web Tokens (JWT)](https://jwt.io/introduction), signed with the ES256 algorithm. These JWTs include certain information about the user in their claims, namely: The user's current session ID The user's Privy DID The token issuer, which should always be [privy.io](https://privy.io) Your Privy app ID The timestamp of when the JWT was issued The timestamp of when the JWT will expire and is no longer valid. This is generally 1 hour after the JWT was issued. Read more about Privy's tokens and their security in our [security guide](/security/authentication/user-authentication). *** ## Sending the access token ### Accessing the token from your client To include the current user's access token in requests from your frontend to your backend, you'll first need to retrieve it, then send it appropriately. You can get the current user's Privy token as a string using the **`getAccessToken`** method from the **`usePrivy`** hook. This method will also automatically refresh the user's access token if it is nearing expiration or has expired. ```tsx theme={"system"} const { getAccessToken } = usePrivy(); const accessToken = await getAccessToken(); ``` If you need to get a user's Privy token *outside* of Privy's React context, you can directly import the **`getAccessToken`** method: ```tsx theme={"system"} import { getAccessToken } from '@privy-io/react-auth'; const authToken = await getAccessToken(); ``` When using direct imports, you must ensure **`PrivyProvider`** has rendered before invoking the method. Whenever possible, you should retrieve **`getAccessToken`** from the **`usePrivy`** hook. In React Native, you can use the `getAccessToken` method from the `PrivyClient` instance to retrieve the user's access token. ```tsx theme={"system"} const privy = createPrivyClient({ appId: '', clientId: '' }); const accessToken = await privy.getAccessToken(); ``` In Swift, you can use the `getAccessToken` method on the PrivyUser object to retrieve the user's access token. ```swift theme={"system"} // Check if user is authenticated guard let user = privy.user else { // If user is nil, user is not authenticated return } // Get the access token do { let accessToken = try await user.getAccessToken() print("Access token: \(accessToken)") } catch { // Handle error appropriately } ``` In Android, you can use the `getAccessToken` method on the PrivyUser object to retrieve the user's access token. ```kotlin theme={"system"} // Check if user is authenticated val user = privy.user if (user != null) { // Get the access token val result: Result = user.getAccessToken() // Handle the result with fold method result.fold( onSuccess = { accessToken -> println("Access token: $accessToken") }, onFailure = { error -> // Handle error appropriately }, ) } ``` In Flutter, you can use the `getAccessToken` method on the PrivyUser object to retrieve the user's access token. ```dart theme={"system"} // Check if user is authenticated final user = privy.user; if (user != null) { // Get the access token final result = await privy.user.getAccessToken(); // Handle the result with fold method result.fold( onSuccess: (accessToken) { print('Access token: $accessToken'); }, onError: (error) { // Handle error appropriately }, ); } ``` In Unity, you can use the `GetAccessToken` method on the `IPrivyUser` instance to retrieve the user's access token. ```csharp theme={"system"} // User will be null if no user is authenticated IPrivyUser user = await PrivyManager.Instance.GetUser(); if (user != null) { string accessToken = await user.GetAccessToken(); Debug.Log(accessToken); } ``` If your app is configured to use HTTP-only cookies (instead of the default local storage), the access token will automatically be included in the cookies for requests to the same domain. In this case, you don't need to manually include the token in the request headers. ### Using the access token with popular libraries When sending requests to your backend, here's how you can include the access token with different HTTP client libraries: ```tsx theme={"system"} // For bearer token approach (when using local storage) const accessToken = await getAccessToken(); const response = await fetch('', { method: 'POST', headers: { Authorization: `Bearer ${accessToken}`, 'Content-Type': 'application/json' }, body: JSON.stringify(data) }); // For HTTP-only cookies approach const response = await fetch('', { method: 'POST', headers: { 'Content-Type': 'application/json' }, credentials: 'include', // This includes cookies automatically body: JSON.stringify(data) }); ``` ```tsx theme={"system"} import axios from 'axios'; // For bearer token approach (when using local storage) const accessToken = await getAccessToken(); const response = await axios({ method: 'post', url: '', headers: { Authorization: `Bearer ${accessToken}` }, data: data }); // For HTTP-only cookies approach const response = await axios({ method: 'post', url: '', withCredentials: true, // This includes cookies automatically data: data }); ``` ```tsx theme={"system"} import {ofetch} from 'ofetch'; // For bearer token approach (when using local storage) const accessToken = await getAccessToken(); const response = await ofetch('', { method: 'POST', headers: { Authorization: `Bearer ${accessToken}` }, body: data }); // For HTTP-only cookies approach const response = await ofetch('', { method: 'POST', credentials: 'include', // This includes cookies automatically body: data }); ``` *** ## Getting the access token ### Accessing the token from your server When your server receives a request, the location of the user's access token depends on whether your app uses **local storage** (the default) or **cookies** to manage user sessions. If you're using local storage for session management, the access token will be passed in the `Authorization` header of the request with the `Bearer` prefix. You can extract it like this: ```tsx theme={"system"} // Example for Express.js const accessToken = req.headers.authorization?.replace('Bearer ', ''); // Example for Next.js API route const accessToken = req.headers.authorization?.replace('Bearer ', ''); // Example for Next.js App Router const accessToken = headers().get('authorization')?.replace('Bearer ', ''); ``` ```go theme={"system"} // Example for Go accessToken := r.Header.Get("Authorization") accessToken = strings.Replace(accessToken, "Bearer ", "", 1) ``` If you're using HTTP-only cookies for session management, the access token will be automatically included in the `privy-token` cookie. You can extract it like this: ```tsx theme={"system"} // Example for Express.js const accessToken = req.cookies['privy-token']; // Example for Next.js API route const accessToken = req.cookies['privy-token']; // Example for Next.js App Router const cookieStore = cookies(); const accessToken = cookieStore.get('privy-token')?.value; ``` ```go theme={"system"} // Example for Go accessToken := r.Cookies["privy-token"] ``` ## Verifying the access token Once you've obtained the user's access token from a request, you should verify the token against Privy's **verification key** for your app to confirm that the token was issued by Privy and the user referenced by the DID in the token is truly authenticated. The access token is a standard [ES256](https://datatracker.ietf.org/doc/html/rfc7518#section-3.1) [JWT](https://jwt.io) and the verification key is a standard [Ed25519](https://en.wikipedia.org/wiki/EdDSA#Ed25519) public key. You can verify the access token against the public key using the **`@privy-io/node`** library or using a third-party library for managing tokens. ### Using Privy SDK Pass the user's access token as a `string` to the **`PrivyClient`**'s **`verifyAccessToken`** method: ```ts @privy-io/node theme={"system"} // `privy` refers to an instance of the `PrivyClient` try { const verifiedClaims = await privy.utils().auth().verifyAccessToken({ access_token: accessToken }); } catch (error) { console.log(`Token verification failed with error ${error}.`); } ``` If the token is valid, **`verifyAccessToken`** will return an object with additional information about the request, with the fields below: | Parameter | Type | Description | | ------------ | -------- | ----------------------------------------------------------------------------- | | `appId` | `string` | Your Privy app ID. | | `userId` | `string` | The authenticated user's Privy DID. Use this to identify the requesting user. | | `issuer` | `string` | This will always be `'privy.io'`. | | `issuedAt` | `number` | Unix timestamp for when the access token was signed by Privy. | | `expiration` | `number` | Unix timestamp for when the access token will expire. | | `sessionId` | `string` | Unique identifier for the user's session. | If the token is invalid, **`verifyAccessToken`** will throw an error and you should **not** consider the requesting user authorized. This generally occurs if the token has expired or is invalid (e.g. corresponds to a different app ID). The Privy Client's `verifyAccessToken` method will make a request to Privy's API to fetch the verification key for your app. You can avoid this API request by copying your verification key from the **Configuration > App settings** page of the [**Dashboard**](https://dashboard.privy.io). ```ts @privy-io/node theme={"system"} const privy = new PrivyClient({ appId: 'your-privy-app-id', appSecret: 'your-privy-app-secret', // Set the copied verification key to use when creating the `PrivyClient` jwtVerificationKey: 'paste-your-verification-key-from-the-dashboard' }); const verifiedClaims = await privy.utils().auth().verifyAccessToken({ access_token: accessToken }); ``` ### Using JavaScript libraries You can also use common JavaScript libraries to verify tokens: To start, install `jose`: ```sh theme={"system"} npm i jose ``` Then, load your Privy public key using [`jose.importSPKI`](https://github.com/panva/jose/blob/main/docs/functions/key_import.importSPKI.md): ```tsx theme={"system"} const verificationKey = await jose.importSPKI( "insert-your-privy-verification-key", "ES256" ); ``` Lastly, using [`jose.jwtVerify`](https://github.com/panva/jose/blob/main/docs/functions/jwt_verify.jwtVerify.md), verify that the JWT is valid and was issued by Privy! ```tsx theme={"system"} const accessToken = "insert-the-users-access-token"; try { const payload = await jose.jwtVerify(accessToken, verificationKey, { issuer: "privy.io", audience: "insert-your-privy-app-id", }); console.log(payload); } catch (error) { console.error(error); } ``` If the JWT is valid, you can extract the JWT's claims from the [`payload`](https://github.com/panva/jose/blob/main/docs/interfaces/types.JWTPayload.md). For example, you can use `payload.sub` to get the user's Privy DID. If the JWT is invalid, this method will throw an error. To start, install `jsonwebtoken`: ```sh theme={"system"} npm i jsonwebtoken ``` Then, load your Privy public key as a string. ```tsx theme={"system"} const verificationKey = "insert-your-privy-verification-key".replace( /\\n/g, "\n" ); ``` The `replace` operation above ensures that any instances of `'\n'` in the stringified public key are replaced with actual newlines, per the PEM-encoded format. Lastly, verify the JWT using [`jwt.verify`](https://github.com/auth0/node-jsonwebtoken#jwtverifytoken-secretorpublickey-options-callback): ```tsx theme={"system"} const accessToken = 'insert-the-user-access-token-from-request'; try { const decoded = jwt.verify(accessToken, verificationKey, { issuer: 'privy.io', audience: /* your Privy App ID */ }); console.log(decoded); } catch (error) { console.error(error); } ``` If the JWT is valid, you can extract the JWT's claims from `decoded`. For example, you can use `decoded.sub` to get the user's Privy DID. If the JWT is invalid, this method will throw an error. For Go, the [`golang-jwt`](https://github.com/golang-jwt/jwt) library is a popular choice for token verification. To start, install the library: ```sh theme={"system"} go get -u github.com/golang-jwt/jwt/v5 ``` Next, load your Privy verification key and app ID as strings: ```go theme={"system"} verificationKey := "insert-your-privy-verification-key" appId := "insert-your-privy-app-id" ``` Then, parse the claims from the JWT and verify that they are valid: ```go theme={"system"} accessToken := "insert-the-users-access-token" // Defining a Go type for Privy JWTs type PrivyClaims struct { AppId string `json:"aud,omitempty"` Expiration uint64 `json:"exp,omitempty"` Issuer string `json:"iss,omitempty"` UserId string `json:"sub,omitempty"` } // This method will be used to check the token's claims later func (c *PrivyClaims) Valid() error { if c.AppId != appId { return errors.New("aud claim must be your Privy App ID.") } if c.Issuer != "privy.io" { return errors.New("iss claim must be 'privy.io'") } if c.Expiration < uint64(time.Now().Unix()) { return errors.New("Token is expired."); } return nil } // This method will be used to load the verification key in the required format later func keyFunc(token *jwt.Token) (interface{}, error) { if token.Method.Alg() != "ES256" { return nil, fmt.Errorf("Unexpected JWT signing method=%v", token.Header["alg"]) } // https://pkg.go.dev/github.com/dgrijalva/jwt-go#ParseECPublicKeyFromPEM return jwt.ParseECPublicKeyFromPEM([]byte(verificationKey)), nil } // Check the JWT signature and decode claims // https://pkg.go.dev/github.com/dgrijalva/jwt-go#ParseWithClaims token, err := jwt.ParseWithClaims(accessToken, &PrivyClaims{}, keyFunc) if err != nil { fmt.Println("JWT signature is invalid.") } // Parse the JWT claims into your custom struct privyClaim, ok := token.Claims.(*PrivyClaims) if !ok { fmt.Println("JWT does not have all the necessary claims.") } // Check the JWT claims err = Valid(privyClaim); if err { fmt.Printf("JWT claims are invalid, with error=%v.", err); fmt.Println(); } else { fmt.Println("JWT is valid.") fmt.Printf("%v", privyClaim) } ``` If the JWT is valid, you can access its claims, including the user's DID, from the `privyClaim` struct above. If the JWT is invalid, an error will be thrown. For Rust, the [`jsonwebtoken`](https://github.com/Keats/jsonwebtoken) crate is a popular choice for token verification. To start, add it to your dependencies: ```toml theme={"system"} [dependencies] jsonwebtoken = "9" serde = { version = "1.0", features = ["derive"] } ``` Next, load your Privy verification key and app ID as strings: ```rust theme={"system"} let verification_key = "insert-your-privy-verification-key"; let app_id = "insert-your-privy-app-id"; ``` Then, parse the claims from the JWT and verify that they are valid: ```rust theme={"system"} use jsonwebtoken::{decode, DecodingKey, Validation, Algorithm}; use serde::{Deserialize, Serialize}; let access_token = "insert-the-users-access-token"; // Defining a Rust struct for Privy JWTs #[derive(Debug, Serialize, Deserialize)] struct PrivyClaims { aud: String, // App ID exp: u64, // Expiration timestamp iss: String, // Issuer sub: String, // User ID (Privy DID) sid: String, // Session ID iat: u64, // Issued at timestamp } // Configure validation settings let mut validation = Validation::new(Algorithm::ES256); validation.set_issuer(&["privy.io"]); validation.set_audience(&[app_id]); // Parse the verification key let decoding_key = DecodingKey::from_ec_pem(verification_key.as_bytes())?; // Verify the JWT and decode claims match decode::(&access_token, &decoding_key, &validation) { Ok(token_data) => { let claims = token_data.claims; println!("JWT is valid"); println!("User ID: {}", claims.sub); println!("App ID: {}", claims.aud); println!("Session ID: {}", claims.sid); // Use the claims for your application logic } Err(err) => { eprintln!("JWT verification failed: {}", err); // Handle invalid token } } ``` If the JWT is valid, you can access its claims, including the user's DID, from the `claims` struct above. If the JWT is invalid, an error will be returned with details about what went wrong. ## Managing expired access tokens A user's access token might expire while they are actively using your app. For example, if a user does not take action on an application for an extended period of time, the access token can become expired. * **Handle invalid token errors**: In these scenarios, if a method returns with an **`'invalid auth token'`** error, we recommend calling the **`getAccessToken`** method with a time-based backoff until the user's access token is refreshed with an updated expiration time. * **Return errors from backend**: If you receive an expired access token in your backend, return an error to your client, and as above, trigger **`getAccessToken`** in your client. * **Handle failed refreshes**: If the user's access token cannot be refreshed, the user will be logged out. # Authentication state Source: https://docs.privy.io/authentication/user-authentication/authentication-state Check if a user is authenticated using the usePrivy hook and gate UI experiences based on auth status Throughout your app, you may want to gate certain user experiences based on whether the current user is authenticated or not. Privy makes it easy to check your user's authentication status and handle it appropriately. You can use the boolean `authenticated` from the `usePrivy` hook to determine if your user is authenticated or not. ```tsx theme={"system"} authenticated: boolean; ``` Before determining a user's auth status from Privy, you should verify that Privy has fully initialized and is **`ready`** ### Usage ```tsx theme={"system"} import { useRouter } from "next/router"; import { usePrivy } from "@privy-io/react-auth"; export default function MyComponent() { const { ready, authenticated, user } = usePrivy(); const router = useRouter(); if (!ready) { // Do nothing while the PrivyProvider initializes with updated user state return <>; } if (ready && !authenticated) { // Replace this code with however you'd like to handle an unauthenticated user // As an example, you might redirect them to a login page router.push("/login"); } if (ready && authenticated) { // Replace this code with however you'd like to handle an authenticated user return

User {user?.id} is logged in.

; } } ```
You can use the `user` object from the `usePrivy` hook to determine if your user is authenticated or not. ```tsx theme={"system"} user: User | null; ``` ### Usage ```tsx theme={"system"} import { useRouter } from "expo-router"; import { usePrivy } from "@privy-io/expo"; import { useEffect } from "react"; import { Text, View } from "react-native"; export default function MyComponent() { const { isReady, user } = usePrivy(); const router = useRouter(); useEffect(() => { if (isReady && !user) { // Replace this code with however you'd like to handle an unauthenticated user // As an example, you might redirect them to a login page router.replace("/login"); } }, [isReady, user, router]); if (!isReady) { // Do nothing while the PrivyProvider initializes with updated user state return null; } if (isReady && !user) { // You could show a loading state or handle this differently return Not authenticated; } if (isReady && user) { // Replace this code with however you'd like to handle an authenticated user return ( User {user.id} is logged in. ); } } ``` The `AuthState` enum is used to describe the user's authenticated state. ```swift theme={"system"} public enum AuthState { /// AuthState has not been determined yet, show loading case notReady /// User is unauthenticated case unauthenticated /// Auth state cannot be determined while no network connectivity is available, but session tokens exist in cache. A call to get privy.getUser() would return null if auth state is authenticatedUnverified as this state confirms a prior user session exists, but can't be verified with the Privy backend. case authenticatedUnverified(AuthenticatedUnverifiedContext) /// User is authenticated and has an associated PrivyUser object case authenticated(PrivyUser) } ``` ### Handling no network connectivity at SDK initialization: When the Privy SDK is initialized while there is no network connectivity, Privy will first check if a prior user session is persisted. If there is not, the auth state will be set to `AuthState.unauthenticated`. If there is, Privy can't verify that validity of the prior session without network connectivity. Thus, the auth state will be set to `AuthState.authenticatedUnverified`. Privy will automatically attempt to confirm the user's authenticated state when network connectivity is restored. Alternatively, you may explicitly call `Privy.onNetworkRestored()` once you determine network is restored. ### Usage There are various ways to determine user's auth state, outlined below: #### 1. Grab the user's current auth state ```swift theme={"system"} public protocol Privy { /// Get the user's current auth state. func getAuthState() async -> AuthState } ``` ```swift theme={"system"} // Grab current auth state if case .authenticated(let user) = await privy.getAuthState() { // User is authenticated. Grab the user's linked accounts let linkedAccounts = user.linkedAccounts } ``` #### 2. Subscribe to auth state updates Auth state is exposed as an AsyncStream on the Privy object: ```swift theme={"system"} public protocol Privy { /// An AsyncStream that emits auth state changes. var authStateStream: AsyncStream { get } } ``` ```swift theme={"system"} let task = Task { for await authState in privy.authStateStream { switch authState { case .authenticated(let user): // User is authenticated. Grab the user's linked accounts let linkedAccounts = user.linkedAccounts case .notReady: // Privy was just initialized and has not determined auth state yet case .authenticatedUnverified: // Prior user session exists, but can't be refreshed / verified. Likely due to network connectivity. case .unauthenticated: // User in not authenticated. Perhaps show login screen. } } } ``` #### 3. Directly grab the User As a convenience, you can grab the user object directly from the Privy instance. If the user is not null, there is an authenticated user. ```swift theme={"system"} let privyUser = await privy.getUser() if (privyUser != null) { // User is authenticated let linkedAccounts = privyUser.linkedAccounts } ``` The `AuthState` sealed type is used to describe the user's authenticated state. ```kotlin theme={"system"} public sealed interface AuthState { // AuthState has not been determined yet, show loading public data object NotReady : AuthState // User is unauthenticated public data object Unauthenticated : AuthState // Auth state cannot be determined while no network connectivity is available, but session tokens exist in cache. A call to get privy.getUser() would return null if auth state is authenticatedUnverified as this state confirms a prior user session exists, but can't be verified with the Privy backend. public data class AuthenticatedUnverified(/* */) : AuthState // User is authenticated and has an associated PrivyUser object public data class Authenticated(val user: PrivyUser) : AuthState } ``` ### Handling no network connectivity at SDK initialization: When the Privy SDK is initialized while there is no network connectivity, Privy will first check if a prior user session is persisted. If there is not, auth state will be set to `AuthState.Unauthenticated`. If there is, Privy can't verify that validity of the prior session without network connectivity. Thus, auth state will be set to `AuthState.AuthenticatedUnverified`. Privy will automatically attempt to confirm the user's authenticated state when network connectivity is restored. Alternatively, you may explicitly call `Privy.onNetworkRestored()` once you determine network is restored. ### Usage There are various ways to determine a user's auth state: #### 1. Grab the user's current auth state ```kotlin theme={"system"} coroutineScope.launch { val authState = privy.getAuthState() if (authState is AuthState.Authenticated) { // User is authenticated. Grab the user's linked accounts val privyUser = currentAuthState.user val linkedAccount = privyUser.linkedAccounts } } ``` #### 2. Subscribe to auth state updates Auth state is exposed as a StateFlow on the Privy object: ```kotlin theme={"system"} public interface Privy { // A state flow that can be subscribed to for auth state updates public val authState: StateFlow } ``` ```kotlin theme={"system"} coroutineScope.launch { privy.authState.collectLatest { authState -> when(authState) { is AuthState.Authenticated -> { // User is authenticated. Grab the user's linked accounts val privyUser = authState.user val linkedAccounts = privyUser.linkedAccounts } AuthState.NotReady -> { // Privy was just initialized and has not determined auth state yet } is AuthState.AuthenticatedUnverified -> { // Prior user session exists, but can't be verified due to no network connectivity. } AuthState.Unauthenticated -> { // User in not authenticated. Perhaps show login screen. } } } } ``` #### 3. Directly grab the User As a convenience, you can grab the user object directly from the Privy instance. If the user is not null, there is an authenticated user. ```kotlin theme={"system"} coroutineScope.launch { val privyUser = privy.getUser() if (privyUser != null) { // User is authenticated val linkedAccounts = privyUser.linkedAccounts } } ``` The `AuthState` enum is used to describe the user's authenticated state. ```csharp theme={"system"} public enum AuthState { NotReady, // Privy has not yet finished initializing Unauthenticated, // User is unauthenticated Authenticated // User is authenticated } ``` ### Usage There are various ways to determine a user's auth state, outlined below: #### 1. Grab the user's current auth state ```csharp theme={"system"} public interface IPrivy { // Get the user's current authentication state. Task GetAuthState(); } ``` ```csharp theme={"system"} var authState = await PrivyManager.Instance.GetAuthState(); Debug.Log(authState); ``` #### 2. Subscribe to auth state updates You can also subscribe to `AuthState` updates via the `AuthStateChanged` event. ```csharp theme={"system"} public interface IPrivy { // Event that fires when the authentication state changes. event Action AuthStateChanged; } ``` ```csharp theme={"system"} PrivyManager.Instance.AuthStateChanged += authState => { // User's authentication state has updated Debug.Log(authState); }; ``` #### 3. Directly grab the User As a convenience, you can grab the user object directly from the Privy instance. If the user is not null, there is an authenticated user. ```csharp theme={"system"} public interface IPrivy { // Get the current user. Task GetUser(); } ``` ```csharp theme={"system"} var privyUser = await PrivyManager.Instance.GetUser(); if (privyUser != null) { var linkedAccounts = privyUser.LinkedAccounts; } ``` A user's authentication state is described by the AuthState sealed class. ```dart theme={"system"} /// Base class representing different authentication states. sealed class AuthState { const AuthState(); } /// Represents the initial state before authentication status is determined. class NotReady extends AuthState { const NotReady(); } /// Represents the state when the user is not authenticated. class Unauthenticated extends AuthState { const Unauthenticated(); } /// Represents the state when the user is authenticated. class Authenticated extends AuthState { final PrivyUser user; /// Constructor accepting the authenticated user's details. const Authenticated(this.user); } ``` The current auth state and an auth state stream are accessible directly on the Privy object. ```dart theme={"system"} abstract interface class Privy { // Get the current authentication state. AuthState get currentAuthState; // A stream for auth state updates. Stream get authStateStream; } ``` ### Accessing authentication state There are various ways to determine user's auth state, outlined below. Mix and match to fit the needs of your application. #### 1. Directly retrieve the user As a convenience, you can grab the user object directly from the Privy instance. If the user is not null, there is an authenticated user. ```dart theme={"system"} final privyUser = privy.user; if (privyUser != null) { // User is authenticated final linkedAccounts = privyUser.linkedAccounts; } ``` #### 2. Retrieve the current auth state ```dart theme={"system"} // Grab current auth state final currentAuthState = privy.currentAuthState; if (currentAuthState is Authenticated) { // User is authenticated. Retrieve the associated user from the auth state. final privyUser = currentAuthState.user; final linkedAccounts = privyUser.linkedAccounts; } ``` #### 3. Subscribe to auth state updates ```dart theme={"system"} privy.authStateStream.listen((authState) { switch (authState) { case Authenticated(): // User is authenticated. Retrieve the user. final privyUser = authState.user; final userId = privyUser.linkedAccounts; break; case NotReady(): // Privy is not yet ready. Ensure Privy is initialized first. break; case Unauthenticated(): // User is not authenticated. You may want to show the login screen. break; } }); ``` # CAPTCHA on login Source: https://docs.privy.io/authentication/user-authentication/captcha Add CAPTCHA verification to your Privy login flow to prevent bots and automated abuse Privy supports adding CAPTCHA to your login flow to prevent botting. Enable CAPTCHA in the [Privy Dashboard](https://dashboard.privy.io/apps?page=settings\&setting=advanced) before implementing this feature. CAPTCHA providers load content in the browser. Your app may need to update its Content Security Policy to allow CAPTCHA content. See the [Content Security Policy guide](/security/implementation-guide/content-security-policy#optional-features) for required directives. Once CAPTCHA is enabled, import the `Captcha` component and place it as a peer to your login form: *(When this component mounts, it will execute the invisible CAPTCHA.)* ```tsx theme={"system"} import {Captcha, useLoginWithEmail} from '@privy-io/react-auth'; const MyLoginForm = () => { const [email, setEmail] = useState(''); const {sendCode, loginWithCode} = useLoginWithEmail(); const handleSendCode = async () => { try { await sendCode(email); } catch (err) { // Captcha failures due to timeout or otherwise will show up here // in addition to possible network errors from the sendCode request // // The `sendCode` method from `useLoginWithSms` and `initOAuth` method // from `useLoginWithOAuth` work exactly the same way. } }; return ( <> setEmail(e.target.value)} /> ); }; ``` **That's it! Whenever a user tries to log into your app, Privy will pre-validate the attempt with an invisible CAPTCHA.** 🎉 ## Captcha providers hCaptcha is supported by the React SDK starting in version `react-auth@3.9.0`. Privy supports [hCaptcha](https://www.hcaptcha.com/) and Cloudflare's [Turnstile](https://www.cloudflare.com/products/turnstile/) as CAPTCHA providers. Both providers enable invisible CAPTCHA verification during login. ### hCaptcha risk tolerance When using hCaptcha, your app can configure a risk tolerance level in the [Privy Dashboard](https://dashboard.privy.io/apps?page=settings\&setting=advanced). This setting determines how strictly Privy blocks suspected bot behavior. By default, Privy uses a balanced setting that only blocks traffic above a mid-range [risk score](https://docs.hcaptcha.com/ent_overview/#real-time-risk-scoring). # Using your own authentication provider Source: https://docs.privy.io/authentication/user-authentication/jwt-based-auth/overview Integrate any JWT-based authentication provider (Auth0, Firebase, Cognito, OIDC) with Privy embedded wallets Privy supports all JWT-based authentication providers. This includes any OIDC compliant authentication system, including OAuth 2.0, Auth0, Firebase, AWS Cognito, and more. Using JWT-based authentication integration, you can use your existing authentication system with Privy's services. This approach allows users to maintain their existing login experience while giving them access to embedded wallets. Privy's authentication is fully compatible with any authentication provider that supports [JWT-based](https://jwt.io/), [stateless](https://auth0.com/blog/stateless-auth-for-stateful-minds/) authentication. When a user logs into your app, your auth provider issues them an access and/or identity token to represent their auth status. Privy validates this token to authenticate your user. JWT-based auth splash # Configuring your authentication provider Source: https://docs.privy.io/authentication/user-authentication/jwt-based-auth/setup Configure your JWT-based authentication provider in the Privy Dashboard for custom auth integration To integrate your authentication provider with Privy: 1. Go to the [**Privy Dashboard**](https://dashboard.privy.io) 2. Select your app from the **App Dropdown** in the left sidebar 3. Request access to **Custom authentication** in the [Integrations > Built-in](https://dashboard.privy.io/apps?page=integrations) tab of the Privy dashboard 4. Navigate to the [JWT Dashboard](https://dashboard.privy.io/apps?logins=basics\&page=login-methods) via User management > Authentication > JWT integration JWT-based auth You'll need to provide the following information: Choose where JWT-authenticated requests can originate from: client-side (end user devices), server-side (your backend), or both. **Server-side only is recommended** if your app exclusively authenticates with Privy from your backend. Allowing client-side requests means an end user with a valid JWT can call Privy directly, bypassing any additional checks your server performs before authenticating. For example, if your server validates subscription status, enforces rate limits, or checks permissions before calling Privy on behalf of a user, a client-side request with the same JWT would skip all of those checks. Only enable client-side if your app uses a Privy client SDK to authenticate users directly from the browser or a mobile app. Privy requires a verification key to ensure the JWTs received are valid. Both the token's signature and its expiration time ([claim](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4)) are verified to ensure secure access. This verification process helps protect user data and prevents unauthorized access to Privy services. You can provide the verification key in one of two ways: If your provider uses [JWKS](https://auth0.com/docs/secure/tokens/json-web-tokens/json-web-key-sets) to sign JWTs, provide a JWKS endpoint URL where Privy can retrieve your auth provider's JWT public key. ```json theme={"system"} { "keys": [ { "kty": "XXX", "n": "XXX", "e": "XXX", "alg": "XXX", // "RS256" or "ES256" "kid": "XXX" // ... } ] } ``` If your provider uses a single key to sign JWTs, provide the corresponding public key certificate used for verification. ```json theme={"system"} -----BEGIN CERTIFICATE----- // Public key -----END CERTIFICATE----- ``` Enter the claim from your authentication service's JWTs that contains the ID for the user **in Privy.** This claim should be the unique user ID claim, typically `sub`. # Integrating your authentication provider with Privy Source: https://docs.privy.io/authentication/user-authentication/jwt-based-auth/usage Pass your auth provider JWT to Privy to authenticate users and access embedded wallets Follow the guide below to integrate your authentication provider with Privy. ### Implementation To integrate JWT-based authentication with Privy in your React application, you'll need to use the `useSubscribeToJwtAuthWithFlag` hook to subscribe the Privy SDK to your auth provider's state. #### Getting the state from your auth provider To get the state from your auth provider, import the provider's hook. ```tsx theme={"system"} // Import your auth provider's hook or state management import { useAuth } from 'your-auth-provider'; // Get auth details from your provider const { getToken, isLoading, isAuthenticated } = useAuth(); ``` #### Subscribing to the auth provider's state In a component that lives below both `PrivyProvider`, and your custom auth provider, call the `useSubscribeToJwtAuthWithFlag` hook to subscribe the Privy SDK to your auth provider's state. ```tsx theme={"system"} import { useAuth } from 'your-auth-provider'; import { useSubscribeToJwtAuthWithFlag } from '@privy-io/react-auth'; const MyStateSyncComponent = () => { const { getToken, isLoading, isAuthenticated } = useAuth(); useSubscribeToJwtAuthWithFlag({ isAuthenticated, isLoading, getExternalJwt: async () => { if (isAuthenticated) { const token = await getToken(); return token; } } }) return null; } ``` This hook will observe state from your auth provider and update the Privy SDK's authentication state accordingly. The hook itself (and the `MyStateSyncComponent` component) should be mounted throughout the lifetime of your app to ensure state is kept in sync. #### Integrate the provider with your app Make sure to nest your custom provider inside your auth provider in your app structure: ```tsx App.tsx theme={"system"} import { AuthProvider } from 'your-auth-provider'; import PrivyAuthProvider from './PrivyAuthProvider'; function App() { return ( {/* Invocation of `useSubscribeToJwtAuthWithFlag` must be below both providers */} ); } export default App; ``` #### Disabling the external auth provider If you want to disable the external auth provider, you can set the `enabled` flag to `false` in the hook configuration. ```tsx theme={"system"} useSubscribeToJwtAuthWithFlag({ enabled: false, isAuthenticated, // ... }); ``` Setting the `enabled` flag to `false` will disable the external auth provider and will stop Privy from attempting to synchronize its state with the external auth provider regardless of the value of the `isAuthenticated` flag, until `enabled` is set to `true` again. ### Advanced Usage This approach is **not recommended for most use cases**, as it increases the complexity of setup significantly and can result in state synchronization issues if used incorrectly. Always prefer the flag-based approach when possible if your auth provider offers an `isAuthenticated` flag. For more advanced usage, or in cases where your auth provider lives outside React or otherwise offers no `isAuthenticated` flag, you can use the `useSyncJwtBasedAuthState` hook to subscribe to the auth provider's state via state listeners. Let's say the library for your auth provider exports an `authStore` object that holds state. ```ts theme={"system"} import { authStore } from 'your-auth-provider'; ``` This object has a `subscribe` method that takes a callback, and invokes it every time the auth state changes, most importantly when the user either logs in or out. ```ts theme={"system"} authStore.subscribe(() => { console.log('Auth state changed'); }); ``` The store object also has a `getState` method that returns the current state, which we can use to get the current JWT token whenever necessary. ```ts theme={"system"} const authState = authStore.getState(); console.log('Is authenticated:', authState.isAuthenticated); console.log('JWT token:', authState.token); ``` By using the `useSyncJwtBasedAuthState` hook, we can link Privy to the auth provider's state store by using those two methods. ```ts theme={"system"} import { useSyncJwtBasedAuthState } from '@privy-io/react-auth'; import { authStore } from 'your-auth-provider'; useSyncJwtBasedAuthState({ subscribe: (onAuthStateChange) => { const unsubscribe = authStore.subscribe((state) => { onAuthStateChange(); // Notify Privy of the auth state change. }); return unsubscribe; // Return the `unsubscribe` to avoid memory leaks. }, getExternalJwt: () => { const authState = authStore.getState(); if (authState.isAuthenticated) { return authState.token; } } }) ``` ### Accessing User Authentication Status Once configured, you can access the user's authentication status through the Privy SDK: ```tsx theme={"system"} import { usePrivy } from '@privy-io/react-auth'; function MainContent() { const { user, ready, authenticated } = usePrivy(); if (!ready) { return
Loading...
; } if (!authenticated) { return
Please log in through your authentication provider
; } return (

Welcome, authenticated user!

User ID: {user.id}

); } ``` When using a custom authentication provider, you should not use the Privy `login` method (from `useLogin` or `usePrivy`). Instead, call the login method of your custom provider, and the Privy SDK will automatically synchronize its state.
### Implementation To integrate JWT-based authentication with Privy in your React Native application, you'll need to create a custom `PrivyProvider` wrapper that supplies your auth token to Privy. #### Create a custom `PrivyProvider` wrapper Create a component that wraps the `PrivyProvider` with your custom auth configuration: ```tsx PrivyAuthProvider.tsx theme={"system"} import { useCallback, PropsWithChildren } from 'react'; import { PrivyProvider } from '@privy-io/expo'; // Import your auth provider's hook or state management import { useAuth0 } from 'react-native-auth0'; const PrivyAuthProvider: React.FC = ({ children }) => { // Get auth details from your auth provider const { user: auth0User, isLoading, getCredentials } = useAuth0(); // Create a callback to get the token const getCustomToken = useCallback(async () => { // Your logic to retrieve the JWT token from your auth provider try { const creds = await getCredentials(); return creds?.idToken; } catch (error) { // If there's an error, the user is likely not authenticated return undefined; } }, [isLoading, auth0User, getCredentials]); // Re-create when auth state changes return ( {children} ); }; export default PrivyAuthProvider; ``` #### Integrate the provider with your app Make sure to nest your custom provider inside your auth provider in your app structure: ```tsx App.tsx theme={"system"} import { Auth0Provider } from 'react-native-auth0'; import PrivyAuthProvider from './PrivyAuthProvider'; function App() { return ( {/* Our custom wrapper must be nested inside your AuthProvider */} {/* Your app content */} ); } export default App; ``` ### Accessing User Authentication Status Once configured, you can access the user's authentication status through the Privy SDK: ```tsx theme={"system"} import { usePrivy } from '@privy-io/expo'; import { View, Text } from 'react-native'; function MainContent() { const { user, ready } = usePrivy(); if (!ready) { return Loading...; } if (!user) { return Please log in through your authentication provider; } return ( Welcome, authenticated user! User ID: {user.id} ); } ``` When using a custom authentication provider in React Native, you should let your auth provider handle the authentication flow. Privy will automatically synchronize its state based on the token provided by your `getCustomAccessToken` callback. ### Implementation To integrate JWT-based authentication with Privy in your Swift application, you'll need to initialize the Privy SDK with a token provider callback and handle authentication. #### Initialize Privy with a token provider callback First, initialize the Privy SDK with a `tokenProvider` callback that will provide the JWT from your custom auth provider: ```swift Privy initialization with custom auth theme={"system"} let privy = PrivyConfig( appId: "YOUR_APP_ID", appClientId: "YOUR_APP_CLIENT_ID", customAuthConfig: PrivyLoginWithCustomAuthConfig { // Client logic to provide the JWT // This might involve network requests or accessing secure storage return await fetchAccessTokenFromAuthProvider() } ) ``` ```swift Example token provider implementation theme={"system"} private func fetchAccessTokenFromAuthProvider() async throws -> String? { // Your custom logic to retrieve the JWT token // This might be from shared preferences, secure storage, or an API call try await yourAuthManager.getAccessToken() } ``` This `tokenProvider` callback should: * Return the current user's access token as a `String` when authenticated * Return `nil` when the user is not authenticated #### Authenticate your user Once you have defined a `tokenProvider` callback, authenticate your user with Privy using the `loginWithCustomAccessToken` method: ```swift Authenticating with Privy theme={"system"} do { try await privy.customJwt.loginWithCustomAccessToken() // User is now authenticated with Privy } catch { // Handle authentication errors print("Failed to authenticate with Privy: \(error)") } ``` If the provided token is valid, Privy will successfully authenticate your user. If the token is invalid, this method will throw an error. #### Example with Auth0 Here's an example using Auth0's Swift SDK for authentication: ```swift Auth0 Integration Example theme={"system"} // Store the Auth0 token var auth0Token: String? = nil // Set up the token provider to return the stored token let config = PrivyConfig( appId: "YOUR_APP_ID", appClientId: "YOUR_APP_CLIENT_ID", customAuthConfig: PrivyLoginWithCustomAuthConfig { return auth0Token } ) // Handle Auth0 authentication Auth0.webAuth().start { result in if case .success(let credentials) = result { auth0Token = credentials.accessToken Task { do { // Authenticate with Privy using the token try await privy.customJwt.loginWithCustomAccessToken() // Now the user is authenticated with Privy // You can access their wallet and other features } catch { print("Privy authentication failed: \(error)") } } } else { print("Auth0 authentication failed") } } ``` ### Authentication Flow When using custom authentication with the Swift SDK: 1. When the Privy SDK is first initialized, it attempts to restore any prior session 2. If a prior session exists, Privy automatically tries to reauthenticate using your `tokenProvider` 3. You can manually trigger authentication by calling `loginWithCustomAccessToken` 4. After successful authentication, you have access to the `PrivyUser` object and wallet functionality When your app starts up, as soon as you determine your user is authenticated via your custom auth provider, you should call Privy's `loginWithCustomAccessToken` method to synchronize the authentication state. ### Accessing User Data Once authenticated, you can access the user's data and embedded wallets: ```swift theme={"system"} // Check if user is authenticated if let user = privy.user { // Access user information let userId = user.id // Access embedded wallets if let wallet = user.embeddedEthereumWallets.first { let walletAddress = wallet.address print("User has Ethereum wallet with address: \(walletAddress)") } } ``` Privy identifies users based on the unique ID assigned by your auth provider (stored in the `sub` claim of their access token). You can view all users in the **Users** section of the Privy Developer Dashboard. ### Implementation To integrate JWT-based authentication with Privy in your Android application, you'll need to initialize the Privy SDK with a token provider callback and handle authentication. #### Initialize Privy with a token provider callback First, initialize the Privy SDK with a `tokenProvider` callback that will provide the JWT from your custom auth provider: ```kotlin Privy initialization with custom auth theme={"system"} private val privy: Privy = Privy.init( context = applicationContext, // Be sure to only pass in Application context config = PrivyConfig( appId = "YOUR_APP_ID", appClientId = "YOUR_APP_CLIENT_ID", logLevel = PrivyLogLevel.NONE, customAuthConfig = LoginWithCustomAuthConfig( tokenProvider = { // Return the user's access token if they're authenticated // Or return null if they're not authenticated fetchTokenFromAuthProvider() } ) ) ) // Example token provider implementation private suspend fun fetchTokenFromAuthProvider(): String? { return try { // Your custom logic to retrieve the JWT token // This might be from shared preferences, secure storage, or an API call yourAuthManager.getAccessToken() } catch (e: Exception) { // If there's an error, the user is likely not authenticated null } } ``` The `tokenProvider` callback should: * Return the current user's access token as a `String` when authenticated * Return `null` when the user is not authenticated * Be implemented as a suspending function that can perform asynchronous operations #### Authenticate your user Once you've initialized Privy with a `tokenProvider` callback, authenticate your user with Privy using the `loginWithCustomAccessToken` method: ```kotlin Authenticating with Privy theme={"system"} // Make sure to call this in a coroutine scope val privyLoginResult = privy.customAuth.loginWithCustomAccessToken() privyLoginResult.fold( onSuccess = { user -> Log.d("Privy", "Privy login success! User: ${user}") // Now you can access user information and wallet functionality }, onFailure = { error -> Log.d("Privy", "Privy login failure! $error") // Handle authentication error } ) ``` If the provided access or identity token is valid, Privy will authenticate your user and return a `Result.success` with the `PrivyUser` object. If the token is invalid, it will return a `Result.failure`. #### Example integration with Auth0 Here's an example of integrating with Auth0 for Android: ```kotlin Auth0 Integration Example theme={"system"} private val auth0 = Auth0( clientId = "YOUR_AUTH0_CLIENT_ID", domain = "YOUR_AUTH0_DOMAIN" ) // Store the Auth0 token private var auth0Token: String? = null // Initialize Privy with token provider that returns the Auth0 token private val privy = Privy.init( context = applicationContext, config = PrivyConfig( appId = "YOUR_PRIVY_APP_ID", appClientId = "YOUR_PRIVY_APP_CLIENT_ID", customAuthConfig = LoginWithCustomAuthConfig( tokenProvider = { auth0Token } ) ) ) // Authenticate with Auth0, then with Privy private fun authenticateUser() { val callback = object : Callback { override fun onSuccess(credentials: Credentials) { // Store the token auth0Token = credentials.accessToken // Authenticate with Privy lifecycleScope.launch { val privyResult = privy.customAuth.loginWithCustomAccessToken() privyResult.fold( onSuccess = { user -> Log.d("Auth", "Successfully authenticated with Privy") // Proceed with authenticated user }, onFailure = { error -> Log.e("Auth", "Failed to authenticate with Privy", error) } ) } } override fun onFailure(error: AuthenticationException) { Log.e("Auth", "Auth0 authentication failed", error) } } // Start Auth0 authentication WebAuthProvider.login(auth0) .withScheme("demo") .start(this, callback) } ``` ### Authentication flow When using custom authentication with the Android SDK: 1. When the Privy SDK is first initialized, it attempts to restore any prior session 2. If a prior session exists, Privy automatically tries to reauthenticate using your `tokenProvider` 3. You can manually trigger authentication by calling `loginWithCustomAccessToken` 4. After successful authentication, you have access to the `PrivyUser` object and wallet functionality It's important to await the `privy.awaitReady()` call before triggering any other Privy flows to ensure the SDK has completed initialization and attempted session restoration. ### Accessing user data and wallets Once authenticated, you can access the user's data and embedded wallets: ```kotlin theme={"system"} // Check if user is authenticated val user = privy.user if (user != null) { // Access user information val userId = user.id // Access embedded Ethereum wallets val ethereumWallets = user.embeddedEthereumWallets if (ethereumWallets.isNotEmpty()) { val walletAddress = ethereumWallets.first().address Log.d("Wallet", "User has Ethereum wallet with address: $walletAddress") } } ``` Privy identifies users based on the unique ID that your auth provider has assigned (stored in the `sub` claim of their access token). You can view all users in the **Users** section of the Privy Developer Dashboard. ### Implementation To integrate JWT-based authentication with Privy in your Flutter application, you'll need to initialize the Privy SDK with a token provider callback and handle authentication. #### Initialize Privy with a token provider callback First, initialize the Privy SDK with a `tokenProvider` callback that will provide the JWT from your custom auth provider: ```dart Privy initialization with custom auth theme={"system"} // Define a function to retrieve the token from your auth provider Future _retrieveCustomAuthAccessToken() async { // Implement logic to fetch the access token from your auth provider // Return the token if the user is authenticated, or null if not try { // Your custom logic to retrieve the JWT token // This might be from secure storage or an API call final token = await yourAuthService.getAccessToken(); return token; } catch (e) { // If there's an error, the user is likely not authenticated return null; } } // Initialize Privy with the token provider final privyConfig = PrivyConfig( appId: "YOUR_APP_ID", appClientId: "YOUR_APP_CLIENT_ID", logLevel: PrivyLogLevel.NONE, customAuthConfig: LoginWithCustomAuthConfig( tokenProvider: _retrieveCustomAuthAccessToken, ), ); final privy = Privy(config: privyConfig); ``` The `tokenProvider` callback should: * Return the current user's access token as a `String` when authenticated * Return `null` when the user is not authenticated * Be implemented as an async function that can perform asynchronous operations #### Await SDK readiness Before performing any operations with the SDK, make sure to await its readiness: ```dart Awaiting SDK readiness theme={"system"} // Wait for the SDK to be ready before proceeding await privy.awaitReady(); ``` This ensures that the SDK has completed initialization and attempted session restoration if a prior session exists. #### Authenticate your user Once you've initialized Privy with a `tokenProvider` callback, authenticate your user with Privy using the `loginWithCustomAccessToken` method: ```dart Authenticating with Privy theme={"system"} // Authenticate with Privy final result = await privy.customAuth.loginWithCustomAccessToken(); result.fold( onSuccess: (user) { print("Privy login success! User: ${user}"); // Now you can access user information and wallet functionality }, onFailure: (error) { print("Privy login failure! ${error.message}"); // Handle authentication error }, ); ``` If the provided access or identity token is valid, Privy will authenticate your user and return `Success()` with an encapsulated `PrivyUser`. If the token is invalid, it will return a `Failure()` with a PrivyException. #### Example integration with Firebase Auth Here's an example of integrating with Firebase Authentication: ```dart Firebase Auth Integration Example theme={"system"} import 'package:firebase_auth/firebase_auth.dart'; import 'package:privy_flutter/privy_flutter.dart'; class AuthService { final FirebaseAuth _auth = FirebaseAuth.instance; late final Privy _privy; // Initialize Privy with Firebase token provider Future initPrivy() async { final privyConfig = PrivyConfig( appId: "YOUR_PRIVY_APP_ID", appClientId: "YOUR_PRIVY_APP_CLIENT_ID", logLevel: PrivyLogLevel.NONE, customAuthConfig: LoginWithCustomAuthConfig( tokenProvider: _getFirebaseIdToken, ), ); _privy = Privy(config: privyConfig); // Wait for Privy to be ready await _privy.awaitReady(); } // Firebase token provider function Future _getFirebaseIdToken() async { try { final user = _auth.currentUser; if (user == null) return null; // Get the ID token return await user.getIdToken(); } catch (e) { print("Error getting Firebase ID token: $e"); return null; } } // Sign in with Firebase, then with Privy Future> signInWithEmailAndPassword(String email, String password) async { try { // Sign in with Firebase await _auth.signInWithEmailAndPassword( email: email, password: password, ); // After Firebase auth succeeds, authenticate with Privy return await _privy.customAuth.loginWithCustomAccessToken(); } catch (e) { return Result.failure(AuthError("Firebase authentication failed: $e")); } } } ``` ### Authentication flow When using custom authentication with the Flutter SDK: 1. When the Privy SDK is first initialized, it attempts to restore any prior session 2. If a prior session exists, Privy automatically tries to reauthenticate using your `tokenProvider` 3. You can manually trigger authentication by calling `loginWithCustomAccessToken` 4. After successful authentication, you have access to the `PrivyUser` object and wallet functionality It's important to `await privy.awaitReady()` before triggering any other Privy flows to ensure the SDK has completed initialization and attempted session restoration. ### Accessing user data and wallets Once authenticated, you can access the user's data and embedded wallets: ```dart theme={"system"} // Check if user is authenticated final user = privy.user; if (user != null) { // Access user information final userId = user.id; // Access embedded Ethereum wallets final ethereumWallets = user.embeddedEthereumWallets; if (ethereumWallets.isNotEmpty) { final walletAddress = ethereumWallets.first.address; print("User has Ethereum wallet with address: $walletAddress"); } } ``` Privy identifies users based on the unique ID that your auth provider has assigned (stored in the `sub` claim of their access token). You can view all users in the **Users** section of the Privy Developer Dashboard. # Additional OAuth providers Source: https://docs.privy.io/authentication/user-authentication/login-methods/custom-oauth Integrate any custom OAuth 2.0 provider not natively supported by Privy for user authentication This guide demonstrates how to integrate any custom [OAuth 2.0 provider](https://oauth.net/2/) that is not natively supported by Privy. For natively supported providers (Google, Apple, Twitter, Discord, etc.), see the [OAuth guide](/authentication/user-authentication/login-methods/oauth). This is an advanced feature. We recommend understanding the basics of OAuth 2.0 before proceeding. Misconfiguring this feature can lead to security vulnerabilities. ## Overview Privy's custom OAuth feature allows you to: * **Integrate any OAuth 2.0 provider** - Add authentication for services like YouTube, Kraken, or Reddit not natively supported by Privy * **Maintain unified user experience** - Custom providers work seamlessly with Privy's existing authentication flows * **Customize branding** - Upload custom provider icons and display names for your login UI * **Advanced configuration** - Support for PKCE, custom scopes, and flexible user data mapping ## Step 1: Configure your OAuth provider First, you'll need to set up your OAuth application with the provider you want to integrate. ### Register your application 1. Navigate to your OAuth provider's developer console 2. Create a new OAuth application 3. Configure the redirect URI to point to Privy: ``` https://auth.privy.io/api/v1/oauth/callback ``` 4. Note your client ID and client secret 5. Review the provider's documentation for: * Authorization endpoint URL * Token endpoint URL * User info endpoint URL (if needed) * Available scopes * User data structure ### Example: Setting up Twitch OAuth Here's how to configure Twitch as an example: 1. Go to the [Twitch Developer Console](https://dev.twitch.tv/console) 2. Create a new application with these settings: * **Name**: Your app name * **OAuth Redirect URLs**: `https://auth.privy.io/api/v1/oauth/callback` * **Category**: Choose appropriate category 3. Note the **Client ID** and generate a **Client Secret** 4. Review Twitch's OAuth endpoints: * Authorization URL: `https://id.twitch.tv/oauth2/authorize` * Token URL: `https://id.twitch.tv/oauth2/token` * User Info: `https://id.twitch.tv/oauth2/userinfo` ## Step 2: Configure custom OAuth in Privy Dashboard Navigate to your Privy Dashboard and configure the custom OAuth provider. Once set, custom OAuth provider fields cannot be changed. Be sure to double check your configuration before saving. Once users are created under a custom OAuth provider, the configuration cannot be deleted. ### Basic configuration 1. Go to **Settings > Login methods** in your Privy Dashboard 2. Click **Add Custom Provider** 3. Fill in the basic configuration: **Display name** (required) * How the provider appears in your login UI * Example: "Twitch", "Kraken", "Reddit" **Provider icon** * Upload a icon for your login button * Will be displayed alongside other social login options **Client ID** (required) * The client ID from your OAuth provider * Used to identify your application during OAuth flow **Client secret** (required) * The client secret from your OAuth provider * Securely stored and used for token exchange ### OAuth endpoints Configure the OAuth flow endpoints: **Authorization URL** (required) * Where users are redirected to grant permissions * Example: `https://provider.com/oauth/authorize` **Token URL** (required) * Endpoint for exchanging authorization code for access token * Also known as the refresh URL * Example: `https://provider.com/oauth/token` **Profile URL** (optional) * Endpoint to fetch user information * Only needed if using "Profile Endpoint" user info source * Example: `https://api.provider.com/user` ### Advanced configuration **Scopes** * List of OAuth scopes to request * Common scopes: `openid`, `profile`, `email` * Provider-specific examples: * Twitch: `user:read:email`, `openid` * Discord: `identify`, `email` **Scopes delimiter** * Character used to separate multiple scopes * Default: space (` `) * Some providers use comma (`,`) or plus (`+`) **PKCE enabled** * Enable Proof Key for Code Exchange for enhanced security * Recommended for public clients and mobile apps * Check if your OAuth provider supports PKCE before enabling this setting **Use cookie domain for redirect** * Advanced setting for custom domain configurations * Leave disabled unless specifically needed ### User information configuration Configure how Privy extracts user data from your OAuth provider. Review your authentication provider's documentation to determine which method is needed for your use case. **ID Token** * Extract user info from OpenID Connect ID token * Works with providers that return ID tokens **Profile endpoint** * Fetch user info from a dedicated API endpoint * Requires additional HTTP request **Access token JWT** * Extract user info directly from access token ### Field mapping Map user data fields from your provider to Privy user attributes using dot notation for nested fields. Review your auth provider's documentation to determine which fields are available for extraction and tell Privy where to put that information on the user's linked account. ```json theme={"system"} { "path_to_name": "display_name", "path_to_username": "login", "path_to_email": "user.email", "path_to_profile_picture_url": "profile_image_url" } ``` ## Step 3: Implement in your application Once configured in the Dashboard, your custom OAuth provider works automatically with Privy's SDKs. ### Using Privy UIs Your custom OAuth provider will appear automatically in Privy's login UI: ```tsx theme={"system"} import {useLogin, usePrivy} from '@privy-io/react-auth'; function LoginPage() { const {ready, authenticated, user} = usePrivy(); const {login} = useLogin(); // Custom providers appear automatically in the login modal if (!ready) return
Loading...
; if (!authenticated) { return ; } return ; } ``` ### Using whitelabel UIs Your app can also use our whitelabel hooks to login with custom OAuth providers. As a parameter to the `initOAuth` method, pass an object with `provider` field set to `custom:`. ```tsx theme={"system"} import {useLoginWithOAuth} from '@privy-io/react-auth'; function CustomTwitchButton() { const {initOAuth} = useLoginWithOAuth({ onComplete: (user) => { console.log('Twitch login successful:', user); }, onError: (error) => { console.error('Twitch login failed:', error); } }); const handleTwitchLogin = () => { initOAuth({provider: 'custom:twitch'}); }; return ( ); } ``` ### Accessing custom OAuth account data Custom OAuth accounts are available in the user object. Filter the linked accounts by `type === custom:` to find the account. ```tsx theme={"system"} import {usePrivy} from '@privy-io/react-auth'; function UserProfile() { const {user} = usePrivy(); // Find custom OAuth accounts const customAccounts = user?.linkedAccounts?.filter((account) => account.type.startsWith('custom:') ); // Find specific provider account const twitchAccount = user?.linkedAccounts?.find((account) => account.type === 'custom:twitch'); return (

Connected Accounts

{customAccounts?.map((account) => (
{account.type.replace('custom:', '')}

Username: {account.username}

Email: {account.email}

))}
); } ``` Your app can configure multiple custom OAuth providers with Privy. # Email Source: https://docs.privy.io/authentication/user-authentication/login-methods/email Authenticate users with email one-time passcodes (OTP) using Privy login methods Privy enables users to login to your application with SMS or email. With Privy, your application can verify ownership of a user's email address or phone number to send them notifications, campaigns, and more to keep them activated. Enable email authentication in the [Privy Dashboard](https://dashboard.privy.io/apps?page=login-methods) before implementing this feature. Privy uses [`mailchecker`](https://github.com/FGRibreau/mailchecker/) to detect temporary email domains. To block them automatically, turn the setting on in the [Privy Dashboard](https://dashboard.privy.io/apps?page=login-methods). To authenticate your users with a one-time passcode (OTP) sent to their email address, use the `useLoginWithEmail` hook. To authenticate your users with Privy's out of the box UIs, check out UI components [here](/authentication/user-authentication/ui-component). ## Email login with OTP When a user signs in with email, Privy sends a one-time passcode (OTP) to the provided address. Enterprise customers can customize the OTP email used for login, including the sender reply address and email branding (such as logo). [Contact us](mailto:sales@privy.io) to enable this for your application. ## Send Code ```tsx theme={"system"} sendCode: ({email: string, disableSignup?: boolean}) => Promise ``` ### Parameters The email address of the user to log in. Whether to disable the ability to sign up with the email address. ### Returns A promise that resolves when the code is sent. ## Login with Code ```tsx theme={"system"} loginWithCode: ({ code: string }) => Promise; ``` ### Parameters The one-time passcode sent to the user's email address. ### Returns A promise that resolves when the user is logged in. ## Usage ```tsx theme={"system"} import { useState } from "react"; import { useLoginWithEmail } from "@privy-io/react-auth"; export default function LoginWithEmail() { const [email, setEmail] = useState(""); const [code, setCode] = useState(""); const { sendCode, loginWithCode } = useLoginWithEmail(); return (
setEmail(e.currentTarget.value)} value={email} /> setCode(e.currentTarget.value)} value={code} />
); } ``` ## Tracking Flow State Track the state of the OTP flow via the `state` variable returned by the `useLoginWithEmail` hook. ```ts theme={"system"} type OtpFlowState = | {status: 'initial'} | {status: 'error'; error: Error | null} | {status: 'sending-code'} | {status: 'awaiting-code-input'} | {status: 'submitting-code'} | {status: 'done'}; ``` The current state of the OTP flow. The error that occurred during the OTP flow. ## Callbacks You can optionally pass callbacks into the `useLoginWithEmail` hook to run custom logic after a successful login or to handle errors that occur during the flow. ### `onComplete` ```tsx theme={"system"} onComplete?: ((params: { user: User; isNewUser: boolean; wasAlreadyAuthenticated: boolean; loginMethod: LoginMethod | null; loginAccount: LinkedAccountWithMetadata | null; }) => void) | undefined ``` #### Parameters The user object corresponding to the authenticated user. Whether the user is a new user or an existing user. Whether the user entered the application already authenticated. The method used by the user to login. The account corresponding to the loginMethod used. ### `onError` ```tsx theme={"system"} onError: (error: Error) => void ``` #### Parameters The error that occurred during the login flow. ## Resources Get started with React and Privy. Get started with Next.js and Privy. Get started with a whitelabel Privy integration.
To authenticate your users with a one-time passcode (OTP) sent to their email address, use the `useLoginWithEmail` hook. To authenticate your users with Privy's out of the box UIs, check out UI components [here](/authentication/user-authentication/ui-component#react-native). ## Send Code ```jsx theme={"system"} sendCode: ({email: string}) => Promise<{success: boolean}> ``` ### Parameters The email address of the user to log in. ### Returns A promise that resolves to an object with a success property indicating if the code was sent successfully. ## Login with Code ```jsx theme={"system"} loginWithCode: ({ code: string, email?: string, disableSignup?: boolean }) => Promise<{user: PrivyUser; isNewUser: boolean}> ``` ### Parameters The one-time passcode sent to the user's email address. The user's email address. Though this parameter is optional, it is highly recommended that you pass the user's email address explicitly. Whether to disable the ability to sign up with the email address. ### Returns The user object returned by the login process. ## Usage ```jsx theme={"system"} import { useState } from 'react'; import { useLoginWithEmail } from '@privy-io/expo'; export default function LoginWithEmail() { const [email, setEmail] = useState(''); const [code, setCode] = useState(''); const { sendCode, loginWithCode } = useLoginWithEmail(); return ( ); } ``` ## Tracking login flow state The state variable returned from useLoginWithEmail will always be one of the following values. ```ts theme={"system"} type OtpFlowState = | {status: 'initial'} | {status: 'error'; error: Error | null} | {status: 'sending-code'} | {status: 'awaiting-code-input'} | {status: 'submitting-code'} | {status: 'done'}; ``` The current state of the email login flow. The error that occurred during the email login flow. ## Callbacks You can optionally pass callbacks into the `useLoginWithEmail` hook to run custom logic after an OTP has been sent, after a successful login, or to handle errors that occur during the flow. ### `onSendCodeSuccess` ```tsx theme={"system"} onSendCodeSuccess?: ((email: string) => void) | undefined ``` #### Parameters The email the code was sent to. ### `onLoginSuccess` ```tsx theme={"system"} onLoginSuccess?: ((user: User, isNewUser: boolean) => void) | undefined ``` #### Parameters The PrivyUser returned by loginWithCode. Whether the user is a new user or an existing user. ### `onError` ```tsx theme={"system"} onError?: (error: Error) => void ``` #### Parameters The error that occurred during the login flow. ## Resources Get started with Expo and Privy. Get started with Expo bare and Privy. To authenticate a user via their email address, use the Privy client's `email` handler. ## Send Code ```swift theme={"system"} sendCode(to email: String) async throws ``` ### Parameters The email address of the user to log in. ### Returns Nothing, indicating success. ### Throws An error if sending the code fails. ## Login with Code ```swift theme={"system"} loginWithCode(_ code: String, sentTo email: String) async throws -> PrivyUser ``` ### Parameters The one-time passcode sent to the user's email address. The user's email address. ### Returns The authenticated Privy user ### Throws An error if logging the user in is unsuccessful. ## Usage ```swift theme={"system"} // Send code to user's email do { try await privy.email.sendCode(to: "myuser@privy.io") // successfully sent code to users email } catch { print("error sending code to \(email): \(error)") } // Log the user in do { let user = try await privy.email.loginWithCode("123456", sentTo: "myuser@privy.io") // user successfully logged in } catch { print("error logging user in: \(error)") } ``` To authenticate a user via their email address, use the Privy client's `email` handler. ## Send Code ```kotlin theme={"system"} sendCode(email: String): Result ``` ### Parameters The email address of the user to log in. ### Returns A Result object that indicates whether the operation was successful. Returns Result.success if the code was sent successfully, or Result.failure if there was an error. ## Login with Code ```kotlin theme={"system"} loginWithCode(code: String, email: String?): Result ``` ### Parameters The one-time passcode sent to the user's email address. (Optional) The user's email address. Though this parameter is optional, it is highly recommended that you pass the user's email address explicitly. If email is omitted, the email from sendCode will be used. ### Returns A Result object containing the PrivyUser if successful, or an error if the operation failed. ## Usage ```kotlin theme={"system"} // Send code to user's email val result: Result = privy.email.sendCode(email = "user_email@gmail.com") result.fold( onSuccess = { // OTP was successfully sent }, onFailure = { println("Error sending OTP: ${it.message}") } ) // Authenticate with the OTP code val result: Result = privy.email.loginWithCode(code = "123456", email = "user_email@gmail.com") result.fold( onSuccess = { user -> // User logged in }, onFailure = { println("Error logging in user: ${it.message}") } ) ``` To authenticate a user via their email address, use the Privy client's `Email` handler. ## Send Code ```csharp theme={"system"} SendCode(string email): Task ``` ### Parameters The email address of the user to log in. ### Returns A Task that resolves to a boolean indicating whether the code was sent successfully. ## Login with Code ```csharp theme={"system"} LoginWithCode(string email, string code): Task ``` ### Parameters The user's email address. The one-time passcode sent to the user's email address. ### Returns A Task that resolves to the user's `AuthState` when successfully authenticated, or throws a `PrivyAuthenticationException` if authentication fails. ## Usage ```csharp theme={"system"} // Send code to user's email bool success = await PrivyManager.Instance.Email.SendCode(email); if (success) { // Prompt user to enter the OTP they received at their email address through your UI } else { // There was an error sending an OTP to your user's email } // Authenticate with the OTP code try { // User will be authenticated if this call is successful await PrivyManager.Instance.Email.LoginWithCode(email, code); // User is now logged in } catch { // If "LoginWithCode" throws an exception, user login was unsuccessful. Debug.Log("Error logging user in."); } ``` To authenticate a user via their email address, use the Privy client's `email` handler. ## Send Code ```dart theme={"system"} sendCode(String email): Future> ``` ### Parameters The email address of the user to log in. ### Returns A Result object that indicates whether the operation was successful. Returns Result.success if the code was sent successfully, or Result.failure if there was an error. ## Login with Code ```dart theme={"system"} loginWithCode({required String code, String? email}): Future> ``` ### Parameters The one-time passcode sent to the user's email address. (Optional) The user's email address. Though this parameter is optional, it is highly recommended that you pass the user's email address explicitly. If email is omitted, the email from sendCode will be used. ### Returns A Result object containing the PrivyUser if successful, or an error if the operation failed. ## Usage ```dart theme={"system"} // Send code to user's email final Result result = await privy.email.sendCode(email); result.fold( onSuccess: (_) { // OTP was sent successfully }, onFailure: (error) { // Handle error sending OTP print(error.message); }, ); // Authenticate with the OTP code final Result result = await privy.email.loginWithCode( code: code, email: email, ); result.fold( onSuccess: (user) { // User authenticated successfully }, onFailure: (error) { // Handle authentication error }, ); ``` ## Resources Get started with Flutter and Privy. # Farcaster Source: https://docs.privy.io/authentication/user-authentication/login-methods/farcaster Enable Sign in with Farcaster (FIP-11) to authenticate users with their Farcaster account [**Farcaster**](https://www.farcaster.xyz/) is a sufficiently decentralized social network whose core social graph is stored onchain. Privy enables your users to log in to your application using their Farcaster account. Privy uses a standard called **Sign in with Farcaster** ([FIP-11](https://github.com/farcasterxyz/protocol/discussions/110)) to issue a signature request to a user's Farcaster account via the client a user has. Enable Farcaster authentication in the [Privy Dashboard](https://dashboard.privy.io/apps?page=login-methods\&logins=socials) before implementing this feature. Your application can even request permissions from the user to become a signer for their Farcaster account, allowing your application to engage with the Farcaster social graph on their behalf. Interested in building a Farcaster Mini App? Check out our [Farcaster Mini App recipe](/recipes/farcaster/mini-apps)! Privy currently only supports Farcaster login in React via the Privy UIs. To enable Farcaster login, you need to configure the Privy SDK with the `farcaster` login method. Explore our UI components [here](/authentication/user-authentication/ui-component). ```tsx theme={"system"} ``` From there, you can prompt your users to authenticate via the `login` method: ```tsx theme={"system"} import { usePrivy } from '@privy-io/react'; ... const { login } = usePrivy(); login(); ``` ## Resources Get started with React and Privy. Get started with Next.js and Privy. Get started with a whitelabel Privy integration. To authenticate a user via Farcaster ([SIWF](https://github.com/farcasterxyz/protocol/discussions/110)), use the `loginWithFarcaster` method from the `useLoginWithFarcaster` hook. To authenticate your users with Privy's out of the box UIs, check out UI components [here](/authentication/user-authentication/ui-component#react-native). ```javascript theme={"system"} loginWithFarcaster(input: { relyingParty: string; redirectUrl?: string; disableSignup?: boolean; }, config?: { pollIntervalMs?: number; pollAttempts?: number; }): Promise; ``` ### Initializing the login flow To initialize login, use the `loginWithFarcaster` function from the `useLoginWithFarcaster` hook to start the Farcaster login flow. ```tsx theme={"system"} import { useLoginWithFarcaster } from '@privy-io/expo'; const { loginWithFarcaster, state } = useLoginWithFarcaster(); ``` As a parameter to `loginWithFarcaster`, you should pass an object containing: Your app's website. Described in SIWF spec as "Origin domain of app frontend." A URL path that Farcaster will automatically redirect to after successful authentication. This defaults to a link back to your app root, eg. `'/'`, if not provided. If true, the flow will only allow existing users to log in, preventing new account creation. The interval in milliseconds which your app will poll a status endpoint to check if the user has successfully signed in using Farcaster. The number of polling attempts that will be made to check for successful login. If you pass in custom polling configuration, make sure to give the user enough time to go through the login process on Farcaster. The default values are `pollIntervalMs = 1000` and `pollAttempts = 10` giving the user 10 seconds to go through the login process. In our testing, this is usually enough time, but you may want to make it longer. When this method is invoked, the user will be deeplinked to the Farcaster app on their device if they have it installed, or an installation page for the app. Within the Farcaster app, they can complete the login flow. If `loginWithFarcaster` succeeds, it will return a `PrivyUser` object with details about the authenticated user. Reasons `loginWithFarcaster` might fail include: * the network request fails * the login attempt is made after the user is already logged in * the user cancels the login flow after being linked out to Farcaster * the user takes too long to login and the polling time expires ### Tracking Flow State Track the state of the Farcaster flow via the `state` variable returned by the `useLoginWithFarcaster` hook. ```tsx theme={"system"} state: | {status: 'initial'} | {status: 'error'; error: Error | null} | {status: 'generating-uri'} | {status: 'awaiting-uri'} | {status: 'polling-status'} | {status: 'submitting-token'} | {status: 'done'}; ``` The current state of the Farcaster flow. The error that occurred during the Farcaster flow (only present when status is 'error'). ### Usage: Conditional Rendering ```tsx theme={"system"} import { View, ActivityIndicator, Text } from 'react-native'; import { useLoginWithFarcaster, hasError } from '@privy-io/expo'; export function LoginScreen() { const { state, loginWithFarcaster } = useLoginWithFarcaster(); return ( ); } ``` ## Flow state The `state` variable returned by `useLoginWithOAuth` tracks the OAuth flow: ```tsx theme={"system"} state: | {status: 'initial'} | {status: 'loading'} | {status: 'done'} | {status: 'error'; error: Error | null}; ``` ## Callbacks Pass optional callbacks to `useLoginWithOAuth`: ```tsx theme={"system"} onComplete: ({user, isNewUser, wasAlreadyAuthenticated, loginMethod, linkedAccount}) => void onError: (error: Error) => void ``` The user object returned after successful login. Whether the user is a new user or an existing user. Whether the user was already authenticated before the OAuth flow. The login method used ('google', 'apple', etc.). The linked account if the user was already authenticated. ## Security and tokens When verifying JWTs from OAuth providers, configure the `aud` (audience) claim to ensure tokens are intended for your application. See [access tokens](/authentication/user-authentication/tokens) for details. Google OAuth may not work in in-app browsers due to [Google's restrictions in embedded webviews](https://developers.googleblog.com/upcoming-security-changes-to-googles-oauth-20-authorization-endpoint-in-embedded-webviews/). * Configure [allowed OAuth redirect URLs](/recipes/react/allowed-oauth-redirects) to restrict post-login redirects. * Access user OAuth and refresh tokens via the [useOAuthTokens](/recipes/react/oauth-tokens) hook when using your own OAuth credentials. ## Resources Get started with React and Privy. Get started with Next.js and Privy. Get started with a whitelabel Privy integration. To authenticate users with Privy's built-in UIs, see [UI components](/authentication/user-authentication/ui-component#react-native). For whitelabel implementations, use `login` from the `useLoginWithOAuth` hook. Privy also supports native [Apple login](/basics/react-native/advanced/setup-apple-login) on iOS. ### Configure allowed URL schemes Prior to integrating OAuth login, make sure you have [properly configured your app's allowed URL schemes in the Privy dashboard](/basics/get-started/dashboard/app-clients#allowed-url-schemes). Login with OAuth might **not** work if you have not completed this step. If your app uses native OAuth with Privy's REST API, include `scheme` in the authenticate request body. Set `scheme` to one of your app's allowed URL schemes configured in the dashboard. ```tsx theme={"system"} login: ({ provider: OAuthProviderType, disableSignup?: boolean }) => Promise ``` The OAuth provider to use for authentication. Valid values are: `'google'`, `'apple'`, `'twitter'`, `'github'`, `'discord'`, `'linkedin'`, `'spotify'`, `'tiktok'`, `'instagram'`, `'telegram'`. If true, the OAuth flow will only allow existing users to log in, preventing new account creation. ### Usage ```tsx theme={"system"} import { useLoginWithOAuth } from '@privy-io/expo'; export function LoginScreen() { const { login, state } = useLoginWithOAuth(); const onPress = async () => { try { const user = await login({ provider: 'google' }); console.log('Login successful', user.id); } catch (error) { console.error('Login failed', error); } }; return ( ); } ``` ## Sign up with Passkey Use `signupWithPasskey` from the `useSignupWithPasskey` hook to trigger the passkey signup flow. ```jsx theme={"system"} signupWithPasskey: () => void ``` ### Usage ```jsx theme={"system"} import { useSignupWithPasskey } from '@privy-io/react-auth'; export default function SignupWithPasskey() { const { signupWithPasskey } = useSignupWithPasskey(); return (
); } ``` ## Tracking Flow State Track the state of the passkey flow via the `state` variable returned by both the `useLoginWithPasskey` and `useSignupWithPasskey` hooks. ```tsx theme={"system"} state: | {status: 'initial'} | {status: 'error'; error: Error | null} | {status: 'generating-challenge'} | {status: 'awaiting-passkey'} | {status: 'submitting-response'} | {status: 'done'}; ``` The current state of the passkey flow. The error that occurred during the passkey flow. ## Callbacks You can optionally pass callbacks into the `useLoginWithPasskey` and `useSignupWithPasskey` hooks to run custom logic after a successful login or signup, or to handle errors that occur during the flow. ### `onComplete` ```tsx theme={"system"} onComplete: ({user, isNewUser, wasAlreadyAuthenticated, loginMethod, linkedAccount}) => void ``` #### Parameters The [user object](/user-management/users/the-user-object) returned after successful login or signup." Whether the user is a new user or an existing user. Whether the user was already authenticated before the passkey flow. The login method used to authenticate the user. The linked account if the user was already authenticated. ### `onError` ```tsx theme={"system"} onError: (error: Error) => void ``` #### Parameters The error that occurred during the passkey flow. ## Resources Get started with React and Privy. Get started with Next.js and Privy. Get started with a whitelabel Privy integration.
Follow the [passkeys setup guide](/basics/react-native/advanced/setup-passkeys) to enable passkey authentication in your React Native app. To authenticate your users with Privy's out of the box UIs, check out UI components [here](/authentication/user-authentication/ui-component#react-native). ## Login with Passkey Use `loginWithPasskey` from the `useLoginWithPasskey` hook to authenticate users using a passkey. Before using this method, ensure you have setup passkeys as described in this [guide](/basics/react-native/advanced/setup-passkeys). ```tsx theme={"system"} loginWithPasskey: ({ relyingParty: string }) => Promise ``` ### Parameters The URL origin where your Apple App Site Association or Digital Asset Links are available (e.g. `https://example.com`). ### Response ### Usage ```tsx theme={"system"} import {useLoginWithPasskey} from '@privy-io/expo/passkey'; export function LoginButton() { const {loginWithPasskey} = useLoginWithPasskey(); return ( ); } ``` ## Sign up with Passkey Use `signupWithPasskey` from the `useSignupWithPasskey` hook to sign up users using a passkey. ```tsx theme={"system"} signupWithPasskey: ({ relyingParty: string }) => Promise ``` ### Parameters The URL origin where your Apple App Site Association or Digital Asset Links are available (e.g. `https://example.com`). ### Response ### Usage ```tsx theme={"system"} import {useSignupWithPasskey} from '@privy-io/expo/passkey'; export function SignupButton() { const {signupWithPasskey} = useSignupWithPasskey(); return ( ); } ``` ## Tracking Flow State Track the state of the passkey flow via the `state` variable returned by both the `useLoginWithPasskey` and `useSignupWithPasskey` hooks. ```tsx theme={"system"} state: | {status: 'initial'} | {status: 'error'; error: Error | null} | {status: 'generating-challenege'} | {status: 'awaiting-passkey'} | {status: 'submitting-response'} | {status: 'done'}; ``` The current state of the passkey flow. The error that occurred during the passkey flow. ## Callbacks You can optionally pass callbacks into the `useLoginWithPasskey` and `useSignupWithPasskey` hooks to run custom logic after a successful login or signup, or to handle errors that occur during the flow. ### `onSuccess` ```tsx theme={"system"} onSuccess: (user: PrivyUser, isNewUser: boolean) => Promise ``` #### Parameters Whether the user is a new user or an existing user. #### Usage ```tsx theme={"system"} import {useLoginWithPasskey} from '@privy-io/expo/passkey'; export function LoginScreen() { const {loginWithPasskey} = useLoginWithPasskey({ onSuccess(user, isNewUser) { // show a toast, send analytics event, etc... }, }); // ... } ``` ### `onError` ```tsx theme={"system"} onError: (error: Error) => Promise ``` #### Parameters The error that occurred during the passkey flow. #### Usage ```tsx theme={"system"} import {useLoginWithPasskey} from '@privy-io/expo/passkey'; export function LoginScreen() { const {loginWithPasskey} = useLoginWithPasskey({ onError(error) { // show a toast, update form errors, etc... }, }); // ... } ``` ## Resources Get started with Expo and Privy. Get started with Expo bare and Privy. Ensure an [apple-app-site-association (AASA) file is present on your domain](https://developer.apple.com/documentation/Xcode/supporting-associated-domains) in the .well-known directory, and that it contains an entry for your app’s App ID for the webcredentials service. ## Login with Passkey Use `login` from the `privy.passkey` interface to authenticate an existing user who has already registered a passkey. This method allows returning users to log in using their previously created passkey credentials. Before using this method, ensure you have setup passkeys as described in the passkey setup guide. ```swift theme={"system"} func login(relyingParty: String) async throws -> PrivyUser ``` ### Parameters The URL origin where your Digital Asset Links are available (e.g., `https://example.com`). ### Returns The authenticated Privy user ### Usage ```swift theme={"system"} do { let user = try await privy.passkey.login(relyingParty: relyingParty) // Successfully authenticated an existing user with a passkey } catch { print("Login failed: \(error.localizedDescription)") } ``` ## Sign up with Passkey Use `signup` from the `privy.passkey` interface to create a new user account and register a passkey for them. This method creates a new user in your Privy app, whereas `login` authenticates an existing user who has already registered a passkey. ```kotlin theme={"system"} func signup(relyingParty: String, displayName: String?) async throws -> PrivyUser ``` ### Parameters The URL origin where your Digital Asset Links are available (e.g., `https://example.com`). An optional display name to associate with the passkey. This name will be shown to the user when selecting which passkey to use for authentication. ### Returns The authenticated Privy user ### Usage ```swift theme={"system"} do { let displayName = "Optional Display Name" let user = try await privy.passkey.signup( relyingParty: relyingParty, displayName: displayName ) // Successfully created a new user with authenticated by a passkey } catch { print("Signup failed: \(error.localizedDescription)") } ``` Follow the [passkeys setup guide](/basics/android/advanced/setup-passkeys) to enable passkey authentication in your Android app. ## Login with Passkey Use `login` from the `privy.passkey` interface to authenticate an existing user who has already registered a passkey. This method allows returning users to log in using their previously created passkey credentials. Before using this method, ensure you have setup passkeys as described in the passkey setup guide. ```kotlin theme={"system"} suspend fun login(relyingParty: String): Result ``` ### Parameters The URL origin where your Digital Asset Links are available (e.g., `https://example.com`). ### Response Returns a `Result` containing the user object after successful login. The unique identifier for this user A list of linked accounts for this user. A linked account can be any of the methods a user authenticated with, or a user's embedded wallet ID of user from custom auth provider UNIX timestamp for when the account was first verified and linked to the user UNIX timestamp for when the account was last verified The unique identifier for this embedded wallet The wallet address The chain ID for this wallet The recovery method configured for this wallet The HD wallet index (0 is the primary wallet) The unique identifier for this embedded wallet The wallet address The chain ID for this wallet The recovery method configured for this wallet The HD wallet index (0 is the primary wallet) The wallet address The type of blockchain: ethereum or solana The chain ID for this wallet The wallet client type The connector type used UNIX timestamp for when the account was first verified and linked to the user UNIX timestamp for when the account was last verified Phone number of user account Email address of user account ID of user from Google user API response Email of user from Google user API response Name of user from Google user API response UNIX timestamp for when the account was first verified and linked to the user UNIX timestamp for when the account was last verified ID of user from Twitter user API response Username of user from Twitter user API response (does not include the '@') Name of user from Twitter user API response Email of user from Twitter user API response Profile picture URL of the user from Twitter user API response UNIX timestamp for when the account was first verified and linked to the user UNIX timestamp for when the account was last verified ID of user from Discord user API response Username of user from Discord user API response Email of user from Discord user API response UNIX timestamp for when the account was first verified and linked to the user UNIX timestamp for when the account was last verified The credential ID for this passkey Name of the authenticator device Browser used to create the passkey Operating system used to create the passkey Device used to create the passkey The public key for this passkey Whether this passkey is enrolled in multi-factor authentication UNIX timestamp for when the account was verified UNIX timestamp for when the account was first verified and linked to the user UNIX timestamp for when the account was last verified
The identity token for this user, if configured in the Privy dashboard. This token is an optional JWT provided by Privy when identity token generation is enabled in the dashboard settings. It returns null if the user is unauthenticated or if the feature is not configured A list of the user's embedded Ethereum wallets. These wallets expose a "provider" instance which can be used to take wallet actions A list of the user's embedded Solana wallets. These wallets expose a "provider" instance which can be used to take wallet actions
### Usage ```kotlin theme={"system"} val result = privy.passkey.login(relyingParty = "https://") result.fold( onSuccess = { user -> // Handle successful login }, onFailure = { error -> // Handle login error } ) ``` ## Sign up with Passkey Use `signup` from the `privy.passkey` interface to create a new user account and register a passkey for them. This method creates a new user in your Privy app, whereas `login` authenticates an existing user who has already registered a passkey. ```kotlin theme={"system"} suspend fun signup(relyingParty: String, displayName: String? = null): Result ``` ### Parameters The URL origin where your Digital Asset Links are available (e.g., `https://example.com`). An optional display name to associate with the passkey. This name will be shown to the user when selecting which passkey to use for authentication. ### Response Returns a `Result` containing the user object after successful signup. The unique identifier for this user A list of linked accounts for this user. A linked account can be any of the methods a user authenticated with, or a user's embedded wallet ID of user from custom auth provider UNIX timestamp for when the account was first verified and linked to the user UNIX timestamp for when the account was last verified The unique identifier for this embedded wallet The wallet address The chain ID for this wallet The recovery method configured for this wallet The HD wallet index (0 is the primary wallet) The unique identifier for this embedded wallet The wallet address The chain ID for this wallet The recovery method configured for this wallet The HD wallet index (0 is the primary wallet) The wallet address The type of blockchain: ethereum or solana The chain ID for this wallet The wallet client type The connector type used UNIX timestamp for when the account was first verified and linked to the user UNIX timestamp for when the account was last verified Phone number of user account Email address of user account ID of user from Google user API response Email of user from Google user API response Name of user from Google user API response UNIX timestamp for when the account was first verified and linked to the user UNIX timestamp for when the account was last verified ID of user from Twitter user API response Username of user from Twitter user API response (does not include the '@') Name of user from Twitter user API response Email of user from Twitter user API response Profile picture URL of the user from Twitter user API response UNIX timestamp for when the account was first verified and linked to the user UNIX timestamp for when the account was last verified ID of user from Discord user API response Username of user from Discord user API response Email of user from Discord user API response UNIX timestamp for when the account was first verified and linked to the user UNIX timestamp for when the account was last verified The credential ID for this passkey Name of the authenticator device Browser used to create the passkey Operating system used to create the passkey Device used to create the passkey The public key for this passkey Whether this passkey is enrolled in multi-factor authentication UNIX timestamp for when the account was verified UNIX timestamp for when the account was first verified and linked to the user UNIX timestamp for when the account was last verified
The identity token for this user, if configured in the Privy dashboard. This token is an optional JWT provided by Privy when identity token generation is enabled in the dashboard settings. It returns null if the user is unauthenticated or if the feature is not configured A list of the user's embedded Ethereum wallets. These wallets expose a "provider" instance which can be used to take wallet actions A list of the user's embedded Solana wallets. These wallets expose a "provider" instance which can be used to take wallet actions
### Usage ```kotlin theme={"system"} val result = privy.passkey.signup(relyingParty = "https://") result.fold( onSuccess = { user -> // Handle successful signup }, onFailure = { error -> // Handle signup error } ) ```
To enable passkeys for your Flutter app, you need to set them up for both iOS and Android. Follow the [Android passkeys setup guide](/basics/android/advanced/setup-passkeys), and ensure an apple-app-site-association (AASA) file is present on your domain in the .well-known directory that contains an entry for your app's App ID for the webcredentials service. ## Login with Passkey Use `login` from the `privy.passkey` interface to authenticate an existing user who has already registered a passkey. This method allows returning users to log in using their previously created passkey credentials. Before using this method, ensure you have setup passkeys as described in the passkey setup guide. ```dart theme={"system"} Future> login({ required String relyingParty, }) ``` ### Parameters The URL origin where your Digital Asset Links are available (e.g., `https://example.com`). ### Response Returns a `Result` containing the user object after successful login. ### Usage ```dart theme={"system"} final result = await privy.passkey.login( relyingParty: "https://", ); result.fold( onSuccess: (user) { // Handle successful login }, onFailure: (error) { // Handle login error }, ); ``` ## Sign up with Passkey Use `signup` from the `privy.passkey` interface to create a new user account and register a passkey for them. This method creates a new user in your Privy app, whereas `login` authenticates an existing user who has already registered a passkey. ```dart theme={"system"} Future> signup({ required String relyingParty, String? displayName, }) ``` ### Parameters The URL origin where your Digital Asset Links are available (e.g., `https://example.com`). An optional display name to associate with the passkey. This name will be shown to the user when selecting which passkey to use for authentication. ### Response Returns a `Result` containing the user object after successful signup. ### Usage ```dart theme={"system"} final result = await privy.passkey.signup( relyingParty: "https://", displayName: "My Passkey", // optional ); result.fold( onSuccess: (user) { // Handle successful signup }, onFailure: (error) { // Handle signup error }, ); ``` # SMS and WhatsApp Source: https://docs.privy.io/authentication/user-authentication/login-methods/sms-whatsapp Authenticate users with SMS or WhatsApp one-time passcodes for phone-based login Privy enables users to login with SMS or WhatsApp. Configure your app following this guide and make sure to read our recipe on [enabling SMS or WhatsApp](/recipes/dashboard/login-methods/sms). Developers can enable **either** SMS or WhatsApp, but cannot utilize both. Once your account is enabled for SMS with your chosen provider, it **cannot** be switched. ## Configuring your application Through your app's Privy configuration, you can set the default country code for phone numbers. This is useful if your application primarily serves users from a specific country. The default country can be set in your PrivyProvider, like so: ```tsx {5} theme={"system"} {children} ``` To authenticate your users with a one-time passcode (OTP) sent to their phone number via either SMS or WhatsApp, use the `useLoginWithSms` hook. To authenticate your users with Privy's out of the box UIs, check out UI components [here](/authentication/user-authentication/ui-component). ## Send Code ```tsx theme={"system"} sendCode: ({phoneNumber: string, disableSignup?: boolean}) => Promise ``` ### Parameters The phone number of the user to log in. Must follow specific formatting conventions (see below). Whether to disable the ability to sign up with the phone number. ### Returns A promise that resolves when the code is sent. ## Formatting the phone number The `sendCode` method requires a `phoneNumber` string param that must follow these formatting conventions: * By default, the implicit phone number country code is +1/US. * Explicitly prepending a `(+)1` to the phone number will still be read as a US phone number. * For non-US phone numbers, append a `+${countryCode}` to the beginning of the input value. * Non-numerical values in the string are ignored, except for a leading `+` that denotes a custom country code. ## Login with Code ```tsx theme={"system"} loginWithCode: ({ code: string }) => Promise; ``` ### Parameters The one-time passcode sent to the user's phone number. ### Returns A promise that resolves when the user is logged in. ## Usage ```tsx theme={"system"} import { useState } from "react"; import { useLoginWithSms } from "@privy-io/react-auth"; export default function LoginWithSms() { const [phoneNumber, setPhoneNumber] = useState(""); const [code, setCode] = useState(""); const { state, sendCode, loginWithCode } = useLoginWithSms(); return (
{/* Prompt your user to enter their phone number */} setPhoneNumber(e.currentTarget.value)} value={phoneNumber} /> {/* Once a phone number has been entered, send the OTP to it on click */} {/* Prompt your user to enter the OTP */} setCode(e.currentTarget.value)} value={code} /> {/* Once an OTP has been entered, submit it to Privy on click */}
); } ``` ## Tracking Flow State Track the state of the OTP flow via the `state` variable returned by the `useLoginWithSms` hook. ```ts theme={"system"} type OtpFlowState = | {status: 'initial'} | {status: 'error'; error: Error | null} | {status: 'sending-code'} | {status: 'awaiting-code-input'} | {status: 'submitting-code'} | {status: 'done'}; ``` The current state of the OTP flow. The error that occurred during the OTP flow. ## Callbacks You can optionally pass callbacks into the `useLoginWithSms` hook to run custom logic after a successful login or to handle errors that occur during the flow. ### `onComplete` ```tsx theme={"system"} onComplete?: ((params: { user: User; isNewUser: boolean; wasAlreadyAuthenticated: boolean; loginMethod: LoginMethod | null; loginAccount: LinkedAccountWithMetadata | null; }) => void) | undefined ``` #### Parameters The user object corresponding to the authenticated user. Whether the user is a new user or an existing user. Whether the user entered the application already authenticated. The method used by the user to login. The account corresponding to the loginMethod used. ### `onError` ```tsx theme={"system"} onError: (error: Error) => void ``` #### Parameters The error that occurred during the login flow. ## Resources Get started with React and Privy. Get started with Next.js and Privy. Get started with a whitelabel Privy integration.
To authenticate your users with a one-time passcode (OTP) sent to their phone number, use the `useLoginWithSMS` hook. To authenticate your users with Privy's out of the box UIs, check out UI components [here](/authentication/user-authentication/ui-component#react-native). ## Send Code ```jsx theme={"system"} sendCode: ({phone: string}) => Promise<{success: boolean}> ``` ### Parameters The phone number of the user to log in. ### Returns A promise that resolves to an object with a success property indicating if the code was sent successfully. ## Login with Code ```jsx theme={"system"} loginWithCode: ({ code: string, phone?: string, disableSignup?: boolean }) => Promise<{user: PrivyUser; isNewUser: boolean}> ``` ### Parameters The one-time passcode sent to the user's phone number. The user's phone number. Though this parameter is optional, it is highly recommended that you pass the user's phone number explicitly. Whether to disable the ability to sign up with the phone number. ### Returns The user object returned by the login process. ## Usage ```jsx theme={"system"} import { useState } from 'react'; import { useLoginWithSMS } from '@privy-io/expo'; export function LoginScreen() { const [phone, setPhone] = useState(''); const [code, setCode] = useState(''); const { sendCode, loginWithCode } = useLoginWithSMS(); return ( Login ); } ``` ## Tracking login flow state The state variable returned from useLoginWithSMS will always be one of the following values. ```ts theme={"system"} type OtpFlowState = | {status: 'initial'} | {status: 'error'; error: Error | null} | {status: 'sending-code'} | {status: 'awaiting-code-input'} | {status: 'submitting-code'} | {status: 'done'}; ``` The current state of the SMS login flow. The error that occurred during the SMS login flow. ## Callbacks You can optionally pass callbacks into the `useLoginWithSMS` hook to run custom logic after an OTP has been sent, after a successful login, or to handle errors that occur during the flow. ### `onSendCodeSuccess` ```tsx theme={"system"} onSendCodeSuccess?: ((phone: string) => void) | undefined ``` #### Parameters The phone number the code was sent to. ### `onLoginSuccess` ```tsx theme={"system"} onLoginSuccess?: ((user: User, isNewUser: boolean) => void) | undefined ``` #### Parameters The PrivyUser returned by loginWithCode. Whether the user is a new user or an existing user. ### `onError` ```tsx theme={"system"} onError?: (error: Error) => void ``` #### Parameters The error that occurred during the login flow. ## Resources Get started with Expo and Privy. Get started with Expo bare and Privy. To authenticate a user via their phone number, use the Privy client's `sms` handler. ## Send Code ```swift theme={"system"} sendCode(to phoneNumber: String) async throws ``` ### Parameters The phone number of the user to log in. Must be in E.164 format (e.g., "+14155552671"). ### Returns Nothing, indicating success. ### Throws An error if sending the code fails. ## Login with Code ```swift theme={"system"} loginWithCode(_ code: String, sentTo phoneNumber: String) async throws -> PrivyUser ``` ### Parameters The one-time passcode sent to the user's phone number. The user's phone number. ### Returns The authenticated Privy user ### Throws An error if logging the user in is unsuccessful. ## Usage ```swift theme={"system"} // Send code to user's phone do { try await privy.sms.sendCode(to: "+14155552671") // successfully sent code to users phone } catch { print("error sending code: \(error)") } // Log the user in do { let user = try await privy.sms.loginWithCode("123456", sentTo: "+14155552671") // user successfully logged in } catch { print("error logging user in: \(error)") } ``` To authenticate a user via their phone number, use the Privy client's `sms` handler. ## Send Code ```kotlin theme={"system"} sendCode(phoneNumber: String): Result ``` ### Parameters The phone number of the user to log in. Must be in E.164 format (e.g., "+14155552671"). ### Returns A Result object that indicates whether the operation was successful. Returns Result.success if the code was sent successfully, or Result.failure if there was an error. ## Login with Code ```kotlin theme={"system"} loginWithCode(code: String, phoneNumber: String?): Result ``` ### Parameters The one-time passcode sent to the user's phone number. (Optional) The user's phone number. Though this parameter is optional, it is highly recommended that you pass the user's phone number explicitly. If phone number is omitted, the phone number from sendCode will be used. ### Returns A Result object containing the PrivyUser if successful, or an error if the operation failed. ## Usage ```kotlin theme={"system"} // Send code to user's phone number val result: Result = privy.sms.sendCode(phoneNumber = "+14155552671") result.fold( onSuccess = { // OTP was successfully sent }, onFailure = { println("Error sending OTP: ${it.message}") } ) // Authenticate with the OTP code val result: Result = privy.sms.loginWithCode(code = "123456", phoneNumber = "+14155552671") result.fold( onSuccess = { user -> // User logged in }, onFailure = { println("Error logging in user: ${it.message}") } ) ``` To authenticate a user via their phone number, use the Privy client's `sms` handler. ## Send Code ```dart theme={"system"} sendCode(String phoneNumber): Future> ``` ### Parameters The phone number of the user to log in. Must be in E.164 format (e.g., "+14155552671"). ### Returns A Result object that indicates whether the operation was successful. Returns Result.success if the code was sent successfully, or Result.failure if there was an error. ## Login with Code ```dart theme={"system"} loginWithCode({required String code, String? phoneNumber}): Future> ``` ### Parameters The one-time passcode sent to the user's phone number. (Optional) The user's phone number. Though this parameter is optional, it is highly recommended that you pass the user's phone number explicitly. If phone number is omitted, the phone number from sendCode will be used. ### Returns A Result object containing the PrivyUser if successful, or an error if the operation failed. ## Usage ```dart theme={"system"} // Send code to user's phone number final Result result = await privy.sms.sendCode("+14155552671"); result.fold( onSuccess: (_) { // OTP was sent successfully }, onFailure: (error) { // Handle error sending OTP print(error.message); }, ); // Authenticate with the OTP code final Result result = await privy.sms.loginWithCode( code: code, phoneNumber: phoneNumber, ); result.fold( onSuccess: (user) { // User authenticated successfully }, onFailure: (error) { // Handle authentication error }, ); ``` ## Resources Get started with Flutter and Privy. # Wallet Source: https://docs.privy.io/authentication/user-authentication/login-methods/wallet Authenticate users by connecting an external wallet (MetaMask, Phantom, Coinbase) via SIWE or Solana sign-in For users who already have wallets, Privy supports signing in with Ethereum (SIWE) or Solana (SIWS). With this flow, users who are already onchain can bring their existing wallet to your app, verify ownership of assets, and take onchain actions. Enable wallet authentication in the [Privy Dashboard](https://dashboard.privy.io/apps?page=login-methods) before implementing this feature. To authenticate a user via an Ethereum wallet ([SIWE](https://eips.ethereum.org/EIPS/eip-4361)) without Privy UIs, use the React SDK's `useLoginWithSiwe` hook. In order to use Privy's login with wallet flow, users must actively have a wallet connected to your app from which you can request signatures. ## Generate SIWE message ```tsx theme={"system"} generateSiweMessage({ address: string, chainId: `eip155:${number}`, disableSignup?: boolean }) => Promise ``` ### Parameters EIP-55 checksum-encoded wallet address performing the signing. EIP-155 Chain ID to which the session is bound (in CAIP-2 format), e.g. `eip155:1`. Whether to disable signup for this login flow. ### Returns A SIWE message that can be signed by the wallet. ## Sign the SIWE message Request an EIP-191 `personal_sign` signature for the `message` returned by `generateSiweMessage` from the connected wallet. ```tsx theme={"system"} import { useWallets } from '@privy-io/react-auth'; const { wallets } = useWallets(); const signature = await wallets[0].sign(message); ``` ## Login with SIWE ```tsx theme={"system"} loginWithSiwe({ signature: string, message: string, disableSignup?: boolean }) => Promise ``` ### Parameters The EIP-191 signature corresponding to the message. The EIP-4361 message returned by `generateSiweMessage`. Whether to disable signup for the login flow. ### Returns The authenticated user. ## Usage ```tsx theme={"system"} import { useLoginWithSiwe, useWallets } from '@privy-io/react-auth'; export function LoginWithWalletButton() { const { generateSiweMessage, loginWithSiwe } = useLoginWithSiwe(); const { wallets } = useWallets(); const handleLogin = async () => { if (!wallets?.length) return; const activeWallet = wallets[0]; const message = await generateSiweMessage({ address: activeWallet.address, chainId: 'eip155:1', }); const signature = await activeWallet.sign(message); await loginWithSiwe({ signature, message }); }; return ( ); } ``` ## Callbacks You can optionally pass callbacks into `useLoginWithSiwe` to run custom logic after a successful login, or to handle errors that occur during the flow. ### `onComplete` ```tsx theme={"system"} onComplete?: (params: { user: User; isNewUser: boolean; wasAlreadyAuthenticated: boolean; loginMethod: LoginMethod | null; loginAccount: LinkedAccountWithMetadata | null; }) => void ``` #### Parameters The user object corresponding to the authenticated user. Whether the user is a new user or an existing user. Whether the user was already authenticated when the flow ran. The method used by the user to login (if applicable). The account corresponding to the login method. ### `onError` ```tsx theme={"system"} onError?: (error: PrivyErrorCode) => void ``` #### Parameters The error that occurred during the login flow. ### Usage ```tsx theme={"system"} import { useLoginWithSiwe } from '@privy-io/react-auth'; export function SiweWithCallbacks() { const { generateSiweMessage, loginWithSiwe } = useLoginWithSiwe({ onComplete: ({ user, isNewUser, wasAlreadyAuthenticated, loginMethod, loginAccount, }) => { // show a toast, update form errors, etc... }, onError: (error) => { // show a toast, update form errors, etc... }, }); // ... use generateSiweMessage and loginWithSiwe as shown above return null; } ``` ## Tracking login flow state The `state` variable returned from `useLoginWithSiwe` will always be one of the following values. ```tsx theme={"system"} type SiweFlowState = | { status: 'initial' } | { status: 'error'; error: Error | null } | { status: 'generating-message' } | { status: 'awaiting-signature' } | { status: 'submitting-signature' } | { status: 'done' }; ``` ### Sign in with Ledger #### EVM For EVM chains, Ledger is supported automatically when connecting through another wallet like MetaMask or Phantom. No additional configuration is required. #### Solana Ledger Solana hardware wallets only support transaction signatures, not the message signatures required for Sign-In With Solana (SIWS) authentication. In order to authenticate with a Solana Ledger wallet, you must mount the `useSolanaLedgerPlugin` hook **inside** your `PrivyProvider`. **Critical:** The `useSolanaLedgerPlugin` hook **must be placed inside** a component that is wrapped by `PrivyProvider`. If the hook is placed alongside or outside the `PrivyProvider`, it will not function correctly. ```tsx theme={"system"} import {PrivyProvider} from '@privy-io/react-auth'; import {useSolanaLedgerPlugin} from '@privy-io/react-auth/solana'; function SolanaLedgerSetup() { // This hook MUST be called inside a component wrapped by PrivyProvider useSolanaLedgerPlugin(); return null; } export default function App() { return ( {/* Your app components */} ); } ``` Then, when you attempt to login with a Phantom Solana wallet, you will be prompted to indicate whether you are signing with a Ledger wallet, which will initiate a separate SIWS flow wherein which a no-op transaction will be signed and used for verification. #### Headless Solana Ledger (useLoginWithSiws) When using `useLoginWithSiws` directly, use `generateSiwsOffchainMessage` to wrap the SIWS message in the [Solana off-chain message format](https://solana.com/developers/guides/advanced/off-chain-message-signing) that Ledger requires. Then pass `messageType: 'offchain-message'` to `loginWithSiws`. ```tsx theme={"system"} import {useLoginWithSiws} from '@privy-io/react-auth'; import {useWallets} from '@privy-io/react-auth/solana'; export function LoginWithLedgerButton() { const {generateSiwsMessage, generateSiwsOffchainMessage, loginWithSiws} = useLoginWithSiws(); const {wallets} = useWallets(); const handleLogin = async () => { if (!wallets?.length) return; const wallet = wallets[0]; // 1. Generate the plaintext SIWS message const message = await generateSiwsMessage({address: wallet.address}); // 2. Wrap in the Solana off-chain format that Ledger requires const offchainBytes = generateSiwsOffchainMessage({message, address: wallet.address}); // 3. Sign the off-chain bytes with the Ledger-connected wallet const {signature} = await wallet.signMessage({message: offchainBytes}); // 4. Submit the original plaintext message with messageType: 'offchain-message' await loginWithSiws({message, signature, messageType: 'offchain-message'}); }; return ; } ``` Pass the original **plaintext `message`** from step 1 to `loginWithSiws`, not the off-chain bytes. Privy's backend reconstructs the off-chain bytes from the plaintext message for signature verification. To authenticate a user via a Solana wallet *([SIWS](https://github.com/phantom/sign-in-with-solana))* without Privy UIs, use the React SDK's `useLoginWithSiws` hook. In order to use Privy's login with wallet flow, users must actively have a wallet connected to your app from which you can request signatures. ## Generate SIWS message ```tsx theme={"system"} generateSiwsMessage({ address: string }) => Promise ``` ### Parameters The base58-encoded Solana wallet address performing the signing. ### Returns A SIWS message that can be signed by the wallet. ## Sign the SIWS message Request a signature for the `message` returned by `generateSiwsMessage` from the connected Solana wallet. ```tsx theme={"system"} import {useWallets} from '@privy-io/react-auth/solana'; const {wallets} = useWallets(); const encodedMessage = new TextEncoder().encode(message); const {signature} = await wallets[0].signMessage({message: encodedMessage}); ``` ## Login with SIWS ```tsx theme={"system"} loginWithSiws({ signature: string, message: string, disableSignup?: boolean }) => Promise ``` ### Parameters The signature corresponding to the SIWS message. The SIWS message returned by `generateSiwsMessage`. Whether to disable signup for the login flow. ### Returns The authenticated user. ## Usage ```tsx theme={"system"} import {useLoginWithSiws} from '@privy-io/react-auth'; import {useWallets} from '@privy-io/react-auth/solana'; export function LoginWithSolanaWalletButton() { const {generateSiwsMessage, loginWithSiws} = useLoginWithSiws(); const {wallets} = useWallets(); const handleLogin = async () => { if (!wallets?.length) return; const activeWallet = wallets[0]; const message = await generateSiwsMessage({ address: activeWallet.address, }); const encodedMessage = new TextEncoder().encode(message); const {signature} = await activeWallet.signMessage({message: encodedMessage}); await loginWithSiws({signature, message}); }; return ( ); } ``` ## Resources Get started with React and Privy. Get started with Next.js and Privy. Get started with a whitelabel Privy integration. To authenticate a user via an Ethereum wallet *([SIWE](https://eips.ethereum.org/EIPS/eip-4361))*, use the React Native SDK's `useLoginWithSiwe` hook. In order to use Privy's login with wallet flow, users must actively have a wallet connected to your app from which you can request signatures. ## Generate SIWE message ```tsx theme={"system"} generateSiweMessage({wallet: {chainId: string, address: string}, from: {domain: string, uri: string}}) => Promise ``` ### Parameters Wallet object containing EIP-55 compliant wallet address and chainId in CAIP-2 format. The chain ID of the wallet. The address of the wallet. Origin object containing domain and uri. The domain of the origin. Must be allowlisted in the Privy dashboard. The uri of the origin. ### Returns A SIWE message that can be signed by the wallet. ### Usage ```tsx theme={"system"} import {useLoginWithSiwe} from '@privy-io/expo'; export function LoginScreen() { const [address, setAddress] = useState(''); const [message, setMessage] = useState(''); const {generateSiweMessage} = useLoginWithSiwe(); const handleGenerate = async () => { const message = await generateSiweMessage({ from: { domain: 'my-domain.com', // domain must be allowlisted in the Privy dashboard. uri: 'https://my-domain.com', }, wallet: { // sepolia chainId with CAIP-2 prefix chainId: `eip155:11155111`, address, }, }); setMessage(message); }; return ( {Boolean(message) && {message}} ); } ``` ## Sign the [SIWE message](https://eips.ethereum.org/EIPS/eip-4361) Then, request an [ EIP-191 ](https://eips.ethereum.org/EIPS/eip-191) `personal_sign` signature for the `message` returned by `generateSiweMessage`, from a connected wallet. There are many ways to connect a wallet to a mobile app, a few good options are: * [Mobile Wallet Protocol](https://mobilewalletprotocol.github.io/wallet-mobile-sdk/) * [Metamask React Native SDK](https://docs.metamask.io/wallet/how-to/use-sdk/javascript/react-native/) * [WalletConnectClient SDK](https://github.com/WalletConnect/react-native-examples) ## Login with SIWE ```tsx theme={"system"} loginWithSiwe({signature: string, messageOverride?: string, disableSignup?: boolean}) => Promise> ``` ### Parameters The signature of the SIWE message, signed by the user's wallet. An optional override for the message that is signed. If true, the user will not be automatically created if they do not exist in the Privy database. ### Returns A PrivyUser object containing the user's information. ## Usage ```tsx theme={"system"} import {useLoginWithSiwe, usePrivy} from '@privy-io/expo'; export function LoginScreen() { const [signature, setSignature] = useState(''); const {user} = usePrivy(); const {loginWithSiwe} = useLoginWithSiwe(); if (user) { return ( <> Logged In {JSON.stringify(user, null, 2)} ); } return ( ); } ``` ## Callbacks You can optionally pass callbacks into the `useLoginWithSiwe` hook to run custom logic after a message has been generated, after a successful login, or to handle errors that occur during the flow. ### `onGenerateMessage` ```tsx theme={"system"} onGenerateMessage?: ((message: string) => void) | undefined ``` #### Parameters The SIWE message that was generated. ### `onSuccess` ```tsx theme={"system"} onSuccess?: ((user: PrivyUser, isNewUser: boolean) => void) | undefined ``` #### Parameters The user object corresponding to the authenticated user. Whether the user is a new user or an existing user. ### `onError` ```tsx theme={"system"} onError?: (error: Error) => void ``` #### Parameters The error that occurred during the login flow. ## Usage ```tsx theme={"system"} import {useLoginWithSiwe} from '@privy-io/expo'; export function LoginScreen() { const {generateSiweMessage, loginWithSiwe} = useLoginWithSiwe({ onGenerateMessage(message) { // show a toast, send analytics event, etc... }, onSuccess(user, isNewUser) { // show a toast, send analytics event, etc... }, onError(error) { // show a toast, update form errors, etc... }, }); // ... } ``` ## Tracking login flow state The `state` variable returned from `useLoginWithSiwe` will **always be one** of the following values. ```tsx theme={"system"} type SiweFlowState = | { status: "initial" } | { status: "error"; error: Error | null } | { status: "generating-message" } | { status: "awaiting-signature" } | { status: "submitting-signature" } | { status: "done" }; ``` To authenticate a user via a Solana wallet *([SIWS](https://github.com/phantom/sign-in-with-solana))*, use the React Native SDK's `useLoginWithSiws` hook. In order to use Privy's login with wallet flow, users must actively have a wallet connected to your app from which you can request signatures. ## Generate SIWS message ```tsx theme={"system"} generateMessage({wallet: {address: string}, from: {domain: string, uri: string}}) => Promise<{message: string}> ``` ### Parameters Wallet object containing Solana wallet address. The address of the wallet. Origin object containing domain and uri. The domain of the origin. The uri of the origin. ### Returns A SIWS message that can be signed by the wallet. ### Usage ```tsx theme={"system"} import {useLoginWithSiws} from '@privy-io/expo'; export function LoginScreen() { const [address, setAddress] = useState(''); const [message, setMessage] = useState(''); const {generateMessage} = useLoginWithSiws(); const handleGenerate = async () => { const {message} = await generateMessage({ from: { domain: 'my-domain.com', uri: 'https://my-domain.com', }, wallet: { address, }, }); setMessage(message); }; return ( {Boolean(message) && {message}} ); } ``` ## Sign the SIWS message Then, request a signature for the `message` returned by `generateMessage`, from a connected wallet. ## Login with SIWS ```tsx theme={"system"} login({signature: string, message: string, wallet: {walletClientType: string, connectorType: string}, disableSignup?: boolean}) => Promise> ``` ### Parameters The signature of the SIWS message, signed by the user's wallet. The original message that was signed. The client of the connected wallet (e.g. 'phantom'). The type of the connector (e.g. 'wallet\_connect' or 'mobile\_wallet\_protocol'). If true, the user will not be automatically created if they do not exist in the Privy database. ### Returns A PrivyUser object containing the user's information. ## Usage ```tsx theme={"system"} import {useLoginWithSiws, usePrivy} from '@privy-io/expo'; export function LoginScreen() { const [signature, setSignature] = useState(''); const {user} = usePrivy(); const {login} = useLoginWithSiws(); if (user) { return ( <> Logged In {JSON.stringify(user, null, 2)} ); } return ( ); } ``` ## Resources Get started with Expo and Privy. Get started with Expo bare and Privy. To authenticate a user via an Ethereum wallet *([SIWE](https://eips.ethereum.org/EIPS/eip-4361))*, use the Privy client's `siwe` handler. ## Generate SIWE message ```swift theme={"system"} func generateMessage(params: SiweMessageParams) async throws -> String ``` ### Parameters Set of parameters required to generate the message. Your app's domain. e.g. "my-domain.com" Your app's URI. e.g. "[https://my-domain.com](https://my-domain.com)" EVM Chain ID, e.g. "1" for Ethereum Mainnet The user's [ERC-55](https://eips.ethereum.org/EIPS/eip-55) compliant wallet address. ### Returns A SIWE message that can be signed by the wallet. ### Usage ```swift theme={"system"} do { let params = SiweMessageParams( appDomain: "my-domain.com", appUri: "https://my-domain.com", chainId: "1", walletAddress: "0x12345..." ) let siweMessage = try await privy.siwe.generateMessage(params: params) } catch { // An error can be thrown if the network call to generate the message fails, // or if invalid metadata was passed in. } ``` ## Sign the SIWE message Using the message returned by `generateMessage`, request an EIP-191 `personal_sign` signature from the user's connected wallet. You should do this using the library your app uses to connect to external wallets (e.g. the MetaMask iOS SDK or WalletConnect). Once the user successfully signs the message, pass it into `login`. ## Login with SIWE ```swift theme={"system"} func login( message: String, signature: String, params: SiweMessageParams, metadata: WalletLoginMetadata? ) async throws -> PrivyUser ``` ### Parameters The message returned from "generateMessage". The signature of the SIWE message, signed by the user's wallet. The same SiweMessageParams passed into "generateMessage". (Optional) you can pass additional metadata that will be stored with the linked wallet. An enum specifying the type of wallet used to login. e.g. WalletClientType.metamask A string identifying how wallet was connected. e.g. "wallet\_connect" ### Returns The authenticated Privy user ### Throws An error if logging the user in is unsuccessful. ## Usage ```swift theme={"system"} do { let params = SiweMessageParams( appDomain: "my-domain.com", appUri: "https://my-domain.com", chainId: "1", walletAddress: "0x12345..." ) // Generate SIWE message let siweMessage = try await privy.siwe.generateMessage(params: siweParams) // Optional metadata let metadata = WalletLoginMetadata( walletClientType: WalletClientType.metamask, connectorType: "wallet_connect" ) // Login try await privy.siwe.login( message: siweMessage, // the signature generated by the user's wallet signature: signature, params: siweParams, metadata: metadata ) } catch { // error logging user in } ``` To authenticate a user via a Solana wallet *([SIWS](https://github.com/phantom/sign-in-with-solana))*, use the Privy client's `siws` handler. ## Generate SIWS message ```swift theme={"system"} func generateMessage(params: SiwsMessageParams) async throws -> String ``` ### Parameters The parameters For building a Sign-In with Solana (SIWS) message. The domain that is requesting the signing. Its value MUST be an *[RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986) authority*. An *[RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986) URI* referring to the resource that is the subject of the signing (as in the subject of a claim). Solana address performing the sign-in. The address is case-sensitive. ### Returns A unique SIWS message as a string ### Usage ```swift theme={"system"} do { let params = SiwsMessageParams( domain: "my-domain.com", uri: "https://my-domain.com", address: "insert-solana-wallet-address" ) let message = try await privy.siws.generateMessage(params: params) } catch { // An error can be thrown if the network call to generate the message fails, // or if invalid parameters are passed in. print(error) } ``` ## Sign the SIWS message Using the message returned by `generateMessage`, request a signature for the `message` returned by `generateMessage`, from a connected wallet. You should do this using the library your app uses to connect to external wallets (e.g. WalletConnect). Once the user successfully signs the message, pass it into `login`. ## Login with SIWS ```swift theme={"system"} func login( message: String, signature: String, metadata: WalletLoginMetadata? ) async throws -> PrivyUser ``` ### Parameters The previously generated SIWS message. The SIWS signature generated by the wallet. (Optional) Additional metadata specifying wallet client and connector type. An enum specifying the type of wallet used to login. e.g. WalletClientType.metamask A string identifying how wallet was connected. e.g. "wallet\_connect" ### Returns The authenticated Privy user ## Usage ```swift theme={"system"} do { let params = SiwsMessageParams( domain: "my-domain.com", uri: "https://my-domain.com", address: "insert-solana-wallet-address" ) // Generate SIWS message let message = try await privy.siws.generateMessage(params: params) // Login try await privy.siws.login( message: message, // the signature generated by the user's wallet signature: signature ) } catch { // Failed to login with SIWS print(error) } ``` ## Linking a wallet with SIWS If instead of logging in, you want to link a Solana wallet to an already authenticated user, you can use the `link` method in the `siws` handler, having generated the SIWS message in the same way. ```swift theme={"system"} func link( message: String, signature: String, metadata: WalletLoginMetadata? ) async throws ``` To authenticate a user via an Ethereum wallet *([SIWE](https://eips.ethereum.org/EIPS/eip-4361))*, use the Privy client's `siwe` handler. ## Generate SIWE message ```kotlin theme={"system"} public suspend fun generateMessage(params: SiweMessageParams): Result ``` ### Parameters Set of parameters required to generate the message. Your app's domain. e.g. "my-domain.com" Your app's URI. e.g. "[https://my-domain.com](https://my-domain.com)" EVM Chain ID, e.g. "1" for Ethereum Mainnet The user's [ERC-55](https://eips.ethereum.org/EIPS/eip-55) compliant wallet address. ### Returns A result type encapsulating the SIWE message that can be signed by the wallet on success. ### Usage ```kotlin theme={"system"} val params = SiweMessageParams( appDomain = domain, appUri = uri, chainId = chainId, walletAddress = walletAddress ) privy.siwe.generateMessage(params = params).fold( onSuccess = { message -> // request an EIP-191 `personal_sign` signature on the message }, onFailure = { e -> // An error can be thrown if the network call to generate the message fails, // or if invalid metadata was passed in. } ) ``` ## Sign the SIWE message Using the message returned by `generateMessage`, request an EIP-191 `personal_sign` signature from the user's connected wallet. You should do this using the library your app uses to connect to external wallets (e.g. the MetaMask SDK or WalletConnect). Once the user successfully signs the message, pass the signature into the `login` function. ## Login with SIWE ```kotlin theme={"system"} public suspend fun login( message: String, signature: String, params: SiweMessageParams, metadata: WalletLoginMetadata?, ): Result ``` ### Parameters The message returned from "generateMessage". The signature of the SIWE message, signed by the user's wallet. The same SiweMessageParams passed into "generateMessage". (Optional) you can pass additional metadata that will be stored with the linked wallet. An enum specifying the type of wallet used to login. e.g. WalletClientType.Metamask A string identifying how wallet was connected. e.g. "wallet\_connect" ### Returns A result type ecapsulating the PrivyUser on success. ## Usage ```kotlin theme={"system"} val params = SiweMessageParams( appDomain = domain, appUri = uri, chainId = chainId, walletAddress = walletAddress ) // optional metadata val metadata = WalletLoginMetadata(walletClientType = walletClient, connectorType = connectorType) privy.siwe.login(message, signature, params, metadata).fold( onSuccess = { privyUser -> // Login success }, onFailure = { e -> // Login failure, either due to invalid signature or network error } ) ``` Want to link a wallet to an existing user? Check out the [linking accounts documentation](/user-management/users/linking-accounts#external-wallets). To authenticate a user via a Solana wallet ([SIWS](https://siws.web3auth.io/)), use the Privy client's `siws` handler. ## Generate SIWS message ```kotlin theme={"system"} public suspend fun generateMessage(params: SiwsMessageParams): Result ``` ### Parameters Set of parameters required to generate the message. Your app's domain. e.g. "my-domain.com" Your app's URI. e.g. "[https://my-domain.com](https://my-domain.com)" The user's Solana wallet address. ### Returns A result type encapsulating the SIWS message that can be signed by the wallet on success. ### Usage ```kotlin theme={"system"} val params = SiwsMessageParams( appDomain = domain, appUri = uri, walletAddress = walletAddress ) privy.siws.generateMessage(params = params).fold( onSuccess = { message -> // request a Solana wallet signature on the message }, onFailure = { e -> // An error can be thrown if the network call to generate the message fails, // or if invalid metadata was passed in. } ) ``` ## Sign the SIWS message Using the message returned by `generateMessage`, request a signature from the user's connected Solana wallet. You should do this using the library your app uses to connect to external wallets (e.g. the Solana Mobile SDK or Phantom). Once the user successfully signs the message, pass the signature into the login function. ## Login with SIWS ```kotlin theme={"system"} public suspend fun login( message: String, signature: String, params: SiwsMessageParams, metadata: WalletLoginMetadata?, ): Result ``` ### Parameters The message returned from "generateMessage". The signature of the SIWS message, signed by the user's wallet. The same SiwsMessageParams passed into "generateMessage". (Optional) you can pass additional metadata that will be stored with the linked wallet. The client of the connected wallet (e.g. "phantom"). A string identifying how wallet was connected e.g. "wallet\_connect" ### Returns A result type encapsulating the PrivyUser on success. ### Usage ```kotlin theme={"system"} val params = SiwsMessageParams( appDomain = domain, appUri = uri, walletAddress = walletAddress ) // optional metadata val metadata = WalletLoginMetadata( walletClientType = walletClient, connectorType = connectorType ) privy.siws.login(message, signature, params, metadata).fold( onSuccess = { privyUser -> // Login success }, onFailure = { e -> // Login failure, either due to invalid signature or network error } ) ``` Want to link a wallet to an existing user? Check out the [linking accounts documentation](/user-management/users/linking-accounts#external-wallets). To authenticate a user via an Ethereum wallet *([SIWE](https://eips.ethereum.org/EIPS/eip-4361))*, use the Privy client's `siwe` handler. ## Generate SIWE message ```dart theme={"system"} Future> generateMessage(SiweMessageParams params) ``` ### Parameters Set of parameters required to generate the message. Your app's domain. e.g. "my-domain.com" Your app's URI. e.g. "[https://my-domain.com](https://my-domain.com)" EVM Chain ID, e.g. "1" for Ethereum Mainnet The user's [ERC-55](https://eips.ethereum.org/EIPS/eip-55) compliant wallet address. ### Returns A result type encapsulating the SIWE message that can be signed by the wallet on success. ### Usage ```dart theme={"system"} final params = SiweMessageParams( appDomain: domain, appUri: uri, chainId: chainId, walletAddress: walletAddress, ); final result = await privy.siwe.generateMessage(params); result.fold( onSuccess: (message) { // request an EIP-191 `personal_sign` signature on the message }, onFailure: (error) { // An error can be thrown if the network call to generate the message fails, // or if invalid metadata was passed in. }, ); ``` ## Sign the SIWE message Using the message returned by `generateMessage`, request an EIP-191 `personal_sign` signature from the user's connected wallet. You should do this using the library your app uses to connect to external wallets (e.g. the MetaMask SDK or WalletConnect). Once the user successfully signs the message, pass the signature into the `login` function. ## Login with SIWE ```dart theme={"system"} Future> login({ required String message, required String signature, required SiweMessageParams params, WalletLoginMetadata? metadata, }) ``` ### Parameters The message returned from "generateMessage". The signature of the SIWE message, signed by the user's wallet. The same SiweMessageParams passed into "generateMessage". (Optional) you can pass additional metadata that will be stored with the linked wallet. An enum specifying the type of wallet used to login. e.g. WalletClientType.metamask A string identifying how wallet was connected. e.g. "wallet\_connect" ### Returns A result type encapsulating the PrivyUser on success. ### Usage ```dart theme={"system"} final params = SiweMessageParams( appDomain: domain, appUri: uri, chainId: chainId, walletAddress: walletAddress, ); // optional metadata final metadata = WalletLoginMetadata( walletClientType: walletClient, connectorType: connectorType, ); final result = await privy.siwe.login( message: message, signature: signature, params: params, metadata: metadata, ); result.fold( onSuccess: (privyUser) { // Login success }, onFailure: (error) { // Login failure, either due to invalid signature or network error }, ); ``` To authenticate a user via a Solana wallet *([SIWS](https://github.com/phantom/sign-in-with-solana))*, use the Privy client's `siws` handler. ## Generate SIWS message ```dart theme={"system"} Future> generateMessage(SiwsMessageParams params) ``` ### Parameters Set of parameters required to generate the message. Your app's domain. e.g. "my-domain.com" Your app's URI. e.g. "[https://my-domain.com](https://my-domain.com)" The user's base58-encoded Solana wallet address. ### Returns A result type encapsulating the SIWS message that can be signed by the wallet on success. ### Usage ```dart theme={"system"} final params = SiwsMessageParams( appDomain: domain, appUri: uri, walletAddress: walletAddress, ); final result = await privy.siws.generateMessage(params); result.fold( onSuccess: (message) { // request a signature on the message using the Solana wallet }, onFailure: (error) { // An error can be thrown if the network call to generate the message fails, // or if invalid metadata was passed in. }, ); ``` ## Sign the SIWS message Using the message returned by `generateMessage`, request a signature from the user's connected Solana wallet. You should do this using the library your app uses to connect to Solana wallets (e.g. the Phantom SDK or Solana Mobile Wallet Adapter). Once the user successfully signs the message, pass the signature into the `login` function. ## Login with SIWS ```dart theme={"system"} Future> login({ required String message, required String signature, required SiwsMessageParams params, WalletLoginMetadata? metadata, }) ``` ### Parameters The message returned from "generateMessage". The signature of the SIWS message, signed by the user's wallet. The same SiwsMessageParams passed into "generateMessage". (Optional) you can pass additional metadata that will be stored with the linked wallet. An enum specifying the type of wallet used to login. e.g. WalletClientType.phantom A string identifying how wallet was connected. e.g. "solana\_mobile\_wallet\_adapter" ### Returns A result type encapsulating the PrivyUser on success. ### Usage ```dart theme={"system"} final params = SiwsMessageParams( appDomain: domain, appUri: uri, walletAddress: walletAddress, ); // optional metadata final metadata = WalletLoginMetadata( walletClientType: walletClient, connectorType: connectorType, ); final result = await privy.siws.login( message: message, signature: signature, params: params, metadata: metadata, ); result.fold( onSuccess: (privyUser) { // Login success }, onFailure: (error) { // Login failure, either due to invalid signature or network error }, ); ``` # Logging users out Source: https://docs.privy.io/authentication/user-authentication/logout End a user authenticated session and log them out using the logout method from usePrivy Logging out a user ends their authenticated session, removing their access credentials from the device and requiring them to authenticate again to access protected resources. ```tsx theme={"system"} logout: () => Promise ``` ### Usage To log a user out, use the `logout` method from the `usePrivy` hook: ```tsx theme={"system"} import { usePrivy } from '@privy-io/react-auth'; function LogoutButton() { const { ready, authenticated, logout } = usePrivy(); // Disable logout when Privy is not ready or the user is not authenticated const disableLogout = !ready || (ready && !authenticated); return ( ); } ``` ### Callbacks You can attach callbacks to the logout process using the `useLogout` hook: ```tsx theme={"system"} import { useLogout } from '@privy-io/react-auth'; function LogoutButton() { const { logout } = useLogout({ onSuccess: () => { console.log('User successfully logged out'); // Redirect to landing page or perform other post-logout actions }, onError: (error) => { console.error('Logout failed', error); } }); return ; } ``` ```tsx theme={"system"} logout: () => Promise ``` ### Usage To log a user out, use the `logout` method from the `usePrivy` hook: ```tsx theme={"system"} import { usePrivy } from '@privy-io/expo'; function LogoutButton() { const { logout } = usePrivy(); return ; } ``` ### Async Handling Since `logout` returns a Promise, you can await it to run code after the user has been logged out: ```tsx theme={"system"} import { usePrivy } from '@privy-io/expo'; function LogoutButton() { const { logout } = usePrivy(); const handleLogout = async () => { await logout(); // Perform actions after logout completes console.log('User logged out successfully'); }; return ; } ``` ```swift theme={"system"} func logout() ``` ### Usage To log out an authenticated user, call the `logout` method on the user object: ```swift theme={"system"} privy.user.logout() ``` ### Example ```swift theme={"system"} import PrivySDK class ProfileViewController: UIViewController { @IBAction func logoutButtonTapped(_ sender: UIButton) { // Check if user is authenticated if let user = privy.user { user.logout() // Navigate back to login screen self.navigationController?.popToRootViewController(animated: true) } } } ``` ### Effect This will clear the user state and delete the persisted user session. ```kotlin theme={"system"} suspend fun logout() ``` ### Usage To log out an authenticated user, call the `logout` method: ```kotlin theme={"system"} coroutineScope.launch { privy.logout() } ``` ### Example ```kotlin theme={"system"} import io.privy.android.Privy import kotlinx.coroutines.launch import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers class ProfileActivity : AppCompatActivity() { private val coroutineScope = CoroutineScope(Dispatchers.Main) private fun setupLogoutButton() { logoutButton.setOnClickListener { coroutineScope.launch { privy.logout() // Navigate back to login activity startActivity(Intent(this@ProfileActivity, LoginActivity::class.java)) finish() } } } } ``` ### Effect This will clear the user state and delete the persisted user session. ```dart theme={"system"} Future logout() ``` ### Usage To log out an authenticated user, call the `logout` method: ```dart theme={"system"} await privy.logout(); ``` ### Example ```dart theme={"system"} import 'package:flutter/material.dart'; import 'package:privy_flutter/privy_flutter.dart'; class ProfileScreen extends StatelessWidget { final Privy privy; const ProfileScreen({Key? key, required this.privy}) : super(key: key); @override Widget build(BuildContext context) { return Scaffold( appBar: AppBar(title: Text('Profile')), body: Center( child: ElevatedButton( onPressed: () async { await privy.logout(); // Navigate back to login screen Navigator.of(context).pushReplacementNamed('/login'); }, child: Text('Log out'), ), ), ); } } ``` ### Effect This will clear the user state and delete the persisted user session. # Overview Source: https://docs.privy.io/authentication/user-authentication/mfa/custom-ui/overview Build custom MFA UI flows that match your app design for enrollment and verification. Build custom MFA flows that match your app's design and user experience. Custom UIs give your app complete control over how users enroll in and verify MFA. If you are using the **React** or **React Native** SDK, you can optionally use [Privy's default UIs](/authentication/user-authentication/mfa/default-ui) for MFA instead of building custom flows. ## When to use custom UIs Custom MFA UIs are appropriate when your app: * Uses an SDK other than React or React Native (Swift, Android, Flutter, Unity) * Requires a fully branded MFA experience that matches your app's design system * Needs custom flows or logic around MFA enrollment and verification * Integrates with existing authentication UIs in your app ## Getting started Building a custom MFA experience involves three main areas: Allow users to enroll in MFA with SMS, TOTP, or passkeys Guide users through completing MFA when required Know when MFA is required and trigger your custom UI ## Additional resources Allow users to remove enrolled MFA methods Handle errors during MFA flows Use Privy's built-in MFA UI instead # Using default Privy UIs Source: https://docs.privy.io/authentication/user-authentication/mfa/default-ui Use Privy built-in default UI components for MFA enrollment and verification in React Enable MFA methods in the [Privy Dashboard](https://dashboard.privy.io/apps?page=login-methods\&logins=mfa) before implementing this feature. Privy's default MFA UIs are available for **React and React Native only**. If you are using another SDK (Swift, Android, Flutter, Unity), use the [custom UI approach](/authentication/user-authentication/mfa/custom-ui/overview) instead. ### Enrolling users in MFA Once you have enabled MFA for your app, **to prompt your users to enroll in MFA for their embedded wallet, use the `showMfaEnrollmentModal` method from the `useMfaEnrollment` hook.** ```tsx Example button for enrolling in wallet MFA theme={"system"} import {useMfaEnrollment} from '@privy-io/react-auth'; function MfaEnrollmentButton() { const {showMfaEnrollmentModal} = useMfaEnrollment(); return ; } ``` When invoked, will open a Privy modal that prompts the user to select their desired MFA method from the ones you've enabled for your app, and will guide them through the enrollment process. images/MFA.png Once a user has successfully enrolled an MFA method, the user's enrolled method will appear under the field of their object: ```tsx theme={"system"} const {user} = usePrivy(); console.log(user.mfaMethods); // ['sms', 'totp', 'passkey'] for a user who has enrolled in all of SMS, TOTP, and passkey MFA ``` ### Managing MFA methods To allow your users to modify their MFA methods, **simply invoke the `showMfaEnrollmentModal` method from the `useMfaEnrollment` hook.** This is the *same* method you would use to prompt your user to enroll in MFA for the first time. Within this modal, users can **remove existing MFA methods or enroll in additional ones** for their embedded wallet. Prior to making changes to their MFA methods, users will be prompted to re-verify their identity using one of their existing MFA methods. By default, removing a passkey as MFA will also unlink it as a valid login method. In order to modify this behavior, you can set the `shouldUnlinkOnUnenrollMfa` option to `false` under the `passkeys` config in `PrivyProvider`. ```tsx theme={"system"} {/* your app's content */} ``` ### Authorizing signatures and transactions Once a user has enrolled in MFA, **every attempt to use the wallet's private key (every signature or transaction) will require the user to complete MFA using their method.** This logic is automatic; you do not need to do anything else once your user has enrolled in wallet MFA. When your app requests a signature or a transaction from the embedded wallet, **Privy will show the user a modal prompting them to enter a 6-digit MFA code sent to their MFA method.** If the user has enrolled in multiple MFA methods, they can choose which method they'd like to use for this given request. Authorizing signatures and transactions with wallet MFA Users must enter their MFA code within 5 minutes of receiving it, and are allowed up to a maximum of 4 code attempts if they incorrectly enter their code. **If the user correctly enters their MFA code, the signature or transaction request will be processed by the wallet.** Additionally, their MFA verification status will be cached for **15 minutes**. This means that for additional signatures or transactions requested within this window, Privy will **not** prompt the user to re-complete MFA. **If the user does not complete MFA or enters in an incorrect code 4 times or more, the signature or transaction will raise an error as if the user rejected the request.** ### Manually prompting for MFA verification If you want to manually prompt your users for MFA verification, you can use the `useMfa` hook. The `useMfa` hook provides a set of functions and state for managing MFA flows in your application. It allows you to: * Prompt the user to complete MFA verification if required. * Initialize and submit MFA challenges (e.g., sending and verifying codes). * Cancel and ongoing MFA flow. * Access the list of available MFA methods for your app. Here's an example of how to use the `useMfa` hook to prompt for MFA verification: ```tsx Example button for prompting MFA verification theme={"system"} import {useMfa} from '@privy-io/react-auth'; function MfaVerificationButton() { const {promptMfa, init} = useMfa(); const handleMfaVerification = async () => { try { await init('totp') await promptMfa(); // Your code on successful MFA verification } catch (error) { // Your code on cancelled or otherwise unsuccessful MFA verification } }; return ; } ``` In this example, when the user clicks the "Verify MFA" button, the `promptMfa` function is called. If the user has enrolled in MFA, they will be prompted to complete the verification process. If successful, you can execute your desired actions after the verification. ### Setting up MFA UIs in React Native Once you've set up `PrivyElements`, you can use Privy's default UIs to enroll your users in MFA and prompt them for MFA during wallet actions. Before integrating MFA UIs, make sure to also set up multi-factor authentication in the dashboard per [this guide](/authentication/user-authentication/mfa). ### MFA enrollment Privy's default UIs for React Native can also be used for allowing your users to enroll their first MFA verification method, or any number of additional ones. For this, use the `useMfaEnrollmentUI` hook to get an `init` method, that you can use to launch the flow. This method takes in a configuration object with the following fields: This will be the array of mfa methods that will be available in the UI. Make sure to have the methods you set here enabled. It should be the URL origin where your [Apple App Site Association](/authentication/user-authentication/login-methods/passkey) or [Digital Asset Links](/authentication/user-authentication/login-methods/passkey) are available (e.g. `https://example.com`). Required to offer `passkey` as an enrollment method. This method does not fall back to the `relyingParty` set on `PrivyProvider`, so pass it here explicitly. ```tsx theme={"system"} import {useMfaEnrollmentUI} from '@privy-io/expo/ui'; export function EnrollMFAMethodButton() { const {init: initMfaEnrollmentUI} = useMfaEnrollmentUI(); const onEnrollMfa = async () => { try { await initMfaEnrollmentUI({ mfaMethods: ['sms', 'totp', 'passkey'], relyingParty: 'https://example.com', }); // Your code on actions to execute after successful mfa enrollment } catch (error) { // Your code on cancelled or otherwise unsuccessful mfa enrollment } }; return ; } ``` `passkey` is only offered in the enrollment UI when the user already has a passkey linked as a login method, and `relyingParty` is set. Otherwise it is omitted from the method list without an error. To let users enroll a passkey for MFA, first link one as a login method with [`useLinkWithPasskey`](/authentication/user-authentication/login-methods/passkey). The UIs that will show up using `useMfaEnrollmentUI` will also **allow the user to unenroll** MFA methods. ### Passkey unenrollment Using the MFA enrollment UIs means users will also be able to **unenroll** a method they had previously set up too. For passkeys, the default behavior when unenrolling is that the passkey is also **removed** as a valid login method. You can change this behavior in the MFA UIs, by setting the `shouldUnlinkOnUnenrollMfa` option in the `PrivyElements` component: ```tsx theme={"system"} import {PrivyElements} from '@privy-io/expo/ui'; export default function RootLayout() { return ( <> {/* Your app's content */} ); } ``` ### MFA verification Privy's default UIs for React Native can be used for your users to verify their already set MFA methods, such as SMS or passkeys. You can use Privy's default UIs for MFA verification even if you're using headless flows for working with the wallet. This way, Privy's UIs can integrate smoothly with your custom flows and experiences. To do this, you must enable the `enableMfaVerificationUIs` option on the `PrivyElements` component: ```tsx theme={"system"} import {PrivyElements} from '@privy-io/expo/ui'; export default function RootLayout() { return ( <> {/* Your app's content */} ); } ``` After doing this, all operations you do on the wallet (such as signing messages or preparing transactions) will automatically trigger the MFA UIs if MFA verification is required at that moment. If you were using the `useRegisterMfaListener` hook before you should now remove it from your codebase, as Privy will handle the MFA events and UI for you. ### Manually prompting for MFA verification If you want to manually prompt your users for MFA verification, you can use the `useMfa` hook. The `useMfa` hook provides a set of functions and state for managing Multi-Factor Authentication (MFA) flows in your application. It allows you to: * Prompt the user to complete MFA verification if required. * Initialize and submit MFA challenges (e.g., sending and verifying codes). * Cancel and ongoing MFA flow. * Access the list of available MFA methods for your app. Here's an example of how to use the `useMfa` hook to prompt for MFA verification: ```tsx Example button for prompting MFA verification theme={"system"} import {useMfa} from '@privy-io/expo'; import {Button} from 'react-native'; export function MfaVerificationButton() { const {prompt, init} = useMfa(); const handleMfaVerification = async () => { try { await init({ mfaMethods: ['sms', 'totp', 'passkey']}); await prompt(); // Your code on successful MFA verification } catch (error) { // Your code on cancelled or otherwise unsuccessful MFA verification } }; return ); } ``` ## Setup To enroll users in MFA with passkeys, use the `initMfaEnrollment` and `submitMfaEnrollment` methods returned by the `useMfaEnrollment` hook: ```tsx theme={"system"} import {useMfaEnrollment} from '@privy-io/expo'; const {initMfaEnrollment, submitMfaEnrollment} = useMfaEnrollment(); ``` ## Initiating enrollment First, initiate enrollment by calling Privy's `initMfaEnrollment` method with a JSON parameter of `{method: 'passkey'}`: ```tsx theme={"system"} await initMfaEnrollment({method: 'passkey'}); ``` ## Completing enrollment Then, to have the user enroll, you must call Privy's `submitMfaEnrollment` method with a list of the user's passkey account `credentialIds`: ```tsx theme={"system"} import {usePrivy} from '@privy-io/expo'; const {user} = usePrivy(); // ... const credentialIds = user.linked_accounts .filter((account): account is PasskeyWithMetadata => account.type === 'passkey') .map((x) => x.credentialId); await submitMfaEnrollment({method: 'passkey', credentialIds}); ``` The component below serves as a reference implementation for how to enroll your users in MFA with passkeys! ```tsx Example enrolling passkeys for MFA theme={"system"} import {useMfaEnrollment, usePrivy} from '@privy-io/expo'; export default function MfaEnrollmentWithPasskey() { const {user} = usePrivy(); const {initMfaEnrollment, submitMfaEnrollment} = useMfaEnrollment(); const handleEnrollmentWithPasskey = async () => { await initMfaEnrollment({method: 'passkey'}); const credentialIds = user.linked_accounts .filter((account): account is PasskeyWithMetadata => account.type === 'passkey') .map((x) => x.credentialId); await submitMfaEnrollment({method: 'passkey', credentialIds}); }; return ( Enable your passkeys for MFA {user.linkedAccounts .filter((account): account is PasskeyWithMetadata => account.type === 'passkey') .map((account) => ( ID: {account.id} {' '} Credential ID: {account.credentialId} ))} {// Initialize and submit the passkey credentials for MFA enrollment in one step } ); } ``` ## Enrolling passkeys To enroll passkeys as an MFA method, call Privy's `submit` method with the list of passkey credential IDs that should be enabled for MFA. You can find the credential IDs from the user's linked accounts: ```swift theme={"system"} guard let user = await privy.getUser() else { return } // Get credential IDs from linked passkey accounts let credentialIds = user.linkedAccounts .compactMap { account -> String? in if case .passkey(let passkey) = account { return passkey.credentialId } return nil } // Submit credential IDs to complete enrollment let updatedUser = try await user.mfa.passkeys.enroll.submit(credentialIds: credentialIds) // The updated user object will contain the newly enrolled mfaMethod print(updatedUser.mfaMethods) ``` ## Enrolling passkeys To enroll passkeys as an MFA method, call Privy's `submit` method with the list of passkey credential IDs that should be enabled for MFA. You can find the credential IDs from the user's linked accounts: ```kotlin theme={"system"} val user = privy.getUser() ?: return // Get credential IDs from linked passkey accounts val credentialIds = user.linkedAccounts .filterIsInstance() .map { it.credentialId } // Submit credential IDs to complete enrollment user.mfa.passkeys.enroll.submit(credentialIds) .onSuccess { updatedUser -> // The updated user object will contain the newly enrolled mfaMethod println(updatedUser.mfaMethods) } .onFailure { error -> // Handle error } ``` ## Enrolling passkeys To enroll passkeys as an MFA method, call Privy's `submit` method with the list of passkey credential IDs that should be enabled for MFA. You can find the credential IDs from the user's linked accounts: ```dart theme={"system"} final user = await privy.getUser(); if (user == null) return; // Get credential IDs from linked passkey accounts final credentialIds = user.linkedAccounts .whereType() .map((account) => account.credentialId) .toList(); // Submit credential IDs to complete enrollment final result = await user.mfa.passkeys.enroll.submit(credentialIds); result.fold( onSuccess: (updatedUser) { // The updated user object will contain the newly enrolled mfaMethod print(updatedUser.mfaMethods); }, onFailure: (error) { // Handle error }, ); ``` # SMS enrollment Source: https://docs.privy.io/authentication/user-authentication/mfa/enrollment/sms Enroll users in SMS-based MFA by registering their phone number for wallet verification codes Enrolling in MFA does not automatically verify the user for wallet operations. Once enrolled, subsequent wallet actions will require MFA verification. See the [verification guides](/authentication/user-authentication/mfa/verify/overview) for how to complete MFA verification. Enroll users in MFA using SMS, where they authenticate with a 6-digit code sent to their phone number. If your app has enabled SMS as a possible *login* method, users will **not** be able to enroll SMS as a valid *MFA* method. SMS must either be used as a login method to secure user accounts, or as an MFA method for additional security on the users' wallets, but cannot be used for both. ## Setup To enroll users in MFA with SMS, use the `initEnrollmentWithSms` and `submitEnrollmentWithSms` methods returned by the `useMfaEnrollment` hook: ```tsx theme={"system"} import {useMfaEnrollment} from '@privy-io/react-auth'; const {initEnrollmentWithSms, submitEnrollmentWithSms} = useMfaEnrollment(); ``` ## Initiating enrollment First, prompt your user to enter the phone number they'd like to use for MFA. Then, call Privy's `initEnrollmentWithSms` method. As a parameter, pass a JSON object with a `phoneNumber` field that contains the user's provided phone number as a string. ```tsx theme={"system"} // Prompt the user for their phone number const phoneNumberInput = 'insert-phone-number-from-user'; // Send an enrollment code to their phone number await initEnrollmentWithSms({phoneNumber: phoneNumberInput}); ``` Once `initEnrollmentWithSms` is called with a valid phone number, Privy will send a 6-digit MFA enrollment code to the provided number. This method returns a `Promise` that will resolve to `void` if the code was successfully sent, or will reject with an `error` if there was an error sending the code (e.g. invalid phone number). ## Completing enrollment Next, prompt the user to enter the 6-digit code that was sent to their phone number, and use the `submitEnrollmentWithSms` method to complete enrollment. As a parameter, you must pass a JSON object with both the original `phoneNumber` that the user enrolled, and the `mfaCode` they received at that number. ```tsx theme={"system"} // Prompt the user for the code sent to their phone number const mfaCodeInput = 'insert-mfa-code-received-by-user'; await submitEnrollmentWithSms({ phoneNumber: phoneNumberInput, // From above mfaCode: mfaCodeInput, }); ``` The component below serves as a reference implementation for how to enroll your users in MFA with SMS! ```tsx Example enrolling a phone number for MFA theme={"system"} import {useMfaEnrollment} from '@privy-io/react-auth'; export default function MfaEnrollmentWithSms() { const {initEnrollmentWithSms, submitEnrollmentWithSms} = useMfaEnrollment(); const [phoneNumber, setPhoneNumber] = useState(null); const [mfaCode, setMfaCode] = useState(null); const [pendingMfaCode, setPendingMfaCode] = useState(false); // Handler for when the user enters their phone number to enroll in MFA. const onEnteredPhoneNumber = () => { await initEnrollmentWithSms({phoneNumber: phoneNumber}); // Sends an MFA code to the `phoneNumber` setPendingMfaCode(true); } // Handler for when the user enters the MFA code sent to their phone number. const onEnteredMfaCode = () => { await submitEnrollmentWithSms({phoneNumber: phoneNumber, mfaCode: mfaCode}); // See the error handling guide for details on how to handle errors setPendingMfaCode(false); } // If no MFA code has been sent yet, prompt the user for their phone number to enroll if (!pendingMfaCode) { // Input field for the user to enter the phone number they'd like to enroll for MFA return <> setPhoneNumber(event.target.value)}/> ; } // Input field for the user to enter the MFA code sent to their phone number return <> setMfaCode(event.target.value)}/> ; } ``` ## Setup To enroll users in MFA with SMS, use the `initMfaEnrollment` and `submitMfaEnrollment` methods returned by the `useMfaEnrollment` hook: ```tsx theme={"system"} import {useMfaEnrollment} from '@privy-io/expo'; const {initMfaEnrollment, submitMfaEnrollment} = useMfaEnrollment(); ``` ## Initiating enrollment First, prompt your user to enter the phone number they'd like to use for MFA. Then, call Privy's `initMfaEnrollment` method with the appropriate parameters: ```tsx theme={"system"} // Prompt the user for their phone number const phoneNumberInput = 'insert-phone-number-from-user'; // Send an enrollment code to their phone number await initMfaEnrollment({method: 'sms', phoneNumber: phoneNumberInput}); ``` Once `initMfaEnrollment` is called with a valid phone number, Privy will send a 6-digit MFA enrollment code to the provided number. ## Completing enrollment Next, prompt the user to enter the 6-digit code that was sent to their phone number, and use the `submitMfaEnrollment` method to complete enrollment: ```tsx theme={"system"} // Prompt the user for the code sent to their phone number const mfaCodeInput = 'insert-mfa-code-received-by-user'; await submitMfaEnrollment({ method: 'sms', phoneNumber: phoneNumberInput, // From above code: mfaCodeInput, }); ``` The component below serves as a reference implementation for how to enroll your users in MFA with SMS! ```tsx Example enrolling a phone number for MFA theme={"system"} import {useMfaEnrollment} from '@privy-io/expo'; export default function MfaEnrollmentWithSms() { const {initMfaEnrollment, submitMfaEnrollment} = useMfaEnrollment(); const [phoneNumber, setPhoneNumber] = useState(null); const [mfaCode, setMfaCode] = useState(null); const [pendingMfaCode, setPendingMfaCode] = useState(false); // Handler for when the user enters their phone number to enroll in MFA. const onEnteredPhoneNumber = () => { await initMfaEnrollment({method: 'sms', phoneNumber: phoneNumber}); // Sends an MFA code to the `phoneNumber` setPendingMfaCode(true); } // Handler for when the user enters the MFA code sent to their phone number. const onEnteredMfaCode = () => { await submitMfaEnrollment({method: 'sms', phoneNumber: phoneNumber, code: mfaCode}); // See the error handling guide for details on how to handle errors setPendingMfaCode(false); } // If no MFA code has been sent yet, prompt the user for their phone number to enroll if (!pendingMfaCode) { // Input field for the user to enter the phone number they'd like to enroll for MFA return ( ) } // Input field for the user to enter the MFA code sent to their phone number return <> ; } ``` ## Initiating enrollment First, prompt your user to enter the phone number they'd like to use for MFA. Then, call Privy's `sendCode` method with the user's phone number: ```swift theme={"system"} guard let user = await privy.getUser() else { return } // Prompt the user for their phone number let phoneNumber = "+15555555555" // Send an enrollment code to their phone number try await user.mfa.sms.enroll.sendCode(to: phoneNumber) ``` Once `sendCode` is called with a valid phone number, Privy will send a 6-digit MFA enrollment code to the provided number. The method throws if there was an error sending the code (e.g. invalid phone number). ## Completing enrollment Next, prompt the user to enter the 6-digit code that was sent to their phone number, and use the `submit` method to complete enrollment. You must pass both the `code` that the user entered and the `phoneNumber` it was sent to: ```swift theme={"system"} // Prompt the user for the code sent to their phone number let mfaCode = "123456" let updatedUser = try await user.mfa.sms.enroll.submit(code: mfaCode, sentTo: phoneNumber) // The updated user object will contain the newly enrolled mfaMethod print(updatedUser.mfaMethods) ``` ## Initiating enrollment First, prompt your user to enter the phone number they'd like to use for MFA. Then, call Privy's `sendCode` method with the user's phone number: ```kotlin theme={"system"} val user = privy.getUser() ?: return // Prompt the user for their phone number val phoneNumber = "+15555555555" // Send an enrollment code to their phone number user.mfa.sms.enroll.sendCode(phoneNumber) .onSuccess { // Code sent successfully, show input for verification code } .onFailure { error -> // Handle error (e.g. invalid phone number) } ``` Once `sendCode` is called with a valid phone number, Privy will send a 6-digit MFA enrollment code to the provided number. The method returns a `Result` that indicates success or failure. ## Completing enrollment Next, prompt the user to enter the 6-digit code that was sent to their phone number, and use the `submit` method to complete enrollment. You must pass both the `code` that the user entered and the `phoneNumber` it was sent to: ```kotlin theme={"system"} // Prompt the user for the code sent to their phone number val mfaCode = "123456" user.mfa.sms.enroll.submit(mfaCode, phoneNumber) .onSuccess { updatedUser -> // The updated user object will contain the newly enrolled mfaMethod println(updatedUser.mfaMethods) } .onFailure { error -> // Handle error } ``` ## Initiating enrollment First, prompt your user to enter the phone number they'd like to use for MFA. Then, call Privy's `sendCode` method with the user's phone number: ```dart theme={"system"} final user = await privy.getUser(); if (user == null) return; // Prompt the user for their phone number const phoneNumber = '+15555555555'; // Send an enrollment code to their phone number final result = await user.mfa.sms.enroll.sendCode(phoneNumber); result.fold( onSuccess: (_) { // Code sent successfully, show input for verification code }, onFailure: (error) { // Handle error (e.g. invalid phone number) }, ); ``` Once `sendCode` is called with a valid phone number, Privy will send a 6-digit MFA enrollment code to the provided number. The method returns a `Result` that indicates success or failure. ## Completing enrollment Next, prompt the user to enter the 6-digit code that was sent to their phone number, and use the `submit` method to complete enrollment. You must pass both the `code` that the user entered and the `phoneNumber` it was sent to: ```dart theme={"system"} // Prompt the user for the code sent to their phone number const mfaCode = '123456'; final result = await user.mfa.sms.enroll.submit( code: mfaCode, phoneNumber: phoneNumber, ); result.fold( onSuccess: (updatedUser) { // The updated user object will contain the newly enrolled mfaMethod print(updatedUser.mfaMethods); }, onFailure: (error) { // Handle error }, ); ``` # TOTP enrollment Source: https://docs.privy.io/authentication/user-authentication/mfa/enrollment/totp Enroll users in TOTP-based MFA using authenticator apps like Google Authenticator or Authy Enrolling in MFA does not automatically verify the user for wallet operations. Once enrolled, subsequent wallet actions will require MFA verification. See the [verification guides](/authentication/user-authentication/mfa/verify/overview) for how to complete MFA verification. Enroll users in MFA using TOTP (Time-based One-Time Password), where they authenticate with a 6-digit code from an authenticator app like Authy or Google Authenticator. ## Setup To enroll users in MFA with TOTP, use the `initEnrollmentWithTotp` and `submitEnrollmentWithTotp` methods returned by the `useMfaEnrollment` hook: ```tsx theme={"system"} import {useMfaEnrollment} from '@privy-io/react-auth'; const {initEnrollmentWithTotp, submitEnrollmentWithTotp} = useMfaEnrollment(); ``` ## Initiating enrollment First, initiate enrollment by calling Privy's `initEnrollmentWithTotp` method with no parameters. This method returns a `Promise` for an `authUrl` and `secret` that the user will need in order to complete enrollment. ```tsx theme={"system"} const {authUrl, secret} = await initEnrollmentWithTotp(); ``` Then, to have the user enroll, you can either: * Display the TOTP `authUrl` as a QR code to the user, and prompt them to scan it with their TOTP client (commonly, a mobile app like Google Authenticator or Authy) * Allow the user to copy the TOTP `secret` and paste it into their TOTP client You can directly pass in the `authUrl` from above into a library like `react-qr-code` to render the URL as a QR code to your user. ## Completing enrollment Once your user has successfully scanned the QR code, an enrollment code for Privy will appear within their TOTP client. Prompt the user to enter this code in your app, and call Privy's `submitEnrollmentWithTotp` method. As a parameter, pass a JSON object with an `mfaCode` field that contains the MFA code from the user as a string. ```tsx theme={"system"} const mfaCodeInput = 'insert-mfa-code-from-user-totp-app'; // Prompt the user for the code in their TOTP app await submitEnrollmentWithTotp({mfaCode: mfaCodeInput}); ``` The component below serves as a reference implementation for how to enroll your users in MFA with TOTP! ```tsx Example enrolling a TOTP client for MFA theme={"system"} import {useMfaEnrollment} from '@privy-io/react-auth'; import QRCode from 'react-qr-code'; import {CopyableElement} from '../components/CopyableElement'; export default function MfaEnrollmentWithTotp() { const {initEnrollmentWithTotp, submitEnrollmentWithTotp} = useMfaEnrollment(); const [totpAuthUrl, setTotpAuthUrl] = useState(null); const [totpSecret, setTotpSecret] = useState(null); const [mfaCode, setMfaCode] = useState(null); // Handler for when the user is ready to enroll in TOTP MFA const onGenerateTotpUrl = async () => { const {authUrl, secret} = await initEnrollmentWithTotp(); setTotpAuthUrl(authUrl); setTotpSecret(secret); } // Handler for when the user enters the MFA code from their TOTP client const onEnteredMfaCode = async () => { await submitEnrollmentWithTotp({mfaCode: mfaCode}); // See the error handling guide for details on how to handle errors } return(
{/* QR code for the user to scan */} {totpAuthUrl && totpSecret ? {/* If TOTP values have been generated... */} <> {/* ...show the user a QR code with the `authUrl` that they can scan... */} {/* ...or give them the option to copy the `secret` into their TOTP client */} : {/* Else, show a button to generate the totpAuthUrl */} } {/* Input field for the user to enter their MFA code */}

Enter the code from your authenticator app below.

setMfaCode(event.target.value)}/>
); } ```
## Setup To enroll users in MFA with TOTP, use the `initMfaEnrollment` and `submitMfaEnrollment` methods returned by the `useMfaEnrollment` hook: ```tsx theme={"system"} import {useMfaEnrollment} from '@privy-io/expo'; const {initMfaEnrollment, submitMfaEnrollment} = useMfaEnrollment(); ``` ## Initiating enrollment First, initiate enrollment by calling Privy's `initMfaEnrollment` method with a JSON parameter of `{method: 'totp'}`: ```tsx theme={"system"} const {authUrl} = await initMfaEnrollment({method: 'totp'}); ``` Then, to have the user enroll, you can display the TOTP `authUrl` as a QR code to the user, and prompt them to scan it with their TOTP client. You can directly pass in the `authUrl` from above into a library like `expo-linking` to deep link into a TOTP application for MFA. ## Completing enrollment Once your user has successfully linked into their TOTP application, prompt the user to enter the code in your app, and call Privy's `submitMfaEnrollment` method: ```tsx theme={"system"} const mfaCodeInput = 'insert-mfa-code-from-user-totp-app'; // Prompt the user for the code in their TOTP app await submitMfaEnrollment({method: 'totp', code: mfaCodeInput}); ``` The component below serves as a reference implementation for how to enroll your users in MFA with TOTP! ```tsx Example enrolling a TOTP client for MFA theme={"system"} import {useMfaEnrollment} from '@privy-io/expo'; export default function MfaEnrollmentWithTotp() { const {initMfaEnrollment, submitMfaEnrollment} = useMfaEnrollment(); const [totpAuthUrl, setTotpAuthUrl] = useState(null); const [mfaCode, setMfaCode] = useState(null); // Handler for when the user is ready to enroll in TOTP MFA const onGenerateTotpUrl = async () => { const {authUrl} = await initMfaEnrollment({method: 'totp'}); setTotpAuthUrl(authUrl); } // Handler for when the user enters the MFA code from their TOTP client const onMfaEnrollmentSubmit = async () => { await submitMfaEnrollment({method: 'totp', code: mfaCode}); // See the error handling guide for details on how to handle errors } return( {/* QR code for the user to scan */} TOTP MFA Enrollment { // Check to see if the totpAuthUrl is generated !!totpAuthUrl && Linking.openURL(totpAuthUrl)} >{totpAuthUrl} {// Once the TOTP code is received in the authenticator app and input above, submit for enrollment} } ); } ``` ## Initiating enrollment First, initiate enrollment by calling Privy's `generateSecret` method. This method returns a `TotpSecret` containing the `secret` and `authUrl` that the user will need in order to complete enrollment: ```swift theme={"system"} guard let user = await privy.getUser() else { return } let totpSecret = try await user.mfa.totp.enroll.generateSecret() // Access the secret for manual entry print(totpSecret.secret) // Access the auth URL for QR code generation print(totpSecret.authUrl) ``` Then, to have the user enroll, you can either: * Display the TOTP `authUrl` as a QR code to the user, and prompt them to scan it with their TOTP client (commonly, a mobile app like Google Authenticator or Authy) * Allow the user to copy the TOTP `secret` and paste it into their TOTP client ## Completing enrollment Once your user has successfully scanned the QR code or entered the secret, an enrollment code for Privy will appear within their TOTP client. Prompt the user to enter this code in your app, and call Privy's `submit` method: ```swift theme={"system"} // Prompt the user for the code in their TOTP app let mfaCode = "123456" let updatedUser = try await user.mfa.totp.enroll.submit(code: mfaCode) // The updated user object will contain the newly enrolled mfaMethod print(updatedUser.mfaMethods) ``` ## Initiating enrollment First, initiate enrollment by calling Privy's `generateSecret` method. This method returns a `TotpSecret` containing the `secret` and `authUrl` that the user will need in order to complete enrollment: ```kotlin theme={"system"} val user = privy.getUser() ?: return user.mfa.totp.enroll.generateSecret() .onSuccess { totpSecret -> // Access the secret for manual entry println(totpSecret.secret) // Access the auth URL for QR code generation println(totpSecret.authUrl) } .onFailure { error -> // Handle error } ``` Then, to have the user enroll, you can either: * Display the TOTP `authUrl` as a QR code to the user, and prompt them to scan it with their TOTP client (commonly, a mobile app like Google Authenticator or Authy) * Allow the user to copy the TOTP `secret` and paste it into their TOTP client ## Completing enrollment Once your user has successfully scanned the QR code or entered the secret, an enrollment code for Privy will appear within their TOTP client. Prompt the user to enter this code in your app, and call Privy's `submit` method: ```kotlin theme={"system"} // Prompt the user for the code in their TOTP app val mfaCode = "123456" user.mfa.totp.enroll.submit(mfaCode) .onSuccess { updatedUser -> // The updated user object will contain the newly enrolled mfaMethod println(updatedUser.mfaMethods) } .onFailure { error -> // Handle error } ``` ## Initiating enrollment First, initiate enrollment by calling Privy's `generateSecret` method. This method returns a `TotpSecret` containing the `secret` and `authUrl` that the user will need in order to complete enrollment: ```dart theme={"system"} final user = await privy.getUser(); if (user == null) return; final result = await user.mfa.totp.enroll.generateSecret(); result.fold( onSuccess: (totpSecret) { // Access the secret for manual entry print(totpSecret.secret); // Access the auth URL for QR code generation print(totpSecret.authUrl); }, onFailure: (error) { // Handle error }, ); ``` Then, to have the user enroll, you can either: * Display the TOTP `authUrl` as a QR code to the user, and prompt them to scan it with their TOTP client (commonly, a mobile app like Google Authenticator or Authy) * Allow the user to copy the TOTP `secret` and paste it into their TOTP client ## Completing enrollment Once your user has successfully scanned the QR code or entered the secret, an enrollment code for Privy will appear within their TOTP client. Prompt the user to enter this code in your app, and call Privy's `submit` method: ```dart theme={"system"} // Prompt the user for the code in their TOTP app const mfaCode = '123456'; final result = await user.mfa.totp.enroll.submit(mfaCode); result.fold( onSuccess: (updatedUser) { // The updated user object will contain the newly enrolled mfaMethod print(updatedUser.mfaMethods); }, onFailure: (error) { // Handle error }, ); ```
# Error handling Source: https://docs.privy.io/authentication/user-authentication/mfa/errors Handle MFA errors including incorrect codes, expired codes, and maximum attempt limits When both enrolling in and completing MFA, Privy sends a 6-digit code to the user's selected MFA method, that the user must submit to Privy in order to verify their identity. When submitting this MFA code, Privy may respond with an error if: * The code is incorrect * The user has reached the maximum number of attempts for this MFA flow * The MFA flow has timed out If the user enters an incorrect code (e.g. by mistyping), they are allowed to retry code submission up to a **maximum of four attempts**. ## Error helper functions Privy provides helper functions to parse errors raised by MFA code submission methods. Each of these functions accepts the raw error raised as a parameter, and returns a `Boolean` indicating if the error meets a certain condition. ```tsx theme={"system"} import { errorIndicatesMfaVerificationFailed, errorIndicatesMaxMfaRetries, errorIndicatesMfaTimeout, } from '@privy-io/react-auth'; ``` ### errorIndicatesMfaVerificationFailed Indicates the user entered an incorrect MFA code. Allow the user to re-enter the code and call `submit` again. ### errorIndicatesMaxMfaRetries Indicates the user has reached the maximum number of attempts for this MFA flow. A new MFA code must be requested via `init`. ### errorIndicatesMfaTimeout Indicates that the current MFA code has expired. A new MFA code must be requested via `init`. ```tsx theme={"system"} import { errorIndicatesMfaVerificationFailed, errorIndicatesMaxMfaRetries, errorIndicatesMfaTimeout, } from '@privy-io/expo'; ``` ### errorIndicatesMfaVerificationFailed Indicates the user entered an incorrect MFA code. Allow the user to re-enter the code and call `submit` again. ### errorIndicatesMaxMfaRetries Indicates the user has reached the maximum number of attempts for this MFA flow. A new MFA code must be requested via `init`. ### errorIndicatesMfaTimeout Indicates that the current MFA code has expired. A new MFA code must be requested via `init`. In Swift, MFA errors are thrown as `PrivyError` values with `EmbeddedWalletFailureReason` cases: | Error Case | Description | | ------------------------------------ | ----------------------------------------------------------- | | `.missingOrInvalidMfa` | The user entered an incorrect MFA code | | `.timeoutOnMfa` | The MFA flow timed out | | `.mfaChallengeExpired` | The MFA challenge expired before verification completed | | `.mfaVerificationMaxAttemptsReached` | The maximum number of MFA verification attempts was reached | In Android, MFA errors are returned as failures in the `Result` type. Catch `MfaRequiredOrInvalidException` to handle cases where MFA is required or the submitted code was invalid. In Flutter, MFA errors are returned as typed exceptions in the `Result` failure. All MFA exception types extend `MfaException`, which itself extends `PrivyException`, so existing catch-all `onFailure` handlers continue to work without changes. | Exception | Description | | -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `MfaMissingOrInvalidException` | The submitted code was wrong, or a wallet operation requires MFA before it can proceed. Allow the user to re-enter the code and call `submit` again. | | `MfaChallengeExpiredException` | The server-side MFA challenge expired before the user submitted a code. Restart the flow by calling `sendCode()` or `generateSecret()` again. | | `MfaMaxAttemptsReachedException` | The user submitted too many incorrect codes and the challenge is locked. Start a new challenge by calling `sendCode()` or `generateSecret()` again. | | `MfaSdkTimeoutException` | `resumeBlockedActions()` was not called within the configured timeout after `onMfaRequired` fired. Retry the wallet operation from scratch — `onMfaRequired` will fire again. | ## Example usage ```tsx Handling errors during MFA code submission theme={"system"} import { errorIndicatesMfaVerificationFailed, errorIndicatesMaxMfaRetries, errorIndicatesMfaTimeout } from '@privy-io/react-auth'; try { // Errors from enrollment methods can be handled similarly await submit('insert-mfa-method', 'insert-mfa-code'); } catch (e) { if (errorIndicatesMfaVerificationFailed(e)) { console.error('Incorrect MFA code, please try again.'); // Allow the user to re-enter the code and call `submit` again } else if (errorIndicatesMaxMfaRetries(e)) { console.error('Maximum MFA attempts reached, please request a new code.'); // Allow the user to request a new code with `init` } else if (errorIndicatesMfaTimeout(e)) { console.error('MFA code has expired, please request a new code.'); // Allow the user to request a new code with `init` } } ``` ```tsx Handling errors during MFA code submission theme={"system"} import { errorIndicatesMfaVerificationFailed, errorIndicatesMaxMfaRetries, errorIndicatesMfaTimeout } from '@privy-io/expo'; try { // Errors from enrollment methods can be handled similarly await submit({method: 'insert-mfa-method', mfaCode: 'insert-mfa-code'}); } catch (e) { if (errorIndicatesMfaVerificationFailed(e)) { console.error('Incorrect MFA code, please try again.'); // Allow the user to re-enter the code and call `submit` again } else if (errorIndicatesMaxMfaRetries(e)) { console.error('Maximum MFA attempts reached, please request a new code.'); // Allow the user to request a new code with `init` } else if (errorIndicatesMfaTimeout(e)) { console.error('MFA code has expired, please request a new code.'); // Allow the user to request a new code with `init` } } ``` ```swift Handling errors during MFA code submission theme={"system"} do { try await user.mfa.totp.verify.submit(code: mfaCode) } catch let error as PrivyError { switch error.errorCode { case .embeddedWalletFailure(let reason): switch reason { case .missingOrInvalidMfa: // Incorrect code - allow the user to re-enter errorMessage = "Incorrect MFA code, please try again." case .timeoutOnMfa: // Code expired - need to start over errorMessage = "MFA code has expired, please request a new code." case .mfaChallengeExpired: // Challenge expired - need to start a new MFA flow errorMessage = "MFA challenge expired, please start over." case .mfaVerificationMaxAttemptsReached: // Max attempts reached - need to start a new MFA flow errorMessage = "Maximum attempts reached, please request a new code." default: errorMessage = "An error occurred: \(error.localizedDescription)" } default: errorMessage = "An error occurred: \(error.localizedDescription)" } } ``` ```kotlin Handling errors during MFA code submission theme={"system"} import io.privy.auth.mfa.MfaRequiredOrInvalidException user.mfa.totp.verify.submit(mfaCode) .onSuccess { // MFA verification succeeded } .onFailure { error -> when (error) { is MfaRequiredOrInvalidException -> { // MFA is required or the code was invalid errorMessage = "Incorrect or invalid MFA code, please try again." } else -> { errorMessage = "An error occurred: ${error.message}" } } } ``` ```dart Handling errors during MFA code submission theme={"system"} import 'package:privy_flutter/privy_flutter.dart'; final result = await user.mfa.totp.verify.submit(mfaCode); result.fold( onSuccess: (_) { // MFA verification succeeded }, onFailure: (error) { if (error is MfaMissingOrInvalidException) { // Incorrect code — allow the user to re-enter and call submit again errorMessage = 'Incorrect MFA code, please try again.'; } else if (error is MfaMaxAttemptsReachedException) { // Too many attempts — must restart the flow errorMessage = 'Maximum MFA attempts reached, please request a new code.'; } else if (error is MfaChallengeExpiredException) { // Challenge expired — must restart the flow errorMessage = 'MFA code has expired, please request a new code.'; } else if (error is MfaSdkTimeoutException) { // Listener timed out — retry the wallet operation errorMessage = 'MFA timed out, please try your action again.'; } else { errorMessage = 'An error occurred: ${error.message}'; } }, ); ``` # MFA required listener Source: https://docs.privy.io/authentication/user-authentication/mfa/listener Register an MFA required listener to trigger custom verification flows when wallet actions need MFA Once you've set up your app's logic for guiding a user to complete MFA, you need to configure Privy to invoke this logic whenever MFA is required by the user's embedded wallet. ## Registering the listener To set up this configuration, use Privy's `useRegisterMfaListener` hook. As a parameter, you must pass a JSON object with an `onMfaRequired` callback. ### onMfaRequired callback Privy will invoke the `onMfaRequired` callback you set whenever the user is required to complete MFA to use the embedded wallet. When this occurs, any use of the embedded wallet will be "paused" until the user has successfully completed MFA with Privy. In this callback, you should invoke your app's logic for guiding through completing MFA (done via the `useMfa` hook). Within this callback, you can also access an `methods` parameter that contains a list of available MFA methods that the user has enrolled in (`'sms'` and/or `'totp'` and/or `'passkey'`). ```tsx MFAProvider.tsx theme={"system"} import {useRegisterMfaListener, MfaMethod} from '@privy-io/react-auth'; import {MFAModal} from '../components/MFAModal'; export const MFAProvider = ({children}: {children: React.ReactNode}) => { const [isMfaModalOpen, setIsMfaModelOpen] = useState(false); const [mfaMethods, setMfaMethods] = useState([]); useRegisterMfaListener({ // Privy will invoke this whenever the user is required to complete MFA onMfaRequired: (methods) => { // Update app's state with the list of available MFA methods for the user setMfaMethods(methods); // Open MFA modal to allow user to complete MFA setIsMfaModalOpen(true); }, }); return (
{/* This `MFAModal` component includes all logic for completing the MFA flow with Privy's `useMfa` hook */} {children}
); }; ``` In order for Privy to invoke your app's MFA flow, the component that calls Privy's `useRegisterMfaListener` hook **must be mounted** whenever the user's embedded wallet requires that they complete MFA. We recommend that you render this component near the root of your application, so that it is always rendered whenever the embedded wallet may be used.
To set up this configuration, use Privy's `useRegisterMfaListener` hook: ```tsx MFAProvider.tsx theme={"system"} import {useRegisterMfaListener, MfaMethod} from '@privy-io/expo'; import {MFAModal} from '../components/MFAModal'; export const MFAProvider = ({children}: {children: React.ReactNode}) => { const [isMfaModalOpen, setIsMfaModelOpen] = useState(false); const [mfaMethods, setMfaMethods] = useState([]); useRegisterMfaListener({ // Privy will invoke this whenever the user is required to complete MFA onMfaRequired: (methods) => { // Update app's state with the list of available MFA methods for the user setMfaMethods(methods); // Open MFA modal to allow user to complete MFA setIsMfaModalOpen(true); }, }); return ( {/* This `MFAModal` component includes all logic for completing the MFA flow with Privy's `useMfa` hook */} {children} ); }; ``` In order for Privy to invoke your app's MFA flow, the component that calls Privy's `useRegisterMfaListener` hook **must be mounted** whenever the user's embedded wallet requires that they complete MFA. We recommend that you render this component near the root of your application, so that it is always rendered whenever the embedded wallet may be used. To set up this configuration, implement the `PrivyMfaPromptDelegate` protocol and configure it via `PrivyMFAConfig`. When an operation requires MFA verification, the SDK will automatically call your delegate's `onMfaRequired` method. ### Setting up the delegate First, create a class that implements the `PrivyMfaPromptDelegate` protocol: ```swift MfaPromptHandler.swift theme={"system"} import PrivySDK @MainActor final class MfaPromptHandler: ObservableObject, PrivyMfaPromptDelegate { @Published var isShowingPrompt = false private var user: PrivyUser! func onMfaRequired(for user: PrivyUser) { self.user = user self.isShowingPrompt = true } func submit(privy: Privy) { // Code would be read from the view model let code = "123456" Task { do { try await user.mfa.totp.verify.submit(code: code) await privy.mfa.resumeBlockedActions() } catch { // Show the error in UI so the user can retry } } } // Called if the user decides to cancel the flow func cancel(privy: Privy) { Task { await privy.mfa.resumeBlockedActions(throwing: MfaError.cancelled) } } } ``` ### Configuring the MFA delegate Configure the delegate either at initialization or at runtime: ```swift Configure at initialization theme={"system"} let mfaHandler = MfaPromptHandler() let mfaConfig = PrivyMFAConfig(delegate: mfaHandler) let config = PrivyConfig( appId: "your-app-id", appClientId: "your-client-id", mfaConfig: mfaConfig ) let privy = PrivySdk.initialize(config: config) ``` ```swift Configure at runtime theme={"system"} // You can also set the config at runtime let mfaHandler = MfaPromptHandler() let mfaConfig = PrivyMFAConfig(delegate: mfaHandler) privy.mfa.setConfig(mfaConfig) ``` ### Completing verification and resuming operations After the user completes MFA verification, you **must** call `resumeBlockedActions()` to unblock any pending wallet operations: ```swift theme={"system"} // After successful MFA verification try await user.mfa.totp.verify.submit(code: mfaCode) await privy.mfa.resumeBlockedActions() ``` If `resumeBlockedActions()` is not called within 5 minutes of the operation that triggered the delegate, the original wallet operation will throw with a `.embeddedWalletFailure(reason: .timeoutOnMfa)` error. ### Handling cancellation If the user cancels the MFA flow, you must still call `resumeBlockedActions` to unblock operations. Pass an error to indicate cancellation: ```swift theme={"system"} // If the user cancels MFA await privy.mfa.resumeBlockedActions(throwing: MyError.mfaCancelled) ``` To set up this configuration, implement the `MfaListener` interface and configure it via `MfaConfig`. When an operation requires MFA verification, the SDK will automatically call your listener's `onMfaRequired` method. ### Setting up the listener First, create a class that implements the `MfaListener` interface: ```kotlin MfaPromptHandler.kt theme={"system"} import io.privy.auth.PrivyUser import io.privy.auth.mfa.MfaListener class MfaPromptHandler( private val onMfaRequired: (user: PrivyUser) -> Unit ) : MfaListener { override fun onMfaRequired(user: PrivyUser) { // Called on the main thread - safe to show UI directly onMfaRequired.invoke(user) } } ``` ### Configuring the MFA listener Configure the listener either at initialization or at runtime: ```kotlin Configure at initialization theme={"system"} import io.privy.sdk.Privy import io.privy.sdk.PrivyConfig import io.privy.auth.mfa.MfaConfig import kotlin.time.Duration.Companion.minutes val mfaHandler = MfaPromptHandler { user -> // Show your MFA UI showMfaBottomSheet(user) } val config = PrivyConfig( appId = "your-app-id", appClientId = "your-client-id", mfaConfig = MfaConfig( listener = mfaHandler, timeout = 5.minutes ) ) val privy = Privy.init(context, config) ``` ```kotlin Configure at runtime theme={"system"} // You can also set the config at runtime val mfaHandler = MfaPromptHandler { user -> showMfaBottomSheet(user) } privy.mfa.setConfig( MfaConfig( listener = mfaHandler, timeout = 5.minutes ) ) ``` ### Completing verification and resuming operations After the user completes MFA verification, you **must** call `resumeBlockedActions()` to unblock any pending wallet operations: ```kotlin theme={"system"} // After successful MFA verification viewModelScope.launch { user.mfa.totp.verify.submit(mfaCode).onSuccess { privy.mfa.resumeBlockedActions() }.onFailure { error -> // Show error to user } } ``` If `resumeBlockedActions()` is not called within the configured timeout (default 5 minutes), the original wallet operation will fail with a timeout error. ### Handling cancellation If the user cancels the MFA flow, you must still call `resumeBlockedActions` to unblock operations. Pass an error to indicate cancellation: ```kotlin theme={"system"} // If the user cancels MFA privy.mfa.resumeBlockedActions(mfaError = Exception("User cancelled MFA")) ``` To set up this configuration, implement the `MfaListener` interface and configure it via `MfaConfig`. When an operation requires MFA verification, the SDK will automatically call your listener's `onMfaRequired` method. ### Setting up the listener First, create a class that implements the `MfaListener` interface: ```dart MfaPromptHandler.dart theme={"system"} import 'package:privy_flutter/privy_flutter.dart'; class MfaPromptHandler implements MfaListener { final void Function(PrivyUser user) onMfaRequiredCallback; MfaPromptHandler({required this.onMfaRequiredCallback}); @override void onMfaRequired(PrivyUser user) { // Called on the main thread - safe to show UI directly onMfaRequiredCallback(user); } } ``` ### Configuring the MFA listener Configure the listener either at initialization or at runtime: ```dart Configure at initialization theme={"system"} import 'package:privy_flutter/privy_flutter.dart'; final mfaHandler = MfaPromptHandler( onMfaRequiredCallback: (user) { // Show your MFA UI showMfaBottomSheet(user); }, ); final privy = Privy.init( config: PrivyConfig( appId: 'your-app-id', appClientId: 'your-client-id', mfaConfig: MfaConfig( listener: mfaHandler, timeout: const Duration(minutes: 5), ), ), ); ``` ```dart Configure at runtime theme={"system"} // You can also set the config at runtime final mfaHandler = MfaPromptHandler( onMfaRequiredCallback: (user) { showMfaBottomSheet(user); }, ); await privy.mfa.setConfig( MfaConfig( listener: mfaHandler, timeout: const Duration(minutes: 5), ), ); ``` ### Completing verification and resuming operations After the user completes MFA verification, you **must** call `resumeBlockedActions()` to unblock any pending wallet operations: ```dart theme={"system"} // After successful MFA verification final result = await user.mfa.totp.verify.submit(mfaCode); result.fold( onSuccess: (_) async { await privy.mfa.resumeBlockedActions(); }, onFailure: (error) { // Show error to user }, ); ``` If `resumeBlockedActions()` is not called within the configured timeout (default 5 minutes), the original wallet operation will fail with a timeout error. ### Handling cancellation If the user cancels the MFA flow, you must still call `resumeBlockedActions` to unblock operations. Pass an error to indicate cancellation: ```dart theme={"system"} // If the user cancels MFA await privy.mfa.resumeBlockedActions(Exception('User cancelled MFA')); ```
## Example MFA modal To simplify the implementation, we recommend abstracting the logic into a self-contained component that can be used whenever the user needs to complete an MFA flow. For instance, you might write an `MFAModal` component that allows the user to (1) select their desired method of their enrolled MFA methods, (2) request an MFA code, and (3) submit the MFA code to Privy for verification. ```tsx Example modal for guiding users through the MFA flow theme={"system"} import Modal from 'react-modal'; import { useMfa, errorIndicatesMfaVerificationFailed, errorIndicatesMaxMfaRetries, errorIndicatesMfaTimeout, MfaMethod, } from '@privy-io/react-auth'; type Props = { // List of available MFA methods that the user has enrolled in mfaMethods: MfaMethod[]; // Boolean indicator to determine whether or not the modal should be open isOpen: boolean; // Helper function to open/close the modal */ setIsOpen: (isOpen: boolean) => void; }; export const MFAModal = ({mfaMethods, isOpen, setIsOpen}: Props) => { const {init, submit, cancel} = useMfa(); // Stores the user's selected MFA method const [selectedMethod, setSelectedMethod] = useState(null); // Stores the user's MFA code const [mfaCode, setMfaCode] = useState(''); // Stores the options for passkey MFA const [options, setOptions] = useState(null); // Stores an error message to display const [error, setError] = useState(''); // Helper function to request an MFA code for a given method const onMfaInit = async (method: MfaMethod) => { const response = await init(method); setError(''); setSelectedMethod(method); if (method === 'passkey') { setOptions(response); } }; // Helper function to submit an MFA code to Privy for verification const onMfaSubmit = async () => { try { if (selectedMethod === 'passkey') { await submit(selectedMethod, options); } else { await submit(selectedMethod, mfaCode); } setSelectedMethod(null); // Clear the MFA flow once complete setIsOpen(false); // Close the modal } catch (e) { // Handling possible errors with MFA code submission if (errorIndicatesMfaVerificationFailed(e)) { setError('Incorrect MFA code, please try again.'); // Allow the user to re-enter the code and call `submit` again } else if (errorIndicatesMaxMfaRetries(e)) { setError('Maximum MFA attempts reached, please request a new code.'); setSelectedMethod(null); // Clear the MFA flow to allow the user to try again } else if (errorIndicatesMfaTimeout(e)) { setError('MFA code has expired, please request a new code.'); setSelectedMethod(null); // Clear the MFA flow to allow the user to try again } } }; // Helper function to clean up state when the user closes the modal const onModalClose = () => { cancel(); // Cancel any in-progress MFA flows setIsOpen(false); }; return ( {/* Button for the user to select an MFA method and request an MFA code */} {mfaMethods.map((method) => ( ))} {/* Input field for the user to enter their MFA code and submit it */} {selectedMethod && selectedMethod !== 'passkey' && (

Enter your MFA code below

setMfaCode(event.target.value)} />
)} {/* Display error message if there is one */} {!!error.length &&

{error}

}
); }; ```
```tsx Example modal for guiding users through the MFA flow theme={"system"} import Modal from 'react-native'; import { useMfa, errorIndicatesMfaVerificationFailed, errorIndicatesMaxMfaRetries, errorIndicatesMfaTimeout, MfaMethod, } from '@privy-io/expo'; type Props = { // List of available MFA methods that the user has enrolled in mfaMethods: MfaMethod[]; // Boolean indicator to determine whether or not the modal should be open isOpen: boolean; // Helper function to open/close the modal */ setIsOpen: (isOpen: boolean) => void; }; export const MFAModal = ({mfaMethods, isOpen, setIsOpen}: Props) => { const {init, submit, cancel} = useMfa(); const [selectedMethod, setSelectedMethod] = useState(null); const [mfaCode, setMfaCode] = useState(''); const [options, setOptions] = useState(null); const [error, setError] = useState(''); const onMfaInit = async (method: MfaMethod) => { const response = await init({method}); setError(''); setSelectedMethod(method); if (method === 'passkey') { setOptions(response); } }; const onMfaSubmit = async () => { try { if (selectedMethod === 'passkey') { await submit({method: selectedMethod, mfaCode: options}); } else { await submit({method: selectedMethod, mfaCode}); } setSelectedMethod(null); setIsOpen(false); } catch (e) { if (errorIndicatesMfaVerificationFailed(e)) { setError('Incorrect MFA code, please try again.'); } else if (errorIndicatesMaxMfaRetries(e)) { setError('Maximum MFA attempts reached, please request a new code.'); setSelectedMethod(null); } else if (errorIndicatesMfaTimeout(e)) { setError('MFA code has expired, please request a new code.'); setSelectedMethod(null); } } }; const onModalClose = () => { cancel(); setIsOpen(false); }; return ( {mfaMethods.map((method) => ( ); } ``` Privy allows users to delete MFA methods via the `unenrollMfa` method returned from the `useMfaEnrollment` hook: ```tsx theme={"system"} import {useMfaEnrollment} from '@privy-io/expo'; const {unenrollMfa} = useMfaEnrollment(); ``` ## Unenrolling SMS To remove SMS as an MFA method: ```tsx theme={"system"} await unenrollMfa({method: 'sms'}); ``` ## Unenrolling TOTP To remove TOTP as an MFA method: ```tsx theme={"system"} await unenrollMfa({method: 'totp'}); ``` ## Unenrolling passkeys To remove passkeys as an MFA method: ```tsx theme={"system"} await unenrollMfa({method: 'passkey'}); ``` By default, unenrolling a passkey will also unlink it as a valid login method. To modify this behavior, set the `removeForLogin` option to `false`: ```tsx theme={"system"} await unenrollMfa({method: 'passkey', removeForLogin: false}); ``` ## Complete example ```tsx Example unenrolling SMS/TOTP/passkey theme={"system"} import {useMfaEnrollment} from '@privy-io/expo'; export default function MfaUnenrollment() { const {unenrollMfa} = useMfaEnrollment(); return ( ); } ``` Privy allows users to unenroll from MFA methods via the `unenroll` method on each MFA namespace: ```swift theme={"system"} guard let user = await privy.getUser() else { return } // Unenroll from specific MFA methods try await user.mfa.sms.unenroll() try await user.mfa.totp.unenroll() try await user.mfa.passkeys.unenroll() ``` ## Unenrolling SMS To remove SMS as an MFA method: ```swift theme={"system"} let updatedUser = try await user.mfa.sms.unenroll() ``` ## Unenrolling TOTP To remove TOTP as an MFA method: ```swift theme={"system"} let updatedUser = try await user.mfa.totp.unenroll() ``` ## Unenrolling passkeys To remove passkeys as an MFA method: ```swift theme={"system"} let updatedUser = try await user.mfa.passkeys.unenroll() ``` By default, unenrolling a passkey will also unlink it as a valid login method. To keep the passkey as a login method, set the `removeForLogin` parameter to `false`: ```swift theme={"system"} let updatedUser = try await user.mfa.passkeys.unenroll(removeForLogin: false) ``` Privy allows users to unenroll from MFA methods via the `unenroll` method on each MFA namespace: ```kotlin theme={"system"} val user = privy.getUser() ?: return // Unenroll from specific MFA methods user.mfa.sms.unenroll() user.mfa.totp.unenroll() user.mfa.passkeys.unenroll() ``` ## Unenrolling SMS To remove SMS as an MFA method: ```kotlin theme={"system"} user.mfa.sms.unenroll() .onSuccess { updatedUser -> // SMS MFA removed successfully } .onFailure { error -> // Handle error } ``` ## Unenrolling TOTP To remove TOTP as an MFA method: ```kotlin theme={"system"} user.mfa.totp.unenroll() .onSuccess { updatedUser -> // TOTP MFA removed successfully } .onFailure { error -> // Handle error } ``` ## Unenrolling passkeys To remove passkeys as an MFA method: ```kotlin theme={"system"} user.mfa.passkeys.unenroll() .onSuccess { updatedUser -> // Passkey MFA removed successfully } .onFailure { error -> // Handle error } ``` By default, unenrolling a passkey will also unlink it as a valid login method. To keep the passkey as a login method, set the `removeForLogin` parameter to `false`: ```kotlin theme={"system"} user.mfa.passkeys.unenroll(removeForLogin = false) .onSuccess { updatedUser -> // Passkey MFA removed, but passkey still valid for login } ``` Privy allows users to unenroll from MFA methods via the `unenroll` method on each MFA namespace: ```dart theme={"system"} final user = await privy.getUser(); if (user == null) return; // Unenroll from specific MFA methods user.mfa.sms.unenroll(); user.mfa.totp.unenroll(); user.mfa.passkeys.unenroll(); ``` ## Unenrolling SMS To remove SMS as an MFA method: ```dart theme={"system"} final result = await user.mfa.sms.unenroll(); result.fold( onSuccess: (updatedUser) { // SMS MFA removed successfully }, onFailure: (error) { // Handle error }, ); ``` ## Unenrolling TOTP To remove TOTP as an MFA method: ```dart theme={"system"} final result = await user.mfa.totp.unenroll(); result.fold( onSuccess: (updatedUser) { // TOTP MFA removed successfully }, onFailure: (error) { // Handle error }, ); ``` ## Unenrolling passkeys To remove passkeys as an MFA method: ```dart theme={"system"} final result = await user.mfa.passkeys.unenroll(); result.fold( onSuccess: (updatedUser) { // Passkey MFA removed successfully }, onFailure: (error) { // Handle error }, ); ``` By default, unenrolling a passkey will also unlink it as a valid login method. To keep the passkey as a login method, set the `removeForLogin` parameter to `false`: ```dart theme={"system"} final result = await user.mfa.passkeys.unenroll(removeForLogin: false); result.fold( onSuccess: (updatedUser) { // Passkey MFA removed, but passkey still valid for login }, onFailure: (error) { // Handle error }, ); ```
# Overview Source: https://docs.privy.io/authentication/user-authentication/mfa/verify/overview Overview of MFA verification required for signing, recovery, and export of Privy embedded wallets. Once users have successfully enrolled in MFA with Privy, they will be required to complete MFA whenever the private key for their embedded wallet must be used. This includes: * Signing messages and transactions * Recovering the embedded wallet on new devices * Exporting the wallet's private key * Setting a password on the wallet * Enrolling another MFA method or unenrolling an existing one Once a user has completed MFA on a given device, they can continue to use the wallet on that device *without* needing to complete MFA for 15 minutes. After 15 minutes have elapsed, Privy will require that the user complete MFA again to re-authorize use of the wallet's private key. ## Requirements for custom MFA verification To ensure users can complete MFA when required, your app must: 1. **Set up a flow** to guide the user through completing MFA when required 2. **Register an event listener** to configure Privy to invoke the flow whenever MFA is required. [Read more about the MFA required listener here](/authentication/user-authentication/mfa/listener). ## Verification interfaces To set up a flow to have the user complete MFA, use Privy's `useMfa` hook: ```tsx theme={"system"} import {useMfa} from '@privy-io/react-auth'; const {init, submit, cancel} = useMfa(); ``` This flow has three core components: 1. **Requesting an MFA challenge** (`init`) - Sends an MFA code to the user's enrolled method 2. **Submitting the MFA verification** (`submit`) - Verifies the code provided by the user 3. **Cancelling the MFA flow** (`cancel`) - Cancels an in-progress MFA flow if needed To set up a flow to have the user complete MFA, use Privy's `useMfa` hook: ```tsx theme={"system"} import {useMfa} from '@privy-io/expo'; const {init, submit, cancel} = useMfa(); ``` This flow has three core components: 1. **Requesting an MFA challenge** (`init`) - Sends an MFA code to the user's enrolled method 2. **Submitting the MFA verification** (`submit`) - Verifies the code provided by the user 3. **Cancelling the MFA flow** (`cancel`) - Cancels an in-progress MFA flow if needed To set up a flow to have the user complete MFA, access the verification interfaces via the authenticated `user.mfa` namespace: ```swift theme={"system"} guard let user = await privy.getUser() else { return } // SMS verification try await user.mfa.sms.verify.sendCode() try await user.mfa.sms.verify.submit(code: mfaCode) // TOTP verification try await user.mfa.totp.verify.submit(code: mfaCode) // Passkey verification try await user.mfa.passkeys.verify.submit(relyingParty: "https://yourdomain.com") ``` This flow has two core components: 1. **Requesting an MFA challenge** (e.g. `sendCode` for SMS) - Sends an MFA code to the user's enrolled method 2. **Submitting the MFA verification** (`submit`) - Verifies the code provided by the user If your app uses an [MFA required listener](/authentication/user-authentication/mfa/listener), you must call `privy.mfa.resumeBlockedActions()` after successful verification to unblock any pending wallet operations. If you do not call this, the initial call that triggered MFA will never resolve. To set up a flow to have the user complete MFA, access the verification interfaces via the authenticated `user.mfa` namespace: ```kotlin theme={"system"} val user = privy.getUser() ?: return // SMS verification user.mfa.sms.verify.sendCode() user.mfa.sms.verify.submit(mfaCode) // TOTP verification user.mfa.totp.verify.submit(mfaCode) // Passkey verification user.mfa.passkeys.verify.submit(relyingParty = "https://yourdomain.com") ``` This flow has two core components: 1. **Requesting an MFA challenge** (e.g. `sendCode` for SMS) - Sends an MFA code to the user's enrolled method 2. **Submitting the MFA verification** (`submit`) - Verifies the code provided by the user If your app uses an [MFA required listener](/authentication/user-authentication/mfa/listener), you must call `privy.mfa.resumeBlockedActions()` after successful verification to unblock any pending wallet operations. If you do not call this, the initial call that triggered MFA will never resolve. To set up a flow to have the user complete MFA, access the verification interfaces via the authenticated `user.mfa` namespace: ```dart theme={"system"} final user = await privy.getUser(); if (user == null) return; // SMS verification user.mfa.sms.verify.sendCode(); user.mfa.sms.verify.submit(code); // TOTP verification user.mfa.totp.verify.submit(code); // Passkey verification user.mfa.passkeys.verify.submit("https://yourdomain.com"); ``` This flow has two core components: 1. **Requesting an MFA challenge** (e.g. `sendCode` for SMS) - Sends an MFA code to the user's enrolled method 2. **Submitting the MFA verification** (`submit`) - Verifies the code provided by the user If your app uses an [MFA required listener](/authentication/user-authentication/mfa/listener), you must call `privy.mfa.resumeBlockedActions()` after successful verification to unblock any pending wallet operations. If you do not call this, the initial call that triggered MFA will never resolve. ## Cancelling the MFA flow After `init` has been called and the corresponding `submit` call has not yet occurred, the user may cancel their in-progress MFA flow if they wish. To cancel the current MFA flow, call the `cancel` method from the `useMfa` hook: ```tsx theme={"system"} cancel(); ``` After `init` has been called and the corresponding `submit` call has not yet occurred, the user may cancel their in-progress MFA flow if they wish. To cancel the current MFA flow, call the `cancel` method from the `useMfa` hook: ```tsx theme={"system"} cancel(); ``` If the user cancels the MFA flow or verification fails, you must still call `resumeBlockedActions` to unblock any pending wallet operations. Pass an error to indicate the verification was cancelled. ```swift theme={"system"} // Cancel the MFA flow by resuming with an error await privy.mfa.resumeBlockedActions(throwing: MyError.mfaCancelled) ``` See our [MFA required listener guide](/authentication/user-authentication/mfa/listener) for more details about this flow. If the user cancels the MFA flow or verification fails, you must still call `resumeBlockedActions` to unblock any pending wallet operations. Pass an error to indicate the verification was cancelled. ```kotlin theme={"system"} // Cancel the MFA flow by resuming with an error privy.mfa.resumeBlockedActions(mfaError = Exception("User cancelled MFA")) ``` See our [MFA required listener guide](/authentication/user-authentication/mfa/listener) for more details about this flow. If the user cancels the MFA flow or verification fails, you must still call `resumeBlockedActions` to unblock any pending wallet operations. Pass an error to indicate the verification was cancelled. ```dart theme={"system"} // Cancel the MFA flow by resuming with an error await privy.mfa.resumeBlockedActions(Exception('User cancelled MFA')); ``` See our [MFA required listener guide](/authentication/user-authentication/mfa/listener) for more details about this flow. ## Next steps Verify users with SMS codes Verify users with authenticator apps Verify users with passkeys Register an MFA event listener Handle MFA verification errors # Passkey verification Source: https://docs.privy.io/authentication/user-authentication/mfa/verify/passkeys Verify users with passkey-based MFA by prompting authentication with their registered passkey Verify users with passkey-based MFA by prompting them to authenticate with their registered passkey. ## Requesting an MFA challenge To request an MFA challenge for the current user, call the `init` method from the `useMfa` hook, passing `'passkey'` as the MFA method parameter: ```tsx theme={"system"} import {useMfa} from '@privy-io/react-auth'; const {init, submit} = useMfa(); // Request a passkey MFA challenge const options = await init('passkey'); ``` When the MFA method is `'passkey'`, `init` will return an object with options to pass to the native passkey system. The method returns a `Promise` that resolves with these options if the challenge was successfully created, and rejects with an error if there was an issue. ## Submitting the MFA verification Once `init` has resolved successfully, prompt the user to select a passkey by calling `submit` with the options returned from the `init` method: ```tsx theme={"system"} const options = await init('passkey'); // Submit will trigger the system's native passkey prompt await submit('passkey', options); ``` When `submit` resolves successfully, the user has completed MFA and can proceed to use their embedded wallet. ## Requesting an MFA challenge To request an MFA challenge for the current user, call the `init` method from the `useMfa` hook with the appropriate parameters: ```tsx theme={"system"} import {useMfa} from '@privy-io/expo'; const {init, submit} = useMfa(); // Request a passkey MFA challenge const options = await init({method: 'passkey'}); ``` When the MFA method is `'passkey'`, `init` will return an object with options to pass to the native passkey system. ## Submitting the MFA verification Once `init` has resolved successfully, prompt the user to select a passkey by calling `submit` with the options returned from the `init` method: ```tsx theme={"system"} const options = await init({method: 'passkey'}); // Submit will trigger the system's native passkey prompt await submit({method: 'passkey', mfaCode: options}); ``` When `submit` resolves successfully, the user has completed MFA and can proceed to use their embedded wallet. ## Submitting the MFA verification For passkey verification, call the `submit` method with the relying party URL. This will trigger the system's native passkey prompt for biometric authentication: ```swift theme={"system"} guard let user = await privy.getUser() else { return } do { // Submit will trigger the system's native passkey prompt try await user.mfa.passkeys.verify.submit(relyingParty: "https://yourdomain.com") // If you've set up an MFA required listener, notify Privy MFA succeeded and to continue pending actions await privy.mfa.resumeBlockedActions() } catch { // Either prompt user to try again, or notify Privy MFA failed and pass the error to the call site await privy.mfa.resumeBlockedActions(throwing: error) } ``` The `relyingParty` parameter should be the URL origin where your [Apple App Site Association](/authentication/user-authentication/login-methods/passkey) file is hosted (e.g., `https://example.com`). When `submit` completes successfully, the user has completed MFA and can proceed to use their embedded wallet. If your app uses an [MFA required listener](/authentication/user-authentication/mfa/listener), you must call `privy.mfa.resumeBlockedActions()` after successful verification to unblock any pending wallet operations. ## Submitting the MFA verification For passkey verification, call the `submit` method with the relying party URL. This will trigger the system's native passkey prompt for biometric authentication: ```kotlin theme={"system"} val user = privy.getUser() ?: return user.mfa.passkeys.verify.submit(relyingParty = "https://yourdomain.com") .onSuccess { // If you've set up an MFA required listener, notify Privy MFA succeeded privy.mfa.resumeBlockedActions() } .onFailure { error -> // Either prompt user to try again, or notify Privy MFA failed privy.mfa.resumeBlockedActions(mfaError = error) } ``` The `relyingParty` parameter should be the URL origin where your [Digital Asset Links](/authentication/user-authentication/login-methods/passkey) file is hosted (e.g., `https://example.com`). When `submit` completes successfully, the user has completed MFA and can proceed to use their embedded wallet. If your app uses an [MFA required listener](/authentication/user-authentication/mfa/listener), you must call `privy.mfa.resumeBlockedActions()` after successful verification to unblock any pending wallet operations. ## Submitting the MFA verification For passkey verification, call the `submit` method with the relying party URL. This will trigger the system's native passkey prompt for biometric authentication: ```dart theme={"system"} final user = await privy.getUser(); if (user == null) return; final result = await user.mfa.passkeys.verify.submit('https://yourdomain.com'); result.fold( onSuccess: (_) async { // If you've set up an MFA required listener, notify Privy MFA succeeded await privy.mfa.resumeBlockedActions(); }, onFailure: (error) async { // Either prompt user to try again, or notify Privy MFA failed await privy.mfa.resumeBlockedActions(error); }, ); ``` The `relyingParty` parameter should be the URL origin of your app's domain (e.g., `https://example.com`). When `submit` completes successfully, the user has completed MFA and can proceed to use their embedded wallet. If your app uses an [MFA required listener](/authentication/user-authentication/mfa/listener), you must call `privy.mfa.resumeBlockedActions()` after successful verification to unblock any pending wallet operations. # SMS verification Source: https://docs.privy.io/authentication/user-authentication/mfa/verify/sms Verify users with SMS-based MFA by submitting a 6-digit code sent to their phone Verify users with SMS-based MFA by requesting and submitting a 6-digit code sent to their enrolled phone number. ## Requesting an MFA challenge To request an MFA challenge for the current user, call the `init` method from the `useMfa` hook, passing `'sms'` as the MFA method parameter: ```tsx theme={"system"} import {useMfa} from '@privy-io/react-auth'; const {init, submit} = useMfa(); // Request an SMS MFA challenge await init('sms'); ``` The `init` method will prepare an MFA challenge for the SMS method. The user will receive an SMS with their MFA code at the phone number they originally enrolled. The method returns a `Promise` that resolves if the challenge was successfully created, and rejects with an error if there was an issue. ## Submitting the MFA verification Once `init` has resolved successfully, prompt the user to get their MFA code from their SMS and enter it within your app. Then, call the `submit` method from `useMfa`. As parameters, pass the MFA method (`'sms'`) and the MFA code that the user entered: ```tsx theme={"system"} const mfaCode = 'insert-mfa-code-from-user'; await submit('sms', mfaCode); ``` When `submit` resolves successfully, the user has completed MFA and can proceed to use their embedded wallet. ## Requesting an MFA challenge To request an MFA challenge for the current user, call the `init` method from the `useMfa` hook with the appropriate parameters: ```tsx theme={"system"} import {useMfa} from '@privy-io/expo'; const {init, submit} = useMfa(); // Request an SMS MFA challenge await init({method: 'sms'}); ``` The `init` method will prepare an MFA challenge for the SMS method. The user will receive an SMS with their MFA code at the phone number they originally enrolled. ## Submitting the MFA verification Once `init` has resolved successfully, prompt the user to get their MFA code from their SMS and enter it within your app. Then, call the `submit` method: ```tsx theme={"system"} const mfaCode = 'insert-mfa-code-from-user'; await submit({method: 'sms', mfaCode}); ``` When `submit` resolves successfully, the user has completed MFA and can proceed to use their embedded wallet. ## Requesting an MFA challenge To request an MFA challenge for the current user, call the `sendCode` method from `user.mfa.sms.verify`: ```swift theme={"system"} guard let user = await privy.getUser() else { return } // Request an SMS MFA challenge try await user.mfa.sms.verify.sendCode() ``` The `sendCode` method will prepare an MFA challenge for the SMS method. The user will receive an SMS with their MFA code at the phone number they originally enrolled. The method throws if there was an issue sending the code. ## Submitting the MFA verification Once `sendCode` has completed successfully, prompt the user to get their MFA code from their SMS and enter it within your app. Then, call the `submit` method: ```swift theme={"system"} let mfaCode = "123456" // Code entered by user do { try await user.mfa.sms.verify.submit(code: mfaCode) // If you've set up an MFA required listener, notify Privy MFA succeeded and to continue pending actions await privy.mfa.resumeBlockedActions() } catch { // Either prompt user to try again, or notify Privy MFA failed and pass the error to the call site await privy.mfa.resumeBlockedActions(throwing: error) } ``` When `submit` completes successfully, the user has completed MFA and can proceed to use their embedded wallet. If your app uses an [MFA required listener](/authentication/user-authentication/mfa/listener), you must call `privy.mfa.resumeBlockedActions()` after successful verification to unblock any pending wallet operations. ## Requesting an MFA challenge To request an MFA challenge for the current user, call the `sendCode` method from `user.mfa.sms.verify`: ```kotlin theme={"system"} val user = privy.getUser() ?: return // Request an SMS MFA challenge user.mfa.sms.verify.sendCode() .onSuccess { // Code sent successfully, show input for verification code } .onFailure { error -> // Handle error } ``` The `sendCode` method will prepare an MFA challenge for the SMS method. The user will receive an SMS with their MFA code at the phone number they originally enrolled. ## Submitting the MFA verification Once `sendCode` has completed successfully, prompt the user to get their MFA code from their SMS and enter it within your app. Then, call the `submit` method: ```kotlin theme={"system"} val mfaCode = "123456" // Code entered by user user.mfa.sms.verify.submit(mfaCode) .onSuccess { // If you've set up an MFA required listener, notify Privy MFA succeeded privy.mfa.resumeBlockedActions() } .onFailure { error -> // Either prompt user to try again, or notify Privy MFA failed privy.mfa.resumeBlockedActions(mfaError = error) } ``` When `submit` completes successfully, the user has completed MFA and can proceed to use their embedded wallet. If your app uses an [MFA required listener](/authentication/user-authentication/mfa/listener), you must call `privy.mfa.resumeBlockedActions()` after successful verification to unblock any pending wallet operations. ## Requesting an MFA challenge To request an MFA challenge for the current user, call the `sendCode` method from `user.mfa.sms.verify`: ```dart theme={"system"} final user = await privy.getUser(); if (user == null) return; // Request an SMS MFA challenge final result = await user.mfa.sms.verify.sendCode(); result.fold( onSuccess: (_) { // Code sent successfully, show input for verification code }, onFailure: (error) { // Handle error }, ); ``` The `sendCode` method will prepare an MFA challenge for the SMS method. The user will receive an SMS with their MFA code at the phone number they originally enrolled. ## Submitting the MFA verification Once `sendCode` has completed successfully, prompt the user to get their MFA code from their SMS and enter it within your app. Then, call the `submit` method: ```dart theme={"system"} const mfaCode = '123456'; // Code entered by user final result = await user.mfa.sms.verify.submit(mfaCode); result.fold( onSuccess: (_) async { // If you've set up an MFA required listener, notify Privy MFA succeeded await privy.mfa.resumeBlockedActions(); }, onFailure: (error) async { // Either prompt user to try again, or notify Privy MFA failed await privy.mfa.resumeBlockedActions(error); }, ); ``` When `submit` completes successfully, the user has completed MFA and can proceed to use their embedded wallet. If your app uses an [MFA required listener](/authentication/user-authentication/mfa/listener), you must call `privy.mfa.resumeBlockedActions()` after successful verification to unblock any pending wallet operations. # TOTP verification Source: https://docs.privy.io/authentication/user-authentication/mfa/verify/totp Verify users with TOTP-based MFA by submitting a 6-digit code from their authenticator app Verify users with TOTP-based MFA by requesting and submitting a 6-digit code from their authenticator app. ## Requesting an MFA challenge To request an MFA challenge for the current user, call the `init` method from the `useMfa` hook, passing `'totp'` as the MFA method parameter: ```tsx theme={"system"} import {useMfa} from '@privy-io/react-auth'; const {init, submit} = useMfa(); // Request a TOTP MFA challenge await init('totp'); ``` The `init` method will prepare an MFA challenge for the TOTP method. The user will receive the MFA code within their authenticator app. The method returns a `Promise` that resolves if the challenge was successfully created, and rejects with an error if there was an issue. ## Submitting the MFA verification Once `init` has resolved successfully, prompt the user to get their MFA code from their authenticator app and enter it within your app. Then, call the `submit` method from `useMfa`. As parameters, pass the MFA method (`'totp'`) and the MFA code that the user entered: ```tsx theme={"system"} const mfaCode = 'insert-mfa-code-from-user'; await submit('totp', mfaCode); ``` When `submit` resolves successfully, the user has completed MFA and can proceed to use their embedded wallet. ## Requesting an MFA challenge To request an MFA challenge for the current user, call the `init` method from the `useMfa` hook with the appropriate parameters: ```tsx theme={"system"} import {useMfa} from '@privy-io/expo'; const {init, submit} = useMfa(); // Request a TOTP MFA challenge await init({method: 'totp'}); ``` The `init` method will prepare an MFA challenge for the TOTP method. The user will receive the MFA code within their authenticator app. ## Submitting the MFA verification Once `init` has resolved successfully, prompt the user to get their MFA code from their authenticator app and enter it within your app. Then, call the `submit` method: ```tsx theme={"system"} const mfaCode = 'insert-mfa-code-from-user'; await submit({method: 'totp', mfaCode}); ``` When `submit` resolves successfully, the user has completed MFA and can proceed to use their embedded wallet. ## Submitting the MFA verification For TOTP verification, no initialization step is required since the code is generated locally by the user's authenticator app. Prompt the user to get their MFA code from their authenticator app and enter it within your app. Then, call the `submit` method: ```swift theme={"system"} guard let user = await privy.getUser() else { return } let mfaCode = "123456" // Code entered by user from authenticator app do { try await user.mfa.totp.verify.submit(code: mfaCode) // If you've set up an MFA required listener, notify Privy MFA succeeded and to continue pending actions await privy.mfa.resumeBlockedActions() } catch { // Either prompt user to try again, or notify Privy MFA failed and pass the error to the call site await privy.mfa.resumeBlockedActions(throwing: error) } ``` When `submit` completes successfully, the user has completed MFA and can proceed to use their embedded wallet. If your app uses an [MFA required listener](/authentication/user-authentication/mfa/listener), you must call `privy.mfa.resumeBlockedActions()` after successful verification to unblock any pending wallet operations. ## Submitting the MFA verification For TOTP verification, no initialization step is required since the code is generated locally by the user's authenticator app. Prompt the user to get their MFA code from their authenticator app and enter it within your app. Then, call the `submit` method: ```kotlin theme={"system"} val user = privy.getUser() ?: return val mfaCode = "123456" // Code entered by user from authenticator app user.mfa.totp.verify.submit(mfaCode) .onSuccess { // If you've set up an MFA required listener, notify Privy MFA succeeded privy.mfa.resumeBlockedActions() } .onFailure { error -> // Either prompt user to try again, or notify Privy MFA failed privy.mfa.resumeBlockedActions(mfaError = error) } ``` When `submit` completes successfully, the user has completed MFA and can proceed to use their embedded wallet. If your app uses an [MFA required listener](/authentication/user-authentication/mfa/listener), you must call `privy.mfa.resumeBlockedActions()` after successful verification to unblock any pending wallet operations. ## Submitting the MFA verification For TOTP verification, no initialization step is required since the code is generated locally by the user's authenticator app. Prompt the user to get their MFA code from their authenticator app and enter it within your app. Then, call the `submit` method: ```dart theme={"system"} final user = await privy.getUser(); if (user == null) return; const mfaCode = '123456'; // Code entered by user from authenticator app final result = await user.mfa.totp.verify.submit(mfaCode); result.fold( onSuccess: (_) async { // If you've set up an MFA required listener, notify Privy MFA succeeded await privy.mfa.resumeBlockedActions(); }, onFailure: (error) async { // Either prompt user to try again, or notify Privy MFA failed await privy.mfa.resumeBlockedActions(error); }, ); ``` When `submit` completes successfully, the user has completed MFA and can proceed to use their embedded wallet. If your app uses an [MFA required listener](/authentication/user-authentication/mfa/listener), you must call `privy.mfa.resumeBlockedActions()` after successful verification to unblock any pending wallet operations. # Using Privy as your authentication provider Source: https://docs.privy.io/authentication/user-authentication/privy-auth Overview of Privy built-in authentication methods including email, SMS, passkeys, OAuth, and wallet login Privy offers a variety of authentication methods, organized here by their security model: ### Direct methods The user owns the credential outright. No third party can revoke or suspend access. * **[Passkey](/authentication/user-authentication/login-methods/passkey)**: Biometric or passkey-based login based on the WebAuthn standard. Phishing-resistant and device-bound. * **[Wallets](/authentication/user-authentication/login-methods/wallet)**: External wallet login via Sign-In With Ethereum and Sign-In With Solana. ### Delegated methods A third party controls the credential. Account access depends on that provider remaining available. * **[OAuth and socials](/authentication/user-authentication/login-methods/oauth)**: Social login with Google, Apple, Twitter, Discord, GitHub, LinkedIn, Spotify, Telegram, Farcaster, and more. * **[Email](/authentication/user-authentication/login-methods/email) or [SMS](/authentication/user-authentication/login-methods/sms-whatsapp)**: Passwordless login via a one-time passcode sent to a user's email address or phone number. Your app can configure each of the account types above to be an upfront login method, or as an account that users link to their profile after login. Account access is wallet access. If your app uses a delegated login method as the primary authenticator, Privy recommends requiring MFA with either a passkey or authenticator app. [Set up MFA →](/authentication/user-authentication/mfa/overview) All of Privy's authentication methods create a common [user object](/user-management/users/the-user-object) containing the user's unique ID and all accounts linked to their profile. Once a user successfully authenticates, Privy issues an [access token](/authentication/user-authentication/access-tokens) that your app can use to represent an authenticated session or make authenticated requests to the backend. # Tokens Source: https://docs.privy.io/authentication/user-authentication/tokens Understanding access tokens, refresh tokens, and identity tokens in Privy authentication Privy issues three types of tokens when users authenticate with an application: **access tokens**, **refresh tokens**, and **identity tokens**. Each token serves a distinct purpose in the authentication system and works together to provide secure, seamless user experiences. Verify user authentication Maintain user sessions Access user data *** ## Access tokens Access tokens are short-lived credentials that prove a user is authenticated. These tokens should be included in requests from the frontend to your backend to verify that the requesting user is genuinely authenticated. **Key characteristics:** * **Format**: ES256-signed JWT * **Lifetime**: 1 hour (default, configurable) * **Purpose**: Authentication verification * **Claims**: Session ID, user DID, app ID, issuer, timestamps * **Automatic refresh**: Yes, when using `getAccessToken` method **When to use:** * Verifying user authentication * Protecting backend API endpoints Access token lifetime can be configured in the [Privy Dashboard](https://dashboard.privy.io) under **User management > Authentication > Advanced**. Learn how to send and verify access tokens ## Refresh tokens Refresh tokens are long-lived credentials used to obtain new access tokens without requiring the user to re-authenticate. These tokens are managed automatically by Privy's SDKs and are not directly accessible to developers. **Key characteristics:** * **Format**: Opaque string (not a JWT) * **Lifetime**: 30 days (default, configurable) * **Purpose**: Session persistence and access token renewal * **Storage**: Secure storage managed by Privy SDK * **Automatic management**: Yes, handled entirely by Privy **When refresh tokens are used:** Refresh tokens automatically come into play when: * `getAccessToken` is called and the current access token is expired * The user returns to the application within the refresh token lifetime **Session lifecycle:** Privy issues both an access token and a refresh token After 1 hour, the access token becomes invalid The SDK uses the refresh token to request a new access token The user remains authenticated without re-logging in After 30 days (default), the user must re-authenticate Refresh token lifetime can be configured in the [Privy Dashboard](https://dashboard.privy.io) under **User management > Authentication > Advanced**. ## Identity tokens Identity tokens are specialized tokens that contain user data, including linked accounts and custom metadata. These tokens enable efficient access to user information without additional API calls. **Key characteristics:** * **Format**: ES256-signed JWT * **Lifetime**: 10 hours (default, configurable) * **Purpose**: User data access * **Claims**: User DID, linked accounts, custom metadata, app ID, issuer, timestamps * **Configuration**: Must be enabled in Dashboard * **Automatic refresh**: Yes, when user data changes or access token refreshes **When to use:** * Accessing user data (linked accounts, custom metadata) on the backend * Avoiding additional API calls to Privy's servers Identity tokens must be enabled in the [Privy Dashboard](https://dashboard.privy.io) under **User management > Authentication > Advanced > Return user data in an identity token**. The lifetime can be configured under **User management > Authentication > Advanced**. Learn how to retrieve and verify identity tokens *** ## Session management **What is a session?** A session represents a user's authenticated period in an application. Sessions are tracked using the session ID (`sid`) claim present in both access tokens and identity tokens. **Session creation:** A new session is created when: * A user successfully logs in * A user's existing session has expired and they re-authenticate **Session persistence:** Sessions persist through: * Access token expiration and renewal (via refresh tokens) * Page refreshes and navigation * Browser restarts (if refresh token is still valid) **Session termination:** A session ends when: * The user explicitly logs out * The refresh token expires (after 30 days by default) Session duration can be configured under **User management > Authentication > Advanced**. *** ## Security best practices **Always use HTTPS** All token transmission must occur over HTTPS to prevent interception. This applies to: * Initial token issuance from Privy * Token transmission from frontend to backend * Token refresh requests **Always verify tokens on the backend** Never trust tokens without verification. Always: * Verify the token signature using Privy's verification key * Check the token expiration (`exp` claim) * Validate the issuer (`iss` claim) is `privy.io` * Validate the audience (`aud` claim) matches the app ID Use Privy SDKs to ensure tokens are verified correctly on the server. **Minimize token exposure** * Never log tokens in application logs * Never include tokens in URLs or query parameters * Never store tokens in insecure locations (plain text files, client-side localStorage on shared devices) * Never share tokens across different applications **Refresh tokens require extra care** Because refresh tokens are long-lived: * They should never be sent to the frontend in web applications using local storage * They should only be used by the Privy SDK, never directly by application code * They should be rotated when used (handled automatically by Privy) * They should be invalidated immediately upon logout Privy SDKs handle all token security automatically. Manual token management is not recommended and may introduce security vulnerabilities. *** ## Verification ### Verifying access tokens Access tokens should be verified on the backend using Privy's server SDKs. The verification process validates the token signature, checks expiration, and returns the authenticated user's information. **Verification returns:** | Field | Type | Description | | ------------ | -------- | ---------------------------------------- | | `userId` | `string` | The authenticated user's Privy DID | | `sessionId` | `string` | The unique session identifier | | `appId` | `string` | The Privy app ID | | `issuer` | `string` | Always `'privy.io'` | | `issuedAt` | `number` | Unix timestamp when the token was issued | | `expiration` | `number` | Unix timestamp when the token expires | View detailed verification examples for Node.js, Go, Rust, and third-party libraries ### Verifying identity tokens Identity tokens should be verified and parsed using Privy's server SDKs. The verification process validates the signature and parses the user data, including linked accounts and custom metadata. The `verifyAuthToken` method only works with access tokens. Always use the appropriate identity token verification method when working with identity tokens. View detailed verification examples and security considerations ### Refresh tokens cannot be verified Refresh tokens are opaque strings that cannot be verified by application code. They are only valid when presented to Privy's authentication endpoints and are managed entirely by Privy's SDKs. Applications should never: * Attempt to decode or parse refresh tokens * Store refresh tokens separately from Privy's SDK storage * Transmit refresh tokens to custom backends * Include refresh tokens in logs or analytics # UI components Source: https://docs.privy.io/authentication/user-authentication/ui-component Use Privy pre-built login modal UI component for quick authentication integration Privy supports easy onboarding with an out-of-the-box user interface to log users in. The fastest way to integrate Privy is with the Privy login modal. Your application can integrate this modal in just a few lines of code and easily toggle on login methods for your application in the Privy dashboard. You can also design your own login UIs, and integrate with Privy's authentication APIs to offer a login experience that feels seamless within your application. images/Onboard.png [Configure your login methods](/basics/get-started/dashboard/configure-login-methods) in the Privy Dashboard before using the UI components. Privy's UIs are highly-customizable to seamlessly match the branding and design system of your app. Learn more about [customizing the login modal](/basics/react/advanced/configuring-appearance). To have users login to your app with Privy's UIs, use the `login` method from the `useLogin` hook. ```tsx theme={"system"} login: ({ loginMethods: PrivyClientConfig['loginMethods'], prefill?: { type: 'email' | 'phone', value: string }, disableSignup?: boolean, walletChainType?: 'ethereum-only' | 'solana-only' | 'ethereum-or-solana' }) => PrivyUser; ``` ### Usage ```tsx theme={"system"} import { useLogin, usePrivy } from '@privy-io/react-auth'; function LoginButton() { const { ready, authenticated} = usePrivy(); const { login } = useLogin(); // Disable login when Privy is not ready or the user is already authenticated const disableLogin = !ready || (ready && authenticated); return ( ); } ``` ### Parameters Optionally specify which login methods to display in the modal. The following login methods are supported:
Optionally pre-fill the login modal with the user's email or phone number. Prevent users from signing up for your app. This is useful when you want to enforce that users can only log in with existing accounts. Filter the login wallet options to only show wallets that support the specified chain type. ### Callbacks You can easily attach callbacks to the `login` method using the `useLogin` hook. This allows you to run custom logic when a user successfully logs in or when there's an error. ```tsx theme={"system"} import { useLogin } from '@privy-io/react-auth'; function LoginButton() { const { login } = useLogin({ onComplete: ({ user, isNewUser, wasAlreadyAuthenticated, loginMethod, loginAccount }) => { console.log('User logged in successfully', user); console.log('Is new user:', isNewUser); console.log('Was already authenticated:', wasAlreadyAuthenticated); console.log('Login method:', loginMethod); console.log('Login account:', loginAccount); // Navigate to dashboard, show welcome message, etc. }, onError: (error) => { console.error('Login failed', error); // Show error message } }); return ; } ``` Callback that executes when a user completes authentication. If the user is already authenticated when the component mounts, this callback executes immediately. Otherwise, it executes after successful login. The user object with DID, linked accounts, and more. Whether this is the user's first login or a returning user. Whether the user was already authenticated when the component mounted. The authentication method used ('email', 'sms', 'siwe', 'apple', 'discord', 'github', 'google', 'linkedin', 'spotify', 'tiktok', 'twitter', 'farcaster', 'passkey', 'telegram', 'line') or null if already authenticated. The account used for authentication with type ('wallet', 'email', 'phone', 'google\_oauth', 'twitter\_oauth', 'discord\_oauth', 'github\_oauth', 'spotify\_oauth', 'instagram\_oauth', 'tiktok\_oauth', 'linkedin\_oauth', 'apple\_oauth', 'line\_oauth', 'custom\_auth', 'farcaster', 'passkey'). Callback that executes when there's an error during login or when the user exits the login flow.
Make sure you have [properly configured PrivyElements](/basics/react-native/advanced/setup-privyelements) before using UI components for authentication. **OAuth providers require URL scheme configuration**: If you plan to use social login methods (`google`, `apple`, `discord`, etc.) or integrate with global wallets, you **must** configure your app's URL scheme in the [App Client settings](/basics/get-started/dashboard/app-clients#allowed-url-schemes). Without this configuration, OAuth login will fail silently with "Authentication failed" errors. Privy's UIs are highly-customizable to seamlessly match the branding and design system of your app. Learn more about [customizing the login modal](/basics/react-native/advanced/configuring-appearance). To have users login to your app with Privy's UIs, use the `login` method from the `useLogin` hook. ```javascript theme={"system"} login: ({ loginMethods: LoginMethod[], appearance?: { logo?: string } }) => void; ``` ### Usage ```tsx theme={"system"} import { useLogin } from '@privy-io/expo/ui'; function LoginButton() { const { login } = useLogin(); return ( ); } ``` Refer to our [OAuth login](/authentication/user-authentication/login-methods/oauth) guide for more information on login with OAuth providers. ## Using the web based flow instead of the native flow Privy will **automatically** fallback to the web-based flow on Android devices, where native Apple sign-in isn't supported. For the best possible user experience, we recommend using the native "Sign in with Apple" flow as described above. However, if you are unable to use the native flow, or prefer not to, you can use the web based flow instead: ```tsx theme={"system"} import {useLoginWithOAuth} from '@privy-io/expo'; export function LoginScreen() { const {login} = useLoginWithOAuth(); return ( ); } ``` # Setup passkeys Source: https://docs.privy.io/basics/react-native/advanced/setup-passkeys To see an example application that has the Privy Expo SDK configured with passkeys, check out our [Expo starter repo!](https://github.com/privy-io/examples/tree/main/privy-expo-starter) ## 0. Ensure you have configured a custom build configuration If you have not already configured a custom build configuration, follow the [custom build configuration guide](/basics/react-native/installation#metro-build-configuration). ## 1. Install additional peer dependencies ```sh theme={"system"} npx expo install react-native-passkeys ``` ## 2. Update native app settings Passkeys require that you associate a website with your app. To do so, you need to have the associated domain file on your website and the appropriate entitlement in your app. #### 1. Apple App Site Association * Create a `JSON` file with *at least* the following content ```json theme={"system"} { "webcredentials": { "apps": ["."] } } ``` * Make the file accessible on your website at the following path ```txt theme={"system"} https:///.well-known/apple-app-site-association ``` **Make sure to use your `teamID` and `bundleID` in the file hosted on your website.** For more information about supporting associated domains [see Apple's documentation](https://developer.apple.com/documentation/xcode/supporting-associated-domains). #### 2. App configuration Next, update your `app.json` (or `app.config.ts`) to include the `associatedDomains` and `deploymentTarget` like so: ```json theme={"system"} { "expo": { "ios": { "associatedDomains": ["webcredentials:"] } "plugins": [ [ "expo-build-properties", { "ios": { "deploymentTarget": "15.0" } } ] ] } } ``` #### 3. Build Lastly, build your app! ```sh theme={"system"} npx expo prebuild -p ios npx expo run:ios ``` To enable passkey support for your Android app, associate your app with a website that your app owns. #### 1. Digital Asset Links * Create a `JSON` file with *at least* the following content ```json theme={"system"} [ { "relation": ["delegate_permission/common.handle_all_urls"], "target": { "namespace": "android_app", "package_name": "", "sha256_cert_fingerprints": [""] } } ] ``` * Make the file accessible on your website at the following path ```txt theme={"system"} https:///.well-known/assetlinks.json ``` **Make sure to use your `package_name` and `sha256_cert_fingerprint` in the file hosted on your website.** For more information on obtaining the `sha256_cert_fingerprint` for your app, see the [signing report documentation](https://developer.android.com/studio/publish/app-signing#signing_report). For more information about generally supporting Digital Asset Links [see Google's documentation](https://developer.android.com/training/sign-in/passkeys#add-support-dal). #### 2. Dashboard You will also need to add your `sha256_cert_fingerprint` to the allowed Android key hashes list in the `Settings` tab of the Privy dashboard. #### 3. App configuration Next, update your `app.json` (or `app.config.ts`) to look like: ```json theme={"system"} { "expo": { "plugins": [ [ "expo-build-properties", { "android": { "compileSdkVersion": 34 } } ] ] } } ``` #### 4. Build Lastly, build your app! ```sh theme={"system"} npx expo prebuild -p android npx expo run:android ``` # Setting up Privy UIs Source: https://docs.privy.io/basics/react-native/advanced/setup-privyelements Before integrating Privy's default UIs into your app, you must first ensure the necessary components and fonts are installed. ## Custom Build Configuration Using Privy UIs requires a custom build configuration for your React Native application. This is necessary to ensure that the Privy SDK can properly interact with the native components and libraries it relies on. For detailed instructions, see the [Custom Build Configuration](/basics/react-native/installation#metro-build-configuration) guide. ## Install Peer Dependencies First, install the necessary peer dependencies: ```bash theme={"system"} npx expo install react-native-svg expo-clipboard react-native-qrcode-styled react-native-safe-area-context viem ``` ## Fonts ### Install Font Packages Install the following packages: ```bash theme={"system"} npx expo install expo-font @expo-google-fonts/inter ``` ### Load Fonts Load the necessary fonts in your app's root layout (typically in `app/_layout.tsx`): ```tsx theme={"system"} import {Inter_400Regular, Inter_500Medium, Inter_600SemiBold} from '@expo-google-fonts/inter'; import {useFonts} from 'expo-font'; export default function RootLayout() { useFonts({ Inter_400Regular, Inter_500Medium, Inter_600SemiBold, }); // ... } ``` Load the necessary fonts in your app's root component (typically in `App.tsx`): ```tsx theme={"system"} import {Inter_400Regular, Inter_500Medium, Inter_600SemiBold} from '@expo-google-fonts/inter'; import {useFonts} from 'expo-font'; export default function App() { useFonts({ Inter_400Regular, Inter_500Medium, Inter_600SemiBold, }); // ... } ``` ## PrivyElements Component Privy's default UIs in the React Native SDK are powered by the `PrivyElements` modal component. Only mount `PrivyElements` once in your app. ```tsx theme={"system"} import {PrivyElements} from '@privy-io/expo/ui'; export default function RootLayout() { return ( <> {/* Your app's content */} ); } ``` # Features Source: https://docs.privy.io/basics/react-native/features Learn about the authentication, wallet, and UI features supported by the Privy React Native SDK. ## Supported features # Installation Source: https://docs.privy.io/basics/react-native/installation Install the Privy React Native SDK (@privy-io/react-native-auth) and configure it for mobile authentication and embedded wallets. ## Requirements * A React Native project using the latest version * iOS and Android platform support (Web is not supported) ## Installation ### Core Dependencies Install the Privy React Native SDK and its peer dependencies: ```bash theme={"system"} npx expo install expo-apple-authentication expo-application expo-crypto expo-linking expo-secure-store expo-web-browser react-native-passkeys react-native-webview @privy-io/expo-native-extensions @privy-io/expo ``` ### Required Polyfills Install the necessary polyfills: ```bash theme={"system"} npm i fast-text-encoding react-native-get-random-values @ethersproject/shims ``` If your app uses the Expo [bare workflow](https://docs.expo.dev/bare/) ("React Native without Expo"), also run: ```bash theme={"system"} npx pod-install ``` ### Configure Polyfills Create an `entrypoint.js` file and update your `package.json`: ```js entrypoint.js theme={"system"} // Import required polyfills first import 'fast-text-encoding'; import 'react-native-get-random-values'; import '@ethersproject/shims'; // Then import the expo router import 'expo-router/entry'; ``` ```json package.json theme={"system"} { "name": "", "main": "entrypoint.js" } ``` Import the polyfills at the root of your application: ```jsx theme={"system"} // Import required polyfills first import 'fast-text-encoding'; import 'react-native-get-random-values'; import '@ethersproject/shims'; // Other imports ... // Your app's root component export default function App() { ... } ``` If you're using the `@solana/web3.js` package, install the buffer dependency: ```bash theme={"system"} npm i buffer ``` And add this code after importing `react-native-get-random-values`: ```js theme={"system"} import 'react-native-get-random-values'; import {Buffer} from 'buffer'; global.Buffer = Buffer; ``` This guide ensures that your application satisfies the following requirements for integrating: * uses an [expo development build](https://docs.expo.dev/develop/development-builds/introduction/). * has a custom [`metro.config.js` file](https://docs.expo.dev/guides/customizing-metro/#customizing) to customize the Metro bundler settings * enables [package exports for the Metro bundler:](https://reactnative.dev/blog/2023/06/21/package-exports-support#for-app-developers) * uses the `bundler` setting for [Typescript's `moduleResolution`](https://www.typescriptlang.org/tsconfig#moduleResolution) ## Enabling Package Exports React Native 0.79, and Expo 53, have [enabled package exports by default](https://reactnative.dev/blog/2025/04/08/react-native-0.79#metro-faster-startup-and-package-exports-support). Some popular packages present incompatibilities with this change, and the community is working to get these fixed at source. In the meantime, we present a fix below by disabling package exports for the incompatibilities we have found. Update your `metro.config.js` like so: ```js theme={"system"} //...other config logic // Enable package exports for select libraries ... const resolveRequestWithPackageExports = (context, moduleName, platform) => { // Package exports in `isows` (a `viem`) dependency are incompatible, so they need to be disabled if (moduleName === "isows") { const ctx = { ...context, unstable_enablePackageExports: false, }; return ctx.resolveRequest(ctx, moduleName, platform); } // Package exports in `zustand@4` are incompatible, so they need to be disabled if (moduleName.startsWith("zustand")) { const ctx = { ...context, unstable_enablePackageExports: false, }; return ctx.resolveRequest(ctx, moduleName, platform); } // Package exports in `jose` are incompatible, so the browser version is used if (moduleName === "jose") { const ctx = { ...context, unstable_conditionNames: ["browser"], }; return ctx.resolveRequest(ctx, moduleName, platform); } // The following block is only needed if you are // running React Native 0.78 *or older*. if (moduleName.startsWith('@privy-io/')) { const ctx = { ...context, unstable_enablePackageExports: true, }; return ctx.resolveRequest(ctx, moduleName, platform); } return context.resolveRequest(context, moduleName, platform); }; config.resolver.resolveRequest = resolveRequestWithPackageExports; ... module.exports = config; ``` ## Typescript's Module Resolution Also configure your `tsconfig.json` like so: ```json theme={"system"} { "extends": "expo/tsconfig.base", "compilerOptions": { "strict": true, // Allows us to use conditional/deep imports on published packages "moduleResolution": "Bundler" } } ``` # Quickstart Source: https://docs.privy.io/basics/react-native/quickstart Learn how to authenticate users, create embedded wallets, and send transactions in your React Native app ## 0. Prerequisites This guide assumes that you have completed the [setup](/basics/react-native/setup) guide. ## 1. Enable a user to log in via email This quickstart guide will demonstrate how to authenticate a user with a one time password as an example, but Privy supports many authentication methods. Explore our [Authentication docs](/authentication/overview) to learn about other methods such as socials, passkeys, and external wallets to authenticate users in your app. **To authenticate a user via their email address, use the React Native SDK's `useLoginWithEmail` hook.** ```tsx theme={"system"} import {useLoginWithEmail} from '@privy-io/expo'; ... const {sendCode, loginWithCode} = useLoginWithEmail(); ``` Ensure that this hook is mounted in a component that is wrapped by the [PrivyProvider](/basics/react-native/setup#initializing-privy). You can use the returned methods **`sendCode`** and **`loginWithCode`** to authenticate your user per the instructions below. ### Send an OTP Send a one-time passcode (OTP) to the user's **email** by passing their email address to the **`sendCode`** method returned from `useLoginWithEmail`: ```tsx theme={"system"} import {useLoginWithEmail} from '@privy-io/expo'; export function LoginScreen() { const [email, setEmail] = useState(''); const [codeSent, setCodeSent] = useState(false); const {sendCode} = useLoginWithEmail(); return ( Login {/* prettier-ignore */} {!codeSent ? ( ) : ( {/* prettier-ignore */} )} ); } ``` ## 2. Create an embedded wallet for the user Your app can configure Privy to [**automatically** create wallets](/basics/react-native/advanced/automatic-wallet-creation) for your users as part of their **login** flow. The embedded wallet will be generated and linked to the user object upon authentication. Alternatively your app can [**manually** create wallets](/wallets/wallets/create/create-a-wallet) for users when required. Privy can provision wallets for your users on both **Ethereum** and **Solana**. ## 3. Send and sign transactions using the embedded wallet To request signatures and transactions from a wallet, you must first get an EIP1193 provider for the wallet. ```ts theme={"system"} import {useEmbeddedEthereumWallet} from '@privy-io/expo'; // Get an EIP-1193 Provider const {wallets} = useEmbeddedEthereumWallet(); const provider = await wallets[0].getProvider(); ``` Once you have the embedded wallet's EIP-1193 provider, you can use the provider's **`request`** method to send JSON-RPC requests that request signatures and transactions from the wallet! The **`request`** method accepts an object with the fields: * **`method`** (required): the name of the JSON-RPC method as a string (e.g. **`personal_sign`** or **`eth_sendTransaction`**) * **`params`** (optional): an array of arguments for the JSON-RPC method specified by **`method`** ```tsx theme={"system"} // Get address const accounts = await provider.request({ method: 'eth_requestAccounts' }); // Sign message const message = 'I hereby vote for foobar'; const signature = await provider.request({ method: 'personal_sign', params: [message, accounts[0]] }); ``` ```tsx theme={"system"} // Get address // Get an EIP-1193 Provider const provider = await wallet.getProvider(); const accounts = await provider.request({ method: 'eth_requestAccounts' }); // Send transaction (will be signed and populated) const response = await provider.request({ method: 'eth_sendTransaction', params: [ { from: accounts[0], to: '0x0000000000000000000000000000000000000000', value: '1' } ] }); ``` ```ts theme={"system"} import {useEmbeddedSolanaWallet} from '@privy-io/expo'; // get a Solana provider const {wallets} = useEmbeddedSolanaWallet(); const provider = await wallets[0].getProvider(); ``` Once you have the embedded wallet's Solana provider, you can use the provider's methods to interact with the Solana blockchain. ```tsx theme={"system"} // Sign message const message = 'Hello world'; const {signature} = await provider.request({ method: 'signMessage', params: {message} }); ``` ```tsx theme={"system"} // Create a connection to the Solana network const connection = new Connection('insert-your-rpc-url-here'); // Create your transaction (either legacy Transaction or VersionedTransaction) // transaction = ... // Send the transaction const {signature} = await provider.request({ method: 'signAndSendTransaction', params: { transaction: transaction, connection: connection } }); ``` [Learn more](/wallets/using-wallets/ethereum/send-a-transaction) about sending transactions with the embedded wallet. Privy enables you to take many actions on the embedded wallet, including [sign a message](/wallets/using-wallets/ethereum/sign-a-message), [sign typed data](/wallets/using-wallets/ethereum/sign-typed-data), and [sign a transaction](/wallets/using-wallets/ethereum/sign-a-transaction). Congratulations, you have successfully been able to integrate Privy authentication and wallet into your React Native application! # Setup Source: https://docs.privy.io/basics/react-native/setup Configure the Privy React Native SDK with your appId and set up the PrivyProvider in your mobile app. ## Prerequisites Before you begin, make sure you have [set up your Privy app and obtained your app ID](/basics/get-started/dashboard/create-new-app) and [client ID](/basics/get-started/dashboard/app-clients) from the Privy Dashboard. A properly set up app client is required for mobile apps and other non-web platforms to allow your app to interact with the Privy API. Please follow [this guide](/basics/get-started/dashboard/app-clients) to configure an app client. ## Initializing Privy In your project, **import the `PrivyProvider` component and wrap your app with it**. The `PrivyProvider` must wrap *any* component or page that will use the Privy React Native SDK, and it is generally recommended to render it as close to the root of your application as possible. Wrap your app with the `PrivyProvider` in the `app/_layout.tsx` file. ```tsx theme={"system"} import {PrivyProvider} from '@privy-io/expo'; import {Slot} from 'expo-router'; export default function RootLayout() { return ( ); } ``` ### Protect routes with `AuthBoundary` Setting up `PrivyProvider` is all you need to use the Privy React Native SDK throughout your app! But if you want to protect certain routes, we recommend you do so by using the `AuthBoundary` component, as follows: Start by setting up a [route group](https://docs.expo.dev/router/layouts/#groups), like `(app)/`, under your `app/` directory. Routes placed under this group will be protected by the `AuthBoundary` component, so only authenticated users can access them. ```text theme={"system"} app ├── (app) │ ├── _layout.tsx │ └── index.tsx ├── _layout.tsx └── sign-in.tsx ``` In the `(app)/_layout.tsx` file, wrap the `Stack` component with the `AuthBoundary` component: ```tsx theme={"system"} import {Stack, Redirect} from 'expo-router'; import {AuthBoundary} from '@privy-io/expo'; export default function AppLayout() { return ( } error={(error) => } unauthenticated={} > ); } ``` You must provide the following props to `AuthBoundary`: * `loading` and `error` are both custom components that you can define to show specific UIs during the loading and error states. * On `unauthenticated`, you should redirect the user to the sign in page, as defined above! If you want more details, or wish to take a manual approach without using `AuthBoundary`, take a look at [Expo Router's docs on Authentication](https://docs.expo.dev/router/reference/authentication/). Wrap your app with the `PrivyProvider` in the `App.tsx` file. ```tsx theme={"system"} import {PrivyProvider} from '@privy-io/expo'; import {HomeScreen} from './HomeScreen'; export default function App() { return ( ); } ``` ## Configuration The `PrivyProvider` component accepts the following props: Your Privy App ID. You can find this in the Privy Dashboard. Your Privy Client ID. You can find this in the Privy Dashboard. ## Waiting for Privy to be ready When the `PrivyProvider` is first rendered, the Privy SDK will initialize some state about the current user. This might include checking if the user has a wallet connected, refreshing expired auth tokens, fetching up-to-date user data, and more. **It's important to wait until the `PrivyProvider` has finished initializing *before* you consume Privy's state and interfaces**, to ensure that the state you consume is accurate and not stale. To determine whether the Privy SDK has fully initialized, **check the `isReady` Boolean returned by the `usePrivy` hook.** When `isReady` is true, Privy has completed initialization, and your app can consume Privy's state and interfaces. ```tsx theme={"system"} import {usePrivy} from '@privy-io/expo'; function YourComponent() { const {isReady} = usePrivy(); if (!isReady) { return ; } // Now it's safe to use other Privy hooks and state return ; } ``` Learn how to [log users in](/authentication/user-authentication/login-methods/email) and [transact with embedded wallets](/wallets/wallets/create/create-a-wallet) Check out our [Expo starter repo](https://github.com/privy-io/examples/tree/main/privy-expo-starter) for a complete example # Automatic wallet creation Source: https://docs.privy.io/basics/react/advanced/automatic-wallet-creation If your app uses embedded wallets, you can configure Privy to create wallets **automatically** for your users as part of their **login** flow. Automatic embedded wallet creation is currently not supported if your app uses Privy's whitelabel login interfaces. If this is the case for your app, you must [manually create embedded wallets](/wallets/wallets/create/create-a-wallet) for your users at the desired point in your onboarding flow. Automatic wallet creation only applies to login via the Privy modal and not from whitelabel login methods. It does not trigger wallet creation for users who authenticate through direct login methods like loginWithCode, useLoginWithOAuth, or similar custom flows. To configure Privy to automatically create embedded wallets for your user when they login, **set the `config.embeddedWallets.ethereum.createOnLogin`** property of your `PrivyProvider`: ```tsx theme={"system"} {children} ``` Determines when to create a wallet for the user. * `'all-users'`: Create a wallet for all users on login. * `'users-without-wallets'`: Create a wallet for users who do not have a wallet on login. * `'off'`: Do not create a wallet on login. To configure Privy to automatically create embedded wallets for your user when they login, **set the `config.embeddedWallets.solana.createOnLogin`** property of your `PrivyProvider`: ```tsx theme={"system"} {children} ``` Determines when to create a wallet for the user. * `'all-users'`: Create a wallet for all users on login. * `'users-without-wallets'`: Create a wallet for users who do not have a wallet on login. * `'off'`: Do not create a wallet on login. To configure Privy to automatically create embedded wallets for your user when they login, **set the `config.embeddedWallets.ethereum.createOnLogin`** and `config.embeddedWallets.solana.createOnLogin` properties of your `PrivyProvider`: ```tsx theme={"system"} {children} ``` # Configuring EVM networks Source: https://docs.privy.io/basics/react/advanced/configuring-evm-networks **Privy is compatible with any EVM-compatible chain, and makes it easy to configure networks for your users' wallets.** You can seamlessly use Privy with Ethereum Mainnet, Base, Polygon, Arbitrum, Monad, Berachain, MegaETH, Mantle, Story, and any chain that supports EVM RPC requests. Check out a [high-level overview](/basics/react/advanced/configuring-evm-networks#overview) of network configuration with Privy, or jump directly into [concrete instructions](/basics/react/advanced/configuring-evm-networks#configuration)! Privy is also compatible with app-specific chains, such as those deployed via a RaaS provider. See more [here](/basics/react/advanced/configuring-evm-networks#other-networks). ## Overview Privy exposes two parameters to configure networks: a single [**default chain**](/basics/react/advanced/configuring-evm-networks#default-chain) and a list of [**supported chains**](/basics/react/advanced/configuring-evm-networks#supported-chains). If you choose not to use these parameters in your app, you can instead use Privy's [default configuration and supported chains](/basics/react/advanced/configuring-evm-networks#default-configuration). ### Default Chain The **default chain** should be the primary network that wallets should use in your app. For **embedded wallets**, when a user logs in or creates a wallet in your app, Privy will initialize the embedded wallet's network to the default chain. Thereafter, the embedded wallet will by default use the **default chain**, unless you manually switch the wallet's network to another [**supported chain**](/basics/react/advanced/configuring-evm-networks#supported-chains). For **external wallets**, when a user connects their wallet to your app, Privy will prompt the user to switch their network to the default chain, as long as the wallet supports the network. If the user declines to switch their network to the **default chain**, they will still be permitted to connect their wallet. Not all wallets support all EVM networks. Please note that the following wallets may reject connection requests if you specify one of their unsupported networks as a `defaultChain`: - **Rainbow Wallet**'s [mobile app](https://rainbow.me/download) does not support testnets, and will reject connections if you specify a testnet as a `defaultChain`. - **Trust Wallet**'s [SWIFT](https://trustwallet.com/blog/introducing-trust-wallet-swift) (in beta) only supports BNB Smart Chain, Polygon, Avalanche C-Chain, Arbitrum, OP Mainnet, Base, and OpBNB. If you specify a `defaultChain` that is not one of these networks, the wallet will reject the connection request. ### Supported Chains The **supported chains** list should be a list of networks that wallets are *permitted* to use in your app. This is intended as a guardrail against accidentally taking actions on the wrong network. For **embedded wallets**, attempting to send a transaction on or switch the wallet to a network *not* in the list of **supported chains** will throw an error. For **external wallets**, attempting to programmatically switch the wallet to a network *not* in the list of **supported chains** will throw an error. If a list of **supported chains** is set but no [**default chain**](/basics/react/advanced/configuring-evm-networks#default-chain) is set: * Embedded wallets will be connected to the first entry of the **supported chains** list by default. * External wallets will **not** be prompted to a particular default chain when connecting or logging in; they will be permitted to login on whatever chain they are on. If you'd like to prompt users to switch to a particular network, you should explicitly set a **default chain**. For external wallets (e.g. MetaMask), users may switch their wallet's network *manually*, independent of both Privy and your application. There is no way to prevent this behavior; Privy will **not** throw an error, and you can only re-prompt the user to switch to a different network. ## Configuration **Privy embedded wallets can support *any* EVM-compatible chain**. ### `viem`-Supported Networks If your desired EVM network is supported by the [**`viem/chains`**](https://viem.sh/docs/chains/introduction#chains) package, continue with the instructions below. The package's supported networks are listed [here](https://github.com/wevm/viem/blob/main/src/chains/index.ts). Otherwise, skip to the [**Other Networks**](/basics/react/advanced/configuring-evm-networks#other-networks) section. To configure [**`viem`**](https://viem.sh/docs/chains/introduction#chains)-supported networks for Privy, **first, install the [`viem`](https://viem.sh/docs/installation#installation) package**. This package contains JSON representations of several EVM networks, which will be used to initialize the Privy SDK. ```sh theme={"system"} npm i viem ``` Next, **import your default chain and/or supported chains from the [`viem/chains`](https://viem.sh/docs/chains/introduction#chains) package**: ```tsx theme={"system"} // Replace this with any of the networks listed at https://github.com/wevm/viem/blob/main/src/chains/index.ts import {base, berachain, polygon, arbitrum, story, mantle, tempo} from 'viem/chains'; ``` **Lastly, configure your `PrivyProvider` with these additional network(s).** In particular, the **`config`** property of the **`PrivyProvider`** contains the optional parameters: * **`defaultChain`** field, where you should pass a *single* chain object for your desired default chain * **`supportedChains`** field, where you should pass a *list* of chain objects for your desired supported chains ```tsx {6,8} theme={"system"} {/* your app's content */} ``` The **`PrivyProvider`** will throw an error if: * an empty array (`[]`) is passed into **`supportedChains`** * a chain is passed into **`defaultChain`** that is *not* also included in **`supportedChains`** array **That's it! You've successfully configured networks for external and embedded wallets in your app.** 🎉 ### Other Networks If your desired EVM network is **not** supported by [**`viem/chains`**](https://viem.sh/docs/chains/introduction#chains), you can still use Privy with it per the steps below! First, **import `viem` and use the package's [`defineChain`](https://viem.sh/docs/chains/introduction#custom-chains) method to build a JSON representation of your desired network.** ```tsx theme={"system"} import {defineChain} from 'viem'; export const myCustomChain = defineChain({ id: 123456789, // Replace this with your chain's ID name: 'My Custom Chain', network: 'my-custom-chain', nativeCurrency: { decimals: 18, // Replace this with the number of decimals for your chain's native token name: 'My Native Currency Name', symbol: 'My Native Currency Symbol' }, rpcUrls: { default: { http: ['https://my-custom-chain-https-rpc'], webSocket: ['wss://my-custom-chain-websocket-rpc'] } }, blockExplorers: { default: {name: 'Explorer', url: 'my-custom-chain-block-explorer'} } }); ``` At minimum, you must provide the network's name and chain ID, native currency, RPC URLs, and a blockexplorer URL. Then, **pass the returned object (`myCustomChain` in the example above) to the `defaultChain` and `supportedChains` properties of the `PrivyProvider`.** ## Overriding a chain's RPC provider **By default, transactions from the embedded wallet will be sent using Privy's default RPC providers.** Please note that Privy's default providers are subject to rate limits; these limits are sufficiently generous for developing your integration and moderate amounts of app usage. **As your app's usage scales, we recommend that you setup your own RPC providers** (with [Alchemy](https://www.alchemy.com/), [QuickNode](https://www.quicknode.com/), [Blast](https://blastapi.io/), etc.) and configure Privy to use these providers per the instructions below. Setting up your own providers gives you maximum control over RPC throughput and rate limits, and offers you much more visibility into RPC analytics and common errors. To configure Privy to use a custom RPC provider, first, **import the chain you want to override, and import the helper function `addRpcUrlOverrideToChain` from `@privy-io/chains` to override the RPC provider** ```ts theme={"system"} import {mainnet} from 'viem/chains'; import {addRpcUrlOverrideToChain} from '@privy-io/chains'; const mainnetOverride = addRpcUrlOverrideToChain(mainnet, 'INSERT_CUSTOM_RPC_URL'); ``` Now, you can **add the chain returned by `addRpcUrlOverrideToChain` (e.g. `mainnetOverride`) to the `supportedChains` config option** like before. ## Default Configuration If neither **`defaultChain`** nor **`supportedChains`** is explicitly set for your app, Privy will automatically default to the following list of EVM-compatible networks: **Want to use a chain not listed below?** Configure Privy with any EVM-compatible chain, like Berachain, Monad, or Story per the guidance [here](/basics/react/advanced/configuring-evm-networks#configuration). | Network | [Chain ID](https://chainlist.org/) | Supported? | Privy RPC | | ----------------- | ---------------------------------- | ---------- | --------- | | Arbitrum | 42161 | ✅ | ✅ | | Arbitrum Sepolia | 421614 | ✅ | ✅ | | Avalanche C-Chain | 43114 | ✅ | | | Avalanche Fuji | 43113 | ✅ | | | Base | 8453 | ✅ | ✅ | | Base Sepolia | 84532 | ✅ | ✅ | | Berachain Artio | 80085 | ✅ | | | Celo | 42220 | ✅ | | | Celo Alfajores | 44787 | ✅ | | | Ethereum | 1 | ✅ | ✅ | | Ethereum Sepolia | 11155111 | ✅ | ✅ | | Holesky | 17000 | ✅ | | | Holesky Redstone | 17001 | ✅ | | | Holesky Garnet | 17069 | ✅ | | | Lukso | 42 | ✅ | | | Linea | 59144 | ✅ | | | Linea Testnet | 59140 | ✅ | | | Optimism | 10 | ✅ | ✅ | | Optimism Sepolia | 11155420 | ✅ | ✅ | | Polygon | 137 | ✅ | ✅ | | Polygon Amoy | 80002 | ✅ | ✅ | | Redstone | 690 | ✅ | | | Tempo | 4217 | ✅ | | | Zora | 7777777 | ✅ | | | Zora Sepolia | 999999999 | ✅ | | * External wallets will **not** be prompted to switch networks when connecting to your app. * Embedded wallets will initialize on **Ethereum mainnet** or the network used in the user's previous session on that device. For both external and embedded wallets, you can switch a wallet to any of the following networks that are available from Privy out-of-the-box. As a reminder, **you can always [configure Privy with additional EVM networks](/basics/react/advanced/configuring-evm-networks#configuration).** Security best practices [suggest maintaining a strict Content Security Policy](/security/implementation-guide/content-security-policy). In order to help with this, some chains are served by Privy out-of-the-box at `*.rpc.privy.systems`. For all other chains, Privy will pull from the Viem default RPC URL in its respective [chain definition](https://github.com/wevm/viem/tree/main/src/chains/definitions) if no override is specified. # Configuring Solana networks Source: https://docs.privy.io/basics/react/advanced/configuring-solana-networks Configure Solana RPC endpoints and clusters (mainnet, devnet, testnet) in the PrivyProvider using createSolanaRpc Privy supports [Solana clusters](https://solana.com/docs/core/clusters) such as Mainnet Beta, Devnet, and Testnet. To configure RPC endpoints for Solana when using the Privy embedded wallet UIs (UI `signTransaction` and `signAndSendTransaction`), set RPC clients under the `config.solana.rpcs` prop of the `PrivyProvider`: ```tsx theme={"system"} {/* your app's content */} ``` The `config.solana.rpcs` configuration is only required for Privy's embedded wallet UIs. If you are using external Solana wallets (e.g., Phantom, Solflare) without embedded wallet UIs, you do not need to set `config.solana.rpcs`. # Custom Solana Virtual Machine (SVM) networks In addition to supporting transactions on Solana mainnet, devnet, and testnet, Privy also supports sending transactions on any blockchain that implements the [Solana Virtual Machine (SVM)](https://squads.so/blog/solana-svm-sealevel-virtual-machine). You can send a transaction on a custom SVM by initializing the `Connection` instance for your transaction with the RPC URL for the SVM, like so: ```tsx theme={"system"} // Initialize connection instance with custom SVM RPC URL let connection = new Connection('insert-custom-SVM-rpc-url'); // Build out the transaction object for your desired program // https://solana-foundation.github.io/solana-web3.js/classes/Transaction.html let transaction = new Transaction(); // Send transaction on custom SVM console.log(await wallet.sendTransaction!(transaction, connection)); ``` # Migrating to 2.0 Source: https://docs.privy.io/basics/react/advanced/migrating-to-2.0 This guide will help you migrate your Privy React SDK from v1.x.x to v2.0.0. To install the latest version, install the package from the `latest` tag: ```bash theme={"system"} npm i @privy-io/react-auth@latest ``` ## New features and improvements 🎉 * Removed ethers v5 dependency, allowing developers to more easily use ethers v6 * Added support for submitting transactions without waiting for confirmation * Added UIs for Ethereum signTransaction For the full set of changes check out our [changelog](/changelogs/react-auth). ## Breaking changes ### Authentication * Guaranteed that `user.wallet` is the first linked wallet on the user object. To maintain state of the latest connected wallet, interact with the wallets array directly. * Removed the `forkSession` method. This feature was experimental and has been removed. * Removed the `PrivyProvider`'s deprecated `onSuccess` prop - use the `onSuccess` callback registered via the `useLogin` hook instead. ### Embedded wallets * Apps using [custom auth providers](/authentication/user-authentication/jwt-based-auth/overview) must now explicitly configure wallet UIs in the dashboard, or use the updated `showWalletUIs` option. * Removed the `PrivyProvider`'s deprecated `createPrivyWalletOnLogin` prop. Use `config.embeddedWallets.createOnLogin` instead. ```tsx theme={"system"} ... ``` * Removed the deprecated `additionalChains` and `rpcConfig` props from `PrivyProvider` config, please configure these via the `supportedChains` ```tsx theme={"system"} ... ``` * Removed the deprecated `noPromptOnSignature` configuration option. Configure wallet UIs in the dashboard, or use the updated `showWalletUIs` option. ```tsx theme={"system"} ... ``` #### EVM * Removed the deprecated `getEthersProvider` and `getWeb3jsProvider` from the `ConnectedWallet` class. Use `getEthereumProvider` instead. ```ts {skip-check} theme={"system"} const provider = await wallet.getEthersProvider(); // [!code --] const privyProvider = await wallet.getEthereumProvider(); // [!code ++] const provider = new ethers.providers.Web3Provider(privyProvider); // [!code ++] const provider = await wallet.getWeb3jsProvider(); // [!code --] const privyProvider = await wallet.getEthereumProvider(); // [!code ++] const provider = new Web3(privyProvider); // [!code ++] ``` * Ethereum `sendTransaction` method now returns a `Promise<{hash: string}>` instead of a `Promise`. To get the full details of the submitted transaction, use a library like [viem](https://viem.sh/docs/actions/public/getTransactionReceipt). ```tsx theme={"system"} const receipt = await sendTransaction({...}); // [!code --] const {hash} = await sendTransaction({...}); // [!code ++] const receipt = await publicClient.waitForTransactionReceipt({hash}); // [!code ++] ``` * Removed the experimental `waitForTransactionConfirmation` config option as it is the default behavior. ```tsx theme={"system"} ... ``` * Updated `signMessage`, `signTypedData`, `sendTransaction`, and `signTransaction` methods: ```tsx theme={"system"} const {signMessage} = usePrivy(); // `uiOptions` and `address` are optional const signature = await signMessage(message, uiOptions, address); // [!code --] // the first argument should be formatted `{message: string}` const {signature} = await signMessage({message}, {uiOptions, address}); // [!code ++] ``` ```tsx theme={"system"} const {signTypedData} = usePrivy(); // `uiOptions` and `address` are optional const signature = await signTypedData(typedData, uiOptions, address); // [!code --] const {signature} = await signTypedData(typedData, {uiOptions, address}); // [!code ++] ``` ```tsx theme={"system"} const {sendTransaction} = usePrivy(); // `uiOptions`, `fundWalletConfig`, and `address` are optional const receipt = await sendTransaction(transaction, uiOptions, fundWalletConfig, address); // [!code --] const {hash} = await sendTransaction(transaction, {uiOptions, fundWalletConfig, address}); // [!code ++] ``` ```tsx theme={"system"} const {signTransaction} = usePrivy(); // `uiOptions`, and `address` are optional const signature = await signTransaction(transaction, uiOptions, fundWalletConfig, address); // [!code --] const {signature} = await signTransaction(transaction, {uiOptions, address}); // [!code ++] ``` #### Smart Wallets * Updated `signMessage`, `signTypedData`, and `sendTransaction` methods of the smart wallet client: ```tsx theme={"system"} import {useSmartWallets} from '@privy-io/react-auth/smart-wallets'; const {client} = useSmartWallets(); // `uiOptions` and `address` are optional const signature = await client.signMessage({message}, uiOptions, address); // [!code --] const signature = await client.signMessage({message}, {uiOptions, address}); // [!code ++] ``` ```tsx theme={"system"} import {useSmartWallets} from '@privy-io/react-auth/smart-wallets'; const {client} = useSmartWallets(); // `uiOptions` and `address` are optional const signature = await client.signTypedData(typedData, uiOptions, address); // [!code --] const signature = await client.signTypedData(typedData, {uiOptions, address}); // [!code ++] ``` ```tsx theme={"system"} import {useSmartWallets} from '@privy-io/react-auth/smart-wallets'; const {client} = useSmartWallets(); // `uiOptions`, `fundWalletConfig`, and `address` are optional const hash = await client.sendTransaction(transaction, uiOptions, fundWalletConfig, address); // [!code --] const hash = await client.sendTransaction(transaction, {uiOptions, fundWalletConfig, address}); // [!code ++] ``` #### Solana * Migrated `useSendSolanaTransaction` from `@privy-io/react-auth` to `useSendTransaction` from `@privy-io/react-auth/solana` (Solana-specific export path) ```tsx theme={"system"} import {useSendSolanaTransaction} from '@privy-io/react-auth'; // [!code --] import {useSendTransaction} from '@privy-io/react-auth/solana'; // [!code ++] ... const {sendSolanaTransaction} = useSendSolanaTransaction(); // [!code --] const {sendTransaction} = useSendTransaction(); // [!code ++] ``` * Removed `sendSolanaTransaction` from `usePrivy` in favor of exporting `sendTransaction` from `useSendTransaction` from `@privy-io/react-auth/solana` ```tsx theme={"system"} import {usePrivy} from '@privy-io/react-auth'; // [!code --] import {useSendTransaction} from '@privy-io/react-auth/solana'; // [!code ++] ... const {sendSolanaTransaction} = usePrivy(); // [!code --] const {sendTransaction} = useSendTransaction(); // [!code ++] ``` * Removed `delegateWalletAction` from `useSolanaWallets`. Use `delegateWallet` from `useDelegatedActions` instead. ```tsx theme={"system"} import {useSolanaWallets} from '@privy-io/react-auth/solana'; // [!code --] import {useDelegatedActions} from '@privy-io/react-auth'; // [!code ++] ... const {delegateWalletAction} = useSolanaWallets(); // [!code --] delegateWalletAction(); // [!code --] const {delegateWallet} = useDelegatedActions(); // [!code ++] await delegateWallet({ // [!code ++] address: '', // [!code ++] chainType: 'solana', // [!code ++] }); // [!code ++] ``` * Removed rpcUrl from `fundWallet` from `useSolanaWallets`. Set rpcUrl in `config.solanaClusters` prop of the `PrivyProvider` instead ```tsx theme={"system"} import {useSolanaWallets} from '@privy-io/react-auth/solana'; const {fundWallet} = useSolanaWallets(); fundWallet({ address: '', cluster: {name: 'mainnet-beta', rpcUrl: 'https://api.mainnet-beta.solana.com'}, // [!code --] cluster: {name: 'mainnet-beta'} // [!code ++] }); {/* your app's content */} ; ``` ### Connectors * Removed the `setActiveWallet` method - use the `wallets` array directly to interact with wallets. ### Callbacks * Updated all non-error [callbacks](/authentication/user-authentication/login-methods/email) to use named arguments instead of positional arguments. ```tsx theme={"system"} const {login} = useLogin({ onComplete: (user, isNewUser, wasAlreadyAuthenticated, loginMethod, linkedAccount) => { // [!code --] onComplete: ({user, isNewUser, wasAlreadyAuthenticated, loginMethod, linkedAccount}) => { // [!code ++] console.log(user, isNewUser, wasAlreadyAuthenticated, loginMethod, linkedAccount); // Any logic you'd like to execute if the user is/becomes authenticated while this // component is mounted }, ... onError: (error) => { // onError will continue to stay as a singular error argument console.log(error) }}) ... const {reauthorize} = useOAuthTokens({ onOAuthTokenGrant: (tokens: OAuthTokens, {user}: {user: User}) => { // [!code --] onOAuthTokenGrant: ({tokens, user}) => { // [!code ++] const oAuthToken = tokens.accessToken ... }}) ``` # Migrating to 3.0 Source: https://docs.privy.io/basics/react/advanced/migrating-to-3.0 This guide will help you migrate your Privy React SDK from v2.x.x to v3.0.0. To install the latest version: ```bash theme={"system"} npm i @privy-io/react-auth@3 ``` ## New features and improvements 🎉 * Simplified Solana integration with one wallet per account and direct method access * Streamlined peer dependencies required for Solana * Removal of deprecated fields and methods For the full set of changes check out our [changelog](/changelogs/react-auth). ## Solana Updates ### Update Peer Dependencies If your app uses Privy's Solana wallets, the required peer dependencies have changed in v3.0: **Remove these peer dependencies:** * `@solana/web3.js` * `@solana/spl-token` **Install these new peer dependencies:** * `@solana/kit` * `@solana-program/memo` * `@solana-program/system` * `@solana-program/token` Additionally, if you are using webpack, include the following configurations to add them to webpack's `externals` config. Note that these configurations are not needed if you are using Turbopack: ```js theme={"system"} // webpack.config.js module.exports = { //... externals: { ['@solana/kit']: 'commonjs @solana/kit', ['@solana-program/memo']: 'commonjs @solana-program/memo', ['@solana-program/system']: 'commonjs @solana-program/system', ['@solana-program/token']: 'commonjs @solana-program/token' } }; // next.config.js module.exports = { webpack: (config) => { // ... config.externals['@solana/kit'] = 'commonjs @solana/kit'; config.externals['@solana-program/memo'] = 'commonjs @solana-program/memo'; config.externals['@solana-program/system'] = 'commonjs @solana-program/system'; config.externals['@solana-program/token'] = 'commonjs @solana-program/token'; return config; } }; ``` ### Solana RPC configuration * For Privy embedded wallet flows only (UI `signTransaction` and `signAndSendTransaction`), set RPCs in `config.solana.rpcs`. This replaces `solanaClusters`. ```tsx theme={"system"} import {createSolanaRpc, createSolanaRpcSubscriptions} from '@solana/kit'; // [!code ++] {/* your app's content */} ; ``` ### Replace `useSolanaWallets` * Replace `useSolanaWallets` with `useWallets`, `useCreateWallet`, and `useExportWallet` from the Solana entrypoint. The new `useWallets` hook returns `ConnectedStandardSolanaWallet[]`. ```tsx theme={"system"} import {useSolanaWallets} from '@privy-io/react-auth/solana'; // [!code --] import {useWallets, useCreateWallet, useExportWallet} from '@privy-io/react-auth/solana'; // [!code ++] const {ready, wallets, createWallet, exportWallet} = useSolanaWallets(); // [!code --] const {ready, wallets} = useWallets(); // [!code ++] const {createWallet} = useCreateWallet(); // [!code ++] const {exportWallet} = useExportWallet(); // [!code ++] ``` Key differences between `ConnectedSolanaWallet` and `ConnectedStandardSolanaWallet`: * Each `wallet` represents a single connected account * Methods are available directly on the wallet instance: * `wallet.signMessage({message})` * `wallet.signTransaction({transaction, chain})` * `wallet.signAndSendTransaction({transaction, chain})` * `wallet.signAndSendAllTransaction({transaction, chain}[])` * `wallet.disconnect()` * The [Solana standard wallet](https://docs.phantom.com/developer-powertools/wallet-standard) is available at `wallet.standardWallet` (for icon/name/etc.) * **Removed `wallet.loginOrLink()` method** - Use `useLoginWithSiws` and `useLinkWithSiws` instead: ```tsx theme={"system"} import {useLoginWithSiws, useLinkWithSiws} from '@privy-io/react-auth'; // [!code ++] const {generateSiwsMessage, loginWithSiws} = useLoginWithSiws(); // [!code ++] const {generateSiwsMessage, linkWithSiws} = useLinkWithSiws(); // [!code ++] // Login flow await wallets[0].loginOrLink(); // [!code --] const message = await generateSiwsMessage({address: wallets[0].address}); // [!code ++] const encodedMessage = new TextEncoder().encode(message); // [!code ++] const results = await wallets[0].signMessage({message: encodedMessage}); // [!code ++] await loginWithSiws({message: encodedMessage, signature: results.signature}); // [!code ++] // Link flow (similar pattern with linkWithSiws) const results = await wallets[0].signMessage({message: encodedMessage}); // [!code ++] await linkWithSiws({message: encodedMessage, signature: results.signature}); // [!code ++] ``` ### Rename `useSendTransaction` * Update `useSendTransaction` from `@privy-io/react-auth/solana` to `useSignAndSendTransaction` from `@privy-io/react-auth/solana` ```tsx theme={"system"} import {useSendTransaction} from '@privy-io/react-auth/solana'; // [!code --] import {useSignAndSendTransaction} from '@privy-io/react-auth/solana'; // [!code ++] ... const {sendTransaction} = useSendTransaction(); // [!code --] const {signAndSendTransaction} = useSignAndSendTransaction(); // [!code ++] ``` ### Usage Examples * All Solana RPCs now expect buffer inputs. #### New solana wallet usage ```tsx theme={"system"} import {useWallets, type ConnectedStandardSolanaWallet} from '@privy-io/react-auth/solana'; import {TextEncoder} from '@solana/kit'; export function SolanaWallets() { const {ready, wallets} = useWallets(); if (!ready) return

Loading...

; return (
{wallets.map((wallet: ConnectedStandardSolanaWallet) => (
{wallet.standardWallet.name} {wallet.address}
))}
); } ``` #### Sign and send via hooks (with optional UI configuration) ```tsx theme={"system"} import { useWallets, useSignMessage, useSignTransaction, useSignAndSendTransaction } from '@privy-io/react-auth/solana'; export function Actions() { const {wallets} = useWallets(); const {signMessage} = useSignMessage(); const {signTransaction} = useSignTransaction(); const {signAndSendTransaction} = useSignAndSendTransaction(); const wallet = wallets[0]; if (!wallet) return null; return (
); } ``` ## Other interface changes ### Funding * **Updated `fundWallet` interface** ```tsx theme={"system"} import {useFundWallet: useFundSolanaWallet} from '@privy-io/react-auth/solana'; import {useFundWallet: useFundEthereumWallet} from '@privy-io/react-auth'; ... const {fundWallet} = useFundSolanaWallet(); await fundWallet('', {amount: '1', asset: 'native-currency', chain: 'solana:devnet'}); // [!code --] await fundWallet({ // [!code ++] address: '', // [!code ++] options: {amount: '1', asset: 'SOL', chain: 'solana:devnet'} // [!code ++] }); // [!code ++] const {fundWallet} = useFundEthereumWallet(); await fundWallet('', {amount: '1000', asset: 'native-currency', chain: {id: 1}}); // [!code --] await fundWallet({ // [!code ++] address: '', // [!code ++] options: {amount: '1000', asset: 'native-currency', chain: {id: 1}} // [!code ++] }); // [!code ++] ``` ## Removed/Deprecated Items * **Removed `suggestedAddress` from `connectWallet` and `linkWallet`** ```tsx theme={"system"} connectWallet({suggestedAddress: '0x123...'}); // [!code --] connectWallet({description: `Connect the wallet with address ${address}`}); // [!code ++] ``` * **Removed `detected_wallets` from wallet lists/configuration** ```tsx theme={"system"} {/* your app's content */} ``` * **Removed deprecated Moonpay config and types, add config to `PrivyProviderConfig` instead** ```tsx theme={"system"} fundEvmWallet(address, { config: { // [!code --] currencyCode: 'ETH_ETHEREUM', // [!code --] quoteCurrencyAmount: 0.01 // [!code --] // [!code --] }, // [!code --] provider: 'moonpay' // [!code --] }); ... ; fundEvmWallet(address, { chain: mainnet, // [!code ++] amount: '0.01', // [!code ++] defaultFundingMethod: 'card' // [!code ++] }); ``` * **Removed deprecated `requireUserPasswordOnCreate` and related embedded wallet config fields** * \*\*Removed `embeddedWallets` level `createOnLogin` field. Use `embeddedWallets.etherum.createOnLogin` or `embeddedWallets.solana.createOnLogin` instead. \*\* ```tsx theme={"system"} {/* your app's content */} ``` * **Removed `useLoginToFrame` and replaced with `useLoginToMiniApp`** ```tsx theme={"system"} export {useLoginToFrame} from '@privy-io/react-auth'; // [!code --] export {useLoginToMiniApp} from '@privy-io/react-auth'; // [!code ++] ``` * **Removed `useSignAuthorization()` - Use `useSign7702Authorization()` instead** ```tsx theme={"system"} import {useSignAuthorization} from '@privy-io/react-auth'; // [!code --] import {useSign7702Authorization} from '@privy-io/react-auth'; // [!code ++] const {signAuthorization} = useSignAuthorization(); // [!code --] const {sign7702Authorization} = useSign7702Authorization(); // [!code ++] ``` * **Removed `useSetWalletPassword()` - Use `useSetWalletRecovery` instead** ```tsx theme={"system"} import {useSetWalletPassword} from '@privy-io/react-auth'; // [!code --] import {useSetWalletRecovery} from '@privy-io/react-auth'; // [!code ++] const {setWalletPassword} = useSetWalletPassword(); // [!code --] const {setWalletRecovery} = useSetWalletRecovery(); // [!code ++] ``` ### Updated Types * **Removed `verifiedAt` from `LinkMetadata` and all linked accounts. Use `firstVerifiedAt` and `latestVerifiedAt` instead of the deprecated `verifiedAt`.** ```tsx theme={"system"} const verifiedDate = user.wallet.verifiedAt; // [!code --] const verifiedDate = user.wallet.firstVerifiedA; // [!code ++] ``` # Features Source: https://docs.privy.io/basics/react/features Learn about the authentication, wallet, and UI features supported by the Privy React SDK. ## Supported features # Installation Source: https://docs.privy.io/basics/react/installation Install the Privy React SDK (@privy-io/react-auth) and optional Solana peer dependencies (@solana/kit) for React and Next.js apps ## Requirements * React 18 or higher * TypeScript 5 or higher ## Installation Install the Privy React SDK with a supported package manager: ```bash npm theme={"system"} npm install @privy-io/react-auth@latest ``` ```bash pnpm theme={"system"} pnpm install @privy-io/react-auth@latest ``` ```bash yarn theme={"system"} yarn add @privy-io/react-auth@latest ``` If your app uses Privy's Solana wallets, install the following peer dependencies: * `@solana/kit` * `@solana-program/memo` * `@solana-program/system` * `@solana-program/token` For webpack, add these packages to the `externals` config. Skip this step for Turbopack: ```js theme={"system"} // webpack.config.js module.exports = { //... externals: { ['@solana/kit']: 'commonjs @solana/kit', ['@solana-program/memo']: 'commonjs @solana-program/memo', ['@solana-program/system']: 'commonjs @solana-program/system', ['@solana-program/token']: 'commonjs @solana-program/token' } }; // next.config.js module.exports = { webpack: (config) => { // ... config.externals['@solana/kit'] = 'commonjs @solana/kit'; config.externals['@solana-program/memo'] = 'commonjs @solana-program/memo'; config.externals['@solana-program/system'] = 'commonjs @solana-program/system'; config.externals['@solana-program/token'] = 'commonjs @solana-program/token'; return config; } }; ``` If build errors remain, see the [Vite troubleshooting guide](/basics/troubleshooting/react-frameworks). One example is `getTransferSolInstruction` export errors. Vite resolves string-literal dynamic imports during dependency optimization, so optional or environment-specific packages must be installed for builds to pass. These packages are not included in the main runtime bundle; they are loaded only when the dynamic import path is used. # Quickstart Source: https://docs.privy.io/basics/react/quickstart Learn how to authenticate users, create embedded wallets, and send transactions in your React or Next.js app with Privy. ## 0. Prerequisites This guide assumes that you have completed the [Setup](/basics/react/setup) guide. ## 1. Enable a user to log in via email This quickstart guide will demonstrate how to authenticate a user with a one time password as an example, but Privy supports many authentication methods. Explore our [Authentication docs](/authentication/overview) to learn about other methods such as socials, passkeys, and external wallets to authenticate users in your app. **To authenticate a user via their email address, use the React SDK's `useLoginWithEmail` hook.** ```tsx theme={"system"} import {useLoginWithEmail} from '@privy-io/react-auth'; ... const {sendCode, loginWithCode} = useLoginWithEmail(); ``` Ensure that this hook is mounted in a component that is wrapped by the [PrivyProvider](/basics/react/setup#initializing-privy). You can use the returned methods **`sendCode`** and **`loginWithCode`** to authenticate your user per the instructions below. ### Send an OTP Send a one-time passcode (OTP) to the user's **email** by passing their email address to the **`sendCode`** method returned from `useLoginWithEmail`: ```tsx theme={"system"} import {useState} from 'react'; import {useLoginWithEmail} from '@privy-io/react-auth'; export default function LoginWithEmail() { const [email, setEmail] = useState(''); const [code, setCode] = useState(''); const {sendCode, loginWithCode} = useLoginWithEmail(); return (
setEmail(e.currentTarget.value)} value={email} /> setCode(e.currentTarget.value)} value={code} />
); } ``` ## 2. Create an embedded wallet for the user Your app can configure Privy to [**automatically** create wallets](/basics/react/advanced/automatic-wallet-creation) for your users as part of their **login** flow. The embedded wallet will be generated and linked to the user object upon authentication. Alternatively your app can [**manually** create wallets](/wallets/wallets/create/create-a-wallet) for users when required. Privy can provision wallets for your users on both **Ethereum** and **Solana**. ## 3. Send a transaction with the embedded wallet With the users' embedded wallet, your application can now prompt the user to sign and send transactions. ```tsx theme={"system"} import {useSendTransaction} from '@privy-io/react-auth'; export default function SendTransactionButton() { const {sendTransaction} = useSendTransaction(); const onSendTransaction = async () => { sendTransaction({ to: '0xE3070d3e4309afA3bC9a6b057685743CF42da77C', value: 100000 }); }; return ; } ``` [Learn more](/wallets/using-wallets/ethereum/send-a-transaction) about sending transactions with the embedded wallet. Privy enables you to take many actions on the embedded wallet, including [sign a message](/wallets/using-wallets/ethereum/sign-a-message), [sign typed data](/wallets/using-wallets/ethereum/sign-typed-data), and [sign a transaction](/wallets/using-wallets/ethereum/sign-a-transaction). With the users' embedded wallet, your application can now prompt the user to sign and send transactions. ```tsx theme={"system"} import {useSendTransaction} from '@privy-io/react-auth/solana'; import {Connection, Transaction, VersionedTransaction, SystemProgram, LAMPORTS_PER_SOL} from '@solana/web3.js'; export default function SendTransactionButton() { const {sendTransaction} = useSendTransaction(); const connection = new Connection('https://api.mainnet-beta.solana.com'); // Create a new transaction const transaction = new Transaction().add( SystemProgram.transfer({ fromPubkey: wallet.publicKey, toPubkey: new PublicKey('RECIPIENT_ADDRESS_HERE'), lamports: 0.1 * LAMPORTS_PER_SOL }) ); const onSendTransaction = async () => { sendTransaction({ transaction, connection }); } return ; } ``` [Learn more](/wallets/using-wallets/solana/send-a-transaction) about sending transactions with the embedded wallet. Privy enables you to take many actions on the embedded wallet, including [send a transaction](/wallets/using-wallets/solana/send-a-transaction), [sign a message](/wallets/using-wallets/solana/sign-a-message), and [sign a transaction](/wallets/using-wallets/solana/sign-a-transaction). Congratulations, you have successfully been able to integrate Privy authentication and wallet into your React application! # Setup Source: https://docs.privy.io/basics/react/setup Configure the PrivyProvider component to wrap your React or Next.js app with your appId and SDK config ## Prerequisites Before you begin, make sure you have [set up your Privy app and obtained your app ID](/basics/get-started/dashboard/create-new-app) from the Privy Dashboard. Deploying your app across multiple domains or environments? Learn how to use [app clients](/basics/get-started/dashboard/app-clients) to customize Privy's behavior for different environments. ## Initializing Privy In your project, **import the `PrivyProvider` component and wrap your app with it**. The `PrivyProvider` must wrap *any* component or page that will use the Privy React SDK, and it is generally recommended to render it as close to the root of your application as possible. If you're new to React and using contexts, check out [these](https://react.dev/learn/thinking-in-react) [resources](https://react.dev/learn/passing-data-deeply-with-context)! ```tsx NextJS theme={"system"} 'use client'; import {PrivyProvider} from '@privy-io/react-auth'; export default function Providers({children}: {children: React.ReactNode}) { return ( {children} ); } ``` ```tsx Create React App theme={"system"} import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import {PrivyProvider} from '@privy-io/react-auth'; import App from './App'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( ); ``` ```tsx NextJS theme={"system"} 'use client'; import {PrivyProvider} from '@privy-io/react-auth'; export default function Providers({children}: {children: React.ReactNode}) { return ( {children} ); } ``` ```tsx Create React App theme={"system"} import React from 'react'; import ReactDOM from 'react-dom/client'; import './index.css'; import {PrivyProvider} from '@privy-io/react-auth'; import App from './App'; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( ); ``` To use external Solana wallets, you must pass `toSolanaWalletConnectors()` to the `externalWallets` prop in your `PrivyProvider` config. Learn more [here](/wallets/connectors/setup/configuring-external-connector-chains). ## Configuration The `PrivyProvider` component accepts the following props: Your Privy App ID. You can find this in the Privy Dashboard. (Optional) A client ID to be used for this app client. Learn more about app clients [here](/basics/get-started/dashboard/app-clients). Configuration options for the Privy SDK. For more information on the `config` object, look under **React > Advanced** for guides like [customizing appearance](/basics/react/advanced/configuring-appearance) for our UI components and [configuring networks](/basics/react/advanced/configuring-evm-networks). ## Waiting for Privy to be ready When the `PrivyProvider` is first rendered on your page, the Privy SDK will initialize some state about the current user. This might include checking if the user has a wallet connected, refreshing expired auth tokens, fetching up-to-date user data, and more. **It's important to wait until the `PrivyProvider` has finished initializing *before* you consume Privy's state and interfaces**, to ensure that the state you consume is accurate and not stale. To determine whether the Privy SDK has fully initialized on your page, **check the `ready` Boolean returned by the `usePrivy` hook.** When `ready` is true, Privy has completed initialization, and your app can consume Privy's state and interfaces. ```tsx theme={"system"} import {usePrivy} from '@privy-io/react-auth'; function YourComponent() { const {ready} = usePrivy(); if (!ready) { return
Loading...
; } // Now it's safe to use other Privy hooks and state return
Privy is ready!
; } ``` **Using wallets?** Use the [ready](/wallets/wallets/get-a-wallet/get-connected-wallet) indicator from the `useWallets` hook to wait for wallets to complete loading. Learn how to log users in and transact with embedded wallets Check out the NextJS app starter repo for a complete example integration Check out the React app starter repo for a complete example integration Check out the whitelabel starter for a complete whitelabel example integration # Quickstart Source: https://docs.privy.io/basics/rest-api/quickstart Create a wallet and send a transaction using Privy's REST API without an SDK. ## 0. Prerequisites API credentials are required for this guide. If you have not already gone through the [API setup guide](/basics/rest-api/setup), go through those steps now. ## 1. Create a wallet Let's create a simple Ethereum wallet: ```bash cURL theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "chain_type": "ethereum" }' ``` [Authorization signatures](/api-reference/authorization-signatures) are an optional security improvement that requires all requests to be authorized by you. The response will include the wallet ID and public address: ```json theme={"system"} { "id": "jf4mev19seymsqulciv8on0c", "address": "0x7Ef5363308127128969618240eDcB9F8f61e90F6", "chain_type": "ethereum", "policy_ids": [], "created_at": 1741362961254 } ``` ## 1b. User wallets If you want the wallet to be owned by a user, you can first create a user and then create a wallet for that user, or create the user and wallet in the same API call as shown below. ```bash cURL theme={"system"} curl --request POST \ --url https://api.privy.io/v1/users \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "linked_accounts": [ {"type": "email", "address": "batman@privy.io"} ], "wallets": [ {"chain_type": "ethereum"} ] }' ``` If your server initiates actions on user wallets, you may need to sign requests with an authorization key and include the `privy-authorization-signature` header. See [authorization signatures](/api-reference/authorization-signatures) and [using authorization keys](/controls/authorization-keys/using-owners/sign). To prevent duplicate operations in case of retries, include an [idempotency key](/api-reference/idempotency-keys). ## 2. Sign a message Now let's sign a message with our new wallet: ```bash cURL theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "personal_sign", "params": { "message": "Hello from Privy!", "encoding": "utf-8" } }' ``` The response will contain the signed message: ```json theme={"system"} { "method": "personal_sign", "data": { "signature": "0x292d67e9c5178447f1c5344b3122997dfba8f00e43102d0b746301e9b4afbbf67d952bf870878d92b8eb066da205840458c0a5fb3f53253dbe1adf9c143678311c", "encoding": "hex" } } ``` ## 3. Send a transaction Finally, let's send a transaction on Ethereum's testnet, [Sepolia](https://sepolia.etherscan.io/): ```bash cURL theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "eth_sendTransaction", "caip2": "eip155:11155111", "chain_type": "ethereum", "params": { "transaction": { "to": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "value": 1000000000000000 } } }' ``` You will need to fund your wallet with Sepolia ETH for this step. Use a [Sepolia faucet](https://cloud.google.com/application/web3/faucet/ethereum/sepolia) and send it to the public address. The response will contain the transaction hash and a Privy transaction ID: ```json theme={"system"} { "method": "eth_sendTransaction", "data": { "hash": "0x7c91ba85d67ef92cc15f3e9c8d8c5788e982cf83fabe9bfcc66a747aa0bd3701", "caip2": "eip155:11155111", "transaction_id": "d2obiyxnblv7jzp73b8scqa8" } } ``` ## Next steps Now that you've created a wallet and made your first transaction, you can explore: 1. Creating [policies](/controls/policies/overview) to control wallet spending and contract interaction 2. Setting up [webhooks](/wallets/gas-and-asset-management/assets/transaction-event-webhooks) for real-time transaction notifications 3. Using [idempotency keys](/api-reference/idempotency-keys) to prevent duplicate transactions 4. Setting up [quorum authorizations](/controls/quorum-approvals/overview#using-quorum-approvals) for sensitive wallets # Setup Source: https://docs.privy.io/basics/rest-api/setup Configure authentication and credentials for direct REST API requests without a Privy SDK. ## Prerequisites Before you begin, make sure you have [set up your Privy app and obtained your app ID](/basics/get-started/dashboard/create-new-app) from the Privy Dashboard. ## Base URL All requests to the Privy API must be made to the following base URL: ``` https://api.privy.io ``` HTTPS is required for all requests. HTTP requests will be rejected. ## Authentication All API endpoints require authentication using Basic Auth and a Privy App ID header. Include the following headers with every request: Basic Auth header with your app ID as the username and your app secret as the password. Your Privy app ID as a string. Requests missing either of these headers will be rejected by Privy's middleware. Your Privy app ID and app secret can be found in the [**App settings** > **Basics**](https://dashboard.privy.io/apps?page=settings\&tab=basics) tab for your app. ## Examples ```javascript theme={"system"} fetch('https://api.privy.io/v1/wallets', { method: 'GET', headers: { 'Authorization': `Basic ${btoa('insert-your-app-id' + ':' + 'insert-your-app-secret')}`, 'privy-app-id': 'insert-your-app-id', 'Content-Type': 'application/json' } }) .then(response => response.json()) .then(data => console.log(data)); ``` ```bash theme={"system"} curl -X GET "https://api.privy.io/v1/wallets" \ --user "insert-your-app-id:insert-your-app-secret" \ -H "privy-app-id: insert-your-app-id" \ -H "Content-Type: application/json" ``` # Installation Source: https://docs.privy.io/basics/ruby/installation Install the Privy Ruby server SDK (privy_ruby) for backend authentication, wallet management, and token verification In a backend Ruby environment, you can use the [**`privy_ruby`**](https://github.com/privy-io/ruby-sdk) gem to authorize requests and manage your application from your server. This library includes helpful utilities around verifying access tokens issued by Privy and interacting with Privy's API to query and create users, create wallets, send assets, and more. Add the Privy Ruby SDK to your application's `Gemfile`: ```ruby Gemfile theme={"system"} gem "privy_ruby", github: "privy-io/ruby-sdk", branch: "main" ``` Then install it via Bundler: ```bash theme={"system"} bundle install ``` The Privy Ruby SDK requires Ruby 3.2.0 or later. ## Next steps Configure the Privy client with app credentials and authorization context. Create wallets, sign messages, and send transactions with the Ruby SDK. # Quickstart Source: https://docs.privy.io/basics/ruby/quickstart Learn how to create users, embedded wallets, and send transactions in a Ruby app using the Privy Ruby SDK. ## 0. Prerequisites This guide assumes the [setup](/basics/ruby/setup) guide is complete and a Privy client instance is available. ## 1. Creating a wallet First, create a wallet. The wallet's `id` is used in future calls to sign messages and send transactions. ```ruby theme={"system"} begin wallet = client.wallets.create( wallet_create_params: {chain_type: :ethereum} ) wallet_id = wallet.id rescue Privy::Errors::APIStatusError => e # Non-2xx HTTP status codes (e.g. 400, 401, 404, 429, 5xx) puts(e.status) puts(e.message) rescue Privy::Errors::APIConnectionError => e # Network-level errors raised by `net/http` puts(e.cause) end ``` ```ruby theme={"system"} begin wallet = client.wallets.create( wallet_create_params: {chain_type: :solana} ) wallet_id = wallet.id rescue Privy::Errors::APIStatusError => e puts(e.status) puts(e.message) rescue Privy::Errors::APIConnectionError => e puts(e.cause) end ``` [Learn more](/wallets/wallets/create/create-a-wallet) about creating wallets. When using the `PrivyClient` to interact with the API, all errors raised inherit from `Privy::Errors::APIError`. Catch `Privy::Errors::APIStatusError` for non-2xx responses (with `status` and `message` available) or more specific subclasses such as `RateLimitError`, `BadRequestError`, or `NotFoundError`. ### User wallets Create a non-custodial user wallet by first creating a user, then provisioning a wallet for that user. ```ruby theme={"system"} begin user = client.users.create( user_create_params: { linked_accounts: [{type: :email, address: "batman@privy.io"}] } ) wallet = client.wallets.create( wallet_create_params: { chain_type: :ethereum, owner: {user_id: user.id} } ) rescue Privy::Errors::APIError => e puts(e.message) end ``` ```ruby theme={"system"} begin user = client.users.create( user_create_params: { linked_accounts: [{type: :email, address: "batman@privy.io"}] } ) wallet = client.wallets.create( wallet_create_params: { chain_type: :solana, owner: {user_id: user.id} } ) rescue Privy::Errors::APIError => e puts(e.message) end ``` When creating a user wallet, specify the user ID as the `owner` of the wallet. Obtain a user ID by first [creating a user](/user-management/migrating-users-to-privy/create-or-import-a-user) before creating the wallet. Alternatively, [create a user and wallet at the same time](/user-management/migrating-users-to-privy/create-or-import-a-user) by passing `wallets:` to `users.create`. ## 2. Signing a message Next, sign a plaintext message with the wallet using the `rpc` method on the wallets service. Use `personal_sign` for Ethereum and `signMessage` for Solana, and specify the wallet ID (not address) from creation. ```ruby theme={"system"} response = client.wallets.rpc( wallet_id, wallet_rpc_request_body: { method: "personal_sign", chain_type: "ethereum", params: {message: "Hello, Privy!", encoding: "utf-8"} } ) # Signature is hex-encoded for Ethereum signature = response.data.signature ``` ```ruby theme={"system"} require "base64" message = "Hello, Privy!" # Solana requires the message to be base64 encoded base64_message = Base64.strict_encode64(message) response = client.wallets.rpc( wallet_id, wallet_rpc_request_body: { method: "signMessage", chain_type: "solana", params: {message: base64_message, encoding: "base64"} } ) # Signature is base64-encoded for Solana signature = response.data.signature ``` [Learn more](/wallets/using-wallets/ethereum/sign-a-message) about signing messages. ## 3. Sending transactions The wallet must have funds to send a transaction. Use a testnet [faucet](https://console.optimism.io/faucet) to test transacting on a testnet (e.g. Base Sepolia) or send funds to the wallet on the network of choice. To send a transaction from a wallet, call `wallets.rpc` with `eth_sendTransaction` for Ethereum or `signAndSendTransaction` for Solana. The SDK populates missing network-related values, signs the transaction, broadcasts it to the network, and returns the transaction hash. In the request, specify the wallet `id` from wallet creation above, as well as the `caip2` chain ID for the target network. ```ruby theme={"system"} caip2 = "eip155:11155111" # Sepolia testnet response = client.wallets.rpc( wallet_id, wallet_rpc_request_body: { method: "eth_sendTransaction", chain_type: "ethereum", caip2: caip2, params: { transaction: { to: recipient_address, value: "0x1", # 1 wei chain_id: 11_155_111 # Sepolia testnet } } } ) transaction_hash = response.data.hash ``` ```ruby theme={"system"} caip2 = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" # Solana Mainnet # A base64 encoded serialized transaction to sign transaction = "insert-base-64-encoded-serialized-transaction" response = client.wallets.rpc( wallet_id, wallet_rpc_request_body: { method: "signAndSendTransaction", chain_type: "solana", caip2: caip2, params: {transaction: transaction, encoding: "base64"} } ) transaction_hash = response.data.hash ``` [Learn more](/wallets/using-wallets/ethereum/send-a-transaction) about sending transactions. For more control, prepare and broadcast the transaction independently, and use raw signing methods (`eth_signTransaction` for [EVM](/wallets/using-wallets/ethereum/sign-a-transaction) and `signTransaction` for [Solana](/wallets/using-wallets/solana/sign-a-transaction)) to sign the transaction with a wallet. ## 4. Creating a user To create a user for an application, use the `create` method on the users service. Pass in linked accounts, custom metadata, and wallets to associate with the user. ```ruby theme={"system"} user = client.users.create( user_create_params: { linked_accounts: [ {type: :custom_auth, custom_user_id: "your-subject-id"}, {type: :email, address: "user@example.com"} ] } ) user_id = user.id ``` [Learn more](/user-management/migrating-users-to-privy/create-or-import-a-user) about creating users, and see the [pregenerating wallets](/recipes/pregenerate-wallets) guide for linking wallets to users before they sign in. ## Next steps Add an extra layer of security by signing requests with authorization keys. Restrict what wallets can do with configurable policies. Prevent duplicate transactions with idempotency key support. Require multiple parties to approve before sending a transaction. # Setup Source: https://docs.privy.io/basics/ruby/setup Configure the Privy Ruby SDK client with app credentials and authorization context for backend operations. ## Prerequisites Before getting started: * Obtain a [Privy app ID and app secret](/basics/get-started/dashboard/create-new-app) from the Privy Dashboard * Install Ruby 3.2.0 or later ## Instantiating the `PrivyClient` Require the `privy` gem and create a new client instance by passing the Privy **app ID** and **app secret** as parameters. ```ruby theme={"system"} require "privy" client = Privy::PrivyClient.new( app_id: "your-privy-app-id", app_secret: "your-app-secret" ) ``` This `client` is the entry point for managing Privy resources from a server. The `PrivyClient` provides services for creating wallets, signing and sending transactions, retrieving user objects, verifying auth tokens, and managing policies and key quorums. The `app_id` and `app_secret` arguments default to the `PRIVY_APP_ID` and `PRIVY_APP_SECRET` environment variables, so they can be omitted if those variables are set. ## Authorization If a resource (i.e. wallet, policy, or key quorum) has an [owner](/controls/authorization-keys/using-owners/overview), [authorization signatures](/api-reference/authorization-signatures) from the owner are required. The [authorization context](/controls/authorization-keys/using-owners/sign/signing-on-the-server) accepts authorization private keys and user JWTs of the wallet's owners. The Ruby SDK generates signatures and signs requests automatically. Review the [signing on the server](/controls/authorization-keys/using-owners/sign/signing-on-the-server) guide before using the Ruby SDK for the best development experience. ```ruby theme={"system"} ctx = Privy::Authorization::AuthorizationContext.build( authorization_private_keys: ["privateKey1", "privateKey2"], user_jwts: ["jwt1", "jwt2"] ) ``` ## Rate limits Privy rate limits REST API endpoints called from a server. Learn more about optimizing request patterns and handling rate limits in the [optimizing](/recipes/dashboard/optimizing) guide. ## Next steps Create wallets, sign messages, and send transactions with the Ruby SDK. Add an extra layer of security by signing requests with authorization keys. # Installation Source: https://docs.privy.io/basics/rust/installation Install the Privy Rust server SDK for backend wallet management, user authentication, and embedded wallet operations. In a backend Rust environment, you can use the [**`privy-rs`**](https://crates.io/crates/privy-rs) crate to authorize requests and manage your application from your server. This library includes helpful utilities around verifying access tokens issued by Privy and interacting with Privy's API to query and import users, create wallets, manage invite lists, and more. Add the Privy Rust SDK to your `Cargo.toml`: ```toml Cargo.toml theme={"system"} [dependencies] privy-rs = "X.Y.Z" ``` You can always get the latest version from [crates.io](https://crates.io/crates/privy-rs) or, alternatively, add it directly using cargo. ```bash theme={"system"} cargo add privy-rs ``` The Privy Rust SDK requires Rust 1.88 or later and uses `tokio` / `reqwest` as its async runtime and HTTP client. # Quickstart Source: https://docs.privy.io/basics/rust/quickstart Learn how to create users, embedded wallets, and send transactions in your Rust backend app with Privy. ## 0. Prerequisites This guide assumes that you have completed the [Setup](/basics/rust/setup) guide to get a Privy client instance. ## 1. Creating a wallet First, we will create a wallet. You will use this wallet's `id` in future calls to sign messages and send transactions. ```rust theme={"system"} use privy_rs::{PrivyClient, generated::types::{CreateWalletBody, WalletChainType}}; let wallet = client .wallets() .create( None, // idempotency_key (optional) &CreateWalletBody { chain_type: WalletChainType::Ethereum, additional_signers: None, owner: None, owner_id: None, policy_ids: vec![], }, ) .await?; let wallet_id = wallet.id; ``` ```rust theme={"system"} use privy_rs::{PrivyClient, generated::types::{CreateWalletBody, WalletChainType}}; let wallet = client .wallets() .create( None, // idempotency_key (optional) &CreateWalletBody { chain_type: WalletChainType::Solana, additional_signers: None, owner: None, owner_id: None, policy_ids: vec![], }, ) .await?; let wallet_id = wallet.id; ``` [Learn more](/wallets/wallets/create/create-a-wallet) about creating wallets. When using the `PrivyClient` to work with the API, all errors thrown will be instances of `privy_rs::Error`. This error type implements `std::error::Error`, so you can use it with `?` to propagate errors or your favorite error handling library such as `anyhow`. You can see more examples in the [error handling](#5-error-handling) section below. ## 2. Signing a message Next, we'll sign a plaintext message with the wallet using chain-specific signing methods. Make sure to specify your wallet ID from creation in the input. ```rust theme={"system"} use privy_rs::AuthorizationContext; // Create empty authorization context for unowned wallet let ctx = AuthorizationContext::new(); let message = "Hello, Privy!"; let response = client .wallets() .ethereum() .sign_message(&wallet_id, message, &ctx, None) .await?; // Signature is hex-encoded for Ethereum let signature = response.signature; ``` ```rust theme={"system"} use privy_rs::AuthorizationContext; use base64::{Engine as _, engine::general_purpose}; // Create empty authorization context for unowned wallet let ctx = AuthorizationContext::new(); let message = "Hello, Privy!"; // Solana requires the message to be base64 encoded let base64_message = general_purpose::STANDARD.encode(message.as_bytes()); let response = client .wallets() .solana() .sign_message(&wallet_id, &base64_message, &ctx, None) .await?; // Signature is base64-encoded for Solana let signature = response.signature; ``` [Learn more](/wallets/using-wallets/ethereum/sign-a-message) about signing messages. ## 3. Sending transactions Your wallet must have some funds in order to send a transaction. You can use a testnet [faucet](https://console.optimism.io/faucet) to test transacting on a testnet (e.g. Base Sepolia) or send funds to the wallet on the network of your choice. To send a transaction from your wallet, use chain-specific transaction methods. The SDK will populate missing network-related values, sign your transaction, broadcast it to the network, and return the transaction hash to you. In the request, make sure to specify your wallet `id` from your wallet creation above, as well as the `caip2` chain ID for the network you want to transact on. ```rust theme={"system"} use privy_rs::{AuthorizationContext, generated::types::*}; // Create empty authorization context for unowned wallet let ctx = AuthorizationContext::new(); let caip2 = "eip155:11155111"; // Sepolia testnet let recipient_address = "0x742d35Cc6635C0532925a3b8c17d6d1E9C2F7ca"; // Your recipient address let transaction = EthereumSendTransactionRpcInputParamsTransaction { to: Some(recipient_address.to_string()), value: Some("0x1".to_string()), // 1 wei gas_limit: None, max_fee_per_gas: None, max_priority_fee_per_gas: None, data: Some("0x".to_string()), chain_id: Some(11155111), // Sepolia testnet from: None, gas_price: None, nonce: None, type_: None, }; let response = client .wallets() .ethereum() .send_transaction(&wallet_id, caip2, transaction, &ctx, None) .await?; let transaction_hash = response.hash; ``` ```rust theme={"system"} use privy_rs::AuthorizationContext; // Create empty authorization context for unowned wallet let ctx = AuthorizationContext::new(); let caip2 = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"; // Solana Mainnet // A base64 encoded serialized transaction to sign let transaction = "insert-base-64-encoded-serialized-transaction"; let response = client .wallets() .solana() .sign_and_send_transaction(&wallet_id, caip2, transaction, &ctx, None) .await?; let transaction_hash = response.hash; ``` [Learn more](/wallets/using-wallets/ethereum/send-a-transaction) about sending transactions. If you're interested in more control, you can prepare and broadcast the transaction yourself, and simply use raw signing methods to sign the transaction with a wallet. ## 4. Creating a user To create a user for your application, you can use the `create` method, passing in linked accounts, custom metadata, and wallets that should be associated with said user. ```rust theme={"system"} use privy_rs::generated::types::{ CreateUserBody, LinkedAccountEmailInput, LinkedAccountEmailInputType, LinkedAccountInput, LinkedAccountCustomAuthInput, LinkedAccountCustomAuthInputType, }; let user = client .users() .create(&CreateUserBody { linked_accounts: vec![ LinkedAccountInput::CustomAuthInput(LinkedAccountCustomAuthInput { custom_user_id: "your-subject-id".to_string(), type_: LinkedAccountCustomAuthInputType::CustomAuth, }), LinkedAccountInput::EmailInput(LinkedAccountEmailInput { address: "user@example.com".to_string(), type_: LinkedAccountEmailInputType::Email, }), ], create_embedded_wallet: Some(false), custom_metadata: None, }) .await?; let user_id = user.id; ``` [Learn more](/user-management/migrating-users-to-privy/create-or-import-a-user) about creating users, and look at our [pregenerating wallets](/recipes/pregenerate-wallets) guide for linking wallets to your users before they even sign in. ## 5. Error handling In the examples above we use `?` to propagate errors, however the Rust SDK provides comprehensive error handling through multiple specialized error types. All error types implement `std::error::Error`, allowing seamless integration with Rust's error handling ecosystem. ### Error types The SDK provides the following main error categories: #### PrivyApiError The core generated client error type that represents all possible API communication failures: | Error Variant | Description | When It Occurs | | ------------------------ | ------------------------------------------- | ------------------------------------------------- | | `InvalidRequest` | Request doesn't conform to API requirements | Missing required fields, invalid data formats | | `CommunicationError` | Network or connection issues | Network timeouts, DNS failures, connection drops | | `InvalidUpgrade` | Connection upgrade failed | WebSocket or HTTP/2 upgrade errors | | `ErrorResponse` | Documented API error response | Authentication failures, insufficient permissions | | `ResponseBodyError` | Failed to read response body | Corrupted response data | | `InvalidResponsePayload` | Response deserialization failed | Unexpected response format | | `UnexpectedResponse` | Undocumented response code | New API responses not yet supported | | `Custom` | Consumer-defined hook error | Custom validation or processing failures | #### PrivyCreateError Client initialization errors that occur when creating a `PrivyClient` instance: | Error Variant | Description | When It Occurs | | -------------------- | ---------------------------------- | ---------------------------- | | `InvalidHeaderValue` | Invalid HTTP header value provided | Malformed app credentials | | `Client` | HTTP client creation failed | Network configuration issues | #### PrivySignedApiError Errors for operations requiring cryptographic signatures (authorization keys): | Error Variant | Description | When It Occurs | | --------------------- | ------------------------------------ | ------------------------------------ | | `Api` | Privy API returned an error response | Authentication failures, rate limits | | `SignatureGeneration` | Failed to generate request signature | Invalid private key, signing errors | #### PrivyExportError Wallet export operation errors: | Error Variant | Description | When It Occurs | | --------------------- | ------------------------------------ | -------------------------------------- | | `Api` | Privy API returned an error response | Export permissions, wallet access | | `SignatureGeneration` | Failed to generate request signature | Invalid authorization key | | `Key` | Decryption or key handling failed | Invalid encryption key, corrupted data | #### CryptoError General cryptographic operation errors: | Error Variant | Description | When It Occurs | | ------------- | ---------------------------------- | --------------------------------------- | | `Signing` | Digital signature operation failed | Invalid key, signature algorithm issues | | `Key` | Key handling operation failed | Key parsing, loading, or format errors | #### KeyError Cryptographic key management errors: | Error Variant | Description | When It Occurs | | ---------------- | -------------------------- | --------------------------------------- | | `Io` | File I/O error reading key | Missing key file, permission denied | | `InvalidFormat` | Key data is malformed | Invalid PEM/DER format, corrupted data | | `HpkeDecryption` | HPKE decryption failed | Wrong decryption key, corrupted payload | | `Other` | Unknown error occurred | Unexpected system errors | #### SigningError Digital signature creation errors: | Error Variant | Description | When It Occurs | | ------------- | ---------------------------- | ---------------------------------- | | `Key` | Invalid signing key | Wrong key type, corrupted key data | | `Signature` | Cryptographic signing failed | Algorithm errors, hardware issues | | `Other` | Unknown signing error | Unexpected cryptographic errors | #### SignatureGenerationError Authorization signature generation errors: | Error Variant | Description | When It Occurs | | --------------- | ---------------------------- | ---------------------------------- | | `Serialization` | Request serialization failed | Invalid request data structure | | `Signing` | Signing process failed | Key errors, cryptographic failures | ### Basic error handling ```rust theme={"system"} use privy_rs::{PrivyClient, PrivyCreateError, PrivyApiError, generated::types::*}; // Using the ? operator for error propagation async fn create_wallet_with_basic_handling() -> Result> { let client = PrivyClient::new("your-app-id", "your-app-secret")?; let wallet = client .wallets() .create( None, &CreateWalletBody { chain_type: WalletChainType::Ethereum, additional_signers: None, owner: None, owner_id: None, policy_ids: vec![], }, ) .await?; Ok(wallet.id) } ``` ### Advanced error handling ```rust theme={"system"} use privy_rs::{PrivyClient, PrivyCreateError, PrivySignedApiError, generated::types::*}; async fn create_wallet_with_detailed_handling() -> Result { // Handle client creation errors let client = match PrivyClient::new("your-app-id", "your-app-secret") { Ok(client) => client, Err(PrivyCreateError::InvalidHeaderValue(e)) => { return Err(format!("Invalid credentials format: {}", e)); } Err(PrivyCreateError::Client(e)) => { return Err(format!("HTTP client creation failed: {}", e)); } }; // Handle wallet creation errors match client .wallets() .create( None, &CreateWalletBody { chain_type: WalletChainType::Ethereum, additional_signers: None, owner: None, owner_id: None, policy_ids: vec![], }, ) .await { Ok(wallet) => Ok(wallet.id), Err(e) => { // e is of type PrivyApiError here match e { PrivyApiError::InvalidRequest(msg) => { Err(format!("Invalid request: {}", msg)) } PrivyApiError::CommunicationError(req_err) => { Err(format!("Network error: {}", req_err)) } PrivyApiError::ErrorResponse(response) => { Err(format!("API error response: {:?}", response)) } _ => Err(format!("Unexpected error: {}", e)), } } } } // Example with authorization key signing async fn sign_with_auth_key() -> Result { use privy_rs::{AuthorizationContext, PrivateKey}; let client = PrivyClient::new("your-app-id", "your-app-secret") .map_err(|e| format!("Client creation failed: {}", e))?; let key = PrivateKey("authorization-key".to_string()); let ctx = AuthorizationContext::new().push(key); let wallet_id = "wallet-id"; let message = "Hello, Privy!"; match client .wallets() .ethereum() .sign_message(wallet_id, message, &ctx, None) .await { Ok(response) => Ok(response.signature), Err(PrivySignedApiError::Api(api_err)) => { Err(format!("API error: {}", api_err)) } Err(PrivySignedApiError::SignatureGeneration(sig_err)) => { Err(format!("Signature generation failed: {}", sig_err)) } } } ``` ### Integration with error handling libraries The SDK works seamlessly with popular Rust error handling libraries: ```rust theme={"system"} use anyhow::Result; use privy_rs::PrivyClient; async fn create_wallet() -> Result { let client = PrivyClient::new("your-app-id", "your-app-secret")?; let wallet = client .wallets() .create(None, &create_wallet_body) .await?; Ok(wallet.id) } ``` ```rust theme={"system"} use thiserror::Error; use privy_rs::{PrivyCreateError, PrivySignedApiError, PrivyExportError, CryptoError, PrivyApiError}; #[derive(Error, Debug)] pub enum AppError { #[error("Client creation failed: {0}")] ClientCreation(#[from] PrivyCreateError), #[error("API communication failed: {0}")] Api(#[from] PrivyApiError), #[error("API operation with signing failed: {0}")] SignedApi(#[from] PrivySignedApiError), #[error("Wallet export failed: {0}")] Export(#[from] PrivyExportError), #[error("Cryptographic operation failed: {0}")] Crypto(#[from] CryptoError), #[error("Configuration error: {0}")] ConfigError(String), } async fn create_wallet() -> Result { let client = PrivyClient::new("your-app-id", "your-app-secret")?; let wallet = client .wallets() .create(None, &create_wallet_body) .await?; Ok(wallet.id) } ``` We also implement logging via the [tracing](https://crates.io/crates/tracing) crate, which is a popular and well-supported logging facade for Rust. ## Next steps & advanced topics * For an additional layer of security, you can choose to sign your requests with [authorization keys](/controls/authorization-keys/overview). * To restrict what wallets can do, you can set up [policies](/controls/policies/overview). * To prevent double sending the same transaction, take a look at our support for [idempotency](/api-reference/idempotency-keys) keys. * If you want to require multiple parties to sign off before sending a transaction for a wallet, you can accomplish this through the use of [quorum approvals](/controls/quorum-approvals/overview). # Setup Source: https://docs.privy.io/basics/rust/setup Configure the Privy Rust SDK client with your app credentials for backend wallet management. ## Prerequisites Before you begin: * Get your [Privy app ID and app secret](/basics/get-started/dashboard/create-new-app) from the Privy Dashboard * Rust 1.88 or later * A `tokio` async runtime ## Instantiating the `PrivyClient` Import the **`PrivyClient`** struct and create an instance by passing your Privy **app ID** and **app secret** as parameters. ```rust theme={"system"} use privy_rs::PrivyClient; #[tokio::main] async fn main() -> Result<(), Box> { let client = PrivyClient::new( "insert-your-app-id".to_string(), "insert-your-app-secret".to_string() )?; Ok(()) } ``` You can also store your credentials as environment variables for security: ```rust theme={"system"} let app_id = std::env::var("PRIVY_APP_ID") .expect("PRIVY_APP_ID environment variable not set"); let app_secret = std::env::var("PRIVY_APP_SECRET") .expect("PRIVY_APP_SECRET environment variable not set"); let client = PrivyClient::new_from_env()?; ``` This `client` **`PrivyClient`** is now your entry point to manage Privy from your server. With the `PrivyClient` you can interact with wallets with methods for creating wallets, signing and sending transactions. You can also manage users with methods for getting a user object, verifying an auth token, and importing new users. ## Authorization If a resource (i.e. wallet, policy, key quorum) has an [owner](/controls/authorization-keys/using-owners/overview), [authorization signatures](/api-reference/authorization-signatures) from the owner are required. Use the [authorization context](/controls/authorization-keys/using-owners/sign/signing-on-the-server) to specify authorization private keys and user JWTs of the wallet's owners, and the Rust SDK will generate signatures and sign requests under the hood. We strongly recommend reading [this guide](/controls/authorization-keys/using-owners/sign/signing-on-the-server) before using the Rust SDK for the best development experience. ```rust theme={"system"} use privy_rs::{AuthorizationContext, PrivateKey, JwtUser}; let client = PrivyClient::new(app_id, app_secret)?; let ctx = AuthorizationContext::new() .push(JwtUser(client.clone(), "jwt1".to_string())) .push(JwtUser(client.clone(), "jwt2".to_string())) .push(PrivateKey("authorization-key".to_string())); ``` ## Rate limits Privy rate limits REST API endpoints that you may call from your server. Learn more about optimizing your setup and handling rate limits in our [optimizing](/recipes/dashboard/optimizing) guide! # Migrating to 2.0 Source: https://docs.privy.io/basics/swift/advanced/migrating-to-2.0 ## Overview If your app previously used Privy's Swift `1.Y.Z` SDK, follow the migration guide below to upgrade to `2.0`. Privy's 2.Y.Z Swift SDK is Swift 6 compliant and adhere's to the new [strict concurrency standards](https://developer.apple.com/documentation/swift/adoptingswift6). We've taken this opportunity to also introduce some major API changes, which are outlined below in logical sections. ## Initialization ### 1. App client ID required at initialization Previously, apps were only required to pass in an `appId` when initializing the Privy SDK. Now, `appClientId` is required too. You can retrieve more information on how to retrieve your appClientId [here](/basics/get-started/dashboard/app-clients). ```swift Initializing the Privy SDK theme={"system"} let config = PrivyConfig(appId: "") // Remove let config = PrivyConfig(appId: "", appClientId: "") // Add let privy: Privy = PrivySdk.initialize(config: config) ``` ### 2. PrivySdk.initialize can only be called once It's important to use a single instance of Privy across the lifetime of your application. Calling PrivySdk.initialize multiple times will result in a fatal error. ### 3. Privy.awaitReady() When the Privy SDK is first initialized, the user's authentication state will be set to `notReady` until Privy finishes initialization. We've added an async `privy.awaitReady()` function that allows you to await initialization completion. During this time, we suggest you show a loading state to your user. Calling PrivySDK functions before calling `privy.awaitReady()` might result in unexpected functionality. Here's an example with some pseudocode: ```swift theme={"system"} Task { // Show loading UI uiState = .loading // Await ready await privy.awaitReady() if let case let .authenticated(privyUser) = privy.authState { // user is authenticated - show authenticated screen } else { // user not authenticated - show login screen } } ``` ## Authentication ### AuthState The `AuthState` represents the authentication state of your user. ```swift theme={"system"} public enum AuthState { /// Auth state has not been determined yet. Call `privy.awaitReady` to ensure auth state is set. case notReady /// Auth state cannot be determined while no network connectivity is available, but session tokens exist in cache. A call to get privy.getUser() would return null if auth state is authenticatedUnverified as this state confirms a prior user session exists, but can't be verified with the Privy backend. case authenticatedUnverified(AuthenticatedUnverifiedContext) /// The user is unauthenticated case unauthenticated /// The user is authenticated, and can be accessed via the associated value case authenticated(PrivyUser) } ``` #### Accessing AuthState The current auth state can be accessed any time via `privy.authState`. #### Subscribing to AuthState updates Auth state updates are exposed via `privy.authStateStream`, which is an AsyncStream. ```swift theme={"system"} func subscribeToAuthStateUpdates() { let task = Task { for await authState in privy.authStateStream { print("New auth state from Privy: \(authState)") } } // Cancel the task at some point in the future // task.cancel() } ``` ### The PrivyUser After authenticating a user via any login method, you will receive the `PrivyUser` object. The `PrivyUser` represents an authenticated user. All user specific actions, such as creating a wallet or retrieving the user's access token, are accessed via the `PrivyUser`. You may retrieve the `PrivyUser` anytime by calling `privy.user`. If this value is non-null, there is an authenticated user. If the value is null, there is no authenticated user. The `PrivyUser` can also be retrieved via the associated type of the "authenticated" auth state: ```swift theme={"system"} if case .authenticated(let privyUser) = privy.authState { // user is authenticated } ``` Use the `PrivyUser` object to: * Get the user's ID * Get the user's identity token * Get the user's access token * Get the user's linked accounts * Get the user's embedded Ethereum wallets * Get the user's embedded Solana wallets * Create an embedded Ethereum wallet * Create an embedded Solana wallet * Refresh the user * Log the user out ### Miscellaneous #### Errors We've significantly enhanced our error handling. When an SDK function throws an error, it will be a `PrivyError`, which contains an `errorCode` and a `localizedDescription`. #### AuthSession The `AuthSession` is no longer exposed. Values previously available in AuthSession are now available through different methods: * `PrivyUser`: can be accessed via `privy.user` as described above. * `accessToken`: can be accessed via `privy.user.getAccessToken()`. This method will return the user's access token, refreshing the session if needed. #### Session Refresh To refresh / update a `PrivyUser`, you'd previously call `privy.refreshSession`. Now, trigger the refresh via the `PrivyUser`, specifically, `privyUser.refresh`. #### Logout To logout an authenticated user, call `await privyUser.logout()` instead of `privy.logout()`. Once calling logout, the `PrivyUser` instance is no longer valid. #### Linked Accounts * `LinkedAccount` and its associated types are no longer `Codable`, `Hashable`, `Identifiable` or `Equatable`. * `LinkedAccount.embeddedWallet` is now split into chain specific values - `LinkedAccount.embeddedEthereumWallet` and `LinkedAccount.embeddedSolanaWallet` * The `chainId` property on the embedded wallet linked accounts is removed. * `firstVerifiedAt` and `latestVerifiedAt` fields are now optional values. #### Other type changes * `firstVerifiedAt`, `latestVerifiedAt`, and `createdAt` fields now have type `Date` instead of `TimeInterval` or `Int` * `verifiedAt` fields now replaced with `firstVerifiedAt` and `latestVerifiedAt` * `LoginMethod` is no longer `Equatable` ### Login with SMS * The `OtpFlowState` enum is no longer exposed. You should manually handle state management based on function results. For example, if `LoginWithSms.loginWithCode` throws an error, you can catch the error and update your UI accordingly. * `LoginWithSms.sendCode` now throws an error if sending code is unsuccessful, instead of returning false * `LoginWithSms.loginWithCode` now returns `PrivyUser` * `LoginWithSms.loginWithCode` now requires phone number to be passed in as a parameter (previously was optional) ### Login with email * The `OtpFlowState` enum is no longer exposed. You should manually handle state management based on function results. For example, if `LoginWithEmail.loginWithCode` throws an error, you can catch the error and update your UI accordingly. * `LoginWithEmail.sendCode` now throws an error if sending code is unsuccessful, instead of returning false * `LoginWithEmail.linkWithCode` no longer returns anything, and throws an error if linking fails * `LoginWithEmail.loginWithCode` now returns `PrivyUser` * `LoginWithEmail.loginWithCode` now requires email to be passed in (no longer optional) ### Login with custom auth * `LoginWithCustomAccessToken.loginWithCustomAccessToken` now returns `PrivyUser` * When initializing the PrivySDK, you should now pass the `TokenProvider` through the `PrivyLoginWithCustomAuthConfig` field in the `PrivyConfig` object. This allows Privy to access your user's access token at initialization while attempting to restore the Privy user's session. ### Login with SIWE * `SiweFlowState` enum is no longer exposed. You should manually handle state management based on function results. For example, if `LoginWithSiwe.loginWithSiwe` throws an error, you can catch the error and update your UI accordingly. * `LoginWithSiwe.loginWithSiwe` now returns `PrivyUser` * `LoginWithSiwe.loginWithSiwe` now requires message and params to be passed in (no longer optional) * `LoginWithSiwe.linkWithSiwe` no long returns anything, and throws an error if linking fails * `LoginWithSiwe.linkWithSiwe` now requires message and params to be passed in (no longer optional) ### Login with OAuth * LoginWithOAuth.login now returns `PrivyUser` ## Embedded wallets ### Overview Previously, all embedded wallet APIs were accessible directly from the `privy.embeddedWallet` object, which is no longer available. All embedded wallet APIs are now available via the `PrivyUser` instead. This is because all embedded wallet actions require an authenticated user, so adding the methods inside the authenticated `PrivyUser` was the most logical. As an example, when creating an Ethereum wallet: ```swift theme={"system"} try await privy.embeddedWallet.createWallet(chainType: .ethereum) // Remove try await privy.user.createEthereumWallet() // Add ``` ### Connecting the wallet In SDK 1.Y.Z, you had to ensure wallets were connected prior to accessing them by calling `privy.connectWallet()`. This method is now removed as **we handle connected state internally!** You may access the user's embedded wallets at anytime, without ensuring "wallet connected" state. Because you no longer need to manage wallet state, `EmbeddedWalletState` has been removed. ### Ethereum vs Solana Now, all embedded wallet APIs are chain specific and available via the `PrivyUser`. #### Creating a wallet Ethereum: ```swift theme={"system"} try await privy.embeddedWallet.createWallet(chainType: .ethereum) // Remove let ethereumWallet = try await privy.user.createEthereumWallet() // Add ``` Solana: ```swift theme={"system"} try await privy.embeddedWallet.createWallet(chainType: .solana) // Remove let solanaWallet = try await privy.user.createSolanaWallet() // Add ``` #### Retrieving a wallet Ethereum: ```swift theme={"system"} // Ensure wallets are connected // Remove guard case .connected(let wallets) = privy.embeddedWallet.embeddedWalletState else { // Remove print("Wallet not connected") // Remove return // Remove } // Remove // Grab first ethereum wallet from connected wallets // Remove guard let wallet = wallets.first, wallet.chainType == .ethereum else { // Remove print("No Ethereum wallets available") // Remove return // Remove } // Remove // Directly grab ethereum wallets, without worrying about connected state // Add let ethereumWallets: [EmbeddedEthereumWallet] = privy.user.embeddedEthereumWallets // Add ``` Solana: ```swift theme={"system"} // Ensure wallets are connected // Remove guard case .connected(let wallets) = privy.embeddedWallet.embeddedWalletState else { // Remove print("Wallet not connected") // Remove return // Remove } // Remove // Grab first ethereum wallet from connected wallets // Remove guard let wallet = wallets.first, wallet.chainType == .solana else { // Remove print("No Solana wallets available") // Remove return // Remove } // Remove // Directly grab ethereum wallets, without worrying about connected state // Add let solanaWallets: [EmbeddedSolanaWallet] = privy.user.embeddedSolanaWallets // Add ``` #### Using a wallet / rpc providers Instead of grabbing the wallet's provider via `try privy.embeddedWallet.getEthereumProvider(for: wallet.address)`, the provider is now available directly on the wallet instance. For example: Ethereum: ```swift theme={"system"} // Create or retrieve the embedded Ethereum wallet let ethereumWallet: EmbeddedEthereumWallet = privy.user.createEthereumWallet() try await ethereumWallet.provider.request( // Note: RpcRequest was renamed to EthereumRpcRequest EthereumRpcRequest(...) ) ``` Further, switching and retrieving the EVM chain is now an async operation: ```swift theme={"system"} // retrieve current chain let currentChain = await ethereumWallet.provider.chainId // set chain to Sepolia await ethereumWallet.provider.switchChain(chainId: 11155111, rpcUrl: nil) ``` Solana: ```swift theme={"system"} // Create or retrieve the embedded Ethereum wallet let solanaWallet: EmbeddedSolanaWallet = privy.user.createSolanaWallet() try await solanaWallet.provider.signMessage(...) ``` #### Changing the EVM chain When utilizing the `EmbeddedEthereumWalletProvider`, you may specify the EVM Chain by calling `provider.switchChain`. This was previously named `provider.configure`. # Features Source: https://docs.privy.io/basics/swift/features Learn about the features supported by the Privy Swift SDK for iOS embedded wallets. ## Supported features # Installation Source: https://docs.privy.io/basics/swift/installation Install the Privy Swift SDK for iOS and macOS apps to enable embedded wallet authentication. ## Requirements * iOS 17+ * Xcode 16+ ## SDK 2.0 All of our documentation reflects the APIs of our 2.Y.Z SDK, which is still in beta. It is highly recommended to integrate 2.Y.Z into your application, as we are close to GA. To find the latest SDK version, see our [Github Releases page](https://github.com/privy-io/privy-ios/releases). ## Installation Install the Privy Swift SDK via the Swift Package Manager: 1. In Xcode, navigate to File > Add Package Dependencies 2. In the "Search or Enter Package URL" search box enter: ``` https://github.com/privy-io/privy-ios ``` 3. Select the appropriate version and click Add Package # Quickstart Source: https://docs.privy.io/basics/swift/quickstart Learn how to authenticate users, create embedded wallets, and send transactions in your Swift iOS app with Privy. ## Prerequisites This guide assumes that you have completed the [setup](/basics/swift/setup) guide. ## Check user's authentication state ```swift theme={"system"} // Grab current auth state let authState = await privy.getAuthState() switch authState { case .authenticated(let user): // User is authenticated. Grab the user's linked accounts let linkedAccounts = user.linkedAccounts case .notReady: // Privy was just initialized and has not determined auth state yet // authState will never be this case after calling getAuthState() case .authenticatedUnverified: // Prior user session exists, but can't be verified due to no network connectivity. // Privy will automatically attempt to verify authenticated state when network is restored. case .unauthenticated: // User in not authenticated. } ``` ## Authenticate your user This quickstart guide will demonstrate how to authenticate a user with a one time password as an example, but Privy supports many authentication methods. Explore our [Authentication docs](/authentication/overview) to learn about other methods such as socials, passkeys, and external wallets to authenticate users in your app. Privy offers a variety of authentication mechanisms. The example below showcases authenticating a user via SMS. This is a two step process: 1. Send an OTP to the user provided phone number. 2. Verify the OTP sent to the user. Please be sure to configure SMS as a login method on the [**Privy Developer Dashboard**](https://dashboard.privy.io) under User Management > Authentication. #### 1. Send an OTP to the user's phone number via SMS After collecting and validating your users phone number, send an OTP by calling the **`sendCode`** method. Note: you must provide the phone number in [E.164 format](https://www.twilio.com/docs/glossary/what-e164). ```swift theme={"system"} do { let phoneNumber = "+14155552671" try await privy.sms.sendCode(to: phoneNumber) // OTP sent successfully - prompt user for OTP } catch { // OTP could fail if the network request fails print("Error sending code: \(error)) } ``` If the OTP is sent successfully, `sendCode` will not throw an error. If the provided phone number is invalid, or sending the OTP fails, **`sendCode`** will throw an error. #### 2. Authenticate with OTP The user will then receive an SMS with a 6-digit OTP. Prompt for this OTP within your application, then authenticate the user with the `loginWithCode` method. Pass the following parameters to this method: OTP code inputted by the user in your app. The user's phone number. ```swift theme={"system"} do { let phoneNumber = "+14155552671" let inputtedOtp = "123456" let privyUser = try await privy.sms.loginWithCode(inputtedOtp, sentTo: phoneNumber) print("Logged in with sms! User: \(privyUser.id)") } catch { print("Error logging user in: \(error)") } ``` If the OTP/phone number combination is valid, Privy will successfully authenticate your user and `loginWithCode` will return the `PrivyUser`. If the provided OTP/phone number combination is invalid, `loginWithCode` will throw an error that speicfies the error reason. ## The embedded wallet Privy's embedded wallets are compatible with the Ethereum and Solana blockchains. ### Creating the embedded wallet To create an EVM embedded wallet for your user, call `PrivyUser.createEthereumWallet`. ```swift theme={"system"} public protocol PrivyUser { // Other privy user methods func createEthereumWallet(allowAdditional: Bool) async throws -> EmbeddedEthereumWallet } ``` Ethereum embedded wallets are [hierarchical deterministic (HD) wallets](https://www.ledger.com/academy/crypto/what-are-hierarchical-deterministic-hd-wallets), and a user's seed entropy can support multiple separate embedded wallets. If a user already has a wallet and you'd like to create additional HD wallets for them, pass in `true` for the `allowAdditional` parameter. If a wallet is successfully created for the user, the newly created EmbeddedEthereumWallet is returned. The method will throw an error if * The user is not authenticated * If a user already has 9 or more wallets * If the network call to create the wallet fails * If a user already has an embedded wallet and allowAdditional is not set to true. #### Example ```swift theme={"system"} if let user = privy.user { // If user not null, user is authenticated do { let ethereumWallet = try await user.createEthereumWallet() print("Created wallet with address: \(ethereumWallet.address)") } catch { print("Error creating embedded wallet: \(error.localizedDescription)") } } ``` ### Using the embedded wallet To enable your app to request signatures and transactions from the embedded wallet, Privy Ethereum embedded wallets expose a provider *inspired by* the [**EIP-1193 provider**](https://eips.ethereum.org/EIPS/eip-1193) standard. This allows you request signatures and transactions from the wallet via a familiar [**JSON-RPC API**](https://ethereum.org/en/developers/docs/apis/json-rpc/) (e.g. [`personal_sign`](https://docs.metamask.io/wallet/reference/personal_sign/)). Once you have an instance of an `EmbeddedEthereumWallet`, you can make RPC requests by using the `provider: EmbeddedEthereumWalletProvider` hook and using its `request` method. For example, `wallet.provider.request(request: rpcRequest)`. ```swift theme={"system"} public protocol EmbeddedEthereumWallet: EmbeddedWalletBehavior { var provider: EmbeddedEthereumWalletProvider { get } } ``` As a parameter to this method, to this method, pass an `EthereumRpcRequest` object that contains: * **method**: the name of the JSON-RPC method for the wallet to execute (e.g. `personal_sign`) * **params**: an array of parameters required by your specified method By default, embedded wallets are connected to the Ethereum mainnet. To send a transaction on a different network, simply set the wallet's chainId in the transaction request. #### Example ```swift theme={"system"} if let user = privy.user { // If user not null, user is authenticated do { // Retrieve list of user's embedded Ethereum wallets let ethereumWallets = user.embeddedEthereumWallets // Grab the desired wallet. Here, we retrieve the first wallet if let wallet = ethereumWallets.first { let data = EthereumRpcRequest(method: "personal_sign", params: ["A message to sign", wallet.address]) let signature = try await wallet.provider.request(data) print("Result signature: \(signature)") } } catch { print("personal_sign error: \(error.localizedDescription)") } } ``` ### Creating the embedded wallet To create a Solana embedded wallet for your user, call `PrivyUser.createSolanaWallet`. If a wallet is successfully created for the user, the newly created EmbeddedSolanaWallet is returned. The method will throw an error if * The user is not authenticated * If a user already has a Solana wallet * If the network call to create the wallet fails #### Example ```swift theme={"system"} if let user = privy.user { // If user not null, user is authenticated do { let solanaWallet = try await user.createSolanaWallet() print("Created wallet with address: \(solanaWallet.address)") } catch { print("Error creating embedded wallet: \(error)") } } ``` ### Using the embedded wallet Privy supports requesting signatures on messages and transactions from a user's Solana embedded wallet using the `signMessage` RPC. To request a signature, get the Solana embedded wallet provider and call the `signMessage` method on it with a base-64 encoded message to sign. If the signature is computed successfully, `signMessage` will return it as a base64-encoded string. ```swift theme={"system"} public protocol EmbeddedSolanaWalletProvider { /// Request a signature on a Base64 encoded message or transaction /// - Parameters: /// - message: Base64 encoded message or transaction /// /// - Returns: The Base64 encoded computed signature /// /// - Throws: an error if signing the message is unsuccessful func signMessage(message: String) async throws -> String } ``` #### Example ```swift theme={"system"} if let user = privy.user { // If user not null, user is authenticated do { // Retrieve list of user's embedded Solana wallets let solanaWallets = user.embeddedSolanaWallets // Grab the desired wallet. Here, we retrieve the first wallet if let wallet = solanaWallets.first { // Base 64 encoded: "Hello! I am the base64 encoded message to be signed." let message = "SGVsbG8hIEkgYW0gdGhlIGJhc2U2NCBlbmNvZGVkIG1lc3NhZ2UgdG8gYmUgc2lnbmVkLg==" let signature = try await solanaProvider.signMessage(message: message) print("Result signature: \(signature)") } } catch { print("Error creating embedded wallet: \(error.localizedDescription)") } } ``` # Setup Source: https://docs.privy.io/basics/swift/setup Configure the Privy Swift SDK with your app credentials to enable embedded wallet authentication on iOS. ## Prerequisites Before you begin, make sure you have [set up your Privy app and obtained your app ID](/basics/get-started/dashboard/create-new-app) and [client ID](/basics/get-started/dashboard/app-clients) from the Privy Dashboard. A properly set up app client is required for mobile apps and other non-web platforms to allow your app to interact with the Privy API. Please follow [this guide](/basics/get-started/dashboard/app-clients) to configure an app client. ## Initializing Privy First, import the **Privy SDK** at the top of the file: ```swift theme={"system"} import PrivySDK ``` Initialize a **Privy** instance with a **`PrivyConfig`** object: ```swift theme={"system"} let config = PrivyConfig( appId: "YOUR_APP_ID", appClientId: "YOUR_APP_CLIENT_ID", loggingConfig: .init( logLevel: .verbose ) ) let privy: Privy = PrivySdk.initialize(config: config) ``` ## Configuration The configuration fields for the PrivyConfig are: Your Privy application ID, which can be obtained from the [**Privy Developer Dashboard**](https://dashboard.privy.io), under App Settings > Basics Your app client ID, which can be obtained from the [**Privy Developer Dashboard**](https://dashboard.privy.io), under App Settings > Clients (Optional) Your preferred log level and logging method. If no log level is specified, it will default to `PrivyLogLevel.NONE`. (Optional) Only use this if you plan to use custom authentication. Find more information [here](/authentication/user-authentication/jwt-based-auth/overview). Be sure to maintain a single instance of Privy across the lifetime of your application. Initializing multiple instances of Privy will result in unexpected errors. # Analytics CORS errors Source: https://docs.privy.io/basics/troubleshooting/analytics-cors You may occasionally see CORS errors in your browser console that look like this: ``` Access to fetch at 'https://auth.privy.io/api/v1/analytics_events' from origin has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. ``` These CORS errors are related to background analytics requests and **do not impact your application's functionality**. They are benign errors that can safely be ignored. The Privy SDK sends anonymous usage analytics in the background, and these requests occasionally trigger CORS warnings in your browser's developer console. While they appear as errors, they do not affect your application's performance or user experience. Still have questions? Reach out to our [support team](https://privy.io/slack) - we're here to help! # API error codes Source: https://docs.privy.io/basics/troubleshooting/error-handling/api-errors This page lists common error codes you may encounter when using the Privy API, along with their descriptions and troubleshooting steps. Encountering an error code that's not listed here? Tell us what you'd like added in [Slack](https://privy.io/slack). | Error Code | Description | | --------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | | [`policy_violation`](#policy-violation) | RPC request denied due to policy violation | | [`insufficient_funds`](#insufficient-funds) | Wallet has insufficient funds to complete the transaction | | [`transaction_broadcast_failure`](#transaction-broadcast-failure) | Transaction failed to broadcast to the network | | [`missing_or_empty_authorization_header`](#missing-or-empty-authorization-header) | Missing `privy-authorization-signature` header or no signatures provided | | [`zero_correct_authorization_signatures`](#zero-correct-authorization-signatures) | No valid authorization signatures were provided | | [`insufficient_correct_authorization_signatures`](#insufficient-correct-authorization-signatures) | Not enough valid authorization signatures provided | | [`incorrect_quantity_of_authorization_signatures`](#incorrect-quantity-of-authorization-signatures) | Number of signatures does not match the wallet's authorization threshold | | [`request_expired`](#request-expired) | The request has expired based on the `privy-request-expiry` header | | [`no_valid_user_session_keys`](#no-valid-user-session-keys) | No valid user signing keys available | | [`user_session_keys_expired`](#user-session-keys-expired) | User signing key is expired | ## Transaction errors ### `policy_violation` **Description:** RPC request denied due to policy violation This error occurs when an RPC request is blocked by a policy configured on the wallet. While this is intended behavior to enforce security controls, it may indicate that your policy configuration needs adjustment or that the transaction needs to be modified to comply with the policy. **Common causes:** * Transaction exceeds spending limits configured in the policy. * Transaction is sent to an address that is not allowlisted. * One or more Solana instructions may not be explicitly allowed. **Troubleshooting:** * **Review the policy configuration:** Navigate to the [Wallets dashboard](https://dashboard.privy.io/apps?page=wallets) to find the wallet and view the policy applied to this wallet * **Retrieve the wallet's policy via API:** Use the [Get Wallet](/api-reference/wallets/get) endpoint to check which policy is applied, then use the [Get Policy](/api-reference/policies/get) endpoint to review its rules * **Verify transaction details:** Ensure the transaction amount, recipient address, and contract interactions align with your policy requirements * **Check policy conditions:** Review specific conditions like spending limits, allowlisted addresses, and restricted operations * **Adjust policy or transaction:** Either modify the policy to accommodate legitimate use cases or adjust the transaction to comply with existing rules *** ### `insufficient_funds` **Description:** Wallet has insufficient funds to complete the transaction This error can appear in two forms: * "Wallet has insufficient funds for this transaction" - The wallet doesn't have enough tokens to cover the transaction and gas fees * "Insufficient gas credits balance" - Your app's gas sponsorship credits are depleted **Common causes:** * Wallet balance is too low to cover transaction value and gas fees * Gas credits have been exhausted (when using gas sponsorship) * Gas price spike causing higher than expected fees * Complex transaction requiring more gas than available * Incorrect gas estimation leaving insufficient buffer **Troubleshooting:** * **For gas credits depletion:** * Check your gas credits balance in the [Gas Sponsorship](https://dashboard.privy.io/billing?tab=gas-sponsorship) page of the Privy dashboard. * Add more credits to continue sponsoring transactions * Enable **Automated credit refill** to avoid this in the future, and make sure **Low credit notifications** are enabled. * **For wallet balance issues:** * Check the wallet's native token balance (ETH, MATIC, SOL, etc.) on a block explorer. Make sure to check the balance on the same chain you are sending the transaction on. * Fund the wallet with sufficient native tokens to cover gas fees * Consider implementing [wallet deposit flows](/financial-flows/deposits/overview) in your app * Use gas sponsorship to eliminate the need for users to hold native tokens for gas *** ### `transaction_broadcast_failure` **Description:** Transaction failed to broadcast to the network This error indicates that the transaction could not be broadcasted to the blockchain. The transaction was **not** broadcasted, meaning it's safe to retry without risk of duplicate transactions. **Common causes:** * Invalid transaction parameters (malformed data, incorrect format) * Network congestion or chain outages * RPC node connectivity issues * Nonce conflicts or sequencing errors **Troubleshooting:** * **Verify transaction inputs:** Double-check all transaction parameters including recipient address, amount, data field, and gas settings * **Retry the transaction:** Since the transaction was not broadcast, it's safe to retry with the same or corrected parameters * **Check network status:** * Visit the [Privy Status Page](https://status.privy.io) to check for known issues * Check the blockchain network's status page or block explorer for chain-wide issues * **Review error details:** Examine any additional error messages returned with the failure for specific validation issues *** ## Authorization signature errors The following errors occur when there is a failure validating [authorization signatures](/api-reference/authorization-signatures) for API requests. Certain API endpoints require authorization signatures from the resource owner to authorize the request. ### `missing_or_empty_authorization_header` **Description:** Missing `privy-authorization-signature` header or no signatures provided This error occurs when an API request requires authorization signatures but the `privy-authorization-signature` header is either missing entirely or contains no signatures. **Common causes:** * Making a request to an endpoint that requires authorization without including the required header * Header is present but contains an empty value * Missing `AuthorizationContext` when using Privy SDKs **Troubleshooting:** * **Implement proper signing:** Follow the [signing requests guide](/controls/authorization-keys/using-owners/sign/overview) to properly sign your API requests * **Verify SDK configuration:** If using a Privy SDK, ensure you've configured the [`AuthorizationContext`](/controls/authorization-keys/using-owners/sign/signing-on-the-server#using-the-authorization-context) correctly * **Check request headers:** Confirm the `privy-authorization-signature` header is being included in your request *** ### `zero_correct_authorization_signatures` **Description:** No valid authorization signatures were provided This error indicates that while authorization signatures were provided, none of them are valid. The signature payload may be malformed or the signing keys may be incorrect or expired. **Common causes:** * Signing the wrong payload (e.g., incorrect request body, URL, or headers) * Malformed signature format * Signing with a key that is not able to authorize the request **Troubleshooting:** * **Verify signature payload:** Ensure you're signing the correct payload according to the [authorization signatures specification](/controls/authorization-keys/using-owners/sign/overview#signature-payload) * **Check signing keys:** Verify that the keys you're using for signing are correct and have proper permissions * **Review signing implementation:** Follow the [signing requests guide](/controls/authorization-keys/using-owners/sign/overview) to ensure proper implementation *** ### `insufficient_correct_authorization_signatures` **Description:** Not enough valid authorization signatures provided This error occurs when some valid signatures were provided, but the number of valid signatures is less than the required authorization threshold for the resource. **Common causes:** * Wallet requires multiple signatures (based on the authorization threshold of the key quorum) but only one was provided * Some provided signatures are valid but others are malformed or expired * Authorization threshold was recently increased but request still uses old signature count * Missing signatures from required signers **Troubleshooting:** * **Check authorization threshold:** Navigate to the [Wallets dashboard](https://dashboard.privy.io/apps?page=wallets) to find the wallet and view the owner or signer applied to this wallet * **Provide all required signatures:** Ensure you're collecting and including signatures from all required owners or signers *** ### `incorrect_quantity_of_authorization_signatures` **Description:** Number of signatures does not match the wallet's authorization threshold This error occurs when the exact number of signatures provided in the `privy-authorization-signature` header doesn't match the wallet's required authorization threshold. **Common causes:** * Providing too few signatures * Incorrectly parsing or concatenating multiple signatures in the header **Troubleshooting:** * **Check authorization threshold:** Navigate to the [Wallets dashboard](https://dashboard.privy.io/apps?page=wallets) to find the wallet and view the owner or signer applied to this wallet * **Match signature count:** Ensure you're providing exactly the number of signatures required by the authorization threshold * **Check signature format:** When providing multiple signatures, ensure they're properly formatted in the header (comma-separated) *** ### `request_expired` **Description:** The request has expired. The `privy-request-expiry` header is invalid or older than the current time. This error occurs when the `privy-request-expiry` header included in the API request contains a timestamp that is in the past or is not a valid timestamp. The header value must be a Unix timestamp in milliseconds. **Common causes:** * The `privy-request-expiry` header contains a timestamp that has already passed * The request took too long to reach the server after the expiry was set * The `privy-request-expiry` header value is malformed or not a valid timestamp **Troubleshooting:** * **Set a valid future expiry:** Ensure the `privy-request-expiry` header is set to a timestamp in the future relative to when the server receives the request * **Account for network latency:** Add a reasonable buffer to the expiry time to account for network delays * **Validate header format:** Ensure the `privy-request-expiry` header value is a valid Unix timestamp in milliseconds (e.g., `1773679531000`) *** ### `no_valid_user_session_keys` **Description:** No valid user signing keys available This error occurs when attempting to authorize a request using user signing keys, but no valid keys are available for the user. **Common causes:** * User signing key was never requested or generated * /wallets/authenticate request was never completed or returned key was not properly decrypted. This happens automatically for Server SDKs using AuthorizationContext * User JWT is invalid or expired **Troubleshooting:** * **Request a user signing key:** Follow the [user signers guide](/controls/authorization-keys/keys/create/user/request) to properly request and use user signing keys * **Check authentication flow:** Verify the /wallets/authenticate request is returning correctly and the updated key is being used. * **Validate user JWT:** Ensure the user's JWT is valid and not expired *** ### `user_session_keys_expired` **Description:** User signing key is expired This error occurs when the user signing key being used to authorize the request has expired. User signing keys are time-bound for security purposes. **Common causes:** * User signing key has exceeded its validity period * Long delay between requesting the session key and making the API call * Using a cached session key that has expired **Troubleshooting:** * **Request a fresh session key:** Follow the [user signers guide](/controls/authorization-keys/keys/create/user/request) to request a new user signing key * **Use provided Server-side SDK AuthorizationContext:** AuthorizationContext will automatically retrieve a fresh user key and construct an authorization signature before making the RPC call. # Client-side error codes Source: https://docs.privy.io/basics/troubleshooting/error-handling/client-errors This page lists common error codes you may encounter when using Privy, along with their descriptions and troubleshooting steps. Encountering an error code that's not listed here? Tell us what you'd like added in [Slack](https://privy.io/slack). ## `invalid_native_app_id` **Description:** Invalid or missing native app identifier for mobile clients **Common Causes:** * Using wrong client ID in your application * Native app identifiers not configured in Privy dashboard * Using Expo Go without allowlisting `host.exp.Exponent` * Web clients accidentally sending `privy-native-app-id` header **Troubleshooting:** * **Verify client ID:** Double-check that you're using the correct client ID from your Privy dashboard * **Configure app client:** Ensure you have an [app client configured in your Privy dashboard](/basics/get-started/dashboard/app-clients) * **For Expo Go development:** Add `host.exp.Exponent` to your allowed application identifiers in the dashboard *** ## `invalid_origin` **Description:** The origin that your requests are coming from has not been allowlisted in your Privy dashboard **Common Causes:** * You are using an `appClient` (and therefore setting `clientId` in your PrivyProvider) that is overriding the allowed origins for your application. * You haven't added the origin to your allowed origins in the dashboard. * Your request is coming from an iFrame whose parent origin is not allowlisted. **Troubleshooting:** * If you are using an `appClient`: * Set the allowed origins for your application in the dashboard [here](https://dashboard.privy.io/apps?setting=domains\&page=settings). * If you are not using an `appClient`, you can set the allowed origins for your application in the dashboard [here](https://dashboard.privy.io/apps?setting=domains\&page=settings). * Make sure to add all parent origins of your application to the allowed origins list [here](https://dashboard.privy.io/apps?setting=domains\&page=settings). *** ## `linked_to_another_user` **Description:** There is a conflict between the current user and an existing user. **Common Causes:** * User previously signed up with Google/Apple OAuth using this email, then tries passwordless email login * User tries to update their linked\_account to one that's already taken * Importing users with duplicate linked\_account > **Use case:** > > 1. A user creates an account with one email ([email1@privy.io](mailto:email1@privy.io)) > 2. This user links a different email via OAuth ([email2@privy.io](mailto:email2@privy.io)) > 3. This user then tries to log in with the linked oauth account ([email2@privy.io](mailto:email2@privy.io)) using passwordless login, this will fail because this email is not associated with a passwordless login method. **Troubleshooting:** * Enable login method transfer to allow users to migrate their accounts. Learn more [here](/recipes/dashboard/account-transfer). * Make sure the user is using the correct email address for the login method they are trying to use. *** ## `failed_to_fetch_jwks_uri_document` **Description:** Failed to fetch JWKS URI document when configuring JWT authentication **Common Causes:** * Cloudflare or similar security measures blocking Privy from accessing your JWKS endpoint * JWKS endpoint not publicly accessible * Incorrect JWKS.json structure * Firewall or security rules restricting external access to your endpoint **Troubleshooting:** * **Check security configurations:** Review your Cloudflare settings or other security configurations that might be blocking external access to your JWKS endpoint * **Validate JWKS structure:** Verify your JWKS.json follows the required structure: ```json theme={"system"} { "keys": [ { "kty": "RSA", "n": "your-n-value", "e": "AQAB", "alg": "RS256", "kid": "your-key-id", "use": "sig" } ] } ``` *** ## `Wallet proxy not initialized` **Description:** Privy was not able to initialize the wallet proxy to interact with embedded wallets. **Common Causes:** * The application's origin is not allowlisted * The app is not waiting for Privy to reach the `ready` state * The app is not waiting for the wallet to be fully initialized before interacting with it **Troubleshooting:** * Confirm that the origin is allowlisted in the dashboard [here](https://dashboard.privy.io/apps?setting=domains\&page=settings). * Ensure that you are waiting for `ready` [here](/basics/react-native/setup#waiting-for-privy-to-be-ready) and potentially `ready` from `useWallets` [here](/wallets/wallets/get-a-wallet/get-connected-wallet#waiting-for-wallets-to-be-ready) # Multiple dialogs Source: https://docs.privy.io/basics/troubleshooting/multiple-dialogs The Privy modal is an [HTML ``](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/dialog) element that will appear in the foreground of your app when opened. If your app makes use of dialog components (most commonly, for modals and pop-ups), you may encounter issues with the Privy dialog interfering with those from your app. When using other non-Privy dialog elements within your app, we generally recommend: * **Avoid UIs that involve a modal overlaying another modal.** This can be a confusing and visually jarring experience for users, especially since users can only interact with a single modal at a time. * **Use the [`Dialog`](https://headlessui.com/react/dialog) component from [`headless-ui`](https://headlessui.com)**, as it has the best compatibility with UI components and HTML elements from third-party libraries like Privy. ## Radix UI dialogs If your app uses the [**`Dialog`**](https://www.radix-ui.com/primitives/docs/components/dialog) component from [**Radix UI**](https://www.radix-ui.com), we suggest making the following modifications to the default [**`Dialog`**](https://www.radix-ui.com/primitives/docs/components/dialog) component: 1. Prevent the default behavior of the Radix dialog closing when the user clicks outside of it, via the [**`onPointerDownOutside`**](https://www.radix-ui.com/primitives/docs/components/dialog#content) prop of the [**`Dialog.Content`**](https://www.radix-ui.com/primitives/docs/components/dialog#content) component. 2. Prevent the default behavior of the Radix dialog always trapping the browser's focus (even if other dialogs are opened), by wrapping your[ **`Dialog.Content`**](https://www.radix-ui.com/primitives/docs/components/dialog#content) with the **`FocusScope`** component from the [**`@radix-ui/react-focus-scope`**](https://www.npmjs.com/package/@radix-ui/react-focus-scope) library. In this **`FocusScope`** component, you should set the prop **`trapped`** to `false`. See this [GitHub discussion](https://github.com/radix-ui/primitives/issues/2544) for more info! Altogether, the modifications to a [**`Dialog`**](https://www.radix-ui.com/primitives/docs/components/dialog) component might look as follows: ```tsx theme={"system"} import * as Dialog from '@radix-ui/react-dialog'; import {FocusScope} from '@radix-ui/react-focus-scope'; ... {/* This wrapper prevents the Radix dialog from stealing focus away from other dialogs in the page. */} {/* The `onPointerDownOutside` handler prevents Radix from closing the dialog when the user clicks outside. */} e.preventDefault()} /> ... ``` # Common framework errors Source: https://docs.privy.io/basics/troubleshooting/react-frameworks If you're running into build errors with your framework, check out the following troubleshooting steps: If you're using a framework like [Gatsby](https://www.gatsbyjs.com/) and are running into build errors, check out some common errors below, and how to resolve them. ## iframe not initialized If you encounter an error like the one below: ``` iframe not initialized ``` There is likely an issue with how you are rendering the **`PrivyProvider`** component within your app. Namely, **if you are using Gatsby's [`wrapRootElement`](https://www.gatsbyjs.com/docs/reference/config-files/gatsby-browser/#wrapRootElement) to wrap your app with the `PrivyProvider`, you should use [`wrapPageElement`](https://www.gatsbyjs.com/docs/reference/config-files/gatsby-browser/#wrapPageElement) instead**, like below: ```tsx gatsby-browser.tsx theme={"system"} import React from 'react'; import {PrivyProvider} from '@privy-io/react-auth'; export const wrapPageElement = ({element}) => { return {element}; }; ``` Though Gatsby typically recommends using [**`wrapRootElement`**](https://www.gatsbyjs.com/docs/reference/config-files/gatsby-browser/#wrapRootElement) for React Contexts, the **`PrivyProvider`** component contains UI (HTML) elements as well, including a dialog (the Privy modal) and an iframe (the Privy iframe, used for embedded wallets). Given these UI elements, [**`wrapPageElement`**](https://www.gatsbyjs.com/docs/reference/config-files/gatsby-browser/#wrapPageElement) must be used instead of **`wrapRootElement`**. Still have questions? Reach out to our [support team](https://privy.io/slack) – we're here to help! If you're using a framework like [NextJS](https://nextjs.org/) and are running into build errors, check out some common errors below, and how to resolve them. ## App router If you are using the new [app router](https://nextjs.org/docs/app), you may encounter issues when attempting to wrap your app with the **`PrivyProvider`**. If so, follow the instructions below to set up your app with Privy: #### 1. Create a wrapper component for the **`PrivyProvider`** Since the **`PrivyProvider`** is a third-party React Context, it can only be used client-side, with the [**`'use client';`**](https://react.dev/reference/react/use-client) directive. Check out [these docs from NextJS](https://nextjs.org/docs/app/building-your-application/rendering/composition-patterns#using-context-providers) for more information. First, create a new component file and add [**`'use client';`**](https://react.dev/reference/react/use-client) as the first line. Then, within this same file, create a custom component (e.g. **`Providers`**) that accepts React [**`children`**](https://react.dev/learn/passing-props-to-a-component#passing-jsx-as-children) as props, and renders these [**`children`**](https://react.dev/learn/passing-props-to-a-component#passing-jsx-as-children), wrapped by the **`PrivyProvider`**: ```tsx theme={"system"} // components/providers.tsx 'use client'; import {PrivyProvider} from '@privy-io/react-auth'; export default function Providers({children}: {children: React.ReactNode}) { return {children}; } ``` This wrapper component ensures that the **`PrivyProvider`** is only ever rendered client-side, as required by NextJS. #### 2. Wrap your app with the providers component in your **`RootLayout`** Next, in your app's [Root Layout](https://nextjs.org/docs/app/building-your-application/routing/pages-and-layouts#root-layout-required), wrap the layout's [**`children`**](https://react.dev/learn/passing-props-to-a-component#passing-jsx-as-children) with your providers component, like so: ```tsx theme={"system"} import Providers from '../components/providers'; export default function RootLayout({children}: {children: React.ReactNode}) { return ( {children} ); } ``` Within your [**`RootLayout`**](https://nextjs.org/docs/app/building-your-application/routing/pages-and-layouts#root-layout-required), make sure you are using the wrapper component you created in step (1), *not* the raw **`PrivyProvider`** exported by the SDK. **That's it!** You can check out a complete example of Privy integrated into a NextJS app using the App Router [here](https://github.com/privy-io/examples/tree/main/privy-next-starter). Still have questions? Reach out to our [support team](https://privy.io/slack) – we're here to help! If you're using a framework like [Create React App](https://create-react-app.dev/) and are running into build errors, check out some common errors and how to resolve them. ## Missing Polyfills (Webpack 5) Since Create React App uses [Webpack 5](https://webpack.js.org/blog/2020-10-10-webpack-5-release/), you may encounter errors like the one below: ``` BREAKING CHANGE: webpack < 5 used to include polyfills for node.js core modules by default. This is no longer the case. Verify if you need this module and configure a polyfill for it. ``` This is because many standard web3 libraries, such as [`ethers.js`](https://docs.ethers.io/v5/), have dependencies that need to be polyfilled into your build environment. Webpack 5 no longer automatically handles these polyfills, which triggers this error. You can work past these issues by explicitly adding in these dependencies and overriding some configurations, as outlined below: #### 1. Install dependencies Run the following command in your project to install the necessary dependencies: ```sh theme={"system"} npm i --save-dev react-app-rewired assert buffer process stream-browserify url ``` #### 2. Configure your project with `react-app-rewired` In your `package.json`, in your start, build, and test scripts, update `react-scripts` to `react-app-rewired`. The "scripts" object should look like the following: ```json package.json theme={"system"} { ..., "scripts": { "start": "react-app-rewired start", "build": "react-app-rewired build", "test": "react-app-rewired test", "eject": "react-scripts eject" }, ... } ``` This allows you to bypass the default webpack configurations from `create-react-app`. #### 3. Add `config-overrides.js` to your project Lastly, at the root of your project, create a file called `config-overrides.js` and paste in the following: ```js config-overrides.js theme={"system"} const webpack = require('webpack'); module.exports = function override(config) { config.resolve.fallback = { assert: require.resolve('assert'), buffer: require.resolve('buffer'), 'process/browser': require.resolve('process/browser'), stream: require.resolve('stream-browserify'), url: require.resolve('url'), http: false, https: false, os: false }; config.plugins.push( new webpack.ProvidePlugin({ process: 'process/browser', Buffer: ['buffer', 'Buffer'] }) ); config.ignoreWarnings = [/Failed to parse source map/]; return config; }; ``` This tells your browser where to look for the dependencies that you've now added. **That's it!** Still have questions? Reach out to our [support team](https://privy.io/slack) – we're here to help! If you're using a framework like [Vite](https://vitejs.dev/) and are running into build errors, check out some common errors below, and how to resolve them. ## `process` is not defined If you encounter an error like the one below: ``` Uncaught (in promise) ReferenceError: process is not defined at ../../../node_modules/@coinbase/wallet-sdk/dist/CoinbaseWalletSDK.js ``` This is due to an issue in one of Privy's necessary dependencies, the [Coinbase Wallet SDK](https://github.com/coinbase/coinbase-wallet-sdk). You can read more about the issue [here](https://github.com/coinbase/coinbase-wallet-sdk/issues/967). **To resolve the issue, we recommend using the [`vite-plugin-node-polyfills`](https://www.npmjs.com/package/vite-plugin-node-polyfills) package, which will polyfill the `process` dependency that Coinbase requires.** #### 1. Install **`vite-plugin-node-polyfills`** First, install [**`vite-plugin-node-polyfills`**](https://www.npmjs.com/package/vite-plugin-node-polyfills) as a dev dependency: ```sh theme={"system"} npm i --save-dev vite-plugin-node-polyfills ``` #### 2. Update your **`vite.config.ts`** Then, update your [**`vite.config.ts`**](https://vitejs.dev/config/) file to include the following to use the plugin: ```ts theme={"system"} import {defineConfig} from 'vite'; import {nodePolyfills} from 'vite-plugin-node-polyfills'; // https://vitejs.dev/config/ export default defineConfig({ plugins: [nodePolyfills()] }); ``` ## Solana optional peer dependency build errors If you encounter a Vite build error like one of the following: ```txt theme={"system"} "getTransferSolInstruction" is not exported by "__vite-optional-peer-dep:@solana-program/system:..." ``` ```txt theme={"system"} Missing "./program-client-core" specifier in "@solana/kit" package ``` you may be hitting optional Solana peer dependency resolution in Vite. If your app does not use Solana wallets, add a Vite alias override for `@solana-program/system` as a temporary workaround. #### 1. Add an alias to your `vite.config.ts` ```ts theme={"system"} import {fileURLToPath, URL} from 'node:url'; import {defineConfig} from 'vite'; export default defineConfig({ resolve: { alias: { '@solana-program/system': fileURLToPath( new URL('./src/shims/solana-program-system.ts', import.meta.url) ) } } }); ``` #### 2. Add a shim file ```ts theme={"system"} // src/shims/solana-program-system.ts export function getTransferSolInstruction() { throw new Error( '@solana-program/system is not installed. Install Solana peer dependencies if you use Solana wallets.' ); } ``` If your app uses Solana wallets, install the required Solana peer dependencies listed in the React installation guide. **That's it!** Still have questions? Reach out to our [support team](https://privy.io/slack) - we're here to help! # Styles Source: https://docs.privy.io/basics/troubleshooting/styles If you're running into issues with the styles of Privy's UIs in your app, check out some common errors below, and how to resolve them. ### Corrupted styles with Sentry If your application uses [**Sentry**](https://sentry.io/welcome/) for monitoring, and you are seeing corrupted styles in Privy's UIs, it may be due to a bug with certain versions of Sentry's JavaScript libraries (e.g. [**`@sentry/react`**](https://www.npmjs.com/package/@sentry/react) and [**`@sentry/nextjs`**](https://www.npmjs.com/package/@sentry/nextjs)). **See this [GitHub issue](https://github.com/getsentry/sentry-javascript/issues/9170#issuecomment-1761391585) for more information.** To resolve this issue, try upgrading your **`@sentry/*`** package to a **version higher than `7.74.0`**. # Embedded wallets Source: https://docs.privy.io/basics/troubleshooting/troubleshooting-embedded-wallets **If you're running into issues with creating and using embedded wallets in your app, check out some common errors below, and how to resolve them.** ## Embedded wallets created on `localhost`, but not on deployment If you are able to successfully create embedded wallets for your users on **`localhost`**, but not in a deployed environment, **double-check that the protocol for your deployment URL is `https://` (secure), and *not* `http://`**. Privy embedded wallets use the browser's native [WebCrypto API](https://developer.mozilla.org/en-US/docs/Web/API/Web_Crypto_API), which is only available in secure contexts like **`https://`**. **In kind, you *must* use a secure context (`https://`) for your deployment.** Embedded wallets will *not* be created or work in insecure contexts like **`http://`**, except **`localhost`**, which is a special case and treated by the browser as a secure context. ## Access to the Base RPC URL has been blocked by CORS If you're using embedded wallets on Base or Base Goerli, and see the following error: ``` Access to fetch at 'https://base-mainnet.blastapi.io/insert-api-key' from origin 'insert-your-origin' has been blocked by CORS policy... ``` **This likely indicates that your IP address has been rate limited by the Blast RPC URL for making too many requests within a short time window.** Though this may appear to be a CORS violation, the initial error sent by Blast should indicate this rate limit (with a 429 status code). Successive errors due to the rate limit may not include the required CORS headers, which is why the overall error message appears as a CORS violation. **If you are seeing this issue, please try again shortly.** If it still does not resolve, please [reach out](https://privy.io/slack) and we can help debug! # Features Source: https://docs.privy.io/basics/unity/features Learn about the embedded wallet and authentication features supported by the Privy Unity SDK. ## Supported features # Installation Source: https://docs.privy.io/basics/unity/installation Install the Privy Unity SDK to enable embedded wallet authentication in your Unity game or app. ## Supported Platforms * ✅ MacOS / iOS * ✅ Android * ✅ WebGL * ❌ Windows * ❌ Linux ## Prerequisites * Unity Editor **2022.3.42f1** or newer ## Installation Privy's Unity SDK can be installed via the Unity Package Manager, OpenUPM, or by copying the SDK folder directly into your project. ### Option 1: Unity Package Manager (git URL) 1. Open your project in the Unity editor. 2. Navigate to **Window > Package Manager**. 3. Click the **+** button and select **Add package from git URL**. 4. Enter the following URL: ``` https://github.com/privy-io/unity-sdk.git?path=SDK ``` ### Option 2: OpenUPM 1. Install the [OpenUPM CLI](https://openupm.com/docs/getting-started-cli.html) if you haven't already. 2. From your project root, run: ```bash theme={"system"} openupm add io.privy.sdk ``` The SDK bundles third-party dependencies (`jsoncanonicalizer` and `unity-webview`) inside `SDK/ExternalDependencies`. If your project already includes any of these packages, manually exclude the duplicates to avoid conflicts. ## Using Privy namespaces Privy v1.0 organizes its public types into specific namespaces. Add the relevant `using` directives at the top of each file: ```csharp theme={"system"} using Privy.Core; // IPrivy, PrivyManager using Privy.Auth; // AuthState using Privy.Wallets; // IEmbeddedEthereumWallet, IEmbeddedSolanaWallet, RpcRequest, RpcResponse ``` ## WebGL Setup Privy's Unity SDK leverages an iframe to [secure the key material]() for a user's embedded wallet. Given the use of an iframe, we recommend testing builds with Privy's Unity SDK in the **browser**, or on a **non-WebGL platform** in the Unity editor. Watch this [demo](https://www.loom.com/share/0bac8322368c44059dff51e2dfc548e8) of setting up the Privy SDK in a Unity Project! To configure settings for your WebGL build to work with Privy, go to your **Project Settings** in the Unity editor. Next, select **Player** and navigate to **WebGL**. Set the following values: * In **Resolution and Presentation**, select `unity-webview`, or `unity-webview-2020` as the template if you are using a Unity editor version newer than 2020. webview-template * In **Other Settings/Optimization**, **managed stripping level** to **minimal** webview-stripping-settings The following versions of the Unity editor are not supported, due to [this bug](https://issuetracker.unity3d.com/issues/webgl-cross-origin-embedder-policy-require-corp-http-header-is-included-when-multithreading-is-off): `2022.3.20f1`, `2022.3.40f1`, `2023.2.12f1`, `6000.0.0b11`. # Quickstart Source: https://docs.privy.io/basics/unity/quickstart Learn how to authenticate users, create embedded wallets, and send transactions in your Unity game or app with Privy. ## Prerequisites This guide assumes that you have completed the [setup](/basics/unity/setup) guide. ## Check user's authentication state ```csharp theme={"system"} var authState = await PrivyManager.Instance.GetAuthState(); switch (authState) { case AuthState.Authenticated: // User is authenticated. Grab the user's linked accounts var privyUser = await PrivyManager.Instance.GetUser(); var linkedAccounts = privyUser.LinkedAccounts; break; case AuthState.Unauthenticated: // User is not authenticated. break; } ``` ## Authenticate your user This quickstart guide will demonstrate how to authenticate a user with a one time password as an example, but Privy supports many authentication methods. Explore our [Authentication docs](/authentication/overview) to learn about other methods such as socials, passkeys, and external wallets to authenticate users in your app. Privy offers a variety of authentication mechanisms. The example below showcases authenticating a user via email. This is a two step process: 1. Send an OTP to the user provided email address. 2. Verify the OTP sent to the user. ### 1. Send an OTP to the user's email address After collecting and validating your users email, send an OTP by calling the **`SendCode`** method. ```csharp theme={"system"} bool success = await PrivyManager.Instance.Email.SendCode(email); if (success) { // Prompt user to enter the OTP they received at their email address through your UI } else { // There was an error sending an OTP to your user's email } ``` ### 2. Authenticate with OTP The user will then receive an email with a 6-digit OTP. Prompt the user for this OTP within your application, then authenticate the user with the **`loginWithCode`** method. As a parameter to this method, pass an object with the following fields: The user's email address. OTP code inputted by the user in your app. ```csharp theme={"system"} try { // User will be authenticated if this call is successful await PrivyManager.Instance.Email.LoginWithCode(email, code); } catch { // If "LoginWithCode" throws an exception, user login was unsuccessful. Debug.Log("Error logging user in."); } ``` This method will throw an error if: * the incorrect OTP code is inputted * the network call to authenticate the user fails ## The embedded wallet ### Create an embedded wallet To create an embedded Ethereum wallet for your user, call the `CreateEthereumWallet` method on `IPrivyUser`. ```csharp theme={"system"} try { IPrivyUser privyUser = await PrivyManager.Instance.GetUser(); if (privyUser != null) { IEmbeddedEthereumWallet wallet = await privyUser.CreateEthereumWallet(); Debug.Log("New wallet created with address: " + wallet.Address); } } catch { Debug.Log("Error creating embedded wallet."); } ``` This method will throw an error if: * the user is not authenticated * the user already has an embedded wallet * wallet creation fails on the user's device To use embedded wallets, Privy implements an `RpcProvider` on the `EmbeddedWallet` class of the Unity SDK. This is an EIP1193 provider is responsible for managing RPC requests to a user's embedded wallet. Currently, Privy's `RpcProvider` only supports the `personal_sign` and `eth_signTypedData_v4` RPCs. We are actively adding support for other methods. #### 1. Get the user's wallet To make an RPC request to a user's wallet, first get the user's embedded wallet like so: ```csharp theme={"system"} // Ensure user is authenticated / non null IPrivyUser privyUser = await PrivyManager.Instance.GetUser(); if ( privyUser != null ) { // Grab the embedded wallet from the embedded wallet list // For demonstration purposes we're just grabbing the first one. IEmbeddedEthereumWallet embeddedWallet = privyUser.EmbeddedEthereumWallets[0]; //Ensure the Wallet is not null if ( embeddedWallet != null ) { //wallet operations } } ``` #### 2. Construct your RPC request Next, construct the RPC request using the `RpcRequest` class from Privy. The class follows the interface below: ```csharp theme={"system"} public class RpcRequest { public string Method { get; set; } public string[] Params { get; set; } } ``` As an example, you can construct a new RPC request like so. ```csharp theme={"system"} var rpcRequest = new RpcRequest { Method = "personal_sign", //a supported method Params = new string[] { "A message to sign", embeddedWallet.Address } //an array of strings, with the message + address }; ``` #### 3. Execute the RPC request Now, simply pass the `rpcRequest` you constructed to the `RpcProvider`'s `Request` method to execute the request: ```csharp theme={"system"} try { //Now that the response has been constructed, we try to execute the request RpcResponse personalSignResponse = await embeddedWallet.RpcProvider.Request(rpcRequest); //If response is successful, we can parse out the data Debug.Log(personalSignResponse.Data) } catch (PrivyWalletException ex) { //If the request method fails, we catch it here Debug.LogError($"Could not sign message due to error: {ex.Error} {ex.Message}"); } catch (Exception ex) { //If there's some other error, unrelated to the request, catch this here Debug.LogError($"Could not sign message exception {ex.Message}"); } ``` This will return an `RpcResponse`, which implements the interface below: ```csharp theme={"system"} public class RpcResponse { public string Method { get; set; } public string Data { get; set; } } ``` #### Handling errors The provider's `Request` method may error if: * the user is not authenticated * the user's wallet does not exist or has not loaded on their device * there is an issue with the RPC request that was sent to the wallet These errors can be caught through a generic exception, or Privy's custom `PrivyAuthenticationException` or `PrivyWalletException`: ```csharp theme={"system"} catch (PrivyAuthenticationException ex) { Debug.LogError($"Error signing message, Type:{ex.Error}, Message:{ex.Message}"); } catch (PrivyWalletException ex) { Debug.LogError($"Could not sign message due to error: {ex.Error} {ex.Message}"); } catch { Debug.LogError("Error signing message"); } ``` ### Full example As a complete example, you can send an RPC request to a wallet and handle corresponding errors like so: ```csharp theme={"system"} try { IPrivyUser privyUser = await PrivyManager.Instance.GetUser(); IEmbeddedEthereumWallet embeddedWallet = privyUser.EmbeddedEthereumWallets[0]; var rpcRequest = new RpcRequest { Method = "personal_sign", Params = new string[] { "A message to sign", embeddedWallet.Address } }; RpcResponse personalSignResponse = await embeddedWallet.RpcProvider.Request(rpcRequest); Debug.Log(personalSignResponse.Data); } catch (PrivyWalletException ex){ Debug.LogError($"Could not sign message due to error: {ex.Error} {ex.Message}"); } catch (Exception ex) { Debug.LogError($"Could not sign message exception {ex.Message}"); } ``` ### Create an embedded Solana wallet To create an embedded Solana wallet for your user, call the `CreateSolanaWallet` method on `IPrivyUser`. ```csharp theme={"system"} try { IPrivyUser privyUser = await PrivyManager.Instance.GetUser(); var solanaWallet = await privyUser.CreateSolanaWallet(); Debug.Log("New Solana wallet created with address: " + solanaWallet.Address); } catch { Debug.Log("Error creating embedded wallet."); } ``` This method will throw an error if: * the user is not authenticated * the user already has an embedded wallet * wallet creation fails on the user's device ### Signing a message with an embedded Solana wallet To use embedded wallets, Privy implements a provider on the `EmbeddedSolanaWallet` class of the Unity SDK. This is responsible for managing requests to a user's embedded Solana wallet, via the `SignMessage` method. To make an RPC request to a user's wallet, first get the user's embedded wallet like so: ```csharp theme={"system"} IPrivyUser privyUser = await PrivyManager.Instance.GetUser(); // Grab the embedded wallet from the embedded wallet list // For demonstration purposes we're just grabbing the first one. var embeddedWallet = privyUser.EmbeddedSolanaWallets[0]; //Ensure the Wallet is not null if (embeddedWallet != null) { //wallet operations } ``` Signatures using the embedded Solana wallet are performed on a **base64-encoded message**. This means you can sign arbitrary strings by encoding their utf-8 bytes to base64, but it also means you can **sign any transaction by serializing it** to a base64 encoded string. ```csharp theme={"system"} // Preparing an arbitrary string for signing string message = "A message to sign"; string base64Message = Convert.ToBase64String(Encoding.UTF8.GetBytes(message)); // Preparing a transaction for signing (using a custom class of your own for building the transaction) byte[] tx = new TransactionBuilder() // Add instructions to the transaction .CompileMessage(); string base64Tx = Convert.ToBase64String(tx); ``` Now, simply pass the message you want signed to the provider's `SignMessage` method to execute the signature request: ```csharp theme={"system"} try { var provider = embeddedWallet.EmbeddedSolanaWalletProvider; string signature = await provider.SignMessage(base64Message); Debug.Log(signature); } catch (PrivyWalletException ex) { //If the request method fails, we catch it here Debug.LogError($"Could not sign message due to error: {ex.Error} {ex.Message}"); } catch (Exception ex) { //If there's some other error, unrelated to the request, catch this here Debug.LogError($"Could not sign message exception {ex.Message}"); } ``` #### Handling errors The provider's `SignMessage` method may error if: * the user is not authenticated * the user's wallet does not exist or has not loaded on their device * there is an issue with the signature request that was sent to the wallet These errors can be caught through a generic exception, or Privy's custom `PrivyAuthenticationException` or `PrivyWalletException`: ```csharp theme={"system"} catch (PrivyAuthenticationException ex) { Debug.LogError($"Error signing message, Type:{ex.Error}, Message:{ex.Message}"); } catch (PrivyWalletException ex) { Debug.LogError($"Could not sign message due to error: {ex.Error} {ex.Message}"); } catch (Exception ex) { Debug.LogError($"Could not sign message exception {ex.Message}"); } ``` # Setup Source: https://docs.privy.io/basics/unity/setup Configure the Privy Unity SDK with your app credentials for game-based embedded wallet authentication. ## Prerequisites Before you begin, make sure you have [set up your Privy app and obtained your app ID](/basics/get-started/dashboard/create-new-app) and [client ID](/basics/get-started/dashboard/app-clients) from the Privy Dashboard. A properly set up app client is required for mobile apps and other non-web platforms to allow your app to interact with the Privy API. Please follow [this guide](/basics/get-started/dashboard/app-clients) to configure an app client. ## Initializing Privy Initialize Privy as early as possible in your game's lifecycle by calling `PrivyManager.Initialize(PrivyConfig config)`: ```csharp theme={"system"} using Privy.Core; using Privy.Config; var config = new PrivyConfig{ AppId = "YOUR_APP_ID", ClientId = "YOUR_CLIENT_ID" }; var privy = PrivyManager.Initialize(config); // Awaiting GetAuthState ensures the SDK is fully initialized var authState = await privy.GetAuthState(); ``` ## Configuration The configuration fields for the PrivyConfig are: Your Privy application ID, which can be obtained from the [**Privy Developer Dashboard**](https://dashboard.privy.io), under App Settings > Basics Your app client ID, which can be obtained from the [**Privy Developer Dashboard**](https://dashboard.privy.io), under App Settings > Clients Be sure to initialize Privy only once at the start of your game. Initializing multiple instances of Privy will result in unexpected errors. # Product updates Source: https://docs.privy.io/changelogs/product-updates ## June 2026 * **First-class [Tempo Transaction](https://docs.privy.io/recipes/tempo/send-transactions) support.** Privy now natively supports Tempo's AA transaction type in APIs and SDKs — enabling 10k+ TPS per user with parallel nonces, fee token selection, transaction batching, and scheduled execution. Gas sponsorship included. * **[Flutter SDK](https://docs.privy.io) at full mobile parity.** MFA via SMS, TOTP, and passkeys, OAuth account linking/unlinking, Telegram Login, signer management, and authorization signatures — now matching native iOS and Android SDKs. * **[Swap API](https://docs.privy.io/wallets/actions/swap/overview) now supports Solana mainnet.** Integrated with dflow aggregator for broad SPL-20 token coverage. Also supports specifying a recipient address different from the wallet owner, plus cross-chain swaps via Relay. * **[Token gas sponsorship](https://docs.privy.io/wallets/gas-and-asset-management/gas/setup#user-pays) — users pay gas in USDC or USDT.** New "User pays" mode lets wallets cover EVM gas fees using stablecoins directly, no native token needed. Supported on Ethereum, Base, Optimism, Arbitrum, and Polygon (USDC, USDT, EURC, USDG, USDC.e). * **[Stripe embedded onramp](https://docs.privy.io/wallets/funding/fiat-onramp) is live.** US users (excluding NY) can buy crypto with credit, debit, Apple/Google Pay, and ACH via Stripe Link without leaving the app. EU support coming soon. * **[Deposit Addresses](https://docs.privy.io/wallets/funding/crypto-deposits/overview) launched.** No-KYC, crypto-to-crypto onramp that handles swapping and bridging for end users — takes less than one hour to integrate. * **[OAuth Device Authorization](https://docs.privy.io/recipes/agent-integrations/agent-authorization) for agents and CLIs.** Issue OAuth access tokens to self-hosted agents (Claude Code, Codex, OpenClaw, custom CLIs) so users can grant authorized wallet access without exposing app secrets. ## May 2026 * **Expanded [fiat-to-crypto onramp](https://docs.privy.io/wallets/funding/fiat-onramp) options.** Meld aggregator now available in 100+ countries with card, bank transfer, and local payment methods. Developers complete KYB with Meld to go live. * **[Cross-chain transfers](https://docs.privy.io/wallets/actions/transfer/bridging) via POST /transfer.** Move stablecoins (USDC, USDT, USDG, pathUSD) across Ethereum, Base, Arbitrum, Polygon, Solana, and Tempo in a single API call — no bridge contracts or approvals to manage. Gas-sponsored by default. * **[Transfer policies](https://docs.privy.io/wallets/actions/transfer/policies) are live.** Restrict /transfer actions based on asset type, amount, chain, or destination address using Privy policies. * **Just-in-time balances and custom token support in the dashboard.** Real-time balance updates for default assets and any custom token on supported chains, plus dashboard transfers and transaction history. * **[Gas spend query endpoint](https://docs.privy.io/wallets/gas-and-asset-management/gas/gas-spend).** New `GET /v1/apps/gas_spend` returns aggregated USD gas credit charges for specified wallet IDs and date ranges — unlocks partner-level billing reconciliation and cost attribution. * **[Organization Secrets](https://docs.privy.io/recipes/create-app-with-organization-secret) for programmatic app provisioning.** Create and manage Privy apps without the dashboard — designed for whitelabel Privy-as-a-Service use cases at scale. * **[Privy Docs](https://docs.privy.io/) redesigned.** Cleaner navigation, faster paths to guides and recipes, and a more intuitive flow from first API call to production. * **[Node SDK](https://github.com/privy-io/node-sdk) is now fully open source.** Fourth SDK to be open sourced — full transparency, debugging, and contribution access. * **[Tron gas sponsorship](https://docs.privy.io/recipes/tron/transatron) with Transatron.** New recipe for sponsoring gas on Tron transactions. * **Command palette in the dashboard.** Press ⌘K (Mac) or Ctrl+K (Windows) to jump to any page or setting instantly. ## April 2026 * **[Swaps](https://docs.privy.io/wallets/actions/swap/overview?utm_source=pylon\&utm_medium=slack\&utm_campaign=privypulse)** are now natively integrated, allowing apps to move between assets with a single API call and bringing digital FX directly to wallets. Available on EVM today. * **[Agent CLI](https://docs.privy.io/recipes/agent-integrations/agent-cli?utm_source=pylon\&utm_medium=slack\&utm_campaign=privypulse)** lets agents spin up, fund, and manage wallets. The CLI pairs with a minimal sandbox so users can view balances and activity, maintain visibility and control over agent actions, and give agents access to their wallets. * **[Dashboard treasury management](https://www.privy.io/treasury?utm_source=pylon\&utm_medium=slack\&utm_campaign=privypulse) upgrades.** The Privy Dashboard now serves as a treasury management command center for orchestrating internal onchain asset flows. * **[Webhooks dashboard](https://docs.privy.io/api-reference/webhooks/overview#webhooks-overview)** is now available. Check webhook status, filter by event type, match sent transactions with a webhook via reference ID, and retrigger webhooks as needed. * **[Custodial wallets on Solana](https://docs.privy.io/wallets/custodial-wallets/sending-funds?utm_source=pylon\&utm_medium=slack\&utm_campaign=privypulse)** are live for USDC, USDB, and EURC. Scale on Solana with fully managed custodial infrastructure and abstracted blockchain experiences for users. * **[SOC 2 Type II certification renewed](https://privy.io/blog/privy-renews-soc-2-type-ii-compliance?utm_source=pylon\&utm_medium=slack\&utm_campaign=privypulse).** Privy's controls have been tested and validated by an independent auditor. ## March 2026 * [**Earn**](https://docs.privy.io/recipes/yield-guide) **on idle stablecoin balances** with configurable revenue sharing on yield from Morpho vaults. Aave and Kamino support coming soon. * [**Manual approvals**](https://docs.privy.io/controls/dashboard/overview) allow team members to review, authorize, or reject proposed transactions and wallet policy changes directly in the Privy Dashboard. Ideal for teams using Privy for [treasury management](https://privy.io/blog/upleveling-stablecoin-treasury-management-with-human-approval-workflows). * [**Nested key quorums**](https://docs.privy.io/controls/key-quorum/overview) encode approval structures directly into wallet policy, requiring the right set of approvals for sensitive actions like transfers or upgrades. * [**Tempo**](https://tempo.xyz/) **mainnet support is live.** Privy supports private transactions, memos, and reversibility. Contact [sales@privy.io](mailto:sales@privy.io) to connect with the Tempo team. * The **Node SDK now natively supports** [**@solana/kit**](https://docs.privy.io/wallets/using-wallets/solana/kit-integrations#nodejs) and [**x402 payments**](https://docs.privy.io/recipes/agent-integrations/x402#node-js-2), enabling agents to operate as [autonomous economic actors](https://privy.io/blog/building-the-agent-native-stack-with-privy-allium-and-x402) that can fund themselves and pay for data on-demand. * **hCaptcha is now available.** Upgrade to the latest SDK to enable it. See the guide on [protecting your app against bots](https://docs.privy.io/recipes/dashboard/preventing-bots). * The [**Billing dashboard role**](https://docs.privy.io/basics/get-started/dashboard/teammate-roles#teammate-roles) allows designated team members to manage payments and gas sponsorship settings without full developer or admin permissions. * The [**Go SDK**](https://docs.privy.io/basics/go/setup) is now available, enabling high-performance, concurrent backend wallet management. ## February 2026 * [**Custodial wallets**](https://docs.privy.io/wallets/custodial-wallets/overview) are now available. Privy is the first provider to offer truly flexible custody options through a single API, supporting both custodial and non-custodial wallets on the same infrastructure. See the [blog post](https://privy.io/blog/adding-custodial-wallets-to-privy-wallet-stack) to learn more. * **Expanded [agentic infrastructure](https://docs.privy.io/recipes/wallets/agentic-wallets)** for commercially viable AI agents with a focus on security: * [**OpenClaw integration**](https://privy.io/blog/securely-equipping-openclaw-agents-with-privy-wallets)**:** Give LLM-based agents the ability to perform secure on-chain actions. * **Usage-based settlements:** [x402](https://privy.io/blog/building-agentic-and-programmatic-payments-with-x402-and-privy) and [Nevermined](https://privy.io/blog/building-the-future-of-agentic-payments-with-nevermined) integrations support automated commercial settlements for AI agents, including specialized payment extensions for HTTP. * **Guardrailed autonomy:** Define spending limits, contract allow-lists, and time-windows via the policy engine to maintain programmatic control over [agent fleets](https://x.com/privy_io/status/2018358840681492906). * [**Stateful policies**](https://docs.privy.io/controls/policies/stateful-policies) bring institutional-grade risk management. Enforce rules like "Allow \$1M in USDC transfers per 24-hour window" or escalate to a multi-sig quorum when weekly volume exceeds a threshold. * [**hCaptcha**](https://docs.privy.io/authentication/user-authentication/captcha) is now integrated directly into the Privy login flow to prevent bot sign-ups. * **The `GET /balance` [endpoint](https://docs.privy.io/api-reference/wallets/get-balance#parameter-one-of-0) now supports ERC-20 and SPL tokens**, providing a unified view of stablecoin holdings and local assets across EVM and Solana. * [**EIP-7702 (Type 4)**](https://docs.privy.io/api-reference/wallets/ethereum/eth-send-transaction) **support** enables transaction bundling and gas sponsorship via the RPC endpoint, reducing multi-step actions to a single user interaction. * **Privy docs now [support MCP](https://docs.privy.io/basics/get-started/using-llms).** Add them to your IDE's AI assistant to generate code and debug integrations directly in your editor. ## January 2026 * **Early access to [Stripe headless crypto onramp](https://docs.privy.io/wallets/funding/headless-fiat-onramp).** Contact [sales@privy.io](mailto:sales@privy.io) to get started. * **[LatAm stablecoin report](https://drive.google.com/file/d/1IH7WnORCKk8H79z7azEvi4XWf5_CvMTv/view)** covering how stablecoins are becoming a parallel financial stack, with a blueprint for instant cross-border settlement and internet-native treasury management. * **[Enhanced wallet analytics](https://privy.io/blog/introducing-enhanced-user-analytics-in-the-dashboard) in the dashboard.** View total funded wallets, aggregate balances, and asset breakdowns per wallet over time across major chains. * **[Agentic wallets](https://docs.privy.io/recipes/wallets/agentic-wallets) and [treasury](https://docs.privy.io/recipes/wallets/treasury-wallets)** support m-of-n key quorums, policy enforcement, and automated flows. See the [configuration guide](https://privy.io/blog/reimagining-treasury-management-with-stablecoins). * **Yield integration recipes** for [Morpho](https://docs.privy.io/recipes/morpho-guide), [Aave](https://docs.privy.io/recipes/aave-guide), and [Kamino Earn](https://docs.privy.io/recipes/kamino-guide) (Solana only). * **[Relay integration](https://docs.privy.io/recipes/relay-deposit-addresses)** for cross-chain funding and swaps directly inside Privy wallets. * **[Prediction markets](https://privy.io/blog/beyond-polymarket-and-kalshi-five-prediction-markets-we-are-paying-attention-to)** continue to grow. Integrate [Polymarket builder codes](https://docs.privy.io/recipes/polymarket-guide) to embed prediction markets in your app. * **[Flexible custody](https://privy.io/blog/introducing-flexible-custody-better-custody-models-for-global-businesses)** (private beta — [sign up for early access](https://www.privy.io/flexible-custody)). Launch custodial and non-custodial wallets side by side using one API: * Integrate regulated custodians for compliant USD/stablecoin access. * Use non-custodial wallets for scalable, low-friction operations across jurisdictions. * **Expanded [Hyperliquid guide](https://docs.privy.io/recipes/hyperliquid-guide)** covering [Builder Codes](https://docs.privy.io/recipes/hyperliquid/builder-codes), [HyperEVM](https://docs.privy.io/recipes/hyperliquid/hyperevm), [trading policies](https://docs.privy.io/recipes/hyperliquid/policies-and-offline-actions) with multi-signature auth, and [agent wallets](https://docs.privy.io/recipes/hyperliquid/agents-and-subaccounts). * **World Mini Apps with Privy.** Build on World App's network of 12M+ verified users: * [SIWE guide](https://docs.privy.io/recipes/react/worldcoin-siwe-guide) for World wallet access * [World Chat integration](https://docs.privy.io/recipes/world/mini-apps) via XMTP # Authorization keys Source: https://docs.privy.io/controls/authorization-keys/keys/create/key Authorization keys allow the party that controls the key to execute actions on wallets and policies by signing requests to the Privy API. Examples of authorization keys include a key controlled by your app's server or a passkey controlled by a user. You can create authorization keys for your application via the **Privy Dashboard** or via the **REST API**. To create a new authorization key in the Dashboard, visit the [**Authorization keys**](https://dashboard.privy.io/apps?page=authorization-keys) page of the **Wallets** section for your app. Click the **New key** button and copy and save the generated **Private key**. Privy does not save this key and cannot help you recover it later. You can also set a human-readable **Key name**. In this process, Privy generates a keypair for your app directly on your device, and shows you the private key. * The private key (e.g. the key you copy) is generated on your device, and is only ever known to your app. Neither Privy nor the secure enclave ever sees the private key, and cannot sign payloads with it. **Make sure you save this key.** * The public key is registered with the secure enclave that secures your wallets, and is used to verify signatures produced by your app. Securely store this private key. Authorization keys can control wallets and execute actions, so treat them like production credentials. Privy does not store the private key and cannot help you retrieve it. To create a new authorization key with the NodeJS SDK, use the `generateP256KeyPair` function. ```ts theme={"system"} import {generateP256KeyPair} from '@privy-io/node'; const {privateKey, publicKey} = await generateP256KeyPair(); ``` This will return a `privateKey` and `publicKey` in DER format (no headers or footers), which you can use directly in the the methods of the Privy SDK, such as when setting `owners` or when building an [`AuthorizationContext`](/controls/authorization-context). For example, you can use the generated keypair as the owner of a wallet: ```ts theme={"system"} import {PrivyClient} from '@privy-io/node'; const wallet = await privy.wallets().create({ chain_type: 'ethereum', owner: { public_key: publicKey } }); const {signature} = await privy.wallets().ethereum().signMessage(wallet.id, { message: 'Hello, world!', authorization_context: { authorization_private_keys: [privateKey] } }); ``` Authorization keys are [P-256](https://neuromancer.sk/std/nist/P-256) public-private keypairs. Securely store the private key. Authorization keys can control wallets and execute actions, so treat them like production credentials. Privy does not store this and cannot help you recover it. You can create a keypair with the following command: ```sh theme={"system"} openssl ecparam -name prime256v1 -genkey -noout -out private.pem && \ openssl ec -in private.pem -pubout -out public.pem ``` This creates PEM-formatted files in your working directory for local storage. When registering the public key with the Privy API, you'll need to convert it to base64-encoded DER format: ```sh theme={"system"} openssl ec -pubin -in public.pem -outform DER | base64 ``` Next, follow [this guide](/controls/key-quorum/create) to register your public key with the Privy API. If you locally generate an authorization key and register it with the Privy API, make sure to note down the `id` in the response. You will use this value as the `owner_id` when specifying owners elsewhere (e.g. creating or updating wallets) or `signer_id` when specifying additional signers. Passkeys can be registered as authorization keys via either the Privy Dashboard or REST API. Simply follow the instructions in the Dashboard or REST API section to register the key, and pass the passkey's public key into the public key field of the request. # Key quorums Source: https://docs.privy.io/controls/authorization-keys/keys/create/key-quorum An owner or a signer is known as a key quorum. It can be composed of a mix of [users](/controls/authorization-keys/keys/create/user/overview), [authorization keys](/controls/authorization-keys/keys/create/key), and other key quorums (one level deep). Key quorums have an authorization threshold that defines how many keys in the quorum must sign a request for the aggregated signature to be valid. You can use key quorums to implement use cases such as: * Allowing users *or* apps to sign requests from user wallets * Requiring both users *and* apps to sign requests from user wallets * Requiring a distributed set of authorization keys to sign requests from a wallet * Hierarchical approval structures with nested quorums Learn more about key quorums in the [**Key quorums**](/controls/key-quorum/overview) section. Key quorums are an advanced integration. To determine if key quorums are right for your use case, please [reach out](https://privy.io/slack). # Configure authentication settings Source: https://docs.privy.io/controls/authorization-keys/keys/create/user/authentication If your app uses Privy as your authentication provider, you can skip this step. In order to issue user keys for users, the Privy API must verify the user's access token to ensure that the authenticated user is the party making the request for the user key. To verify a user's access token, Privy requires that your app register details of your authentication setup in the Privy Dashboard. Namely: 1. Get your **JWKS.json** endpoint from your authentication provider (e.g. Auth0, Firebase, Stytch). Privy will use this endpoint to verify access tokens for your users. 2. In the **Authentication** page of the **Configuration** section of the Privy Dashboard, enable **JWT-based authentication**. 3. Once JWT-based authentication has been enabled: 1. Determine whether your app will be authenticating requests that contain your provider's JWTs from a **server side or client side environment**. 2. Register the **JWKS.json** endpoint from your authentication provider and the name of the **JWT claim** that specifies the user's ID (typically `sub`). Privy can now verify access tokens issued by your authentication provider to authenticate users, and issue user keys for users. # Users Source: https://docs.privy.io/controls/authorization-keys/keys/create/user/overview Users can be owners and/or signers in Privy. You can create user non-custodial wallets by setting a user as the owner of the wallet, whether you use your own existing authentication provider or Privy as your authentication provider. When you make a request to the Privy API with a valid **access token** for a user, Privy returns a **user key** for the user. Requests to the Privy API to update or take actions with a resource owned by this user must be signed by the user key. To ensure the security of user keys: * User keys are **time-bound**, meaning they can only sign requests for a limited window before they expire, and a new user key must be requested. * When returning a user's key, Privy encrypts the key under a public-private keypair that your app generates. This ensures that only your server can decrypt the user's key. At a high-level, the flow to request user authentication keys is as follows: In the [Privy Dashboard](/controls/authorization-keys/keys/create/user/authentication), configure your authentication settings from your authentication provider. In particular, register the JWKS.json endpoint that will be used to verify your user's access token. If you are using Privy as your authentication provider, you can skip this step. Generate a public-private keypair (ECH P-256) that will be used to encrypt the user key. Make sure to save both the public and private keys. Make a request to the Privy API with the user's access token and the public key you generated. Privy will return a user key for the user, encrypted under the public key you provided, which you can decrypt with the corresponding private key. # Using user owners & signers Source: https://docs.privy.io/controls/authorization-keys/keys/create/user/request Once your application has successfully configured authentication settings, users can update and take actions with resources they own per the following flow. Make a request to the Privy API with the user's access token to request a user key. If the token is valid per your configured authentication settings, Privy will return a time-bound user key that can be used to sign requests. Given the returned user key, [sign the request](/controls/authorization-keys/using-owners/sign) to update or take actions with a resource the user owns. Lastly, [pass the signature](/controls/authorization-keys/using-owners/action) from the user key in a `privy-authorization-signature` header for the request. Privy will verify the signature and execute the request only if the signature is valid. Follow the guide below to learn how to request and use user keys from the Privy API. ### Set the authorization context to use the user's keypair Given the user's access token, the NodeJS SDK handles requesting the user key via the Privy API under the hood. Use the [authorization context](/controls/authorization-keys/using-owners/sign/signing-on-the-server) builder to set the user JWT, and pass it into wallet API functions that require owner's authorization, by setting the `user_jwts` property. ```ts theme={"system"} import {AuthorizationContext} from '@privy-io/node'; const authorizationContext: AuthorizationContext = { user_jwts: ['insert-user-jwt'] }; ``` Wallet requests on the wallets owned by the user can now be made by passing in this newly created authorization context on the call to the `PrivyClient`. ```ts title="Example: Sign a message with the user's wallet" highlight={15-17} theme={"system"} import {PrivyClient} from '@privy-io/node'; const privyClient = new PrivyClient({ appId: 'insert-your-app-id', appSecret: 'insert-your-app-secret' }); try { // With the authorization context, this method automatically signs the request. const response = await privyClient .wallets() .ethereum() .signMessage('insert-user-wallet-id', { message: 'Hello, Ethereum.', authorization_context: { user_jwts: ['insert-user-jwt'] } }); const signature = response.signature; } catch (error) { console.error(error); } ``` ### Set the authorization context to use the user's keypair Given the user's access token, the Java SDK handles requesting the user key via the Privy API under the hood. Use the [authorization context](/controls/authorization-keys/using-owners/sign/signing-on-the-server) builder to set the user JWT, and pass it into wallet API functions that require owner's authorization, by using `.addUserJwt()`. ```java theme={"system"} AuthorizationContext authorizationContext = AuthorizationContext.builder() .addUserJwt("insert-user-jwt") .build(); ``` Wallet requests on the wallets owned by the user can now be made by passing in this newly created authorization context on the call to the `PrivyClient`. ```java title="Example: Sign a message with the user's wallet" highlight={4-6,14} theme={"system"} try { String message = "Hello, Ethereum."; AuthorizationContext authorizationContext = AuthorizationContext.builder() .addUserJwt("insert-user-jwt") .build(); // With the authorization context, this method automatically signs the request. EthereumPersonalSignRpcResponseData response = privyClient .wallets() .ethereum() .signMessage( walletId, message.getBytes(StandardCharsets.UTF_8), authorizationContext ); String signature = response.signature(); } catch (APIException e) { String errorBody = e.bodyAsString(); System.err.println(errorBody); } catch (Exception e) { System.err.println(e.getMessage()); } ``` ### Set the authorization context to use the user's keypair Given the user's access token, the Go SDK handles requesting the user key via the Privy API under the hood. Use the [authorization context](/controls/authorization-keys/using-owners/sign/signing-on-the-server) to set the user JWT, and pass it into wallet API functions that require owner's authorization, by setting the `UserJwts` field. ```go theme={"system"} import "github.com/privy-io/go-sdk/authorization" authCtx := &authorization.AuthorizationContext{ UserJwts: []string{"insert-user-jwt"}, } ``` Wallet requests on the wallets owned by the user can now be made by passing in this newly created authorization context on the call to the Privy client. ```go title="Example: Sign a message with the user's wallet" highlight={6-8,15} theme={"system"} import ( privy "github.com/privy-io/go-sdk" "github.com/privy-io/go-sdk/authorization" ) authCtx := &authorization.AuthorizationContext{ UserJwts: []string{"insert-user-jwt"}, } // With the authorization context, this method automatically signs the request. response, err := client.Wallets.Ethereum.SignMessage( context.Background(), "insert-user-wallet-id", "Hello, Ethereum.", privy.WithAuthorizationContext(authCtx), ) if err != nil { log.Fatalf("failed to sign message: %v", err) } signature := response.Signature ``` ### Set the authorization context to use the user's keypair Given the user's access token, the Ruby SDK handles requesting the user key via the Privy API under the hood. Use the [authorization context](/controls/authorization-keys/using-owners/sign/signing-on-the-server) builder to set the user JWT, and pass it into wallet API functions that require owner's authorization, by setting the `user_jwts` property. ```ruby theme={"system"} ctx = Privy::Authorization::AuthorizationContext.build( user_jwts: ["insert-user-jwt"] ) ``` Wallet requests on the wallets owned by the user can now be made by passing in this newly created authorization context on the call to the `Privy::PrivyClient`. ```ruby title="Example: Sign a message with the user's wallet" highlight={1-3,13} theme={"system"} ctx = Privy::Authorization::AuthorizationContext.build( user_jwts: ["insert-user-jwt"] ) # With the authorization context, this method automatically signs the request. response = client.wallets.rpc( "insert-user-wallet-id", wallet_rpc_request_body: { method: "personal_sign", chain_type: "ethereum", params: {message: "Hello, Ethereum.", encoding: "utf-8"} }, authorization_context: ctx ) signature = response.data.signature ``` Directly managing user authorization keys via the REST API is an advanced integration. If you are using a Privy SDK, you do not need to directly manage the user's authorization key or manually generate authorization signatures. For security, Privy encrypts user authorization keys under a public key you provide to ensure that only your app can decrypt them. If you are just getting started with your integration, you can test the flow without encryption by following the **Without encryption** sections of the guide below. In production environments, we strongly recommend requesting user authorization keys **with encryption** as a security best practice. #### 1. Generate an ECH P-256 keypair To begin, create an [ECH P-256](https://csrc.nist.gov/csrc/media/events/workshop-on-elliptic-curve-cryptography-standards/documents/papers/session6-adalier-mehmet.pdf) public-private keypair to encrypt and decrypt your user's authorization key. Privy will encrypt the authorization under the public key for your keypair, and your server can decrypt the authorization key using the keypair's corresponding private key. When interacting with the Privy API, your ECH P-256 public-private keypair must be in the [SPKI](https://en.wikipedia.org/wiki/Simple_public-key_infrastructure) format. As an example, you can create an ECH P-256 keypair like so. ```typescript theme={"system"} import * as crypto from 'crypto'; async function generateEcdhP256KeyPair(): Promise<{ privateKey: crypto.webcrypto.CryptoKey; recipientPublicKey: string; }> { // Generate a P-256 key pair const keyPair = await crypto.subtle.generateKey( { name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveBits'] ); // The privateKey will be used later to decrypt the encapsulatedKey data returned from the /v1/user_signers/authenticate endpoint. const privateKey = keyPair.privateKey; // The publicKey will be used to encrypt the session key and will be sent to the /v1/user_signers/authenticate endpoint. // The publicKey must be a base64-encoded, SPKI-format string const publicKeyInSpkiFormat = await crypto.subtle.exportKey('spki', keyPair.publicKey); const recipientPublicKey = Buffer.from(publicKeyInSpkiFormat).toString('base64'); return {privateKey, recipientPublicKey}; } ``` If you are requesting user authorization keys without encryption, you can skip this step. #### 2. Request a user's authorization key If you are just getting started with your integration and skipped step 1, you should omit the `encryption_type` and `recipient_public_key` parameters of the request body blank. Next, use the user's access token to request a user authorization key for the user. If you generated a P256 keypair in step 1, you will also use the public key you generated to request the user authorization key. Make a request to: ```sh theme={"system"} https://api.privy.io/v1/wallets/authenticate ``` In the request body, pass the following parameters. The user's JWT, to be used to authenticate the user. If your app is using your own authentication provider, the user's JWT should verify against the JWKS.json endpoint you registered in the Dashboard. If your app is using Privy as your authentication provider, the user's JWT should be the access token issued by Privy. The encryption type for the authentication response. Currently only supports HPKE. Omit this field if you are requesting the authorization key unencrypted. The public key of your ECDH keypair, in base64-encoded, SPKI-format, whose private key will be able to decrypt the session key. This keypair must be generated securely and the private key must be kept confidential. The public key sent should be in base64-encoded DER format. The user's JWT, to be used to authenticate the user. If your app is using your own authentication provider, the user's JWT should verify against the JWKS.json endpoint you registered in the Dashboard. If your app is using Privy as your authentication provider, the user's JWT should be the access token issued by Privy. In the response, Privy will return the following. Make sure to save the `encrypted_authorization_key.encapsulated_key` and `encrypted._authorization_key.ciphertext` fields to use later. The encrypted authorization key, once decrypted, can be used to sign transactions on the wallet, acting as a temporary AuthorizationPrivateKey. Once decrypted, you will need to generate an [authorization signature](/api-reference/authorization-signatures) and pass it as a header under `privy-authorization-signature`. The encryption type used. Currently only supports HPKE. Base64-encoded ephemeral public key used in the HPKE encryption process. Required for decryption. The encrypted authorization key corresponding to the user's current authentication session. The expiration time of the authorization key in seconds since the epoch. The wallets that the signer has access to. The raw authorization key. Using this key, you will need to generate an [authorization signature](/api-reference/authorization-signatures) and pass it as a header under `privy-authorization-signature`. The expiration time of the authorization key in seconds since the epoch. The wallets that the signer has access to. See an example request and successful response below. An example request for an authorization key with encryption might look like the following: ```sh theme={"system"} curl -X POST "https://api.privy.io/v1/wallets/authenticate" \ -H "Authorization: Basic " \ -H "Content-Type: application/json" \ -H "privy-app-id: " \ -d '{ "user_jwt": , "encryption_type": "HPKE", "recipient_public_key": }' ``` A successful sample response will look like the following: ```json theme={"system"} { "encrypted_authorization_key": { "encryption_type": "HPKE", "encapsulated_key": "", "ciphertext": "" }, "expires_at": 1715270400, "wallets": [ { "id": "", "chain_type": "ethereum", "address": "" } ] } ``` An example request for an authorization key without encryption might look like the following: ```sh theme={"system"} curl -X POST "https://api.privy.io/v1/wallets/authenticate" \ -H "Authorization: Basic " \ -H "Content-Type: application/json" \ -H "privy-app-id: " \ -d '{ "user_jwt": }' ``` A successful sample response will look like the following: ```json theme={"system"} { "authorization_key": "", "expires_at": 1715270400, "wallets": [ { "id": "", "chain_type": "ethereum", "address": "" } ] } ``` ### 3. Decrypt the authorization key Finally, decrypt the authorization key using the returned `encrypted_authorization_key.encapsulated_key` and `encrypted_authorization_key.ciphertext` fields, as well as the private key you generated in step 1. ```ts theme={"system"} import {Chacha20Poly1305} from '@hpke/chacha20poly1305'; import {CipherSuite, DhkemP256HkdfSha256, HkdfSha256} from '@hpke/core'; // Initialize the cipher suite const suite = new CipherSuite({ kem: new DhkemP256HkdfSha256(), kdf: new HkdfSha256(), aead: new Chacha20Poly1305(), }); // Convert base64 to ArrayBuffer using browser APIs const base64ToBuffer = (base64: string) => Uint8Array.from(atob(base64), (c) => c.charCodeAt(0)).buffer; // Import private key using WebCrypto const privateKey = await crypto.subtle.importKey( 'pkcs8', base64ToBuffer('insert-base64-encoded-private-key'), { name: 'ECDH', namedCurve: 'P-256', }, true, ['deriveKey', 'deriveBits'], ); // Create recipient context and decrypt const recipient = await suite.createRecipientContext({ recipientKey: privateKey, enc: base64ToBuffer('insert-encapsulated-key-from-api-response'), }); return new TextDecoder().decode(await recipient.open(base64ToBuffer('insert-ciphertext-from-api-response'))); ``` If you did not provide a public key with which Privy encrypted the authorization key, you can skip this step. You can simply used the returned user `authorization_key` to sign requests. ### 4. Sign requests with the authorization key Now that you have successfully retrieved the authorization key for your user, continue to [this guide](/controls/authorization-keys/using-owners/sign) to learn how to sign requests to the Privy API. # Overview Source: https://docs.privy.io/controls/authorization-keys/owners/configuration/overview Overview of configuration options for authorization key owners in Privy. At a high-level, you should determine the minimal permissions your users, your app, and any third parties require for your wallets. Then, **configure each wallet with appropriate owners and additional signers to reflect your desired permissions**. ## Permissions Owners and signers differ in the permissions over wallets as outlined below. | | Owners | Signers | | ------------------------------- | ------ | ------- | | Sign messages | ✅ | ✅ | | Send transactions | ✅ | ✅ | | Update policies | ✅ | ❌ | | Update owners | ✅ | ❌ | | Update signers | ✅ | ❌ | | Export wallet | ✅ | ❌ | | Can be configured with policies | ✅ | ✅ | View common use cases around configuring owners and signers for wallets in the following guides. Create non-custodial user wallets and enable offline actions, server-side transactions, and more. Create wallets with custom approval configurations and give scoped controls to third-parties. # Programmable controls Source: https://docs.privy.io/controls/authorization-keys/owners/configuration/programmable Privy supports creating **wallets** that can be associated with your app, a third-party, or your own notion of a user. Common configurations of wallets are listed below. ### Single party can unilaterally approve actions If you'd like a single party to be able to unilaterally approve all actions, such as updating a wallet's owner or executing transactions with the wallet, simply **create the wallet with an authorization key controlled by the party as the owner**. The party can use this authorization key to update the wallet it controls and execute actions with it. ### Multiple parties can unilaterally approve actions If you'd like one of many parties to be able to unilaterally approve actions, such as updating a wallet's owner or policies, or executing transactions with the wallet, simply **create the wallet with a *1-of-k* key quorum**, whose elements are authorization keys associated with your different parties. Each party can use its associated authorization key to unilaterally update the wallet and execute actions with it. ### Multiple parties must collectively approve actions If you'd like authorization from multiple parties to update or take actions with a wallet, **create the wallet with an *m-of-k* key quorum**, where: * the key quorum is composed of authorization keys, associated with each party that can approve actions * *m* is defined such that your desired threshold of parties must approve the action Then, *m* of the *k* parties can use their associated authorization keys to sign requests to update the wallet and execute actions with it. Privy will only execute requests if *m* valid signatures are provided in the request. ### Scoping wallet policies to specific parties If you'd like multiple parties to be subject to different policies when taking action with a wallet, create the wallet with an "administrator" party as the owner and an additional signers array consisting of each of the parties. For each entry in the additional signer array, * Set the signer to the party that the policies should be subject to. * Set the signer's override policies to the policies that should apply to the signer. Each signer is then subject to the policies associated with them in the additional signers field of the wallet. ### Giving permissions to third parties If you'd like to give certain signature and transaction permissions to a third-party, create the wallet with: * an authorization key associated with the primary party as the **owner** * authorization keys associated with each of the third parties as additional **signers** with any necessary policies This ensures that the primary party is the only entity that can update the wallet, execute all actions with it, and export private keys, while third-parties can execute actions with the wallet within the scope of their associated policy. # Requiring user and server approvals Source: https://docs.privy.io/controls/authorization-keys/owners/configuration/user/dual-approval Many apps require *both* users and servers to approve transactions, which can be used to enhance the security of your application. For example, if a user's account is compromised, attackers cannot unilaterally take actions with the user's wallets without the server's approval. To enable a configuration where both users and servers must approve transactions, Privy recommends the following: Create a wallet owned by an *m-of-k* key quorum (m ≥ 2) whose elements include at least a **user** and an **authorization key** controlled by your server. You can do this via Privy's [REST API](/wallets/wallets/create/create-a-wallet). Next, construct your transaction request and have [users](/controls/authorization-keys/owners/configuration/user#sending-transactions-from-your-server) *and* [servers](/controls/authorization-keys/using-owners/sign) sign the transaction request. Finally, [execute the transaction request](/wallets/using-wallets/ethereum/send-a-transaction) with both signatures. # Enabling offline actions Source: https://docs.privy.io/controls/authorization-keys/owners/configuration/user/offline Many apps require taking specified actions with a user's wallets, even when the user is offline. This includes use cases like **limit orders, agentic trading on behalf of users (e.g. with a Telegram bot), or portfolio rebalancing.** For offline actions, Privy generally recommends: To ensure the wallet can only be updated by the user, create the wallet with a user owner. If you use one of Privy's client-side SDKs to create wallets, wallets are created with a user owner by default. Next, add an authorization key controlled by your server as an additional signer on the wallet. You can also configure the additional signer to have a specific set of policies associated with it, restricting the actions it can take. You can do this via one of Privy's [client-side SDKs](/wallets/using-wallets/signers/overview) or [REST API](/wallets/wallets/create/create-a-wallet) Your additional signer can now execute actions, such as signing messages or sending transactions, subject to its policies. These actions can occur while the user is offline. # Overview Source: https://docs.privy.io/controls/authorization-keys/owners/configuration/user/overview Overview of user-controlled authorization key configuration for Privy embedded wallets. Common flows to create non-custodial user wallets include: * **Creating wallets with a user owner.** This configures wallets such that users are the only entity that can update policies, add additional signers, export the wallet, or change the wallet's owner. If you create wallets via one of Privy's client-side SDKs, your app's wallets are automatically created with user owners. * **Creating wallets with a *1-of-n* key quorum, where one member of the key quorum is the user**. This gives users full permissions over their wallet, while enabling other parties to easily update wallets (e.g. policies and signers) and take actions with them. You can extend non-custodial user wallets to support various use cases, as outlined below. Take actions with wallets while users are offline, such as limit orders, agentic trading, and portfolio rebalancing. Require that both users and servers sign transaction requests. Send transactions from your server for increased control over transaction flows. Update the policies and signers assigned to wallets from your server, even when users are offline. Export wallets from your server to enable users to recover their account outside of your core application. # Exporting wallets from your server Source: https://docs.privy.io/controls/authorization-keys/owners/configuration/user/server-export Many apps want the server to to have the ability to export the private key for a user's wallet. This can be used to self-host a recovery site where users can export their private key outside of your core application. To enable this, Privy generally recommends: Create your wallet with a *1-of-k* key quorum, whose members include at least a **user** and an **authorization key** controlled by your server. You can do this via Privy's [REST API](/wallets/wallets/create/create-a-wallet). As a satisfying member of the key quorum that owns the wallet, **your server's authorization key can unilaterally export the wallet**. Follow [this guide](/wallets/wallets/export) to export the private key for your user's wallet from the server. # Sending transactions from your server Source: https://docs.privy.io/controls/authorization-keys/owners/configuration/user/server-transactions Many apps would like users to explicitly authorize transactions, but to send transaction requests from their server for increased reliability, retries, and various other use cases. To send transactions from your server by default: To ensure the wallet can only be controlled by the user, create the wallet with a user owner. If you use one of Privy's client-side SDKs to create wallets, wallets are created with a user owner by default. [Construct your transaction request](/controls/authorization-keys/using-owners/sign) and use Privy's [client-side SDKs' methods](/controls/authorization-keys/using-owners/sign#react%2C-expo) to have the user sign the transaction request. Send your user's authorization signature from your client to your server, and [send your transaction request](/controls/authorization-keys/using-owners/action) with the user's signature to Privy's API. To ensure the wallet can only be controlled by the user, create the wallet with a user owner. If you use one of Privy's client-side SDKs to create wallets, wallets are created with a user owner by default. Next, given a user's access token from your authentication provider, [request a user key](/controls/authorization-keys/keys/create/user/request) from Privy's API. You will use this key to sign transaction requests to Privy's aPI. Next, [construct your transaction request](/controls/authorization-keys/using-owners/sign) and [sign the transaction request](/controls/authorization-keys/using-owners/sign) with the user key Send your user's authorization signature from your client to your server, and [send your transaction request](/controls/authorization-keys/using-owners/action) with the user's signature to Privy's API. # Updating wallets from your server Source: https://docs.privy.io/controls/authorization-keys/owners/configuration/user/server-updates Many apps want the server to be able to update a wallet. For example, an app might want to update the policies or signers on a wallet, even with the user offline. For server-side wallet updates, Privy generally recommends: Create your wallet with a *1-of-k* key quorum, whose members include at least a **user** and an **authorization key** controlled by your server. You can do this via Privy's [REST API](/wallets/wallets/create/create-a-wallet). As a satisfying member of the key quorum that owns the wallet, **your server's authorization key can unilaterally update the policies and additional signers assigned to the wallet**. This enables your app to update wallet configurations, even when users are offline. # Overview Source: https://docs.privy.io/controls/authorization-keys/owners/overview Overview of Privy authorization key owners and how they control wallet access. Privy resources, such as wallets and policies, are controlled or managed by a [**user**](/controls/authorization-keys/owners/types#users), an [**authorization key**](/controls/authorization-keys/owners/types#authorization-key), or a [**key quorum**](/controls/authorization-keys/owners/types#key-quorum). These are collectively referred to as **owners** and **signers**. At a high-level: * **Owners** define who has ultimate control over a resource, including the ability to update policies or modify ownership configurations. * **Signers** are additional parties that can perform actions with a wallet, subject to the policies and permissions applied to them. Privy’s model allows you to assign different levels of control to different parties and to update these configurations over time. Ownership changes require authorization from an existing owner, ensuring that control is maintained through the same key-level guarantees as any other sensitive action. Learn more about the differences betweens [**owners**](/controls/authorization-keys/owners/overview#owners) and [**signers**](/controls/authorization-keys/owners/overview#signers) and the [**three types**](/controls/authorization-keys/owners/overview#types) of owners and signers. ## Owners Generally, owners have full control over a resource in the Privy API. Once assigned to a resource, owners have the ability to **update that resource**. Owners can also **update the owner** for a resource they control, enabling transfer of control over resources. With wallets, owners have the ability to: * sign and transact with the wallet (within the scope of the wallet's policies) * update the policies assigned to a wallet * update the additional signers assigned to the wallet, and the policies assigned to each signer * update the owner of the wallet * export the wallet's private key * delete the wallet With policies, owners have the ability to: * update the rules of the policy * update the owner of the policy * delete the policy ## Signers **Signers**, or **additional signers**, are parties that are given scoped permissions to take actions with a wallet. Signers on a wallet enable use cases like: * Scoping the permissions for a wallet by a signing authorization key, user, or key quorum * Taking offline actions on behalf of a user, such as limit orders, agentic trading, and portfolio rebalancing * Giving scoped permissions to a third-party to take actions on behalf of a wallet A wallet's owner can add or remove signers on the wallet, and assign policies to each signer to restrict the actions they can take. Signers **cannot update a wallet's owner, signers, or policies** and **cannot export the wallet's private key**. They can only take actions (signatures and transactions) with the wallet subject to their policies. ## Types Learn more about three types of owners and signers: [**users**](/controls/authorization-keys/owners/types#users), [**authorization keys**](/controls/authorization-keys/owners/types#authorization-key), and [**key quorums**](/controls/authorization-keys/owners/types#key-quorum). ## Permissions Owners and signers have different permissions over wallets, as outlined below. | | Owners | Signers | | ------------------------------- | ------ | ------- | | Sign messages | ✅ | ✅ | | Send transactions | ✅ | ✅ | | Update policies | ✅ | ❌ | | Update owners | ✅ | ❌ | | Update signers | ✅ | ❌ | | Export wallet | ✅ | ❌ | | Can be configured with policies | ✅ | ✅ | # Types of owners & signers Source: https://docs.privy.io/controls/authorization-keys/owners/types There are three types of owners & signers: [**users**](/controls/authorization-keys/owners/types#users), [**authorization keys**](/controls/authorization-keys/owners/types#authorization-key), and [**key quorums**](/controls/authorization-keys/owners/types#key-quorum) ### Users **Users** of your application can own and take actions with wallets and are represented by the Privy user ID. Users can be assigned to resources or can take actions with wallets by including their user ID in the API request. Privy [**users**](/user-management/users/overview) represent individuals. To share wallet access across a business or team, add users to a key quorum and follow the [organization wallet setup guide](/organizations/setup/overview). You can create user non-custodial wallets by setting a user as the owner of the wallet, whether you use your own existing authentication provider or Privy as your authentication provider. ### Authorization keys **Authorization keys** are P256 cryptographic keys that allow any party that controls the key to take actions with associated wallets. You can assign authorization keys to a resource or execute actions with authorization keys by signing the request with the respective private key. Common examples of authorization keys include: * app keys, which are controlled by your app's server, allowing your app to execute requests * a biometric key or passkey, following the [WebAuthn](https://webauthn.io/) standard, which allow users to easily sign and execute requests with a P256 key ### Key quorums Owners and signers can also be composed of a mix of users and authorization keys. This is known as a **key quorum**. Key quorums have an authorization threshold that defines how members of the quorum must sign a request for the aggregated signature to be valid. You can use key quorums to implement use cases such as: * Allowing users *or* apps to sign requests from user wallets * Requiring both users *and* apps to sign requests from user wallets * Requiring a distributed set of authorization keys to sign requests from a wallet Key quorums are an advanced integration. To determine if key quorums are right for your use case, please [reach out](https://privy.io/slack). # Owning resources with owners Source: https://docs.privy.io/controls/authorization-keys/using-owners/assign To have an owner own a resource, you can pass an identifier for the owner in the `owner` field of the request to create or update a resource. To assign a user as the owner of a resource, in the request to create or update the resource, pass `{user_id: 'insert-user-id-of-owner'}` object in the `owner` field of your request. Refer to the following guides for: * [Creating a wallet with a user owner](/wallets/wallets/create/create-a-wallet#rest-api) * [Updating a wallet to have a user owner](/wallets/wallets/update-a-wallet#rest-api) To assign an authorization key as the owner of a resource, in the request to create or update the resource, pass a `{public_key: 'insert-public-key'}` object in the `owner` field of your request. Refer to the following guides for: * [Creating a wallet with a authorization key owner](/wallets/wallets/create/create-a-wallet#rest-api) * [Updating a wallet to have an authorization key owner](/wallets/wallets/update-a-wallet#rest-api) * [Creating a policy with an authorization key owner](/controls/policies/create-a-policy#rest-api) * [Updating a policy to have an authorization key owner](/controls/policies/update-a-policy#rest-api) To assign a key quorum as the owner a resource, in the request to create or update the resource, pass your key quorum ID in the `owner_id` field of your request. Refer to the following guides for: * [Creating a wallet with a key quorum owner](/wallets/wallets/create/create-a-wallet#rest-api) * [Updating a wallet to have a key quorum owner](/wallets/wallets/update-a-wallet#rest-api) * [Creating a policy with a key quorum owner](/controls/policies/create-a-policy#rest-api) * [Updating a policy to have a key quorum owner](/controls/policies/update-a-policy#rest-api) # Using owners Source: https://docs.privy.io/controls/authorization-keys/using-owners/overview Owners control resources in the Privy API, allowing them to modify or take actions with resources, such as updating a policy or sending a transaction with a wallet. Signers have the ability to send transactions from a given wallet within the scope of certain policies. Learn more about using owners and signers below. ### Owners To assign an owner to a resource, pass the owner's identifier (authorization key, user ID, or key quorum ID) in the `owner` field of the resource. If the resource is a wallet, set the wallet's `policy_ids` to set the policy that the `owner` should be subject to. Once an owner is set on a resource: * The owner must sign all updates or deletions of the resource. * If the resource is a wallet, the owner must sign signature or transaction requests to the Privy API and is subject to the policies set on the wallet. ### Signers Signers enable setting different permissions that different parties can take with a given wallet. To attach a signer to a resource, add a new entry in the `additional_signers` array with the key quorum ID of your signer. You can then set `override_policy_ids` that apply to this specific signer. This enables you to set specific policies for certain key quorums over the same wallet. # Implementing signing directly Source: https://docs.privy.io/controls/authorization-keys/using-owners/sign/direct-implementation If you are unable to use Privy's SDKs for signing, you can implement request signing directly in your service. Implementing request signing directly is an advanced integration. Wherever possible, we suggest [using Privy's SDKs to handle request signing.](/controls/authorization-keys/using-owners/sign/signing-on-the-server) ## Steps At a high-level, directly implementing request signing requires the following steps: Generate a JSON payload containing the following fields. All fields are required unless otherwise specified. | Field | Type | Description | | | | | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | - | - | - | | `version` | `1` | Authorization signature version. Currently, `1` is the only version. | | | | | `method` | `'POST' \| 'PUT' \| 'PATCH' \| 'DELETE'` | HTTP method for the request. Signatures are not required on `'GET'` requests. | | | | | `url` | `string` | The full URL for the request. Should not include a trailing slash. | | | | | `body` | `JSON` | JSON body for the request. | | | | | `headers` | `JSON` | JSON object containing any Privy-specific headers, e.g. those that are prefixed with `'privy-'`. This should **not** include any other headers, such as authentication headers, `content-type`, or trace headers. | | | | | `headers['privy-app-id']` | `string` | Privy app ID header (required). | | | | | `headers['privy-idempotency-key']` | `string` | Privy idempotency key header (optional). If the request does not contain an idempotency key, leave this field out of the payload. | | | | | `headers['privy-request-expiry']` | `string` | Privy request expiry header (optional). If the request does not contain an expiry header, leave this field out of the payload. | | | | Next, canonicalize the payload per [RFC 8785](https://www.rfc-editor.org/rfc/rfc8785) and serialize it to a string. [This GitHub repository](https://github.com/cyberphone/json-canonicalization) links to various libraries for JSON canonicalization in different languages. Sign the serialized JSON with ECDSA P-256 using the private key of your user key or authorization key and serialize it to a base64-encoded string. Lastly, include the base64-encoded signature over the payload in the `privy-authorization-signature` header of your request to the Privy API. ## Code examples View code examples for signing requests in various languages below. If the desired resource requires a user owner or user signer, make sure to [request the user key](/controls/authorization-keys/keys/create/user/request) before signing requests with it. ```typescript theme={"system"} import canonicalize from 'canonicalize'; // Support JSON canonicalization import crypto from 'crypto'; // Support P-256 signing // Replace this with your private key from the Dashboard const PRIVY_AUTHORIZATION_KEY = 'wallet-auth:insert-your-private-key-here'; // ... function getAuthorizationSignature({url, body}: {url: string; body: object}) { const payload = { version: 1, method: 'POST', url, body, headers: { 'privy-app-id': 'insert-your-app-id' // If your request includes an idempotency key, include that header here as well } }; // JSON-canonicalize the payload and convert it to a buffer const serializedPayload = canonicalize(payload) as string; const serializedPayloadBuffer = Buffer.from(serializedPayload); // Replace this with your user or authorization key. We remove the 'wallet-auth:' prefix // from authorization keys before using it to sign requests const privateKeyAsString = PRIVY_AUTHORIZATION_KEY.replace('wallet-auth:', ''); // Convert your private key to PEM format, and instantiate a node crypto KeyObject for it const privateKeyAsPem = `-----BEGIN PRIVATE KEY-----\n${privateKeyAsString}\n-----END PRIVATE KEY-----`; const privateKey = crypto.createPrivateKey({ key: privateKeyAsPem, format: 'pem' }); // Sign the payload buffer with your private key and serialize the signature to a base64 string const signatureBuffer = crypto.sign('sha256', serializedPayloadBuffer, privateKey); const signature = signatureBuffer.toString('base64'); return signature; } const authorizationSignature = getAuthorizationSignature({ // Replace with your desired path url: 'https://api.privy.io/v1/wallets//rpc', // Replace with your desired body body: { method: 'personal_sign', params: { message: 'Hello world', // ... }, } }); ``` ```rust signature.rs theme={"system"} use anyhow::{anyhow, Result}; use p256::ecdsa::{signature::Signer, Signature, SigningKey}; use base64::{engine::general_purpose::STANDARD, Engine as _}; use serde_json::json; /// Signs the canonicalized JSON payload using ECDSA (P-256 + SHA-256). /// /// - `private_key_string` - A string containing your user or authorization key. /// For authorization keys, remove the "wallet-api:" prefix. /// - `payload` - JSON payload to sign, serialized to a string /// fn sign_payload(private_key_string: &str, payload: &str) -> Result { let bytes = extract_32_byte_key_from_pkcs8_base64(private_key_string)?; let signing_key = SigningKey::from_slice(bytes.as_slice())?; // Sign the payload (SHA-256 is implied by ECDSA in P256's default) let signature: Signature = signing_key.sign(payload.as_bytes()); // base64 encode the signature let signature_b64 = STANDARD.encode(signature.to_der()); Ok(signature_b64) } /// Extracts the raw 32-byte private key from a base64-encoded PKCS#8 blob. /// Returns an error if `0x04 0x20` cannot be found or if the data is too short. fn extract_32_byte_key_from_pkcs8_base64(pkcs8_b64: &str) -> Result<[u8; 32]> { // 1. Decode base64 let pkcs8_bytes = STANDARD.decode(pkcs8_b64)?; // 2. Search for the 2-byte pattern [0x04, 0x20] let pattern = [0x04, 0x20]; let private_key_start = pkcs8_bytes .windows(pattern.len()) .position(|window| window == pattern) .ok_or(anyhow!( "Invalid wallet authorization private key: marker not found" ))?; // 3. Extract the 32 bytes following 0x04, 0x20 let start = private_key_start + 2; let end = start + 32; if end > pkcs8_bytes.len() { return Err(anyhow!( "Invalid wallet authorization private key: data too short" )); } let mut private_key_bytes = [0u8; 32]; private_key_bytes.copy_from_slice(&pkcs8_bytes[start..end]); Ok(private_key_bytes) } /// Main function to generate the authorization signature. fn main() -> Result<()> { let privy_authorization_key = "wallet-auth:your-authorization-private-key"; let private_key_string = privy_authorization_key.replace("wallet-auth:", ""); let url = "https://api.privy.io/v1/wallets"; let body = json!({ "chain_type": "ethereum" }); // --- Build the payload to sign --- let mut payload = json!({ "version": 1, "method": "POST", "url": url, "body": body, "headers": { "privy-app-id": "insert-your-app-id" } }); // --- Canonicalize (sort keys, minimal separators) and serialize --- payload.sort_all_objects(); let serialized_payload = serde_json::to_string(&payload)?; println!("{}", serialized_payload); // --- Sign the serialized payload using P-256 ECDSA --- let authorization_signature = sign_payload(&private_key_string, &serialized_payload)?; println!("{}", authorization_signature); Ok(()) } ``` ```rust Cargo.toml theme={"system"} [dependencies] serde_json = {version = "1.0", features = ["preserve_order"]} p256 = "0.13" base64 = "0.22" anyhow = "1.0" ``` ```go theme={"system"} import ( "crypto/ecdsa" "crypto/elliptic" "crypto/rand" "crypto/x509" "encoding/base64" "encoding/json" "fmt" "hash/fnv" "strings" "crypto/sha256" ) // SignPayload signs the canonicalized JSON payload using ECDSA (P-256 + SHA-256). // // privyAuthorizationKey - A string containing your user key or authorization key. // payload - JSON payload to sign, serialized to a string // // Returns the base64-encoded DER signature or an error. func SignPayload(privyAuthorizationKey string, payload string) (string, error) { privateKey, err := parsePrivateKeyFromAuthorizationKey(privyAuthorizationKey) if err != nil { return "", fmt.Errorf("failed to parse private key: %w", err) } // Hash the payload using SHA-256 hash := sha256.Sum256([]byte(payload)) // Sign the hash signature, err := ecdsa.SignASN1(rand.Reader, privateKey, hash[:]) if err != nil { return "", fmt.Errorf("failed to sign payload: %w", err) } // Base64 encode the signature signatureB64 := base64.StdEncoding.EncodeToString(signature) return signatureB64, nil } // We parse the ecdsa key from the user or authorization key here func parsePrivateKeyFromAuthorizationKey(privyAuthorizationKey string) (*ecdsa.PrivateKey, error) { pkcs8B64 := strings.TrimPrefix(privyAuthorizationKey, "wallet-auth:") pkcs8Bytes, err := base64.StdEncoding.DecodeString(pkcs8B64) if err != nil { return nil, err } // This handles PKCS#8 parsing automatically key, err := x509.ParsePKCS8PrivateKey(pkcs8Bytes) if err != nil { return nil, err } // Type assert to ECDSA private key ecdsaKey, ok := key.(*ecdsa.PrivateKey) if !ok { return nil, fmt.Errorf("key provided is not an ECDSA private key") } return ecdsaKey, nil } // Utility function to verify the signature (for testing purposes) func VerifySignature(publicKey *ecdsa.PublicKey, payload, signatureB64 string) (bool, error) { // Decode the base64 signature signature, err := base64.StdEncoding.DecodeString(signatureB64) if err != nil { return false, fmt.Errorf("failed to decode signature: %w", err) } // Hash the payload hash := sha256.Sum256([]byte(payload)) // Verify the signature valid := ecdsa.VerifyASN1(publicKey, hash[:], signature) return valid, nil } ``` ```ruby theme={"system"} require "base64" require "json" require "openssl" # Replace this with your private key from the Dashboard PRIVY_AUTHORIZATION_KEY = "wallet-auth:your-authorization-private-key" # Sign the canonicalized JSON payload using ECDSA (P-256 + SHA-256). # # privy_authorization_key - A string containing your user key or authorization key. # payload - JSON payload to sign, serialized to a string # # Returns the base64-encoded DER signature. def sign_payload(privy_authorization_key, payload) private_key = parse_private_key_from_authorization_key(privy_authorization_key) # Hash the payload using SHA-256 and sign it digest = OpenSSL::Digest.new("SHA256").digest(payload) signature_der = private_key.dsa_sign_asn1(digest) # Base64 encode the signature Base64.strict_encode64(signature_der) end # Parse the ECDSA private key from the user or authorization key. def parse_private_key_from_authorization_key(privy_authorization_key) pkcs8_b64 = privy_authorization_key.sub("wallet-auth:", "") pkcs8_der = Base64.strict_decode64(pkcs8_b64) # OpenSSL handles PKCS#8 parsing automatically OpenSSL::PKey.read(pkcs8_der) end # JSON-canonicalize the payload with recursive key sorting and minimal separators. def canonicalize(value) case value when Hash body = value.keys.sort_by(&:to_s) .map { |k| "#{JSON.generate(k.to_s)}:#{canonicalize(value[k])}" } .join(",") "{#{body}}" when Array "[#{value.map { |v| canonicalize(v) }.join(',')}]" else JSON.generate(value) end end def get_authorization_signature(url:, body:) payload = { version: 1, method: "POST", url: url, body: body, headers: {"privy-app-id" => "insert-your-app-id"} } serialized_payload = canonicalize(payload) sign_payload(PRIVY_AUTHORIZATION_KEY, serialized_payload) end authorization_signature = get_authorization_signature( url: "https://api.privy.io/v1/wallets//rpc", body: { method: "personal_sign", params: {message: "Hello world"} } ) ``` # Signing requests Source: https://docs.privy.io/controls/authorization-keys/using-owners/sign/overview When updating resources like wallets, policies, or key quorums in the Privy API, requests must be signed by the resource owner in order to be authorized. When signing messages or sending transactions with a wallet, requests must be signed by the wallet owner or an additional signer whose policies allow for the signature or transaction. Learn more about the [abstractions](/controls/authorization-keys/using-owners/sign/overview#abstractions) that Privy offers to for request signing and the underlying [steps](/controls/authorization-keys/using-owners/sign/overview#steps) involved. ## Abstractions Privy offers several level of abstractions through SDKs to simplify the implementation of request signing. In order of highest-level to lowest-level, these abstractions are **automatic signing**, **utility functions**, and **direct implementation**. Wherever possible, we strongly recommend using a Privy SDKs' **automatic signing** functionality or **utility functions** to sign requests. Implementing request signing directly is an advanced integration. ### Automatic signing With automatic signing, Privy SDKs automatically handles producing signatures when making requests to the Privy API. This means your application does not directly need to handle any signing logic. Learn how to [use automatic signing](/controls/authorization-keys/using-owners/sign/signing-on-the-server) in your application. ### Utility functions If your application is unable to use automatic signing as part of Privy's SDKs, Privy's SDKs also offer utility functions for signature payload preparation and in-line signing. Using utility functions over automatic signing may be preferred if your authorization keys are secured in a separate service (e.g. KMS) and signing can only be executed within that service. Learn how to [use these utility functions](/controls/authorization-keys/using-owners/sign/utility-functions) in your application. Privy SDKs typically offer two utilities: * **Formatting requests for authorization signatures.** This accepts a request you intend to make to the Privy API and constructs the required payload for signing. * **Generating authorization signatures.** Given a formatted signature payload, this method accepts the private key for an authorization key or an authorization context generally and produces the corresponding signature over the payload. You can combine these utility functions with your own direct implementation of signing or a call out to an external signing service (e.g. AWS KMS) to generate your authorization signature. As an example, you might: 1. Construct your request payload 2. Use the Privy SDK's formatting requests function to generate your signature payload 3. Make a call out to your external signing service to sign the payload from step (2) 4. Include the signature in a `privy-authorization-signature` header for the request. ### Direct implementation Learn how to implement [direct signing](/controls/authorization-keys/using-owners/sign/direct-implementation) in your application. If your application cannot integrate one of Privy's SDKs, you can also directly implement request signing in your stack. This is an advanced integration; wherever possible, we recommend using Privy SDKs for request signing. ## Signature payload When signing a request to the Privy API, the payload to sign must be a JSON object containing the following fields: | Field | Type | Description | | | | | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | - | - | - | | `version` | `1` | Authorization signature version. Currently, `1` is the only version. | | | | | `method` | `'POST' \| 'PUT' \| 'PATCH' \| 'DELETE'` | HTTP method for the request. Signatures are not required on `'GET'` requests. | | | | | `url` | `string` | The full URL for the request. Should not include a trailing slash. | | | | | `body` | `JSON` | JSON body for the request. | | | | | `headers` | `JSON` | JSON object containing any Privy-specific headers, e.g. those that are prefixed with `'privy-'`. This should **not** include any other headers, such as authentication headers, `content-type`, or trace headers. | | | | | `headers['privy-app-id']` | `string` | Privy app ID header (required). | | | | | `headers['privy-idempotency-key']` | `string` | Privy idempotency key header (optional). If the request does not contain an idempotency key, leave this field out of the payload. | | | | | `headers['privy-request-expiry']` | `string` | Privy request expiry header (optional). If the request does not contain an expiry header, leave this field out of the payload. | | | | # Signing on the client Source: https://docs.privy.io/controls/authorization-keys/using-owners/sign/signing-on-the-client Privy's client-side SDKs offer abstractions that allow you to automatically sign requests to the Privy API when invoking SDK methods. With this level of abstraction, your application does not need to handle directly signing requests or including the signature in request headers. For user-owned wallets, Privy's client-side SDKs (React, React Native, iOS, etc.) automatically sign requests to the Privy API when invoking SDK methods. Under the hood, Privy's SDK will fetch an ephemeral user signing key, sign the request with it, and include the signature in the request headers when you invoke an SDK method. **When using Privy's client-side SDKs, you do not need to implement any additional logic to sign requests to the Privy API.** # Signing on the server Source: https://docs.privy.io/controls/authorization-keys/using-owners/sign/signing-on-the-server Privy's server-side SDKs offer an abstraction called the [authorization context](/controls/authorization-keys/using-owners/sign/signing-on-the-server) to enable automatic request signing. When an SDK method may require request signing (e.g. sending a transaction), your application can pass the authorization context to the SDK method with relevant inputs needed to sign the request. Concretely, this includes: * **Authorization private keys.** For authorization key-based signatures, the SDK will directly use these keys to compute P256 signatures over the request * **User JWTs.** For user-based signatures, the SDK will request user signing keys given the provided JWTs and compute P256 signatures over the request. * **Custom signing function.** If your application logic requires signing to occur in a separate service (e.g. KMS), you can pass a custom signing function that the SDK will invoke to sign requests. * **Signatures.** If you compute signatures in your application separately from calling Privy's SDK, you can pass these signatures directly into the authorization context. The SDK will compute all signatures given the parameters passed in the authorization context, and include all signatures in the underlying request to Privy's API. ## Using the authorization context At a high-level, there are two steps to using the authorization context. [Build the authorization context](/controls/authorization-keys/using-owners/sign/signing-on-the-server#1-build-the-authorization-context) with the private key(s), user(s), custom sign function that you'd like to sign your request. Include any signatures that you have already computed in the context as well. Once you've built the authorization context, [pass the populated context to SDK methods](/controls/authorization-keys/using-owners/sign/signing-on-the-server#2-pass-the-authorization-context-to-sdk-methods) that may require signatures, such as updating wallets or policies or sending transactions. ### 1. Build the authorization context To build the authorization context with your signing inputs, follow the instructions below depending on your setup. To sign a request with an authorization key, get the private key(s) that you saved locally when creating your signer in the Privy API or Dashboard. See the guide on [authorization keys](/controls/authorization-keys/keys/create/key) for more details. Then, add the private key(s) to the authorization context to automatically have them sign requests to the Privy API. ```java Java highlight={2} theme={"system"} AuthorizationContext authorizationContext = AuthorizationContext.builder() .addAuthorizationPrivateKey("authorization-key") .build(); ``` ```ts @privy-io/node highlight={2} theme={"system"} import {AuthorizationContext} from '@privy-io/node'; const authorizationContext: AuthorizationContext = { authorization_private_keys: ['authorization-key'] }; ``` ```rust Rust highlight={4} theme={"system"} use privy_rs::{AuthorizationContext, PrivateKey}; let ctx = AuthorizationContext::new().push( PrivateKey("authorization-key".to_string()) ); ``` ```go Go theme={"system"} import "github.com/privy-io/go-sdk/authorization" authCtx := &authorization.AuthorizationContext{ PrivateKeys: []string{"authorization-key"}, } ``` ```ruby Ruby highlight={2} theme={"system"} ctx = Privy::Authorization::AuthorizationContext.build( authorization_private_keys: ["authorization-key"] ) ``` To sign requests with a user, add the user's valid JWT to the authorization context. The SDK will automatically request a signing key for the user given the JWT and sign the request with it. See the guide on [user owners and signers](/controls/authorization-keys/keys/create/user/request) for more details. ```java Java highlight={2} theme={"system"} AuthorizationContext authorizationContext = AuthorizationContext.builder() .addUserJwt("user-jwt") .build(); ``` ```ts @privy-io/node highlight={2} theme={"system"} import {AuthorizationContext} from '@privy-io/node'; const authorizationContext: AuthorizationContext = { user_jwts: ['user-jwt'] }; ``` ```rust Rust highlight={4} theme={"system"} use privy_rs::{AuthorizationContext, JwtUser, PrivyClient}; let client = PrivyClient::new(app_id, app_secret)?; let jwt_user = JwtUser(client.clone(), "user-jwt".to_string()); let ctx = AuthorizationContext::new().push(jwt_user); ``` ```go Go theme={"system"} import "github.com/privy-io/go-sdk/authorization" authCtx := &authorization.AuthorizationContext{ UserJwts: []string{"user-jwt-token"}, } ``` ```ruby Ruby highlight={2} theme={"system"} ctx = Privy::Authorization::AuthorizationContext.build( user_jwts: ["user-jwt"] ) ``` In case you are not able to pass an authorization private key or user JWT to the authorization context directly, you can instead pass a custom signing function that the SDK will invoke to automatically sign requests. As an example, you might implement a custom signing function that calls out to a KMS where your authorization keys are secured and returns the necessary signature. The sign functions should perform an ECDSA P-256 signature on the payload received, and return the base64-encoded signature. ```java Java theme={"system"} // This feature is not yet supported in the Java SDK. ``` ```ts @privy-io/node highlight={1,9} {skip-check} theme={"system"} async function mySignFunction(payload: Uint8Array): Promise { // Perform an ECDSA P-256 signature on the payload // This is an example using a fictitious KMS API call. const signature = await kms.sign(payload); return signature; // This should be a base64-encoded string } const authorizationContext: AuthorizationContext = { sign_functions: [mySignFunction] }; ``` ```rust Rust theme={"system"} //! This newtype is just a convenience blanket implementation. //! You can of course just implement the trait for your own types. //! //! See the API reference for details: //! https://docs.rs/privy_rs/latest/privy_rs/trait.IntoSignature.html //! https://docs.rs/privy_rs/latest/privy_rs/trait.IntoKey.html use privy_rs::{AuthorizationContext, FnSigner}; // Create a custom signer using a closure with FnSigner wrapper let custom_signer = FnSigner(|message: &[u8]| async move { // Perform an ECDSA P-256 signature on the payload // This is an example using a fictitious KMS API call. let signature_bytes = kms_sign(message).await?; let signature_b64 = general_purpose::STANDARD.encode(&signature_bytes); Ok(Signature { signature: signature_b64, key_id: "custom-key".to_string() }) }); let ctx = AuthorizationContext::new().push(custom_signer); ``` ```go Go theme={"system"} import "github.com/privy-io/go-sdk/authorization" // Implement the AuthorizationSigner interface type MySigner struct{} func (s *MySigner) Sign(ctx context.Context, payload []byte) (string, error) { // Custom signing logic return "signature", nil } authCtx := &authorization.AuthorizationContext{ Signers: []authorization.AuthorizationSigner{&MySigner{}}, } ``` ```ruby Ruby theme={"system"} sign_fn = ->(payload) { # Perform an ECDSA P-256 signature on the payload # This is an example using a fictitious KMS API call. kms.sign(payload) # This should return a base64-encoded string } ctx = Privy::Authorization::AuthorizationContext.build(sign_fns: [sign_fn]) ``` The binary payload received by the sign function is already formatted and ready to be signed. There is no need to canonicalize or serialize the payload before signing when using this method. You may combine the different signing mechanisms in the authorization context to produce a fully customizable key quorum. For instance: * You may want to keep a wallet under control of both a user and an authorization key, requiring both signatures to authorize an action. This would be a 2-of-2 key quorum, and can be built by combining both the "user jwt" and "authorization private key" properties, as shown below. * You may want to keep a wallet under control of several authorization keys. You can build the required authorization context by passing all authorization private keys into the "authorization private key" property. ```java Java theme={"system"} // Example: A 2-of-2 key quorum, of a user and an authorization private key AuthorizationContext authorizationContext = AuthorizationContext.builder() .addUserJwt("user-jwt") .addAuthorizationPrivateKey("authorization-key") .build(); ``` ```ts @privy-io/node theme={"system"} import {AuthorizationContext} from '@privy-io/node'; // Example: A 2-of-2 key quorum, of a user and an authorization private key const authorizationContext: AuthorizationContext = { user_jwts: ['user-jwt'], authorization_private_keys: ['authorization-key'] }; ``` ```rust Rust theme={"system"} // Example: A 2-of-2 key quorum, of a user and an authorization private key use privy_rs::{AuthorizationContext, JwtUser, PrivateKey, PrivyClient}; let client = PrivyClient::new(app_id, app_secret)?; // Add user JWT for user-based authorization let jwt_user = JwtUser(client.clone(), "user-jwt".to_string()); // Add private key for authorization key-based signing let auth_key = PrivateKey("authorization-key".to_string()); let ctx = AuthorizationContext::new() .push(jwt_user) .push(auth_key); ``` ```go Go theme={"system"} import "github.com/privy-io/go-sdk/authorization" authCtx := &authorization.AuthorizationContext{ PrivateKeys: []string{"authorization-key"}, UserJwts: []string{"user-jwt-token"}, } ``` ```ruby Ruby theme={"system"} # Example: A 2-of-2 key quorum, of a user and an authorization private key ctx = Privy::Authorization::AuthorizationContext.build( user_jwts: ["user-jwt"], authorization_private_keys: ["authorization-key"] ) ``` If your application computes the signature directly separately from the SDK, you can pass signatures directly into the authorization context. ```java Java highlight={2} theme={"system"} AuthorizationContext authorizationContext = AuthorizationContext.builder() .addSignature("signature-you-produced") .build(); ``` ```ts @privy-io/node highlight={3,5} theme={"system"} import {AuthorizationContext} from '@privy-io/node'; const authorizationContext: AuthorizationContext = { signatures: ['signature-you-produced'] }; ``` ```rust Rust highlight={3,13,18} theme={"system"} //! `p256::ecdsa::Signature` implements `IntoSignature`, so //! you can push it directly to the authorization context. use privy_rs::AuthorizationContext; use p256::{ecdsa::Signature, generic_array::GenericArray}; use base64::{Engine as _, engine::general_purpose}; let ctx = AuthorizationContext::new(); // Option 1: Add a pre-computed signature from bytes let signature_bytes = general_purpose::STANDARD.decode("your-base64-signature")?; let signature = Signature::from_bytes(GenericArray::from_slice(&signature_bytes))?; let ctx = ctx.push(signature); // Option 2: Add a signature from DER format let der_bytes = &[/* your DER encoded signature bytes */]; let signature = Signature::from_der(der_bytes)?; let ctx = ctx.push(signature); ``` ```go Go theme={"system"} import "github.com/privy-io/go-sdk/authorization" authCtx := &authorization.AuthorizationContext{ Signatures: []string{"pre-computed-signature"}, } // Pass to SDK methods via: privy.WithAuthorizationContext(authCtx) ``` ```ruby Ruby highlight={2} theme={"system"} ctx = Privy::Authorization::AuthorizationContext.build( signatures: ["signature-you-produced"] ) ``` ### 2. Pass the authorization context to SDK methods Once you have built your authorization context, pass the context as a parameter to the SDK method that requires request signing. As an example, to send a request to sign a message that needs to be signed by an authorization context: ```java Java theme={"system"} String message = "Hello, Ethereum."; // Example: If wallet's owner is an authorization private key AuthorizationContext authorizationContext = AuthorizationContext.builder() .addAuthorizationPrivateKey("authorization-key") .build(); EthereumPersonalSignRpcResponseData response = privyClient .wallets() .ethereum() .signMessage( walletId, message.getBytes(StandardCharsets.UTF_8), authorizationContext ); String signature = response.signature(); ``` ```ts @privy-io/node theme={"system"} import {PrivyClient, type AuthorizationContext} from '@privy-io/node'; const privy = new PrivyClient({ appId: 'insert-your-app-id', appSecret: 'insert-your-app-secret' }); // Build your authorization context per step (1) above const authorizationContext: AuthorizationContext = { authorization_private_keys: ['authorization-key'] }; // Pass the authorization context to the SDK method as the `authorization_context` parameter const response = await privy.wallets().ethereum().signMessage('insert-wallet-id', { message: 'Hello, world!', authorization_context: authorizationContext }); ``` ```rust Rust theme={"system"} let ethereum_service = client.wallets().ethereum(); let auth_ctx = AuthorizationContext::new(); let signature = ethereum_service .sign_message( &wallet_id, "Hello, Ethereum!", &auth_ctx, Some("unique-request-id-123"), ) .await?; println!("Message signed successfully"); ``` ```go Go theme={"system"} import ( privy "github.com/privy-io/go-sdk" "github.com/privy-io/go-sdk/authorization" ) authCtx := &authorization.AuthorizationContext{ PrivateKeys: []string{"authorization-key"}, } response, err := client.Wallets.Ethereum.SignMessage( context.Background(), walletID, "Hello, Privy!", privy.WithAuthorizationContext(authCtx), ) ``` ```ruby Ruby theme={"system"} # Build your authorization context per step (1) above ctx = Privy::Authorization::AuthorizationContext.build( authorization_private_keys: ["authorization-key"] ) # Pass the authorization context to the SDK method as the `authorization_context` parameter response = client.wallets.rpc( "insert-wallet-id", wallet_rpc_request_body: { method: "personal_sign", chain_type: "ethereum", params: {message: "Hello, world!", encoding: "utf-8"} }, authorization_context: ctx ) ``` The SDK will sign the request given the authorization context and automatically include the signature in the underlying request to the Privy API. # Signing with utility functions Source: https://docs.privy.io/controls/authorization-keys/using-owners/sign/utility-functions If your integration cannot leverage the [automatic signing](/controls/authorization-keys/using-owners/sign/signing-on-the-server) capabilities of Privy's SDKs, Privy also offers utility functions for formatting and signing requests. You can use these functions to sign requests, even without initializing an instance of the Privy SDK, and then can manually include the returned signature in requests to the Privy API. ## Client-side SDKs There are two ways to sign requests on the client: 1. **Sign a server-formatted binary payload (recommended)** — Your server formats the request into canonical bytes using the server SDK, sends the bytes to the client, and the client signs them directly. This gives the server full control over payload construction and allows server-side changes without requiring client SDK updates. 2. **Sign a structured payload** — The client constructs the payload locally and signs it. This approach is simpler for prototyping but couples the client to the payload format. ### Signing a server-formatted binary payload (recommended) In this approach, your server serializes the API request into canonical bytes using Privy's server SDK. The client receives these bytes and signs them directly, without needing to understand the payload structure. This is the recommended approach because: * Your server has full control over constructing the correct payload * Server-side updates to the payload format do not require client SDK changes * The client does not need to construct or serialize the payload itself Use the server SDK's `formatRequestForAuthorizationSignature` function to serialize the request into bytes. See [Formatting requests](#formatting-requests) below for the full reference. ```ts theme={"system"} import {formatRequestForAuthorizationSignature} from '@privy-io/node'; const serializedPayload = formatRequestForAuthorizationSignature({ version: 1, url: 'https://api.privy.io/v1/wallets//rpc', method: 'POST', headers: {'privy-app-id': ''}, body: { method: 'personal_sign', params: {message: 'Hello from Privy!', encoding: 'utf-8'} } }); // Send the bytes to your client as base64 const payloadBase64 = Buffer.from(serializedPayload).toString('base64'); ``` Send the base64-encoded bytes from your server to the client (e.g., as a JSON response field). Decode the base64 payload and pass the raw bytes to `generateAuthorizationSignature`. ```tsx theme={"system"} import {useAuthorizationSignature} from '@privy-io/react-auth'; const {generateAuthorizationSignature} = useAuthorizationSignature(); // Decode the base64 payload received from the server const payloadBytes = Uint8Array.from(atob(payloadBase64), (c) => c.charCodeAt(0)); // Sign the binary payload const {signature} = await generateAuthorizationSignature(payloadBytes); ``` ```tsx theme={"system"} import {useAuthorizationSignature} from '@privy-io/expo'; import {Buffer} from 'buffer'; const {generateAuthorizationSignature} = useAuthorizationSignature(); // Decode the base64 payload received from the server const payloadBytes = new Uint8Array(Buffer.from(payloadBase64, 'base64')); // Sign the binary payload const {signature} = await generateAuthorizationSignature(payloadBytes); ``` ```swift theme={"system"} guard let user = try await privy.getUser() else { return } // Decode the base64 payload received from the server guard let payloadData = Data(base64Encoded: payloadBase64) else { return } // Sign the binary payload let signature = try await user.generateAuthorizationSignature(payload: payloadData) ``` ```kotlin theme={"system"} import kotlin.io.encoding.Base64 import kotlin.io.encoding.ExperimentalEncodingApi val user = privy.getUser() ?: return // Decode the base64 payload received from the server @OptIn(ExperimentalEncodingApi::class) val payloadBytes = Base64.decode(payloadBase64) // Sign the binary payload user.generateAuthorizationSignature(payloadBytes) .onSuccess { signature -> // Use signature in API request headers as 'privy-authorization-signature' } .onFailure { error -> // Handle error } ``` ```csharp theme={"system"} using System; using Privy.Auth.Models; IPrivyUser user = await PrivyManager.Instance.GetUser(); // Decode the base64 payload received from the server byte[] payloadBytes = Convert.FromBase64String(payloadBase64); // Sign the binary payload string signature = await user.GenerateAuthorizationSignature(payloadBytes); ``` Return the base64-encoded signature string to your server. From your server, include the signature when making the request to the Privy API. You can pass it via the Node SDK's `authorization_context.signatures` field, or include it directly as the `privy-authorization-signature` header. ```ts theme={"system"} import {PrivyClient} from '@privy-io/node'; const client = new PrivyClient({appId: '', appSecret: ''}); // Pass the client-generated signature in authorization_context const response = await client.wallets().rpc('', { method: 'personal_sign', params: {message: 'Hello from Privy!', encoding: 'utf-8'}, authorization_context: {signatures: [signature]} }); ``` ```ts theme={"system"} fetch('https://api.privy.io/v1/wallets//rpc', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Basic ${btoa(':')}`, 'privy-app-id': '', 'privy-authorization-signature': signature }, body: JSON.stringify({ method: 'personal_sign', params: {message: 'Hello from Privy!', encoding: 'utf-8'} }) }); ``` ### Signing a structured payload Alternatively, the client can construct the signature payload locally and sign it directly. The SDK canonicalizes the payload to JSON (RFC 8785) before signing. #### 1. Construct your signature payload Given your desired request to the Privy API, build a JSON payload with the following fields. Your application will sign this entire payload to authorize the request to the Privy API. | Field | Type | Description | | | | | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | - | - | - | | `version` | `1` | Authorization signature version. Currently, `1` is the only version. | | | | | `method` | `'POST' \| 'PUT' \| 'PATCH' \| 'DELETE'` | HTTP method for the request. Signatures are not required on `'GET'` requests. | | | | | `url` | `string` | The full URL for the request. Should not include a trailing slash. | | | | | `body` | `JSON` | JSON body for the request. | | | | | `headers` | `JSON` | JSON object containing any Privy-specific headers, e.g. those that are prefixed with `'privy-'`. This should **not** include any other headers, such as authentication headers, `content-type`, or trace headers. | | | | | `headers['privy-app-id']` | `string` | Privy app ID header (required). | | | | | `headers['privy-idempotency-key']` | `string` | Privy idempotency key header (optional). If the request does not contain an idempotency key, leave this field out of the payload. | | | | | `headers['privy-request-expiry']` | `string` | Privy request expiry header (optional). If the request does not contain an expiry header, leave this field out of the payload. | | | | As an example, you might build a payload for an Ethereum `personal_sign` RPC request like so: ```ts theme={"system"} const signaturePayload = { version: 1, url: 'https://api.privy.io/v1/wallets//rpc', method: 'POST', headers: { 'privy-app-id': '' }, body: { method: 'personal_sign', params: { message: 'Hello from Privy!', encoding: 'utf-8' } } } as const; ``` ```swift theme={"system"} import PrivySDK // Define structs for the request body (must conform to Encodable) struct PersonalSignParams: Encodable { let message: String let encoding: String } struct PersonalSignRpcRequest: Encodable { let method: String let params: PersonalSignParams } // Create the RPC request body let rpcRequest = PersonalSignRpcRequest( method: "personal_sign", params: PersonalSignParams(message: "Hello from Privy!", encoding: "utf-8") ) // Create the signature payload using WalletApiPayload let signaturePayload = WalletApiPayload( version: 1, url: "https://api.privy.io/v1/wallets//rpc", method: "POST", headers: ["privy-app-id": ""], body: rpcRequest ) ``` ### WalletApiPayload parameters The payload version. Currently, only version `1` is supported. The full URL of the API endpoint, including protocol and domain. Should not include a trailing slash. The HTTP method for the request (e.g., `"POST"`, `"PUT"`, `"PATCH"`, `"DELETE"`). Privy-specific headers (those prefixed with `privy-`). This should not include authentication headers, content-type, or trace headers. The `privy-app-id` header is always required. The request body to be serialized and included in the signature. Must conform to `Encodable`. The request body type and all nested types must be annotated with `@Serializable` from `kotlinx.serialization`. ```kotlin theme={"system"} import io.privy.wallet.walletApi.WalletApiPayload import kotlinx.serialization.Serializable // Define data classes for the request body (must be @Serializable) @Serializable data class PersonalSignParams( val message: String, val encoding: String ) @Serializable data class PersonalSignRpcRequest( val method: String, val params: PersonalSignParams ) // Create the RPC request body val rpcRequest = PersonalSignRpcRequest( method = "personal_sign", params = PersonalSignParams(message = "Hello from Privy!", encoding = "utf-8") ) // Create the signature payload using WalletApiPayload val signaturePayload = WalletApiPayload( version = 1, url = "https://api.privy.io/v1/wallets//rpc", method = "POST", headers = mapOf("privy-app-id" to ""), body = rpcRequest ) ``` ### WalletApiPayload parameters The payload version. Currently, only version `1` is supported. The full URL of the API endpoint, including protocol and domain. Should not include a trailing slash. The HTTP method for the request (e.g., `"POST"`, `"PUT"`, `"PATCH"`, `"DELETE"`). Privy-specific headers (those prefixed with `privy-`). This should not include authentication headers, content-type, or trace headers. The `privy-app-id` header is always required. The request body to be serialized and included in the signature. Must be annotated with `@Serializable`. In Flutter, the body is passed as a `Map` — no special serialization annotations are needed. ```dart theme={"system"} import 'package:privy_flutter/privy_flutter.dart'; // Create the RPC request body as a Map final rpcRequestBody = { 'method': 'personal_sign', 'params': { 'message': 'Hello from Privy!', 'encoding': 'utf-8', }, }; // Create the signature payload final signaturePayload = WalletApiPayload( version: 1, url: 'https://api.privy.io/v1/wallets//rpc', method: 'POST', headers: {'privy-app-id': ''}, body: rpcRequestBody, ); ``` ### WalletApiPayload parameters The payload version. Currently, only version `1` is supported. The full URL of the API endpoint, including protocol and domain. Should not include a trailing slash. The HTTP method for the request (e.g., `"POST"`, `"PUT"`, `"PATCH"`, `"DELETE"`). Privy-specific headers (those prefixed with `privy-`). This should not include authentication headers, content-type, or trace headers. The `privy-app-id` header is always required. The request body as a JSON-serializable map. Serialized automatically before signing. In Unity, the body is passed as an `object` — any JSON-serializable type works (Newtonsoft.Json handles serialization). ```csharp theme={"system"} using System.Collections.Generic; using Privy.Wallets; // Create the signature payload var signaturePayload = new WalletApiPayload { Version = 1, Url = "https://api.privy.io/v1/wallets//rpc", Method = "POST", Headers = new Dictionary { { "privy-app-id", "" } }, Body = new { method = "personal_sign", @params = new { message = "Hello from Privy!", encoding = "utf-8" } } }; ``` ### WalletApiPayload parameters The payload version. Currently, only version `1` is supported. The full URL of the API endpoint, including protocol and domain. Should not include a trailing slash. The HTTP method for the request (e.g., `"POST"`, `"PUT"`, `"PATCH"`, `"DELETE"`). Privy-specific headers (those prefixed with `privy-`). This should not include authentication headers, content-type, or trace headers. The `privy-app-id` header is always required. The request body. Must be JSON-serializable via Newtonsoft.Json. #### 2. Sign your request Next, use the SDK's `generateAuthorizationSignature` method to sign the request. Pass the payload from step (1) as a parameter to this method. The method will sign the request with the current authenticated user's signing key, and return the base64-encoded signature. ```tsx theme={"system"} import {useAuthorizationSignature} from '@privy-io/react-auth'; const {generateAuthorizationSignature} = useAuthorizationSignature(); // Sign the request using the current authenticated user's signing key. // The `signaturePayload` here refers to the JSON payload constructed in step (1). const authorizationSignature = await generateAuthorizationSignature(signaturePayload); ``` ```tsx theme={"system"} import {useAuthorizationSignature} from '@privy-io/expo'; const {generateAuthorizationSignature} = useAuthorizationSignature(); // Sign the request using the current authenticated user's signing key. // The `signaturePayload` here refers to the JSON payload constructed in step (1). const authorizationSignature = await generateAuthorizationSignature(signaturePayload); ``` Use the `generateAuthorizationSignature` method on the `PrivyUser` object to sign the request. ### Usage ```swift theme={"system"} // Get the authenticated user guard let user = try await privy.getUser() else { // User is not authenticated return } // Sign the request using the payload from step (1) do { let authorizationSignature = try await user.generateAuthorizationSignature(payload: signaturePayload) // Use signature in API request headers as 'privy-authorization-signature' } catch { // Handle error } ``` ### Returns The cryptographic signature as a base64-encoded `String` that can be included in Privy API requests as the `privy-authorization-signature` header. Use the `generateAuthorizationSignature` method on the `PrivyUser` object to sign the request. ### Usage ```kotlin theme={"system"} val user = privy.getUser() ?: return // Sign the request using the payload from step (1) user.generateAuthorizationSignature(signaturePayload) .onSuccess { authorizationSignature -> // Use signature in API request headers as 'privy-authorization-signature' } .onFailure { error -> // Handle error } ``` ### Returns The cryptographic signature as a base64-encoded `String` that can be included in Privy API requests as the `privy-authorization-signature` header. Use the `generateAuthorizationSignature` method on the `PrivyUser` object to sign the request. ### Usage ```dart theme={"system"} final user = await privy.getUser(); if (user == null) return; // Sign the request using the payload from step (1) final result = await user.generateAuthorizationSignature(signaturePayload); result.fold( onSuccess: (signature) { // Use signature in API request headers as 'privy-authorization-signature' print('Signature: $signature'); }, onFailure: (error) { // Handle error print('Error: $error'); }, ); ``` ### Returns The cryptographic signature as a base64-encoded `String` that can be included in Privy API requests as the `privy-authorization-signature` header. Use the `GenerateAuthorizationSignature` method on the `IPrivyUser` object to sign the request. ### Usage ```csharp theme={"system"} using Privy.Core; using Privy.Auth.Models; IPrivyUser user = await PrivyManager.Instance.GetUser(); // Sign the request using the payload from step (1) string signature = await user.GenerateAuthorizationSignature(signaturePayload); // Use signature in API request headers as 'privy-authorization-signature' ``` ### Returns The cryptographic signature as a base64-encoded `string` that can be included in Privy API requests as the `privy-authorization-signature` header. #### 3. Send the request and signature to your backend Next, make a request from your frontend to your backend including the request you intend to make to the Privy API and the corresponding signature from step (2). Your backend will proxy this request to the Privy API. #### 4. Send the request to the Privy API Finally, make your request to the Privy API and include the signature in the `privy-authorization-signature` header for your request. As an example, in NodeJS, you can make the request like so: ```ts theme={"system"} fetch('https://api.privy.io/v1/wallets//rpc', { method: 'POST', headers: { 'Content-Type': 'application/json', Authorization: `Basic ${btoa(':')}`, 'privy-app-id': '', 'privy-authorization-signature': '' }, body: JSON.stringify({ method: 'personal_sign', params: { message: 'Hello from Privy!', encoding: 'utf-8' } }) }) .then((res) => console.log(res)) .catch((err) => console.error(err)); ``` ## Server-side SDKs Privy's server SDKs offer two utilities for signing requests: * **Formatting requests for authorization signatures.** This accepts your desired request to the Privy API and formats it into the required signature payload to be signed. * This utility is particularly helpful if your application signs requests via a separate service, e.g. an isolated KMS. Your primary server can format your request and generate the signature payload and call out to your signing service with the payload. * **Generating authorization signatures.** This accepts a formatted signature payload and signs it with your provided signing key. * This utility is particularly useful within a specific signing service. Within your signing service, you can import this function and use it to sign requests, and return the signature to your primary service. ### Constructing your input Both the formatting and signing functions of Privy's SDKs require a JSON input with the following fields: | Field | Type | Description | | | | | ---------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | - | - | - | | `version` | `1` | Authorization signature version. Currently, `1` is the only version. | | | | | `method` | `'POST' \| 'PUT' \| 'PATCH' \| 'DELETE'` | HTTP method for the request. Signatures are not required on `'GET'` requests. | | | | | `url` | `string` | The full URL for the request. Should not include a trailing slash. | | | | | `body` | `JSON` | JSON body for the request. | | | | | `headers` | `JSON` | JSON object containing any Privy-specific headers, e.g. those that are prefixed with `'privy-'`. This should **not** include any other headers, such as authentication headers, `content-type`, or trace headers. | | | | | `headers['privy-app-id']` | `string` | Privy app ID header (required). | | | | | `headers['privy-idempotency-key']` | `string` | Privy idempotency key header (optional). If the request does not contain an idempotency key, leave this field out of the payload. | | | | | `headers['privy-request-expiry']` | `string` | Privy request expiry header (optional). If the request does not contain an expiry header, leave this field out of the payload. | | | | ### Formatting requests Use the SDK's formatting function to generate your signature payload. As a parameter to this function, pass the JSON object as defined above. ```ts NodeJS theme={"system"} import { formatRequestForAuthorizationSignature, type WalletApiRequestSignatureInput } from '@privy-io/node'; // Replace this with your desired request to the Privy API, including // url, method, headers, and body. const input: WalletApiRequestSignatureInput = { version: 1, url: 'https://api.privy.io/v1/wallets//rpc', method: 'POST', headers: { 'privy-app-id': '' }, body: { method: 'personal_sign', params: { message: 'Hello from Privy!', encoding: 'utf-8' } } }; const serializedPayload = formatRequestForAuthorizationSignature(input); ``` ```java Java theme={"system"} // Build the request, in this case an ethereum personal_sign request EthereumPersonalSignRpcInputParams params = EthereumPersonalSignRpcInputParams.builder() .message(message) .encoding(Encoding.of(EncodingUtf8.UTF8)) .build(); EthereumPersonalSignRpcInput request = EthereumPersonalSignRpcInput.builder() .method(EthereumPersonalSignRpcInputMethod.PERSONAL_SIGN) .params(params) .build(); byte[] serializedPayload = privyClient.utils() .requestFormatter() .formatRequestForAuthorizationSignature( new WalletApiRequestSignatureInput( 1, request, HttpMethod.POST, "https://api.privy.io/wallets//rpc", null ) ); ``` ```rust Rust theme={"system"} use privy_rs::format_request_for_authorization_signature; use privy_rs::generated::types::*; // Build the request, in this case an ethereum personal_sign request let params = EthereumPersonalSignRpcInputParams { message: "Hello from Privy!".to_string(), encoding: Some(EthereumPersonalSignRpcInputParamsEncoding::Utf8), }; let request = EthereumPersonalSignRpcInput { method: EthereumPersonalSignRpcInputMethod::PersonalSign, params, }; let encoded_request_payload = format_request_for_authorization_signature( &app_id, crate::Method::POST, "https://api.privy.io/wallets//rpc".to_string(), &request, None, )?; ``` You can then take the returned serialized payload and call out to a signing service to generate a P256 signature over the payload. ### Signing requests To directly produce a signature over a request, use the SDK's generate authorization signature method. As a parameter to this method, pass the JSON object as defined above. ```ts Node theme={"system"} import {generateAuthorizationSignature, type WalletApiRequestSignatureInput} from '@privy-io/node'; // Replace this with your desired request to the Privy API, including // url, method, headers, and body. const input: WalletApiRequestSignatureInput = { version: 1, url: 'https://api.privy.io/v1/wallets//rpc', method: 'POST', headers: { 'privy-app-id': '' }, body: { method: 'personal_sign', params: { message: 'Hello from Privy!', encoding: 'utf-8' } } }; // Pass your base64 encoded authorization private key as `authorizationPrivateKey` const signature = generateAuthorizationSignature({ input, authorizationPrivateKey: 'insert-private-key' }); ``` ```java Java theme={"system"} // Build the request, in this case an ethereum personal_sign request EthereumPersonalSignRpcInputParams params = EthereumPersonalSignRpcInputParams.builder() .message(message) .encoding(Encoding.of(EncodingUtf8.UTF8)) .build(); EthereumPersonalSignRpcInput request = EthereumPersonalSignRpcInput.builder() .method(EthereumPersonalSignRpcInputMethod.PERSONAL_SIGN) .params(params) .build(); String signature = privyClient.utils() .requestSigner() .generateAuthorizationSignature( "authorization-key", new WalletApiRequestSignatureInput( 1, request, HttpMethod.POST, "https://api.privy.io/wallets//rpc", null ) ); ``` ```rust Rust theme={"system"} use privy_rs::generate_authorization_signatures; let ctx = AuthorizationContext::new().push(key); let body = serde_json::json!({"test": "data"}); // Returns a list of signatures given the authorization context let sig = generate_authorization_signatures( ctx, &self.app_id, crate::Method::PATCH, format!("{}/v1/policies/{}", self.base_url, policy_id.as_str()), body, "idempotency_key".to_string(), ) .await?; ``` This will return a base64-encoded signature over the payload you defined. Include this signature as the `privy-authorization-signature` header when making the request to the Privy API. # Delegating permissions Source: https://docs.privy.io/controls/common-use-cases/delegation Privy wallets' powerful [**owners and signers**](/controls/authorization-keys/owners/overview) abstraction allow your application to configure granular permissions around the actions that various parties can take on wallets. Namely, **owners** have full control over wallets and can delegate permissions to **signers** to execute transactions from the wallet within the scope of a specific [**policy**](/controls/policies/overview). delegate A good rule of thumb is: * If you need **third-parties to take actions on behalf of your business**, configure your business as the wallet's owner and each of the third-parties as a signer. * If you need **your business to take actions on behalf of a third-party or a user**, configure the third-party as the wallet's owner and your business as a signer. Owners and signers can be configured flexibly, including support for unilateral or quorum approvals. Learn more about delegating permissions with signers below. Use owners and signers to enforce granular permissions and control models in your application. # Quorum approvals Source: https://docs.privy.io/controls/common-use-cases/quorum-approval If your business needs multiple parties to be able to approve updates to or actions taken by wallets, the most common setup is to set up a [key quorum](/controls/key-quorum/overview) consisting of a set of multiple [authorization keys](/controls/authorization-keys/keys/create/key) or [users](/controls/authorization-keys/keys/create/user/overview) in your authentication system. quorum approval You can define the quorum such that a certain number of members of the quorum must approve actions to wallets. This is known as the quorum's **authorization threshold**. Privy's TEE infrastructure enforces that at least that many members of the quorum must sign the request to take an action with a wallet. To allow multiple parties to unilaterally approve wallet actions, you can set this threshold to 1. Quorum approvals allow your app to create setups where multiple parties must sign-off on actions taken by wallets, enhancing security for sensitive operations. Key quorums can also include other key quorums as members (one level deep). This enables hierarchical approval structures. For example, a "Security Team" quorum can nest inside a broader "Org Admins" quorum. Once the nested quorum meets its own authorization threshold, it counts as one approval toward the parent quorum's threshold. Learn more about configuring quorum approvals below. Use key quorums to configure quorum approvals on wallet actions. # Single-party approvals Source: https://docs.privy.io/controls/common-use-cases/single-party-approval If your business needs a single party to be able to unilaterally approve actions applied to or taken by wallets, the most common setup is to assign an [authorization key](/controls/authorization-keys/keys/create/key) or a [user](/controls/authorization-keys/keys/create/user/overview) in your authentication system as the [owner](/controls/authorization-keys/owners/overview) of the wallet. single party approval Privy's TEE infrastructure enforces that all requests to update a wallet (e.g. assign policies or delegate permissions to signers) or take actions with the wallet (e.g. sign messages or send transactions) must be **signed** by the authorization key or a time-bound key associated with your user. If no valid signature is provided on requests, Privy will not execute the action. Learn more about setting up user-owned wallets below. Learn how to set up non-custodial wallets owned by users in your authentication system. # Review and approve intents Source: https://docs.privy.io/controls/dashboard/approvals After a team member proposes an intent, reviewers in the corresponding key quorum can review and approve it in the Privy Dashboard. To view intents, visit the [Approvals](https://dashboard.privy.io/apps?page=approvals) page. The page lists all intents, their type, corresponding resource, and approval progress. images/manual-approvals-splash.png ### Proposing intents from the Dashboard Team members can propose intents directly in the Dashboard. Once proposed, an intent enters the review queue for the assigned key quorum. * **Transfer funds:** On the [Wallets](https://dashboard.privy.io/apps?page=wallets) page, click **Transfer**, choose a source wallet, destination address, token, chain, and amount, then submit. * **Update a wallet:** On the [Wallets](https://dashboard.privy.io/apps?page=wallets) page, select a wallet, click **Update wallet**, make changes, and select **Propose changes**. * **Update a policy or its rules:** On the [Policies](https://dashboard.privy.io/apps?page=policies) page, select a policy or its rules, make changes, and select **Propose changes**. * **Update a key quorum:** On the [Authorization](https://dashboard.privy.io/apps?page=authorization-keys) page, select a key quorum, click **Update key quorum**, make changes, and select **Propose changes**. To propose intents via the API--including RPC transactions, which the Dashboard does not support––[view the guides for creating intents](/transaction-management/intents/create/execute-rpc). ### Reviewing and approving intents Open the **Pending** tab to find intents awaiting review. Select an intent to view its details: who proposed it, what resource it affects, when it expires, and how many reviewers have approved so far. The intent detail view also shows a preview of the proposed action: * For wallet and policy updates, a diff comparing the current state to the proposed state. * For signatures and transactions, a preview of the transaction to sign and/or broadcast. Once a reviewer has carefully inspected the proposal and confirmed the changes, they can click the **Approve** button and complete MFA to submit their approval. Review an intent carefully before approving. Approvals cannot be revoked after submission. Under the hood, each approval generates an [authorization signature](/api-reference/authorization-signatures) over the request. Privy accumulates signatures and executes the intent once the threshold is met. #### Intent execution If the latest approval meets the authorization threshold, **the intent executes automatically as soon as the approval is granted.** For example, if your app creates an intent to execute a transaction with a wallet that is owned by a key quorum with an authorization threshold of 3, and 2 approvals have already been provided, Privy will automatically execute the transaction upon the 3rd approval. ### Rejecting intents For intents proposed via the Dashboard, the creator can reject it from the [Approvals](https://dashboard.privy.io/apps?page=approvals) page by selecting **Cancel proposal**. This prevents other team members from approving the intent. Any intent proposed via the REST API can be deleted by any team member from the [Approvals](https://dashboard.privy.io/apps?page=approvals) page using **Cancel proposal**. ### Viewing intents The [Approvals](https://dashboard.privy.io/apps?page=approvals) page shows all intents for the app, not just those pending review. Click the link icon at the top of an intent modal to copy a deeplink. Share this link with other team members for easy access. Learn more about the lifecycle of an intent. # Create a key quorum Source: https://docs.privy.io/controls/dashboard/key-quorum To enable manual approvals, first create a [key quorum](/controls/key-quorum/overview) of team members who serve as reviewers. Assign this group as an owner or signer on resources so that proposed changes, signatures, and transactions require their approval before taking effect. Invite team members from the [Account](https://dashboard.privy.io/account) page of the Dashboard. Give each member the **Developer** or **Admin** role so they can be enrolled in a key quorum. Each key quorum member **must** set up biometric or TOTP MFA for their Dashboard account. Team members enroll in MFA by clicking the profile icon at the bottom left of the Dashboard, selecting **Account preferences**, then clicking **MFA enrollment**. images/dashboard-mfa-1.png Visit the [Authorization](https://dashboard.privy.io/apps?page=authorization-keys) page and click **New key**. In the modal, select **Register key quorum**. Set a **Name** for the quorum and select members from the **Team members** dropdown. Then set the **Authorization threshold** -- the number of reviewers who must approve an intent before it executes. images/create-key-quorum.png When creating wallets or policies that require manual approval, set the new key quorum as the `owner`. This assigns the key quorum as the resource's Owner, requiring its members to review and approve any proposed updates, signatures, or transactions. Separately, this key quorum can be set as a Signer on a wallet, enabling that quorum to sign and send transactions. images/create-wallet-with-owner.png Alternatively, set the `owner_id` on a resource via the API when [creating a wallet](/api-reference/wallets/create) or [creating a policy](/api-reference/policies/create). ## Next steps Propose intents to authorize a transaction or update a wallet or policy. Approve or reject intents in the Privy Dashboard. Learn more about the lifecycle of an intent. # Manual approvals Source: https://docs.privy.io/controls/dashboard/overview Manual approvals add a human review step before sensitive actions take effect. Team members can review, authorize, or reject proposed changes to wallets or policies or proposed transactions in the Privy Dashboard. Manual approvals is an Enterprise feature. Reach out to [sales@privy.io](mailto:sales@privy.io) to request access for your app. All authorizations performed through the Privy Dashboard are secured by biometric and/or TOTP MFA. ## How it works To set up manual approvals for your Privy account: Create a [key quorum](/controls/dashboard/key-quorum) of team members from your Privy account in the Dashboard. Configure the members of the group and the approval threshold needed to reach consensus. This group of reviewers can later approve or reject proposed intents such as wallet or policy updates, signatures, and transactions via the Privy Dashboard. When creating a [wallet](/api-reference/wallets/create) or [policy](/api-reference/policies/create) via the Privy Dashboard or API, assign the key quorum as the [owner](/controls/authorization-keys/owners/overview). The quorum must then review and approve any proposed updates. For wallets, the quorum must also approve proposed signatures or transactions. Your app can alternatively add the quorum as a [signer](/controls/authorization-keys/owners/overview) on the wallet to give it permission to approve certain signatures and transactions without the ability to update the wallet itself. Via the Privy API or Dashboard, [propose an **intent**](/transaction-management/intents/create/execute-rpc) such as updating a wallet, updating a policy, or executing a signature or transaction. The intent then enters a review queue for team members in the assigned key quorum. Team members [review the intent](/controls/dashboard/approvals) on the [Approvals](https://dashboard.privy.io/apps?page=approvals) page of the Dashboard. Each reviewer inspects the proposed change and decides to approve or reject it. Approvals are secured by biometric or TOTP MFA. Once enough team members approve (based on the quorum threshold), the proposed action executes. Manual approvals flow diagram ## Get started Get started with manual approvals using the guides below. Create a key quorum of team members who review intents. Propose intents to authorize a transaction or update a wallet or policy. Approve or reject intents in the Privy Dashboard. Understand intent statuses from pending to executed. Retrieve intent status and execution results via the API. Receive real-time notifications when intents are created or authorized. # Creating key quorums Source: https://docs.privy.io/controls/key-quorum/create To create a key quorum, first [create the authorization keys](/controls/authorization-keys/keys/create/key) and/or [get the user IDs](/controls/authorization-keys/keys/create/user/overview) of the users that will constitute the key quorum. Key quorums can also include other [key quorums](/controls/key-quorum/overview) as members (one level deep). Once you have the user ID(s), authorization key(s), and/or key quorum ID(s), register the key quorum with Privy via the Dashboard or the REST API. Visit the [**Authorization keys**](https://dashboard.privy.io/apps?page=authorization-keys) page of the **Wallets** section for your app, click **New key**, and select **Register key quorum instead**. Specify the public keys you'd like to add to the quorum and an authorization threshold. Key quorums containing both user IDs and authorization keys must be created via the REST API. Dashboard You can create a key quorum using the Node SDK by using the `keyQuorums().create()` method. ### Usage The returned `id` for the key quorum is used as the `owner_id` field when creating or updating resources (e.g. wallets or policies) in the Privy API. ```ts title="Example: Create a 2-of-2 key quorum with an authorization key and a user" theme={"system"} try { const keyQuorum = await privyClient.keyQuorums().create({ public_keys: ['authorization-key'], user_ids: ['user-id'], key_quorum_ids: ['key-quorum-id'], // Optional: nest other key quorums as members display_name: '2 of 2 Test Key Quorum', authorization_threshold: 2, // Require 2 signatures (both keys) }); const keyQuorumId = keyQuorum.id; } catch (error) { console.error(error); } ``` Refer to the [API reference](/api-reference/key-quorums/create) for more details on the available parameters and returns. You can create a key quorum using the Java SDK by using the `keyQuorums().create()` method. ### Usage The returned `id` for the key quorum is used as the `ownerId` field when creating or updating resources (e.g. wallets or policies) in the Privy API. ```java title="Example: Create a 2-of-2 key quorum with an authorization key and a user" theme={"system"} try { KeyQuorumCreateRequestBody keyQuorumRequest = KeyQuorumCreateRequestBody.builder() .publicKeys(List.of("authorization-key")) .userIds(List.of("user-id")) .keyQuorumIds(List.of("key-quorum-id")) // Optional: nest other key quorums as members .displayName("2 of 2 Test Key Quorum") .authorizationThreshold(2.0) // Require 2 signatures (both keys) .build(); KeyQuorumCreateResponse keyQuorumResponse = privyClient .keyQuorums() .create(keyQuorumRequest); if (keyQuorumResponse.keyQuorum().isPresent()) { KeyQuorum keyQuorum = keyQuorumResponse.keyQuorum().get(); String keyQuorumId = keyQuorum.id(); } } catch (APIException e) { String errorBody = e.bodyAsString(); System.err.println(errorBody); } catch (Exception e) { System.err.println(e.getMessage()); } ``` ### Parameters When creating a key quorum, you can specify the following values on the `KeyQuorumCreateRequestBody` builder: A list of base64-encoded, DER-formatted P-256 public keys to register. A list of user IDs to include in the key quorum. A list of key quorum IDs to include as members of this key quorum (one level deep). Each nested quorum counts as one member toward the parent's authorization threshold. The minimum number of signatures required to authorize an action. If left unset, the default is all keys. Human readable display name to attach to the key. ### Returns The `KeyQuorumCreateResponse` object contains an optional `keyQuorum()` field, present if the key quorum was created successfully. The created `KeyQuorum` object. Unique ID for the key quorum, used to assign the `owner_id` to a resource. The list of authorization keys included in the key quorum. The public key of the authorization key. The display name of the authorization key. The list of user IDs included in the key quorum. The list of key quorum IDs nested as members of this key quorum. The minimum number of signatures required to authorize an action. If left unset, the default is all keys. Human readable display name to attach to the key. You can create a key quorum using the Rust SDK by using the `key_quorums().create()` method. ### Usage The returned `id` for the key quorum is used as the `owner_id` field when creating or updating resources (e.g. wallets or policies) in the Privy API. ```rust title="Example: Create a 2-of-2 key quorum with an authorization key and a user" theme={"system"} use privy_rs::{PrivyClient, generated::types::*}; let client = PrivyClient::new(app_id, app_secret)?; let request = CreateKeyQuorumBody { public_keys: Some(vec!["authorization-key".to_string()]), user_ids: Some(vec!["user-id".to_string()]), key_quorum_ids: Some(vec!["key-quorum-id".to_string()]), // Optional: nest other key quorums display_name: Some("2 of 2 Test Key Quorum".to_string()), authorization_threshold: Some(2.0), // Require 2 signatures (both keys) }; let key_quorum = client .key_quorums() .create(request) .await?; let key_quorum_id = key_quorum.id; println!("Created key quorum: {}", key_quorum_id); ``` ### Parameters and Returns See the Rust SDK documentation for detailed parameter and return types, including embedded examples: * [KeyQuorumsClient::create](https://docs.rs/privy-rs/latest/privy_rs/subclients/struct.KeyQuorumsClient.html#method.create) For REST API details, see the [API reference](/api-reference/key-quorums/create). To create a key quorum with the Go SDK, use the `New` method on the `KeyQuorums` service. ### Usage The returned `Id` for the key quorum is used as the `OwnerId` field when creating or updating resources (e.g. wallets or policies) in the Privy API. ```go title="Example: Create a 2-of-2 key quorum with an authorization key and a user" theme={"system"} quorum, err := client.KeyQuorums.New(context.Background(), privy.KeyQuorumNewParams{ KeyQuorumCreateRequestBody: privy.KeyQuorumCreateRequestBody{ PublicKeys: []string{"authorization-key"}, UserIDs: []string{"user-id"}, KeyQuorumIDs: []string{"key-quorum-id"}, // Optional: nest other key quorums DisplayName: privy.String("2 of 2 Test Key Quorum"), AuthorizationThreshold: privy.Float(2), // Require 2 signatures (both keys) }, }) if err != nil { log.Fatalf("failed to create key quorum: %v", err) } keyQuorumID := quorum.ID ``` ### Parameters and Returns See the [API reference](/api-reference/key-quorums/create) for more details. To create a key quorum with the Ruby SDK, use the `create` method on the `key_quorums` service. ### Usage The returned `id` for the key quorum is used as the `owner_id` field when creating or updating resources (e.g. wallets or policies) in the Privy API. ```ruby title="Example: Create a 2-of-2 key quorum with an authorization key and a user" theme={"system"} quorum = client.key_quorums.create( key_quorum_create_params: { public_keys: ["authorization-key"], user_ids: ["user-id"], key_quorum_ids: ["key-quorum-id"], # Optional: nest other key quorums display_name: "2 of 2 Test Key Quorum", authorization_threshold: 2 # Require 2 signatures (both keys) } ) puts(quorum.id) ``` ### Parameters and Returns See the [API reference](/api-reference/key-quorums/create) for more details. Register the key quorum with Privy by making a `POST` request to: ```sh theme={"system"} https://api.privy.io/v1/key_quorums ``` In the request body, include the following. A list of base64-encoded, DER-formatted P-256 public keys to register. A list of user IDs to include in the key quorum. A list of key quorum IDs to include as members of this key quorum (one level deep). Each nested quorum counts as one member toward the parent's authorization threshold. The minimum number of signatures required to authorize an action. If left unset, the default is all keys. Human readable display name to attach to the key. If the request is successful, Privy will return the following fields in the response. Unique ID for the key quorum, used to assign the `owner_id` to a resource. The list of public keys and their display names. The list of user IDs included in the key quorum. The list of key quorum IDs nested as members of this key quorum. The minimum number of signatures required to authorize an action. If left unset, the default is all keys. Human readable display name to attach to the key. The returned `id` for the key quorum is used as the `owner_id` field when creating or updating resources (e.g. wallets or policies) in the Privy API. See an example request for creating a key quorum below. As an example, a request to register a 2 of 2 key quorum might look like the following: ```bash theme={"system"} $ curl --request POST https://api.privy.io/v1/key_quorums \ -u ":" \ -H "privy-app-id: " \ -H 'Content-Type: application/json' \ -d '{ "display_name": "Sample key", "public_keys": [ "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEx4aoeD72yykviK+f/ckqE2CItVIG\n1rCnvC3/XZ1HgpOcMEMialRmTrqIK4oZlYd1RfxU3za/C9yjhboIuoPD3g==", "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAErzZtQr/bMIh3Y8f9ZqseB9i/AfjQ\nhu+agbNqXcJy/TfoNqvc/Y3Mh7gIZ8ZLXQEykycx4mYSpqrxp1lBKqsZDQ==" ], "key_quorum_ids": [""], "authorization_threshold": 2 }' ``` An example successful response would look like: ```json theme={"system"} { "id": "", "display_name": "Sample key", "public_keys": [ { "public_key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEx4aoeD72yykviK+f/ckqE2CItVIG\n1rCnvC3/XZ1HgpOcMEMialRmTrqIK4oZlYd1RfxU3za/C9yjhboIuoPD3g==" }, { "public_key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAErzZtQr/bMIh3Y8f9ZqseB9i/AfjQ\nhu+agbNqXcJy/TfoNqvc/Y3Mh7gIZ8ZLXQEykycx4mYSpqrxp1lBKqsZDQ==" } ], "key_quorum_ids": [""], "authorization_threshold": 2 } ``` # Overview Source: https://docs.privy.io/controls/key-quorum/overview Overview of key quorums for multi-party authorization on Privy wallet actions. Key quorums are an advanced feature. [Reach out](https://privy.io/slack) to discuss whether this setup is right for your integration. Key quorums define how ownership and authorization work. A quorum is a set of authorization keys and/or users that control a resource (such as a wallet or policy) in the Privy API. Key quorums can be configured such that a **quorum** of *m*-of-*n* of the keys in the set must sign requests to the Privy API. This authorization threshold gives you flexible, production-grade control over how sensitive operations are approved. Key quorums enable setups such as: * **Fine-grained ownership model**: Decide which actions require a user signature, an authorization key, or both. * **Distributed authorization**: Require signatures from multiple authorization keys running across different servers. * **Multi-signature security**: Enforce independent approvals for high-risk or high-value operations. * **Hierarchical approval structures**: Nest a key quorum inside another key quorum, such as a "Security Team" quorum nested inside an "Org Admins" quorum. ## Nested key quorums Key quorums can include other key quorums as members (one level deep), enabling hierarchical authorization structures that mirror how organizations actually make decisions. A nested quorum counts as a single member of the parent quorum — once it meets its own authorization threshold, it counts as one approval toward the parent's threshold. This lets organizations enforce team-level sign-off as part of a broader approval flow. All authorization thresholds — both at the parent and nested level — are enforced within Privy's [TEE infrastructure](/security/wallet-infrastructure/secure-enclaves), so approval requirements cannot be bypassed by any single party. Learn more about how to create key quorums and sign requests with the guides below. Create key quorums from authorization keys. Sign requests with a quorum of *m*-of-*n* keys. # Signing requests with key quorums Source: https://docs.privy.io/controls/key-quorum/sign To sign a request with a key quorum: 1. Collect the private keys for a threshold of authorization keys in the key quorum. For example, if your key quorum is configured with an *m*-of-*n* authorization threshold, you must have the private keys for at least *m* of the authorization keys in the key quorum. For users in your key quorum, request the user key per [this guide](/controls/authorization-keys/keys/create/user/request). 2. [Sign the request](/controls/authorization-keys/using-owners/sign) with each authorization key individually. 3. Pass the signatures as a comma-delimited string in the `privy-authorization-signature` header for your requests to the Privy API. If the parent quorum contains a nested key quorum, members of the nested quorum sign the same way via `privy-authorization-signature`. Once enough nested quorum members sign to meet its authorization threshold, the nested quorum counts as one approval toward the parent's threshold. No special signing flow is needed. An example request signed by a *2*-of-*n* key quorum might look as follows ```bash theme={"system"} curl --request POST https://api.privy.io/v1/wallets/y5ofctvacjiv53u4hmnqi0e5/rpc \ -u ":" \ -H "privy-app-id: " \ -H "privy-authorization-signature: ," \ -H 'Content-Type: application/json' \ -d '{ "caip2": "eip155:1", "method": "eth_sendTransaction", "params": { "transaction": { "to": "0xE3070d3e4309afA3bC9a6b057685743CF42da77C", "value": "0x2386f26fc10000", "data": "0x" } } }' ``` Use the [`AuthorizationContext`](/controls/authorization-keys/using-owners/sign/signing-on-the-server) to set the authorization key(s) in the quorum to use for signing the request. ```ts title="Example: Using ethereum eth_sendTransaction" focus={22-27} theme={"system"} import {PrivyClient} from '@privy-io/node'; const privyClient = new PrivyClient({ appId: 'insert-your-app-id', appSecret: 'insert-your-app-secret' }); try { const caip2 = 'eip155:1'; // Ethereum mainnet const response = await privyClient .wallets() .ethereum() .sendTransaction('insert-user-wallet-id', { caip2, params: { transaction: { to: '0xE3070d3e4309afA3bC9a6b057685743CF42da77C', value: '0x2386f26fc10000', data: '0x' } }, authorization_context: { // Example: building an authorization context for a 2-of-2 key quorum, // consisting of a user and authorization key authorization_private_keys: ['authorization-key'], user_jwts: ['user-jwt'] } }); const transactionHash = response.hash; } catch (error) { console.error(error); } ``` Use the [`AuthorizationContext` builder](/controls/authorization-keys/using-owners/sign/signing-on-the-server) to set the authorization key(s) in the quorum to use for signing the request. ```java title="Example: Using ethereum eth_sendTransaction" focus={9-13,15,23} theme={"system"} try { String caip2 = "eip155:1"; // Ethereum mainnet UnsignedStandardEthereumTransaction txn = UnsignedStandardEthereumTransaction.builder() .to("0xE3070d3e4309afA3bC9a6b057685743CF42da77C") .value(Quantity.of("0x2386f26fc10000")) .data("0x") .build(); // Example: Building an authorization context for a 2-of-2 key quorum, consisting of a user and authorization key AuthorizationContext authorizationContext = AuthorizationContext.builder() .addUserJwt("user-jwt") .addAuthorizationPrivateKey("authorization-key") .build(); // Pass the authorization context to the method to have the SDK automatically sign the request EthereumSendTransactionRpcResponseData response = privyClient .wallets() .ethereum() .sendTransaction( walletId, caip2, txn, authorizationContext ); String transactionHash = response.hash(); } catch (APIException e) { String errorBody = e.bodyAsString(); System.err.println(errorBody); } catch (Exception e) { System.err.println(e.getMessage()); } ``` Use the [`AuthorizationContext`](/controls/authorization-keys/using-owners/sign/signing-on-the-server) to set the authorization key(s) in the quorum to use for signing the request. ```rust title="Example: Using ethereum eth_sendTransaction" focus={7-10} theme={"system"} use privy_rs::{PrivyClient, AuthorizationContext, JwtUser, PrivateKey, generated::types::*}; let client = PrivyClient::new(app_id, app_secret)?; // Example: Building an authorization context for a 2-of-2 key quorum, // consisting of a user and authorization key let auth_ctx = AuthorizationContext::new() .push(JwtUser(client.clone(), "user-jwt".to_string())) .push(PrivateKey("authorization-key".to_string())); let request = EthereumSendTransactionRpcInput { method: "eth_sendTransaction".to_string(), caip2: "eip155:1".to_string(), // Ethereum mainnet sponsor: None, params: EthereumSendTransactionRpcInputParams { transaction: EthereumSendTransactionRpcInputParamsTransaction { to: Some("0xE3070d3e4309afA3bC9a6b057685743CF42da77C".to_string()), value: Some("0x2386f26fc10000".to_string()), data: Some("0x".to_string()), gas: None, gas_price: None, nonce: None, } } }; let response = client .wallets() .ethereum() .send_transaction("wallet-id", request, &auth_ctx, None) .await?; let transaction_hash = response.data.transaction_hash; println!("Transaction hash: {}", transaction_hash); ``` ### Parameters and Returns See the Rust SDK documentation for detailed parameter and return types, including embedded examples: * [EthereumService::send\_transaction](https://docs.rs/privy-rs/latest/privy_rs/ethereum/struct.EthereumService.html#method.send_transaction) For more details on AuthorizationContext, see the [authorization context guide](/controls/authorization-keys/using-owners/sign/signing-on-the-server#rust). Use the [`AuthorizationContext` builder](/controls/authorization-keys/using-owners/sign/signing-on-the-server) to set the authorization key(s) in the quorum to use for signing the request. ### Usage ```go focus={4-7,14} theme={"system"} import "github.com/privy-io/go-sdk/authorization" // Create authorization context with credentials from quorum members authCtx := &authorization.AuthorizationContext{ PrivateKeys: []string{"authorization-key-1"}, UserJwts: []string{"user-jwt"}, } // Use the authorization context when calling wallet operations response, err := client.Wallets.Ethereum.SignMessage( context.Background(), "wallet-id", "Hello, Privy!", privy.WithAuthorizationContext(authCtx), ) if err != nil { log.Fatalf("failed to sign message: %v", err) } fmt.Println("Signature:", response.Signature) ``` Use the [`AuthorizationContext`](/controls/authorization-keys/using-owners/sign/signing-on-the-server) to set the authorization key(s) in the quorum to use for signing the request. ```ruby title="Example: Using ethereum personal_sign" focus={2-5,16} theme={"system"} # Build an authorization context for a 2-of-2 key quorum, # consisting of a user and authorization key ctx = Privy::Authorization::AuthorizationContext.build( authorization_private_keys: ["authorization-key-1"], user_jwts: ["user-jwt"] ) # Use the authorization context when calling wallet operations response = client.wallets.rpc( "wallet-id", wallet_rpc_request_body: { method: "personal_sign", chain_type: "ethereum", params: {message: "Hello, Privy!", encoding: "utf-8"} }, authorization_context: ctx ) puts(response.data.signature) ``` When the API receives the request, Privy validates that: 1. The required number of signatures are provided. 2. All signatures are valid for the request payload. 3. All signatures come from authorization keys in the key quorum for the wallet. If any validation fails, the request is rejected. # Controls and policies Source: https://docs.privy.io/controls/overview Privy’s wallet system provides a layered control model that defines who can authorize actions and how wallets behave. These controls are built into the architecture of Privy wallets and help teams design secure, predictable flows without adding friction for users. controls splash ## Security without compromise Privy wallets are designed with [security](/security/overview) at their core. Our approach uses a combination of key splitting (Shamir's secret sharing) and private key reconstitution in [secure execution environments](/security/wallet-infrastructure/secure-enclaves) to ensure that only authorized parties can access their wallets. Wallets remain fully non-custodial and users ultimately have full control over their assets. ## Flexible owner configurations Privy’s control model allows you to specify exactly who can approve different types of actions. Quorums can include users, authorization keys, or both, enabling patterns such as: * **User-controlled wallets**: The user approves everything. * **Delegated permissions**: Users grant limited, scoped authorization to the application. * **Application-managed control**: Services approve operational actions under strict policies. * **Shared control**: Multiple parties must sign off on sensitive operations. These configurations allow you to align wallet ownership with your product's risk and UX requirements. ## Programmable policies Policies define the actions a wallet is allowed to take. They operate as key-level enforceable guardrails, ensuring wallets behave only as your application intends. By default, the trusted execution environment (secure enclave) enforces policies when processing wallet actions, such as signature requests, transactions, and key export. The enclave evaluates policy rules in a tamper-proof environment before any operations proceed. Privy enforces some policies at the API level. For example, limiting transfer sizes requires transaction simulation which runs outside the enclave today. * **Transaction limits**: Set maximum amounts that can be transferred. * **Approved destinations**: Specify recipients where funds can be sent. * **Contract interactions**: Control which smart contracts can be used. * **Action parameters**: Define what specific operations are permitted. Policies help protect users and applications by preventing unauthorized or unintended actions, making them essential for features like payment subscriptions, trading limits, or scheduled transactions. images/Policies.png ## Enhanced security options For sensitive wallet operations, Privy supports multi-factor authentication, biometric verification, and hardware security keys. Learn more about [configuring MFA](/authentication/user-authentication/mfa/overview). ## Getting started To learn more about implementing specific controls and policies for your application, explore our detailed documentation on wallet [policies](/controls/policies/overview) and [controls](/controls/authorization-keys/owners/overview). # Condition sets Source: https://docs.privy.io/controls/policies/condition-sets # Overview Condition sets provide a flexible way to define reusable lists of values that can be referenced in policy conditions. Instead of hardcoding values directly in policy rules, you can create a named condition set (e.g., "Approved Recipients") and reference it using the `in_condition_set` operator. Together, condition sets make it easy to express complex constraints cleanly and keep policies maintainable as your application grows. This approach offers several benefits: * **Maintainability**: Update the list of values in one place without modifying policy rules * **Reusability**: Reference the same condition set across multiple policies and rules * **Scalability**: Manage large lists of values efficiently * **Dynamic Updates**: Add or remove values without redeploying policies ## Concepts Condition sets are defined by three core primitives: condition sets, condition set items, and policy conditions. At a high-level: * Condition sets are lists of values that can be referenced in policy conditions. * Condition set items are individual items that belong to a condition set, whose values are directly evaluated against. * Policy conditions are boolean statements that the policy engine can evaluate RPC requests against (see [Conditions section](/controls/policies/overview#conditions)) ## The `in_condition_set` Operator The `in_condition_set` operator allows you to check if the value of a transaction field exists in a condition set. This is particularly useful for maintaining allowlists or denylists of addresses, contracts, or other string values. The `in_condition_set` operator can be configured with a variety of fields and field sources, including `ethereum_transaction.to`, `solana_system_program_instruction.Transfer.to`, etc. ## Create condition sets and items Refer to the [API reference](/api-reference/condition-sets/create) for creating condition sets and items. * Creating a condition set requires an [owner](/controls/authorization-keys/using-owners/overview). * Updating condition sets or condition set items with the following endpoints requires [authorization signature](/api-reference/authorization-signatures#usage). * [`PATCH /v1/condition_sets/{condition_set_id}`](/api-reference/condition-sets/update) * [`DELETE /v1/condition_sets/{condition_set_id}`](/api-reference/condition-sets/delete) * [`POST /v1/condition_sets/{condition_set_id}/condition_set_items`](/api-reference/condition-sets/condition-set-items/create) * [`PUT /v1/condition_sets/{condition_set_id}/condition_set_items`](/api-reference/condition-sets/condition-set-items/update) * [`DELETE /v1/condition_sets/{condition_set_id}/condition_set_items/{condition_set_item_id}`](/api-reference/condition-sets/condition-set-items/delete) * Deleting a condition set will delete all condition set items that have the same condition set id. ## Condition sets evaluation When the rules that are associated with the requested RPC method is evaluated: 1. The policy engine extracts the value of the corresponding field from the transaction. 2. If a `ConditionSetItem` item is found with `conditionSetId` and the `value` (the value from the previous step), the condition evaluates to `true`. 3. If all conditions in the rule pass, the rule evaluates to `ALLOW` action. The policy engine evaluates the raw value from the transaction directly against values of condition set items without any conversion, so each `ConditionSetItem` must be *exactly* the value of the field. Case sensitivity depends on the condition's field source, not on the chain. Comparisons are case-insensitive for EVM addresses and hex byte strings reached through a signing field source: `ethereum_transaction.to`, address fields on `ethereum_typed_data_message`, address arguments on `ethereum_calldata`, `ethereum_typed_data_domain.verifyingContract`, `ethereum_7702_authorization.contract`, and `tempo_transaction.fee_token`. For these, a checksummed and a lowercase address are interchangeable, so each address needs only one item. Every other comparison is exact. That includes `action_request_body` fields such as `destination.address` on `transfer` rules, even though the value is an EVM address, as well as all Solana, Sui, Tron, and XRPL values. Because `action_request_body` comparisons are exact, a `transfer` denylist keyed on a checksummed address does not match the same address sent in lowercase, and the rule silently fails to fire. Normalize destination addresses to one form in the app before calling `transfer`, or store both forms in the condition set. If a condition set is deleted, all conditions that evaluate against that condition set will evaluate to `false`. ## Example: Allowlist of recipient addresses This example demonstrates how to create a policy that only allows transactions to approved recipient addresses using a condition set. ### Step 1: Create a condition set ```json theme={"system"} POST /v1/condition_sets { "name": "Approved Recipients", "owner_id": "asgkan0r7gi0wdbvf9cw8qio" } ``` Response: ```json theme={"system"} { "id": "qvah5m2hmp9abqlxdmfiht95", "name": "Approved Recipients", "owner_id": "asgkan0r7gi0wdbvf9cw8qio", "created_at": 1761271537642 } ``` ### Step 2: Add approved addresses to the condition set ```json theme={"system"} POST /v1/condition_sets/qvah5m2hmp9abqlxdmfiht95/condition_set_items [ { "value": "0x5B8b13e8f3E6Ec888e88C77cf039EB6281F21D93" }, { "value": "0xB00F0759DbeeF5E543Cc3E3B07A6442F5f3928a2" } ] ``` ### Step 3: Create a policy rule using the condition set ```json theme={"system"} { "version": "1.0", "name": "example of in_condition_set operator", "chain_type": "ethereum", "rules": [ { "name": "allow if recipient is in allow_list", "action": "ALLOW", "method": "eth_sendTransaction", "conditions": [ { "field_source": "ethereum_transaction", "field": "to", "operator": "in_condition_set", "value": "qvah5m2hmp9abqlxdmfiht95" } ] } ] } ``` The following transaction is allowed because `0x5B8b13e8f3E6Ec888e88C77cf039EB6281F21D93` is in the condition set. ```json theme={"system"} { "method": "eth_sendTransaction", "params": { "transaction": { "to": "0x5B8b13e8f3E6Ec888e88C77cf039EB6281F21D93", "value": "0x1000000000000000" } } } ``` The following transaction denied because `0x0000000000000000000000000000000000000000` is not in the condition set. ```json theme={"system"} { "method": "eth_sendTransaction", "params": { "transaction": { "to": "0x0000000000000000000000000000000000000000", "value": "0x1000000000000000" } } } ``` ## Example: Denylist of recipient addresses The example `Allowlist of recipient addresses` functions as a denylist of recipient addresses if the `action` is set to to `DENY` at [step 3](#step-3%3A-create-a-policy-rule-using-the-condition-set). # Create a policy Source: https://docs.privy.io/controls/policies/create-a-policy You can create a policy using the Privy Dashboard, the NodeJS SDK, or the REST API. Policies optionally have owners, which represent the signatures required to modify the policy after creation, see [setting authorization signatures](/api-reference/authorization-signatures). We highly recommend specifying owners for your policies to further restrict the parties that can modify them. Without an owner, the policies can be updated by your app secret alone. Use the **`PrivyClient`**'s **`create`** method from the `policies()` interface to create a new policy. ```tsx theme={"system"} const policy = await privy.policies().create({ name: 'Allow list certain smart contracts', version: '1.0', chain_type: 'ethereum', rules: [ { name: 'Allow list USDC', method: 'eth_sendTransaction', action: 'ALLOW', conditions: [ { field_source: 'ethereum_transaction', field: 'to', operator: 'eq', value: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' } ] } ], owner_id: 'fmfdj6yqly31huorjqzq38zc' }); ``` You can create a policy using the Java SDK by using the `policies().create()` method. ```java theme={"system"} try { // Create a policy rule to allow USDC transfers Rule allowUsdc = Rule.builder() .name("Allowlist USDC") .method(PolicyRuleMethod.ETH_SEND_TRANSACTION) .action(Action.ALLOW) .conditions(List.of( EthereumTransactionCondition.builder() .fieldSource(EthereumTransactionConditionFieldSource.ETHEREUM_TRANSACTION) .field(EthereumTransactionConditionField.TO) .operator(ConditionOperator.EQ) .value(ConditionValue.of("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913")) .build() )) .build(); // Create a policy that contains your rules PolicyCreateRequestBody policy = PolicyCreateRequestBody.builder() .version(Version.ONE_DOT0) .name("Allowlist certain smart contracts") .chainType(WalletChainType.ETHEREUM) .rules(List.of( allowUsdc )) .build(); PolicyCreateResponse response = privyClient .policies() .create(policy); if (response.policy().isPresent()) { Policy policy = response.policy().get(); String policyId = policy.id(); } } catch (APIException e) { String errorBody = e.bodyAsString(); System.err.println(errorBody); } catch (Exception e) { System.err.println(e.getMessage()); } ``` ### Parameters When defining a policy, you may specify the following values on the `PolicyCreateRequestBody` builder: Version of the policy. Name to assign to policy. Chain type for wallets that the policy will be applied to. A list of `Rule` objects describing what rules to apply to each RPC method (e.g. `'eth_sendTransaction'`) that the wallet can take. [Learn more about `Rules`](/controls/policies/overview#rules). The owner of the policy. You should specify either an `owner` or an `ownerId`, but not both. The key quorum ID of the owner of the policy. You should specify either an `owner` or an `ownerId`, but not both. ### Returns The `PolicyCreateResponse` object contains an optional `policy()` field that contains the created policy if the policy was created successfully. The created policy. Version of the policy. Name of the policy. Chain type of the wallets that the policy will be applied to. Unique ID of the policy. The key quorum ID of the owner of the policy. The Unix time of when the policy was created. A list of `Rule` objects describing what rules to apply to each RPC method (e.g. `'eth_sendTransaction'`) that the wallet can take. [Learn more about `Rules`](/controls/policies/overview#rules). Use the **`PrivyClient`**'s **`create`** method from the `policies()` interface to create a new policy. ```rust theme={"system"} use privy_rs::{PrivyClient, generated::types::*}; let client = PrivyClient::new(app_id, app_secret)?; // Create policy rules let usdc_condition = PolicyRuleCondition { field_source: "ethereum_transaction".to_string(), field: "to".to_string(), operator: "eq".to_string(), value: serde_json::Value::String("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913".to_string()), }; let allow_usdc_rule = PolicyRule { name: "Allow list USDC".to_string(), method: "eth_sendTransaction".to_string(), action: PolicyRuleAction::Allow, conditions: vec![usdc_condition], }; let request = CreatePolicyBody { name: "Allow list certain smart contracts".to_string(), version: "1.0".to_string(), chain_type: "ethereum".to_string(), rules: vec![allow_usdc_rule], owner_id: Some("fmfdj6yqly31huorjqzq38zc".to_string()), owner: None, }; let policy = client .policies() .create(request, &authorization_context) .await?; println!("Created policy: {}", policy.id); ``` ### Parameters and Returns See the Rust SDK documentation for detailed parameter and return types, including embedded examples: * [PoliciesClient::create](https://docs.rs/privy-rs/latest/privy_rs/subclients/struct.PoliciesClient.html#method.create) For REST API details, see the [API reference](/api-reference/policies/create). To create a policy with the Go SDK, use the `New` method on the `Policies` service. ### Usage ```go theme={"system"} policy, err := client.Policies.New(context.Background(), privy.PolicyNewParams{ Name: "Allow list certain smart contracts", ChainType: privy.WalletChainTypeEthereum, Version: privy.PolicyNewParamsVersion1_0, OwnerID: privy.String("fmfdj6yqly31huorjqzq38zc"), Rules: []privy.PolicyNewParamsRule{ { Name: "Allow list USDC", Method: "eth_sendTransaction", Action: "ALLOW", Conditions: []privy.PolicyNewParamsRuleConditionUnion{ { OfEthereumTransaction: &privy.PolicyNewParamsRuleConditionEthereumTransaction{ Field: "to", Operator: "eq", Value: privy.PolicyNewParamsRuleConditionEthereumTransactionValueUnion{ OfString: privy.String("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913"), }, }, }, }, }, }, }) if err != nil { log.Fatalf("failed to create policy: %v", err) } fmt.Println("Created policy:", policy.ID) ``` ### Parameters and Returns See the [API reference](/api-reference/policies/create) for more details. To create a policy with the Ruby SDK, use the `create` method on the `policies` service. ### Usage ```ruby theme={"system"} policy = client.policies.create( policy_create_params: { version: "1.0", name: "Allow list certain smart contracts", chain_type: "ethereum", owner_id: "fmfdj6yqly31huorjqzq38zc", rules: [ { name: "Allow list USDC", method: "eth_sendTransaction", action: "ALLOW", conditions: [ { field_source: "ethereum_transaction", field: "to", operator: "eq", value: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" } ] } ] } ) puts(policy.id) ``` ### Parameters and Returns See the [API reference](/api-reference/policies/create) for more details. To create a new policy, make a `POST` request to: ```sh theme={"system"} https://api.privy.io/v1/policies ``` In the request headers, make sure to include Privy's [required authentication headers](/basics/rest-api/setup#authentication) and [headers that may be required for your app's wallet API setup](/basics/rest-api/quickstart#2-sign-a-message). You can also include an [idempotency key](/api-reference/idempotency-keys) header. ## **Body** In the request body, include the following: Version of the policy. Currently, 1.0 is the only version. Name to assign to policy. Chain type for wallets that the policy will be applied to. A list of `Rule` objects describing what rules to apply to each RPC method (e.g. `'eth_sendTransaction'`) that the wallet can take. [Learn more about `Rules`](/controls/policies/overview#rules). The P-256 public key of the owner of the policy. If you provide this, do not specify an owner\_id as it will be generated automatically. View [this guide](/controls/authorization-keys/owners/overview) to learn more about owners. The key quorum ID of the owner of the policy. If you provide this, do not specify an owner. View [this guide](/controls/authorization-keys/owners/overview) to learn more about owners. Once you have successfully created a policy, you can assign that policy to a wallet at [creation](/wallets/wallets/create/create-a-wallet#param-policy-ids). ## **Response** If the policy is created successfully, the response will include the request body as well as an additional unique `id` field for the policy. Unique ID for the policy. Version of the policy. Currently, 1.0 is the only version. Name to assign to policy. Chain type for wallets that the policy will be applied to. A list of `Rule` objects describing what rules to apply to each RPC method (e.g. `'eth_sendTransaction'`) that the wallet can take. [Learn more about `Rules`](/controls/policies/overview#rules). The key quorum ID of the owner of the policy, whose signature is required to modify the policy. ## Example As an example, a sample request to create a new `eth_sendTransaction` policy might look like the following: ```bash theme={"system"} $ curl --request POST https://api.privy.io/v1/policies \ -u ":" \ -H "privy-app-id: " \ -H "privy-authorization-signature: " \ -H 'Content-Type: application/json' \ -d '{ "version": "1.0", "name": "Allowlist certain smart contracts", "chain_type": "ethereum", "rules": [{ "name": "Allowlist USDC", "method": "eth_sendTransaction", "conditions": [ { "field_source": "ethereum_transaction", "field": "to", "operator": "eq", "value": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" } ], "action": "ALLOW" }], "owner": { "public_key": "MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEx4aoeD72yykviK+f/ckqE2CItVIG1rCnvC3/XZ1HgpOcMEMialRmTrqIK4oZlYd1RfxU3za/C9yjhboIuoPD3g==" } }' ``` A successful response will look like the following: ```json theme={"system"} { "id": "fmfdj6yqly31huorjqzq38zc", "name": "Allowlist certain smart contracts", "version": "1.0", "chain_type": "ethereum", "rules": [ { "name": "Allowlist USDC", "method": "eth_sendTransaction", "conditions": [ { "field_source": "ethereum_transaction", "field": "to", "operator": "eq", "value": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" } ], "action": "ALLOW" } ], "owner_id": "fmfdj6yqly31huorjqzq38zc" } ``` # Ethereum examples Source: https://docs.privy.io/controls/policies/example-policies/ethereum ## Allowlist a specific smart contract ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Allowlisted contracts', chain_type: 'ethereum', rules: [ { name: 'Allowlist the USDC address', method: 'eth_sendTransaction', action: 'ALLOW', conditions: [ { field_source: 'ethereum_transaction', field: 'to', operator: 'eq', value: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' }, ] }, { name: 'Allowlist for Base specifically', method: 'eth_signTypedData_v4', action: 'ALLOW', conditions: [ { field_source: 'ethereum_typed_data_domain', field: 'chainId', operator: 'eq', value: '8453' } ] } ], } ``` ## Configure a max transfer value of ETH ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Native token transfer maximums', chain_type: 'ethereum', rules: [{ name: 'Restrict ETH transfers to a maximum value', method: 'eth_sendTransaction', conditions: [ { field_source: 'ethereum_transaction', field: 'value', operator: 'lte', value: '0x2386F26FC10000', }, ], action: 'ALLOW' }] } ``` ## Restrict transactions to specific chains Use the `chain_id` field to ensure transactions can only be executed on specific chains. ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Base chain only', chain_type: 'ethereum', rules: [{ name: 'Only allow transactions on Base', method: 'eth_sendTransaction', conditions: [ { field_source: 'ethereum_transaction', field: 'chain_id', operator: 'eq', value: '8453' // Base mainnet chain ID }, ], action: 'ALLOW' }] } ``` ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Mainnet and Base only', chain_type: 'ethereum', rules: [{ name: 'Allow transactions on only Ethereum mainnet or Base', method: 'eth_sendTransaction', conditions: [ { field_source: 'ethereum_transaction', field: 'chain_id', operator: 'in', value: ['1', '8453'] // Ethereum mainnet and Base }, ], action: 'ALLOW' }] } ``` ## Configure a max transfer value of an ERC20 token ```ts {skip-check} theme={"system"} { version: '1.0', name: 'ERC20 maximums', chain_type: 'ethereum', rules: [ { name: 'Restrict USDC transfers to be less than or equal to some value', method: 'eth_sendTransaction', conditions: [ { field_source: 'ethereum_transaction', field: 'to', operator: 'eq', value: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' // USDC address on Base }, { field_source: 'ethereum_calldata', // 'transfer' must match the function name, 'amount' must match an input name. field: 'transfer.amount', abi: [{ "inputs": [ { "internalType": "address", "name": "recipient", "type": "address" }, { "internalType": "uint256", "name": "amount", "type": "uint256" } ], "name": "transfer", "outputs": [ { "internalType": "bool", "name": "", "type": "bool" } ], "stateMutability": "nonpayable", "type": "function" }], operator: 'lte', value: '0x2386F26FC10000', } ], action: 'ALLOW' } ] } ``` ## Allow specific smart contract function calls Use `field: "function_name"` to match specific functions being called, regardless of their parameters. This is useful for: * Functions with no parameters (like `deposit()` or `withdraw()`) * Functions where you want to allow any parameter values ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Allow WETH deposit', chain_type: 'ethereum', rules: [ { name: 'Allow deposit to WETH contract', method: 'eth_sendTransaction', action: 'ALLOW', conditions: [ { field_source: 'ethereum_transaction', field: 'to', operator: 'eq', value: '0xC02aaA39b223FE8D0A0e5C4F27eAD9083C756Cc2' }, { field_source: 'ethereum_calldata', field: 'function_name', abi: [{ "name": "deposit", "type": "function", "stateMutability": "payable", "inputs": [], "outputs": [] }], operator: 'eq', value: 'deposit' } ] } ] } ``` ## Only allow transfers after a certain start date ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Only allow transfers after a certain start date', chain_type: 'ethereum', rules: [{ name: 'Only allow transfers after a certain start date', method: 'eth_sendTransaction', conditions: [{ field_source: 'system', field: 'current_unix_timestamp', operator: 'gte', value: '1757304000' // 2025-09-08 00:00:00 UTC in seconds since epoch }], action: 'ALLOW' }] } ``` ## Denylist recipients of a transaction ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Denylisted addresses', chain_type: 'ethereum', rules: [{ name: 'Deny interactions with the USDC contract', method: 'eth_sendTransaction', conditions: [ { field_source: 'ethereum_transaction', field: 'to', operator: 'eq', value: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' }, ], action: 'DENY' }] } ``` ## Denylist recipients of a transaction with condition sets ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Denylisted addresses with condition set', chain_type: 'ethereum', rules: [{ name: 'Deny interactions with the USDC contract', method: 'eth_sendTransaction', conditions: [ { field_source: 'ethereum_transaction', field: 'to', operator: 'in_condition_set', value: 'a2p4etpcbj2dltbjfigybi8j' }, ], action: 'DENY' // Note: setting the action to 'ALLOW' makes this an allowlist }] } ``` ## Enforce policies across multiple RPC methods ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Example policy with multiple RPC methods', chain_type: 'ethereum', rules: [{ name: 'Deny interactions with the USDC contract', method: 'eth_sendTransaction', conditions: [ { field_source: 'ethereum_transaction', field: 'to', operator: 'eq', value: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' }, ], action: 'DENY' }, { name: 'Only allow certain messages to be signed', method: 'personal_sign', conditions: [ { field_source: 'message', field: 'content', operator: 'eq', value: 'Hello world' }, ], action: 'ALLOW' }] } ``` ## Restrict message signing by content Use the `message` field source on `personal_sign` rules to constrain what messages a wallet can sign. The `content` field supports string operators (`eq`, `contains`, `starts_with`, `ends_with`, `in`, `in_condition_set`), and the `byte_length` field supports numeric operators. ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Only allow ownership proof messages', chain_type: 'ethereum', rules: [{ name: 'Allow messages that start with an ownership proof prefix', method: 'personal_sign', conditions: [ { field_source: 'message', field: 'content', operator: 'starts_with', value: 'Sign to prove ownership of' }, ], action: 'ALLOW' }] } ``` ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Limit signed message size', chain_type: 'ethereum', rules: [{ name: 'Only allow messages up to 256 bytes', method: 'personal_sign', conditions: [ { field_source: 'message', field: 'byte_length', operator: 'lte', value: '256' }, ], action: 'ALLOW' }] } ``` ## Deny all requests ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Example policy to deny all requests', chain_type: 'ethereum', rules: [{ name: 'Deny all requests', method: '*', conditions: [], action: 'DENY' }] } ``` ## Restrict typed data domains to a specific chain ID and verifying contract ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Example policy to allow a specific signing domain', chain_type: 'ethereum', rules: [{ name: 'Allow specific domain to sign messages', method: 'eth_signTypedData_v4', conditions: [ { field_source: 'ethereum_typed_data_domain', field: 'chainId', operator: 'eq', value: '8453' }, { field_source: 'ethereum_typed_data_domain', field: 'verifyingContract', operator: 'eq', value: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' } ], action: 'ALLOW' }], } ``` ## Restrict parameters of a typed data message An `ethereum_typed_data_message` condition only evaluates when the `types` map declared in the policy matches the `types` map in the signing request **exactly**. That includes `EIP712Domain` and every other type the client sends, even types the condition's `field` path never traverses, and the fields within each type must appear in the same order. On a mismatch the condition evaluates to `false` rather than skipping: in a DENY rule this means the rule never fires, and a permissive ALLOW rule on the same method signs the request. Copy the `types` map verbatim from what the client emits. ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Allow ERC20 Permits for known owners, max value', chain_type: 'ethereum', rules: [{ name: 'Allow specific owner addresses and a max value', method: 'eth_signTypedData_v4', conditions: [ { field_source: 'ethereum_typed_data_message', typed_data: { types: { // Declared because the client sends it, even though no condition reads it. // Clients using a viem WalletClient send this; a raw LocalAccount may not. EIP712Domain: [ {name: 'name', type: 'string'}, {name: 'version', type: 'string'}, {name: 'chainId', type: 'uint256'}, {name: 'verifyingContract', type: 'address'}, ], Person: [ {name: 'name', type: 'string'}, {name: 'wallet', type: 'address'}, ], Permit: [ {name: 'owner', type: 'Person'}, {name: 'spender', type: 'Person'}, {name: 'value', type: 'uint256'}, {name: 'deadline', type: 'uint256'}, {name: 'v', type: 'uint8'}, {name: 'r', type: 'bytes32'}, {name: 's', type: 'bytes32'}, ], }, primary_type: 'Permit', }, field: 'owner.wallet', // dot-separated path to primitive 'address' type that 'value' will be compared against. operator: 'in', value: ['0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913', '0x123589fCD6eDb6E08f4c7C32D4f71b54bdA02911'], }, { field_source: 'ethereum_typed_data_message', typed_data: { types: { EIP712Domain: [ {name: 'name', type: 'string'}, {name: 'version', type: 'string'}, {name: 'chainId', type: 'uint256'}, {name: 'verifyingContract', type: 'address'}, ], Person: [ {name: 'name', type: 'string'}, {name: 'wallet', type: 'address'}, ], Permit: [ {name: 'owner', type: 'Person'}, {name: 'spender', type: 'Person'}, {name: 'value', type: 'uint256'}, {name: 'deadline', type: 'uint256'}, {name: 'v', type: 'uint8'}, {name: 'r', type: 'bytes32'}, {name: 's', type: 'bytes32'}, ], }, primary_type: 'Permit', }, field: 'value', operator: 'lte', value: '0x2386F26FC10000', }, ], action: 'ALLOW' }], } ``` To screen typed-data recipients against a denylist, see [sanctions screening for x402 payments](/recipes/agent-integrations/x402-sanctions-screening). ## Restrict the delegation contract for EIP-7702 ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Restrict EIP-7702 delegation contracts', chain_type: 'ethereum', rules: [{ name: 'Allow only specific delegation contracts', method: 'eth_sign7702Authorization', conditions: [ { field_source: 'ethereum_7702_authorization', field: 'contract', operator: 'in', value: ['0xf5De540DabE85ecA73D61C4004cF2c243bbf4a5B'] } ], action: 'ALLOW' }] } ``` ## Prevent private key exports while allowing other actions ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Prevent private key exports', chain_type: 'ethereum', rules: [ { name: 'Block private key exports', method: 'exportPrivateKey', conditions: [], action: 'DENY' }, { name: 'Allow all other actions', method: '*', conditions: [], action: 'ALLOW' } ] } ``` ## Only permit private key exports ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Only allow private key exports', chain_type: 'ethereum', rules: [ { name: 'Allow private key exports', method: 'exportPrivateKey', conditions: [], action: 'ALLOW' }, { name: 'Block all other actions', method: '*', conditions: [], action: 'DENY' } ] } ``` ## Anti patterns ### Avoid adding rules that may override other rules ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Restrict the maximum value of ETH transfers', chain_type: 'ethereum', rules: [ { // This rule restricts the value of ETH transfers. name: 'Restrict ETH transfers to 1', method: 'eth_sendTransaction', conditions: [ { field_source: 'ethereum_transaction', field: 'value', operator: 'lte', value: '1' } ], action: 'ALLOW' }, { name: 'Restrict ETH transfers to 5', method: 'eth_sendTransaction', conditions: [ // This rule will override the previous rule by allowing a 5 ETH transfer. { field_source: 'ethereum_transaction', field: 'value', operator: 'lte', value: '5' } ], action: 'ALLOW' } ] } ``` # Solana examples Source: https://docs.privy.io/controls/policies/example-policies/solana ## Allowlist specific Solana Programs ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Allowlisted programs', chain_type: 'solana', rules: [{ name: 'Allowlist the Compute Budget Program and System Program', method: 'signAndSendTransaction', conditions: [ { // This field_source is used only to allowlist Solana Programs. field_source: 'solana_program_instruction', field: 'programId', operator: 'in', value: ['ComputeBudget111111111111111111111111111111', '11111111111111111111111111111111'] } ], action: 'ALLOW' }] } ``` ## Allow a SOL Transfer instruction with a max value ```ts {skip-check} theme={"system"} { version: '1.0', name: 'SOL transfer maximums', chain_type: 'solana', rules: [{ name: 'Restrict SOL transfers to a maximum value', method: 'signAndSendTransaction', conditions: [ { // This field_source is used for all System Program instructions. field_source: 'solana_system_program_instruction', field: 'Transfer.lamports', operator: 'lte', value: '1000000000' // 1 SOL }, ], action: 'ALLOW' }] } ``` ## Allow sending Solana transactions within a time window ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Time-bound Solana transactions policy', chain_type: 'solana', rules: [{ name: 'Allow Solana transactions only during the month of September 2025', method: 'signAndSendTransaction', conditions: [{ field_source: 'system', field: 'current_unix_timestamp', operator: 'gte', value: '1756699200' // 2025-09-01 00:00:00 UTC in seconds since epoch }, { field_source: 'system', field: 'current_unix_timestamp', operator: 'lt', value: '1759291200' // 2025-10-01 00:00:00 UTC in seconds since epoch }], action: 'ALLOW' }] } ``` ### Allow a SOL Transfer instruction with a max value to allowlisted recipients ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Restrict SOL transfers to a specific recipient', chain_type: 'solana', rules: [{ name: 'Restrict SOL transfers to a maximum value to a specific recipient', method: 'signAndSendTransaction', conditions: [ { // This condition restricts the value of all SOL transfers to <= 1 SOL. // This field_source is used for all System Program instructions. field_source: 'solana_system_program_instruction', field: 'Transfer.lamports', operator: 'lte', value: '1000000000' // 1 SOL }, { // This additional condition restricts Transfer recipients to a list of allowed addresses. // This field_source is used for all System Program instructions. field_source: 'solana_system_program_instruction', field: 'Transfer.to', operator: 'in', value: ['4tFqt2qzaNsnZqcpjPiyqYw9LdRzxaZdX2ewPncYEWLA', '4tFqt2qzaNsnZqcpjPiyqYw9LdRzxaZdX2ewPncYEWLA'] } ], action: 'ALLOW' }] } ``` ### Allow a Solana Transaction that has a Create and Transfer instruction, while limiting Transfers to 1 SOL ```ts {skip-check} theme={"system"} { version: '1.0', name: 'SOL transfer maximums', chain_type: 'solana', rules: [ { // This rule restricts the value of all SOL transfer instructions to <= 1 SOL. name: 'Restrict SOL transfers to a maximum value', method: 'signAndSendTransaction', conditions: [{ // This field_source is used for all System Program instructions. field_source: 'solana_system_program_instruction', field: 'Transfer.lamports', operator: 'lte', value: '1000000000' // 1 SOL }], action: 'ALLOW' }, { // This rule allows the Create instruction to be present in the transaction. name: 'Allow the Create instruction', method: 'signAndSendTransaction', conditions: [ { // This field_source is used for all System Program instructions. field_source: 'solana_system_program_instruction', field: 'instructionName', operator: 'eq', value: 'Create' } ], action: 'ALLOW' } ] } ``` ## Allow a TransferChecked instruction with a max value of a USDC token ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Restrict USDC transfers to a maximum value', chain_type: 'solana', rules: [{ name: 'Restrict transfers to be less than or equal to 5 USDC', method: 'signAndSendTransaction', conditions: [ { // This field_source is used for all Token Program instructions. field_source: 'solana_token_program_instruction', field: 'TransferChecked.mint', operator: 'eq', // This is the USDC mint address on the Solana mainnet. value: 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' }, { // This field_source is used for all Token Program instructions. field_source: 'solana_token_program_instruction', field: 'TransferChecked.amount', operator: 'lte', value: '5000000' // 5 USDC assuming 6 decimals }, ], action: 'ALLOW' }] } ``` ## Denylist recipients of a transaction ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Denylist recipients of SOL transfer', chain_type: 'solana', rules: [{ name: 'Deny SOL transfers to a list of addresses', method: 'signAndSendTransaction', conditions: [ { // This field_source is used for all System Program instructions. field_source: 'solana_system_program_instruction', field: 'Transfer.to', operator: 'in', value: ['4tFqt2qzaNsnZqcpjPiyqYw9LdRzxaZdX2ewPncYEWLA', '4tFqt2qzaNsnZqcpjPiyqYw9LdRzxaZdX2ewPncYEWLA'] }, ], action: 'DENY' }] } ``` ## Allowlist some System Program instructions and some Token Program instructions ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Allowlist all System Program instructions and some Token Program instructions', chain_type: 'solana', rules: [ { name: 'Allowlist System Program instructions', method: 'signAndSendTransaction', conditions: [ { // This field_source is used for all System Program instructions. field_source: 'solana_system_program_instruction', field: 'instructionName', operator: 'in', value: ['Create', 'Transfer'] } ], action: 'ALLOW' }, { name: 'Allowlist Token Program instructions', method: 'signAndSendTransaction', conditions: [ { // This field_source is used for all Token Program instructions. field_source: 'solana_token_program_instruction', field: 'instructionName', operator: 'in', value: ['TransferChecked', 'CloseAccount'] } ], action: 'ALLOW' } ] } ``` ## Allowlist some Solana Programs and restrict SOL transfers ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Allowlist some Solana Programs and restrict SOL transfers', chain_type: 'solana', rules: [ { name: 'Allowlist Programs', method: 'signAndSendTransaction', conditions: [ { field_source: 'solana_program_instruction', field: 'programId', operator: 'in', value: [ 'ComputeBudget111111111111111111111111111111', // Compute Budget Program 'JUP6LkbZbjS1jKKwapdHNy74zcZ3tLUZoi5QNyVTaV4' // Jupiter v6 Swap Program ] } ], action: 'ALLOW' }, { name: 'Restrict SOL transfers', method: 'signAndSendTransaction', conditions: [ { field_source: 'solana_system_program_instruction', field: 'Transfer.lamports', operator: 'lte', value: '1000000000' // 1 SOL } ], action: 'ALLOW' } ] } ``` ## Restrict message signing Use the `message` field source on `signMessage` rules to constrain what messages a Solana wallet can sign. The `content` field supports string operators (`eq`, `contains`, `starts_with`, `ends_with`, `in`, `in_condition_set`), and the `byte_length` field supports numeric operators. ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Only allow ownership proof messages', chain_type: 'solana', rules: [{ name: 'Allow messages that start with an ownership proof prefix', method: 'signMessage', conditions: [ { field_source: 'message', field: 'content', operator: 'starts_with', value: 'Sign to prove ownership of' }, ], action: 'ALLOW' }] } ``` ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Limit signed message size', chain_type: 'solana', rules: [{ name: 'Only allow messages up to 256 bytes', method: 'signMessage', conditions: [ { field_source: 'message', field: 'byte_length', operator: 'lte', value: '256' }, ], action: 'ALLOW' }] } ``` ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Block messages with sensitive patterns', chain_type: 'solana', rules: [{ name: 'Deny messages containing transfer authorization language', method: 'signMessage', conditions: [ { field_source: 'message', field: 'content', operator: 'contains', value: 'authorize transfer' }, ], action: 'DENY' }] } ``` ## Prevent private key exports while allowing other actions ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Prevent private key exports', chain_type: 'solana', rules: [ { name: 'Block private key exports', method: 'exportPrivateKey', conditions: [], action: 'DENY' }, { name: 'Allow all other actions', method: '*', conditions: [], action: 'ALLOW' } ] } ``` ## Only permit private key exports ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Only allow private key exports', chain_type: 'solana', rules: [ { name: 'Allow private key exports', method: 'exportPrivateKey', conditions: [], action: 'ALLOW' }, { name: 'Block all other actions', method: '*', conditions: [], action: 'DENY' } ] } ``` ## Anti patterns ### Avoid adding rules that may override other rules ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Allowlist the System Program Transfer instruction and restrict SOL transfers', chain_type: 'solana', rules: [ { // This rule restricts the value of all SOL transfers. name: 'Restrict SOL transfers', method: 'signAndSendTransaction', conditions: [ { field_source: 'solana_system_program_instruction', field: 'Transfer.lamports', operator: 'lte', value: '1000000000' // 1 SOL } ], action: 'ALLOW' }, { name: 'Allowlist System Program Transfer instruction', method: 'signAndSendTransaction', conditions: [ // This rule will override the previous rule by allowing all Transfer instructions via the System Program. { field_source: 'solana_system_program_instruction', field: 'instructionName', operator: 'eq', value: 'Transfer' } ], action: 'ALLOW' } ] } ``` ## Known Limitations ### Address Lookup Tables (ALTs) Solana policy evaluation does not support resolving addresses from [Address Lookup Tables (ALTs)](https://solana.com/docs/advanced/lookup-tables). If your policy has conditions that reference addresses stored in an ALT (e.g., recipient/sender allowlists), policy evaluation will fail and the transaction will be rejected. **What this means for your application:** * Transactions using ALTs **work normally** if your policy does not have address-based conditions, or if the addresses being evaluated are in the transaction's static account keys (not the ALT) * Policy evaluation will **only fail** if a policy condition needs to inspect an address that is stored in the ALT (e.g., `Transfer.to`, `Transfer.from`, recipient allowlists where the address is in the ALT portion) * Policies that only check `programId`, `instructionName`, transfer amounts, or time-based conditions work fine with ALT transactions **Workaround:** If you need address-based policy conditions (recipient/sender allowlists), ensure those addresses are included in the transaction's static account keys rather than resolved via ALT. Most simple transactions (SOL transfers, basic token transfers) do not require ALTs and will work with all policy types. If ALT support for address-based policies is a requirement for your use case, please [reach out](https://privy.io/slack) to discuss your needs. # Sui examples Source: https://docs.privy.io/controls/policies/example-policies/sui Sui policies use two method names for raw signing requests. Use `signRawMessageBytes` for unparsed raw signing requests. Use `signTransactionBytes` for parsed transaction bytes that evaluate `sui_transaction_command` or `sui_transfer_objects_command` conditions. There is no separate `signTransactionBytes` API endpoint. A Sui transaction consists of one or multiple inputs and commands. Common Sui commands to transfer stablecoins include: * `SplitCoins`, which splits off one or more coins from a single coin. * `MergeCoins`, which merges one or more coins of the same type into a single coin. * `TransferObjects` is used to transfer objects to a specified destination address. ## Allow raw signing after a certain start date Use `signRawMessageBytes` for unparsed raw signing requests. `signRawMessageBytes` rules support system conditions, but do not support decoded transaction field sources like `sui_transaction_command` or `sui_transfer_objects_command`. ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "Only allow raw signing after a certain start date", "chain_type": "sui", "rules": [ { "name": "Only allow raw signing after a certain start date", "method": "signRawMessageBytes", "conditions": [ { "field_source": "system", "field": "current_unix_timestamp", "operator": "gt", "value": "1757304000" // 2025-09-08 00:00:00 UTC in seconds since epoch } ], "action": "ALLOW" } ] } ``` ## Allowlist specific Sui transaction commands ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "Allow TransferObjects, SplitCoins and MergeCoins", "chain_type": "sui", "rules": [ { "name": "Allow TransferObjects, SplitCoins and MergeCoins commands", "method": "signTransactionBytes", "conditions": [ { "field_source": "sui_transaction_command", "field": "commandName", "operator": "in", "value": ["TransferObjects", "SplitCoins", "MergeCoins"] } ], "action": "ALLOW" } ] } ``` ## Configure a max amount on the TransferObjects command (summed amount per command, assuming coins are of the same type) ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "TransferObjects summed maximum amount", "chain_type": "sui", "rules": [ { "name": "TransferObjects amount summed maximum", "method": "signTransactionBytes", "conditions": [ { "field_source": "sui_transfer_objects_command", "field": "amount", "operator": "lt", "value": "10000000" } ], "action": "ALLOW" } ] } ``` ## Allowlist a specific Sui transaction recipient ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "Allow specific recipient", "chain_type": "sui", "rules": [ { "name": "Allow specific recipient", "method": "signTransactionBytes", "conditions": [ { "field_source": "sui_transfer_objects_command", "field": "recipient", "operator": "eq", "value": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" } ], "action": "ALLOW" } ] } ``` ## Allowlist specific Sui transaction recipients with condition set ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "Allow specific recipients with condition set", "chain_type": "sui", "rules": [ { "name": "Allow specific recipients with condition set", "method": "signTransactionBytes", "conditions": [ { "field_source": "sui_transfer_objects_command", "field": "recipient", "operator": "in_condition_set", "value": "a2p4etpcbj2dltbjfigybi8j" } ], "action": "ALLOW" } ] } ``` ## Restrict message signing Use the `message` field source on `signRawMessageBytes` rules to constrain what messages a Sui wallet can sign. The `content` field supports string operators (`eq`, `contains`, `starts_with`, `ends_with`, `in`, `in_condition_set`), and the `byte_length` field supports numeric operators. ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "Only allow ownership proof messages", "chain_type": "sui", "rules": [ { "name": "Allow messages that start with an ownership proof prefix", "method": "signRawMessageBytes", "conditions": [ { "field_source": "message", "field": "content", "operator": "starts_with", "value": "Sign to prove ownership of" } ], "action": "ALLOW" } ] } ``` ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "Limit signed message size", "chain_type": "sui", "rules": [ { "name": "Only allow messages up to 256 bytes", "method": "signRawMessageBytes", "conditions": [ { "field_source": "message", "field": "byte_length", "operator": "lte", "value": "256" } ], "action": "ALLOW" } ] } ``` ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "Allowlist specific messages", "chain_type": "sui", "rules": [ { "name": "Only allow known message strings to be signed", "method": "signRawMessageBytes", "conditions": [ { "field_source": "message", "field": "content", "operator": "in", "value": ["I agree to the terms of service", "Confirm login"] } ], "action": "ALLOW" } ] } ``` ## Only allow transactions after a certain start date ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "Only allow transactions after a certain start date", "chain_type": "sui", "rules": [ { "name": "Only allow transactions after a certain start date", "method": "signTransactionBytes", "conditions": [ { "field_source": "system", "field": "current_unix_timestamp", "operator": "gt", "value": "1757304000" // 2025-09-08 00:00:00 UTC in seconds since epoch } ], "action": "ALLOW" } ] } ``` ## Allow transfers to a specific recipients after a certain timestamp This is an example of mixing TransferObjects and System configurations. ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "Allow specific recipients after a certain timestamp", "chain_type": "sui", "rules": [ { "name": "Allow specific recipients after a certain timestamp", "method": "signTransactionBytes", "conditions": [ { "field_source": "sui_transfer_objects_command", "field": "recipient", "operator": "in_condition_set", "value": "a2p4etpcbj2dltbjfigybi8j", }, { "field_source": "system", "field": "current_unix_timestamp", "operator": "gt", "value": "1757304000", // 2025-09-08 00:00:00 UTC in seconds since epoch } ], "action": "ALLOW" } ] } ``` ## Denylist recipients of a TransferObjects with condition sets ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "Denylist TransferObjects recipients with condition set", "chain_type": "sui", "rules": [ { "name": "Denylist TransferObjects recipients with condition set", "method": "signTransactionBytes", "conditions": [ { "field_source": "sui_transfer_objects_command", "field": "recipient", "operator": "in_condition_set", "value": "a2p4etpcbj2dltbjfigybi8j" } ], "action": "DENY", // Note: setting the action to 'ALLOW' makes this an allowlist } ] } ``` # Tempo examples Source: https://docs.privy.io/controls/policies/example-policies/tempo Tempo transactions carry fields that don't exist on standard EVM transactions. The `tempo_transaction` field source exposes these fields, giving policies control over Tempo-specific intents: fee token selection, transaction sponsorship, and validity windows. Standard `ethereum_transaction` conditions (`to`, `value`, `data`, and others) are enforced against all transactions on Tempo. Use `tempo_transaction` conditions on top of these to gate on Tempo-specific fields that don't exist on standard EVM transactions. Both field sources can be combined in the same rule. See [Ethereum examples](/controls/policies/example-policies/ethereum) for the full set of available `ethereum_transaction` fields. ## Require a specific fee token Use this policy when your app controls the gas token. For example, to ensure all transactions pay fees in your app's stablecoin rather than whichever token a wallet happens to hold. ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Restrict fee token to pathUSD', chain_type: 'ethereum', rules: [{ name: 'Only allow pathUSD as fee token', method: 'eth_sendTransaction', conditions: [ { field_source: 'tempo_transaction', field: 'fee_token', operator: 'eq', value: '0x' } ], action: 'ALLOW' }] } ``` The `in` operator supports multiple allowlisted fee tokens: ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Allowlist fee tokens', chain_type: 'ethereum', rules: [{ name: 'Allow pathUSD or USDC as fee token', method: 'eth_sendTransaction', conditions: [ { field_source: 'tempo_transaction', field: 'fee_token', operator: 'in', value: ['0x', '0x'] } ], action: 'ALLOW' }] } ``` ## Require sponsored transactions only Use this policy when your app sponsors gas for all users. It prevents wallets from submitting unsponsored transactions that would fail due to insufficient gas funds. ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Require sponsorship', chain_type: 'ethereum', rules: [{ name: 'Only allow transactions with a fee payer signature', method: 'eth_sendTransaction', conditions: [ { field_source: 'tempo_transaction', field: 'fee_payer_signature', operator: 'eq', value: 'true' } ], action: 'ALLOW' }] } ``` ## Block sponsored transactions Use this policy when wallets should always self-pay for gas. It prevents third-party fee payers from fronting costs, which can be useful for compliance or cost-control reasons. ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Block sponsored transactions', chain_type: 'ethereum', rules: [{ name: 'Deny transactions with a fee payer signature', method: 'eth_sendTransaction', conditions: [ { field_source: 'tempo_transaction', field: 'fee_payer_signature', operator: 'eq', value: 'true' } ], action: 'DENY' }] } ``` ## Time-bound a validity window Use these conditions to limit when transactions can be executed, such as requiring transactions to become valid after a future timestamp or expiring transactions that are not broadcast promptly. These fields control when the **chain accepts** a transaction, not when Privy broadcasts it. `valid_before` and `valid_after` are enforced by Tempo at execution time. This is distinct from [time-bound policies](/controls/policies/example-policies/timebound), which restrict when Privy will sign or send a transaction. `valid_before` and `valid_after` take Unix timestamps in seconds. Unset fields evaluate as `false` (no constraint). The `value` in a condition is a static timestamp computed at policy creation time. The example below computes a cutoff 5 minutes in the future. ```ts {skip-check} theme={"system"} const fiveMinutesFromNow = String(Math.floor(Date.now() / 1000) + 300); const policy = { version: '1.0', name: 'Enforce 5-minute transaction expiry', chain_type: 'ethereum', rules: [ { name: 'Only allow transactions expiring within 5 minutes of policy creation', method: 'eth_sendTransaction', conditions: [ { field_source: 'tempo_transaction', field: 'valid_before', operator: 'lte', value: fiveMinutesFromNow } ], action: 'ALLOW' } ] }; ``` Both fields together define a strict validity window: ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Strict validity window', chain_type: 'ethereum', rules: [{ name: 'Transaction must be valid within business hours window', method: 'eth_sendTransaction', conditions: [ { field_source: 'tempo_transaction', field: 'valid_after', operator: 'gte', value: '1757300000' }, { field_source: 'tempo_transaction', field: 'valid_before', operator: 'lte', value: '1757386400' } ], action: 'ALLOW' }] } ``` ## Restrict nonce key Tempo supports 2D nonces via `nonce_key`. A value of `0` uses the protocol-managed sequential nonce; values above `0` are user-managed nonces that allow parallel transaction submission across independent lanes. Use this policy for apps where transactions must execute in submission order. For example, an `approve` followed by a `transferFrom` will fail if the transfer lands first. For trading apps, an out-of-sequence limit order or a sell that lands before a buy produces incorrect results. Forcing `nonce_key` to `0` guarantees strict ordering at the cost of throughput. This policy forces all transactions to use the protocol nonce: ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Protocol nonce only', chain_type: 'ethereum', rules: [{ name: 'Restrict to protocol-managed nonce', method: 'eth_sendTransaction', conditions: [ { field_source: 'tempo_transaction', field: 'nonce_key', operator: 'eq', value: '0x0' } ], action: 'ALLOW' }] } ``` ## Composite rule: fee token and recipient `tempo_transaction` and `ethereum_transaction` conditions can be combined in a single rule. All conditions must pass for the rule to match. ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Controlled payments to allowlisted addresses', chain_type: 'ethereum', rules: [{ name: 'Allow only to known recipients and with approved fee token', method: 'eth_sendTransaction', conditions: [ { field_source: 'ethereum_transaction', field: 'to', operator: 'in', value: [ '0xRecipientAddress1', '0xRecipientAddress2' ] }, { field_source: 'tempo_transaction', field: 'fee_token', operator: 'eq', value: '0x' } ], action: 'ALLOW' }] } ``` # Time-bound examples Source: https://docs.privy.io/controls/policies/example-policies/timebound ## Enable a signer to take any wallet action until a certain date ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Time-bound signer policy', // to be set as override_policy for the signer chain_type: 'ethereum', rules: [{ name: 'Allow all actions before 9/8/2026', method: '*', conditions: [{ field_source: 'system', field: 'current_unix_timestamp', operator: 'lt', value: '1788840000' // 2026-09-08 00:00:00 UTC in seconds since epoch }], action: 'ALLOW' }] } ``` ## Only permit private key exports within a time window ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Only allow private key exports within a time window', chain_type: 'solana', rules: [ { name: 'Allow private key exports between 9/8/2025 and 10/8/2025', method: 'exportPrivateKey', conditions: [ { field_source: 'system', field: 'current_unix_timestamp', operator: 'gte', value: '1757304000' // 2025-09-08 00:00:00 UTC in seconds since epoch }, { field_source: 'system', field: 'current_unix_timestamp', operator: 'lte', value: '1759896000' // 2025-10-08 00:00:00 UTC in seconds since epoch } ], action: 'ALLOW' }, { name: 'Block all other actions', method: '*', conditions: [], action: 'DENY' } ] } ``` # Tron examples Source: https://docs.privy.io/controls/policies/example-policies/tron Tron policies support raw signing methods and Tron RPC transaction methods. * Use `signRawMessageBytes` for unparsed raw signing requests. * Use `signTransactionBytes` for parsed raw signing requests. * Use `tron_signTransaction` or `tron_sendTransaction` for the RPC transaction method the policy applies to. `signTransactionBytes`, `tron_sendTransaction`, and `tron_signTransaction` all use transaction bytes that evaluate `tron_transaction` or `tron_trigger_smart_contract_data` conditions. The decision to use a `TransferContract` or a `TriggerSmartContract` fields on Tron depends on the asset you are moving and whether you are interacting with a smart contract. * Use a `TransferContract` when you are performing a simple, direct transfer of native TRX between two standard accounts. * Use a `TriggerSmartContract` when you are interacting with any smart contract, which is necessary for transferring TRC-20 tokens and calling any function on a smart contract. ## Allowlist a specific TransferContract to\_address Use `tron_signTransaction` and `tron_sendTransaction` for structured Tron RPC transaction requests. ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "RPC TransferContract to_address", "chain_type": "tron", "rules": [ { "name": "Allow specific TransferContract to_address", "method": "tron_signTransaction", "conditions": [ { "field_source": "tron_transaction", "field": "TransferContract.to_address", "operator": "eq", "value": "TBia4uHnb3oSSZm5isP284cA7Np1v15Vhi" } ], "action": "ALLOW" }, { "name": "Allow specific TransferContract to_address", "method": "tron_sendTransaction", "conditions": [ { "field_source": "tron_transaction", "field": "TransferContract.to_address", "operator": "eq", "value": "TBia4uHnb3oSSZm5isP284cA7Np1v15Vhi" } ], "action": "ALLOW" } ] } ``` ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "TransferContract to_address", "chain_type": "tron", "rules": [ { "name": "Allow specific TransferContract to_address", "method": "signTransactionBytes", "conditions": [ { "field_source": "tron_transaction", "field": "TransferContract.to_address", "operator": "eq", "value": "TBia4uHnb3oSSZm5isP284cA7Np1v15Vhi" } ], "action": "ALLOW" } ] } ``` ## Configure a max TransferContract amount (TRX) ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "RPC TransferContract amount", "chain_type": "tron", "rules": [ { "name": "TransferContract amount maximum", "method": "tron_sendTransaction", "conditions": [ { "field_source": "tron_transaction", "field": "TransferContract.amount", "operator": "lt", "value": "10000000" } ], "action": "ALLOW" } ] } ``` ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "TransferContract amount", "chain_type": "tron", "rules": [ { "name": "TransferContract amount maximum", "method": "signTransactionBytes", "conditions": [ { "field_source": "tron_transaction", "field": "TransferContract.amount", "operator": "lt", "value": "10000000" } ], "action": "ALLOW" } ] } ``` ## Configure a max TriggerSmartContract call\_value (TRX) ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "RPC TriggerSmartContract call_value", "chain_type": "tron", "rules": [ { "name": "TriggerSmartContract call_value", "method": "tron_sendTransaction", "conditions": [ { "field_source": "tron_transaction", "field": "TriggerSmartContract.call_value", "operator": "lt", "value": "10000000" } ], "action": "ALLOW" } ] } ``` ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "TriggerSmartContract contract address", "chain_type": "tron", "rules": [ { "name": "TriggerSmartContract call_value", "method": "signTransactionBytes", "conditions": [ { "field_source": "tron_transaction", "field": "TriggerSmartContract.call_value", "operator": "lt", "value": "10000000" } ], "action": "ALLOW" } ] } ``` ## Disallow a certain TriggerSmartContract token\_id (TRC-10) ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "RPC TriggerSmartContract token id", "chain_type": "tron", "rules": [ { "name": "TriggerSmartContract token_id", "method": "tron_sendTransaction", "conditions": [ { "field_source": "tron_transaction", "field": "TriggerSmartContract.token_id", "operator": "eq", "value": "1000100" } ], "action": "DENY" } ] } ``` ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "TriggerSmartContract token id", "chain_type": "tron", "rules": [ { "name": "TriggerSmartContract token_id", "method": "signTransactionBytes", "conditions": [ { "field_source": "tron_transaction", "field": "TriggerSmartContract.token_id", "operator": "eq", "value": "1000100" } ], "action": "DENY" } ] } ``` ## Configure a max TriggerSmartContract call\_token\_value (TRC-10) ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "RPC TriggerSmartContract call_token_value", "chain_type": "tron", "rules": [ { "name": "TriggerSmartContract call_token_value", "method": "tron_sendTransaction", "conditions": [ { "field_source": "tron_transaction", "field": "TriggerSmartContract.call_token_value", "operator": "lt", "value": "10000000" } ], "action": "ALLOW" } ] } ``` ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "TriggerSmartContract call_token_value", "chain_type": "tron", "rules": [ { "name": "TriggerSmartContract call_token_value", "method": "signTransactionBytes", "conditions": [ { "field_source": "tron_transaction", "field": "TriggerSmartContract.call_token_value", "operator": "lt", "value": "10000000" } ], "action": "ALLOW" } ] } ``` ## Allow a TRC-20 transfer with ABI decoding ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "RPC TRC-20 transfer with ABI", "chain_type": "tron", "rules": [ { "name": "Allow specific TRC-20 recipient", "method": "tron_sendTransaction", "conditions": [ { "field_source": "tron_transaction", "field": "TriggerSmartContract.contract_address", "operator": "eq", "value": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t" }, { "field_source": "tron_trigger_smart_contract_data", "field": "transfer.to", "operator": "eq", "value": "TG3XXyExBkPp9nzdajDZsozEu4BkaSJozs", "abi": [ { "name": "transfer", "type": "function", "inputs": [{"name": "to", "type": "address"}] } ] } ], "action": "ALLOW" } ] } ``` ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "TRC-20 transfer with ABI", "chain_type": "tron", "rules": [ { "name": "Allow specific TRC-20 recipient", "method": "signTransactionBytes", "conditions": [ { "field_source": "tron_transaction", "field": "TriggerSmartContract.contract_address", "operator": "eq", "value": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t" }, { "field_source": "tron_trigger_smart_contract_data", "field": "transfer.to", "operator": "eq", "value": "TG3XXyExBkPp9nzdajDZsozEu4BkaSJozs", "abi": [ { "name": "transfer", "type": "function", "inputs": [{"name": "to", "type": "address"}] } ] } ], "action": "ALLOW" } ] } ``` ## Allow a certain owner address to transfer TRC20 token ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "RPC TriggerSmartContract with ABI", "chain_type": "tron", "rules": [ { "name": "TriggerSmartContract transferFrom._from", "method": "tron_sendTransaction", "conditions": [ { "field_source": "tron_trigger_smart_contract_data", "field": "transferFrom._from", "operator": "eq", "value": "THscQ8SwfcD7tdts9cnMxTnH2hwvpX3ujy", "abi": [ { "type": "function", "name": "transferFrom", "inputs": [ { "name": "_from", "type": "address" }, { "name": "_to", "type": "address" }, { "name": "_value", "type": "uint256" } ], "outputs": [ { "type": "bool" } ], "stateMutability": "nonpayable" } ] } ], "action": "ALLOW" } ] } ``` ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "TriggerSmartContract with ABI", "chain_type": "tron", "rules": [ { "name": "TriggerSmartContract transferFrom._from", "method": "signTransactionBytes", "conditions": [ { "field_source": "tron_trigger_smart_contract_data", "field": "transferFrom._from", "operator": "eq", "value": "THscQ8SwfcD7tdts9cnMxTnH2hwvpX3ujy", "abi": [ { "type": "function", "name": "transferFrom", "inputs": [ { "name": "_from", "type": "address" }, { "name": "_to", "type": "address" }, { "name": "_value", "type": "uint256" } ], "outputs": [ { "type": "bool" } ], "stateMutability": "nonpayable" } ] } ], "action": "ALLOW" } ] } ``` ## Allow transfers to a specific address with a specific contract address This is an example of mixing TriggerSmartContract and ABI configurations. ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "RPC Allow a specific address with a specific contract", "chain_type": "tron", "rules": [ { "name": "Allow a specific address with a specific contract", "method": "tron_sendTransaction", "conditions": [ { "field_source": "tron_transaction", "field": "TriggerSmartContract.contract_address", "operator": "eq", "value": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t" }, { "field_source": "tron_trigger_smart_contract_data", "field": "transfer.to", "operator": "eq", "value": "TG3XXyExBkPp9nzdajDZsozEu4BkaSJozs", "abi": [ { "name": "transfer", "type": "function", "inputs": [{"name": "to", "type": "address"}] } ] } ], "action": "ALLOW" } ] } ``` ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "Allow a specific address with a specific contract", "chain_type": "tron", "rules": [ { "name": "Allow a specific address with a specific contract", "method": "signTransactionBytes", "conditions": [ { "field_source": "tron_transaction", "field": "TriggerSmartContract.contract_address", "operator": "eq", "value": "TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t" }, { "field_source": "tron_trigger_smart_contract_data", "field": "transfer.to", "operator": "eq", "value": "TG3XXyExBkPp9nzdajDZsozEu4BkaSJozs", "abi": [ { "name": "transfer", "type": "function", "inputs": [{"name": "to", "type": "address"}] } ] } ], "action": "ALLOW" } ] } ``` ## Denylist recipients with condition sets ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "RPC Denylist TransferContract to_address", "chain_type": "tron", "rules": [ { "name": "Denylist TransferContract to_address", "method": "tron_sendTransaction", "conditions": [ { "field_source": "tron_transaction", "field": "TransferContract.to_address", "operator": "in_condition_set", "value": "a2p4etpcbj2dltbjfigybi8j" } ], "action": "DENY" } ] } ``` ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "Denylist TransferContract to_address", "chain_type": "tron", "rules": [ { "name": "Denylist TransferContract to_address", "method": "signTransactionBytes", "conditions": [ { "field_source": "tron_transaction", "field": "TransferContract.to_address", "operator": "in_condition_set", "value": "a2p4etpcbj2dltbjfigybi8j" } ], "action": "DENY" } ] } ``` ## Only allow raw signing after a certain start date Use `signRawMessageBytes` for unparsed raw signing requests. `signRawMessageBytes` rules support system conditions, but do not support decoded transaction field sources like `tron_transaction` or `tron_trigger_smart_contract_data`. ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "Only allow raw signing after a certain start date", "chain_type": "tron", "rules": [ { "name": "Only allow raw signing after a certain start date", "method": "signRawMessageBytes", "conditions": [ { "field_source": "system", "field": "current_unix_timestamp", "operator": "gt", "value": "1757304000" // 2025-09-08 00:00:00 UTC in seconds since epoch } ], "action": "ALLOW" } ] } ``` ## Only allow transactions after a certain start date ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "RPC Only allow transactions after a certain start date", "chain_type": "tron", "rules": [ { "name": "Only allow transactions after a certain start date", "method": "tron_sendTransaction", "conditions": [ { "field_source": "system", "field": "current_unix_timestamp", "operator": "gt", "value": "1757304000" // 2025-09-08 00:00:00 UTC in seconds since epoch } ], "action": "ALLOW" } ] } ``` ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "Only allow transactions after a certain start date", "chain_type": "tron", "rules": [ { "name": "Only allow transactions after a certain start date", "method": "signTransactionBytes", "conditions": [ { "field_source": "system", "field": "current_unix_timestamp", "operator": "gt", "value": "1757304000" // 2025-09-08 00:00:00 UTC in seconds since epoch } ], "action": "ALLOW" } ] } ``` ## Allow transfers to a specific address after a certain timestamp This is an example of mixing TransferContract and System configurations. ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "RPC Allow specific address after a certain timestamp", "chain_type": "tron", "rules": [ { "name": "Allow specific address after a certain timestamp", "method": "tron_sendTransaction", "conditions": [ { "field_source": "tron_transaction", "field": "TransferContract.to_address", "operator": "eq", "value": "TG3XXyExBkPp9nzdajDZsozEu4BkaSJozs" }, { "field_source": "system", "field": "current_unix_timestamp", "operator": "gt", "value": "1757304000" // 2025-09-08 00:00:00 UTC in seconds since epoch } ], "action": "ALLOW" } ] } ``` ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "Allow specific address after a certain timestamp", "chain_type": "tron", "rules": [ { "name": "Allow specific address after a certain timestamp", "method": "signTransactionBytes", "conditions": [ { "field_source": "tron_transaction", "field": "TransferContract.to_address", "operator": "eq", "value": "TG3XXyExBkPp9nzdajDZsozEu4BkaSJozs" }, { "field_source": "system", "field": "current_unix_timestamp", "operator": "gt", "value": "1757304000" // 2025-09-08 00:00:00 UTC in seconds since epoch } ], "action": "ALLOW" } ] } ``` ## Allow transfers to a specific address with ABI after a certain timestamp This is an example of mixing TriggerSmartContract with ABI and System configurations. ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "RPC Allow specific address with ABI after a timestamp", "chain_type": "tron", "rules": [ { "name": "Allow specific address with ABI after a timestamp", "method": "tron_sendTransaction", "conditions": [ { "field_source": "tron_trigger_smart_contract_data", "field": "transfer.to", "operator": "eq", "value": "TG3XXyExBkPp9nzdajDZsozEu4BkaSJozs", "abi": [ { "name": "transfer", "type": "function", "inputs": [{"name": "to", "type": "address"}] } ] }, { "field_source": "system", "field": "current_unix_timestamp", "operator": "gt", "value": "1757304000" // 2025-09-08 00:00:00 UTC in seconds since epoch } ], "action": "ALLOW" } ] } ``` ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "Allow specific address with ABI after a timestamp", "chain_type": "tron", "rules": [ { "name": "Allow specific address with ABI after a timestamp", "method": "signTransactionBytes", "conditions": [ { "field_source": "tron_trigger_smart_contract_data", "field": "transfer.to", "operator": "eq", "value": "TG3XXyExBkPp9nzdajDZsozEu4BkaSJozs", "abi": [ { "name": "transfer", "type": "function", "inputs": [{"name": "to", "type": "address"}] } ] }, { "field_source": "system", "field": "current_unix_timestamp", "operator": "gt", "value": "1757304000" // 2025-09-08 00:00:00 UTC in seconds since epoch } ], "action": "ALLOW" } ] } ``` ## Deny all requests ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "Example policy to deny all requests", "chain_type": "tron", "rules": [ { "name": "Deny all requests", "method": "*", "conditions": [], "action": "DENY" } ] } ``` ## Anti patterns ### Avoid adding rules that may override other rules ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "Restrict the maximum value of TRX transfers", "chain_type": "tron", "rules": [ { // This rule restricts the value of TRX transfers. "name": "Restrict TRX transfers to 1 TRX (in sun)", "method": "tron_sendTransaction", "conditions": [ { "field_source": "tron_transaction", "field": "TransferContract.amount", "operator": "lte", "value": "1000000" // 1 TRX = 1,000,000 sun } ], "action": "ALLOW" }, { // This rule will override the previous rule by allowing a 5 TRX transfer. "name": "Restrict TRX transfers to 5 TRX (in sun)", "method": "tron_sendTransaction", "conditions": [ { "field_source": "tron_transaction", "field": "TransferContract.amount", "operator": "lte", "value": "5000000" // 5 TRX = 5,000,000 sun } ], "action": "ALLOW" } ] } ``` # Get a policy Source: https://docs.privy.io/controls/policies/get-a-policy Use the **`PrivyClient`**'s **`get`** method from the `policies()` interface to get a policy by its ID. ```tsx theme={"system"} const policy = await privy.policies().get('fmfdj6yqly31huorjqzq38zc'); ``` You can get a policy using the Java SDK by using the `policies().retrieve()` method. ```java theme={"system"} try { PolicyRetrieveResponse response = privyClient .policies() .retrieve("fmfdj6yqly31huorjqzq38zc"); if (response.policy().isPresent()) { Policy policy = response.policy().get(); String policyId = policy.id(); } } catch (APIException e) { String errorBody = e.bodyAsString(); System.err.println(errorBody); } catch (Exception e) { System.err.println(e.getMessage()); } ``` ### Parameters The ID of the policy to retrieve. ### Returns The `PolicyRetrieveResponse` object contains an optional `policy()` field that contains the retrieved policy if the policy was retrieved successfully. The retrieved policy. Version of the policy. Name of the policy. Chain type of the wallets that the policy will be applied to. Unique ID of the policy. The key quorum ID of the owner of the policy. The Unix time of when the policy was created. A list of `Rule` objects describing what rules to apply to each RPC method (e.g. `'eth_sendTransaction'`) that the wallet can take. [Learn more about `Rules`](/controls/policies/overview#rules). Use the **`PrivyClient`**'s **`get`** method from the `policies()` interface to get a policy by its ID. ```rust theme={"system"} use privy_rs::PrivyClient; let client = PrivyClient::new(app_id, app_secret)?; let policy = client .policies() .get("fmfdj6yqly31huorjqzq38zc") .await?; println!("Policy: {}", policy.name); println!("Rules: {:?}", policy.rules); ``` ### Parameters and Returns See the Rust SDK documentation for detailed parameter and return types, including embedded examples: * [PoliciesClient::get](https://docs.rs/privy-rs/latest/privy_rs/subclients/struct.PoliciesClient.html#method.get) For REST API details, see the [API reference](/api-reference/policies/get). To get a policy with the Go SDK, use the `Get` method on the `Policies` service. ### Usage ```go theme={"system"} policy, err := client.Policies.Get(context.Background(), "policy-id") if err != nil { log.Fatalf("failed to get policy: %v", err) } fmt.Println("Policy:", policy.ID, policy.Name) ``` ### Parameters and Returns See the [API reference](/api-reference/policies/get) for more details. To get a policy with the Ruby SDK, use the `get` method on the `policies` service. ### Usage ```ruby theme={"system"} policy = client.policies.get("policy-id") puts(policy.id, policy.name) ``` ### Parameters and Returns See the [API reference](/api-reference/policies/get) for more details. To get a policy by its ID, make a `GET` request to: ```bash theme={"system"} https://api.privy.io/v1/policies/ ``` Replacing `` with the ID of your desired policy. ## **Response** A successful response will return the following fields: Unique ID for the policy. Version of the policy. Currently, 1.0 is the only version. Name to assign to policy. Chain type for wallets that the policy will be applied to. A list of `Rule` objects describing what rules to apply to each RPC method (e.g. `'eth_sendTransaction'`) that the wallet can take. [Learn more about `Rules`](/controls/policies/overview#rules). The key quorum ID of the owner of the policy, whose signature is required to modify the policy. ## Example A sample request to fetch a policy with ID `fmfdj6yqly31huorjqzq38zc` looks like: ```bash theme={"system"} curl --request GET https://api.privy.io/v1/policies/fmfdj6yqly31huorjqzq38zc \ -u ":" \ -H "privy-app-id: " ``` ## Response A successful response will look like the following: ```json theme={"system"} { "id": "fmfdj6yqly31huorjqzq38zc", "name": "Allowlist certain smart contracts", "version": "1.0", "chain_type": "ethereum", "rules": [ { "name": "Allowlist USDC", "method": "eth_sendTransaction", "conditions": [ { "field_source": "ethereum_transaction", "field": "to", "operator": "eq", "value": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" } ], "action": "ALLOW" } ], "owner_id": "fmfdj6yqly31huorjqzq38zc" } ``` # Overview Source: https://docs.privy.io/controls/policies/overview Overview of Privy policies for restricting wallet actions like transfers, swaps, and signing. Privy’s policy engine gives your application programmable control over how every wallet can be used. Instead of relying on ad-hoc checks in your code, you can define enforceable rules at the key level that govern what actions a wallet may take. With policies, you can configure: * Transfer limits * Time-bound signers * Allowlists and denylists of transfer recipients * Allowlists and denylists of smart contracts and programs * Allowlists and denylists of networks * Allowed time window for key export * Granular constraints around calldata and parameters that can be passed to smart contracts * Restrictions around signatures needed for transactions, such as EVM typed data (EIP712) This allows teams to define security, compliance, and behavioral rules that are applied consistently across all wallets in production. Managing policies in the Privy Dashboard # Concepts Privy's policy engine is defined by three core primitives: **policies**, **rules**, and **conditions**; and is designed to give developers precise control over wallets behavior. At a high-level: * A **policy** is the complete set of constraints that govern a wallet. It is a list of rules that define the total set of actions that are allowed or denied for a wallet. * A **rule** describes when an action should be allowed or denied. Each rule is composed of one or more conditions. When a request satisfies all of the conditions, the policy engine executes the action (`ALLOW` or `DENY`) prescribed by the rule. * A **condition** is a Boolean expression that the policy engine evaluates against an incoming RPC request. This structure makes Privy’s policy engine flexible enough for complex workflows and predictable enough for production environments. You can create and manage policies through the [Privy Dashboard](https://dashboard.privy.io), `nodeJS` [SDK](/controls/policies/create-a-policy), or via the [REST API](/controls/policies/create-a-policy). ## Policies A **policy** is composed from a **list of rules for each RPC method that a wallet can execute** that define what actions are allowed or denied for the wallet. `DENY` actions take precedence over `ALLOW` actions. If no rules resolve, the policy will default to `DENY`. ### Allowlisted RPCs and wallet actions If a wallet's policy **does not** include a rule for a given RPC method or wallet action API, **usage of that RPC method or API will be denied.** If a policy is set on a wallet, the policy **must** include a rule for any RPC methods or wallet action APIs that the wallet intends to use. Policy objects have the following properties: Version of the policy. Currently, 1.0 is the only version. Name to assign to policy. Chain type for wallets that the policy will be applied to. A list of `Rule` objects describing what rules to apply to each RPC method (e.g. `'eth_sendTransaction'`) that the wallet can take. ### Policy evaluation By default, the trusted execution environment (secure enclave) enforces policies when processing wallet actions, such as signature requests, transactions, and key export. The enclave evaluates policy rules in a tamper-proof environment before any operations proceed. Privy enforces some policies at the API level. For example, limiting transfer sizes requires transaction simulation which runs outside the enclave today. For operations where Privy both signs and broadcasts a transaction, transaction simulation runs before policy evaluation. If simulation fails — for example, because the wallet has insufficient funds or the contract call would revert — Privy returns the simulation error and does not evaluate the policy. If a request would both fail simulation and violate a policy, the response reflects the simulation failure rather than a policy violation. Privy does not sign or broadcast the transaction in this case. When your application makes an RPC request on a wallet that has a policy, the policy engine evaluates the `rules` that are associated with the requested RPC method. For instance, if your application makes an `'eth_signTransaction'` request, the policy engine will only evaluate rules associated with the `'eth_signTransaction'` method in the policy. The rules are evaluated as follows: 1. If **any** rule evaluates to a `DENY` action, the policy engine will `DENY` the request. 2. If **any** rule evaluates to an `ALLOW` action, and **no** rules evaluate to `DENY`, then the policy engine will `ALLOW` the request. If the request does not satisfy *any* of the rules for the policy, the policy engine defaults to `DENY` the request. This also applies to Solana transactions such that every Instruction in a Solana transaction is evaluated against the rules of the policy. Every instruction must evaluate to an `ALLOW` action for the transaction to be allowed. If your application makes a request to a wallet with RPC method `X`, and the policy's `rules` contains no entry with a `method` corresponding to `X`, the engine will deny the request by default. If you'd like the policy engine to instead allow requests for RPC method `X` by default, we recommend setting up an "Allow all" `Rule` for that RPC method [like so](/controls/policies/example-policies/ethereum#allow-all-requests-for-a-given-rpc-method). ## Rules The nested `Rule` object within the policy's `rules` array. A **rule** is composed of an set of boolean **conditions** and an **action** (`ALLOW` or `DENY`) that is taken if an RPC request satisfies all of the conditions in the rule. Rule objects have the following fields: Name to assign to the rule. Method to apply the `conditions` to. If an RPC method, must correspond to the `chain_type` of the parent policy. Methods `signRawMessageBytes` and `signTransactionBytes` are applicable to Tron and Sui only. Use `signRawMessageBytes` for unparsed raw signing requests. Use `signTransactionBytes` for parsed transaction bytes that use Tron or Sui transaction field sources. XRPL policies only support the `xrpl_signTransaction`, `exportPrivateKey`, and `exportSeedPhrase` methods — `*` is not supported for the `xrpl` chain type. A set of boolean conditions that define the action the rule allows or denies. For `exportPrivateKey`, leave `conditions` empty. Whether the rule should allow or deny a wallet request if it satisfies all of the rule's `conditions`. Each rule corresponds to an individual action that should be allowed or denied by a wallet. For example, you might configure rules for a policy to: * `ALLOW` transfers of the native token to a set of allowlisted recipient addresses * `DENY` interactions with specific Ethereum smart contracts or Solana programs ## Conditions A **condition** is a boolean statement about a wallet request. When evaluating a wallet request against a rule, the policy engine checks whether the wallet request satisfies each of the boolean conditions in the rule. If all of the conditions are satisfied, the engine executes the action associated with the rule. Conditions allow you to define specific action types that should be allowed or denied for a wallet. Data source from which to derive the `field` for the condition. The attribute to evaluate for a wallet request. As an example, the field for the recipient of an EVM transaction is `'to'`. Contract ABI to decode Ethereum or Tron calldata against. Should only be set for `'ethereum_calldata'` or `'tron_trigger_smart_contract_data'` policies. Must strictly be formatted as JSON. Boolean operator used to compare a `field` with a `value` Static value to compare a `field` to. Conditions for certain sources may have additional parameters. For instance, `ethereum_calldata` and `tron_trigger_smart_contract_data` conditions also require an `abi` parameter used to decode the calldata, and `ethereum_typed_data_message` conditions require a `typed_data` parameter to define the schema for the typed data message. ### Field **Fields** are attributes of a wallet request that can be parsed or interpreted from the wallet request. Examples of fields include the `to` parameter of an EVM transaction, the `fee_payer` parameter of a Solana transaction, or an `spl_transfer_recipient` field that is populated when the policy engine interprets a transaction. Fields are derived from **field sources**, which surface data from the wallet request. Possible field sources are listed below. The policy engine evaluates numerical data exactly as passed in the request body—no conversion is applied. For example, ETH values will be evaluated in wei, SOL in lamports, and USDC in microdollars. | Field source | Description | Example fields | | ------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `'ethereum_transaction'` | The verbatim Ethereum transaction object in an `eth_signTransaction`, `eth_sendTransaction`, `eth_signUserOperation`, or `wallet_sendCalls` request. | `to`, `value`, `chain_id` | | `'ethereum_calldata'` | The decoded calldata in a smart contract interaction, with fields representing both the function name and, if applicable, function arguments. Note: `'ethereum_calldata'` conditions must always include an `abi` parameter with the contract's JSON ABI—even for functions with no input parameters (such as `deposit()`). The value of `field` can be just the function name (e.g., `function_name`) to match any call to a given function, or the function name plus argument (e.g., `function_name.param_name`) to match a specific parameter. | `function_name` (e.g., allow any call to `deposit()`), `function_name._to`, `function_name._value` (for ERC20 and similar interactions) | | `'ethereum_typed_data_domain'` | Attributes from the signing domain that will verify the signature. | `chainId`, `verifyingContract` | | `'ethereum_typed_data_message'` | `types` and `primary_type` attributes of the TypedData JSON object defined in [EIP-712](https://eips.ethereum.org/EIPS/eip-712#specification-of-the-eth_signtypeddata-json-rpc). | dot-separated path to value in `message` object, i.e. `to.wallet` | | `'ethereum_7702_authorization'` | EIP-7702 authorization data from an `eth_sign7702Authorization` request. | `contract` | | `'solana_program_instruction'` | Solana program instruction from a `signTransaction` or `signAndSendTransaction` request. | `programId` | | `'solana_system_program_instruction'` | Fields relevant to the Solana System Program and its Transfer instruction. | `instructionName`, `Transfer.to`, `Transfer.from`, `Transfer.lamports` | | `'solana_token_program_instruction'` | Fields relevant to the SPL Token Program and its supported instructions: Transfer, TransferChecked, Burn, MintTo, CloseAccount, and InitializeAccount3. | `instructionName`, `Transfer.source`, `Transfer.destination`, `Transfer.authority`, `Transfer.amount`, `TransferChecked.source`, `TransferChecked.destination`, `TransferChecked.authority`, `TransferChecked.amount`, `Burn.account`, `Burn.mint`, `Burn.authority`, `Burn.amount`, `MintTo.mint`, `MintTo.account`, `MintTo.authority`, `MintTo.amount`, `CloseAccount.account`, `CloseAccount.destination`, `CloseAccount.authority`, `InitializeAccount3.account`, `InitializeAccount3.mint`, `InitializeAccount3.authority` | | `'tron_transaction'` | The Tron transaction object in a `signTransactionBytes` raw signing request or a `tron_signTransaction`/`tron_sendTransaction` RPC policy rule. | `TransferContract.to_address`, `TransferContract.amount`, `TriggerSmartContract.contract_address`, `TriggerSmartContract.call_value`, `TriggerSmartContract.token_id`, `TriggerSmartContract.call_token_value` | | `'tron_trigger_smart_contract_data'` | The decoded calldata in a smart contract interaction as the smart contract method's parameters. Applies to `signTransactionBytes` raw signing requests and `tron_signTransaction`/`tron_sendTransaction` RPC policy rules. Note that `'tron_trigger_smart_contract_data'` conditions must contain an `abi` parameter with the JSON ABI of the smart contract. | `function_name`, `_to`, `_value` (for a TRC20 interaction) | | `'sui_transaction_command'` | Commands in a Sui transaction in an `signTransactionBytes` request. | `TransferObjects`, `SplitCoins`, `MergeCoins` | | `'sui_transfer_objects_command'` | The Sui TransferObjects command in an `signTransactionBytes` request. Sends one or more objects to a specified address. | `amount`, `recipient` | | `'xrpl_transaction'` | The decoded XRPL transaction in an `xrpl_signTransaction` RPC policy rule. Supported on `Payment`, `OfferCreate`, `OfferCancel`, and `TrustSet` transaction types. See [XRPL policy examples](/controls/policies/example-policies/xrpl) for the full list of supported fields. | `TransactionType`, `Payment.Destination`, `Payment.Amount.drops`, `OfferCreate.TakerPays.value`, `TrustSet.LimitAmount.currency` | | `'message'` | The decoded message content in a message signing request. Applicable to `personal_sign` (Ethereum), `signMessage` (Solana), and `signRawMessageBytes` (Sui). Supports string operators (`eq`, `contains`, `starts_with`, `ends_with`, `in`, `in_condition_set`) on the `content` field, and numeric operators (`eq`, `gt`, `gte`, `lt`, `lte`) on the `byte_length` field. | `content`, `byte_length` | | `'tempo_transaction'` | Tempo transaction specific fields from an `eth_signTransaction` or `eth_sendTransaction` request. Kept separate from `'ethereum_transaction'` so existing EVM rules are unaffected. See [Tempo policy examples](/controls/policies/example-policies/tempo) for usage. | `fee_token` (address), `fee_payer_signature` (presence), `nonce_key` (bigint), `valid_before` (bigint, Unix seconds), `valid_after` (bigint, Unix seconds), `aa_authorization_list` (presence), `access_list` (presence) | | `'action_request_body'` | Fields from the original wallet action request body. For wallet action APIs (e.g. [Transfer](/wallets/actions/transfer/policies), [Earn](/wallets/actions/earn/policies)), policy evaluation runs against the request body sent to the API before Privy prepares the underlying transactions. | Transfer: `source.asset`, `source.asset_address`, `source.amount`, `source.chain`, `destination.address`, `destination.asset`, `destination.chain`; Earn: `vault_id`, `amount`, `raw_amount` | | `'system'` | Chain-agnostic system fields, such as the current timestamp at the time of the request. | `current_unix_timestamp` | | `'reference'` | A reference to an Aggregation for [stateful policies](/controls/policies/stateful-policies). Allows a condition to evaluate against historical data such as cumulative transaction values over a time window. Each condition can reference one Aggregation. Only supported for Ethereum methods (`eth_signTransaction`, `eth_signUserOperation`). | `aggregation.{aggregation_id}` (e.g., `aggregation.abc123def456ghi789`) | ### Operator **Operators** are boolean operators used to compare fields and values. Operators include `eq`, `neq`, `lt`, `lte`, `gt`, `gte`, `in`, `in_condition_set`, `contains`, `starts_with`, and `ends_with`. All string comparisons are case-sensitive. The `in` operator can be configured with up to 100 values. Consider `in_condition_set` operator if you need more. ### Values A condition compares a field using its boolean operator to a static **value**. As an example, if a condition determines whether an Ethereum transaction has specific recipient address `X`, the value for the condition is `X`. # Stateful policies Source: https://docs.privy.io/controls/policies/stateful-policies # Overview Aggregations enable tracking and persistence of metric data from RPC requests over time. They allow policy rules to evaluate wallet activity against historical data, enabling more sophisticated controls like rate limiting and spending caps. Together with policies, aggregations make it possible to express constraints that depend on cumulative behavior rather than just the current request. This approach offers several benefits: * **Rate limiting**: Enforce transaction volume limits over time windows * **Spending caps**: Limit total value transferred within a period * **Activity monitoring**: Track transaction patterns across chain IDs or contract addresses * **Dynamic policies**: Create rules that adapt based on historical wallet behavior ## Concepts Aggregations are defined by four core primitives: aggregations, metrics, windows, and group-by fields. At a high-level: * **Aggregations** define what data to track from RPC requests and how to aggregate it over time * **Metrics** specify which field to extract from requests and how to aggregate values (e.g., sum transaction values) * **Windows** define the time period over which to aggregate data (e.g., rolling 1-hour windows) * **Conditions** define pre-filters that determine which requests should be included in the aggregation (e.g., only transactions to a specific contract) * **Group-by fields** optionally partition aggregations by specific attributes (e.g., by chain ID or recipient) Once created, aggregations can be referenced in policy conditions using the `reference` field source. When a condition uses `field_source: 'reference'` and `aggregation.{aggregation_id}` as the `field`, the policy engine evaluates the current aggregated value against the specified threshold. ## Create aggregations Refer to the [API reference](/api-reference/aggregations/create) for creating aggregations. Aggregations are associated with the app that creates them. Each aggregation tracks data for RPC requests made through that app's wallets. Each app can have a maximum of **10 aggregations**. Plan your aggregation strategy carefully to stay within this limit. ## How aggregations are applied When a wallet receives an RPC request, aggregations are processed in two phases: **data collection** and **policy evaluation**. ### Data collection When a request is made, **all aggregations** referenced in the wallet's policies are updated. Multiple policies can reference the same aggregation, which is useful for sharing limits across different policy types (e.g., multiple signer override policies can reference the same aggregation to limit total transfer value out of a wallet in a rolling time window). This includes aggregations from: * The wallet's directly assigned policy * Owner policies (if the wallet has an owner) * Signer override policies (if the request includes a signer with an override policy) For each aggregation: 1. The aggregation's `conditions` are evaluated against the request. If all conditions pass, the aggregation proceeds to value extraction. 2. The metric value is extracted from the request based on the aggregation's `metric` configuration. 3. If the value **cannot be extracted** (e.g., the field doesn't exist or the calldata doesn't match the ABI), the value defaults to **0**. Be careful when defining metrics: if you want the limit to apply to multiple operations (e.g., both `approve()` and `transfer()` calls against a contract ABI), you must configure separate aggregations or conditions for each operation. 4. The extracted value is added to the aggregation's running total for the current time bucket. Aggregation data is collected for all matching aggregations, regardless of whether the policy ultimately allows or denies the request. This ensures accurate tracking even when requests are denied for other reasons. Aggregation values are updated **after** a request is successfully signed, not before. This means multiple concurrent requests may all pass policy evaluation before any of their values are recorded. Stateful policies are designed for **disaster prevention** (e.g., catching runaway scripts or limiting blast radius) rather than strict real-time enforcement. To mitigate concurrency risks, combine aggregation-based limits with lower per-transaction thresholds and rate limit your application's request throughput. ### Policy evaluation When evaluating a policy condition that references an aggregation: 1. The policy engine retrieves the current aggregated value for the time window. 2. If group-by fields are configured, the engine uses the group key derived from the current request. 3. If the metric value **cannot be extracted** from the current request, the policy engine uses the **existing aggregated value** (without including the current request). 4. If the metric value **can be extracted**, the policy engine uses the aggregated value **plus the current request's value**. 5. The total is compared against the condition's threshold using the specified operator. This means policy evaluation is "forward-looking" — it considers what the aggregated value would be **after** the current request is processed, not just the historical total. If an aggregation is deleted, any policy conditions that reference it will evaluate to `false`, which may result in requests being denied. ## Supported RPC methods Aggregations can be configured for the following RPC methods: | Method | Chain | Description | | ----------------------- | -------- | ------------------------------------- | | `eth_signTransaction` | Ethereum | Standard Ethereum transaction signing | | `eth_signUserOperation` | Ethereum | ERC-4337 user operation signing | ## Metric configuration The `metric` object defines what value to extract from requests: The field to extract from the request. For example, `'value'` for transaction value. The data source from which to derive the field. Must correspond to the aggregation's `method`. The aggregation function to apply. Currently, only `'sum'` is supported. Contract ABI to decode calldata against. Required when `field_source` is `'ethereum_calldata'`. ### Supported field sources | Field source | Description | Example fields | | ------------------------ | -------------------------------------- | ----------------------------------------------- | | `'ethereum_transaction'` | Direct transaction fields | `value`, `chain_id` | | `'ethereum_calldata'` | Decoded calldata fields (requires ABI) | `function_name._amount`, `function_name._value` | ## Window configuration The `window` object defines the time bucketing strategy: The bucketing type. Currently, only `'rolling'` windows are supported. The duration of the time window in seconds. Minimum value is `3600` (1 hour), and maximum is `259200` (72 hours). ## Conditions configuration The optional `conditions` array defines pre-filters that determine which requests should be included in the aggregation. Only requests that match all conditions will have their metric values aggregated. The data source from which to derive the field for the condition. The field to evaluate. For example, `'to'` for the transaction recipient or `'chain_id'` for the chain ID. The comparison operator to use. The `in_condition_set` operator is supported for aggregation conditions. The value to compare against. Use an array for the `'in'` operator. Contract ABI to decode calldata against. Required when `field_source` is `'ethereum_calldata'`. Use conditions to scope aggregations to specific contracts, chains, or transaction types. For example, you can track ERC-20 transfers to a specific token contract by adding a condition that checks the `to` field. ## Group-by configuration The optional `group_by` array partitions aggregations by specific fields: The field to group by. For example, `'chain_id'`. The data source from which to derive the grouping field. When group-by fields are configured, separate aggregation buckets are maintained for each unique combination of group-by values. ## Examples ### Limit USDC transfers per recipient on Base This example demonstrates a rolling spending cap that tracks USDC transfer amounts on a per-recipient basis. It uses conditions to scope the aggregation to only USDC transfers on Base, group-by to partition totals by recipient address, and a 24-hour rolling window. **Step 1: Create the aggregation** Create an aggregation that tracks the sum of USDC `transfer` amounts, filtered to the USDC contract on Base, grouped by the `recipient` parameter in the calldata. Refer to the [API reference](/api-reference/aggregations/create) for the full request details. ```json theme={"system"} { "method": "eth_signTransaction", "metric": { "field": "transfer.amount", "field_source": "ethereum_calldata", "function": "sum", "abi": [ { "inputs": [ {"internalType": "address", "name": "recipient", "type": "address"}, {"internalType": "uint256", "name": "amount", "type": "uint256"} ], "name": "transfer", "outputs": [{"internalType": "bool", "name": "", "type": "bool"}], "stateMutability": "nonpayable", "type": "function" } ] }, "window": { "type": "rolling", "seconds": 86400 }, "conditions": [ { "field_source": "ethereum_transaction", "field": "to", "operator": "eq", "value": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" }, { "field_source": "ethereum_transaction", "field": "chain_id", "operator": "eq", "value": "8453" } ], "group_by": [ { "field": "transfer.recipient", "field_source": "ethereum_calldata" } ] } ``` This returns an aggregation object with an `id` (e.g., `cmtd4d5i10bf94m5m2o8tp`). **Step 2: Reference the aggregation in a policy** Use the aggregation ID in a policy condition with `field_source: 'reference'` to enforce a per-recipient cap of 1000 USDC (1000e6 = `0x3B9ACA00` in hex) within the rolling 24-hour window. ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Per-recipient USDC spending cap on Base', chain_type: 'ethereum', rules: [ { name: 'Allow USDC transfers under 1000 USDC per recipient per 24h', method: 'eth_signTransaction', conditions: [ { field_source: 'ethereum_transaction', field: 'to', operator: 'eq', value: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' // USDC on Base }, { field_source: 'ethereum_transaction', field: 'chain_id', operator: 'eq', value: '8453' // Base }, { field_source: 'reference', field: 'aggregation.gml7d33d3i10bf94m5m2o8tc', // replace with your aggregation ID operator: 'lte', value: '0x3B9ACA00' // 1000 USDC (6 decimals) } ], action: 'ALLOW' } ] } ``` With this configuration, each unique recipient address has its own rolling 24-hour budget of 1000 USDC. A wallet can send 500 USDC to address A and 800 USDC to address B in the same window, since the limits are tracked independently per recipient. ## Recipes Step-by-step implementation of a rolling USDC spending cap on a server wallet using stateful aggregation policies. ### Limit total ETH spending over a rolling 24-hour window This example caps the total native ETH value across all transactions and chains using a global 24-hour rolling window with no group-by partitioning. **Step 1: Create the aggregation** ```json theme={"system"} { "method": "eth_signTransaction", "metric": { "field": "value", "field_source": "ethereum_transaction", "function": "sum" }, "window": { "type": "rolling", "seconds": 86400 } } ``` **Step 2: Reference the aggregation in a policy** ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Global daily ETH spending cap', chain_type: 'ethereum', rules: [ { name: 'Allow up to 10 ETH total per 24h', method: 'eth_signTransaction', conditions: [ { field_source: 'reference', field: 'aggregation.gml7d33d3i10bf94m5m2o8tc', // replace with your aggregation ID operator: 'lte', value: '0x8AC7230489E80000' // 10 ETH in wei } ], action: 'ALLOW' } ] } ``` # Update a policy Source: https://docs.privy.io/controls/policies/update-a-policy You can update a policy by updating rules one at a time, or by updating the whole policy at once. You can do this using the Privy Dashboard, the NodeJS SDK, or the REST API. If a policy has an owner, the owner's signature is required to modify the policy, see [setting authorization signatures](/api-reference/authorization-signatures). ## Updating policy rules individually You can create, get, update, and delete individual rules in a policy. We recommend this over updating the whole policy at once, especially if you find yourself updating the same policy over time. This way, you can ensure there would be no race conditions when updating the policy. ### Add a rule to a policy Use the **`PrivyClient`**'s **`createRule`** method in the `policies()` interface to add a rule to a policy. ```tsx theme={"system"} const rule = await client.policies().createRule('insert-policy-id', { name: 'Allow list USDT', method: 'eth_sendTransaction', conditions: [ { field_source: 'ethereum_transaction', field: 'to', operator: 'eq', value: '0xdAC17F958D2ee523a2206206994597C13D831ec7' } ], action: 'ALLOW' }); ``` Use the **`PrivyClient`**'s **`create_rule`** method in the `policies()` interface to add a rule to a policy. ```rust theme={"system"} use privy_rs::{PrivyClient, generated::types::*}; let client = PrivyClient::new(app_id, app_secret)?; let usdt_condition = PolicyRuleCondition { field_source: "ethereum_transaction".to_string(), field: "to".to_string(), operator: "eq".to_string(), value: serde_json::Value::String("0xdAC17F958D2ee523a2206206994597C13D831ec7".to_string()), }; let request = CreatePolicyRuleBody { name: "Allow list USDT".to_string(), method: "eth_sendTransaction".to_string(), action: PolicyRuleAction::Allow, conditions: vec![usdt_condition], }; let rule = client .policies() .create_rule("insert-policy-id", request, &authorization_context) .await?; println!("Created rule: {}", rule.id); ``` ### Parameters and Returns See the Rust SDK documentation for detailed parameter and return types, including embedded examples: * [PoliciesClient::create\_rule](https://docs.rs/privy-rs/latest/privy_rs/subclients/struct.PoliciesClient.html#method.create_rule) For REST API details, see the [API reference](/api-reference/policies/rules/create). To add a rule to a policy, make a `POST` request to: ```sh theme={"system"} https://api.privy.io/v1/policies//rules ``` Replacing `` with the ID of your desired policy. In the request body, include the following fields: Name to assign to the rule. RPC method to apply the `conditions` to. Must correspond to the `chain_type` of the parent policy. A set of boolean conditions that define the action the rule allows or denies. Whether the rule should allow or deny a wallet request if it satisfies all of the rule's `conditions`. **Body** Here is an example of a request body: ```bash theme={"system"} $ curl --request POST https://api.privy.io/v1/policies/fmfdj6yqly31huorjqzq38zc/rules \ -u ":" \ -H "privy-app-id: " \ -H "privy-authorization-signature: " \ -H 'Content-Type: application/json' \ -d '{ "name": "Allowlist USDT", "method": "eth_sendTransaction", "conditions": [ { "field_source": "ethereum_transaction", "field": "to", "operator": "eq", "value": "0xdAC17F958D2ee523a2206206994597C13D831ec7" } ], "action": "ALLOW" }' ``` **Response** If the rule is added successfully, the response will include the full rule object, like below: ```json theme={"system"} { "name": "Allowlist USDT", "method": "eth_sendTransaction", "conditions": [ { "field_source": "ethereum_transaction", "field": "to", "operator": "eq", "value": "0xdAC17F958D2ee523a2206206994597C13D831ec7" } ], "action": "ALLOW", "id": "allow-list-usdt-18381838" } ``` ### Edit a rule in a policy Use the **`PrivyClient`**'s **`updateRule`** method in the `policies()` interface to update a rule in a policy. ```tsx theme={"system"} const rule = await client.policies().updateRule('insert-rule-id', { policy_id: 'insert-policy-id', name: 'Allow list USDT', method: 'eth_sendTransaction', conditions: [ { field_source: 'ethereum_transaction', field: 'to', operator: 'eq', value: '0xdAC17F958D2ee523a2206206994597C13D831ec7' } ], action: 'ALLOW' }); ``` Use the **`PrivyClient`**'s **`update_rule`** method in the `policies()` interface to update a rule in a policy. ```rust theme={"system"} use privy_rs::{PrivyClient, generated::types::*}; let client = PrivyClient::new(app_id, app_secret)?; let usdt_condition = PolicyRuleCondition { field_source: "ethereum_transaction".to_string(), field: "to".to_string(), operator: "eq".to_string(), value: serde_json::Value::String("0xdAC17F958D2ee523a2206206994597C13D831ec7".to_string()), }; let request = UpdatePolicyRuleBody { policy_id: "insert-policy-id".to_string(), name: "Allow list USDT".to_string(), method: "eth_sendTransaction".to_string(), action: PolicyRuleAction::Allow, conditions: vec![usdt_condition], }; let rule = client .policies() .update_rule("insert-rule-id", request, &authorization_context) .await?; println!("Updated rule: {}", rule.id); ``` ### Parameters and Returns See the Rust SDK documentation for detailed parameter and return types, including embedded examples: * [PoliciesClient::update\_rule](https://docs.rs/privy-rs/latest/privy_rs/subclients/struct.PoliciesClient.html#method.update_rule) For REST API details, see the [API reference](/api-reference/policies/rules/update). To add a rule to a policy, make a `PATCH` request to: ```sh theme={"system"} https://api.privy.io/v1/policies//rules/ ``` Replacing `` with the ID of your desired policy. In the request body, include the following fields: Name to assign to the rule. RPC method to apply the `conditions` to. Must correspond to the `chain_type` of the parent policy. A set of boolean conditions that define the action the rule allows or denies. Whether the rule should allow or deny a wallet request if it satisfies all of the rule's `conditions`. **Body** Here is an example of a request body: ```bash theme={"system"} $ curl --request PATCH https://api.privy.io/v1/policies/fmfdj6yqly31huorjqzq38zc/rules/allow-list-usdt-18381838 \ -u ":" \ -H "privy-app-id: " \ -H "privy-authorization-signature: " \ -H 'Content-Type: application/json' \ -d '{ "name": "Allowlist USDT", "method": "eth_sendTransaction", "conditions": [ { "field_source": "ethereum_transaction", "field": "to", "operator": "eq", "value": "0xdAC17F958D2ee523a2206206994597C13D831ec7" } ], "action": "ALLOW" }' ``` **Response** If the rule is added successfully, the response will include the full rule object, like below: ```json theme={"system"} { "name": "Allowlist USDT", "method": "eth_sendTransaction", "conditions": [ { "field_source": "ethereum_transaction", "field": "to", "operator": "eq", "value": "0xdAC17F958D2ee523a2206206994597C13D831ec7" } ], "action": "ALLOW", "id": "allow-list-usdt-18381838" } ``` ### Delete a rule from a policy Use the **`PrivyClient`**'s **`deleteRule`** method in the `policies()` interface to delete a rule from a policy. ```tsx theme={"system"} const rule = await client.policies().deleteRule('insert-rule-id', { policy_id: 'insert-policy-id' }); ``` Use the **`PrivyClient`**'s **`delete_rule`** method in the `policies()` interface to delete a rule from a policy. ```rust theme={"system"} use privy_rs::PrivyClient; let client = PrivyClient::new(app_id, app_secret)?; let request = DeletePolicyRuleBody { policy_id: "insert-policy-id".to_string(), }; let response = client .policies() .delete_rule("insert-rule-id", request, &authorization_context) .await?; println!("Rule deleted successfully"); ``` ### Parameters and Returns See the Rust SDK documentation for detailed parameter and return types, including embedded examples: * [PoliciesClient::delete\_rule](https://docs.rs/privy-rs/latest/privy_rs/subclients/struct.PoliciesClient.html#method.delete_rule) For REST API details, see the [API reference](/api-reference/policies/rules/delete). To delete a rule from a policy, make a `DELETE` request to: ```sh theme={"system"} https://api.privy.io/v1/policies//rules/ ``` Replacing `` with the ID of your desired policy and `` with the ID of the rule you want to delete. **Response** If the rule is deleted successfully, the response will be ```sh theme={"system"} {success: true} ``` ## Update a whole policy Use the **`PrivyClient`**'s **`update`** method from the `policies()` interface to update an existing policy. ```tsx theme={"system"} const policy = await client.policies().update('fmfdj6yqly31huorjqzq38zc', { name: 'Transactions must be <= 5ETH', rules: [ { name: 'Transactions must be <= 5ETH', method: 'eth_sendTransaction', action: 'ALLOW', conditions: [ { field_source: 'ethereum_transaction', field: 'value', operator: 'lte', value: '0x2386F26FC10000' } ] } ] }); ``` You can update a policy using the Java SDK by using the `policies().update()` method. If the policy has an owner, the owner's signature is required to modify the policy. Use an [authorization context](/controls/authorization-keys/using-owners/sign/signing-on-the-server) to pass into the `update()` method and sign the request. ```java theme={"system"} try { Rule valueUnder5Eth = Rule.builder() .name("Transactions must be <= 5ETH") .method(PolicyRuleMethod.ETH_SEND_TRANSACTION) .action(Action.ALLOW) .conditions(List.of( EthereumTransactionCondition.builder() .fieldSource(EthereumTransactionConditionFieldSource.ETHEREUM_TRANSACTION) .field(EthereumTransactionConditionField.VALUE) .operator(ConditionOperator.LTE) .value(ConditionValue.of("0x2386F26FC10000")) .build() )) .build(); PolicyUpdateRequestBody updateRequest = PolicyUpdateRequestBody.builder() .name("Transactions must be <= 5ETH") .rules(List.of(valueUnder5Eth)) .build(); // Example: If wallet's owner is an authorization private key AuthorizationContext authorizationContext = AuthorizationContext.builder() .addAuthorizationPrivateKey("authorization-key") .build(); PolicyUpdateResponse response = privyClient .policies() .update( "fmfdj6yqly31huorjqzq38zc", updateRequest, authorizationContext ); if (response.policy().isPresent()) { Policy policy = response.policy().get(); String policyId = policy.id(); } } catch (APIException e) { String errorBody = e.bodyAsString(); System.err.println(errorBody); } catch (Exception e) { System.err.println(e.getMessage()); } ``` ### Parameters When updating a policy, you may specify the following values on the `PolicyUpdateRequestBody` builder: Name to assign to policy. Chain type for wallets that the policy will be applied to. A list of `Rule` objects describing what rules to apply to each RPC method (e.g. `'eth_sendTransaction'`) that the wallet can take. [Learn more about `Rules`](/controls/policies/overview#rules). The owner of the policy. The key quorum ID of the owner of the policy. ### Returns The `PolicyUpdateResponse` object contains an optional `policy()` field that contains the updated policy if the policy was updated successfully. The updated policy. Version of the policy. Name of the policy. Chain type of the wallets that the policy will be applied to. Unique ID of the policy. The key quorum ID of the owner of the policy. The Unix time of when the policy was created. A list of `Rule` objects describing what rules to apply to each RPC method (e.g. `'eth_sendTransaction'`) that the wallet can take. [Learn more about `Rules`](/controls/policies/overview#rules). Use the **`PrivyClient`**'s **`update`** method from the `policies()` interface to update an existing policy. ```rust theme={"system"} use privy_rs::{PrivyClient, generated::types::*}; let client = PrivyClient::new(app_id, app_secret)?; let value_condition = PolicyRuleCondition { field_source: "ethereum_transaction".to_string(), field: "value".to_string(), operator: "lte".to_string(), value: serde_json::Value::String("0x2386F26FC10000".to_string()), }; let value_rule = PolicyRule { name: "Transactions must be <= 5ETH".to_string(), method: "eth_sendTransaction".to_string(), action: PolicyRuleAction::Allow, conditions: vec![value_condition], }; let request = UpdatePolicyBody { name: Some("Transactions must be <= 5ETH".to_string()), rules: Some(vec![value_rule]), owner_id: None, owner: None, }; let policy = client .policies() .update("fmfdj6yqly31huorjqzq38zc", request, &authorization_context) .await?; println!("Updated policy: {}", policy.name); ``` ### Parameters and Returns See the Rust SDK documentation for detailed parameter and return types, including embedded examples: * [PoliciesClient::update](https://docs.rs/privy-rs/latest/privy_rs/subclients/struct.PoliciesClient.html#method.update) For REST API details, see the [API reference](/api-reference/policies/update). To update a policy with the Go SDK, use the `Update` method on the `Policies` service. An authorization context is required. ### Usage ```go theme={"system"} import "github.com/privy-io/go-sdk/authorization" authCtx := &authorization.AuthorizationContext{ PrivateKeys: []string{"authorization-key"}, } policy, err := client.Policies.Update( context.Background(), "policy-id", privy.PolicyUpdateParams{ Name: privy.String("updated-policy"), }, privy.WithAuthorizationContext(authCtx), ) if err != nil { log.Fatalf("failed to update policy: %v", err) } fmt.Println("Updated policy:", policy.ID) ``` ### Parameters and Returns See the [API reference](/api-reference/policies/update) for more details. To update a policy with the Ruby SDK, use the `update` method on the `policies` service. An authorization context is required for owned policies. ### Usage ```ruby theme={"system"} ctx = Privy::Authorization::AuthorizationContext.build( authorization_private_keys: ["authorization-key"] ) policy = client.policies.update( "policy-id", policy_update_params: {name: "updated-policy"}, authorization_context: ctx ) puts(policy.id) ``` ### Parameters and Returns See the [API reference](/api-reference/policies/update) for more details. To update an existing policy, make a `PATCH` request to: ```sh theme={"system"} https://api.privy.io/v1/policies/ ``` Replacing `` with the ID of your desired policy. In the request headers, make sure to include Privy's [required authentication headers](/basics/rest-api/setup#authentication) and [headers that may be required for your app's wallet API setup](/basics/rest-api/quickstart#2-sign-a-message). ## **Body** In the request body, include the following fields: (Optional) New name to assign to policy. (Optional) New list of `Rule` objects describing what rules to apply to each RPC method (e.g. `'eth_sendTransaction'`) that the wallet can take. [Learn more about `Rules`](/controls/policies/overview#rules). The P-256 public key of the owner of the policy. If you provide this, do not specify an owner\_id as it will be generated automatically. View [this guide](/controls/authorization-keys/owners/overview) to learn more about owners. The key quorum ID of the owner of the policy. If you provide this, do not specify an owner. View [this guide](/controls/authorization-keys/owners/overview) to learn more about owners. Any fields not included in the `PATCH` request body will remain unchanged from the original policy. ## **Response** If the policy is updated successfully, the response will include the full updated policy object. Unique ID for the policy. Version of the policy. Currently, 1.0 is the only version. Updated name of the policy. Chain type for wallets that the policy will be applied to. Updated list of `Rule` objects describing what rules to apply to each RPC method (e.g. `'eth_sendTransaction'`) that the wallet can take. [Learn more about `Rules`](/controls/policies/overview#rules). The key quorum ID of the owner of the policy, whose signature is required to modify the policy. ## Example As an example, a sample request to update the `rules` of a policy with ID `fmfdj6yqly31huorjqzq38zc` might look like the following: ```bash theme={"system"} $ curl --request PATCH https://api.privy.io/v1/policies/fmfdj6yqly31huorjqzq38zc \ -u ":" \ -H "privy-app-id: " \ -H "privy-authorization-signature: " \ -H 'Content-Type: application/json' \ -d '{ "rules": [{ "name": "Allowlist USDT", "method": "eth_sendTransaction", "conditions": [ { "field_source": "ethereum_transaction", "field": "to", "operator": "eq", "value": "0xdAC17F958D2ee523a2206206994597C13D831ec7" } ], "action": "ALLOW" }] }' ``` A successful response will look like the following: ```json theme={"system"} { "id": "fmfdj6yqly31huorjqzq38zc", "name": "Allowlist certain smart contracts", "version": "1.0", "chain_type": "ethereum", "rules": [ { "name": "Allowlist USDT", "method": "eth_sendTransaction", "conditions": [ { "field_source": "ethereum_transaction", "field": "to", "operator": "eq", "value": "0xdAC17F958D2ee523a2206206994597C13D831ec7" } ], "action": "ALLOW", "id": "allow-list-usdt-18381838" } ], "owner_id": "fmfdj6yqly31huorjqzq38zc" } ``` # Cards Source: https://docs.privy.io/financial-flows/cards Issue debit cards that spend stablecoins directly from a Privy account Cards enable consumers and businesses to spend stablecoins directly from a Privy account at any merchant that accepts Visa. Your app issues virtual or physical debit cards backed by a Privy wallet, so users spend their onchain balance without manually offramping first. Cards Card issuing is powered by [Bridge](https://bridge.xyz) and [Stripe Issuing](https://docs.stripe.com/issuing/bridge-stablecoin-cards). Bridge manages customer identity, KYC, onchain fund movement, and statements. Stripe Issuing creates the card and handles authorization, spend controls, and webhooks. ## How it works Lead Bank issues the card, and Bridge is the program manager that moves funds onchain. Stripe provides Issuing, identity verification, and managed support, while Privy provides the embedded wallet, all in one integration. Cards spend just-in-time from the account's stablecoin balance; funds are never preloaded onto the card. Each purchase pulls stablecoins from the account onchain at the moment of authorization. Wallet owners must submit an onchain approval so Bridge's smart contract can pull funds at authorization time. ## Spend from vault balances Balances backing a card don't have to sit idle. Users can deposit their stablecoins in [Earn](/wallets/actions/earn/overview) vaults, where they accrue yield, and withdraw just-in-time to fund card spend. A single balance both grows and stays spendable, so users earn on their stablecoins right up until they use the card. ## Choose your integration Privy supports two ways to add cards to your app. Most teams launching a consumer card program start with pre-built components. Teams that need full control over the cardholder experience integrate the APIs directly. | | Pre-built components | API integration | | ------------------ | -------------------------------------------------------------------- | ---------------------------------------------------------------------- | | **Best for** | Launching a compliant consumer card program with minimal UI to build | Launching a commercial or consumer card program with a fully custom UI | | **How you build** | Pre-built UI components in the Privy React SDK | Stripe Issuing and Bridge APIs | | **Cardholder UI** | Provided by Privy | Built by your app | | **Compliance** | Powered by Stripe | Powered by Stripe or an in-house compliance team | | **Time to launch** | 1 month | 2 months | Embed bank-approved card UI with the React SDK. Issue cards server-side with Stripe Issuing and Bridge. ## Get started Cards are available through a guided Privy and Bridge onboarding. Reach out to [sales@privy.io](mailto:sales@privy.io) to get started. # Cards API integration Source: https://docs.privy.io/financial-flows/cards/integration-guide How to issue debit cards that spend stablecoins from wallets using Bridge and Stripe Issuing Your app can issue debit cards that spend stablecoins directly from a wallet. Card creation is handled through Stripe Issuing, while Bridge manages customer identity, KYC, and onchain fund-pulling. When a cardholder makes a purchase, Bridge pulls funds from the linked wallet via an onchain approval. Before starting, set up a Bridge account and verify your users by following the [KYC and KYB](/kyc-kyb/overview) guides. Additionally, set up a new Stripe account on the [Stripe Dashboard](https://dashboard.stripe.com/). Bridge sends you a URL (Stripe App Install Link) to associate the Bridge Developer to the Stripe account, and activate Stripe Issuing on this account. This flow connects Bridge to your Stripe account. This guide covers: 1. Creating a card via Stripe Issuing 2. Setting up a token approval 3. Handling card transaction webhooks Set up Bridge and verify users before issuing cards. Official Bridge documentation for Stripe Issuing card integration. ## Supported chains Bridge supports the following chains for non-custodial card funding: | Chain | Contract address | | ----------- | --------------------------------------------- | | Tempo | `0x661AA387dF0A94e81c06f5C00e9706665B7be686` | | Solana | `cardWArqhdV5jeRXXjUti7cHAa4mj41Nj3Apc6RPZH2` | | Base | `0x65bf8b55EEDef53C094E40003a03390De744DF33` | | World Chain | `0x6B0D105999491a48d5793FB6Cb54f5cE079E0da9` | | Linea | `0x930fa762919fDE945fD2d2c1dE25084daD2f8bBd` | # Create a card via Stripe Issuing Bridge uses Stripe Issuing for card creation. Your app creates a Bridge customer with a cards endorsement, then issues the card through Stripe's API. A wallet can only be tied to one card. Bridge does not support issuing multiple cards that spend from the same wallet. ## Request cards endorsement After [creating a Bridge customer](https://apidocs.bridge.xyz/api-reference/customers/create-a-customer), request the `cards` endorsement. The customer must complete KYC within 24 hours or the endorsement is revoked. ```bash theme={"system"} curl -X POST https://api.bridge.xyz/v0/customers/{customerID}/endorsements \ -H "Api-Key: " \ -H "Content-Type: application/json" \ -d '{ "endorsement_type": "cards" }' ``` Once approved, Bridge automatically creates a Stripe Cardholder and returns the `stripe_cardholder_id` on the customer object. ## Create the card Use the `stripe_cardholder_id` to create a card via Stripe's Issuing API. Set the `crypto_wallet` parameters to link the card to the non-custodial wallet. For Solana, provide the owner address (not the [associated token account](https://solana.com/docs/tokens/basics/create-token-account)). Bridge automatically derives the token account from the owner address and currency. ```bash theme={"system"} curl -X POST https://api.stripe.com/v1/issuing/cards \ -H "Authorization: Bearer " \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "cardholder=ich_1234" \ -d "currency=usd" \ -d "type=virtual" \ -d "status=active" \ -d "crypto_wallet[chain]=solana" \ -d "crypto_wallet[currency]=usdc" \ -d "crypto_wallet[type]=standard" \ -d "crypto_wallet[address]=BDkZQv1DqS7RJG5MZjVEP8FbN9Xvpf5b67kpi3765rQb" ``` ```bash theme={"system"} curl -X POST https://api.stripe.com/v1/issuing/cards \ -H "Authorization: Bearer " \ -H "Content-Type: application/x-www-form-urlencoded" \ -d "cardholder=ich_1234" \ -d "currency=usd" \ -d "type=virtual" \ -d "status=active" \ -d "crypto_wallet[chain]=world_chain" \ -d "crypto_wallet[currency]=usdc" \ -d "crypto_wallet[type]=standard" \ -d "crypto_wallet[address]=0xC37e5d75F3D212D22e943D8DD93849a17F79dc79" ``` On Solana, Bridge submits an onchain transaction to register the program delegate address for the wallet after the card is created. Create the card before setting up the token approval. This ensures Bridge ties the address to the customer before your app submits any approvals onchain. # Set up token approval After creating the card, the wallet must approve Bridge's smart contract to pull funds during card transactions. Bridge provides a consolidated version of the Solana delegate approval logic in a [GitHub Gist](https://gist.github.com/lvn/dd41a7233f63f1c45b4ec25c220ecff6). ## Set up the connection and transaction Initialize a connection and a new transaction to contain the approval instructions. ```typescript theme={"system"} import {Connection, Transaction, PublicKey, clusterApiUrl} from '@solana/web3.js'; import { getAssociatedTokenAddressSync, getAccount, createAssociatedTokenAccountInstruction, createApproveInstruction, TOKEN_PROGRAM_ID } from '@solana/spl-token'; const connection = new Connection(clusterApiUrl('mainnet-beta'), 'confirmed'); const transaction = new Transaction(); const walletAddress = new PublicKey('BDkZQv1DqS7RJG5MZjVEP8FbN9Xvpf5b67kpi3765rQb'); ``` ## Set up the ATA Check whether the associated token account (ATA) exists for the wallet. If it does not, add an instruction to the transaction to create it. ```typescript theme={"system"} const MINT_PUBKEY = new PublicKey( 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v' // USDC on Solana ); const userAta = getAssociatedTokenAddressSync( MINT_PUBKEY, walletAddress, false, TOKEN_PROGRAM_ID // use TOKEN_2022_PROGRAM_ID if the currency requires it ); try { await getAccount(connection, userAta, undefined, TOKEN_PROGRAM_ID); } catch { transaction.add( createAssociatedTokenAccountInstruction( walletAddress, userAta, walletAddress, MINT_PUBKEY, TOKEN_PROGRAM_ID ) ); } ``` ## Set up the delegate approval Add an instruction that approves Bridge's card program to spend from the ATA. Bridge assigns the `MERCHANT_ID` to your developer account during onboarding. ```typescript theme={"system"} const PROGRAM_ID = new PublicKey('cardWArqhdV5jeRXXjUti7cHAa4mj41Nj3Apc6RPZH2'); const MINT_DECIMALS = 6; const APPROVAL_AMOUNT = BigInt(100 * 10 ** MINT_DECIMALS); // Approve $100 const bridgeSdk = new BridgeSDK(PROGRAM_ID); const [delegatePda] = bridgeSdk.findUserDelegatePDA(MERCHANT_ID, MINT_PUBKEY, userAta); transaction.add( createApproveInstruction(userAta, delegatePda, walletAddress, APPROVAL_AMOUNT, [], TOKEN_PROGRAM_ID) ); ``` ## Send the transaction Submit the transaction using Privy. Pass `sponsor: true` to enable [Solana gas sponsorship](/wallets/gas-and-asset-management/gas/solana), which handles the fee payer and recent blockhash automatically. Use the [`useSignAndSendTransaction`](/wallets/using-wallets/solana/send-a-transaction) hook to sign and broadcast the transaction from the user's embedded wallet: ```tsx theme={"system"} import {useWallets, useSignAndSendTransaction} from '@privy-io/react-auth/solana'; function ApproveButton() { const {wallets} = useWallets(); const {signAndSendTransaction} = useSignAndSendTransaction(); async function handleApprove() { const wallet = wallets[0]; const {signature} = await signAndSendTransaction({ transaction: transaction.serialize({requireAllSignatures: false}), wallet, options: { sponsor: true, }, }); console.log('Approval tx:', Buffer.from(signature).toString('base64')); } return ; } ``` Use the [Wallet API](/wallets/using-wallets/solana/send-a-transaction) to sign and send the transaction server-side: ```typescript theme={"system"} import {PrivyClient} from '@privy-io/node'; const client = new PrivyClient({ appId: PRIVY_APP_ID, appSecret: PRIVY_APP_SECRET, }); const {hash} = await client.wallets().solana.signAndSendTransaction('WALLET_ID', { transaction: transaction.serialize({requireAllSignatures: false}), caip2: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', // Solana mainnet sponsor: true, }); console.log('Approval tx:', hash); ``` The result is a delegate approval resembling [this transaction](https://solscan.io/tx/5o1aAuCFpmB9sEt1djX9PDENGmkNEitUYoqg93a9JLdvQ1pZbFX5ChkVFULq38aArBCg9sQTREQt9fTFr1puPJ1H). Submit a standard ERC-20 `approve` call to allow the Bridge issuer contract to spend from the wallet. The card issuer contract address is specific to your developer account and differs from the main contract listed in the chain table above. Bridge provides this address during onboarding. ## Build the approval transaction Encode the ERC-20 `approve` calldata for the Bridge issuer contract: ```typescript theme={"system"} import {encodeFunctionData, parseUnits} from 'viem'; const TOKEN_ADDRESS = '0x...'; // ERC-20 stablecoin address (e.g., USDC) const ISSUER_ADDRESS = '0x...'; // Developer-specific contract from Bridge const approveData = encodeFunctionData({ abi: [{name: 'approve', type: 'function', inputs: [{name: 'spender', type: 'address'}, {name: 'amount', type: 'uint256'}], outputs: [{type: 'bool'}]}], functionName: 'approve', args: [ISSUER_ADDRESS, parseUnits('100', 6)], // Approve $100 USDC }); ``` ## Send the transaction Use the [`useSendTransaction`](/wallets/using-wallets/ethereum/send-a-transaction) hook to send the approval from the user's embedded wallet: ```tsx theme={"system"} import {useSendTransaction} from '@privy-io/react-auth'; import {worldchain} from 'viem/chains'; function ApproveButton() { const {sendTransaction} = useSendTransaction(); async function handleApprove() { const {hash} = await sendTransaction({ to: TOKEN_ADDRESS, data: approveData, chainId: worldchain.id, }); console.log('Approval tx:', hash); } return ; } ``` Use the [Wallet API](/wallets/using-wallets/ethereum/send-a-transaction) to send the transaction server-side: ```typescript theme={"system"} import {PrivyClient} from '@privy-io/node'; const client = new PrivyClient({ appId: PRIVY_APP_ID, appSecret: PRIVY_APP_SECRET, }); const {hash} = await client.wallets().ethereum.sendTransaction('WALLET_ID', { caip2: 'eip155:480', // World Chain transaction: { to: TOKEN_ADDRESS, data: approveData, }, }); console.log('Approval tx:', hash); ``` The result is a transaction resembling [this transaction](https://worldscan.org/tx/0xd5a24c25cefd7fa414f109dae95e544c9f18697fba603c45b758e5c51fdb7b33). # Handle card transaction webhooks The card is now ready to use. When a cardholder makes a purchase, Bridge publishes webhook events for both the card network authorization and the onchain transaction. Cards created via Stripe Issuing also emit Stripe webhook events (`issuing_authorization.created`, `issuing_transaction.created`). See the [Stripe Issuing webhooks documentation](https://docs.stripe.com/issuing/controls/real-time-authorizations) for details on handling these events. Bridge submits transactions onchain at the time of card authorization, but they complete asynchronously. This results in two Bridge webhook events: ### `card_transaction.created` Bridge publishes this event when the card authorization occurs. It contains the authorization details but not yet the onchain transaction: ```json theme={"system"} { "event_type": "card_transaction.created", "event_object_status": "approved", "event_object": { "id": "86d30f38-5ea0-402d-ad48-48003d3b3f29", "amount": "-10.0", "status": "approved", "category": "purchase", "currency": "usd", "customer_id": "2c21ddbb-cf34-4170-b9b2-076a3ed55de9", "merchant_name": "BRIDGE CAFE, SAN FRANCISCO, CA", "card_account_id": "3d698985-fbd4-4889-999a-4fa7bd578452", "authorization_infos": [ { "amount": "-10.0", "network": "visa", "approval_status": "approved" } ] } } ``` ```json theme={"system"} { "event_type": "card_transaction.created", "event_object_status": "approved", "event_object": { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "amount": "-10.0", "status": "approved", "category": "purchase", "currency": "usd", "customer_id": "2c21ddbb-cf34-4170-b9b2-076a3ed55de9", "merchant_name": "BRIDGE CAFE, SAN FRANCISCO, CA", "card_account_id": "4e789012-abc3-4567-890d-ef1234567890", "authorization_infos": [ { "amount": "-10.0", "network": "visa", "approval_status": "approved" } ] } } ``` ### `card_transaction.updated` Bridge publishes this event seconds later when the onchain transaction confirms. It includes `crypto_details` with the chain, amount, and transaction hash: ```json theme={"system"} { "event_type": "card_transaction.updated", "event_object": { "id": "86d30f38-5ea0-402d-ad48-48003d3b3f29", "amount": "-10.0", "status": "approved", "authorization_infos": [ { "amount": "-10.0", "crypto_details": { "chain": "solana", "amount": "10.0", "tx_hash": "618p4RWZ4UPe6uCeFo0BaRb2H4gSQJjLayDihFD7fERAnfyoxyMJQZc4WmKkPkLu7QHnXgpWp6pPUMqc2HzGqH3", "currency": "usdc" }, "approval_status": "approved" } ] } } ``` ```json theme={"system"} { "event_type": "card_transaction.updated", "event_object": { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "amount": "-10.0", "status": "approved", "authorization_infos": [ { "amount": "-10.0", "crypto_details": { "chain": "world_chain", "amount": "10.0", "tx_hash": "0x3a1b2c3d4e5f67890abcdef1234567890abcdef1234567890abcdef12345678", "currency": "usdc" }, "approval_status": "approved" } ] } } ``` Bridge rejects an authorization if the onchain approval is inactive, the approved amount is insufficient, or the wallet lacks funds. Incremental authorizations trigger additional onchain transactions to cover the extra charge. For the full list of webhook scenarios, see the [Bridge card transaction webhooks documentation](https://apidocs.bridge.xyz/platform/cards/overview/webhooks#card-transaction-webhooks). # Preventing double-spend Card transactions are initiated by the card network, not your app — funds can leave a wallet without your backend's involvement. To prevent users from withdrawing funds that are already committed to a card purchase: Use a [Privy policy](/controls/policies/overview) to deny all outbound transfers from card-linked wallets except those to the Bridge issuer contract. This is the simplest approach if your app doesn't need to support withdrawals alongside card usage. See [creating a policy](/controls/policies/create-a-policy) for setup instructions. If your app allows withdrawals alongside card usage, use Bridge's `card_transaction.created` and `card_transaction.updated` [webhooks](#handle-card-transaction-webhooks) to maintain a real-time ledger. Reserve funds on `created`, confirm on `updated`, and check available balance before processing any app-initiated withdrawal. [Configure cookies](/recipes/react/cookies) so Privy automatically sends the user's access token to your backend with each request. Before processing any wallet action, [verify the access token](/authentication/user-authentication/access-tokens) server-side to confirm the request came from an authenticated user — preventing forged or replayed withdrawal requests. # Cards Source: https://docs.privy.io/financial-flows/cards/overview Issue debit cards that spend stablecoins directly from a Privy account Cards enable consumers and businesses to spend stablecoins directly from a Privy account at any merchant that accepts Visa. Your app issues virtual or physical debit cards backed by a Privy wallet, so users spend their onchain balance without manually offramping first. Cards Card issuing is powered by [Bridge](https://bridge.xyz) and [Stripe Issuing](https://docs.stripe.com/issuing/bridge-stablecoin-cards). Bridge manages customer identity, KYC, onchain fund movement, and statements. Stripe Issuing creates the card and handles authorization, spend controls, and webhooks. ## How it works Lead Bank issues the card, and Bridge is the program manager that moves funds onchain. Stripe provides Issuing, identity verification, and managed support, while Privy provides the embedded wallet, all in one integration. Cards spend just-in-time from the account's stablecoin balance; funds are never preloaded onto the card. Each purchase pulls stablecoins from the account onchain at the moment of authorization. Wallet owners must submit an onchain approval so Bridge's smart contract can pull funds at authorization time. ## Spend from vault balances Balances backing a card don't have to sit idle. Users can deposit their stablecoins in [Earn](/wallets/actions/earn/overview) vaults, where they accrue yield, and withdraw just-in-time to fund card spend. A single balance both grows and stays spendable, so users earn on their stablecoins right up until they use the card. ## Choose your integration Privy supports two ways to add cards to your app. Most teams launching a consumer card program start with pre-built components. Teams that need full control over the cardholder experience integrate the APIs directly. | | Pre-built components | API integration | | ------------------ | -------------------------------------------------------------------- | ---------------------------------------------------------------------- | | **Best for** | Launching a compliant consumer card program with minimal UI to build | Launching a commercial or consumer card program with a fully custom UI | | **How you build** | Pre-built UI components in the Privy React SDK | Stripe Issuing and Bridge APIs | | **Cardholder UI** | Provided by Privy | Built by your app | | **Compliance** | Powered by Stripe | Powered by Stripe or an in-house compliance team | | **Time to launch** | 1 month | 2 months | Embed bank-approved card UI with the React SDK. Issue cards server-side with Stripe Issuing and Bridge. ## Get started Cards are available through a guided Privy and Bridge onboarding. Reach out to [sales@privy.io](mailto:sales@privy.io) to get started. # Cards pre-built components Source: https://docs.privy.io/financial-flows/cards/pre-built-components/overview Embed bank-approved card UI in your app with the Privy React SDK Pre-built components are embeddable UI components that let your app offer stablecoin-funded consumer cards. Pre-built components are available to select customers, with general availability coming soon. Reach out to [sales@privy.io](mailto:sales@privy.io) to request access. The components are delivered through the Privy React SDK. They drop into your app and cover the full cardholder journey: onboarding and disclosures, identity verification, balances, transaction history, statements, card management, and customer support. React Native support is coming soon. Pre-built components let your team: * **Launch in 1 month** with pre-built, customizable UI components and card art. * **Stay compliant.** Onboarding, disclosures, statements, and support are handled for you. ## What's included Pre-built components enable consumer virtual card issuance with embedded KYC onboarding and all required disclosures and agreements. Cardholders can add cards to Apple Pay and Google Pay, view balances and transaction history, download monthly statements, and freeze, replace, manage, or close their cards. Stripe provides Managed Support to handle disputes, complaints, and inquiries by phone. ## The cardholder journey The SDK components encapsulate the entire card experience. The cardholder accepts the required terms and long-form bank disclosures before a card is issued. The cardholder completes KYC, including identity document and selfie verification, inside your app. The cardholder approves the onchain spend contract for their wallet, and a virtual card is issued. The cardholder views their available balance, browses transaction history, and opens individual transactions for merchant, amount, and fee details. The cardholder freezes, replaces, or closes the card, downloads statements, and reaches managed support to file disputes. ## Get started Connect your Stripe and Bridge accounts. Integrate cards pre-built components with the Privy React SDK. ## Going to production Going to production requires submitting additional documents, which Stripe reviews before provisioning your production access. Your Privy account manager will work with you throughout the process. # Cards React integration Source: https://docs.privy.io/financial-flows/cards/pre-built-components/react-integration Integrate stablecoin card pre-built components with the Privy React SDK This guide covers the complete cardholder flow. First, check for an existing card. If none exists, use `useSignUpForCard` with `SignUpForCardView` to onboard the cardholder. Then, render `CardSummaryView` to display and manage the card. These APIs are exported from `@privy-io/react-auth/cards` and must be used within `PrivyProvider`. Before continuing, complete the [cards setup](/financial-flows/cards/pre-built-components/setup) and [configure embedded wallets](/basics/react/advanced/automatic-wallet-creation) for your app. ## Install and import the SDK Card pre-built components require `@privy-io/react-auth` version 3.40.0 or later. Install the React SDK: ```bash theme={"system"} npm install @privy-io/react-auth ``` Import `usePrivy` from the main entrypoint. Import the card hooks and components from the `/cards` entrypoint. ```tsx theme={"system"} import {usePrivy} from '@privy-io/react-auth'; import { CardSummaryView, SignUpForCardView, useGetCardsForUser, useSignUpForCard } from '@privy-io/react-auth/cards'; ``` ## Get the funding wallet The `signUp` method takes the Privy wallet ID, not the wallet address. Read it from the authenticated user's `linkedAccounts`. The following helper returns the ID of an embedded EVM wallet: ```tsx theme={"system"} import type {LinkedAccountWithMetadata, WalletWithMetadata} from '@privy-io/react-auth'; const getCardWalletId = (accounts: LinkedAccountWithMetadata[]) => accounts.find( (x): x is WalletWithMetadata => x.type === 'wallet' && x.walletClientType === 'privy' && x.connectorType === 'embedded' && x.chainType === 'ethereum' )?.id; ``` For a Solana-funded card, select an embedded wallet with `chainType === 'solana'` instead. ## Add the card flow Use `isOpen` to mount the container for `SignUpForCardView`. Connect the container's close action to `close`. The view renders the onboarding steps using the options passed to `signUp`. The promise returns the new card ID after signup completes and rejects if the flow does not complete. This sandbox example uses Tempo Moderato and PathUSD. The SDK supplies built-in stablecoin and Bridge spend-approval targets for supported sandbox networks. ```tsx theme={"system"} 'use client'; import {useState} from 'react'; import { usePrivy, type LinkedAccountWithMetadata, type WalletWithMetadata } from '@privy-io/react-auth'; import { CardSummaryView, SignUpForCardView, useGetCardsForUser, useSignUpForCard } from '@privy-io/react-auth/cards'; const getCardWalletId = (accounts: LinkedAccountWithMetadata[]) => accounts.find( (x): x is WalletWithMetadata => x.type === 'wallet' && x.walletClientType === 'privy' && x.connectorType === 'embedded' && x.chainType === 'ethereum' )?.id; const Cards = () => { const {user} = usePrivy(); const {signUp, close, isOpen} = useSignUpForCard(); const {getCardsForUser} = useGetCardsForUser(); const [cardId, setCardId] = useState(null); const [error, setError] = useState(null); const walletId = getCardWalletId(user?.linkedAccounts ?? []); const openCard = async () => { if (!walletId) return; setError(null); try { const {data} = await getCardsForUser({environment: 'sandbox', limit: 20}); const existing = data.find( (x) => x.wallet_id === walletId && (x.status === 'active' || x.status === 'inactive') ); if (existing) { setCardId(existing.id); return; } const {id} = await signUp({ environment: 'sandbox', walletId, chainId: 'eip155:42431', asset: 'path_usd' }); setCardId(id); } catch (error) { setError(error instanceof Error ? error.message : 'Could not open card'); } }; return ( <> {error &&

{error}

} {!cardId && ( )} {isOpen && ( )} {cardId && } ); }; ``` The example checks for an open card before starting signup. A failed list request stops the flow. The cardholder can use the same button to retry. The example checks the newest 20 cards. Apps with more records should use the pagination helper below before starting signup. Replace the `aside` with the modal or side panel that fits the app. Keep `SignUpForCardView` mounted while `isOpen` is `true`. `CardSummaryView` uses `cardId` to load the card, balance, activity, and statements. It also handles card-detail reveal, wallet provisioning, freezing, replacement, and cancellation. ## Find the user's existing cards The signup promise only returns a card ID during the current page session. To support returning users, use `useGetCardsForUser` to fetch the authenticated user's cards. `getCardsForUser` returns one page for a single environment, ordered newest first. Results include canceled cards. The default page size is 5 and the maximum is 20; pass each `next_cursor` back as `cursor` until it returns `null`. ```tsx theme={"system"} const {getCardsForUser} = useGetCardsForUser(); const getAllCards = async (environment: 'sandbox' | 'production') => { const first = await getCardsForUser({environment, limit: 20}); const cards = [...first.data]; let cursor = first.next_cursor; while (cursor) { const page = await getCardsForUser({environment, limit: 20, cursor}); cards.push(...page.data); cursor = page.next_cursor; } return cards; }; ``` Call the helper after the user authenticates. Keep the full result if your app lists card history, or select the newest open card to pass to `CardSummaryView`: ```tsx theme={"system"} const cards = await getAllCards('sandbox'); const existing = cards.find( (x) => x.wallet_id === wallet.id && (x.status === 'active' || x.status === 'inactive') ); setCardId(existing?.id ?? null); ``` ## Handle card states The list response can include these card states: * `active`: The card is open and can spend. Pass it to `CardSummaryView`. * `inactive`: The card is frozen but remains open. Pass it to `CardSummaryView` to allow unfreezing. * `canceled`: The card is permanently closed. Keep it for history, but do not select it for management. * `replaced`: A newer card replaced this closed card. Select the newer `active` or `inactive` card instead. Cards are ordered newest first. Select the first `active` or `inactive` card for the funding wallet. Do not treat a failed `getCardsForUser` request as an empty list. Show the error and retry the request. Offer signup only after a successful request returns no open cards. ## Configure production spend approval Sandbox uses built-in targets for Ethereum Sepolia, OP Sepolia, Polygon Amoy, Base Sepolia, Arbitrum Sepolia, Avalanche Fuji, Tempo Moderato, and Solana devnet. Production requires the spend-approval target for the mainnet chain behind the app's Bridge integration. Pass the target that a Privy account manager provides. Do not guess or hardcode another integration's spender or merchant ID: the card can be issued but cannot spend if its wallet approves the wrong target. Pass the stablecoin contract and Bridge spender for the card's chain: ```tsx theme={"system"} const openProductionSignUp = async () => { const {id} = await signUp({ environment: 'production', walletId: wallet.id, chainId: 'eip155:4217', asset: 'path_usd', spendApproval: { stablecoinAddress: process.env.NEXT_PUBLIC_CARD_STABLECOIN_ADDRESS!, spenderAddress: process.env.NEXT_PUBLIC_CARD_SPENDER_ADDRESS!, }, }); setCardId(id); }; ``` Pass the stablecoin mint, Bridge card program, and merchant ID: ```tsx theme={"system"} const openProductionSignUp = async () => { const {id} = await signUp({ environment: 'production', walletId: wallet.id, chainId: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', asset: 'usdc', spendApproval: { stablecoinAddress: process.env.NEXT_PUBLIC_CARD_STABLECOIN_ADDRESS!, programId: process.env.NEXT_PUBLIC_CARD_PROGRAM_ID!, merchantId: process.env.NEXT_PUBLIC_CARD_MERCHANT_ID!, }, }); setCardId(id); }; ``` The `environment` option determines which configured card ledger the flow uses. Sandbox does not accept a caller-supplied `spendApproval`; production requires one. ## API reference ### `useGetCardsForUser` `useGetCardsForUser()` returns a `getCardsForUser` method for the authenticated user: ```tsx theme={"system"} const {getCardsForUser} = useGetCardsForUser(); ``` `getCardsForUser(options)` returns a page with the user's cards in `data` and the next page cursor in `next_cursor`. | Option | Type | Description | | ------------- | --------------------------- | --------------------------------------------------------------- | | `environment` | `'sandbox' \| 'production'` | Card ledger to query. | | `limit` | `number` | Optional page size from 1 to 20. Defaults to 5. | | `cursor` | `string` | Optional cursor returned as `next_cursor` by the previous page. | ### `useSignUpForCard` `useSignUpForCard()` returns the `signUp` and `close` methods and the `isOpen` state: ```tsx theme={"system"} const {signUp, close, isOpen} = useSignUpForCard(); ``` `signUp(options)` returns `Promise<{id: string}>`. Only one card signup can be active at a time; starting another before the first finishes rejects with an error. `isOpen` is `true` while signup is in progress. Use it to control the container that mounts `SignUpForCardView`. Call `close()` when the user dismisses that container. Closing before a card exists rejects the pending `signUp` promise. | Option | Type | Description | | --------------- | --------------------------- | ------------------------------------------------------------------------------------------------------- | | `environment` | `'sandbox' \| 'production'` | Card ledger to use. | | `walletId` | `string` | Privy ID of the embedded wallet that funds the card. | | `chainId` | `string` | CAIP-2 chain ID for the funding wallet. | | `asset` | `string` | Spend asset for the card, such as `'path_usd'` or `'usdc'`. | | `spendApproval` | `DevSpendApprovalTarget` | Required in production and unavailable in sandbox. Shape depends on whether `chainId` is EVM or Solana. | `DevSpendApprovalTarget` is a union of the EVM and Solana target shapes: ```tsx theme={"system"} type EvmDevSpendApprovalTarget = { stablecoinAddress: string; spenderAddress: string; }; type SvmDevSpendApprovalTarget = { stablecoinAddress: string; programId: string; merchantId: number | string | bigint; }; type DevSpendApprovalTarget = EvmDevSpendApprovalTarget | SvmDevSpendApprovalTarget; ``` For EVM chains, `spenderAddress` identifies the Bridge contract approved to spend the stablecoin. For Solana, `programId` and `merchantId` identify the Bridge delegate. ### `SignUpForCardView` `SignUpForCardView` takes no props. Mount one instance within `PrivyProvider` while `isOpen` is `true`. The hook controls its options and completion state. The view handles disclosures, bank and provider terms, KYC, card creation, and the wallet spend approval. ### `CardSummaryView` | Prop | Type | Description | | ------------- | --------------------------- | ----------------------------------------------- | | `cardId` | `string` | Privy card ID to display. | | `environment` | `'sandbox' \| 'production'` | Card ledger that contains the card. | | `onClose` | `() => void` | Optional. Called when the user closes the view. | Always pass the same `environment` used to create the card. Cards are scoped by app, user, and environment, so the other environment cannot load the card. # Cards setup Source: https://docs.privy.io/financial-flows/cards/pre-built-components/setup Get your app ready to issue stablecoin cards with pre-built components Complete this one-time setup before integrating the components. It connects your Bridge and Stripe accounts and adds your API keys to the Privy dashboard so your app can issue cards. Sign up and complete KYB in the [Bridge dashboard](https://dashboard.bridge.xyz/get-started?=privy). Bridge is the program manager for your cards. Create an account in the [Stripe dashboard](https://dashboard.stripe.com/). Stripe provides Issuing for your card program. Open the [Stripe app install page](https://marketplace.stripe.com/apps/install/link/com.stripe.bridge.cards?redirect_uri=https://dashboard.bridge.xyz/app/cards) to connect your Bridge account and activate Stripe Issuing. If you have more than one Stripe account, select the correct one during installation. On the [cards page](https://dashboard.privy.io/apps?page=stablecoin-cards) of the Privy dashboard, add your Bridge and Stripe API keys for both sandbox and production. [Set up gas sponsorship](/wallets/gas-and-asset-management/gas/setup) so cardholders can approve the spend contract and sign transactions without holding a native token for gas. Select the background color for cardholders' debit cards. More customization options are coming soon. Your application name and company legal name are shown to users in the card signup flow, marketing, and disclosures. Add both in the Privy dashboard. Your program is now ready. Continue to the [React integration](/financial-flows/cards/pre-built-components/react-integration) to add the components to your app. # Configuration Source: https://docs.privy.io/financial-flows/deposits/configuration Enable and configure deposit methods in the Privy Dashboard Privy supports multiple deposit methods that can be enabled and configured from the [Privy Dashboard](https://dashboard.privy.io). Select your app and navigate to the [Funding](https://dashboard.privy.io/apps?page=funding) page. From there, enable your desired deposit methods. ## Card onramps To enable users to [fund wallets with debit cards, credit cards, Apple Pay, and Google Pay](/wallets/funding/add-funds), enable **card onramps.** By default, Privy enables [Stripe Crypto Onramp](https://stripe.com/crypto-onramp) (USD, EUR) and [MoonPay](https://www.moonpay.com/) (AUD, BRL) for your application. To enable users to purchase with additional fiat currencies, complete KYB with [Meld](https://www.meld.io/) to access their global onramp network. Once card onramps are enabled, your app can prompt users via the React and React Native SDKs to fund their wallets. Visit the [Privy Demo](https://demo.privy.io/) to try card onramps as a user. ## Bank transfer To enable [fiat deposits](/wallets/funding/fiat-deposits/overview), enable **bank transfer** funding. Once the method is enabled, configure your Bridge API keys per the steps below. ### Onboard with Bridge If your app does not already have a Bridge account, [sign up here](https://dashboard.bridge.xyz/get-started?utm_source=privy). If your app has already verified users with Bridge directly, those customers do not need to be verified again. Please reach out to [support@privy.io](mailto:support@privy.io) for additional details on linking an existing Bridge customer to a Privy user. ### Register a Bridge API key with Privy 1. Create an API key in the [Bridge dashboard](https://dashboard.bridge.xyz/). Bridge issues separate keys for its sandbox and production environments. 2. Register the key in the Privy Dashboard under [Onramps > Bridge > Configure](https://dashboard.privy.io/apps?page=funding). Configure Bridge Add at least one sandbox or production API key to enable Bridge for your app. For each environment, select where Bridge can be used: | Setting | Description | | ------------------------------------ | ------------------------------------------------------------------------------------------------------------- | | Enable client-side flows (React SDK) | Allows Bridge flows to be initiated from your app's frontend. | | Enable server-side flows (REST API) | Allows Bridge flows to be initiated from your app's backend, including KYC verification and KYB verification. | The sandbox environment does not move real money and does not require real identity or bank information. It is not a testnet: wallets and assets used in sandbox flows are still mainnet. See Bridge's guide to [setting up a sandbox environment](https://apidocs.bridge.xyz/get-started/introduction/quick-start/setting-up-sandbox) for what Bridge simulates, including [simulated KYC approvals](https://apidocs.bridge.xyz/api-reference/sandbox/simulate-kyc-approval-sandbox-only). ## Exchange To enable users to [fund their wallets via a crypto exchange](/wallets/funding/add-funds), enable **exchange** funding. Privy currently supports users funding their wallets with a connected exchange account from Coinbase. Once the method is enabled, follow [this guide](https://docs.cdp.coinbase.com/onramp/introduction/welcome) to create a Coinbase Developer Platform account and add your Onramp API keys. ## Crypto deposits To fund wallets via [crypto deposits](/wallets/funding/crypto-deposits/overview), enable [swaps](/wallets/actions/swap/setup) and [app-pays gas sponsorship](/wallets/gas-and-asset-management/gas/setup) for each source chain. Incoming deposits are then converted into a target asset you specify. The **crypto deposits** switch on the Funding page reflects those dependencies. It is not a separate enablement step. Under the hood, Privy's crypto deposits feature uses the [swap](/financial-flows/swap) API. # Deposits Source: https://docs.privy.io/financial-flows/deposits/overview Move money into Privy wallets from fiat and crypto sources Deposits bring funds into a Privy wallet from fiat or crypto sources. Privy can automatically convert incoming funds into a target asset you specify. Deposits Each flow is built on Privy wallets and delivered as a UI component, SDK method, or API, so your business can accept funds at scale without operating payments infrastructure of its own. | Method | Description | Platform availability | | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | --------------------- | | [Fiat deposits](/wallets/funding/fiat-deposits/overview) | Create dedicated bank accounts (VBANs) for wallets that convert incoming deposits into a target asset. | All | | [Crypto deposits](/wallets/funding/crypto-deposits/overview) | Send crypto to wallets and configure routing to automatically convert it to a target asset. | All | | [Onramp modal](/wallets/funding/add-funds) | Present a UI to users with available deposit methods, including cards, bank transfers, and exchanges. | React | | [Card onramps](/wallets/funding/fiat-onramp) | Initiate a card onramp flow for users. | React, React Native | # Earn Source: https://docs.privy.io/financial-flows/earn Put wallet balances to work with yield from tokenized money market funds and DeFi vaults Earn enables apps to generate yield on wallet balances. Deposit into vaults, withdraw at any time, and track positions in real time, all with a few API calls. Privy simplifies vault deployment, smart contract interactions, and onchain execution. Earn ## Capabilities * **Deposit and withdraw** assets into yield vaults with a single API call per operation * **Query positions** to display real-time holdings, accrued yield, and vault shares * **Collect fees** from a configurable share of the yield generated through your app * **Sponsor gas** for your users — if your app has [gas sponsorship](/wallets/actions/overview#gas-management) enabled, Privy automatically sponsors gas for earn deposit, withdraw, and incentive claim actions ## Supported yield providers Earn supports multiple providers through a single API. Your app deposits, withdraws, and reads positions through the same endpoints, regardless of provider. Yield is generated via tokenized money market funds, other real world assets, and DeFi lending. A select set of yield sources are available in the Privy Dashboard for self-serve setup. Contact [sales@privy.io](mailto:sales@privy.io) to enable additional Veda, Aave, Morpho, and Kamino vaults from any curator, on any chain. Your app should make clear to end users that yield is generated via a tokenized money market fund, real world asset, or DeFi protocol independent from the wallet provider. Users keep full control of their assets and should explicitly direct the deposit action. ## Tokenized money market funds Tokenized money market funds (TMMFs) invest in short-term, high-quality debt such as US Treasury bills and repurchase agreements. The fund earns interest on these holdings, and that interest is the source of the yield. Rates track prevailing short-term benchmarks rather than onchain borrower demand. Yield reaches token holders in one of two ways, depending on the fund: * **Accruing tokens** rise in value as interest accrues; each redeems for more of the underlying asset over time. * **Distributing tokens** hold a stable value and pay yield as additional tokens on a recurring schedule. Contact [sales@privy.io](mailto:sales@privy.io) to enable tokenized money market fund yield for your app. ## DeFi yield DeFi vaults allocate deposited assets into onchain lending markets where borrowers pay interest to access liquidity. That interest flows back to the vault, increasing the value of deposited shares over time. Vault strategies are managed by curators who determine how capital is allocated across markets to balance risk and return. APY fluctuates based on borrower demand, market utilization, and the curator's allocation strategy. Some vaults also distribute additional token incentives on top of the base lending yield. All lending and borrowing happens onchain through non-custodial smart contracts. ### How DeFi yield accrues ERC-4626 vaults track balances in **shares**. A deposit converts assets into shares at the current share price. As interest accrues, the share price rises — each share redeems for more of the underlying asset. Yield accrues passively with no claiming or compounding required, and withdrawals return the original deposit plus earned yield. Example: a wallet deposits 1,000 USDC at a share price of 1.00 and receives 1,000 shares. When the share price reaches 1.05, those shares are worth 1,050 USDC. No new shares are minted; existing shares appreciate. Vault shares are standard ERC-20 tokens and can be transferred between wallets like any other token. ## Revenue sharing Your app can earn revenue by keeping a configurable share of the yield generated by its users' deposits. An admin wallet your app controls receives the fees. How the fee is capped, split, and collected depends on the vault provider — Morpho captures up to 50% of yield as shares in the admin wallet, Aave applies a performance fee of up to 100% (split 50/50 with Aave Labs), and Veda distributes fees per your agreement. See [revenue sharing](/wallets/actions/earn/revenue-sharing) for how to configure and collect fees across providers. ## Next steps Deploy a fee wrapper and configure your vault in the Privy Dashboard. A working Next.js app with end-to-end deposit and withdraw flows. Privy does not control DeFi vaults or underlying protocols. Vault information is provided for reference only and may change or be inaccurate. Earnings are generated from third-party vaults and are not guaranteed. Using vaults involves risk, including loss of funds. These materials are for general information purposes only and are not investment advice or a recommendation or solicitation to engage in any specific transaction. You are responsible for evaluating vaults at your own discretion. Privy does not provide investment, financial, legal, or tax advice. # Monetization Source: https://docs.privy.io/financial-flows/monetization Keep a share of the revenue generated through Privy Earn, swaps, and transfers Privy lets your app keep a share of the revenue generated by its financial flows. Configure a fee on earn, swaps, or transfers, and your app's share of the fee will be captured at the infrastructure layer, with no billing system or protocol-level engineering to build and maintain. ## What your app can monetize Fees apply across three products. Each one attaches revenue to activity your app already supports. | Product | What your app keeps | | --------------------------------------------------- | ------------------------------------------------ | | [Earn](/wallets/actions/earn/revenue-sharing) | A performance fee on the yield a vault generates | | [Swaps](/wallets/actions/swap/collect-fees) | A developer fee on token swaps | | [Transfers](/wallets/actions/transfer/collect-fees) | A developer fee on cross-chain transfers | ## Why monetize with Privy * **Simple integration.** Revenue attaches to the earn, swap, and transfer APIs your app already calls. There is no separate billing system to build or reconcile. * **Tune fees to fit.** Adjust the fee percentage to find the balance between revenue and user retention that works for your app. * **Consolidate the stack.** Capturing revenue at the infrastructure layer reduces the need for third-party billing services or custom fee capture logic. Fees are configurable and can be set to zero. Your app chooses whether to monetize each product. ## The end-user experience Fees are applied within the existing product flow. Users see a consistent experience, and each product surfaces the total cost, including any fee, before an action is confirmed: * **Earn** shares a portion of generated yield with your app; users keep the rest and can withdraw at any time. * **Swaps** and **transfers** return a fee breakdown in the quote, so the amount received is clear before execution. ## Monetize each product Keep a performance fee on the yield generated by your users' deposits. Add a developer fee to cross-chain swaps. Add a developer fee to cross-chain transfers. Custom fees for swaps and transfers are in early access. Contact [sales@privy.io](mailto:sales@privy.io) to enable them for your app. # Financial flows Source: https://docs.privy.io/financial-flows/overview Use Privy APIs to fund, move, grow, and spend the balance in a user or business account Once users and businesses have wallets, they will need to manage their money. Privy delivers a full suite of financial services via a single API, so your team can offer a financial account without building payments infrastructure from scratch or taking custody of funds. Financial flows ## Build a full financial platform Developers can offer a full suite of financial services in-app, powered by the Privy API. These building blocks power neobanks, global payments providers, and treasury platforms, including Ramp and Deel. | Capability | Build it with | | ---------------------------------------------- | --------------------------------------------- | | Hold funds | [Wallets](/wallets/overview) | | Move money in from crypto and fiat sources | [Funding](/financial-flows/deposits/overview) | | Move money in out to crypto and fiat recipient | [Funding](/financial-flows/payouts/overview) | | Earn yield | [Earn](/wallets/actions/earn/overview) | | Spend at any merchant | [Cards](/financial-flows/cards/overview) | | Convert between assets | [Trade](/wallets/actions/swap/overview) | | Collect fees | [Monetization](/financial-flows/monetization) | # Funding Source: https://docs.privy.io/financial-flows/payments Move money into and out of Privy accounts with deposits and payouts Funding moves money into and out of a Privy account. **Deposits** add funds and **payouts** disburse them. Funding Each flow is built on Privy wallets and delivered as a UI component, SDK method, or API, so your business can move money at scale without operating payments infrastructure of its own. ## Deposits Deposits bring funds into an account, from fiat or from crypto sources. These methods automatically handle conversion of incoming fiat or crypto deposits into a target asset you specify. | Method | Description | Platform availability | | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------ | --------------------- | | [Fiat deposits](/wallets/funding/fiat-deposits/overview) | Create dedicated bank accounts (VBANs) for wallets that convert incoming deposits into a target asset. | All | | [Crypto deposits](/wallets/funding/crypto-deposits/overview) | Send crypto to wallets and configure routing to automatically convert them to target assets. | All | | [Onramp modal](/wallets/funding/add-funds) | Present a UI to users to offer all available deposit methods (cards, bank transfer, exchange, crypto). | React | | [Card onramps](/wallets/funding/fiat-onramp) | Initiate a card onramp flow for users. | React, React Native | ## Payouts Payouts send funds from an account to a recipient wallet address or bank account. These methods automatically handles conversion of outgoing funds into a target fiat or crypto asset you specify. | Method | Description | Platform availability | | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------ | --------------------- | | [Fiat payouts](/financial-flows/transfers/fiat-payouts/overview) | Payout crypto from wallets and have it settle as fiat currencies in recipient bank accounts. | All | | [Crypto transfers](/wallets/actions/transfer/overview) | Payout crypto from wallets and have it settle as crypto in a recipient wallet, with native support for bridging and stablecoin conversions | All | # Configuration Source: https://docs.privy.io/financial-flows/payouts/configuration Configure Privy to send fiat and crypto payouts Crypto payouts do not require a payout provider. You can configure optional controls such as [policies](/wallets/actions/transfer/policies), [gas sponsorship](/wallets/gas-and-asset-management/gas/setup), and [developer fees](/wallets/actions/transfer/collect-fees) for the transfer action. Fiat payouts require a verified entity on the paying wallet and a provider configured for server-side flows. See [fiat payout setup](/financial-flows/transfers/fiat-payouts/setup) for the complete prerequisites. ## Bank transfer To enable [fiat payouts](/financial-flows/transfers/fiat-payouts/overview), enable **bank transfer** funding in the [Privy Dashboard](https://dashboard.privy.io/apps?page=funding). Once the method is enabled, configure your Bridge API keys per the steps below. ### Onboard with Bridge If your app does not already have a Bridge account, [sign up here](https://dashboard.bridge.xyz/get-started?utm_source=privy). If your app has already verified users with Bridge directly, those customers do not need to be verified again. Please reach out to [support@privy.io](mailto:support@privy.io) for additional details on linking an existing Bridge customer to a Privy user. ### Register a Bridge API key with Privy 1. Create an API key in the [Bridge dashboard](https://dashboard.bridge.xyz/). Bridge issues separate keys for its sandbox and production environments. 2. Register the key in the Privy Dashboard under [Onramps > Bridge > Configure](https://dashboard.privy.io/apps?page=funding). Configure Bridge Add at least one sandbox or production API key to enable Bridge for your app. For each environment, select where Bridge can be used: | Setting | Description | | ------------------------------------ | ------------------------------------------------------------------------------------------------------------- | | Enable client-side flows (React SDK) | Allows Bridge flows to be initiated from your app's frontend. | | Enable server-side flows (REST API) | Allows Bridge flows to be initiated from your app's backend, including KYC verification and KYB verification. | The sandbox environment does not move real money and does not require real identity or bank information. It is not a testnet: wallets and assets used in sandbox flows are still mainnet. See Bridge's guide to [setting up a sandbox environment](https://apidocs.bridge.xyz/get-started/introduction/quick-start/setting-up-sandbox) for what Bridge simulates, including [simulated KYC approvals](https://apidocs.bridge.xyz/api-reference/sandbox/simulate-kyc-approval-sandbox-only). # Payouts Source: https://docs.privy.io/financial-flows/payouts/overview Send funds from Privy wallets to bank accounts and crypto wallets Payouts send funds from a Privy wallet to a recipient wallet address or bank account. Privy can automatically convert outgoing funds into a target fiat or crypto asset you specify. Payouts Each flow is built on Privy wallets and delivered as an SDK method or API, so your business can disburse funds at scale without operating payments infrastructure of its own. | Method | Description | Platform availability | | ---------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | --------------------- | | [Fiat payouts](/financial-flows/transfers/fiat-payouts/overview) | Send crypto from wallets and have it settle as fiat currency in recipient bank accounts. | All | | [Crypto payouts](/wallets/actions/transfer/overview) | Send crypto to a recipient wallet, with native support for bridging across chains and converting between stablecoins of the same peg. | All | # Trade Source: https://docs.privy.io/financial-flows/swap Swap tokens on supported EVM chains and Solana via Privy wallet actions Privy enables your app to support token swaps on supported EVM chains and Solana directly from a wallet. Swaps execute as [wallet actions](/wallets/actions/overview), and Privy automates token approvals and transaction submission. Swap Swaps are executed on third-party decentralized protocols. Privy does not have discretion over how a swap is routed to the protocol or the price at which the swap is ultimately executed by the blockchain network. Swap rates may differ from quoted estimates due to market volatility, liquidity conditions, and slippage. These materials are for general information purposes only and are not investment advice or a recommendation or solicitation to engage in any specific transaction. Privy does not provide investment, financial, legal, or tax advice. ## How it works [Enable swaps](/wallets/actions/swap/setup) in the Privy Dashboard and configure [gas sponsorship](/wallets/gas-and-asset-management/gas/setup) for your app. Call the [quote endpoint](/wallets/actions/swap/get-quote) with the token pair, chain, and amount. The response includes estimated output amounts and a gas estimate. Call the [swap endpoint](/wallets/actions/swap/execute) with the same parameters. The response is a wallet action that can be polled for confirmation. ## Supported chains Swaps are available on the following chains. | Chain | Chain ID | CAIP-2 identifier | Native token | | --------------- | -------- | ----------------- | ------------ | | Ethereum | 1 | `eip155:1` | ETH | | Optimism | 10 | `eip155:10` | ETH | | BNB Smart Chain | 56 | `eip155:56` | BNB | | Unichain | 130 | `eip155:130` | ETH | | Polygon | 137 | `eip155:137` | POL | | Monad | 143 | `eip155:143` | MON | | World Chain | 480 | `eip155:480` | ETH | | Tempo | 4217 | `eip155:4217` | None | | Robinhood Chain | 4663 | `eip155:4663` | ETH | | Base | 8453 | `eip155:8453` | ETH | | Arbitrum | 42161 | `eip155:42161` | ETH | Tempo does not have a native token. Specify token contract addresses for Tempo swaps. | Chain | CAIP-2 identifier | Native token | | -------------- | ----------------------------------------- | ------------ | | Solana mainnet | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | SOL | | Chain | Chain ID | CAIP-2 identifier | Native token | | ----------------- | -------- | ----------------- | ------------ | | Unichain Sepolia | 1301 | `eip155:1301` | ETH | | Monad Testnet | 10143 | `eip155:10143` | MON | | Robinhood Testnet | 46630 | `eip155:46630` | ETH | | Sepolia | 11155111 | `eip155:11155111` | ETH | | Base Sepolia | 84532 | `eip155:84532` | ETH | ## Token addresses Specify token addresses as ERC-20 contract addresses (for EVM chains), TIP-20 contract addresses (for Tempo), or SPL token mint addresses (for Solana). Use `"native"` only on chains with native token support (e.g., ETH on Ethereum or SOL on Solana). The `input_token` and `output_token` must be different. Token addresses are chain-specific. Ensure that the addresses provided for `input_token` and `output_token` correspond to token contracts deployed on the chain specified in the `caip2` field. The same token (e.g., USDC) may have different contract addresses on different chains. Use a resource like [Token Lists](https://tokenlists.org/) to source correct addresses for your target chain. ## Fees The following fees may apply when executing a swap. Fees are subject to change. | Fee | Description | | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Privy swap fee** | Privy charges up to 0.25% on each swap, calculated from the input token amount. This fee is included in the swap rate. | | **Protocol fee** | Underlying DEX protocols charge a fee on each swap. For EVM chains, Uniswap liquidity pools typically charge 0.3% for V2 pools and 0.01%–1% for V3/V4 pools depending on the pool's fee tier. This fee is included in the swap rate. | | **Network fee (gas)** | Blockchain transaction processing fees paid to the network. Gas sponsorship is required for swaps, meaning Privy sponsors these fees from your app's gas credits. This includes any token approval transactions required for the swap (e.g., the first time a wallet swaps a given ERC-20 or TIP-20 token). Gas fees fluctuate based on network congestion and transaction complexity. | | **Relayer fee** | (Cross-chain only) Paid to the bridge provider for routing the swap across chains. Varies with network conditions and liquidity. Included in `estimated_fees` on the quote response. | | **Developer fee** | (Cross-chain only) A configurable fee allocated to your app. Requires a [custom fees configuration](/wallets/actions/swap/collect-fees). Set via the `fee_configuration` parameter on the quote and swap endpoints. Included in `estimated_fees` on the quote response. | The estimated output amounts returned by the [quote endpoint](/wallets/actions/swap/get-quote) reflect the swap rate after Privy and protocol fees. The `gas_estimate` field provides a separate estimate of the network fee. ## Slippage Slippage is the difference between the quoted price of a swap and the price at which it executes. Because token prices can change quickly, especially during periods of high volatility or low liquidity, the final execution price may differ from the quoted price. The `slippage_bps` parameter sets the maximum slippage tolerance in basis points (e.g., `50` for 0.5%). This controls the maximum percentage difference your app is willing to accept between the quoted price and the execution price. * If the maximum slippage is set too low, the swap may fail if the price moves beyond the specified tolerance. * If the maximum slippage is set higher, the swap is more likely to succeed, but the wallet may receive a less favorable price if the market moves significantly. * If a swap fails due to slippage, the wallet is still responsible for any network fees incurred. Privy enables the use of auto-slippage by omitting the `slippage_bps` parameter. When omitted, an appropriate slippage tolerance is automatically determined based on the tokens being swapped and current market conditions. Your app can also set a specific `slippage_bps` value and use the `minimum_output_amount` from the [quote response](/wallets/actions/swap/get-quote) to verify the minimum tokens received before executing. ## Cross-chain swaps Privy's swap API supports swapping tokens across different chains. A cross-chain swap routes output tokens to a destination chain — for example, swapping ETH on Base and receiving ETH on Arbitrum, or swapping USDC on Ethereum and receiving SOL on Solana. Cross-chain swaps use a `source` and `destination` object format with separate `caip2` identifiers for each side. See the [get a quote](/wallets/actions/swap/get-quote#cross-chain-swaps) and [execute a swap](/wallets/actions/swap/execute#cross-chain-swaps) pages for usage. Supported cross-chain routes mirror those for [transfers](/wallets/actions/transfer/overview#native-bridging): | Source chain | Supported destination chains | | --------------- | ----------------------------------------------------------- | | Ethereum | Base, Tempo, Robinhood Chain, Arbitrum, Polygon, Solana | | Base | Ethereum, Tempo, Robinhood Chain, Arbitrum, Polygon, Solana | | Tempo | Ethereum, Base, Robinhood Chain, Arbitrum, Polygon, Solana | | Robinhood Chain | Ethereum, Base, Tempo, Arbitrum, Polygon, Solana | | Arbitrum | Ethereum, Base, Tempo, Robinhood Chain, Polygon, Solana | | Polygon | Ethereum, Base, Tempo, Robinhood Chain, Arbitrum, Solana | | Solana | Ethereum, Base, Tempo, Robinhood Chain, Arbitrum, Polygon | Cross-chain swaps are subject to relayer fees, slippage, and current liquidity conditions. Quotes expire — use the `expires_at` field to detect stale quotes and fetch a fresh one before executing. ## Next steps Enable swaps and configure routing in the Privy Dashboard. Fetch a price quote before executing a swap. Execute a token swap from a wallet. Error reference for swap APIs. # Execute a payout Source: https://docs.privy.io/financial-flows/transfers/fiat-payouts/execute-payout Convert crypto from a wallet to fiat and settle it to a bank account A payout converts crypto from a wallet and settles it to a [registered bank account](/financial-flows/transfers/fiat-payouts/register-bank-account) in a single call. ## Supported assets and chains Pass `source.asset` as one of [Privy's well-known assets](/wallets/actions/transfer/overview#well-known-assets). The asset value differs by chain: Tempo uses `usdc_e` and `usdt0`, not `usdc` and `usdt`. | Chain | `source.chain` | Supported `source.asset` | | -------- | -------------- | ------------------------------ | | Tempo | `tempo` | `usdc_e`, `usdt0` | | Ethereum | `ethereum` | `usdc`, `usdt`, `usdb`, `eurc` | | Base | `base` | `usdc`, `usdb`, `eurc` | | Arbitrum | `arbitrum` | `usdc` | | Optimism | `optimism` | `usdc` | | Polygon | `polygon` | `usdc` | | Solana | `solana` | `usdc`, `usdt`, `usdb`, `eurc` | On Polygon, pay out `usdc` rather than `usdc_e`. Privy's `usdc_e` on Polygon is PoS-bridged USDC, which the provider does not accept, so a payout naming it is rejected. Use the `create` method from the `payout.fiat` resource on `wallets()`. ```ts {skip-check} theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID!, appSecret: process.env.PRIVY_APP_SECRET! }); const payout = await privy.wallets().payout.fiat.create('', { source: { asset: 'usdc_e', chain: 'tempo', amount: '100.00' }, destination: { fiat_account_id: '' } }); // payout.id is the wallet action ID to track ``` To pay out from a wallet, make a `POST` request to: ```bash theme={"system"} https://api.privy.io/v1/wallets/{wallet_id}/payout/fiat ``` See the [API reference](/api-reference/wallets/payout/create) for the full request and response schema. In the body of the request, include the following fields: The crypto to offramp. Asset to offramp. See [supported assets and chains](#supported-assets-and-chains). Chain the asset is held on. Must match the wallet's `chain_type`. See [supported assets and chains](#supported-assets-and-chains). Amount to offramp, as a decimal string in the asset's standard units, such as `"100.00"`. Where the fiat settles. ID of a registered bank account. The account's currency and rail determine how the fiat settles, so neither is passed here. Payouts are not yet a [policy](/controls/policies/overview) method. Because the policy engine denies any method a policy does not explicitly allow, attaching a policy that only covers other methods, such as `transfer`, blocks payouts from that wallet. Pass a `privy-idempotency-key` header to make retries safe. If the wallet has an owner, the request also requires an [authorization signature](/api-reference/authorization-signatures). Below is a sample cURL command for this request: ```bash theme={"system"} curl --request POST https://api.privy.io/v1/wallets/{wallet_id}/payout/fiat \ -u ":" \ -H "privy-app-id: " \ -H 'Content-Type: application/json' \ -H 'privy-idempotency-key: ' \ -d '{ "source": { "asset": "usdc_e", "chain": "tempo", "amount": "100.00" }, "destination": { "fiat_account_id": "fa_3ad996de-e827-4d2e-99fc-799838520453" } }' ``` A successful response is a pending payout wallet action: ID of the wallet action. Use this to track the payout. Type of the wallet action. Current status of the payout. Provider settling the payout. Provider environment the payout runs against. The `asset`, `chain`, and `amount` being offramped. The `fiat_account_id` the payout settles to. ```json theme={"system"} { "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "wallet_id": "fmfdj6yqly31huorjqzq38zc", "type": "payout", "status": "pending", "provider": "bridge", "environment": "sandbox", "source": { "asset": "usdc_e", "chain": "tempo", "amount": "100.00" }, "destination": { "fiat_account_id": "fa_3ad996de-e827-4d2e-99fc-799838520453" }, "created_at": "2026-08-03T12:00:00Z" } ``` ## Next steps Follow a payout from on-chain transfer to bank settlement Learn how wallet actions are authorized and executed # Get bank accounts Source: https://docs.privy.io/financial-flows/transfers/fiat-payouts/get-bank-accounts List, read, and delete the bank accounts registered for payouts Use the `list`, `get`, and `delete` methods from the `externalFiatAccounts` resource on `users()`. ```ts theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID!, appSecret: process.env.PRIVY_APP_SECRET! }); const {accounts} = await privy.users().externalFiatAccounts.list('did:privy:xxxxx', { provider: 'bridge', environment: 'sandbox' }); const {external_fiat_account: bankAccount} = await privy .users() .externalFiatAccounts.get('', {user_id: 'did:privy:xxxxx'}); await privy.users().externalFiatAccounts.delete('', { user_id: 'did:privy:xxxxx' }); ``` To list a user's registered accounts, make a `GET` request to: ```bash theme={"system"} https://api.privy.io/v1/users/{user_id}/external_fiat_accounts ``` See the API reference for [listing](/api-reference/fiat/external-fiat-accounts/list), [reading](/api-reference/fiat/external-fiat-accounts/get), and [deleting](/api-reference/fiat/external-fiat-accounts/delete) bank accounts. The endpoint accepts the following query parameters: Provider to list accounts for. Provider environment to list accounts for. Defaults to `production`. The response contains an `accounts` array alongside a `next_cursor`. To read or delete a single account, make a `GET` or `DELETE` request to `/v1/users/{user_id}/external_fiat_accounts/{account_id}`. A successful delete returns `{"success": true}`. ```bash theme={"system"} curl --request DELETE https://api.privy.io/v1/users/did:privy:xxxxx/external_fiat_accounts/fa_3ad996de-e827-4d2e-99fc-799838520453 \ -u ":" \ -H "privy-app-id: " ``` The same routes exist under `/v1/organizations/{organization_id}/external_fiat_accounts`. ## Next steps Convert crypto to fiat in a single call Save the bank account a payout settles to # Fiat payouts Source: https://docs.privy.io/financial-flows/transfers/fiat-payouts/overview Convert crypto in a wallet to fiat and settle it to a bank account in a single API call A payout converts crypto held in a Privy wallet to fiat and settles it to an external bank account. Payouts run as a [wallet action](/wallets/actions/overview), so a single API call sends the crypto on-chain and settles the fiat. Privy handles the provider-side routing, so your app only names the wallet, the amount, and the destination bank account. ## How a payout works 1. [Register the external bank account](/financial-flows/transfers/fiat-payouts/register-bank-account) against the user or organization being paid. 2. [Create a payout action](/financial-flows/transfers/fiat-payouts/execute-payout) to that external bank account, specifying the source asset, chain, and amount. 3. [Track the status](/financial-flows/transfers/fiat-payouts/track-payouts) as Privy sends the crypto on-chain and the provider converts and settles the fiat. ## Getting started Verify the paying entity and configure the provider Save the bank account a payout settles to Convert crypto to fiat in a single call # Register a bank account Source: https://docs.privy.io/financial-flows/transfers/fiat-payouts/register-bank-account Register the external bank account a fiat payout settles to A bank account is registered once against a user or organization, and can then receive payouts from any of that entity's wallets. Use the `create` method from the `externalFiatAccounts` resource on `users()`. Organizations have the same methods under `organizations()`. ```ts theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID!, appSecret: process.env.PRIVY_APP_SECRET! }); const {external_fiat_account: bankAccount} = await privy .users() .externalFiatAccounts.create('did:privy:xxxxx', { provider: 'bridge', environment: 'sandbox', currency: 'usd', account_owner_name: 'John Doe', bank_name: 'Chase', account: { type: 'us', account_number: '1234567899', routing_number: '121212121', checking_or_savings: 'checking' }, address: { street_line_1: '123 Washington St', city: 'New York', state: 'NY', postal_code: '10001', country: 'USA' } }); // bankAccount.id is the fiat_account_id to pay out to ``` To register a bank account for a user, make a `POST` request to: ```bash theme={"system"} https://api.privy.io/v1/users/{user_id}/external_fiat_accounts ``` See the [API reference](/api-reference/fiat/external-fiat-accounts/create) for the full request and response schema. To register one for an organization, make a `POST` request to `/v1/organizations/{organization_id}/external_fiat_accounts`. The request and response bodies are identical. In the body of the request, include the following fields: Provider that settles payouts to this account. Provider environment to use. Defaults to `production`. Fiat currency the account settles in, such as `usd` or `eur`. Legal name of the account holder. Set this to a third party's name to pay someone other than the verified entity. Name of the bank holding the account. Bank account details. The `type` field determines which other fields apply. Type of the bank account, which determines the rail the payout settles over. Settles over ACH or wire. Requires `account_number` and a 9-digit `routing_number`, and accepts `checking_or_savings`. Settles over Faster Payments. Requires an 8-digit `account_number` and a 6-digit `sort_code`. Settles over SEPA. Requires `account_number` (the IBAN), `bic`, and `country` as an ISO 3166-1 alpha-3 code. Settles over Pix. Requires exactly one of `pix_key` (an EVP, CPF, CNPJ, Brazilian phone number, or email) or `br_code`, and accepts `document_number`. Settles over wire, cross-border. Requires `account_number`, `bic`, `category`, at least one `purpose_of_funds`, and a `short_business_description`. Address of the account holder, containing `street_line_1`, `city`, `country` as an ISO 3166-1 alpha-3 code, and optionally `street_line_2`, `state`, and `postal_code`. Required for `us` and `swift` accounts. Below is a sample cURL command for this request: ```bash theme={"system"} curl --request POST https://api.privy.io/v1/users/did:privy:xxxxx/external_fiat_accounts \ -u ":" \ -H "privy-app-id: " \ -H 'Content-Type: application/json' \ -d '{ "provider": "bridge", "environment": "sandbox", "currency": "usd", "account_owner_name": "John Doe", "bank_name": "Chase", "account": { "type": "us", "account_number": "1234567899", "routing_number": "121212121", "checking_or_savings": "checking" }, "address": { "street_line_1": "123 Washington St", "city": "New York", "state": "NY", "postal_code": "10001", "country": "USA" } }' ``` A successful response includes the following fields: The registered account. Unique ID of the account. Pass this as `destination.fiat_account_id` when creating a payout. ID of the user the account belongs to. Organization accounts return `organization_id` instead. Provider that settles payouts to this account. Provider environment the account belongs to. Fiat currency the account settles in. Type of the bank account. Name of the bank holding the account. Last four digits of the account number. The full number is never returned after creation. Legal name of the account holder. When the account was registered, as an ISO 8601 timestamp. ```json theme={"system"} { "external_fiat_account": { "id": "fa_3ad996de-e827-4d2e-99fc-799838520453", "user_id": "did:privy:xxxxx", "provider": "bridge", "environment": "sandbox", "currency": "usd", "bank_name": "Chase", "account_type": "us", "last_4": "7899", "account_owner_name": "John Doe", "created_at": "2026-07-30T12:00:00Z" } } ``` A registered account's bank details cannot be changed. To pay out to different details, delete the account and register a new one. This prevents a payout from being redirected to another bank account without your app's knowledge. ## Next steps List, read, and delete registered bank accounts Convert crypto to fiat in a single call # Setup Source: https://docs.privy.io/financial-flows/transfers/fiat-payouts/setup Verify the paying entity and configure the provider for fiat payouts Payouts require a verified entity on the paying wallet and a provider configured for server-side flows. ## Verify the paying entity A payout settles from the verified person or business the wallet is attributed to, called its [entity](/kyc-kyb/entities). That entity must have completed verification: * [KYC](/kyc-kyb/kyc) for individuals * [KYB](/kyc-kyb/kyb) for organizations Wallets created for a user are [attributed automatically](/kyc-kyb/entities#automatic-attribution). Wallets created server-side with an app secret are not, even when their owner is a user, so those must have an entity [assigned explicitly](/kyc-kyb/entities#assign-an-entity) before a payout can be created. ## Configure the provider Register a Bridge API key in the Privy Dashboard under [Onramps > Bridge > Configure](https://dashboard.privy.io/apps?page=funding) and enable **server-side flows (REST API and server SDKs)** for the environment your app is using. Payouts fail if only client-side flows are enabled. See [KYC and KYB setup](/kyc-kyb/setup) for the full walkthrough. Configure Bridge for server-side flows Manage these resources through Privy only. Creating, modifying, or deleting the underlying resources directly in the Bridge API or Bridge dashboard can leave Privy and Bridge out of sync and break the integration. ## Next steps Save the bank account a payout settles to Learn how wallets are attributed to a verified user or organization # Track a payout Source: https://docs.privy.io/financial-flows/transfers/fiat-payouts/track-payouts Track payout status, webhooks, and failure modes A payout moves crypto out of the wallet on-chain and then settles fiat at the provider, so it completes asynchronously. The action's `status` is the primary signal: `succeeded` means the fiat settled, not just that the crypto moved. Read the current state with [`GET /v1/wallets/{wallet_id}/actions/{action_id}`](/api-reference/wallets/actions/get), or subscribe to [wallet action webhooks](/wallets/actions/webhooks): | Event | Fires when | | -------------------------------- | ------------------------------------------------------------------------------------------------- | | `wallet_action.payout.created` | The payout is created and queued. | | `wallet_action.payout.succeeded` | The provider settled the fiat to the bank account. | | `wallet_action.payout.rejected` | Privy rejected the payout before broadcasting the on-chain transfer. No crypto left the wallet. | | `wallet_action.payout.failed` | The payout failed after the on-chain transfer was broadcast. See [failure modes](#failure-modes). | Payload fields include `provider`, `environment`, `source_asset`, `source_chain`, `source_amount`, `destination_fiat_account_id`, `destination_currency`, and `destination_payment_rail`, alongside the standard wallet action fields, plus `failure_reason` on the terminal failure events. ## Failure modes `rejected` and `failed` call for different handling, and `failed` itself covers more than one situation. The question that decides the response is whether the crypto has already left the wallet, because retrying a payout sends more crypto. The specific steps Privy runs for a payout are an [implementation detail](/wallets/actions/lifecycle#steps-by-action-type) and may change. Inspect `steps` for diagnostics rather than assuming a fixed order, count, or set of types. On a terminal failure, read the action's `steps` and work through them: Look for a step of type `evm_transaction` or `svm_transaction` with status `confirmed`. If none confirmed, the crypto never left the wallet. The failure is either pre-broadcast (`rejected`) or an on-chain failure such as `reverted`, `replaced`, or `abandoned`. Read the step's `failure_reason`, confirm the balance is intact, and retry once the cause is fixed. The crypto reached the provider and the fiat leg is what failed. Read the failed step's `failure_reason` and the provider state below to determine where the funds are. Reissuing the payout would send a second transfer. When the fiat leg fails, the provider's state says where the funds ended up: | Provider state | What it means | | ------------------------------------ | ----------------------------------------------------------------------------------- | | `returned`, `refunded` | The provider sent the crypto back on-chain. Expect it to arrive back in the wallet. | | `refund_failed` | Settlement failed and the refund also failed. The funds are held at the provider. | | `error`, `canceled`, `undeliverable` | Settlement could not complete, for example because the bank rejected the transfer. | A payout whose fiat leg failed with `refund_failed`, `error`, `canceled`, or `undeliverable` needs provider support to resolve, since the crypto has already left the wallet. Reach out to Privy support rather than reissuing the payout. `rejected` means no crypto was broadcast, so the wallet is untouched and the payout can be retried once the cause is fixed. Common causes are a [policy](/controls/policies/overview) denial, a missing or invalid authorization signature, an unverified [entity](/kyc-kyb/entities), and a source asset or chain the provider does not support. ## Next steps Track payouts and other wallet actions in real time Fund a wallet with fiat through a dedicated bank account # Entities Source: https://docs.privy.io/kyc-kyb/entities Understand a wallet's entity, how it differs from ownership, and how it is assigned Identity verification and compliance is scoped to a wallet's **entity**: the user or organization the wallet is for. Products that require KYC or KYB read a wallet's entity to determine whether the person or business behind it is verified. A wallet's entity is immutable once set. The `entity` field is distinct from the `owner` field: | Field | What it means | Permanence | | ---------- | ------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | | **Owner** | Who can configure and authorize actions from the wallet. May be a user, authorization key, or key quorum. | Mutable with an authorization signature | | **Entity** | Who the wallet is for. May be a [user](/user-management/users/overview) or [organization](/organizations/overview). | Immutable once set | A wallet owned by an authorization key still needs an entity to participate in a regulated flow, because the authorization key is not a verified person or business. ## Where entities are required To participate in a regulated flow, a wallet must have an `entity` set: * [Fiat deposits](/wallets/funding/fiat-deposits/overview) * [Fiat payouts](/financial-flows/transfers/fiat-payouts/overview) If a wallet has no entity, or its entity has not completed [KYC](/kyc-kyb/kyc) or [KYB](/kyc-kyb/kyb), these requests fail. ## Automatically assigned entities Wallets created for a user are assigned to that user automatically: * Wallets created alongside the user, through `POST /v1/users` * Wallets created for an existing user, through `POST /v1/users/{user_id}/wallets`, including wallets created at login * Wallets created through Privy's client-side SDKs, which are always created for the authenticated user * Wallets created through `POST /v1/wallets` using that user's access token, when `entity` is omitted Wallets created through `POST /v1/wallets` with an app secret are **not** assigned automatically, even when `owner` is a user. Ownership and entity assignment are independent, so the entity must be set explicitly in these cases. ## Explicitly assign an entity An entity can be assigned when the wallet is created, or afterwards. Pass `entity` to `create` to assign at creation, or use `assignEntity` on an existing wallet. ```ts theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID!, appSecret: process.env.PRIVY_APP_SECRET! }); // At creation const wallet = await privy.wallets().create({ chain_type: 'ethereum', entity: {id: 'did:privy:xxxxx', type: 'user'} }); // After creation await privy.wallets().assignEntity(wallet.id, { id: 'did:privy:xxxxx', type: 'user' }); ``` Pass `entity` when creating the wallet, in a `POST` request to: ```bash theme={"system"} https://api.privy.io/v1/wallets ``` ```bash theme={"system"} curl --request POST https://api.privy.io/v1/wallets \ -u ":" \ -H "privy-app-id: " \ -H 'Content-Type: application/json' \ -d '{ "chain_type": "ethereum", "entity": { "id": "did:privy:xxxxx", "type": "user" } }' ``` Make a `POST` request to: ```bash theme={"system"} https://api.privy.io/v1/wallets/{wallet_id}/entity ``` See the [API reference](/api-reference/wallets/entity) for the full request and response schema. In the body of the request, include the following fields: ID of the Privy user or organization the wallet is for. Type of the entity being assigned. ```bash theme={"system"} curl --request POST https://api.privy.io/v1/wallets/{wallet_id}/entity \ -u ":" \ -H "privy-app-id: " \ -H 'Content-Type: application/json' \ -d '{ "id": "did:privy:xxxxx", "type": "user" }' ``` A wallet's entity is immutable. Once set, it cannot be changed or reassigned — a second assignment fails with a `wallet_entity_already_set` error. Create a new wallet if the entity needs to change. ## Read a wallet's entity Wallets return their entity as an `entity` object containing the entity `id` and `type` (`'user'` or `'organization'`), or `null` if none is assigned. Wallets can also be filtered by `entity_id` when listing them. ## Next steps Verify an individual user Verify an organization # KYB Source: https://docs.privy.io/kyc-kyb/kyb Verify an organization with terms of service acceptance and a hosted KYB flow Privy verifies an [organization](/organizations/overview) with Bridge. Privy creates the [Bridge business customer](https://apidocs.bridge.xyz/platform/customers/customers/api) on the first request and links it to the Privy organization, so your app only ever references the Privy organization ID. At a high level, KYB involves two steps: An authorized representative of the organization accepts Bridge's terms of service through a hosted link. The representative submits the organization's information through a hosted verification flow. Bridge currently supports [hosted KYB](https://apidocs.bridge.xyz/platform/customers/customers/kyclinks) only. Both endpoints return a link that your app passes to its frontend for an authorized representative of the organization to open and complete. Before verifying organizations, [register a Bridge API key with Privy](/kyc-kyb/setup). Bridge reviews business customers manually, so KYB usually takes longer than KYC for an individual. Bridge may also require the organization's associated persons to complete their own verification. ## Accept terms of service Use the `initiateTos` method from the `kyb` resource on `organizations()`. ```ts {skip-check} theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID!, appSecret: process.env.PRIVY_APP_SECRET! }); const tos = await privy.organizations().kyb.initiateTos('', { provider: 'bridge', environment: 'sandbox', email: 'finance@acme.com', business_name: 'Acme, Inc.' }); // tos.link is the terms of service link to pass to your frontend ``` To generate a [terms of service](https://apidocs.bridge.xyz/platform/customers/customers/tos) link for an organization, make a `POST` request to: ```bash theme={"system"} https://api.privy.io/v1/organizations/{organization_id}/kyb/tos ``` See the [API reference](/api-reference/fiat/kyb/tos) for the full request and response schema. In the body of the request, include the following fields: Provider to verify the organization with. Bridge environment to use. Defaults to `production`. Email address for the Bridge business customer. Bridge sends requests for information to this address. Legal name of the business. Below is a sample cURL command for this request: ```bash theme={"system"} curl --request POST https://api.privy.io/v1/organizations/xxxxx/kyb/tos \ -u ":" \ -H "privy-app-id: " \ -H 'Content-Type: application/json' \ -d '{ "provider": "bridge", "environment": "sandbox", "email": "finance@acme.com", "business_name": "Acme, Inc." }' ``` A successful response includes the following fields: Provider the organization is being verified with. Bridge environment used for the request. Status of terms of service acceptance, as reported by Bridge. URL the organization's representative opens to accept Bridge's terms of service. ```json theme={"system"} { "provider": "bridge", "environment": "sandbox", "status": "pending", "link": "https://compliance.sandbox.bridge.xyz/accept-tos?customer_id=..." } ``` Pass the `link` to your app's frontend so the organization's representative can accept the terms of service. The request is idempotent: calling it again for the same organization returns a link for the existing Bridge customer. ## Create a KYB link Use the `initiateLinks` method from the `kyb` resource on `organizations()`. ```ts {skip-check} theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID!, appSecret: process.env.PRIVY_APP_SECRET! }); const status = await privy.organizations().kyb.initiateLinks('', { provider: 'bridge', environment: 'sandbox', email: 'finance@acme.com', business_name: 'Acme, Inc.', endorsements: ['base'], redirect_uri: 'https://your-app.com/kyb/complete' }); // status.kyb.link is the hosted KYB link to pass to your frontend ``` To generate a hosted KYB link for an organization, make a `POST` request to: ```bash theme={"system"} https://api.privy.io/v1/organizations/{organization_id}/kyb/links ``` See the [API reference](/api-reference/fiat/kyb/links) for the full request and response schema. In the body of the request, include the following fields: Provider to verify the organization with. Bridge environment to use. Defaults to `production`. Email address for the Bridge business customer. Legal name of the business. [Endorsements](https://apidocs.bridge.xyz/platform/customers/customers/endorsements) to request from Bridge. Each endorsement unlocks a set of rails and regions. Defaults to `['base']`. URI the representative is redirected to after completing the hosted flow. Identifier of the terms of service agreement the organization accepted in your app. Only applicable if your app has arranged [terms of service reliance](https://apidocs.bridge.xyz/platform/customers/compliance/terms-of-service-reliance) with Bridge. Below is a sample cURL command for this request: ```bash theme={"system"} curl --request POST https://api.privy.io/v1/organizations/xxxxx/kyb/links \ -u ":" \ -H "privy-app-id: " \ -H 'Content-Type: application/json' \ -d '{ "provider": "bridge", "environment": "sandbox", "email": "finance@acme.com", "business_name": "Acme, Inc.", "endorsements": ["base"], "redirect_uri": "https://your-app.com/kyb/complete" }' ``` The response is the organization's full KYB status, including the link to complete verification. See [Track KYB status](#track-kyb-status) for the complete set of fields. ```json theme={"system"} { "provider": "bridge", "environment": "sandbox", "status": "not_started", "tos": { "status": "approved" }, "kyb": { "status": "not_started", "link": "https://bridge.withpersona.com/verify?..." }, "endorsements": [ { "name": "base", "status": "incomplete", "missing": ["proof_of_ownership"] } ], "capabilities": { "payin_crypto": "pending", "payout_crypto": "pending", "payin_fiat": "pending", "payout_fiat": "pending" }, "requirements_due": [], "future_requirements_due": [] } ``` Pass `kyb.link` to your app's frontend so the organization's representative can complete verification. The request is idempotent: calling it again for the same organization returns the existing link. ## Track KYB status Verification is asynchronous. Bridge reviews the submission after the representative completes the hosted flow, and can revoke an endorsement later. See [track KYB status](/kyc-kyb/kyb-status) to poll an organization's status or subscribe to webhooks. ## Next steps Poll an organization's verification status or subscribe to webhooks Create wallets an organization's members can operate together Configure an endpoint to receive Privy webhook events # Track KYB status Source: https://docs.privy.io/kyc-kyb/kyb-status Poll an organization's KYB status or subscribe to verification webhooks Verification is asynchronous. Bridge reviews the submission after the representative completes the hosted flow, and can revoke an endorsement later. Your app can poll for status or subscribe to webhooks. See Bridge's [customer status lifecycle](https://apidocs.bridge.xyz/platform/customers/customers/api) for how Bridge derives each status. Before tracking status, [create a KYB link for the organization](/kyc-kyb/kyb). ## Fetch by API Use the `list` method from the `kyb` resource on `organizations()`. ```ts {skip-check} theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID!, appSecret: process.env.PRIVY_APP_SECRET! }); const {kyb_statuses: statuses} = await privy.organizations().kyb.list(''); // one entry per provider the organization has been verified with for (const status of statuses) { console.log(status.provider, status.kyb.status, status.endorsements); } ``` To read an organization's current KYB status, make a `GET` request to: ```bash theme={"system"} https://api.privy.io/v1/organizations/{organization_id}/kyb ``` See the [API reference](/api-reference/fiat/kyb/get) for the full response schema. The endpoint accepts the following query parameter: Only return status for this provider. If omitted, the response includes every provider the organization has been verified with. Below is a sample cURL command for this request: ```bash theme={"system"} curl --request GET 'https://api.privy.io/v1/organizations/xxxxx/kyb?provider=bridge' \ -u ":" \ -H "privy-app-id: " ``` The response contains a `kyb_statuses` array with one entry per provider, alongside a `next_cursor`. Privy fetches status from Bridge on every request, so revoked endorsements are reflected immediately. One entry per provider the organization has been verified with. Each entry contains the fields below. Cursor for the next page of results, or `null` when there are no more. Provider that verified the organization. Bridge environment the business customer belongs to. Top-level status of the business customer, as reported by Bridge. A customer is `active` once any endorsement is approved. Terms of service state. Status of terms of service acceptance. URL to accept the terms of service. Present while acceptance is pending. Verification state. Status of the organization's verification. URL to complete verification. Present while verification is outstanding. [Reasons](https://apidocs.bridge.xyz/platform/customers/customers/rejection_reasons) Bridge rejected the submission. Endorsements requested for the organization. Name of the [endorsement](https://apidocs.bridge.xyz/platform/customers/customers/endorsements), such as `base` or `sepa`. Status of the endorsement, as reported by Bridge. Requirements Bridge is still waiting on. `null` when the endorsement is complete. Status of each capability the business customer can use. Whether the organization can deposit crypto. Whether the organization can receive crypto payouts. Whether the organization can deposit fiat. Whether the organization can receive fiat payouts. Requirements the organization must still satisfy, such as linking a bank account. Requirements the organization will need to satisfy in the future. ```json theme={"system"} { "kyb_statuses": [ { "provider": "bridge", "environment": "sandbox", "status": "active", "tos": { "status": "approved" }, "kyb": { "status": "approved", "rejection_reasons": [] }, "endorsements": [ { "name": "base", "status": "approved", "missing": null } ], "capabilities": { "payin_crypto": "active", "payout_crypto": "active", "payin_fiat": "active", "payout_fiat": "active" }, "requirements_due": [], "future_requirements_due": [] } ], "next_cursor": null } ``` ## Webhooks Privy emits an `organization.kyb.updated` [webhook](/api-reference/webhooks/overview) whenever an organization's verification state changes at Bridge, re-emitting Bridge's [customer webhooks](https://apidocs.bridge.xyz/platform/additional-information/webhooks/structure) as a Privy event. The payload carries a full state snapshot in `data` and a `changes` diff of the fields that moved, so your app can react to a specific transition without storing prior state. Webhooks can be tested at no cost in development environments. To enable webhooks in production, upgrade to the Enterprise plan in the Privy Dashboard. Type of the webhook event. ID of the Privy organization whose verification state changed. Provider that reported the change. Bridge environment the business customer belongs to. Full snapshot of the organization's verification state at the time of the event. Top-level status of the business customer, as reported by Bridge. Terms of service state, containing `status`. Verification state, containing `status`. Endorsements for the organization. Each entry contains `name`, `status`, and `missing`, which is `null` when the endorsement is complete. Status of `payin_crypto`, `payout_crypto`, `payin_fiat`, and `payout_fiat`. Fields that changed, keyed by dot-notation path. Each value is a `[previous, current]` tuple. Privy omits events where no meaningful verification field changed. ```json theme={"system"} { "type": "organization.kyb.updated", "organization_id": "xxxxx", "provider": "bridge", "environment": "sandbox", "data": { "status": "active", "tos": { "status": "approved" }, "kyb": { "status": "active" }, "endorsements": [ { "name": "base", "status": "approved", "missing": null } ], "capabilities": { "payin_crypto": "active", "payout_crypto": "active", "payin_fiat": "pending", "payout_fiat": "pending" } }, "changes": { "endorsements.base.status": ["incomplete", "approved"], "capabilities.payin_crypto": ["pending", "active"] } } ``` If an endorsement is incomplete, read `data.endorsements[].missing` for the requirements Bridge is still waiting on, then create a new KYB link for the organization to resolve them. ## Next steps Configure an endpoint to receive Privy webhook events Verify an individual user # KYC Source: https://docs.privy.io/kyc-kyb/kyc Verify an individual user with terms of service acceptance and a hosted KYC flow Privy verifies an individual user with Bridge. Privy creates the [Bridge customer](https://apidocs.bridge.xyz/platform/customers/customers/api) on the first request and links it to the Privy user, so your app only ever references the Privy user ID. At a high level, KYC involves two steps: The user accepts Bridge's terms of service through a hosted link. The user submits their identity information through a hosted verification flow. Bridge currently supports [hosted KYC](https://apidocs.bridge.xyz/platform/customers/customers/kyclinks) only. Both endpoints return a link that your app passes to its frontend for the user to open and complete. Before verifying users, [register a Bridge API key with Privy](/kyc-kyb/setup). ## Accept terms of service Use the `initiateTos` method from the `kyc` resource on `users()`. ```ts theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID!, appSecret: process.env.PRIVY_APP_SECRET! }); const tos = await privy.users().kyc.initiateTos('did:privy:xxxxx', { provider: 'bridge', environment: 'sandbox', email: 'user@example.com' }); // tos.link is the terms of service link to pass to your frontend ``` To generate a [terms of service](https://apidocs.bridge.xyz/platform/customers/customers/tos) link for a user, make a `POST` request to: ```bash theme={"system"} https://api.privy.io/v1/users/{user_id}/kyc/tos ``` See the [API reference](/api-reference/fiat/kyc-server/tos) for the full request and response schema. In the body of the request, include the following fields: Provider to verify the user with. Bridge environment to use. Defaults to `production`. Email address for the Bridge customer. Falls back to the user's linked email account. Required if the user has no linked email. Below is a sample cURL command for this request: ```bash theme={"system"} curl --request POST https://api.privy.io/v1/users/did:privy:xxxxx/kyc/tos \ -u ":" \ -H "privy-app-id: " \ -H 'Content-Type: application/json' \ -d '{ "provider": "bridge", "environment": "sandbox", "email": "user@example.com" }' ``` A successful response includes the following fields: Provider the user is being verified with. Bridge environment used for the request. Status of terms of service acceptance, as reported by Bridge. URL the user opens to accept Bridge's terms of service. ```json theme={"system"} { "provider": "bridge", "environment": "sandbox", "status": "pending", "link": "https://compliance.sandbox.bridge.xyz/accept-tos?customer_id=..." } ``` Pass the `link` to your app's frontend so the user can accept the terms of service. The request is idempotent: calling it again for the same user returns a link for the existing Bridge customer. ## Create a KYC link Use the `initiateLinks` method from the `kyc` resource on `users()`. ```ts theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID!, appSecret: process.env.PRIVY_APP_SECRET! }); const status = await privy.users().kyc.initiateLinks('did:privy:xxxxx', { provider: 'bridge', environment: 'sandbox', email: 'user@example.com', endorsements: ['base'], redirect_uri: 'https://your-app.com/kyc/complete' }); // status.kyc.link is the hosted KYC link to pass to your frontend ``` To generate a hosted KYC link for a user, make a `POST` request to: ```bash theme={"system"} https://api.privy.io/v1/users/{user_id}/kyc/links ``` See the [API reference](/api-reference/fiat/kyc-server/links) for the full request and response schema. In the body of the request, include the following fields: Provider to verify the user with. Bridge environment to use. Defaults to `production`. Email address for the Bridge customer. Falls back to the user's linked email account. Required if the user has no linked email. [Endorsements](https://apidocs.bridge.xyz/platform/customers/customers/endorsements) to request from Bridge. Each endorsement unlocks a set of rails and regions. Defaults to `['base']`. URI the user is redirected to after completing the hosted flow. Identifier of the terms of service agreement the user accepted in your app. Only applicable if your app has arranged [terms of service reliance](https://apidocs.bridge.xyz/platform/customers/compliance/terms-of-service-reliance) with Bridge. Below is a sample cURL command for this request: ```bash theme={"system"} curl --request POST https://api.privy.io/v1/users/did:privy:xxxxx/kyc/links \ -u ":" \ -H "privy-app-id: " \ -H 'Content-Type: application/json' \ -d '{ "provider": "bridge", "environment": "sandbox", "email": "user@example.com", "endorsements": ["base"], "redirect_uri": "https://your-app.com/kyc/complete" }' ``` The response is the user's full KYC status, including the link to complete verification. See [Track KYC status](#track-kyc-status) for the complete set of fields. ```json theme={"system"} { "provider": "bridge", "environment": "sandbox", "status": "not_started", "tos": { "status": "approved" }, "kyc": { "status": "not_started", "link": "https://bridge.withpersona.com/verify?..." }, "endorsements": [ { "name": "base", "status": "incomplete", "missing": ["government_id_verification"] } ], "capabilities": { "payin_crypto": "pending", "payout_crypto": "pending", "payin_fiat": "pending", "payout_fiat": "pending" }, "requirements_due": [], "future_requirements_due": [] } ``` Pass `kyc.link` to your app's frontend so the user can complete verification. The request is idempotent: calling it again for the same user returns the existing link. ## Track KYC status Verification is asynchronous. Bridge reviews the submission after the user completes the hosted flow, and can revoke an endorsement later. See [track KYC status](/kyc-kyb/kyc-status) to poll a user's status or subscribe to webhooks. ## Next steps Poll a user's verification status or subscribe to webhooks Verify an organization Configure an endpoint to receive Privy webhook events # Track KYC status Source: https://docs.privy.io/kyc-kyb/kyc-status Poll a user's KYC status or subscribe to verification webhooks Verification is asynchronous. Bridge reviews the submission after the user completes the hosted flow, and can revoke an endorsement later. Your app can poll for status or subscribe to webhooks. See Bridge's [customer status lifecycle](https://apidocs.bridge.xyz/platform/customers/customers/api) for how Bridge derives each status. Before tracking status, [create a KYC link for the user](/kyc-kyb/kyc). ## Fetch by API Use the `list` method from the `kyc` resource on `users()`. ```ts theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID!, appSecret: process.env.PRIVY_APP_SECRET! }); const {kyc_statuses: statuses} = await privy.users().kyc.list('did:privy:xxxxx'); // one entry per provider the user has been verified with for (const status of statuses) { console.log(status.provider, status.kyc.status, status.endorsements); } ``` To read a user's current KYC status, make a `GET` request to: ```bash theme={"system"} https://api.privy.io/v1/users/{user_id}/kyc ``` See the [API reference](/api-reference/fiat/kyc-server/get) for the full response schema. The endpoint accepts the following query parameter: Only return status for this provider. If omitted, the response includes every provider the user has been verified with. Below is a sample cURL command for this request: ```bash theme={"system"} curl --request GET 'https://api.privy.io/v1/users/did:privy:xxxxx/kyc?provider=bridge' \ -u ":" \ -H "privy-app-id: " ``` The response contains a `kyc_statuses` array with one entry per provider, alongside a `next_cursor`. Privy fetches status from Bridge on every request, so revoked endorsements are reflected immediately. One entry per provider the user has been verified with. Each entry contains the fields below. Cursor for the next page of results, or `null` when there are no more. Provider that verified the user. Bridge environment the customer belongs to. Top-level status of the customer, as reported by Bridge. A customer is `active` once any endorsement is approved. Terms of service state. Status of terms of service acceptance. URL to accept the terms of service. Present while acceptance is pending. Verification state. Status of the user's verification. URL to complete verification. Present while verification is outstanding. [Reasons](https://apidocs.bridge.xyz/platform/customers/customers/rejection_reasons) Bridge rejected the submission. Endorsements requested for the user. Name of the [endorsement](https://apidocs.bridge.xyz/platform/customers/customers/endorsements), such as `base` or `sepa`. Status of the endorsement, as reported by Bridge. Requirements Bridge is still waiting on. `null` when the endorsement is complete. Status of each capability the customer can use. Whether the customer can deposit crypto. Whether the customer can receive crypto payouts. Whether the customer can deposit fiat. Whether the customer can receive fiat payouts. Requirements the user must still satisfy, such as linking a bank account. Requirements the user will need to satisfy in the future. ```json theme={"system"} { "kyc_statuses": [ { "provider": "bridge", "environment": "sandbox", "status": "active", "tos": { "status": "approved" }, "kyc": { "status": "approved", "rejection_reasons": [] }, "endorsements": [ { "name": "base", "status": "approved", "missing": null } ], "capabilities": { "payin_crypto": "active", "payout_crypto": "active", "payin_fiat": "active", "payout_fiat": "active" }, "requirements_due": [], "future_requirements_due": [] } ], "next_cursor": null } ``` ## Webhooks Privy emits a `user.kyc.updated` [webhook](/api-reference/webhooks/overview) whenever a user's verification state changes at Bridge, re-emitting Bridge's [customer webhooks](https://apidocs.bridge.xyz/platform/additional-information/webhooks/structure) as a Privy event. The payload carries a full state snapshot in `data` and a `changes` diff of the fields that moved, so your app can react to a specific transition without storing prior state. Webhooks can be tested at no cost in development environments. To enable webhooks in production, upgrade to the Enterprise plan in the Privy Dashboard. Type of the webhook event. ID of the Privy user whose verification state changed. Provider that reported the change. Bridge environment the customer belongs to. Full snapshot of the user's verification state at the time of the event. Top-level status of the customer, as reported by Bridge. Terms of service state, containing `status`. Verification state, containing `status`. Endorsements for the user. Each entry contains `name`, `status`, and `missing`, which is `null` when the endorsement is complete. Status of `payin_crypto`, `payout_crypto`, `payin_fiat`, and `payout_fiat`. Fields that changed, keyed by dot-notation path. Each value is a `[previous, current]` tuple. Privy omits events where no meaningful verification field changed. ```json theme={"system"} { "type": "user.kyc.updated", "user_id": "did:privy:xxxxx", "provider": "bridge", "environment": "sandbox", "data": { "status": "active", "tos": { "status": "approved" }, "kyc": { "status": "active" }, "endorsements": [ { "name": "base", "status": "approved", "missing": null } ], "capabilities": { "payin_crypto": "active", "payout_crypto": "active", "payin_fiat": "pending", "payout_fiat": "pending" } }, "changes": { "endorsements.base.status": ["incomplete", "approved"], "capabilities.payin_crypto": ["pending", "active"] } } ``` If an endorsement is incomplete, read `data.endorsements[].missing` for the requirements Bridge is still waiting on, then create a new KYC link for the user to resolve them. ## Next steps Configure an endpoint to receive Privy webhook events Verify an organization # KYC and KYB Source: https://docs.privy.io/kyc-kyb/overview Verify individuals (KYC) and businesses (KYB) through Privy before using regulated products Privy natively supports identity verification flows through partner providers to enable your app to verify individuals (KYC) and businesses (KYB) for regulated flows. Verification is a prerequisite for products that move regulated value on a user's behalf, including: * [Fiat onramps and offramps](/financial-flows/overview) * [Issued cards](/financial-flows/cards/overview) * [Custodial wallets](/wallets/custodial-wallets/overview) ## Getting started Configure identity verification for your app Verify an individual user Verify an organization # Setup Source: https://docs.privy.io/kyc-kyb/setup Register a provider API key and enable server-side flows for KYC and KYB Privy uses [Bridge](https://apidocs.bridge.xyz/get-started/introduction/overview) for fiat orchestration capabilities, including identity verification. ### Onboard with Bridge If your app does not already have a Bridge account, [sign up here](https://dashboard.bridge.xyz/get-started?utm_source=privy). If your app has already verified users with Bridge directly, those customers do not need to be verified again. Please reach out to [support@privy.io](mailto:support@privy.io) for additional details on linking an existing Bridge customer to a Privy user. ### Register a Bridge API key with Privy 1. Create an API key in the [Bridge dashboard](https://dashboard.bridge.xyz/). Bridge issues separate keys for its sandbox and production environments. 2. Register the key in the Privy Dashboard under [Onramps > Bridge > Configure](https://dashboard.privy.io/apps?page=funding). Configure Bridge Add at least one sandbox or production API key to enable Bridge for your app. For each environment, select where Bridge can be used: | Setting | Description | | ------------------------------------ | ------------------------------------------------------------------------------------------------------------- | | Enable client-side flows (React SDK) | Allows Bridge flows to be initiated from your app's frontend. | | Enable server-side flows (REST API) | Allows Bridge flows to be initiated from your app's backend, including KYC verification and KYB verification. | The sandbox environment does not move real money and does not require real identity or bank information. It is not a testnet: wallets and assets used in sandbox flows are still mainnet. See Bridge's guide to [setting up a sandbox environment](https://apidocs.bridge.xyz/get-started/introduction/quick-start/setting-up-sandbox) for what Bridge simulates, including [simulated KYC approvals](https://apidocs.bridge.xyz/api-reference/sandbox/simulate-kyc-approval-sandbox-only). ## Environments Every KYC and KYB request specifies which Bridge environment to use through the `environment` field, which defaults to `production`. A request only succeeds if an API key is registered for that environment. ## Next steps Verify an individual user Verify an organization # Approve actions Source: https://docs.privy.io/organizations/actions/approvals Once an organization has created intent(s), users in the organization may then approve the intent if assigned to the appropriate key quorum(s). To have a user approve an intent, follow the steps below: Authenticate the user with your own authentication service or Privy's login methods. Once the user is authenticated, fetch the user's access token (JWT). Next, use the user's access token to [fetch a signing key](/controls/authorization-keys/keys/create/user/request) for the user to sign the intent. Then, [sign the intent payload](/controls/authorization-keys/using-owners/sign/direct-implementation) with this key. We **strongly** recommend using Privy's [client-side SDKs](/controls/authorization-keys/using-owners/sign/signing-on-the-client) or [server-side SDKs](/controls/authorization-keys/using-owners/sign/signing-on-the-server) for this step. Finally, once the user has signed the intent payload, [make a request to the Privy API](/transaction-management/intents/sign-intents) with the intent ID and the user's authorization signature to append the signature to the intent. Once a sufficient threshold of signatures has been collected, Privy will automatically execute the intent. # Consume executed actions Source: https://docs.privy.io/organizations/actions/intent-execution Once a sufficient threshold of users have approved an intent, Privy will automatically execute the intent. Organizations can then retrieve the result of the executed intent (e.g. a signature or a transaction hash) or a failure reason if there was an error. Organizations may either fetch intents by ID or subscribe to webhooks to consume the result of intent execution. ## Fetch intent by ID Organizations can fetch intents by ID using Privy's [`GET /v1/intents/:id`](/api-reference/intents/get) endpoint. The returned intent will include its status, creator, the users that approved the intent, the execution result and more. ## Intent webhooks Your application may also subscribe to intent webhooks to consume the result of intent execution. Your application can forward these webhooks as notifications to the organizations you serve, to notify them of newly created intents, new approvals, and intent execution. Follow [this guide](/api-reference/webhooks/overview) to subscribe to Privy's webhooks and enable webhooks for the following any of the following events. | Event | Description | | ------------------- | -------------------------------------------------------------------------------------------------------------------- | | `intent.created` | A new intent was created. Can be used to notify organizations and users of a new pending intent. | | `intent.authorized` | An intent has received sufficient authorization signatures to meet the threshold, but has not yet been executed yet. | | `intent.executed` | An intent has been executed successfully after receiving sufficient authorization signatures. | | `intent.failed` | An intent encountered an error during execution after receiving sufficient authorization signatures. | | `intent.rejected` | A user of the organization rejected the intent before it received sufficient authorization signatures. | # Propose actions via intents Source: https://docs.privy.io/organizations/actions/intents To take an action with an organization wallet, the first step is to create an intent for the action. This can be initiated from your application's backend with just an app secret, and is authorized for execution later. Once an organization has created an intent, **make sure to save the `id` for the intent** to be used in authorization and execution flows later. View the references below for creating intents for a number of common flows. ## Wallet configuration Update the policy assigned to a wallet as well as access control settings across key quorums. Update the policy rules applied to a wallet. ## Wallet operations Sign an arbitrary message or transaction with the wallet. Sign and broadcast a transaction with the wallet, with native support for gas sponsorship. Execute a transfer with the wallet, with native support for bridging and stablecoin conversions. # Get pending actions Source: https://docs.privy.io/organizations/actions/list-intents Once an organization has created intent(s), your application may want to fetch a list of intents to surface them to their respective organizations with a preview of the action, its approval status, and more. To facilitate such flows, use Privy's [list intents API](/api-reference/intents/list) to fetch a complete list of intents for your application. In particular, you can inspect the intent's: * ID: a unique identifier for the intent * Creator: the user ID and display name of the user that created the intent * Authorization details: the required threshold and the users that have submitted approvals thus feature * Status: the status of the intent (e.g. `'pending'`, `'failed'`, `'executed'`) * Request: the original request body for the intent In particular, you can filter this list for intents with `status: 'pending'` to identify intents that should actively be surfaced to organizations for approval within your application. # Taking actions with organization wallets Source: https://docs.privy.io/organizations/actions/overview Once your organizations have been set up with wallets, you can facilitate users taking actions with those wallets using **intents**. ## What are intents? **Intents** are an abstraction in the Privy system for **asynchronous authorization.** With intents, your application expresses an intent to take an action (e.g. update a policy, send a transaction), and later appends [authorization signatures](/controls/overview) to the intent as users of your application approve the intent. Once the approval threshold for the intent is met, the intent is automatically executed. [Learn more about intents](/transaction-management/intents/overview) to familiarize yourself with how organizations can asynchronously approve actions. ## Using intents At a high-level, you can use intents to take actions with organization wallets as follows: To start, programmatically create an intent for the action the organization intends to take with the wallet. This may be a wallet update (e.g. updating a policy), a signature, or a transaction. Next, authenticate users of the organization who have been provisioned access to the wallet (via the default key quorum or other key quorums). If using your own authentication service, simply [retrieve a valid JWT](/authentication/user-authentication/jwt-based-auth/usage) for the authenticated user. If using Privy's authentication service, simply [log in the user](/authentication/user-authentication/privy-auth) with your configured login methods using Privy's client-side SDKs. Once the user is authenticated, have them authorize the intent by fetching a user signing key with their access token and signing the intent request with it. We strongly suggest using Privy's [client-side SDKs](/controls/authorization-keys/using-owners/sign/signing-on-the-client) or [server-side SDKs](/controls/authorization-keys/using-owners/sign/signing-on-the-server) to facilitate intent authorization with a user's access token. Once a sufficient threshold of users approves the intent (e.g. the threshold of the default key quorum), Privy will automatically execute the intent. You can consume the result of intent execution via Privy's [REST API](/api-reference/intents/get) or by subscribing to [intent webhooks](/api-reference/webhooks/intents/executed). # Provision access Source: https://docs.privy.io/organizations/actions/provision-access At a high-level, there are two ways to provision access for an organization wallet to a new user. ### Option 1: Add the new user to the default key quorum In this approach, your app updates the organization's default key quorum to include the new user. This effectively makes the new user an "administrator" of the organization's wallets, and gives them the ability to approve or partially approve sensitive actions such as wallet configuration updates and policies. Generally, this approach grants "administrator privilege" to the new user. Updating the default key quorum requires a sufficient threshold of signatures from the existing default key quorum. [View the reference](/transaction-management/intents/create/update-key-quorum) for updating the key quorum. ### Option 2: Add the new user as an additional\_signer In this approach, your app should: 1. Create a [new key quorum](/controls/key-quorum/create) containing the new user. Alternatively, if your app has existing lower-privilege key quorums for other users, you may simply update those quorum(s) to include the new user. 2. Create a [policy](/controls/policies/overview) defining the scope of signatures and transactions the user should be able to take. 3. Add this key quorum as an `additional_signer` to the wallet with the policy from step 2. This grants the new user the permission to transact from the wallet within the scope of the policy, but does not give them administration privileges such as wallet configuration updates. View the full [guide](/organizations/setup/signers) for configuring conditional policies for different users. # Organization wallets Source: https://docs.privy.io/organizations/overview Privy powers programmable wallets for organizations, allowing multiple people to securely hold funds, manage access, and operate financial workflows together. Organizations can represent businesses, institutions, nonprofits, agencies, or any other group that shares ownership of funds and financial operations. They can define granular roles, permissions, and approval workflows to govern how funds move. Organization wallets support both custodial and non-custodial deployments. In non-custodial setups, neither Privy nor your application ever holds complete control of the wallet's private key. images/org-overview.png Read below to learn how developers use organization wallets to build payments platforms, business banking products, payroll systems, spend management tools, and more. ## The organization object At the core of Privy's organization wallet system is the [organization object](/organizations/setup/organizations), which provides a consistent representation of a business and its default wallet administration configuration. Each organization object contains: * A unique organization ID * A display name * A default key quorum that owns and administers new wallets for the organization ## Controls Privy's wallet abstraction allows you to assign granular permissions over organization wallets to specific individuals within the organization. An organization's [**default key quorum**](/organizations/setup/organizations) owns and administers its wallets by default. The default key quorum can then [provision scoped wallet access](/organizations/setup/signers) to other members of the organization. Privy's ownership semantics also support arbitrary quorum-based approvals, enabling you to require any permutation and threshold of users in order to take sensitive actions. ## Asynchronous authorization With organizations, sensitive actions taken by wallets often require a quorum of approvals from users that may not all be online simultaneously. Privy's [intents](/organizations/actions/overview) system support asynchronous authorization of wallet actions, streamlining the process of collecting approvals across a quorum of users. Simply create an intent to take an action (e.g. send a transaction) and have users approve asynchronously at their leisure. ## Policies Privy's [policy engine](/controls/policies/overview) supports enforcing granular rules on signing and transaction execution across chains with [Tier 2 or Tier 3 support](/wallets/overview/chains). Organizations can configure guardrails including allowlisted and denylisted addresses, amount thresholds, allowed chains, and more. You can also assign specific policies to specific users or groups of users to [formalize access control within Privy's policy engine](/organizations/setup/signers). ## Funding Privy has native support for [fiat and crypto deposits and payouts](/financial-flows/payments). Organizations can fund wallets via bank deposits, cross-chain crypto deposits, card payments, and more. Organizations can execute payouts to fiat (across a variety of currencies) and crypto, with native support for swapping and bridging at fixed rates. # Example wallet configuration Source: https://docs.privy.io/organizations/setup/example-wallet As an example, the wallet for an organization might be configured like the following object: ```jsonc theme={"system"} { "id": "id2tptkqrxd39qo9j423etij", "address": "0xF1DBff66C993EE895C8cb176c30b07A559d76496", "chain_type": "ethereum", // Organization Details "display_name": "Acme Corporation Treasury", "external_id": "acme-co-id", // Ownership configuration for the wallet // The owner ID corresponds to the default key quorum for the organization, applied // from the organization's default_key_quorum_id at creation. // Only this key quorum can update this wallet object. "owner_id": "rkiz0ivz254drv1xw982v3jq", // Assignment for the wallet. // The entity records which organization this wallet is for. It is set once, and // is what determined the owner above. "entity": { "id": "cm7zx4k9a0000l308abcd1234", "type": "organization" }, "policy_ids": [], // Access control (signer) configuration for the wallet. // Each entry in this array represents a group of users (a key quorum) in the // organization that has been given scoped access to the wallet. "additional_signers": [ { "signer_id": "skiz0ivz254drv1xw982v3jq", // This array defines the scope of actions that this group of users is allowed to take. "override_policy_ids": ["q7m2v9kx4c8dr1zw6n0b5t3y"] } ], "created_at": 1741834854578, "exported_at": null, "imported_at": null, "archived_at": null } ``` # Create an organization Source: https://docs.privy.io/organizations/setup/organizations Creating an organization takes two steps. First, designate a **default key quorum** to administer the organization's wallets. Then, create the **organization** object itself. ## Create default key quorum The default key quorum should be a highly-privileged, locked-down set of individuals. By default, it administers every wallet for the organization, including: * Executing signatures and transactions * Adding, removing, and modifying policies * Provisioning scoped access to other organization members * Managing private key export Privy strongly recommends keeping the default key quorum narrow and highly trusted. To create it, [create a key quorum](/api-reference/key-quorums/create) with a `user_ids` array of the desired members and an `authorization_threshold` for required approvals. Keep the threshold above 1 to avoid a unilateral point of control over organization wallets. ### Creating quorums for other roles Your app may also create additional key quorums for the organization with fewer privileges than the default key quorum. For example, these key quorums wouldn't be able to update a wallet's configuration or manage private key export, but could execute signatures and transactions within their defined scope. ## Create organization With the default key quorum in place, create the **organization** itself. An organization is a first-class object that groups wallets under a single business. It tracks every wallet assigned to it and records which key quorum administers it. [Create an organization](/api-reference/organizations/create) with a `display_name` and the `default_key_quorum_id` from above: ```json theme={"system"} { "display_name": "Acme Corporation", "default_key_quorum_id": "" } ``` # Set up organization wallets Source: https://docs.privy.io/organizations/setup/overview This documentation assumes your business serves other businesses or organizations as your end customers. For clarity, we use the terminology: * **organization** to refer to the business/organization that is your end customer * **user** or **member** to refer to an individual within an organization At a high-level, setting up wallets for an organization requires the following steps. For each user that will need to take action with an organization's wallet(s), [create a Privy user](/organizations/setup/users) to represent that member. This user can then be assigned specific roles and permissions for the organization's wallet(s). Next, create a [default key quorum of users](/organizations/setup/organizations#create-default-key-quorum) in the organization representing a **strict, highly-privileged** group of users that will by default administer the organization's wallets (including provisioning access, policy updates, and more). You can always modify this later on a per wallet basis. The default key quorum should be an extremely locked-down set of users. The default key quorum may provision lower-privilege access to other users as well. Then, [create an organization](/organizations/setup/organizations#create-organization) to represent the business in the Privy API, setting its `default_key_quorum_id` to the default key quorum created above. Finally, [create wallet(s)](/organizations/setup/wallet) for the organization, setting each wallet's `entity` to the organization. Privy assigns the organization's `default_key_quorum_id` as the wallet's owner. This ensures that the default key quorum is exclusively responsible for managing the wallet's configuration and access. These steps should be completed for **each organization** (each of your end customers). For an end-to-end implementation, follow the [organization wallets recipe](/recipes/wallets/organization-wallets). # Assigning conditional policies Source: https://docs.privy.io/organizations/setup/signers A key feature for organization wallets is the ability to configure **conditional policies**, where different users (or quorums of users) have different restrictions for the signatures and transactions they can execute with a wallet. Conditional policies enable you to set up granular access control with organization wallets. Highly-sensitive or high value operations can be restricted to privileged individuals (the default key quorum) while day-to-day wallet operations can be provisioned to those with less permissions. Follow the steps below to configure organization wallets with conditional policies. In addition to the default key quorum, create additional key quorums for members of the organization with lower privileges. In general, each key quorum should correspond to a specific set of permissions (e.g. the ability to transact \< 1000 USDC on Tempo). For each key quorum in the organization (including the default key quorum), define a policy for what actions that quorum should be able to take with the wallet (e.g. allowlisted addresses, transfer limits). Finally, when creating or updating the wallet, set the additional key quorums from step (1) as `additional_signers` on the wallet, with their `override_policy_ids` set to the ID for their corresponding policy. This provisions access to the wallet to that quorum of users within the scope of the associated policy. View [this guide](/recipes/wallets/conditional-signer-policies) for a more in-depth walkthrough of conditional policies. # Create users Source: https://docs.privy.io/organizations/setup/users For each organization, [create Privy users](/api-reference/users/create) to represent the individuals within that organization. The user IDs for these users can then be assigned specific roles and permissions over the organization's wallet(s). Privy's infrastructure ensures that only authenticated users are able to take access from wallets within their defined scope. ## Using your own authentication system Privy seamlessly integrates with any OIDC-compatible, JWT-based authentication system (including Auth0, Firebase, AWS Cognito, etc.). To allow users to access organization wallets via your own authentication system, first [configure your authentication provider's settings](/authentication/user-authentication/jwt-based-auth/setup) in the Privy Dashboard. Next, for each user, set the user's `linked_accounts` to an array with a single entry containing: * `type: 'custom_auth'` * `custom_user_id: string`, which should be the unique ID for the user in your authentication system ## Using Privy's authentication system Alternatively, if your application does not have an existing authentication system or you would prefer to use Privy's authentication service, simply create users with a `linked_accounts` array containing all of the accounts (e.g. email, SMS, socials) that they should be able to authenticate with. View the [API reference for creating users](/api-reference/users/create) to inspect the full types of linked accounts. # Create organization wallet Source: https://docs.privy.io/organizations/setup/wallet Once you've created the [organization](/organizations/setup/organizations) and its [default key quorum](/organizations/setup/organizations#create-default-key-quorum), create the organization's wallet(s). View the [API reference](/api-reference/wallets/create) to inspect the types for wallet creation directly. ## Create a wallet When [creating a wallet](/api-reference/wallets/create), set its `entity` to the [organization](/organizations/setup/organizations): ```json theme={"system"} { "entity": { "id": "", "type": "organization" } } ``` A wallet's `entity` is set once and never changes. To assign a wallet to a different organization, create a new wallet and transfer the funds. You can assign up to 150 wallets to an organization. ## Overriding the default owner When `entity` identifies an organization, the wallet owner is automatically set to the organization's `default_key_quorum_id`. To override the organization's default for a specific wallet, you may pass `owner_id` or `owner` when creating the wallet. Updating an organization's `default_key_quorum_id` only affects wallets created thereafter. Existing wallets keep the owner they were created with, and changing that owner follows the enclave's [ownership update semantics](/controls/authorization-keys/owners/overview). ## Fetch an organization's wallets [Filter wallets](/api-reference/wallets/get-all) by the organization's ID: ```http theme={"system"} GET /v1/wallets?entity_id= ``` ## Assign an existing wallet If the wallet already exists and is not already assigned to an entity, you can [assign it to the organization](/api-reference/wallets/entity). Assigning an entity after creation does not change the wallet's owner. The default key quorum is only applied at creation time. ## Configuring policies In addition to the wallet's owner, you may also [create policies](/controls/policies/overview) for the organization and assign them to the wallet via the `policy_ids` field of the wallet creation request. This enables you to configure guardrails around the signatures and transactions that can be executed by an organization wallets. After a wallet is created, organizations may update the policy for their wallet, but it will require approval from the default key quorum for the organization (the wallet owner). ## Configuring access control \[optional] To provision access to organization members beyond the default key quorum, your app may [create additional key quorums](/organizations/setup/organizations#creating-quorums-for-other-roles) to model other users within the organization. You might consider creating a key quorum for each distinct permission you'd like to assign (e.g. permission to send \< 1000 USDC), and you can add the users that should have that permission to the corresponding key quorum. Then, when creating a wallet, the organization may specify these lower-privilege key quorums as `additional_signers`, with scoped permissions defined by their `override_policy_ids`. These key quorums have the ability to authorize signatures and transactions within the scope of their policy, but not to manage the wallet's configuration. # Storing smart account addresses Source: https://docs.privy.io/recipes/account-abstraction/address Privy now allows you to natively use smart wallet for a better developer experience. Check out the docs [here](/wallets/using-wallets/evm-smart-wallets/overview). Once you've used Privy's embedded wallet as a **signer** to create smart accounts, you can also store the user's smart account address on their user object. At a high-level, this is accomplished by requesting a Sign-In With Ethereum (SIWE) signature from the user's smart account, and passing the resulting signature to Privy to verify that the smart account is associated with the authenticated user. Read below to learn more! Storing a user's smart account address on the user object makes the address available in their [ `user` state in your client](/user-management/users/the-user-object) and the user object you might [query from your server](/user-management/users/managing-users/querying-users). This enables you to easily associate smart account addresses with users. ## 1. Generate a SIWE message for the smart account To start, import the `useLinkWithSiwe` hook from `@privy-io/react-auth`. This hook allows you to generate a SIWE message for an arbitrary wallet and pass the resulting signature for verification. ```tsx theme={"system"} import {useLinkWithSiwe} from '@privy-io/react-auth'; ``` Then, call the `generateSiweMessage` method returned by the hook to generate a SIWE message for the smart account to sign. As parameters to this method, pass an object with the following fields: | Parameter | Type | Description | | --------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `address` | `string` | Required. The user's smart account address. Must be properly checksummed per [EIP-55](https://eips.ethereum.org/EIPS/eip-55). | | `chainId` | `number` | Required. The chain ID of the smart account. Must be a [CAIP-2 formatted](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md) chain ID that correspond to a valid network where signatures from the smart account can be verified. | Make sure the `chainId` you pass to `generateSiweMessage` and `linkWithSiwe` is CAIP-2 formatted. As an example, you might generate a SIWE message for the smart account like so: ```tsx theme={"system"} const {generateSiweMessage} = useLinkWithSiwe(); const message = await generateSiweMessage({ address: 'insert-smart-account-address', chainId: 'eip155:8453' // Replace with a CAIP-2 chain ID where signatures from the smart account can be verified }); ``` ## 2. Request a `personal_sign` signature from the smart account Next, with the `message` you generated in step (1), request a EIP191 `personal_sign` signature from the smart account. The interface for requesting this signature may depend on which smart account provider ([ZeroDev](/recipes/account-abstraction/custom-implementation), [Pimlico](/recipes/account-abstraction/custom-implementation), [Safe](/recipes/account-abstraction/custom-implementation), [Biconomy](/recipes/account-abstraction/custom-implementation), [Alchemy](/recipes/account-abstraction/custom-implementation) your app uses; see the corresponding guides to understand the best way to request a signature from the wallet. ```tsx theme={"system"} // In this example, the `kernelClient` corresponds to a ZeroDev smart account. The interface for requesting // may depend on your chosen smart account provider, so be sure to swap out this implementation for the // correct one for your setup. const signature = await kernelClient.signMessage({ // This `message` is what you generated in step (1) message: message }); ``` ## 3. Pass the signature to Privy Lastly, pass the `signature` from the smart account to Privy using the `linkWithSiwe` method returned by the `useLinkWithSiwe` hook. As parameters to this method, include an object with the following fields: | Parameter | Type | Description | | ------------------ | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `message` | `string` | Required. The SIWE message you generated with `generateSiweMessage`. | | `chainId` | `number` | Required. The [CAIP-2](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md) chain ID you passed to `generateSiweMessage`. | | `signature` | `string` | Required. The signature produced by the smart account. | | `walletClientType` | `string` | Recommended. A signature indicating the wallet client you'd like to associate with the smart account. We recommend using `'privy_smart_account'`. | | `connectorType` | `string` | Recommended. A signature indicating the connector type you'd like to associate with the smart account. We recommend using the snake\_cased name of your smart account provider, e.g. `'zerodev'`. | As an example, you can pass the smart account's signature to Privy for verification like so: ```tsx theme={"system"} const {linkWithSiwe} = useLinkWithSiwe(); await linkWithSiwe({ // The SIWE message generated from `generateSiweMessage` message: message, // The same `chainId` you passed to `generateSiweMessage` chainId: 'eip155:8453', // The signature from the smart account signature: signature, // You can replace this with whatever wallet client you'd like to associate with the smart account walletClientType: 'privy_smart_account', // You can replace this with whatever connector type you'd like to associate with the smart account connectorType: 'zerodev' }); ``` ## 4. Get the smart account address Once you've successfully linked the smart account to the user, you can easily get their smart account address from their Privy `user` object. Simply inspect the `linkedAccounts` array for the entry with: * `type: 'wallet'` * `walletClientType: 'privy_smart_account'`, or any other custom `walletClientType` you chose As an example, you can get the smart account address like so! ```tsx theme={"system"} const {user} = usePrivy(); const address = user.linkedAccounts.find( (account): account is WalletWithMetadata => account.type === 'wallet' && account.walletClientType === 'privy_smart_account' ); ``` **That's it!** You can also find the user's smart account when [querying Privy's API from your server](/user-management/users/managing-users/querying-users), and applying the same logic to parsing the `linked_accounts` array. # Custom account abstraction implementation Source: https://docs.privy.io/recipes/account-abstraction/custom-implementation Privy now allows you to natively use smart wallet for a better developer experience. Check out the docs [here](/wallets/using-wallets/evm-smart-wallets/overview). ## Account abstraction with ZeroDev [ZeroDev](https://zerodev.app/) is a toolkit for creating [ERC-4337](https://eips.ethereum.org/EIPS/eip-4337)-compatible smart wallets for your users, using the user's EOA as the smart wallet's signer. This allows you to easily add [Account Abstraction](https://ethereum.org/en/roadmap/account-abstraction/) features into your app. **You can easily integrate ZeroDev alongside Privy to create smart wallets from your user's embedded or external wallets, allowing you to enhance your app with gas sponsorship, batched transactions, and more!** Read below to learn how to configure your app to create smart wallets for *all* your users!
What is an EOA? An [**EOA, or externally-owned account**](https://ethereum.org/en/developers/docs/accounts/), is any Ethereum account that is controlled by a private key. Privy's embedded wallets and most external wallets (MetaMask, Coinbase Wallet, Rainbow Wallet, etc.) are EOAs. EOAs differ from **contract accounts**, which are instead controlled by smart contract code and do not have their own private key. ZeroDev's smart wallet is a contract account. Contract accounts have [enhanced capabilities, such as gas sponsorship and batched transactions](https://ethereum.org/en/roadmap/account-abstraction/). Since they do not have their own private key, contract accounts cannot *directly* produce signatures and initiate transaction flows. Instead, each contract account is generally "managed" by an EOA, which authorizes actions taken by the contract account via a signature; this EOA is called a **signer**. In this integration, the user's EOA (from Privy) serves as the signer for their smart wallet (from ZeroDev). The smart wallet (ZeroDev) holds all assets and submits all transactions to the network, but the signer (Privy) is responsible for producing signatures and "kicking off" transaction flows.
How much does deploying a smart wallet for a user cost? The transaction to deploy a ZeroDev smart wallet requires approximately 258522 in gas. At time of writing, this corresponds to: * 0.0168 ETH (28 USD) on **Ethereum Mainnet** * 0.04 POL (0.024 USD) on **Polygon** * 0.00005 ETH (0.08 USD) on **Arbitrum** The exact deployment cost you see will vary depending on the current gas price. **Importantly, ZeroDev deploys smart wallets lazily, ensuring that you do not pay deployment costs for functionally unused wallets.** When you first initialize a ZeroDev smart wallet for a user, ZeroDev does not yet deploy the wallet, but instead *predicts* the smart wallet's address (via the [`CREATE2`](https://docs.openzeppelin.com/cli/2.8/deploying-with-create2) opcode). This allows you to associate a smart wallet with your user, without any upfront deployment costs. ZeroDev only *deploys* the smart wallet to the predicted address when the user sends their first transaction with the smart wallet. This ensures that you only ever pay deployment costs for wallets that are actually used to transact on-chain.
### 1. Install the required dependencies from Privy and ZeroDev In your app's repository, install the required dependencies from Privy and ZeroDev, as well as the [`permissionless`](https://www.npmjs.com/package/permissionless), and [`viem`](https://www.npmjs.com/package/viem) libraries: ```sh theme={"system"} npm i @privy-io/react-auth @zerodev/sdk @zerodev/ecdsa-validator permissionless viem ``` ### 2. Sign up for a ZeroDev account and get your project ID Visit the [**ZeroDev dashboard**](https://dashboard.zerodev.app/) and sign up for a new account if you do not have one already. Set up a new project for your required chain(s) and retrieve your ZeroDev **project ID**, as well as your **paymaster and bundler URLs** for the project. Within this Dashboard, you can also configure [settings for gas sponsorship and other ZeroDev features](https://docs.zerodev.app/sdk/getting-started/tutorial)! ### 2. Configure your app's Privy settings First, follow the instructions in the [**Privy Quickstart**](/basics/react/quickstart) to get your app set up with a basic Privy integration. Next, set **Add confirmation modals** to "off" in your app's \[**Embedded wallets**] page in the Privy [**dashboard**](https://dashboard.privy.io). This will configure Privy to *not* show its default UIs when your user must sign messages or send transactions. Instead, we recommend you use your own custom UIs for showing users the [user operations](https://www.alchemy.com/overviews/user-operations)s they sign. Lastly, update the **`config.embeddedWallets.createOnLogin`** property of your **`PrivyProvider`** to `'users-without-wallets'`.This will configure Privy to create an embedded wallet for users logging in via a web2 method (email, phone, socials), ensuring that *all* of your users have a wallet that can be used as an EOA. Your **`PrivyProvider`** should then look like: ```tsx theme={"system"} {/* Your app's components */} ``` ### 3. Create a smart account for your user You'll now create a smart account for your user, using the Privy embedded wallet (an EOA) as the signer. To do so, when the user logs in, first find the user's embedded wallet from Privy's **`useWallets`** hook, and get its [EIP1193 provider](/wallets/using-wallets/ethereum/web3-integrations). You can find embedded wallet by finding the only entry in the **`useWallets`** array with a **`walletClientType`** of `'privy'`. ```tsx theme={"system"} import {useWallets} from '@privy-io/react-auth'; import {sepolia} from 'viem/chains'; // Replace this with the chain used by your application import {createWalletClient, custom} from 'viem'; ... // Find the embedded wallet and get its EIP1193 provider const {wallets} = useWallets(); const embeddedWallet = wallets.find((wallet) => (wallet.walletClientType === 'privy')); const provider = await embeddedWallet.getEthereumProvider(); ``` Next, pass the returned EIP1193 `provider` to the [`toSimpleSmartAccount`](https://docs.pimlico.io/references/permissionless/reference/accounts/toSimpleSmartAccount#tosimplesmartaccount) method from `permissionless` to create a SmartAccount. This signer corresponds to the user's embedded wallet and authorizes actions for the user's smart account. ```ts {skip-check} theme={"system"} import {createSmartAccountClient} from 'permissionless'; import {toSimpleSmartAccount} from 'permissionless/accounts'; import {createPublicClient, http, zeroAddress} from 'viem'; import {sepolia} from 'viem/chains'; import {createPimlicoClient} from 'permissionless/clients/pimlico'; import {entryPoint07Address} from 'viem/account-abstraction'; const publicClient = createPublicClient({ chain: sepolia, // or whatever chain you are using transport: http() }); const pimlicoUrl = `https://api.pimlico.io/v2/sepolia/rpc?apikey=`; const pimlicoClient = createPimlicoClient({ transport: http(pimlicoUrl), entryPoint: { address: entryPoint07Address, version: '0.7' } }); // Use the EIP1193 `provider` from Privy to create a `SmartAccount` const kernelSmartAccount = await toKernelSmartAccount({ owners: [provider], client: publicClient, entryPoint: { address: entryPoint07Address, version: '0.7' } }); ``` Finally, using the `SmartAccount` from above, initialize a smart account client for the user like so: ```tsx theme={"system"} import {sepolia} from 'viem/chains'; // Replace this with the chain used by your application import {createPublicClient, http} from 'viem'; import {ENTRYPOINT_ADDRESS_V07} from 'permissionless'; import {createZeroDevPaymasterClient, createKernelAccount, createKernelAccountClient} from "@zerodev/sdk"; import {signerToEcdsaValidator} from "@zerodev/ecdsa-validator"; ... // Initialize a viem public client on your app's desired network const publicClient = createPublicClient({ transport: http(sepolia.rpcUrls.default.http[0]), }) // Create a ZeroDev ECDSA validator from the `smartAccountSigner` from above and your `publicClient` const ecdsaValidator = await signerToEcdsaValidator(publicClient, { signer: kernelSmartAccount, entryPoint: ENTRYPOINT_ADDRESS_V07, }) // Create a Kernel account from the ECDSA validator const account = await createKernelAccount(publicClient, { plugins: { sudo: ecdsaValidator, }, entryPoint: ENTRYPOINT_ADDRESS_V07, }); // Create a Kernel account client to send user operations from the smart account const kernelClient = createKernelAccountClient({ account, chain: sepolia, entryPoint: ENTRYPOINT_ADDRESS_V07, bundlerTransport: http('insert-your-bundler-RPC-from-the-dashboard'), middleware: { sponsorUserOperation: async ({ userOperation }) => { const zerodevPaymaster = createZeroDevPaymasterClient({ chain: sepolia, entryPoint: ENTRYPOINT_ADDRESS_V07, transport: http('insert-your-paymaster-RPC-to-the-dashboard'), }) return zerodevPaymaster.sponsorUserOperation({ userOperation, entryPoint: ENTRYPOINT_ADDRESS_V07, }) } } }) ``` The `kernelClient` is a drop-in replacement for a `viem` [Wallet Client](https://viem.sh/docs/clients/wallet.html), and requests to the smart account can be made using [`viem`'s API](https://docs.zerodev.app/sdk/core-api/send-transactions). You can also store the user's smart account address on Privy's user object. See [this guide](./address.md) for more.
Want to see this code end-to-end? You can find the code snippets above pasted in an end-to-end example below. ```tsx theme={"system"} /** * This example assumes your app is wrapped with the `PrivyProvider` and * is configured to create embedded wallets for users upon login. Aside from * the imports, all of the code in this snippet must be used within a React component * or context. */ import {createSmartAccountClient} from 'permissionless'; import {toSimpleSmartAccount} from 'permissionless/accounts'; import {createPublicClient, http, zeroAddress} from 'viem'; import {sepolia} from 'viem/chains'; import {createPimlicoClient} from 'permissionless/clients/pimlico'; import {entryPoint07Address} from 'viem/account-abstraction'; import {useWallets} from '@privy-io/react-auth'; import { createZeroDevPaymasterClient, createKernelAccount, createKernelAccountClient } from '@zerodev/sdk'; import {signerToEcdsaValidator} from '@zerodev/ecdsa-validator'; const {wallets} = useWallets(); const embeddedWallet = wallets.find((wallet) => wallet.walletClientType === 'privy'); const provider = await embeddedWallet.getEthereumProvider(); // Initialize a viem public client on your app's desired network const publicClient = createPublicClient({ chain: sepolia, // or whatever chain you are using transport: http() }); const pimlicoUrl = `https://api.pimlico.io/v2/sepolia/rpc?apikey=`; const pimlicoClient = createPimlicoClient({ transport: http(pimlicoUrl), entryPoint: { address: entryPoint07Address, version: '0.7' } }); // Use the EIP1193 `provider` from Privy to create a `SmartAccount` const kernelSmartAccount = await toKernelSmartAccount({ owners: [provider], client: publicClient, entryPoint: { address: entryPoint07Address, version: '0.7' } }); // Create a ZeroDev ECDSA validator from the `smartAccountSigner` from above and your `publicClient` const ecdsaValidator = await signerToEcdsaValidator(publicClient, { signer: smartAccountSigner, entryPoint: entryPoint07Address }); // Create a Kernel account from the ECDSA validator const account = await createKernelAccount(publicClient, { plugins: { sudo: ecdsaValidator }, entryPoint: entryPoint07Address }); // Create a Kernel client to send user operations from the smart account const kernelClient = createKernelAccountClient({ account, chain: sepolia, entryPoint: entryPoint07Address, bundlerTransport: http('insert-your-bundler-RPC-from-the-dashboard'), middleware: { // See https://docs.zerodev.app/sdk/core-api/sponsor-gas sponsorUserOperation: async ({userOperation}) => { const zerodevPaymaster = createZeroDevPaymasterClient({ chain: sepolia, entryPoint: entryPoint07Address, transport: http('insert-your-paymaster-RPC-from-the-dashboard') }); return zerodevPaymaster.sponsorUserOperation({ userOperation, entryPoint: entryPoint07Address }); } } }); ``` Note: if your app uses React, we suggest that you store the user's `kernelClient` in a [React context](https://react.dev/learn/passing-data-deeply-with-context) that wraps your application. This allows you to easily access the smart account from your app's pages and components.
### 4. Send user operations (transactions) from the smart account Now that your users have Kernel (ZeroDev) smart accounts, they can now send [**UserOperations**](https://eips.ethereum.org/EIPS/eip-4337) from their smart account. This is the AA analog to sending a transaction. **To send a user operation from a user's smart account, use the Kernel client's [`sendTransaction`](https://docs.zerodev.app/sdk/core-api/send-transactions#sending-transactions-1) method.** ```tsx theme={"system"} const txHash = await kernelClient.sendTransaction({ to: 'TO_ADDRESS', value: VALUE, // default to 0 data: '0xDATA' // default to 0x }); ``` This is a drop-in replacement for viem's [`sendTransaction`](https://viem.sh/docs/actions/wallet/sendTransaction.html) method, and will automatically apply any smart account configurations (e.g. gas sponsorship) you configure in the `middleware` before sending the transaction. **That's it! You've configured your app to create smart wallets for all of your users, and can seamlessly add in AA features like gas sponsorship, batched transactions, and more.** 🎉
## Account Abstraction with Safe [Safe Smart Accounts](https://safe.global/) is a product by [Safe](https://safe.global/wallet) for creating [ERC-4337](https://eips.ethereum.org/EIPS/eip-4337)-compatible smart accounts for your users, using the user's EOA as the smart account's signer. The product builds off of the smart contract infrastructure powering the widely-used [Safe wallet](https://safe.global/wallet) and allows you to easily add [Account Abstraction](https://ethereum.org/en/roadmap/account-abstraction/) and other Safe features into your app.
What is an EOA? An [**EOA, or externally-owned account**](https://ethereum.org/en/developers/docs/accounts/), is any Ethereum account that is controlled by a private key. Privy's embedded wallets and most external wallets (MetaMask, Coinbase Wallet, Rainbow Wallet, etc.) are EOAs. EOAs differ from **contract accounts**, which are instead controlled by smart contract code and do not have their own private key. Safe's smart wallet is a contract account. Contract accounts have [enhanced capabilities, such as gas sponsorship and batched transactions](https://ethereum.org/en/roadmap/account-abstraction/). Since they do not have their own private key, contract accounts cannot *directly* produce signatures and initiate transaction flows. Instead, each contract account is generally "managed" by an EOA, which authorizes actions taken by the contract account via a signature; this EOA is called a **signer**. In this integration, the user's EOA (from Privy) serves as the signer for their smart wallet (from Safe). The smart wallet (Safe) holds all assets and submits all transactions to the network, but the signer (Privy) is responsible for producing signatures and "kicking off" transaction flows.
**To create Safe smart accounts for your users, simply follow our Pimlico integration guide.** Safe does not operate its own paymaster and bundler infrastructure, and developers generally compose the Safe smart account with paymasters or bundlers from Pimlico. **When integrating Safe alongside Pimlico, the only change from the default Pimlico setup is to replace the [`toSimpleSmartAccount`](https://docs.pimlico.io/references/permissionless/reference/accounts/toSimpleSmartAccount#usage) method with [`toSafeSmartAccount`](https://docs.pimlico.io/references/permissionless/reference/accounts/toSafeSmartAccount#usage).** This modifies the setup to deploy a Safe smart account for the user instead of a simple smart account. For example, when initializing the smart account from a `viem` wallet client for the user's Privy embedded wallet, you should update your code as follows: ```tsx theme={"system"} import {createSmartAccountClient} from 'permissionless'; import {toSimpleSmartAccount} from 'permissionless/accounts'; // [!code --] import {toSafeSmartAccount} from 'permissionless/accounts'; // [!code ++] import {createPimlicoClient} from 'permissionless/clients/pimlico'; import {createPublicClient, http} from 'viem'; import {entryPoint07Address} from 'viem/account-abstraction'; // Create a viem public client for RPC calls const publicClient = createPublicClient({ chain: sepolia, // Replace this with the chain of your app transport: http() }); // Initialize the smart account for the user const simpleSmartAccount = await toSimpleSmartAccount({ // [!code --] client: publicClient, // [!code --] owner: privyClient.account, // [!code --] factoryAddress: '0x9406Cc6185a346906296840746125a0E44976454' // [!code --] }); // [!code --] const safeSmartAccount = await toSafeSmartAccount({ // [!code ++] owners: [privyClient.account], // [!code ++] safeVersion: '1.4.1', // [!code ++] entryPoint: { // [!code ++] address: ENTRYPOINT_ADDRESS_V07, // [!code ++] version: '0.7' // [!code ++] } // [!code ++] }); // [!code ++] // Create the Paymaster for gas sponsorship using the API key from your Pimlico dashboard const pimlicoPaymaster = createPimlicoClient({ transport: http('https://api.pimlico.io/v2/sepolia/rpc?apikey=YOUR_PIMLICO_API_KEY') }); // Create the SmartAccountClient for requesting signatures and transactions (RPCs) const smartAccountClient = createSmartAccountClient({ account: simpleSmartAccount, // [!code --] account: safeSmartAccount, // [!code ++] chain: sepolia, // Replace this with the chain for your app bundlerTransport: http('https://api.pimlico.io/v1/sepolia/rpc?apikey=YOUR_PIMLICO_API_KEY'), paymaster: pimlicoPaymaster // If your app uses a paymaster for gas sponsorship }); ``` You can also store the user's smart account address on Privy's user object. See [this guide](/recipes/account-abstraction/address) for more.
## Account Abstraction with permissionless.js and Pimlico [**`permissionless.js`**](https://www.npmjs.com/package/permissionless) is a modular and extensible TypeScript library originally created by [**Pimlico**](https://pimlico.io) for deploying and managing ERC-4337 smart accounts. You can use this library for all major smart account implementations, including [Safe](https://docs.pimlico.io/guides/how-to/accounts/use-safe-account), [Kernel](https://docs.pimlico.io/references/permissionless/how-to/accounts/use-kernel-account), [Biconomy](https://docs.pimlico.io/guides/how-to/accounts/use-nexus-account), [SimpleAccount](https://docs.pimlico.io/guides/how-to/accounts/use-simple-account), and more. **You can easily integrate [`permissionless.js`](https://www.npmjs.com/package/permissionless) alongside Privy to create smart wallets from your user's embedded or external wallets, allowing you to enhance your app with gas sponsorship, batched transactions, and more.** Just follow the steps below! Want to see an end-to-end integration of Privy with `permissionless.js`? Check out [**our example app**](https://github.com/privy-io/examples/tree/main/examples/privy-next-permissionless)!
What is an EOA? An [**EOA, or externally-owned account**](https://ethereum.org/en/developers/docs/accounts/), is any Ethereum account that is controlled by a private key. Privy's embedded wallets and most external wallets (MetaMask, Coinbase Wallet, Rainbow Wallet, etc.) are EOAs. EOAs differ from **contract accounts**, which are instead controlled by smart contract code and do not have their own private key. Smart wallets are contract accounts. Contract accounts have [enhanced capabilities, such as gas sponsorship and batched transactions](https://ethereum.org/en/roadmap/account-abstraction/). Since they do not have their own private key, contract accounts cannot *directly* produce signatures and initiate transaction flows. Instead, each contract account is generally "managed" by an EOA, which authorizes actions taken by the contract account via a signature; this EOA is called a **signer**. In this integration, the user's EOA (from Privy) serves as the signer for their smart wallet (from permissionless). The smart wallet holds all assets and submits all transactions to the network, but the signer (Privy) is responsible for producing signatures and "kicking off" transaction flows.
### 1. Install Privy and `permissionless.js` In your project, install the necessary dependencies from Privy, Pimlico, and [`viem`](https://viem.sh/): ```bash theme={"system"} npm i @privy-io/react-auth permissionless viem ``` ### 2. Sign up for a Pimlico account and create an API key. To send transactions from smart accounts, you will need access to a [**bundler**](https://www.alchemy.com/overviews/what-is-a-bundler). We also recommend using [**paymaster**](https://www.alchemy.com/overviews/what-is-a-paymaster) to sponsor your user's transactions. To get a **bundler** and **paymaster** for your application, [**sign up for a Pimlico account**](https://dashboard.pimlico.io/) and copy down your API key for the rest of this guide! ### 3. Configure your app's `PrivyProvider` First, follow the instructions in the [**Privy Quickstart**](/basics/react/quickstart) to get your app set up with Privy. Next, set **Add confirmation modals** to "off" in your app's **Embedded wallets** page in the Privy [**dashboard**](https://dashboard.privy.io). This will configure Privy to *not* show its default UIs when your user must sign messages or send transactions. Instead, we recommend you use your own custom UIs for showing users the [`UserOperation`](https://www.alchemy.com/overviews/user-operations)s they sign. Then, update the **`config.embeddedWallets.createOnLogin`** property of your **`PrivyProvider`** to `'users-without-wallets'`.This will configure Privy to create an embedded wallet for users logging in via a web2 method (email, phone, socials), ensuring that *all* of your users have a wallet that can be used as an EOA. Your **`PrivyProvider`** should then look like: ```tsx theme={"system"} {/* Your app's components */} ``` ### 4. Create a smart account for your user You'll now create a smart account for your user, using the Privy embedded wallet (an EOA) as the signer. To do so, when the user logs in, **find the user's embedded wallet from Privy's `useWallets` hook, and create a viem [`WalletClient`](https://viem.sh/docs/clients/wallet.html) for it**. You can find embedded wallet by finding the only entry in the **`useWallets`** array with a **`walletClientType`** of `'privy'`. ```tsx theme={"system"} import {useWallets} from '@privy-io/react-auth'; import {sepolia} from 'viem/chains'; // Replace this with the chain used by your application import {createWalletClient, custom} from 'viem'; ... // Find the embedded wallet and get its EIP1193 provider const {wallets} = useWallets(); const embeddedWallet = wallets.find((wallet) => (wallet.walletClientType === 'privy')); const eip1193provider = await embeddedWallet.getEthereumProvider(); // Create a viem WalletClient from the embedded wallet's EIP1193 provider // This will be used as the signer for the user's smart account const privyClient = createWalletClient({ account: embeddedWallet.address, chain: sepolia, // Replace this with the chain used by your application transport: custom(eip1193provider) }); ``` Next, using the **`privyClient`** from above, create a **`SmartAccountClient`** which represents the user's smart account. In creating the smart account, you can also specify which smart account implementation you'd like to use. Possible options include: [Safe](https://docs.pimlico.io/guides/how-to/accounts/use-safe-account), [Kernel](https://docs.pimlico.io/guides/how-to/accounts/use-kernel-account), [Biconomy](https://www.biconomy.io/), and [SimpleAccount](https://docs.pimlico.io/guides/how-to/accounts/use-simple-account) (the original smart account implementation). If your app also uses a **paymaster** to sponsor gas on behalf of users, you can also specify which paymaster to use by calling the **`createPimlicoClient`** method from `permissionless` with the RPC URL in your Pimlico Dashboard. ```ts {skip-check} theme={"system"} import {createSmartAccountClient} from 'permissionless'; import {toSimpleSmartAccount} from 'permissionless/accounts'; import {createPimlicoClient} from 'permissionless/clients/pimlico'; import {createPublicClient, http} from 'viem'; import {sepolia} from 'viem/chains'; import {entryPoint07Address} from 'viem/account-abstraction'; import {useWallets} from '@privy-io/react-auth'; const {wallets} = useWallets(); const embeddedWallet = wallets.find((wallet) => wallet.walletClientType === 'privy'); const provider = await embeddedWallet.getEthereumProvider(); // Create a viem public client for RPC calls const publicClient = createPublicClient({ chain: sepolia, // Replace this with the chain of your app transport: http() }); // Initialize the smart account for the user const simpleSmartAccount = await toSimpleSmartAccount({ client: publicClient, owner: {request: provider.request}, entryPoint: { address: entryPoint07Address, version: '0.7' } }); // Create the Paymaster for gas sponsorship using the API key from your Pimlico dashboard const pimlicoPaymaster = createPimlicoClient({ transport: http('https://api.pimlico.io/v2/sepolia/rpc?apikey=YOUR_PIMLICO_API_KEY') }); // Create the SmartAccountClient for requesting signatures and transactions (RPCs) const smartAccountClient = createSmartAccountClient({ account: simpleSmartAccount, chain: sepolia, // Replace this with the chain for your app bundlerTransport: http('https://api.pimlico.io/v1/sepolia/rpc?apikey=YOUR_PIMLICO_API_KEY'), paymaster: pimlicoPaymaster // If your app uses a paymaster for gas sponsorship }); ``` When using the snippets above, make sure replace `YOUR_PIMLICO_API_KEY` with your Pimlico API key that you created in step 2! You can also store the user's smart account address on Privy's user object. See [this guide](./address.md) for more.
Want to see this code end-to-end? You can find the code snippets above pasted in an end-to-end example below. ```tsx theme={"system"} /** * This example assumes your app is wrapped with the `PrivyProvider` and * is configured to create embedded wallets for users upon login. Aside from * the imports, all of the code in this snippet must be used within a React component * or context. */ import {useWallets} from '@privy-io/react-auth'; import {sepolia} from 'viem/chains'; // Replace this with the chain used by your application import {createWalletClient, createPublicClient, custom, http} from 'viem'; import {entryPoint07Address} from 'viem/account-abstraction'; import {createSmartAccountClient, walletClientToCustomSigner} from "permissionless"; import {createPimlicoClient} from "permissionless/clients/pimlico"; import {toSimpleSmartAccount} from "permissionless/accounts"; ... // Find the embedded wallet and get its EIP1193 provider const {wallets} = useWallets(); const embeddedWallet = wallets.find((wallet) => (wallet.walletClientType === 'privy')); const eip1193provider = await embeddedWallet.getEthereumProvider(); // Create a viem WalletClient from the embedded wallet's EIP1193 provider const privyClient = createWalletClient({ account: embeddedWallet.address, chain: sepolia, // Replace this with the chain used by your application transport: custom(eip1193provider) }); // Create a viem public client for RPC calls const publicClient = createPublicClient({ chain: sepolia, // Replace this with the chain of your app transport: http() }) // Initialize the smart account for the user using the embedded wallet as the signer const customSigner = walletClientToCustomSigner(privyClient); const simpleSmartAccount = await toSimpleSmartAccount({ client: publicClient, owner: eip1193provider, entryPoint: { address: entryPoint07Address, version: "0.7" }, }) // Create the Paymaster for gas sponsorship using the API key from your Pimlico dashboard const pimlicoPaymaster = createPimlicoClient({ transport: http( "https://api.pimlico.io/v2/sepolia/rpc?apikey=YOUR_PIMLICO_API_KEY", ), }) // Create the SmartAccountClient for requesting signatures and transactions const smartAccountClient = createSmartAccountClient({ account: simpleSmartAccount, chain: sepolia, // Replace this with the chain for your app transport: http("https://api.pimlico.io/v1/sepolia/rpc?apikey=YOUR_PIMLICO_API_KEY"), paymaster: pimlicoPaymaster // If your app uses a paymaster for gas sponsorship }) ``` Note: if your app uses React, we suggest that you store the user's `SmartAccountClient` in a [React context](https://react.dev/learn/passing-data-deeply-with-context) that wraps your application. This allows you to easily access the smart account from your app's pages and components.
### 5. Send transactions from the smart account You can now send transactions using the **`sendTransaction`** method on the [**`SmartAccountClient`**](https://docs.pimlico.io/references/permissionless/reference/clients/smartAccountClient) object, like so: ```ts {skip-check} theme={"system"} const txHash = await smartAccountClient.sendTransaction({ account: smartAccountClient.account, to: 'zero-address', data: '0x', value: BigInt(0) }); ``` You can also request signatures, typed data signatures, and more from the smart account! The [**`SmartAccountClient`**](https://docs.pimlico.io/references/permissionless/reference/clients/smartAccountClient) functions as a drop-in replacement for [`viem`'s wallet client](https://viem.sh/docs/clients/wallet#wallet-client) - you can use the same interfaces with the [**`SmartAccountClient`**](https://docs.pimlico.io/references/permissionless/reference/clients/smartAccountClient) object! **That's it! Once you've created smart accounts for your users, you can easily add AA features into your application like gas sponsorship, batched transactions, and more.** 🎉 To learn more about what you can do with smart accounts, check out the [**`permissionless.js` guide**](https://docs.pimlico.io/guides/how-to/signers/privy).
## Account abstraction with Biconomy Biconomy has an updated guide for using the new [Biconomy Nexus](https://www.biconomy.io/post/nexus-modular-smart-account) smart accounts. Please refer to the [Biconomy guide](https://docs.biconomy.io/tutorials/signers/privy) for the most up-to-date information. [Biconomy](https://www.biconomy.io/) is a toolkit for creating [ERC-4337](https://eips.ethereum.org/EIPS/eip-4337)-compatible smart accounts for your users, using the user's EOA as the smart account's signer. This allows you to easily add [Account Abstraction](https://ethereum.org/en/roadmap/account-abstraction/) features into your app. **You can easily integrate Biconomy alongside Privy to create smart wallets from your user's embedded or external wallets, allowing you to enhance your app with [gas sponsorship](https://docs.biconomy.io/dashboard/paymaster) and more!** Read below to learn how to configure your app to create smart wallets for *all* your users! Want to see an end-to-end integration of Privy with Biconomy? Check out **an example [app](https://aaprivy.vercel.app/) and [repo](https://github.com/bcnmy/biconomy_privy_example)**!
What is an EOA? An [**EOA, or externally-owned account**](https://ethereum.org/en/developers/docs/accounts/), is any Ethereum account that is controlled by a private key. Privy's embedded wallets and most external wallets (MetaMask, Coinbase Wallet, Rainbow Wallet, etc.) are EOAs. EOAs differ from **contract accounts**, which are instead controlled by smart contract code and do not have their own private key. Biconomy's smart wallet is a contract account. Contract accounts have [enhanced capabilities, such as gas sponsorship and batched transactions](https://ethereum.org/en/roadmap/account-abstraction/). Since they do not have their own private key, contract accounts cannot *directly* produce signatures and initiate transaction flows. Instead, each contract account is generally "managed" by an EOA, which authorizes actions taken by the contract account via a signature; this EOA is called a **signer**. In this integration, the user's EOA (from Privy) serves as the signer for their smart wallet (from Biconomy). The smart wallet (Biconomy) holds all assets and submits all transactions to the network, but the signer (Privy) is responsible for producing signatures and "kicking off" transaction flows.
### 1. Install Privy and Biconomy In your app's repository, install the [**`@privy-io/react-auth`**](https://www.npmjs.com/package/@privy-io/react-auth) SDK from Privy and the [**`@biconomy/account`**](https://docs.biconomy.io/Account/integration#installation) SDK from Biconomy: ```sh theme={"system"} npm i @privy-io/react-auth @biconomy/account ``` ### 2. Configure your app's `PrivyProvider` First, follow the instructions in the [**Privy Quickstart**](/basics/react/quickstart) to get your app set up with Privy. Next, set **Add confirmation modals** to "off" in your app's **Embedded wallets** page in the Privy [**dashboard**](https://dashboard.privy.io). This will configure Privy to *not* show its default UIs when your user must sign messages or send transactions. Instead, we recommend you use your own custom UIs for showing users the [`UserOperation`](https://www.alchemy.com/overviews/user-operations)s they sign. Then, update the **`config.embeddedWallets.createOnLogin`** property of your **`PrivyProvider`** to `'users-without-wallets'`.This will configure Privy to create an embedded wallet for users logging in via a web2 method (email, phone, socials), ensuring that *all* of your users have a wallet that can be used as an EOA. Your **`PrivyProvider`** should then look like: ```tsx theme={"system"} {/* Your app's components */} ``` ### 3. Configure your Biconomy bundler and paymaster Go to the [**Biconomy Dashboard**](https://dashboard.biconomy.io/) and configure a **Paymaster** and a **Bundler** for your app. Make sure these correspond to the desired network for your user's smart accounts.
Pregenerate user wallets
Configuring your Biconomy Paymaster
Once you've configured a **Paymaster**, you can also deposit funds into your app's gas tank and configure specific policies for [**gas sponsorship**](https://docs.biconomy.io/dashboard/paymaster). Save the bundler URL and paymaster API key for your project, as you will need those values later. ### 4. Initialize your users' smart accounts When users log into your app, Privy provisions each user an embedded wallet, which is an EOA. In order to leverage the features of Biconomy's account abstraction, each user also needs a Biconomy smart account. **You can provision Biconomy smart accounts for each user by assigning their embedded wallet as a signer for their smart account**. To start, after a user logs in, **find the user's embedded wallet from Privy's `useWallets` hook, and switch its network to your app's target network**. You can find embedded wallet by finding the only entry in the **`useWallets`** array with a **`walletClientType`** of `'privy'`. ```tsx theme={"system"} import {useWallets} from '@privy-io/react-auth'; ... // Find the embedded wallet const {wallets} = useWallets(); const embeddedWallet = wallets.find((wallet) => (wallet.walletClientType === 'privy')); // Switch the embedded wallet to your target network // Replace '80001' with your desired chain ID. await embeddedWallet.switchChain(80001); ``` Next, using your paymaster API key and bundler URL from the Biconomy Dashboard, **initialize the user's smart account using Biconomy's [`createSmartAccountClient`](https://docs.biconomy.io/Account/methods#createsmartaccountclient) method**: ```tsx theme={"system"} import { createSmartAccountClient } from "@biconomy/account"; ... // Get an ethers provider and signer for the user's embedded wallet const provider = await embeddedWallet.getEthereumProvider(); const ethersProvider = new ethers.providers.Web3Provider(provider); const ethersSigner = ethersProvider.getSigner() const smartAccount = await createSmartAccountClient({ signer: ethersSigner, bundlerUrl: 'your-bundler-url-from-the-biconomy-dashboard', biconomyPaymasterApiKey: 'your-paymaster-api-key-from-the-biconomy-dashboard' }); ``` You can also store the user's smart account address on Privy's user object. See [this guide](./address.md) for more.
Want to see this code end-to-end? You can find the code snippets above pasted in an end-to-end example below. ```tsx theme={"system"} import {createSmartAccountClient} from '@biconomy/account'; import {useWallets} from '@privy-io/react-auth'; // Find the embedded wallet and switch it to your target network const {wallets} = useWallets(); const embeddedWallet = wallets.find((wallet) => wallet.walletClientType === 'privy'); await embeddedWallet.switchChain(80001); const provider = await embeddedWallet.getEthereumProvider(); const ethersProvider = new ethers.providers.Web3Provider(provider); const ethersSigner = ethersProvider.getSigner(); // Initialize your smart account const smartAccount = await createSmartAccountClient({ signer: ethersSigner, bundlerUrl: 'your-bundler-url-from-the-biconomy-dashboard', biconomyPaymasterApiKey: 'your-paymaster-api-key-from-the-biconomy-dashboard' }); ``` Note: if your app uses React, we suggest that you store the user's Biconomy `smartAccount` in a React context that wraps your application. This allows you to easily access the smart account from your app's pages and components.
## 5. Send transactions from the smart account Now that your users have Biconomy smart accounts, they can now send transaction from their smart account. To send a transaction from a user's smart account, use Biconomy's [**`sendTransaction`**](https://docs.biconomy.io/Account/methods#sendtransaction-) method. An example of sending a transaction to mint an NFT gaslessly is below: ```tsx theme={"system"} // Initialize an ethers JsonRpcProvider for your network const provider = new ethers.providers.JsonRpcProvider(`insert-rpc-url-for-your-network`); // Initialize an ethers contract instance for your NFT const nft = new ethers.Contract('insert-your-NFT-address', insertYourNftAbi, provider); // Construct a Transaction for the minting transaction const mintTransaction = await nft.populateTransaction.mint!('insert-the-smart-account-address'); // `smartAccount` is the Biconomy smart account we initialized above const mintTx = { to: 'insert-your-NFT-address', data: mintTransaction.data }; // Send transaction to mempool, to mint NFT gaslessly const userOpResponse = await smartAccount.sendTransaction(mintTx, { paymasterServiceData: {mode: PaymasterMode.SPONSORED} }); const {transactionHash} = await userOpResponse.waitForTxHash(); console.log('Transaction Hash', transactionHash); const userOpReceipt = await userOpResponse.wait(); if (userOpReceipt.success == 'true') { console.log('UserOp receipt', userOpReceipt); console.log('Transaction receipt', userOpReceipt.receipt); } ``` **That's it! You've configured your app to create smart wallets for all of your users, and can seamlessly add in AA features like [gas sponsorship](https://docs.biconomy.io/dashboard/paymaster) and more.** 🎉
## Account Abstraction with AccountKit [AccountKit](https://accountkit.alchemy.com/) is a toolkit by [Alchemy](https://www.alchemy.com/) for creating [ERC-4337](https://eips.ethereum.org/EIPS/eip-4337)-compatible smart accounts for your users, using the user's EOA as the smart account's signer. This allows you to easily add [Account Abstraction](https://ethereum.org/en/roadmap/account-abstraction/) features into your app. **You can easily integrate AccountKit alongside Privy to create smart wallets from your user's embedded or external wallets, allowing you to enhance your app with gas sponsorship, batched transactions, and more!** Read below to learn how to configure your app to create smart wallets for *all* your users!
What is an EOA? An [**EOA, or externally-owned account**](https://ethereum.org/en/developers/docs/accounts/), is any Ethereum account that is controlled by a private key. Privy's embedded wallets and most external wallets (MetaMask, Coinbase Wallet, Rainbow Wallet, etc.) are EOAs. EOAs differ from **contract accounts**, which are instead controlled by smart contract code and do not have their own private key. AccountKit's smart wallet is a contract account. Contract accounts have [enhanced capabilities, such as gas sponsorship and batched transactions](https://ethereum.org/en/roadmap/account-abstraction/). Since they do not have their own private key, contract accounts cannot *directly* produce signatures and initiate transaction flows. Instead, each contract account is generally "managed" by an EOA, which authorizes actions taken by the contract account via a signature; this EOA is called a **signer**. In this integration, the user's EOA (from Privy) serves as the signer for their smart wallet (from AccountKit). The smart wallet (AccountKit) holds all assets and submits all transactions to the network, but the signer (Privy) is responsible for producing signatures and "kicking off" transaction flows.
### 1. Install Privy and AccountKit Install the [**`@privy-io/react-auth`**](https://www.npmjs.com/package/@privy-io/react-auth) SDK from Privy, the [**`@account-kit/privy-integration`**](https://www.alchemy.com/docs/wallets/third-party/signers/privy) SDKs from Alchemy, and [**`viem`**](https://viem.sh/): ```sh theme={"system"} npm i @privy-io/react-auth @account-kit/privy-integration viem ``` ### 2. Configure your app's `PrivyProvider` First, follow the instructions in the [**Privy Quickstart**](/basics/react/quickstart) to get your app set up with Privy. Next, set **Add confirmation modals** to "off" in your app's **Embedded wallets** page in the Privy [**dashboard**](https://dashboard.privy.io). This will configure Privy to *not* show its default UIs when your user must sign messages or send transactions. Instead, we recommend you use your own custom UIs for showing users the [`UserOperation`](https://www.alchemy.com/overviews/user-operations)s they sign. Then, update the **`config.embeddedWallets.createOnLogin`** property of your **`PrivyProvider`** to `'users-without-wallets'`.This will configure Privy to create an embedded wallet for users logging in via a web2 method (email, phone, socials), ensuring that *all* of your users have a wallet that can be used as an EOA. Lastly, add the `AlchemyProvider` as a child of your `PrivyProvider`, passing in your Alchemy API key and gas policy ID. Your **`PrivyProvider`** should then look like: ```tsx theme={"system"} {/* Your app's components */} ``` Just like that, your app is now configured to create smart accounts for all of your users automatically upon login! ### 3. Send gasless transactions with Alchemy's AccountKit SDK Now that your users have smart accounts created for them automatically, you can use Alchemy's AccountKit SDK to send gasless transactions from their smart accounts. ```tsx theme={"system"} import {useAlchemySendTransaction} from '@account-kit/privy-integration'; function SendButtons() { const {sendTransaction, isLoading} = useAlchemySendTransaction(); const single = async () => await sendTransaction({to: '0x...', data: '0x...', value: '0x0'}); const batch = async () => await sendTransaction([ {to: '0x...', data: '0x...'}, {to: '0x...', data: '0x...'} ]); return ( <> ); } ``` **That's it! You've configured your app to create smart wallets for all of your users, and can seamlessly add in AA features like gas sponsorship, batched transactions, and more.** 🎉 To learn more about using Alchemy's AccountKits with Privy, check out the [full guide from Alchemy](https://www.alchemy.com/docs/wallets/third-party/signers/privy).
# Create a smart wallet on iOS and Android Source: https://docs.privy.io/recipes/account-abstraction/native-mobile Use Privy embedded wallets on native mobile as signers for ERC-4337 smart wallets ## Overview This guide covers creating and using an ERC-4337 smart wallet from a native iOS or Android app. By the end, your app will be able to: * Create an embedded wallet on mobile that serves as the smart wallet's signer * Derive a deterministic smart wallet address from the signer * Send gas-sponsored transactions via a hybrid client-server architecture Privy's native smart wallet integration (`SmartWalletsProvider`) is available for React and React Native. For iOS and Android apps, a **hybrid architecture** enables smart wallet functionality. The mobile app creates an embedded wallet and signs UserOperation hashes. A Node.js backend manages the smart wallet infrastructure. This architecture keeps wallet keys on the device (non-custodial). Server-side tooling handles ERC-4337 operations. Smart wallets architecture ## Prerequisites * Smart wallets [enabled in the Privy Dashboard](/wallets/using-wallets/evm-smart-wallets/setup/configuring-dashboard) with a bundler and paymaster configured * The [iOS SDK](/basics/swift/setup) or [Android SDK](/basics/android/setup) installed and configured * A Node.js backend with `permissionless` and `viem` installed ```sh theme={"system"} npm i permissionless viem ``` ## Architecture overview The transaction flow for a smart wallet on native mobile: 1. The mobile app creates an embedded wallet (EOA). This wallet acts as the smart wallet's **signer**. 2. The backend derives a deterministic smart wallet address from the signer's address. 3. For each transaction: * The mobile app sends transaction details to the backend. * The backend constructs a UserOperation and returns the hash to sign. * The mobile app signs the hash with the embedded wallet. * The mobile app sends the signature back to the backend. * The backend submits the signed UserOperation to the bundler. ## Step 1: Create an embedded wallet Create an Ethereum embedded wallet on the mobile device. This wallet acts as the EOA signer controlling the smart wallet. ```swift theme={"system"} guard let user = privy.user else { return } do { let ethereumWallet = try await user.createEthereumWallet() let signerAddress = ethereumWallet.address print("Signer address: \(signerAddress)") } catch { print("Error creating wallet: \(error.localizedDescription)") } ``` ```kotlin theme={"system"} privy.user?.let { privyUser -> val result = privyUser.createEthereumWallet(allowAdditional = false) result.fold( onSuccess = { ethereumWallet -> val signerAddress = ethereumWallet.address println("Signer address: $signerAddress") }, onFailure = { println("Error creating wallet: ${it.message}") } ) } ``` If your app already creates embedded wallets for authenticated users, skip this step and use the existing wallet as the signer. ## Step 2: Set up the server-side smart wallet infrastructure Use `permissionless` and `viem` on the backend to derive the smart wallet address. This example uses Kernel (ZeroDev). Any ERC-4337 smart account implementation works. ```ts {skip-check} theme={"system"} import {toKernelSmartAccount} from 'permissionless/accounts'; import {createPublicClient, http} from 'viem'; import {base} from 'viem/chains'; import {entryPoint07Address} from 'viem/account-abstraction'; import {toAccount} from 'viem/accounts'; const publicClient = createPublicClient({ chain: base, transport: http() }); function createRemoteSigner(signerAddress: `0x${string}`) { return toAccount({ address: signerAddress, async signMessage() { // This will be replaced with the mobile app's signature // See Step 4 for the full implementation throw new Error('Use prepareUserOperation flow instead'); }, async signTransaction() { throw new Error('Use prepareUserOperation flow instead'); }, async signTypedData() { throw new Error('Use prepareUserOperation flow instead'); } }); } async function getSmartWalletAddress(signerAddress: `0x${string}`) { const remoteSigner = createRemoteSigner(signerAddress); const kernelAccount = await toKernelSmartAccount({ client: publicClient, owners: [remoteSigner], entryPoint: { address: entryPoint07Address, version: '0.7' } }); return kernelAccount.address; } ``` The smart wallet address is deterministic. Given the same signer address, the derived smart wallet address is always the same, even before deployment. ## Step 3: Prepare a UserOperation on the server The backend constructs the UserOperation and returns the hash for the mobile app to sign. ```ts {skip-check} theme={"system"} import {toKernelSmartAccount} from 'permissionless/accounts'; import {createSmartAccountClient} from 'permissionless'; import {createPimlicoClient} from 'permissionless/clients/pimlico'; import {createPublicClient, http} from 'viem'; import {base} from 'viem/chains'; import {entryPoint07Address, getUserOperationHash} from 'viem/account-abstraction'; import {toAccount} from 'viem/accounts'; const publicClient = createPublicClient({ chain: base, transport: http() }); const bundlerUrl = 'https://api.pimlico.io/v2/base/rpc?apikey=YOUR_PIMLICO_API_KEY'; const paymasterUrl = 'https://api.pimlico.io/v2/base/rpc?apikey=YOUR_PIMLICO_API_KEY'; const pimlicoClient = createPimlicoClient({ transport: http(paymasterUrl), entryPoint: { address: entryPoint07Address, version: '0.7' } }); function createRemoteSigner(signerAddress: `0x${string}`) { return toAccount({ address: signerAddress, async signMessage() { throw new Error('Use prepareUserOperation flow instead'); }, async signTransaction() { throw new Error('Use prepareUserOperation flow instead'); }, async signTypedData() { throw new Error('Use prepareUserOperation flow instead'); } }); } async function prepareUserOperation( signerAddress: `0x${string}`, transaction: {to: `0x${string}`; value?: bigint; data?: `0x${string}`} ) { const remoteSigner = createRemoteSigner(signerAddress); const kernelAccount = await toKernelSmartAccount({ client: publicClient, owners: [remoteSigner], entryPoint: { address: entryPoint07Address, version: '0.7' } }); const smartAccountClient = createSmartAccountClient({ account: kernelAccount, chain: base, bundlerTransport: http(bundlerUrl), paymaster: pimlicoClient, userOperation: { estimateFeesPerGas: async () => (await pimlicoClient.getUserOperationGasPrice()).fast } }); const userOp = await smartAccountClient.prepareUserOperation({ calls: [transaction] }); const userOpHash = getUserOperationHash({ userOperation: {...userOp, sender: kernelAccount.address}, chainId: base.id, entryPointAddress: entryPoint07Address, entryPointVersion: '0.7' }); return {userOp, userOpHash, smartWalletAddress: kernelAccount.address}; } ``` ## Step 4: Sign the UserOperation hash on mobile The mobile app receives the `userOpHash` from the backend and signs it with `personal_sign`. ```swift theme={"system"} func signUserOperationHash(wallet: EmbeddedEthereumWallet, userOpHash: String) async throws -> String { let request = EthereumRpcRequest( method: "personal_sign", params: [userOpHash, wallet.address] ) let signature = try await wallet.provider.request(request) return signature } // Usage if let wallet = privy.user?.embeddedEthereumWallets.first { do { // 1. Request UserOperation preparation from your backend let prepareResponse = try await yourBackend.prepareUserOperation( signerAddress: wallet.address, to: "0xRecipientAddress", value: "0x0", data: "0x" ) // 2. Sign the UserOperation hash let signature = try await signUserOperationHash( wallet: wallet, userOpHash: prepareResponse.userOpHash ) // 3. Submit the signed UserOperation via your backend let txHash = try await yourBackend.submitUserOperation( userOpHash: prepareResponse.userOpHash, signature: signature ) print("Transaction hash: \(txHash)") } catch { print("Error: \(error.localizedDescription)") } } ``` ```kotlin theme={"system"} suspend fun signUserOperationHash( wallet: EmbeddedEthereumWallet, userOpHash: String ): Result { return wallet.provider.request( request = EthereumRpcRequest.personalSign(userOpHash, wallet.address) ).map { response -> response.result } } // Usage privy.user?.embeddedEthereumWallets?.firstOrNull()?.let { wallet -> coroutineScope.launch { // 1. Request UserOperation preparation from your backend val prepareResponse = yourBackend.prepareUserOperation( signerAddress = wallet.address, to = "0xRecipientAddress", value = "0x0", data = "0x" ) // 2. Sign the UserOperation hash val signResult = signUserOperationHash( wallet = wallet, userOpHash = prepareResponse.userOpHash ) signResult.fold( onSuccess = { signature -> // 3. Submit the signed UserOperation via your backend val txHash = yourBackend.submitUserOperation( userOpHash = prepareResponse.userOpHash, signature = signature ) println("Transaction hash: $txHash") }, onFailure = { error -> println("Error signing: ${error.message}") } ) } } ``` ## Step 5: Submit the signed UserOperation The backend attaches the signature to the UserOperation and submits it to the bundler. ```ts {skip-check} theme={"system"} import {createPublicClient, http} from 'viem'; import {base} from 'viem/chains'; import {entryPoint07Address, createBundlerClient} from 'viem/account-abstraction'; const publicClient = createPublicClient({ chain: base, transport: http() }); const bundlerUrl = 'https://api.pimlico.io/v2/base/rpc?apikey=YOUR_PIMLICO_API_KEY'; async function submitUserOperation(userOp: any, signature: `0x${string}`) { const bundlerClient = createBundlerClient({ client: publicClient, transport: http(bundlerUrl) }); const hash = await bundlerClient.sendUserOperation({ ...userOp, signature, entryPointAddress: entryPoint07Address }); const receipt = await bundlerClient.waitForUserOperationReceipt({hash}); return receipt.receipt.transactionHash; } ``` ## Full example: Sponsored USDC transfer This example shows the complete flow for a gas-sponsored USDC transfer. ```swift theme={"system"} import PrivySDK func sendSponsoredUSDC(wallet: EmbeddedEthereumWallet, to: String, amount: String) async throws -> String { // 1. Prepare the UserOperation on the server let prepareResponse = try await yourBackend.prepareUserOperation( signerAddress: wallet.address, to: "0xUSDC_CONTRACT_ADDRESS", value: "0x0", data: encodeTransferData(to: to, amount: amount) ) // 2. Sign the UserOperation hash let request = EthereumRpcRequest( method: "personal_sign", params: [prepareResponse.userOpHash, wallet.address] ) let signature = try await wallet.provider.request(request) // 3. Submit to bundler via server let txHash = try await yourBackend.submitUserOperation( userOpHash: prepareResponse.userOpHash, signature: signature ) return txHash } ``` ```kotlin theme={"system"} import io.privy.android.EmbeddedEthereumWallet import io.privy.android.EthereumRpcRequest suspend fun sendSponsoredUSDC( wallet: EmbeddedEthereumWallet, to: String, amount: String ): String { // 1. Prepare the UserOperation on the server val prepareResponse = yourBackend.prepareUserOperation( signerAddress = wallet.address, to = "0xUSDC_CONTRACT_ADDRESS", value = "0x0", data = encodeTransferData(to, amount) ) // 2. Sign the UserOperation hash val signResult = wallet.provider.request( request = EthereumRpcRequest.personalSign( prepareResponse.userOpHash, wallet.address ) ) val signature = signResult.getOrThrow().result // 3. Submit to bundler via server return yourBackend.submitUserOperation( userOpHash = prepareResponse.userOpHash, signature = signature ) } ``` ```ts {skip-check} theme={"system"} import {toKernelSmartAccount} from 'permissionless/accounts'; import {createSmartAccountClient} from 'permissionless'; import {createPimlicoClient} from 'permissionless/clients/pimlico'; import {createPublicClient, http, encodeFunctionData, parseAbi} from 'viem'; import {base} from 'viem/chains'; import { entryPoint07Address, createBundlerClient, getUserOperationHash } from 'viem/account-abstraction'; import {toAccount} from 'viem/accounts'; const PIMLICO_API_KEY = 'YOUR_PIMLICO_API_KEY'; const bundlerUrl = `https://api.pimlico.io/v2/base/rpc?apikey=${PIMLICO_API_KEY}`; const publicClient = createPublicClient({ chain: base, transport: http() }); const pimlicoClient = createPimlicoClient({ transport: http(bundlerUrl), entryPoint: { address: entryPoint07Address, version: '0.7' } }); // POST /prepare-user-operation async function handlePrepare( signerAddress: `0x${string}`, to: `0x${string}`, value: bigint, data: `0x${string}` ) { const remoteSigner = toAccount({ address: signerAddress, async signMessage() { throw new Error('Signing handled by mobile client'); }, async signTransaction() { throw new Error('Signing handled by mobile client'); }, async signTypedData() { throw new Error('Signing handled by mobile client'); } }); const kernelAccount = await toKernelSmartAccount({ client: publicClient, owners: [remoteSigner], entryPoint: { address: entryPoint07Address, version: '0.7' } }); const smartAccountClient = createSmartAccountClient({ account: kernelAccount, chain: base, bundlerTransport: http(bundlerUrl), paymaster: pimlicoClient, userOperation: { estimateFeesPerGas: async () => (await pimlicoClient.getUserOperationGasPrice()).fast } }); const userOp = await smartAccountClient.prepareUserOperation({ calls: [{to, value, data}] }); const userOpHash = getUserOperationHash({ userOperation: {...userOp, sender: kernelAccount.address}, chainId: base.id, entryPointAddress: entryPoint07Address, entryPointVersion: '0.7' }); return {userOp, userOpHash, smartWalletAddress: kernelAccount.address}; } // POST /submit-user-operation async function handleSubmit(userOp: any, signature: `0x${string}`) { const bundlerClient = createBundlerClient({ client: publicClient, transport: http(bundlerUrl) }); const hash = await bundlerClient.sendUserOperation({ ...userOp, signature, entryPointAddress: entryPoint07Address }); const receipt = await bundlerClient.waitForUserOperationReceipt({hash}); return receipt.receipt.transactionHash; } ``` ## Next steps Configure paymasters to sponsor gas fees for your users. Learn more about smart wallet features and supported providers. Send multiple transactions atomically from a smart wallet. Set up bundlers, paymasters, and supported networks. # Integrating smart accounts with wagmi Source: https://docs.privy.io/recipes/account-abstraction/wagmi If your app uses wagmi and one of Privy's account abstraction integrations to set up smart accounts for the embedded wallet, you can configure wagmi to reflect the smart account for the embedded wallet instead of the externally-owned account (e.g. signer) by following the instructions below. ## Resources Complete starter repository showcasing Privy's wagmi integration with smart accounts and embedded wallets. ## 0. Setup This guide assumes that you have already integrated both Privy and wagmi into your app, and are now looking to set up wagmi with smart accounts. If you have not yet set up the basic integration, please first follow the [Privy quickstart](/basics/react/quickstart) and our [wagmi integration guide](/wallets/connectors/ethereum/integrations/wagmi). ## 1. Initialize an EIP1193 provider for the smart account Once you've set up your app with wagmi, implement a function that initializes the smart account of your choice (Kernel, SimpleAccount, Biconomy, etc.) from the Privy embedded wallet. As a parameter, the function should accept an object with a `signer` field that contains a viem [`EIP1193Provider`](https://github.com/wevm/viem/blob/74dbb2e7276af349bc03988eca5ec99b83292e61/src/types/eip1193.ts#L26) for the Privy embedded wallet. It should then initialize a smart account, using the embedded wallet as a signer, and should return a Promise for a viem [`EIP1193Provider`](https://github.com/wevm/viem/blob/74dbb2e7276af349bc03988eca5ec99b83292e61/src/types/eip1193.ts#L26) for the smart account.The function should have the following type: ```tsx theme={"system"} async ({signer}: {signer: EIP1193Provider}) => Promise; ``` As an example, if you are using Privy alongside ZeroDev for smart account support, you can define this function like so: ```tsx theme={"system"} import {signerToEcdsaValidator} from '@zerodev/ecdsa-validator'; import { createKernelAccount, createZeroDevPaymasterClient, createKernelAccountClient, KernelEIP1193Provider, KernelAccountClient } from '@zerodev/sdk'; import {getEntryPoint, KERNEL_V3_1} from '@zerodev/sdk/constants'; import {http, createPublicClient, EIP1193Provider} from 'viem'; import {baseSepolia} from 'viem/chains'; // Create a public client const publicClient = createPublicClient({ transport: http(process.env.BUNDLER_RPC) }); const entryPoint = getEntryPoint('0.7'); const kernelVersion = KERNEL_V3_1; export const signerToZeroDevSmartAccount = async ({ signer }: { signer: EIP1193Provider; }): Promise => { // Create an ECDSA validator using the EIP1193Provider directly const ecdsaValidator = await signerToEcdsaValidator(publicClient, { signer: signer, entryPoint, kernelVersion }); // Create a Kernel account const kernelAccount = await createKernelAccount(publicClient, { plugins: { sudo: ecdsaValidator }, entryPoint, kernelVersion }); // Initialize a Kernel (smart account) client from the signer const kernelClient = createKernelAccountClient({ account: kernelAccount, chain: baseSepolia, bundlerTransport: http(process.env.BUNDLER_RPC), client: publicClient, paymaster: { getPaymasterData: async ({userOperation}) => { const paymasterClient = createZeroDevPaymasterClient({ chain: baseSepolia, transport: http(process.env.PAYMASTER_RPC), entryPoint }); return paymasterClient.sponsorUserOperation({ userOperation, entryPoint }); } } }) as KernelAccountClient; // Get an EIP1193Provider for the Kernel smart account and return it const kernelProvider = new KernelEIP1193Provider(kernelClient); return kernelProvider as EIP1193Provider; }; ``` ## 2. Use the smart account's EIP1193Provider to register a wagmi connector Next, import the `useEmbeddedSmartAccountConnector` hook from `@privy-io/wagmi`. This hook allows you to register a smart account connector for wagmi that replaces the regular embedded wallet connector. ```tsx theme={"system"} import {useEmbeddedSmartAccountConnector} from '@privy-io/wagmi'; ``` Now, call the `useEmbeddedSmartAccountConnector` hook to register a smart account connector with wagmi. As a parameter to the hook, pass an object with the following fields: | Field | Type | Description | | --------------------------- | ------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `getSmartAccountFromSigner` | `async ({signer}: {signer: EIP1193Provider}) => Promise` | A function that takes an `EIP1193Provider` for the user's embedded wallet and converts it to an `EIP1193Provider` for the smart account. This is the same function you implemented in step 1. | **The `useEmbeddedSmartAccountConnector` hook must be mounted at all times when using wagmi with the smart account.** We recommend calling the hook in a component close to the root of your application. As an example, if you are using Privy alongside ZeroDev for smart account support, you might call the hook like so: ```tsx theme={"system"} // This hook must be mounted whenever using wagmi with the smart account // See step 1 for the implementation of `signerToZeroDevSmartAccount` useEmbeddedSmartAccountConnector({ getSmartAccountFromSigner: signerToZeroDevSmartAccount }); ``` **That's it! You've now registered a smart account connector for the embedded wallet with Privy's wagmi integration, and can use wagmi hooks to interface with the smart account 🎉.** Currently, Privy's wagmi integration only supports using a smart account with **embedded wallets**, not external wallets (e.g. MetaMask). With the setup above, if a user has an embedded wallet, the smart account connector will be the *only* wallet connected to wagmi. If a user does not have an embedded wallet and is using an external wallet, wagmi will interface with the external wallets as usual. # Account funding Source: https://docs.privy.io/recipes/account-funding/overview Account funding recipes help teams move funds into and out of Privy wallets through onramps, deposit addresses, and off-ramp providers. Embed card-based funding directly in app flows. Create deposit addresses and route inbound funds automatically. Add localized fiat conversion through Due. Offer MXNB funding flows for LATAM markets. # Authorized wallet access for self-hosted agents Source: https://docs.privy.io/recipes/agent-integrations/agent-authorization **This document describes how to let any self-hosted agent or CLI securely access user wallets in a Privy app without distributing app secrets or requiring a browser during transactions.** The flow uses the [OAuth 2.0 Device Authorization Grant](https://oauth.net/2/device-flow/), the same pattern GitHub CLI and other developer tools use for headless authentication. Users approve agent access once via a browser; the agent stores tokens and transacts autonomously within that authorization. ## When to use agent authorization Use agent authorization when you want to allow your users to use their wallets via a self-hosted agent (such as Claude Code, Codex, OpenClaw, or a custom CLI). Agent authorization is the foundation for building agent-first experiences on top of your Privy app. By implementing this flow, your app's will be able to deliver rich experiences via any of agentic interfaces, including CLIs, MCP servers, Skills, connectors, and autonomous agents. Agent authorization is a good fit when: * The agent runs in a CLI or headless environment with no persistent browser * Wallet operations need to happen autonomously after a one-time user approval * App secrets must not be distributed to end-user machines * The app does not want to require a backend server to proxy wallet requests to the Privy API, including the authentication and authorization that entails ## How it works The flow has three participants: the **agent** (CLI or autonomous process), the **user** (approves via browser), and **your app** (hosts the verification page and holds the Privy app). The agent calls Privy's device authorization endpoint and receives a `device_code` (kept secret on the machine) and a `user_code` (short, human-readable). The agent displays the verification URL and user code: ``` Visit: https://your-app.com/authorize?user_code=ABCD-1234 Enter: ABCD-1234 Waiting for authorization… ``` The user opens the verification URL (a page your app hosts), signs in with Privy, and approves the agent's request. The verification page calls Privy's `device_verify` endpoint with the user code and the user's access token. The agent polls Privy's token endpoint at a fixed interval. Once the user approves, the response contains an access token and a refresh token. The agent stores these tokens for subsequent requests. Store tokens in a secure credential store — for example, the OS keychain on macOS or an encrypted secrets store on Linux. Never write tokens to a plaintext file on disk. For each wallet operation, the agent exchanges the access token for an ephemeral signing key, signs the request, and calls Privy's wallet RPC endpoint directly. No app secret required. ## Enable in dashboard Navigate to your app in the [Privy Dashboard](https://dashboard.privy.io), open **Authentication** -> **Advanced** and toggle **Enable for CLI and agent access** on. Set the **Verification URI**, the URL where users will approve agent requests. The verification URI must point to a page your app hosts. Privy returns this URI to the agent in the device authorization response so the agent can display it to the user. The verification URI is typically a dedicated route in your web app, for example `https://your-app.com/authorize`. ## Build the verification page The verification page is the only browser-side step in the flow. It reads the `user_code` from the query string, prompts the user to log in if needed, and calls `device_verify` to approve or deny the agent's request. Privy's device authorization endpoint returns a `verification_uri_complete` that pre-fills the user code as a query parameter, so users arriving via that link will not need to type a code manually. ```tsx theme={"system"} import {useSearchParams} from 'react-router-dom'; import {usePrivy} from '@privy-io/react-auth'; const PRIVY_APP_ID = process.env.NEXT_PUBLIC_PRIVY_APP_ID!; export function AgentAuthorizationPage() { const {getAccessToken, authenticated, login} = usePrivy(); const [searchParams] = useSearchParams(); const userCode = searchParams.get('user_code') ?? ''; async function submit(action: 'approve' | 'deny') { if (!authenticated) { await login(); return; } const userAccessToken = await getAccessToken(); const res = await fetch('https://auth.privy.io/api/oauth/v2/device_verify', { method: 'POST', headers: { 'Content-Type': 'application/json', 'privy-app-id': PRIVY_APP_ID, Authorization: `Bearer ${userAccessToken}`, }, body: JSON.stringify({user_code: userCode, action}), }); if (!res.ok) throw new Error(`device_verify failed: ${res.status}`); } if (!userCode) { return

No code found. Open the link provided by the agent.

; } return (

Code: {userCode}

{!authenticated ? ( ) : ( <> )}
); } ```
```javascript theme={"system"} async function verifyDeviceAuth({appId, userAccessToken, userCode, action}) { const response = await fetch('https://auth.privy.io/api/oauth/v2/device_verify', { method: 'POST', headers: { 'Content-Type': 'application/json', 'privy-app-id': appId, Authorization: `Bearer ${userAccessToken}`, }, body: JSON.stringify({user_code: userCode, action}), }); if (!response.ok) throw new Error(`device_verify HTTP ${response.status}`); return response.json(); } ```
The `device_verify` endpoint returns `400` for invalid or expired user codes. If the agent reports `expired_token` or `access_denied` during polling, instruct the user to restart the login flow. ## Integrate device authorization in your agent The following covers the full API flow for a CLI or agent. All requests go to `https://auth.privy.io` and require the `privy-app-id` header. For the complete request and response schemas, see the [OpenAPI specification](https://auth.privy.io/api/v1/openapi.json). ### Request a device code Call this once at the start of login to receive the codes the agent will use for polling and display. ```shell theme={"system"} POST https://auth.privy.io/api/oauth/v2/device_authorization privy-app-id: Content-Type: application/json {} ``` ```json theme={"system"} { "device_code": "a3f8c2e1d4b7...", "user_code": "ABCD-1234", "verification_uri": "https://your-app.com/authorize", "verification_uri_complete": "https://your-app.com/authorize?user_code=ABCD-1234", "expires_in": 600, "interval": 5 } ``` Display `verification_uri_complete` (or `verification_uri` + `user_code` separately) to the user. Keep `device_code` secret on the machine; it is used only for polling. The device code expires after 10 minutes. **Error:** `403 device_auth_not_enabled`: device authorization is not enabled for this app in the dashboard. ### Poll for an access token Poll this endpoint at the `interval` from the previous response (in seconds) until the user approves. ```shell theme={"system"} POST https://auth.privy.io/api/oauth/v2/token privy-app-id: Content-Type: application/json { "grant_type": "device_code", "device_code": "a3f8c2e1d4b7..." } ``` ```json theme={"system"} { "access_token": "eyJhbGci...", "token_type": "Bearer", "expires_in": 900, "refresh_token": "d9e3f1a2b4c6..." } ``` While polling, the endpoint returns `400` with one of the following errors: | Error | Action | | ----------------------- | ----------------------------------------------------- | | `authorization_pending` | User has not approved yet; keep polling | | `slow_down` | Polling too fast; increase interval by 5 seconds | | `expired_token` | Device code expired; restart from the previous step | | `access_denied` | User denied access; stop polling and surface an error | ### Refresh an access token Exchange a refresh token for a new access token when the current one is near expiry or after receiving a `401` from a wallet endpoint. The old refresh token is immediately invalidated. ```shell theme={"system"} POST https://auth.privy.io/api/oauth/v2/token privy-app-id: Content-Type: application/json { "grant_type": "refresh_token", "refresh_token": "d9e3f1a2b4c6..." } ``` ```json theme={"system"} { "access_token": "eyJhbGci...", "token_type": "Bearer", "expires_in": 900, "refresh_token": "e8f2a3b5c7d1..." } ``` Store the new `refresh_token`. If this endpoint returns `access_denied`, the refresh token has expired or the user revoked access, and the user must re-authorize from the beginning. ### Get a wallet signing key Before submitting wallet operations, exchange the access token for an ephemeral signing key. Provide an HPKE public key so the response is encrypted to the agent process only. ```shell theme={"system"} POST https://auth.privy.io/api/oauth/v2/wallets/authenticate Authorization: Bearer privy-app-id: privy-grant-type: device_code Content-Type: application/json { "encryption_type": "HPKE", "recipient_public_key": "" } ``` ```json theme={"system"} { "encrypted_authorization_key": { "encapsulated_key": "", "ciphertext": "" }, "expires_at": "2024-01-01T00:15:00.000Z", "wallets": [{"id": "wallet_abc123", "address": "0x...", "chain_type": "ethereum"}] } ``` Decrypt `encrypted_authorization_key` with the corresponding HPKE private key. Cache and reuse the key until `expires_at`. It produces the `privy-authorization-signature` header on RPC requests. Keep the decrypted signing key in memory only. Never write it to disk or log it — it grants direct signing authority over the user's wallet for the duration of its lifetime. ### Submit a wallet RPC Use the Privy wallet ID (for example, `wallet_abc123`), not the on-chain address. ```shell theme={"system"} POST https://auth.privy.io/api/oauth/v2/wallets/{wallet_id}/rpc Authorization: Bearer privy-app-id: privy-grant-type: device_code privy-authorization-signature: Content-Type: application/json ``` ```json Send a transaction theme={"system"} { "method": "eth_sendTransaction", "params": { "transaction": { "to": "0xdeadbeef...", "value": "0x5af3107a4000", "chain_id": 8453 } } } ``` ```json Sign a message theme={"system"} { "method": "personal_sign", "params": { "message": "Hello from the agent" } } ``` ```json theme={"system"} { "method": "eth_sendTransaction", "data": { "hash": "0x7f8e9d..." } } ``` | Error | Action | | --------------------------- | ------------------------------------------------ | | `401 access token expired` | Refresh via the token endpoint, then retry | | `403 wallet not accessible` | Wallet does not belong to the authenticated user | ## Token lifetimes | Artifact | Lifetime | | --------------------------- | ----------------------------- | | `device_code` / `user_code` | 10 minutes | | Access token | 15 minutes | | Refresh token | 30 days (rotated on each use) | ## Managing authorizations Users can list and revoke active agent authorizations at any time. These endpoints require a valid user access token. ### List active authorizations ```shell theme={"system"} GET https://auth.privy.io/api/oauth/v2/grants Authorization: Bearer privy-app-id: ``` ```json theme={"system"} { "data": [ { "id": "grant_abc123", "grant_type": "device_code", "created_at": 1717000000, "last_used_at": 1717003600 } ] } ``` ### Revoke an authorization ```shell theme={"system"} DELETE https://auth.privy.io/api/oauth/v2/grants/{grant_id} Authorization: Bearer privy-app-id: ``` ```json theme={"system"} {"success": true} ``` Revoking a grant immediately invalidates all refresh tokens associated with it. The corresponding access token (JWT) remains valid until its natural expiry of up to 15 minutes. Surface the list and revoke endpoints in any user-facing account settings so users can audit and terminate agent access. ## Learn more Give any agent a wallet with a CLI command. No integration code needed. Create developer-controlled agent wallets with policy guardrails. Constrain agent behavior with transfer limits, allowlists, and time-based controls. Enable HTTP-native payments for APIs and digital content. # Agent CLI Source: https://docs.privy.io/recipes/agent-integrations/agent-cli **The Privy Agent CLI gives agents a simple way to spin up, fund, and manage wallets without any integration work. It pairs with an [Agent Sandbox](https://agents.privy.io/) that lets users track agent spending activity, manage funds, and stay in control.** The sandbox CLI (`@privy-io/agent-wallet-cli`) enables authentication, funding, and transactions directly from assistants like OpenClaw, Claude Code, and more. Go to [agents.privy.io](https://agents.privy.io) to track agent activity and spend in the agent sandbox. ## Getting started Go to [agents.privy.io](https://agents.privy.io) to create an account, view wallets, and manage agent activity. Install the agent sandbox CLI globally: ```bash theme={"system"} npm install -g @privy-io/agent-wallet-cli ``` Or run it directly with `npx`: ```bash theme={"system"} npx @privy-io/agent-wallet-cli login ``` Start the authentication flow. The CLI displays a verification URL and a short code, then waits for approval. ```bash theme={"system"} privy-agent-wallets login ``` Open the URL in a browser and sign in to the agent sandbox. Enter the code shown in the terminal to approve agent access. The CLI automatically completes once approval is confirmed. Open the agent sandbox in a browser to add funds via onramp: ```bash theme={"system"} privy-agent-wallets fund ``` The agent sandbox at [agents.privy.io](https://agents.privy.io) provides a visual interface to view balances, review transaction history, and onramp funds into the wallet. List the Ethereum and Solana wallet addresses linked to the session: ```bash theme={"system"} privy-agent-wallets list-wallets ``` Output: ``` Ethereum: 0x1a2b...3c4d (wallet_id_xxx) Solana: 7hQ5p...mN9r (wallet_id_yyy) ``` Use the `rpc` command to sign messages and send transactions. Pass the RPC body as JSON: ```bash theme={"system"} privy-agent-wallets rpc --json '{"method": "eth_sendTransaction", "params": {"to": "0xRecipient", "value": "0.01"}}' ``` The body can also be piped from stdin: ```bash theme={"system"} echo '{"method": "personal_sign", "params": {"message": "hello"}}' | privy-agent-wallets rpc ``` ``` Set up https://agents.privy.io/skill.md ``` ## Supported RPC methods ### Ethereum | Method | Description | | --------------------------- | -------------------------------- | | `personal_sign` | Sign a plaintext message | | `eth_sendTransaction` | Send a transaction | | `eth_signTransaction` | Sign without broadcasting | | `eth_signTypedData_v4` | Sign EIP-712 typed data | | `secp256k1_sign` | Raw secp256k1 signature | | `eth_sign7702Authorization` | EIP-7702 authorization | | `eth_signUserOperation` | Sign a user operation (ERC-4337) | ### Solana | Method | Description | | ------------------------ | -------------------------------- | | `signTransaction` | Sign a Solana transaction | | `signAndSendTransaction` | Sign and broadcast a transaction | | `signMessage` | Sign an arbitrary message | ## Design principles * **CLI-first distribution**: Agents already execute shell commands. A CLI is the most natural interface for agent-driven wallets. * **Skill-based discovery**: The [skill file](https://agents.privy.io/skill.md) teaches agents how to authenticate and transact without human guidance beyond the initial login. * **Browser-based funding**: The human owner retains a visual dashboard to check balances, view transaction history, and onramp funds, keeping the human in control. * **Cryptographic authorization**: Each transaction uses an ephemeral signing key obtained by exchanging an OAuth access token. The agent is never given the wallet private key or app secret. ## How it works 1. The CLI calls Privy's device authorization endpoint and displays a short verification URL and code. 2. The human visits the URL, signs in through Privy, and approves agent access. 3. The CLI polls until approval is confirmed and stores the resulting access and refresh tokens in the OS credential manager. 4. For every transaction, the CLI exchanges the access token for an ephemeral signing key, signs the request, and submits it directly to Privy's wallet API. See [Authorized wallet access for self-hosted agents](/recipes/agent-integrations/agent-authorization) for the full API flow. ## Session and credential storage The CLI attempts to use the OS-backed credential manager when available, and otherwise falls back to storing session data in an encrypted file at `~/.privy/session.json`. It is the responsibility of the agent or user to install any required prerequisites for the OS credential manager. | Platform | Credential manager | Prerequisites | | -------- | ----------------------------- | ------------------------------------------------------ | | macOS | Keychain (`security` CLI) | None (available by default) | | Linux | libsecret (`secret-tool` CLI) | `sudo apt install -y libsecret-tools` (Debian/Ubuntu) | | Windows | PowerShell SecretManagement | `Install-Module Microsoft.PowerShell.SecretManagement` | When the OS credential manager is not available (e.g., Docker containers, headless servers, or missing prerequisites), the CLI falls back to an encrypted file at `~/.privy/session.json`. This file is not portable between machines. Each session contains: * The app ID for the Privy agent wallet app * Ethereum and Solana wallet IDs and addresses * The OAuth access token and refresh token * A creation timestamp Sessions remain active for up to 30 days. The access token refreshes automatically on each transaction; the refresh token rotates on each use. To end a session early: ```bash theme={"system"} privy-agent-wallets logout ``` Users can always revoke agent access to their wallet via the agent sandbox at [agents.privy.io/manage](https://agents.privy.io/manage). ## Learn more Track agent activity, view balances, and manage wallets. Build developer-controlled agent wallets with policy guardrails. Enable HTTP-native payments for APIs and digital content. Machine-to-machine payments over HTTP with Privy wallets. Sponsor gas fees for agent transactions. Let agents from any platform access user wallets in your own Privy app. # Amazon Bedrock AgentCore Source: https://docs.privy.io/recipes/agent-integrations/agentcore-payments # Amazon Bedrock AgentCore + Privy Enable AI agents to make autonomous stablecoin payments via AWS Bedrock AgentCore Payments using Privy embedded wallets. This recipe covers how to use [Privy](https://dashboard.privy.io/) embedded wallets as the payment provider for [AWS Bedrock AgentCore Payments](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/payments.html), so agents can autonomously pay for APIs, MCP servers, and content using the x402 protocol. ## Overview Amazon Bedrock AgentCore is AWS's managed platform for building and operating AI agents. **AgentCore Payments** is the service that lets an agent pay for paid endpoints (APIs, MCPs, and web content) over the x402 protocol. It owns the entire payment lifecycle: storing provider credentials, enforcing per-session spend limits, signing the payment, and recording the transaction. Privy provides the **user-owned embedded wallet** that holds the stablecoins. AgentCore connects to Privy through a *PaymentConnector*, retrieves signing material through *AgentCore Identity*, and calls `ProcessPayment` to produce the signed x402 proof. The agent never talks to Privy's wallet APIs directly — it calls AgentCore, and AgentCore talks to Privy. With this integration, agents can: * Discover and call paid APIs, MCP tools, and paywalled content that return `402 Payment Required` * Accept funds from the end user via fiat (cards, Apple Pay, Google Pay, ACH) or USDC stablecoin * Pay autonomously to the paid endpoint over x402 (v1 and v2) * Operate within spend limits enforced per **Payment Session** by AgentCore * Settle on-chain in USDC ## How it works AgentCore Payments is built from a small set of resources. Create them once, in order, and the agent uses them at runtime. * **PaymentManager** — The top-level resource for your account. It defines how agents authenticate (`AWS_IAM` or `CUSTOM_JWT`) and references the IAM execution role AgentCore assumes to do payment work. * **PaymentCredentialProvider** — Stores your Privy credentials (App ID, App Secret, authorization key) inside **AgentCore Identity**, backed by AWS Secrets Manager. The agent runtime never reads these directly. * **PaymentConnector** — Binds the PaymentManager to a payment provider. For Privy, create a `StripePrivy` connector that references the credential provider above. * **PaymentInstrument** — The end user's wallet. Create it through the AgentCore `CreatePaymentInstrument` API (type `EMBEDDED_CRYPTO_WALLET`), **not** through the Privy SDK. AgentCore provisions the Privy embedded wallet on your behalf and returns the wallet address. * **PaymentSession** — A time-bounded spending context (`maxSpendAmount`, `currency`, `expiryTimeInMinutes`). When the session expires or the limit is reached, further payments in that session are denied. * **ProcessPayment** — At runtime, when the agent hits a `402`, it calls `ProcessPayment`. AgentCore checks the session limit, retrieves the Privy signing key from Identity, signs the x402 proof, and returns it. The agent retries the request with the proof. The runtime flow: ``` 1. Agent calls a paid resource (x402) -> 402 Payment Required 2. Agent calls AgentCore ProcessPayment -> session limit checked 3. AgentCore retrieves the Privy signing key -> signs the x402 proof 4. Agent retries the request with the proof -> 200 OK + paid content Your agent --(402)--> Paid resource Your agent --(ProcessPayment)--> AgentCore Payments --(via Identity)--> Privy wallet Your agent --(retry with signed proof)--> Paid resource ``` ## Automated setup using the AgentCore Payments skill You can provision everything in this recipe two ways. The manual, step-by-step path is documented below ([Manual setup](#manual-setup)). If you use an AI coding agent such as Kiro, Claude Code, or Codex, the **AgentCore Payments skill** can provision the same resources for you through a guided conversation, handling the CLI commands, SDK scripts, and framework wiring automatically. The skill provisions the following resources: * **PaymentCredentialProvider** — Stores your Privy credentials (App ID, App Secret, Authorization ID, Authorization Private Key) in AgentCore Identity. * **Payment Manager** — The top-level resource that coordinates payment operations. * **Payment Connector** — Links the manager to your Privy credentials via the AgentCore CLI. * **Payment Instrument** — A Privy embedded crypto wallet that your agent uses to pay merchants on behalf of a user. * **Payment Session** — A time-bounded context with spending limits. The skill also wires payments into your agent with a framework-agnostic tool, so it works with Strands, LangGraph, OpenAI Agents SDK, or any Python framework. ### Skill prerequisites Before starting, make sure you have: * **AWS account** with credentials configured (`aws configure`). * **An AWS Region where AgentCore Payments is available** — `us-east-1`, `us-west-2`, `eu-central-1`, or `ap-southeast-2`. * **Node.js 20+** installed (the skill installs the AgentCore CLI automatically). * **A dedicated Privy app and authorization key** — Have your App ID, App Secret, Authorization ID, and Authorization Private Key (with the `wallet-auth:` prefix stripped) ready. See the [Manual setup prerequisites](#prerequisites) below for how to obtain these. * **An agent that accesses a paid endpoint** — The skill enables your agent to pay for x402-protected APIs. For testing, use the sandbox endpoint `https://sandbox.node4all.com/v1/x402-test`. * **The [Agent Toolkit for AWS](https://github.com/aws/agent-toolkit-for-aws) `aws-agents` plugin** installed in your AI coding agent. **Claude Code:** ``` /plugin marketplace add aws/agent-toolkit-for-aws /plugin install aws-agents@agent-toolkit-for-aws ``` **Codex:** The plugin is discovered automatically from the marketplace manifest. To add the marketplace, run: ``` codex plugin marketplace add aws/agent-toolkit-for-aws ``` ### Invoke the skill The payments skill is part of the `agents-build` skill in the [Agent Toolkit for AWS](https://github.com/aws/agent-toolkit-for-aws). To trigger it, describe your intent in your AI coding agent. For example: * "Add payments to my agent using `agents-build` skill in `aws-agents` plugin" * "Set up microtransactions for my agent using `agents-build` skill in `aws-agents` plugin" * "I need to handle 402 Payment Required responses using `agents-build` skill in `aws-agents` plugin" * "Wire my agent to pay for x402-protected APIs using `agents-build` skill in `aws-agents` plugin" The skill detects payment-related intent and loads the payments workflow automatically. ### What the skill does The skill runs an automated process that provisions your payment infrastructure end-to-end. It runs most steps automatically and pauses twice for your input: 1. Verifies or installs the AgentCore CLI and sets up the project. 2. Creates the payment manager. 3. **Pauses** — Run `agentcore add payment-connector` to enter your Privy provider secrets (App ID, App Secret, Authorization ID, and Authorization Private Key). 4. Deploys resources to your AWS account (`agentcore deploy -y`). 5. Wires a framework-agnostic payment tool (`x402_payment_tool.py`) into your agent. 6. Creates a per-user Privy embedded wallet (instrument) and a budget-bounded session via the SDK. 7. **Pauses** — Authorize the wallet (delegation) through the Privy wallet hub and fund it with testnet USDC from the [Circle faucet](https://faucet.circle.com/). 8. Sets environment variables and runs a test payment against a paid endpoint. Before running the connector command, obtain your **Stripe Privy** credentials from the [Privy dashboard](https://dashboard.privy.io/): App ID, App Secret, Authorization ID, and Authorization Private Key (with the `wallet-auth:` prefix stripped). A successful run shows the agent calling `x402_fetch`, detecting a `402`, settling payment via the AgentCore SDK, and the retry returning `200` with paid content. ## Manual setup This section walks through provisioning each resource manually with the AWS SDK (boto3). Follow these steps if you are not using the AgentCore Payments skill described above. Complete them in order, from prerequisites through creating a payment session. ### Prerequisites * **AWS account with AgentCore Payments access** — Install and configure the AWS CLI v2 and Python 3.10+ with `boto3`. Verify your credentials with `aws sts get-caller-identity`. AgentCore Payments is available in `us-east-1`, `us-west-2`, `eu-central-1`, and `ap-southeast-2`. * **A dedicated Privy app** — Create a developer account at [dashboard.privy.io](https://dashboard.privy.io/) and create a **dedicated** Privy app for AgentCore. Do not reuse an app that serves other purposes; this keeps credential scope and auditing clean. Copy the **App ID** and **App Secret** from the app settings. * **A Privy authorization key** — In your Privy app, go to **Wallet Infrastructure > Authorization > New Key** to generate a P-256 key pair. This key is what AgentCore uses to sign wallet operations. Note the **Authorization ID** (signer ID) shown alongside the key. Privy prefixes the generated private key with `wallet-auth:`. AgentCore Payments does **not** accept this prefix. Strip it and store only the raw base64 content after the prefix. ``` Privy gives you: wallet-auth:MBMGByqGSM49AgEGCC... Store only: MBMGByqGSM49AgEGCC... ``` After this, there are four Privy values to hand to AgentCore: **App ID**, **App Secret**, **Authorization ID**, and the **Authorization Private Key** (prefix stripped). For full provider detail, see the AWS [Prerequisites for AgentCore payments](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/payments-prerequisites.html). ### Step 1: Store Privy credentials in AgentCore Identity Create a `PaymentCredentialProvider` so AgentCore can store your Privy credentials securely. Resource names must be lowercase alphanumeric with hyphens only. ```python theme={"system"} import boto3 cp = boto3.client("bedrock-agentcore-control", region_name="us-west-2") cred = cp.create_payment_credential_provider( name="agentcore-privy-creds", credentialProviderVendor="StripePrivy", providerConfigurationInput={ "stripePrivyConfiguration": { "appId": PRIVY_APP_ID, "appSecret": PRIVY_APP_SECRET, "authorizationId": PRIVY_AUTH_ID, "authorizationPrivateKey": PRIVY_AUTH_PRIVATE_KEY, # wallet-auth: prefix stripped } }, ) credential_provider_arn = cred["credentialProviderArn"] ``` Never embed Privy credentials in agent source code or paste them in chat. Load them from environment variables (`source .env.payments`) at setup time. Once stored in AgentCore Identity, restrict the underlying Secrets Manager secret to the AgentCore Payments service role only. ### Step 2: Create the Payment Manager The Payment Manager needs an IAM role that trusts `bedrock-agentcore.amazonaws.com` and grants `GetWorkloadAccessToken`, `GetResourcePaymentToken`, and `secretsmanager:GetSecretValue` (see the [IAM roles](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/payments-iam-roles.html) page for the exact policy). ```python theme={"system"} import uuid, time mgr = cp.create_payment_manager( name="agentcoreprivy", description="Payment manager for Privy wallets", authorizerType="AWS_IAM", roleArn=role_arn, clientToken=str(uuid.uuid4()), ) payment_manager_id = mgr["paymentManagerId"] # used for control-plane ops payment_manager_arn = mgr["paymentManagerArn"] # used for data-plane ops # Wait for READY before creating a connector while cp.get_payment_manager(paymentManagerId=payment_manager_id)["status"] != "READY": time.sleep(5) ``` ### Step 3: Create the Payment Connector Bind the manager to Privy by referencing the credential provider from Step 1. ```python theme={"system"} conn = cp.create_payment_connector( paymentManagerId=payment_manager_id, name="agentcoreprivyconnector", description="Privy connector", type="StripePrivy", credentialProviderConfigurations=[ {"stripePrivy": {"credentialProviderArn": credential_provider_arn}} ], clientToken=str(uuid.uuid4()), ) connector_id = conn["paymentConnectorId"] ``` ### Step 4: Create the Payment Instrument (wallet) Create the user's embedded wallet through AgentCore. The end user's email is linked here: it is the account they will log into when granting the agent permission to spend. ```python theme={"system"} dp = boto3.client("bedrock-agentcore", region_name="us-west-2") instr = dp.create_payment_instrument( paymentManagerArn=payment_manager_arn, paymentConnectorId=connector_id, userId="agentcore-user", paymentInstrumentType="EMBEDDED_CRYPTO_WALLET", paymentInstrumentDetails={ "embeddedCryptoWallet": { "network": "ETHEREUM", # ETHEREUM covers Base + Base Sepolia "linkedAccounts": [{"email": {"emailAddress": END_USER_EMAIL}}], } }, clientToken=str(uuid.uuid4()), ) instrument = instr.get("paymentInstrument", instr) payment_instrument_id = instrument["paymentInstrumentId"] wallet_address = instrument["paymentInstrumentDetails"]["embeddedCryptoWallet"]["walletAddress"] ``` A new instrument starts with **0 USDC** and the agent has **no permission to spend** until the end user grants it. Funding and delegation come next, in that order. ### Step 5: Grant the agent permission (delegation) This is a required step. The agent's authorization key must be added as a signer on the end user's embedded wallet, and only the user can approve that. 1. Stand up a frontend using the [Privy AgentCore SDK](https://github.com/privy-io/aws-agentcore-sdk), which provides a reference wallet hub for login, agent connection, and on-ramping. 2. Have the end user log in with the email linked in Step 4 (`END_USER_EMAIL`). 3. The user approves delegation for the wallet, authorizing the agent to sign within AgentCore's controls. If delegation is skipped, `ProcessPayment` fails with a "Delegation not completed" error. The agent acts as an authorized signer only: the user retains ownership and can revoke access at any time. ### Step 6: Fund the wallet Once delegation is approved, fund the wallet with USDC. * **Testnet:** Get free testnet USDC on Base Sepolia from [Circle's faucet](https://faucet.circle.com/) and send it to the wallet address from Step 4. * **Mainnet:** The end user funds the wallet through the Privy wallet hub: crypto-to-crypto transfer or supported fiat methods (cards, Apple Pay, Google Pay, ACH; availability varies by region). ### Step 7: Create a Payment Session and enable payments Create a session to bound spending, then wire payment handling into your agent. ```python theme={"system"} session = dp.create_payment_session( paymentManagerArn=payment_manager_arn, userId="agentcore-user", expiryTimeInMinutes=60, limits={"maxSpendAmount": {"value": "5.00", "currency": "USD"}}, ) payment_session_id = session["paymentSession"]["paymentSessionId"] ``` The agent uses an x402-aware fetch tool. When it hits a `402`, the tool reads the challenge, calls `ProcessPayment`, and retries with the signed proof. AgentCore checks the session limit, signs through Privy, and returns the proof. ```python theme={"system"} import os, json, base64, httpx, boto3 dp = boto3.client("bedrock-agentcore", region_name=os.environ["AWS_REGION"]) def x402_fetch(url: str, method: str = "GET") -> str: """Fetch a URL, paying via AgentCore + Privy if it returns 402.""" resp = httpx.request(method, url, timeout=30) if resp.status_code != 402: return json.dumps({"status_code": resp.status_code, "body": resp.text}) # Extract the x402 challenge (body for v1, payment-required header otherwise) challenge = None try: body = resp.json() if "x402Version" in body and "accepts" in body: challenge = body except Exception: pass if not challenge and (h := resp.headers.get("payment-required")): challenge = json.loads(base64.b64decode(h)) if not challenge: return json.dumps({"error": "402 without an x402 challenge"}) # Pick the accept that matches your wallet's network family. # This recipe uses an ETHEREUM (EVM) wallet, so prefer eip155/base; fall back to the first. accepts_list = challenge["accepts"] accepts = next( (a for a in accepts_list if str(a.get("network", "")).lower().startswith(("eip155", "base", "ethereum"))), accepts_list[0], ) # AgentCore signs the payment through the Privy wallet. # Forward the FULL accept object as the payload — it includes `extra` # (e.g. {"name": "USDC"}), which is required for EVM payments. pay = dp.process_payment( paymentManagerArn=os.environ["PAYMENT_MANAGER_ARN"], paymentInstrumentId=os.environ["PAYMENT_INSTRUMENT_ID"], paymentSessionId=os.environ["PAYMENT_SESSION_ID"], userId=os.environ["PAYMENT_USER_ID"], paymentType="CRYPTO_X402", paymentInput={"cryptoX402": { "version": str(challenge.get("x402Version", "1")), "payload": accepts, }}, ) out = pay["paymentOutput"]["cryptoX402"] version = int(challenge.get("x402Version", 1)) # Build the retry proof to match the challenge version. if version >= 2: proof = { "x402Version": 2, "resource": challenge.get("resource"), "accepted": accepts, "extensions": challenge.get("extensions", {}), "payload": out["payload"], } header = "PAYMENT-SIGNATURE" else: proof = { "x402Version": 1, "scheme": accepts.get("scheme", "exact"), "network": accepts["network"], "payload": out["payload"], } header = "X-PAYMENT" token = base64.b64encode(json.dumps(proof, separators=(",", ":")).encode()).decode() # Retry with a FRESH client — reusing cookies from the 402 can break the retry with httpx.Client(verify=True) as client: retry = client.request(method, url, headers={header: token}, timeout=30) return json.dumps({ "status_code": retry.status_code, "body": retry.text, "payment_made": 200 <= retry.status_code < 300, }) ``` Register `x402_fetch` as a tool in your agent framework (Strands, LangGraph, OpenAI Agents SDK, etc.) and the agent will pay for `402` resources autonomously. Use a **fresh** HTTP client for the retry. Some merchants set cookies on the 402 response that cause the paid retry to fail if reused. Also build the proof to match the challenge's `x402Version`: a v2 merchant silently ignores a v1 `X-PAYMENT` header and re-issues the same 402. ## Spend controls AgentCore enforces spending at the **Payment Session** level. There are no per-recipient allowlists or separate budget objects. | Field | Description | | --------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | `maxSpendAmount` | The ceiling for the session | | `currency` | The session currency | | `expiryTimeInMinutes` | Keep sessions short-lived (60 minutes or less); create a fresh session per user interaction rather than reusing a long-lived one | When a payment would exceed the limit or the session has expired, AgentCore denies it before signing. For recipient-level controls, attach a Privy [policy](/controls/policies/overview) to the Payment Instrument's wallet. Policies are enforced in the enclave at signing time, so they apply regardless of which session or agent initiated the payment. See [sanctions screening for x402 payments](/recipes/agent-integrations/x402-sanctions-screening) for a recipient denylist. Policies are enforced per wallet, so the policy ID must be set when the Payment Instrument is created. When AgentCore creates wallets on a developer's behalf, pass the policy ID alongside the app ID, app secret, and authorization key. ## Observability AgentCore Payments integrates with Amazon CloudWatch. Once enabled, every data plane API call (`ProcessPayment`, `CreatePaymentInstrument`, etc.) emits metrics, logs, and X-Ray spans automatically. ### Enable it 1. Create a CloudWatch log group (e.g. `/bedrock-agentcore/payments/my-logs`). 2. Grant your IAM principal vended-log and X-Ray delivery permissions (`logs:CreateDelivery`, `xray:PutTraceSegments`, `bedrock-agentcore:AllowVendedLogDeliveryForResource`, and related). 3. On the Payment Manager details page, under **Log deliveries and tracing**, point log delivery at your log group and enable traces. ### Vended metrics Key payment metrics published to CloudWatch: `PaymentRequestCount`, `PaymentSuccessCount`, `PaymentFailureCount`, `PaymentLatency`, and `SpendAmount`, plus per-operation `OperationSuccess`, `OperationFailure`, `OperationLatency`, `Throttles`, `UserErrors`, and `ActiveSessions`. Dimensions: `Operation`, `PaymentManagerId`, `PaymentConnectorId`, `AgentName`, and `Currency`. Alarm on `PaymentFailureCount` (misconfiguration/abuse signal) and `PaymentLatency` against your SLA. ### Vended spans One span per API call, named `Bedrock.AgentCore.Payments.`, viewable in X-Ray with payment-specific attributes: `spend_amount`, `spend_currency`, `merchant` (payTo), `session_remaining_budget`, `total_budget_amount`, and `token_fetch_latency_ms` — plus resource IDs and standard AWS attributes. For the full reference, see the [AgentCore Payments observability docs](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/payments-observability.html). ## Supported networks AgentCore Payments with Privy settles in **USDC**. For instrument creation, choose a network family; the x402 challenge then specifies the exact chain. | Network (instrument) | Chains | Asset | Type | | -------------------- | ---------------------------------------------- | ----- | ------- | | `ETHEREUM` | Base Sepolia (`base-sepolia` / `eip155:84532`) | USDC | Testnet | | `ETHEREUM` | Base (`eip155:8453`), Ethereum (`eip155:1`) | USDC | Mainnet | | `SOLANA` | Solana Devnet (`solana-devnet`) | USDC | Testnet | | `SOLANA` | Solana Mainnet | USDC | Mainnet | For development, start with `ETHEREUM` / Base Sepolia and free testnet USDC from [Circle's faucet](https://faucet.circle.com/). ## Testing * Use Base Sepolia for development. * Fund the wallet with testnet USDC from [Circle's faucet](https://faucet.circle.com/). * Point the agent at an x402-enabled test endpoint and confirm it pays and returns content. Browse live x402 services at [x402scan.com](https://x402scan.com/). If the agent loops on `402` after a successful `ProcessPayment`, the most common causes are cookie contamination on the retry, an x402 version/header mismatch, or an expired proof (\~60s validity window). ## Security considerations * **User ownership.** AgentCore is an authorized signer, not the wallet owner. The user grants delegation and can revoke it at any time. The user can also withdraw funds from the wallet at any time. * **Credential isolation.** Privy credentials live in AgentCore Identity / Secrets Manager. Restrict the secret to the AgentCore Payments service role only. * **Session limits.** Per-session `maxSpendAmount` and short expiry bound runaway spending. * **Recipient screening.** Session limits bound how much an agent can spend, not who it can pay. Use a wallet policy to deny payments to specific recipients, such as [sanctioned addresses](/recipes/agent-integrations/x402-sanctions-screening). * **HTTPS only.** Reject non-HTTPS targets and block private/internal IP ranges to prevent SSRF. * **Audit.** AgentCore Observability and CloudTrail capture every `ProcessPayment` call; alarm on failed-payment spikes. * **Rotate** the App Secret and authorization key on a regular schedule (e.g., every 90 days). ## Further reading * [AWS Bedrock AgentCore Payments](https://docs.aws.amazon.com/bedrock-agentcore/latest/devguide/payments.html) * [AgentCore payments samples: tutorials and end-to-end use-case patterns](https://github.com/awslabs/agentcore-samples/tree/main/01-features/08-agents-that-transact) * [AgentCore Payments launch blog](https://aws.amazon.com/blogs/machine-learning/agents-that-transact-introducing-amazon-bedrock-agentcore-payments-built-with-coinbase-and-stripe/) * [Privy AgentCore SDK](https://github.com/privy-io/aws-agentcore-sdk) * [Privy authorization keys](https://docs.privy.io/basics/get-started/organization) # Agentic wallets Source: https://docs.privy.io/recipes/agent-integrations/agentic-wallets Privy enables developers to create wallets for agents and autonomous systems that can execute onchain transactions independently while maintaining strict policy controls and security guardrails. Agentic wallets are designed for use cases where autonomous systems need to make decisions and execute transactions without human intervention, such as trading agents, portfolio managers, automated market makers, and autonomous service providers. At a high-level, this recipe will teach developers how to set up wallets that agents can control, implement policies to constrain agent behavior, and enable secure autonomous transaction execution. Privy supports two primary models for agentic wallets depending on your custody and control requirements: **Model 1: Agent-controlled, developer-owned wallets** * Your application backend controls the wallet via authorization keys * Suitable for fully autonomous agents where users delegate complete control * Agent can execute transactions within policy constraints without user approval **Model 2: User-owned wallets with agent signers** * Users maintain ownership while granting limited permissions to agents * Agent operates as an [additional signer](/controls/authorization-keys/owners/overview#signers) with scoped policies * Users retain ultimate control and can revoke agent access at any time For this recipe, we'll focus on **Model 1** for fully autonomous agents. For Model 2, see the [signers guide](/wallets/using-wallets/signers/overview). Set up authorization keys that your application backend will use to control agent wallets. To start, create [authorization keys](/controls/authorization-keys/owners/types#authorization-keys) in the Privy Dashboard and securely store the corresponding private keys. Your backend will use these keys to sign requests to Privy's API on behalf of agents. For enhanced security, register the authorization keys in a [key quorum](/controls/authorization-keys/owners/types#key-quorums). This enables multi-party approval for critical actions like updating policies or exporting wallets. Create authorization keys in your Privy Dashboard. Set up a key quorum for enhanced security. Policies are critical as they define the boundaries within which your agents can operate. Well-designed policies prevent agents from taking unintended or harmful actions while allowing them to function effectively. Common policy constraints for agents include: * **Transfer limits**: Maximum amounts per transaction or within time windows * **Allowlisted contracts**: Restrict agents to interact only with approved protocols * **Recipient restrictions**: Limit where funds can be sent * **Time-based controls**: Define when agents can operate * **Action-specific rules**: Control parameters for swaps, trades, or other operations Follow the guide below to create policies for your desired use case. After creating your policy, save the `id` to assign the policy to the wallet(s) you create later. Learn how to construct policies with Privy's policy language. Create policies for your agents. Create a wallet owned by your authorization key, with the policies you previously defined attached. Make sure to: * Set the `owner_id` of the wallet to the `id` of the authorization key you created earlier * Set the `policy_ids` array of the wallet to a singleton containing the `id` of the policy you created earlier You can reuse the same policy ID to provision additional wallets so every agent in your fleet is subject to these controls. Create a wallet. You can now send transactions, sign transactions, or sign messages with Privy's API. Follow the guide below to send a transaction. Execute transactions on Ethereum and EVM chains. Execute transactions on Solana. Execute transactions on other supported chains. Implement monitoring and logging to track your agent's actions and ensure it operates as intended. Privy provides webhooks for transaction events and balance changes. Monitor transaction status and completion. Track deposits and withdrawals. ## Learn more Use Privy wallets with OpenClaw agents. Set up your agent to pay for APIs and content. Set up your agent to trade on Hyperliquid. Automatically sponsor gas fees for agent transactions. # Privy Agent on auto.exchange Source: https://docs.privy.io/recipes/agent-integrations/auto-exchange-privy-agent Ask questions about Privy — wallets, authentication, signing, policies — and get answers grounded in the full Privy documentation. The agent runs on GPT-4o Mini with 45 structured knowledge files covering the complete Privy docs. ## Overview The [Privy Agent](https://auto.exchange/agent/privy-expert) is an agent on [auto.exchange](https://auto.exchange) that answers Privy integration questions. It has the complete Privy documentation (from `docs.privy.io/llms-full.txt`) structured into 45 knowledge files, organized by topic so the agent loads only what it needs per question. **Key stats:** * **25 benchmark tests** across auth, signing, wallets, users, policies, and advanced features * \*\*$0.02 avg per question** (vs $0.14 for Claude Opus + MCP docs) * **96.9% accuracy** on the benchmark suite ## Quick start ### Option A: MCP (recommended for Claude Code, Cursor, etc.) Add to your MCP config: ```json theme={"system"} { "auto.exchange": { "url": "https://api.auto.exchange/mcp", "headers": { "Authorization": "Bearer YOUR_API_KEY" } } } ``` Get an API key: ```bash theme={"system"} npx auto-exchange login ``` Fund your account with USDC at [auto.exchange/account](https://auto.exchange/account), then use the `call_agent` tool: ``` call_agent with slug "privy-expert" and prompt "How do I verify a JWT in Node.js?" ``` ### Option B: REST API ```bash theme={"system"} curl -X POST https://api.auto.exchange/agents/ab7527c0-6099-4145-9d8b-47f09dab9390/run \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"prompt": "How do I create a server wallet and sign a transaction?"}' ``` Response: ```json theme={"system"} { "text": "To create a server wallet...", "tokens_used": 2028, "cost": "0.002028", "session_id": "uuid" } ``` ### Option C: Web chat Visit [auto.exchange/agent/privy-expert/chat](https://auto.exchange/agent/privy-expert/chat) and chat directly in the browser. Multi-turn conversations are supported. ## What it covers The agent has the complete Privy documentation organized into focused knowledge files: | Topic | Coverage | | ------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | **Authentication** | JWT verification, access tokens, OAuth (Google, Apple, Discord, etc.), passkeys, email/SMS OTP, Telegram, Farcaster, MFA/TOTP | | **Ethereum signing** | `eth_sendTransaction`, `eth_signTransaction`, `secp256k1_sign`, `raw_sign`, `personal_sign`, `eth_signTypedData_v4`, `wallet_sendCalls`, EIP-7702 | | **Solana** | `signAndSendTransaction`, `signTransaction`, `signMessage`, provider setup, `@solana/kit` integration | | **Spark/Bitcoin** | Lightning invoices, static deposit addresses, balance, transfers | | **Wallet management** | Create (single + batch), export, import, server wallets, embedded wallets, smart wallets, HD wallets | | **Users** | Create, lookup (by email/phone/wallet/social), migrate, batch import, custom metadata, allowlist/denylist | | **Policies and security** | Authorization keys, key quorums, policy rules, condition sets, intents, aggregations | | **React SDK** | `PrivyProvider` config, `usePrivy` hooks, login methods, wallet UI, global wallets, ConnectKit/RainbowKit | | **Advanced** | Gas sponsorship, fiat onramp/offramp, agentic wallets, transaction management, webhooks | ## Multi-turn conversations Pass `session_id` to maintain context across questions: ```bash theme={"system"} # First question curl -X POST https://api.auto.exchange/agents/ab7527c0-6099-4145-9d8b-47f09dab9390/run \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"prompt": "How do I set up Privy auth?", "session_id": "new"}' # Response includes session_id: "abc-123" # Follow-up curl -X POST https://api.auto.exchange/agents/ab7527c0-6099-4145-9d8b-47f09dab9390/run \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Content-Type: application/json" \ -d '{"prompt": "Now add Google OAuth to that setup", "session_id": "abc-123"}' ``` ## Pricing $0.001 per 1,000 tokens (at cost — no markup). A typical question costs $0.005–\$0.03 depending on complexity. ## Benchmarks 25 tests scored against Claude Opus + official Privy MCP docs as baseline: | Test | Agent | Baseline | Agent cost | Baseline cost | | ---------------------- | ----- | -------- | ---------- | ------------- | | JWT verification | 3/3 | 3/3 | \$0.005 | \$0.14 | | Embedded wallet config | 3/3 | 2/3 | \$0.024 | \$0.14 | | Solana signing | 3/3 | 3/3 | \$0.032 | \$0.14 | | Gas sponsorship | 3/3 | 3/3 | \$0.030 | \$0.14 | | Code review | 5/5 | 3/3 | \$0.011 | \$0.14 | | Agentic wallets | 3/3 | 3/3 | \$0.071 | \$0.14 | [Full 25-test benchmark](https://auto.exchange/agent/privy-expert) ## Links * **Agent page**: [auto.exchange/agent/privy-expert](https://auto.exchange/agent/privy-expert) * **Web chat**: [auto.exchange/agent/privy-expert/chat](https://auto.exchange/agent/privy-expert/chat) * **API docs**: [auto.exchange/docs](https://auto.exchange/docs) * **Get an API key**: `npx auto-exchange login` # Bankr Twitter bot guide Source: https://docs.privy.io/recipes/agent-integrations/bankr-bot-guide This guide will walk through building a Twitter bot on top of the Clanker protocol similar to **[Bankr](https://bankr.bot/)**, **[Dealr](https://dealr.fun/)**, **[Beamr](https://beamr.xyz/)**, and others. This bot will use an LLM to interpret user requests, Privy wallets to manage EVM accounts, and the Clanker protocol to deploy tokens on Base. ### Resources Learn how to deploy tokens on Base using the Clanker API. Privy wallets for secure EVM wallet management. Reference for building bots on Twitter. ## Set up your Twitter bot To interact with users, you'll need a Twitter developer account and a bot. Learn more [here](https://developer.twitter.com/en/docs/twitter-api/getting-started/getting-access-to-the-twitter-api). 1. Go to the [Twitter Developer Portal](https://developer.twitter.com/en/portal/dashboard). 2. Create a new project and app. 3. Generate API keys and access tokens. 4. Safely store your credentials. 1. Use a library like `twitter-api-v2` to interact with Twitter. 2. Install the library: ```bash npm theme={"system"} npm install twitter-api-v2 ``` ```bash pnpm theme={"system"} pnpm install twitter-api-v2 ``` ```bash yarn theme={"system"} yarn add twitter-api-v2 ``` 3. Create a basic Twitter client setup: ```typescript theme={"system"} import { TwitterApi } from 'twitter-api-v2'; // Initialize the Twitter client with your credentials const client = new TwitterApi({ appKey: process.env.TWITTER_API_KEY!, appSecret: process.env.TWITTER_API_SECRET!, accessToken: process.env.TWITTER_ACCESS_TOKEN!, accessSecret: process.env.TWITTER_ACCESS_SECRET!, }); // Get the read/write client const rwClient = client.readWrite; ``` 4. Example Node.js code to poll for mentions and robustly parse commands: ```json theme={"system"} { "data": { "id": "1234567890123456789", "text": "@bankr_bot send @elonmusk 1 eth from my wallet", "author_id": "09876543210987654321", "entities": { "mentions": [ { "start": 0, "end": 10, "username": "bankr_bot" }, { "start": 16, "end": 25, "username": "elonmusk" } ] } }, "includes": { "users": [ { "id": "09876543210987654321", "username": "sender_username", "name": "Sender Name" }, { "id": "12345678909876543210", "username": "bankr_bot", "name": "Bankr Bot" }, { "id": "11223344556677889900", "username": "elonmusk", "name": "Elon Musk" } ] } } ``` ```typescript theme={"system"} async function pollMentions() { let sinceId: string | undefined = undefined; while (true) { const mentions = await rwClient.v2.userMentionTimeline('YOUR_BOT_USER_ID', { since_id: sinceId, expansions: ['author_id', 'entities.mentions.username'], 'user.fields': ['username', 'name'], max_results: 5, }); for (const tweet of mentions.data?.data || []) { const parsed = processTweet(tweet, mentions); if (parsed) { // Call your LLM or transaction logic here // e.g. handleCommand(parsed) } sinceId = tweet.id; } await new Promise(res => setTimeout(res, 10000)); // poll every 10s } } function processTweet(tweet, mentionsResponse) { // Extract basic tweet information const text = tweet.text; const authorId = tweet.author_id; const tweetId = tweet.id; const mentions = tweet.entities?.mentions || []; // Extract all mentioned users const mentionedUsers = mentions.map(mention => { const username = mention.username; const user = mentionsResponse.includes?.users.find(u => u.username === username); return { username, userId: user?.id, isBot: username === 'bankr_bot' // Identify if this is our bot }; }); // Pass the structured data to the LLM for intent detection return { text, authorId, tweetId, mentions: mentionedUsers, // Additional context can be added here }; } ``` * This code polls for new mentions, parses the tweet for sender, recipient, amount, and currency, and passes the result to your LLM or transaction logic. * For production, consider using the [filtered stream](https://github.com/PLhery/node-twitter-api-v2/blob/master/doc/streaming.md) for real-time events. ## Set up Privy wallets Privy wallets let you create and control EVM wallets programmatically. Learn more about [getting started with wallets](/basics/nodeJS/installation). ## Using wallets In our example application, we will build two basic interactions with Privy wallets: * **Create a wallet** * **Get a user's wallet** Using these core building blocks, we can allow our bot to seamlessly and securely create and manage wallets for users. This function creates a new wallet for a user and saves the wallet ID to the database. ```ts @privy-io/node {skip-check} theme={"system"} /** * Creates a new wallet for a user * @param userId - The Twitter user ID * @returns The wallet object with id, address, and other properties */ async function createUserWallet(userId) { // Create a new wallet for the user const wallet = await privy.wallets().create({chain_type: 'ethereum'}); // EXAMPLE: Save the wallet ID to the database to save the mapping between the user and their wallet await db.wallets.set(userId, wallet.id); return wallet; } ``` This function checks if a user has a wallet and returns it if it exists. ```ts @privy-io/node {skip-check} theme={"system"} /** * Gets a user's wallet if it exists * @param userId - The Twitter user ID * @returns The wallet object or null if no wallet exists */ async function getUserWallet(userId) { // EXAMPLE: Check if user already has a wallet const walletId = await db.wallets.get(userId); // If wallet exists, retrieve and return the wallet if (walletId) { const wallet = await privy.wallets().get(walletId); return wallet; } return null; } ``` A higher level function that uses the `getUserWallet` and `createUserWallet` functions to get or create a wallet for a user. ```typescript {skip-check} theme={"system"} /** * Gets a user's wallet or creates one if they don't have one * @param userId - The Twitter user ID * @returns The wallet object with id, address, and other properties */ async function getOrCreateUserWallet(userId) { // Try to get existing wallet const existingWallet = await getUserWallet(userId); // Return existing wallet if found if (existingWallet) { return existingWallet; } // Create a new wallet if none exists return createUserWallet(userId); } // Example usage: const wallet = await getOrCreateUserWallet(parsed.authorId); ``` ## Integrate LLM for intent detection Use an LLM to interpret user messages and decide what action to take. Treat the LLM as a black box that receives a prompt and returns a structured intent. **Prompt:** ```text theme={"system"} User: Launch a token called $CAT with 1B supply System: Extract the intent and parameters for a token launch on Base. Output format: { action: string, params: object } ``` **LLM Response:** ```json theme={"system"} { "action": "launch_token", "params": { "name": "CAT", "symbol": "$CAT", "supply": "1000000000" } } ``` ```typescript theme={"system"} // Pseudocode: send user message to LLM and parse response const llmResponse = await llm.query({ prompt: userMessage }); if (llmResponse.action === 'launch_token') { // Proceed to Clanker integration } ``` *** ## Getting set up with Clanker API To deploy tokens via the Clanker API, you first need to obtain an API key. 1. Visit the [Clanker API documentation](https://clanker.gitbook.io/clanker-documentation/developers/api/deploy-a-token). 2. Follow the instructions or contact the Clanker team via their documentation or [contact page](https://clanker.gitbook.io/clanker-documentation/references/contact) to request API access. 3. Once approved, you'll receive an `x-api-key` to use in your API requests. Once you have your API key, you can use it to deploy tokens via the Clanker API. See the next section for a full deployment code example. *** ## Example Interactions ### Deploy a token Let users deploy tokens on Base by simply messaging the bot. The LLM interprets the intent, and the bot handles wallet lookup/creation and token deployment. **Prompt:** ```text theme={"system"} User: create a new $Example token System: Extract the intent and parameters for a token launch on Base. Output format: { action: string, params: object } ``` **LLM Response:** ```json theme={"system"} { "action": "launch_token", "params": { "name": "Example", "symbol": "$Example" } } ``` ```typescript theme={"system"} // Example incoming tweet const tweet = { text: "@YOUR_BOT_HANDLE create a new $Example token", author_id: "1234567890", id: "9876543210", // ...other fields }; // Remove bot mention to get user command const userMessage = tweet.text.replace(/@YOUR_BOT_HANDLE\s*/i, "").trim(); ``` ```typescript theme={"system"} // Send the user message to your LLM const llmResponse = await llm.query({ prompt: userMessage }); // Example LLM response: // { // action: "launch_token", // params: { name: "Example", symbol: "$Example" } // } if (llmResponse.action !== 'launch_token') { throw new Error('Not a token launch command'); } const { name, symbol } = llmResponse.params; ``` ```typescript theme={"system"} // Get or create a wallet for the user const wallet = await getOrCreateUserWallet(tweet.author_id); // wallet.address will be used as the requestorAddress ``` ```typescript theme={"system"} import axios from 'axios'; import crypto from 'crypto'; const apiKey = process.env.CLANKER_API_KEY; const requestKey = crypto.randomBytes(16).toString('hex'); const payload = { name, symbol, image: 'https://example.com/token.png', // Optional: add your image requestorAddress: wallet.address, requestKey, // ...other optional params }; const response = await axios.post('https://www.clanker.world/api/tokens/deploy', payload, { headers: { 'x-api-key': apiKey, 'Content-Type': 'application/json' } }); const tokenInfo = response.data; ``` ```typescript theme={"system"} // Send a DM or reply to the user with the token address await twitterClient.sendDM({ userId: tweet.author_id, text: `Token deployed! Address: ${tokenInfo.address}` }); ``` ### Send tokens to another Twitter user with LLM Let users send tokens (e.g., ETH on Base) to other Twitter users by simply messaging the bot. The LLM interprets the intent, and the bot handles wallet lookup/creation and transaction sending. **Prompt:** ```text theme={"system"} User: Hey bot, send 0.01 ETH from my wallet to @privy_io on twitter System: Extract the intent and parameters for a token transfer on Base. Output format: { action: string, params: object } ``` **LLM Response:** ```json theme={"system"} { "action": "send_token", "params": { "amount": "0.01", "token": "ETH", "recipient": "@privy_io" } } ``` ```typescript theme={"system"} // 1. Get sender's Twitter ID from the parsed tweet const senderTwitterId = parsed.authorId; // 2. Get recipient's Twitter handle from LLM response (e.g., '@privy_io') const recipientHandle = llmResponse.params.recipient.replace('@', ''); // 3. Look up recipient's Twitter ID from parsed mentions const recipientMention = parsed.mentions.find(m => m.username === recipientHandle); if (!recipientMention || !recipientMention.userId) { throw new Error('Recipient not found in tweet mentions'); } const recipientTwitterId = recipientMention.userId; // 4. Get or create recipient's wallet const recipientWallet = await getOrCreateUserWallet(recipientTwitterId); // 5. Get or create sender's wallet const senderWallet = await getOrCreateUserWallet(senderTwitterId); ``` ```ts @privy-io/node theme={"system"} import { parseEther } from 'viem'; // 1. Parse the amount to wei (ETH uses 18 decimals) const amountEth = llmResponse.params.amount; // e.g., '1' const amountWei = parseEther(amountEth); // '1000000000000000000' // 2. Prepare transaction details const transaction = { to: recipientWallet.address, // Recipient's EVM address value: amountWei, // Amount in wei chainId: 8453, // Base chain ID // (Optional: add gas, data, etc.) }; // 3. Send the transaction using Privy wallet const sendResult = await privy.wallets().ethereum().sendTransaction(senderWallet.id, { caip2: 'eip155:8453', // CAIP2 for Base params: { transaction, } }); ``` # Machine Payments Protocol (MPP) Source: https://docs.privy.io/recipes/agent-integrations/mpp Enable agents to pay for APIs and content using MPP, an open protocol for machine-to-machine payments over HTTP. Privy's server wallets provide the signing layer, while the `mppx` SDK handles the 402 payment flow automatically. ## What is MPP? [MPP](https://mpp.dev) (Machine Payments Protocol) is an open protocol for machine-to-machine payments over HTTP. When a resource requires payment, the server responds with `402 Payment Required` and payment details. The client signs a payment credential using the agent's wallet and retries the request. MPP supports multiple chains and payment assets—this guide uses [Tempo](https://tempo.xyz) with PathUSD as the settlement layer, but other networks and assets are supported by the protocol. ## Installation ```bash theme={"system"} npm install @privy-io/node mppx viem ``` * `@privy-io/node` provides server-side wallet creation and signing * `mppx` provides the MPP client that handles 402 payment flows * `viem` provides the account interface used by Privy's viem helper and `mppx` ## Creating a Privy-backed account MPP's `tempo.charge()` payment method expects a [viem Account](https://viem.sh/docs/accounts/local/toAccount) for signing. Use Privy's `createViemAccount` helper to create a viem account backed by a Privy wallet. ```typescript theme={"system"} import {PrivyClient} from '@privy-io/node'; import {createViemAccount} from '@privy-io/node/viem'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID!, appSecret: process.env.PRIVY_APP_SECRET! }); const account = createViemAccount(privy, { walletId: 'insert-wallet-id', address: '0x0000000000000000000000000000000000000000' }); ``` Use `@privy-io/node` v0.20.0 or later. Earlier versions required custom Tempo transaction serialization logic. ## Making MPP payments Pass the Privy-backed account to the MPP client's `tempo.charge()` method. The client automatically handles 402 responses, signs payment credentials, and retries requests. ### Using `mppx.fetch` ```typescript {skip-check} theme={"system"} import {PrivyClient} from '@privy-io/node'; import {createViemAccount} from '@privy-io/node/viem'; import {Mppx, tempo} from 'mppx/client'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID!, appSecret: process.env.PRIVY_APP_SECRET! }); async function makePayment(walletId: string, address: `0x${string}`, url: string) { const account = createViemAccount(privy, { walletId, address }); const mppx = Mppx.create({ polyfill: false, methods: [tempo.charge({account})] }); const response = await mppx.fetch(url); const data = await response.json(); return data; } ``` `mppx.fetch` is a drop-in replacement for `fetch`. When a server returns `402 Payment Required`, the client reads the payment requirements, signs a credential with the Privy wallet, and retries the request automatically. Use `tempo({account})` instead when the client should support both one-time charges and Tempo sessions. ### Using polyfill mode Your app can also polyfill the global `fetch` so all HTTP requests handle 402 challenges automatically: ```typescript {skip-check} theme={"system"} import {PrivyClient} from '@privy-io/node'; import {createViemAccount} from '@privy-io/node/viem'; import {Mppx, tempo} from 'mppx/client'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID!, appSecret: process.env.PRIVY_APP_SECRET! }); const account = createViemAccount(privy, { walletId: 'insert-wallet-id', address: '0x0000000000000000000000000000000000000000' }); Mppx.create({ polyfill: true, methods: [tempo.charge({account})] }); // All fetch calls now handle 402 responses automatically const response = await fetch('https://api.example.com/weather'); ``` ## How it works 1. **Agent requests content**: Your app calls `mppx.fetch()` or the polyfilled `fetch()` 2. **Server responds 402**: Returns payment requirements (amount, currency, recipient) 3. **MPP client signs credential**: Uses the Privy-backed account to sign a payment credential 4. **Retry with credential**: Request repeats with the signed credential attached 5. **Server verifies and settles**: Verifies the credential and settles payment on-chain (Tempo in this guide) 6. **Server delivers**: Returns content with `200 OK` ## Creating an MPP-enabled service The `mppx/nextjs` package provides middleware for adding payment requirements to API routes: ```typescript theme={"system"} // app/api/weather/route.ts import {Mppx, tempo} from 'mppx/nextjs'; // MPP supports multiple chains and assets. This example uses Tempo with PathUSD. // See https://mpp.dev for other supported networks and payment assets. const mppx = Mppx.create({ methods: [ tempo.charge({ currency: '0x20c0000000000000000000000000000000000000', // PathUSD recipient: process.env.MPP_RECIPIENT as `0x${string}` }) ], secretKey: process.env.MPP_SECRET_KEY! }); export const GET = mppx.charge({amount: '0.1'})(() => Response.json({ temperature: 72, condition: 'Sunny', location: 'San Francisco, CA' }) ); ``` When a client calls this route without a payment credential, the middleware responds with `402 Payment Required`. With a valid credential, it verifies the payment, settles on Tempo, and returns the data. ## Full example ```typescript theme={"system"} import {PrivyClient} from '@privy-io/node'; import {createViemAccount} from '@privy-io/node/viem'; import {Mppx, tempo} from 'mppx/client'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID!, appSecret: process.env.PRIVY_APP_SECRET! }); // 1. Create a wallet for the agent const wallet = await privy.wallets().create({chain_type: 'ethereum'}); // 2. Build a viem account backed by Privy const account = createViemAccount(privy, { walletId: wallet.id, address: wallet.address as `0x${string}` }); // 3. Create the MPP client const mppx = Mppx.create({ polyfill: false, methods: [tempo.charge({account})] }); // 4. Make a paid request const response = await mppx.fetch('https://api.example.com/weather'); const weather = await response.json(); ``` ## Learn more * [MPP Protocol documentation](https://mpp.dev) - Protocol specification and guides * [mppx SDK](https://www.npmjs.com/package/mppx) - Client and server SDK for MPP * [Privy server wallets](/recipes/agent-integrations/agentic-wallets) - Creating and managing server-side wallets * [Full demo application](https://github.com/privy-io/examples/tree/main/privy-next-mpp-agent-demo) - Complete Next.js example # OpenClaw agentic wallets Source: https://docs.privy.io/recipes/agent-integrations/openclaw-agentic-wallets # Using Privy Server Wallets with OpenClaw Enable your agent to autonomously execute onchain transactions using Privy server wallets. This recipe shows how to set up an OpenClaw agent with its own wallet, complete with policy-based guardrails. **Experimental Integration**: OpenClaw is a third-party open-source project and is not officially supported by Privy. This recipe is provided for educational purposes. Use at your own risk and always test thoroughly before deploying to production. ## What is OpenClaw? [OpenClaw](https://github.com/openclaw/openclaw) is an open-source framework for running agents that can interact with external tools and services. By combining OpenClaw with Privy server wallets, you can create agents that autonomously execute blockchain transactions within defined policy constraints. **About OpenClaw**: OpenClaw is a community-driven project. Privy does not maintain, endorse, or provide support for OpenClaw. For OpenClaw-specific issues, please refer to their [GitHub repository](https://github.com/openclaw/openclaw) and [Discord community](https://discord.com/invite/clawd). ## Use Cases What can autonomous agents do with their own wallets? * **Trading & DeFi**: Execute swaps, rebalance portfolios, compound yields * **Payments**: Pay for API calls, tip creators, handle subscriptions * **On-chain Automation**: Vote in governance, renew ENS domains, bridge assets * **Agent-to-Agent**: Pay other agents for tasks, escrow funds, settle debts * **NFTs**: Mint, purchase, and manage digital assets ## Prerequisites * A Privy account with API credentials ([dashboard.privy.io](https://dashboard.privy.io)) * OpenClaw installed and configured * Node.js 18+ ## Installation ### Option 1: Install from ClawHub (Recommended) The easiest way to install the skill: **Review Before Installing**: Before equipping your OpenClaw agent with any skill, always review its contents first: 1. Read the `SKILL.md` file to understand what the skill does 2. Examine the ZIP file contents before extraction 3. Inspect all source files in the skill to verify there's no malicious code Never blindly install skills from untrusted sources. ```bash theme={"system"} clawhub install privy ``` Don't have ClawHub? Install it first: `npm i -g clawhub` ### Option 2: Clone from GitHub Alternatively, clone directly into your workspace: ```bash theme={"system"} git clone https://github.com/privy-io/privy-agentic-wallets-skill.git ~/.openclaw/workspace/skills/privy ``` ### Configure Privy Credentials Add your Privy credentials to your OpenClaw config (`~/.openclaw/openclaw.json`): ```json theme={"system"} { "env": { "vars": { "PRIVY_APP_ID": "your-app-id", "PRIVY_APP_SECRET": "your-app-secret" } } } ``` **Security Notice**: Your Privy App Secret grants full access to create wallets and sign transactions. Never commit it to version control, share it publicly, or expose it in client-side code. ### Restart OpenClaw ```bash theme={"system"} openclaw gateway restart ``` ## Usage Once configured, your OpenClaw agent can interact with Privy wallets through natural language commands. ### Create a Wallet Ask your agent: > "Create an Ethereum wallet for yourself using Privy" The agent will call the Privy API to create a server wallet and return the address. ### Create a Policy Policies define guardrails for what the agent can do: > "Create a policy that limits transactions to 0.1 ETH max, only on Base mainnet" ### Attach Policy to Wallet > "Attach the spending limit policy to my wallet" ### Execute a Transaction > "Send 0.01 ETH to 0x1234... on Base" The agent will execute the transaction through Privy's RPC endpoint, subject to any attached policies. ## Policy Examples ### Spending Limit Restrict maximum value per transaction: ```json theme={"system"} { "name": "Max 0.1 ETH per tx", "method": "eth_sendTransaction", "conditions": [ { "field_source": "ethereum_transaction", "field": "value", "operator": "lte", "value": "100000000000000000" } ], "action": "ALLOW" } ``` ### Chain Restriction Lock the wallet to a specific chain: ```json theme={"system"} { "name": "Base mainnet only", "method": "eth_sendTransaction", "conditions": [ { "field_source": "ethereum_transaction", "field": "chain_id", "operator": "eq", "value": "8453" } ], "action": "ALLOW" } ``` ### Contract Allowlist Only allow interactions with specific contracts: ```json theme={"system"} { "name": "Only Uniswap Router", "method": "eth_sendTransaction", "conditions": [ { "field_source": "ethereum_transaction", "field": "to", "operator": "in", "value": ["0x3fC91A3afd70395Cd496C647d5a6CC9D4B2b7FAD"] } ], "action": "ALLOW" } ``` ## How It Works 1. **Agent receives request**: User asks the agent to perform a wallet operation 2. **Skill provides context**: OpenClaw loads the Privy skill which teaches the agent how to use the API 3. **API authentication**: Agent uses stored credentials to authenticate with Privy 4. **Policy enforcement**: Privy validates the transaction against attached policies 5. **Transaction execution**: If policies allow, Privy signs and broadcasts the transaction 6. **Result returned**: Agent reports the transaction hash or error to the user ## Security Considerations **Important Disclaimers** 1. **Not Production Ready**: This integration is experimental. Thoroughly test in testnet environments before using real funds. 2. **Fund Limits**: Only fund agent wallets with amounts you're comfortable losing. Start with small amounts for testing. 3. **Policy Design**: Design policies conservatively. It's safer to start restrictive and loosen over time than the reverse. 4. **Credential Security**: Privy credentials in OpenClaw config are stored in plaintext. Ensure your machine is secure and the config file has appropriate permissions. 5. **Agent Autonomy**: Autonomous agents can make mistakes. Consider requiring human approval for high-value transactions. 6. **No Warranty**: This integration is provided as-is. Neither Privy nor OpenClaw maintainers are responsible for lost funds. ## If Your Agent Is Compromised If you suspect your agent or credentials have been compromised, we recommend taking the following steps immediately: 1. **Rotate your Privy App Secret**: Generate a new App Secret from [dashboard.privy.io](https://dashboard.privy.io) and update your OpenClaw config. This invalidates the old credentials. 2. **Rotate authorization keys**: If you've equipped your wallets with extra signers and stored authorization keys locally, rotate those keys as well. 3. **Review recent transactions**: Check your wallet activity in the Privy dashboard for any unauthorized transactions. 4. **Transfer remaining funds**: Move any remaining funds to a new, uncompromised wallet. 5. **Audit your setup**: Investigate how the compromise occurred and address any security gaps before redeploying. ## Supported Chains | Chain | chain\_type | CAIP-2 | | -------- | ----------- | ---------------- | | Ethereum | `ethereum` | `eip155:1` | | Base | `ethereum` | `eip155:8453` | | Polygon | `ethereum` | `eip155:137` | | Arbitrum | `ethereum` | `eip155:42161` | | Optimism | `ethereum` | `eip155:10` | | Solana | `solana` | `solana:mainnet` | Privy also supports: Cosmos, Stellar, Sui, Aptos, Tron, Bitcoin (SegWit), NEAR, TON, Starknet ## Testing For testnet development: 1. Create wallets on Base Sepolia (chain ID 84532) 2. Get testnet ETH from [Base Sepolia Faucet](https://www.coinbase.com/faucets/base-ethereum-goerli-faucet) 3. Test transactions before moving to mainnet ## Troubleshooting **"Credentials not configured"** * Ensure `PRIVY_APP_ID` and `PRIVY_APP_SECRET` are set in your OpenClaw config * Restart the gateway after config changes **"Policy violation"** * Check your policy conditions match the transaction parameters * Verify chain\_id matches the target network **"Wallet not found"** * Confirm the wallet ID exists in your Privy dashboard * Ensure you're using the correct App ID ## Learn More * [Privy Server Wallets](https://docs.privy.io/guide/server-wallets) * [Privy Policies](https://docs.privy.io/guide/server-wallets/policies) * [OpenClaw Documentation](https://docs.openclaw.ai) * [Privy Agentic Wallets Skill](https://github.com/privy-io/privy-agentic-wallets-skill) # Overview Source: https://docs.privy.io/recipes/agent-integrations/overview Overview of integrating Privy agentic wallets for autonomous AI agents and server-side automation. Build autonomous agent experiences with Privy using programmable wallets, policy controls, and payment flows. You can launch agents that trade, pay, and automate tasks with auditable controls. Privy provides the core infrastructure for production-ready agent systems. Give any agent a wallet with a CLI command. No integration code needed. Create wallets for autonomous agents with policy guardrails. Enable HTTP-native payments for APIs and digital content. Coordinate automated wallet-to-wallet payments for agent systems. Build a Twitter bot with Privy wallets and Clanker. Connect OpenClaw agents to Privy server wallets. Let any CLI or self-hosted agent access user wallets in your Privy app. # Build a robo-advisor with Hermes agents and Robinhood chain Source: https://docs.privy.io/recipes/agent-integrations/robo-advisor-agent **This recipe shows how to build a portfolio-management app where the underlying assets are onchain stocks on [Robinhood Chain](https://robinhood.com/us/en/chain/) and an AI agent manages positions on the user's behalf.** The app holds a Privy wallet for each user. An AI agent, [Hermes](https://hermes-agent.nousresearch.com/), reachable over Telegram, gains secure access to that wallet through [agent authorization](/recipes/agent-integrations/agent-authorization). Once the user approves access one time in the browser, the agent can read the portfolio, propose trades, and execute them onchain, all from a chat conversation. The agent never holds an app secret or a wallet private key. ## What you'll build The full experience has three parts: 1. **A portfolio app**: a Privy app with an embedded wallet per user and a simple frontend to view holdings. 2. **A Hermes agent on Telegram**: the conversational surface where the user talks to their advisor. 3. **An agent skill**: the component that authorizes the agent against the user's wallet and executes trades on Robinhood Chain. This recipe glosses over the first two (they are standard Privy and Hermes setup) and focuses on the third: giving the agent secure, autonomous access to a user's wallet. ## How it works ```mermaid theme={"system"} sequenceDiagram participant U as User (Telegram) participant H as Hermes agent participant P as Privy participant R as Robinhood Chain U->>H: "Connect my portfolio wallet" H->>P: Request device code P-->>H: user_code + verification URL H->>U: Approve here: https://your-app.com/authorize?code=… U->>P: Sign in + approve in browser H->>P: Poll for tokens P-->>H: access + refresh tokens U->>H: "Buy 5 shares of AAPL" H->>P: Exchange token for signing key, sign RPC P->>R: eth_sendTransaction (chain_id 4663) R-->>U: Trade settled onchain ``` The agent uses the [OAuth 2.0 Device Authorization Grant](https://oauth.net/2/device-flow/), the same pattern GitHub CLI uses for headless login. The user approves once; the agent stores tokens and transacts autonomously within that authorization. ## Prerequisites Follow the [React quickstart](/basics/react/quickstart) to create a Privy app with an embedded wallet for each user. The frontend only needs to display wallet holdings; the agent handles trading. Note the **app ID** from the [Privy Dashboard](https://dashboard.privy.io); the skill references it as ``. In the [Privy Dashboard](https://dashboard.privy.io), open **Authentication → Advanced** and toggle **Enable for CLI and agent access** on. Set the **Verification URI** to a page your app hosts, for example `https://your-app.com/authorize`. Build that verification page following [Authorized wallet access for self-hosted agents](/recipes/agent-integrations/agent-authorization#build-the-verification-page). This is the only browser step in the flow: the user signs in, sees which agent is requesting access, and approves or denies. Deploy a [Hermes](https://hermes-agent.nousresearch.com/) agent and connect its Telegram gateway so users can message it directly. This recipe assumes the agent can run shell commands and load skills, the standard Hermes configuration. With those in place, the rest of this recipe builds the skill that connects the agent to the wallet. ## Build the agent skill A skill is a self-contained folder the agent loads at startup. It teaches the agent when and how to authorize against the user's wallet and execute trades. The structure: ``` portfolio-wallet/ ├── SKILL.md # when to use the skill + command reference └── scripts/ └── privy_agent.py # device-flow auth + wallet RPC ``` ### Write the skill definition `SKILL.md` tells the agent what the skill does, when to trigger it, and the safety rules it must follow. Replace `` and the verification URL with the values from your app. ```markdown SKILL.md theme={"system"} --- name: portfolio-wallet description: "The user's portfolio wallet: the Privy wallet in the app that holds their onchain stock positions. Use whenever the user asks to connect their wallet, check holdings, or buy or sell positions." version: 1.0.0 platforms: [macos] --- # Portfolio wallet: agent authorization and trading Gives the agent authorized access to the user's wallet in the Privy app using the OAuth 2.0 Device Authorization Grant, then signs and sends trades on Robinhood Chain via Privy's wallet RPC. No app secret lives on this machine. **App config:** - App ID: `` - Verification page: `https://your-app.com/authorize` (returned by the API) - API base: `https://auth.privy.io` - Trading chain: Robinhood Chain (`chain_id` 4663, CAIP-2 `eip155:4663`) All logic lives in `scripts/privy_agent.py`. Tokens are stored in the macOS Keychain (service `privy-agent`), never in plaintext files. ## Safety rules (non-negotiable) 1. **Always get explicit user confirmation before any trade** (`eth_sendTransaction` or anything that moves funds). Show the user the asset, share count, destination, value, and chain, then wait for a clear "yes". 2. Never print, log, or echo tokens or decrypted authorization keys. 3. If the user asks to revoke access, run `logout` locally and point them to their account settings in the app. ## Commands All commands run via: `python3 /scripts/privy_agent.py ` ### `login`: run the device authorization flow python3 -u scripts/privy_agent.py login > /tmp/privy-login.log 2>&1 Requests a device code, prints the verification URL and user code, then polls until the user approves. Send the verification link to the user on Telegram, formatted as a consent prompt: 🔐 Authorization Request Hermes is requesting access to your portfolio wallet. This will allow the agent to: • View your wallet address and holdings • Sign messages on your behalf • Execute trades (with your confirmation) 👉 Approve here: Verification code: XXXXX-XXXXX ⏱ Expires in 10 minutes. On approval the script stores tokens in the Keychain and prints the wallet list. ### `status`: check auth state and list wallets python3 scripts/privy_agent.py status Prints whether tokens exist, refreshes if needed, and lists wallets (id, address, chain_type). Use the Privy wallet `id` (e.g. `wallet_abc123`), not the 0x address, in RPC calls. ### `rpc`: sign or trade with a wallet # Buy an asset on Robinhood Chain (CONFIRM WITH USER FIRST!) python3 scripts/privy_agent.py rpc '{"method":"eth_sendTransaction","params":{"transaction":{"to":"0xAssetContract","value":"0x0","data":"0x...","chain_id":4663}}}' # Sign a message python3 scripts/privy_agent.py rpc '{"method":"personal_sign","params":{"message":"Hello from Hermes"}}' ### `logout`: drop stored tokens python3 scripts/privy_agent.py logout ``` Keep the skill description specific and unambiguous. A precise trigger ("the Privy wallet in the `` app") prevents the agent from confusing the wallet with an unrelated service when the user speaks casually. ### Write the auth and trading script `privy_agent.py` implements the full device flow and wallet RPC. The script reads the app ID from the `PRIVY_APP_ID` environment variable, stores tokens in the OS keychain, and signs each RPC request with an ephemeral authorization key it never writes to disk. ```python scripts/privy_agent.py theme={"system"} #!/usr/bin/env python3 """Portfolio agent wallet access: device-flow auth + wallet RPC. Implements https://docs.privy.io/recipes/agent-integrations/agent-authorization - OAuth 2.0 Device Authorization Grant against https://auth.privy.io - Tokens stored in the macOS Keychain (service: privy-agent) - Wallet RPC with an HPKE-decrypted ephemeral authorization key and RFC 8785 canonical JSON + ECDSA P-256 request signatures. Commands: login | status | rpc | logout """ import base64 import functools import json import os import subprocess import sys import time import urllib.error import urllib.request print = functools.partial(print, flush=True) # unbuffered for background use APP_ID = os.environ["PRIVY_APP_ID"] BASE = "https://auth.privy.io" KEYCHAIN_SERVICE = "privy-agent" KEYCHAIN_ACCOUNT = APP_ID # ---------------------------------------------------------------- keychain -- def kc_get(): try: out = subprocess.run( ["security", "find-generic-password", "-s", KEYCHAIN_SERVICE, "-a", KEYCHAIN_ACCOUNT, "-w"], capture_output=True, text=True, check=True) return json.loads(out.stdout.strip()) except (subprocess.CalledProcessError, json.JSONDecodeError): return None def kc_set(tokens: dict): subprocess.run( ["security", "add-generic-password", "-U", "-s", KEYCHAIN_SERVICE, "-a", KEYCHAIN_ACCOUNT, "-w", json.dumps(tokens)], capture_output=True, check=True) def kc_delete(): subprocess.run( ["security", "delete-generic-password", "-s", KEYCHAIN_SERVICE, "-a", KEYCHAIN_ACCOUNT], capture_output=True) # -------------------------------------------------------------------- http -- def post(path, body, headers=None, bearer=None): # Send a custom User-Agent: Cloudflare blocks Python's default UA on # auth.privy.io with a 403 (error code 1010). h = {"Content-Type": "application/json", "privy-app-id": APP_ID, "User-Agent": "portfolio-agent/1.0"} if bearer: h["Authorization"] = f"Bearer {bearer}" if headers: h.update(headers) req = urllib.request.Request(BASE + path, data=json.dumps(body).encode(), headers=h, method="POST") try: with urllib.request.urlopen(req, timeout=30) as r: return r.status, json.loads(r.read().decode()) except urllib.error.HTTPError as e: try: return e.code, json.loads(e.read().decode()) except Exception: return e.code, {"error": "http_error", "detail": str(e)} # ------------------------------------------------------------------ tokens -- def now(): return int(time.time()) def refresh_tokens(tokens): status, data = post("/api/oauth/v2/token", {"grant_type": "refresh_token", "refresh_token": tokens["refresh_token"]}) if status != 200: if data.get("error") == "access_denied": kc_delete() sys.exit("ERROR: refresh token expired or access revoked. " "Run 'login' again; user must re-approve.") sys.exit(f"ERROR: token refresh failed ({status}): {data}") tokens = { "access_token": data["access_token"], "refresh_token": data["refresh_token"], # rotates on every use; store the newest "expires_at": now() + int(data.get("expires_in", 900)), } kc_set(tokens) return tokens def get_valid_tokens(): tokens = kc_get() if not tokens: sys.exit("ERROR: not authorized. Run 'login' first.") if now() >= tokens.get("expires_at", 0) - 60: tokens = refresh_tokens(tokens) return tokens # ------------------------------------------------------------------- login -- def cmd_login(): status, data = post("/api/oauth/v2/device_authorization", {}) if status == 403 and data.get("error") == "device_auth_not_enabled": sys.exit("ERROR: device auth not enabled in Privy Dashboard " "(Authentication -> Advanced -> Enable for CLI and agent access)") if status != 200: sys.exit(f"ERROR: device_authorization failed ({status}): {data}") device_code = data["device_code"] interval = int(data.get("interval", 5)) expires_in = int(data.get("expires_in", 600)) print("=== AUTHORIZATION REQUIRED ===") print(f"URL: {data['verification_uri_complete']}") print(f"Code: {data['user_code']}") print(f"(expires in {expires_in // 60} minutes)") print("Waiting for user approval...") deadline = now() + expires_in while now() < deadline: time.sleep(interval) # The poll grant_type MUST be the RFC 8628 URN, not the bare # "device_code" string. status, tok = post("/api/oauth/v2/token", {"grant_type": "urn:ietf:params:oauth:grant-type:device_code", "device_code": device_code}) if status == 200: kc_set({ "access_token": tok["access_token"], "refresh_token": tok["refresh_token"], "expires_at": now() + int(tok.get("expires_in", 900)), }) print("APPROVED. Tokens stored in Keychain.") cmd_status() return err = tok.get("error", "") if err == "authorization_pending": continue if err == "slow_down": interval += 5 continue if err == "expired_token": sys.exit("ERROR: device code expired. Run 'login' again.") if err == "access_denied": sys.exit("ERROR: user denied the authorization request.") sys.exit(f"ERROR: unexpected polling response ({status}): {tok}") sys.exit("ERROR: device code expired (timeout). Run 'login' again.") # ------------------------------------------------------------ hpke + p-256 -- def gen_p256_keypair(): from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives import serialization priv = ec.generate_private_key(ec.SECP256R1()) spki = priv.public_key().public_bytes( serialization.Encoding.DER, serialization.PublicFormat.SubjectPublicKeyInfo) return priv, base64.b64encode(spki).decode() def hpke_decrypt(priv, encapsulated_key_b64, ciphertext_b64): """Decrypt the authorization key. Privy uses DHKEM-P256 + HKDF-SHA256; AEAD is ChaCha20Poly1305, with AES-GCM tried as a fallback.""" from pyhpke import AEADId, CipherSuite, KDFId, KEMId, KEMKey from cryptography.hazmat.primitives import serialization enc = base64.b64decode(encapsulated_key_b64) ct = base64.b64decode(ciphertext_b64) pem = priv.private_bytes( serialization.Encoding.PEM, serialization.PrivateFormat.PKCS8, serialization.NoEncryption()).decode() last_err = None for aead in (AEADId.CHACHA20_POLY1305, AEADId.AES256_GCM, AEADId.AES128_GCM): try: suite = CipherSuite.new(KEMId.DHKEM_P256_HKDF_SHA256, KDFId.HKDF_SHA256, aead) recipient = suite.create_recipient_context(enc, KEMKey.from_pem(pem)) return recipient.open(ct).decode() except Exception as e: last_err = e raise RuntimeError(f"HPKE decryption failed with all AEADs: {last_err}") def canonicalize(obj): """RFC 8785 (JCS) canonical JSON: sorted keys + compact separators.""" return json.dumps(obj, sort_keys=True, separators=(",", ":"), ensure_ascii=False) def sign_payload(auth_key_str, payload_obj): """ECDSA P-256 / SHA-256 over canonical JSON; DER signature, base64.""" from cryptography.hazmat.primitives.asymmetric import ec from cryptography.hazmat.primitives import hashes, serialization # Authorization keys arrive prefixed with "wallet-auth:"; strip it. key_b64 = auth_key_str.replace("wallet-auth:", "").strip() priv = serialization.load_der_private_key(base64.b64decode(key_b64), password=None) sig = priv.sign(canonicalize(payload_obj).encode(), ec.ECDSA(hashes.SHA256())) return base64.b64encode(sig).decode() # ----------------------------------------------------------- authenticate -- def wallet_authenticate(tokens): priv, pub_spki_b64 = gen_p256_keypair() body = {"encryption_type": "HPKE", "recipient_public_key": pub_spki_b64} hdrs = {"privy-grant-type": "device_code"} status, data = post("/api/oauth/v2/wallets/authenticate", body, headers=hdrs, bearer=tokens["access_token"]) if status == 401: tokens = refresh_tokens(tokens) status, data = post("/api/oauth/v2/wallets/authenticate", body, headers=hdrs, bearer=tokens["access_token"]) if status != 200: sys.exit(f"ERROR: wallets/authenticate failed ({status}): {data}") ek = data["encrypted_authorization_key"] auth_key = hpke_decrypt(priv, ek["encapsulated_key"], ek["ciphertext"]) return tokens, auth_key, data.get("wallets", []) # ------------------------------------------------------------------ status -- def cmd_status(): tokens = get_valid_tokens() _tokens, _auth_key, wallets = wallet_authenticate(tokens) print("Authorized: YES") print(f"Wallets ({len(wallets)}):") for w in wallets: print(f" id={w.get('id')} address={w.get('address')} " f"chain={w.get('chain_type')}") # --------------------------------------------------------------------- rpc -- def cmd_rpc(wallet_id, body_json): body = json.loads(body_json) tokens = get_valid_tokens() tokens, auth_key, _wallets = wallet_authenticate(tokens) url = f"{BASE}/api/oauth/v2/wallets/{wallet_id}/rpc" # The signature payload includes ONLY privy-prefixed headers. payload = {"version": 1, "method": "POST", "url": url, "body": body, "headers": {"privy-app-id": APP_ID}} signature = sign_payload(auth_key, payload) rpc_headers = {"privy-grant-type": "device_code", "privy-authorization-signature": signature} status, data = post(f"/api/oauth/v2/wallets/{wallet_id}/rpc", body, headers=rpc_headers, bearer=tokens["access_token"]) if status == 401: tokens = refresh_tokens(tokens) tokens, auth_key, _ = wallet_authenticate(tokens) rpc_headers["privy-authorization-signature"] = sign_payload(auth_key, payload) status, data = post(f"/api/oauth/v2/wallets/{wallet_id}/rpc", body, headers=rpc_headers, bearer=tokens["access_token"]) print(json.dumps({"http_status": status, "response": data}, indent=2)) if status != 200: sys.exit(1) # -------------------------------------------------------------------- main -- def main(): if len(sys.argv) < 2: sys.exit(__doc__) cmd = sys.argv[1] if cmd == "login": cmd_login() elif cmd == "status": cmd_status() elif cmd == "rpc": if len(sys.argv) < 4: sys.exit("usage: privy_agent.py rpc ''") cmd_rpc(sys.argv[2], sys.argv[3]) elif cmd == "logout": kc_delete() print("Tokens removed from Keychain.") else: sys.exit(f"unknown command: {cmd}\n{__doc__}") if __name__ == "__main__": main() ``` The script depends on two Python packages: ```bash theme={"system"} python3 -m pip install --user pyhpke cryptography ``` Keep the decrypted authorization key in memory only. It grants direct signing authority over the user's wallet until it expires. Never write it to disk or log it. The script above never persists it. ## Trade on Robinhood Chain [Robinhood Chain](https://robinhood.com/us/en/chain/) is an EVM-compatible chain where tokenized stocks trade as onchain assets. Because it is EVM-compatible, the agent trades on it exactly like any other EVM chain. The only difference is the `chain_id`. | Property | Value | | --------------- | ------------- | | Chain ID | `4663` | | CAIP-2 | `eip155:4663` | | Native currency | ETH | | Testnet ID | `46630` | To buy or sell a position, the agent submits an `eth_sendTransaction` RPC that calls the asset's contract, passing `4663` as the `chain_id`: ```json theme={"system"} { "method": "eth_sendTransaction", "params": { "transaction": { "to": "0xAssetContract", "value": "0x0", "data": "0x...", "chain_id": 4663 } } } ``` Use the Privy wallet `id` (for example, `wallet_abc123`) in the RPC path, not the on-chain address. Run `status` to look up the id. ## Iterate with the agent over Telegram Once the skill is installed, the user drives everything from chat. A typical session: **User:** "Connect my portfolio wallet." Hermes runs `login` in the background, reads the verification link from the log, and sends it to the user as a consent prompt. The user opens the link, signs in with Privy, and approves. Hermes reports back with the connected wallet address. **User:** "How's my portfolio looking today?" Hermes runs `status` to confirm access, reads on-chain balances, and summarizes the current positions and their value. **User:** "Rebalance, move 20% into AAPL." Hermes proposes the exact trade (asset, share count, value, chain), waits for the user to confirm, then executes it with an `eth_sendTransaction` RPC on Robinhood Chain and reports the transaction hash. Because the agent re-derives an ephemeral signing key for every operation and never stores an app secret, the same conversation works from any device the agent runs on. Access lasts until the refresh token expires (30 days) or the user revokes it. ## Keep the user in control * **Confirm every trade.** The skill's safety rules require explicit user confirmation before any fund-moving RPC. Show the asset, amount, and chain before executing. * **Revoke anytime.** Users can list and revoke active agent authorizations from the app's account settings. See [managing authorizations](/recipes/agent-integrations/agent-authorization#managing-authorizations). Running `logout` locally drops the agent's tokens; its access then dies within 15 minutes since nothing can refresh it. * **Constrain with policies.** Attach [policies](/controls/policies/overview) to the wallet to enforce transfer limits, allowlists, and time-based controls at the infrastructure layer. These guardrails hold even if the agent misbehaves. ## Extensions The same wallet the agent uses to trade can also pay for the data behind its decisions. A natural next step is to let the agent buy financial research on demand through [x402](/recipes/agent-integrations/x402), the HTTP-native payment protocol for agents. With x402, the agent pays per request for a resource and receives the response in the same round trip. No subscription, no API key to manage. Services like [DripStack](https://dripstack.xyz) expose market data and research reports behind x402 paywalls, so the agent can pull a fresh analyst report or price feed the moment a user asks about a position, and settle the micropayment from the same Privy wallet. A typical flow: 1. The user asks the agent for a view on a stock before trading. 2. The agent calls an x402-protected research endpoint and pays the quoted price from the wallet. 3. The agent folds the research into its recommendation, then executes the trade on Robinhood Chain if the user confirms. This closes the loop: the agent funds its own research, forms a view, and acts on it, all from one authorized wallet. See the [x402 recipe](/recipes/agent-integrations/x402) to add paid API calls to the skill. ## Learn more The full device-flow API reference this skill is built on. Give any agent a wallet with a CLI command. No integration code needed. Constrain agent behavior with transfer limits, allowlists, and time-based controls. Create developer-controlled agent wallets with policy guardrails. Let the agent pay per request for research and other APIs. # Virtuals EconomyOS with Privy wallets Source: https://docs.privy.io/recipes/agent-integrations/virtuals-economyos **EconomyOS** is Virtuals' operating system for agents. It provides the identity, financial, and economic infrastructure an agent needs to operate as an economic actor — wallet, payment cards, email, access to capital, compute, and the ability to buy and sell services with other agents. EconomyOS uses Privy server wallets as its default wallet layer. This guide is adapted from the [original EconomyOS recipe](https://virtualsprotocol.notion.site/Recipe-EconomyOS-powered-with-Privy-Wallets-34c2d2a429e9805c9583ee79498511be) published by the Virtuals team. For full documentation, advanced flows, and SDK usage, visit [os.virtuals.io](https://os.virtuals.io/). ## What EconomyOS includes EconomyOS is organized around four composable pillars. | Pillar | What it gives the agent | | ------------ | ------------------------------------------------------------------------------------------------ | | **Identity** | Wallet, dedicated email, and a domain | | **Capital** | Tokenize to raise capital; deploy capital across permissionless markets | | **Commerce** | Payment cards for real-world checkout, cross-chain payments, agent-to-agent ACP jobs, reputation | | **Compute** | Pay for inference, memory, and managed runtime from the wallet | ## EconomyOS wallet The wallet is the hub of EconomyOS. Privy server wallets are the default wallet layer, with the following properties: * **Non-custodial.** The creator holds the authorization key and has full custody. Virtuals cannot withdraw funds, change signers, or move assets on the agent's behalf. * **Secure agent identity.** The wallet address is the agent's on-chain identity — the root every other primitive binds to. Transactions are authorized by a separate P256 signer generated locally and stored in the OS keychain (macOS Keychain, Linux Secret Service, Windows Credential Manager). The private key never enters application code. * **Guard-railed agent actions (coming soon).** Once a signer is attached, the agent can transact without per-action approval. The creator controls authority through per-wallet spend policies, allowlists, and rate limits. Using the agent on a new machine requires adding a new signer. * **Transaction security layer (coming soon).** An additional layer providing anomaly detection enforced at sign-time, letting creators grant agents broad autonomy while retaining the ability to constrain it. ## Installation The ACP CLI is the fastest way to provision a fully commerce-ready agent from a terminal. ### Prerequisites * Node.js >= 18 * A browser on the machine for one-time OAuth and signer approval ```bash theme={"system"} npm install -g acp-cli acp configure ``` OAuth tokens are stored in the OS keychain and refreshed automatically. ```bash theme={"system"} acp agent create --name "MyAgent" --description "What this agent does" acp agent add-signer ``` `agent create` registers the agent and provisions its on-chain wallet. `add-signer` generates a P256 keypair locally, opens a browser for approval, and persists the private key in the OS keychain. ```bash theme={"system"} acp agent whoami acp wallet address --json acp wallet balance --chain-id 8453 acp wallet topup --chain-id 8453 ``` Fund the wallet via Coinbase Pay, card (Crossmint), or QR code. Pass `--method coinbase`, `--method card --amount 25 --email you@example.com`, or `--method qr` to skip the interactive picker. With the wallet live, activate the remaining identity primitives. Each binds to the same wallet address. **Email** — dedicated inbox for logins, OTPs, and notifications: ```bash theme={"system"} acp email provision --display-name "My Agent" --local-part "my.agent" ``` Creates an address like `my.agent@yourdomain.agents.world`. **Card** — single-use virtual Visa cards for checkout: ```bash theme={"system"} acp card signup --email "agent@example.com" ``` Card setup is a guided multi-step flow (signup → profile → payment method → limit → request). Each response returns a `nextStep` field indicating which command to run next. **Token** *(optional)* — route trading-fee revenue back to the agent wallet, enable co-ownership, and anchor on-chain reputation: ```bash theme={"system"} acp agent tokenize ``` Available flags: `--anti-sniper`, `--prebuy`, `--acf` (Agent Capital Formation), `--60-days`, `--airdrop-percent`, `--robotics`. At this point the agent has a funded wallet, email, card, and optionally a token — all anchored to one address. *** ## Buying and selling with ACP The Agent Commerce Protocol (ACP) is how agents transact with each other. It is the reference implementation of [ERC-8183](https://ethereum-magicians.org/t/erc-8183-agentic-commerce/27902), with escrow holding settlement until delivery is verified. ### Sell: publish an offering and get hired ```bash theme={"system"} # Publish a service acp offering create \ --name "Logo Design" \ --description "Minimalist logo design in PNG" \ --price-type fixed \ --price-value 5.00 \ --sla-minutes 60 \ --requirements '{"type":"object","properties":{"style":{"type":"string"}},"required":["style"]}' # Stream incoming job events acp events listen # When a job is funded, set a budget and submit the deliverable acp provider set-budget --job-id --amount 5.00 acp provider submit --job-id --deliverable '{"url":"https://..."}' ``` When the client approves, USDC releases from escrow into the agent's wallet automatically. ### Buy: hire another agent ```bash theme={"system"} # Find a provider acp browse "logo design" # Create a job from an existing offering acp client create-job-from-offering \ --offering-id \ --requirement '{"style":"minimalist"}' # Fund escrow once the provider sets a budget acp client fund --job-id # Approve or reject on delivery acp client complete --job-id # or acp client reject --job-id --reason "Off-brief" ``` Every action is signed locally by the attached signer and submitted through the agent's wallet — no human approval per transaction, no raw keys in code. *** ## Error handling | Error | Cause | Recommended action | | --------------------- | ----------------------------------------- | ------------------------------------------------------------- | | `signer not attached` | No signer registered on this machine | Run `acp agent add-signer` and complete the browser approval | | `insufficient funds` | Wallet lacks gas or USDC | Top up the wallet address via crypto, credit card, or on-ramp | | `session expired` | OAuth tokens expired | Run `acp configure` again | | `signature rejected` | A spend guardrail blocked the transaction | Adjust the guardrail in the Virtuals Console | ## Learn more Full documentation, advanced flows, and SDK usage from Virtuals. Dedicated inbox for agent logins, OTPs, and notifications. Virtual Visa cards for real-world agent checkout. How agents buy and sell services on-chain. Learn how Privy server wallets work under the hood. Pay for inference, memory, and managed runtime. # x402 Source: https://docs.privy.io/recipes/agent-integrations/x402 # Using x402 payments with Privy Enable users to pay for APIs and content using x402, an open HTTP payment protocol. Privy's x402 integration helps integrate x402 payment authorizations from embedded wallets in both client-side React apps and server-side Node.js applications. Payment settlement is handled by the selected facilitator. ## What is x402? [x402](https://x402.org) is an open payment protocol that enables instant, automatic payments for APIs and digital content over HTTP. When a resource requires payment, the server responds with `402 Payment Required`. The client constructs an `X-PAYMENT` header with a signed payment authorization and retries the request. ## Installation ```bash theme={"system"} npm install @privy-io/react-auth ``` The `useX402Fetch` hook is built into `@privy-io/react-auth` (v3.7.0+). ```bash theme={"system"} npm install @privy-io/node @x402/fetch ``` The `createX402Client` function is available in `@privy-io/node`. ## Usage ### Basic example ```tsx theme={"system"} import {useX402Fetch, useWallets} from '@privy-io/react-auth'; function MyComponent() { const {wallets} = useWallets(); const {wrapFetchWithPayment} = useX402Fetch(); async function fetchPremiumContent() { // Wrap fetch with your wallet const fetchWithPayment = wrapFetchWithPayment({ walletAddress: wallets[0]?.address, fetch }); // Use exactly like native fetch - automatically handles 402 payments const response = await fetchWithPayment('https://api.example.com/premium'); const data = await response.json(); return data; } return ; } ``` ```typescript theme={"system"} import {createX402Client} from '@privy-io/node/x402'; import {wrapFetchWithPayment} from '@x402/fetch'; // Get wallet details const wallet = await privy.wallets().get({walletId: 'your-wallet-id'}); // Create x402 client (chain type is inferred from address) const x402client = createX402Client(privy, { walletId: wallet.id, address: wallet.address, }); // Wrap fetch - 402 payments are handled automatically const fetchWithPayment = wrapFetchWithPayment(fetch, x402client); const response = await fetchWithPayment('https://api.example.com/premium'); const data = await response.json(); ``` ### Using default connected wallet ```tsx theme={"system"} import {useX402Fetch} from '@privy-io/react-auth'; function MyComponent() { const {wrapFetchWithPayment} = useX402Fetch(); async function fetchPremiumContent() { // Omit walletAddress to use first connected wallet const fetchWithPayment = wrapFetchWithPayment({fetch}); const response = await fetchWithPayment('https://api.example.com/premium'); const data = await response.json(); return data; } return ; } ``` ### With maximum payment protection ```typescript theme={"system"} import {useX402Fetch, useWallets} from '@privy-io/react-auth'; const {wallets} = useWallets(); const {wrapFetchWithPayment} = useX402Fetch(); const fetchWithPayment = wrapFetchWithPayment({ walletAddress: wallets[0].address, fetch, maxValue: BigInt(1000000) // Max 1 USDC (6 decimals) }); ``` ### With gas-sponsored wallets If your app uses Privy's gas sponsorship, pass `signatureOptions: { type: 'erc1271' }` to `wrapFetchWithPayment`. ```tsx theme={"system"} import {useX402Fetch, useWallets} from '@privy-io/react-auth'; function MyComponent() { const {wallets} = useWallets(); const {wrapFetchWithPayment} = useX402Fetch(); async function fetchPremiumContent() { const fetchWithPayment = wrapFetchWithPayment({ walletAddress: wallets[0]?.address, fetch, signatureOptions: {type: 'erc1271'}, }); const response = await fetchWithPayment('https://api.example.com/premium'); const data = await response.json(); return data; } return ; } ``` If your app uses Privy's gas sponsorship, pass `signatureOptions: { type: 'erc1271' }` to `createX402Client`. ```typescript theme={"system"} import {PrivyClient} from '@privy-io/node'; import {createX402Client} from '@privy-io/node/x402'; import {wrapFetchWithPayment} from '@x402/fetch'; const privy = new PrivyClient({appId, appSecret}); const wallet = await privy.wallets().get('your-wallet-id'); const x402client = createX402Client(privy, { walletId: wallet.id, address: wallet.address, signatureOptions: {type: 'erc1271'}, }); const fetchWithPayment = wrapFetchWithPayment(fetch, x402client); const response = await fetchWithPayment('https://api.example.com/premium'); const data = await response.json(); ``` ## Key details **Requirements:** * Users need USDC in their Privy embedded wallet on the correct network (e.g. Base, Base Sepolia, or Solana) * The facilitator pays gas fees (users only need USDC, not ETH or SOL) **Testing:** * For testnet: Get free USDC from [Circle's faucet](https://faucet.circle.com/) ## x402 facilitators Facilitators are services that verify payment authorizations and submit transactions onchain on behalf of users. They handle gas fees and transaction settlement, allowing users to pay only with USDC without needing native tokens like ETH or SOL. Several x402 facilitators are available, including: * Pay AI: [facilitator](https://facilitator.payai.network/), [docs](https://docs.payai.network/x402/reference) * Corbits: [facilitator](https://facilitator.corbits.dev/), [docs](https://docs.corbits.dev/) * Coinbase: [facilitator](http://api.cdp.coinbase.com/platform/v2/x402), [docs](https://docs.cdp.coinbase.com/api-reference/v2/rest-api/x402-facilitator/x402-facilitator) ## Example x402-enabled APIs CoinGecko and Allium provide x402-enabled data APIs for agent workflows. ### CoinGecko * x402 docs: [docs.coingecko.com/x402](https://docs.coingecko.com/x402) * Example endpoint: `https://pro-api.coingecko.com/api/v3/x402/simple/price` ### Allium * AgentHub: [agents.allium.so](https://agents.allium.so/) * API docs: [docs.allium.so](https://docs.allium.so/) ## Screening payment recipients x402 payments are signed as EIP-712 typed data, not as ordinary transactions, so a policy on `eth_sendTransaction` does not cover them. To restrict who an agent or user can pay, attach a wallet [policy](/controls/policies/overview) that screens the recipient in the signing request. See [sanctions screening for x402 payments](/recipes/agent-integrations/x402-sanctions-screening). ## Learn more * [x402 docs](https://x402.gitbook.io/x402) * [Privy sign typed data](https://docs.privy.io/wallets/using-wallets/ethereum/sign-typed-data) * [EIP-3009 standard](https://eips.ethereum.org/EIPS/eip-3009) * [Sanctions screening for x402 payments](/recipes/agent-integrations/x402-sanctions-screening) You can find more x402-enabled APIs at [x402scan.com](http://x402scan.com). # Sanctions screening for x402 payments Source: https://docs.privy.io/recipes/agent-integrations/x402-sanctions-screening Use Privy policies and condition sets to deny x402 payments to sanctioned recipient addresses on Base, Tempo, and Solana before anything is signed. # Block x402 payments to sanctioned addresses This recipe shows how an app can prevent x402 payments to addresses on a sanctions list, enforced by Privy's policy engine before anything is signed. It covers Base and Tempo (EVM) and Solana. Privy is infrastructure, not a compliance program. Each app determines its own compliance obligations and layers the tooling it sees fit on top of Privy. A denylist blocks known addresses. It does not screen for indirect exposure, nested services, or newly generated addresses. Pair it with a screening provider such as Chainalysis, TRM, or Blockaid for risk scoring. Nothing in this guide constitutes compliance or legal advice. Validate the approach with legal and compliance teams, and keep the source data and review process current. ## Where to screen an x402 payment x402 payments are not ordinary transactions, so screening `eth_sendTransaction` alone does not catch them. * **Base and Tempo (EVM)**: an x402 payment is an [EIP-3009](https://eips.ethereum.org/EIPS/eip-3009) `transferWithAuthorization` authorization. The client signs an EIP-712 typed-data message over `eth_signTypedData_v4`, and a facilitator submits it onchain and pays gas. The recipient to screen is the `to` field inside the `TransferWithAuthorization` message. * **Solana**: the payment is a USDC (SPL) transfer signed via `signTransaction` or `signAndSendTransaction`. The x402 library builds it as a `TransferChecked` instruction, so the recipient to screen is `TransferChecked.destination`. The policy below also covers `Transfer.destination` in case the client emits a plain `Transfer`. The policy below enforces that the payment recipient is not on the sanctions list at the moment Privy signs, inside the secure enclave, so a modified client cannot bypass it. ``` User initiates an x402 request -> Client builds the payment authorization -> Privy policy engine extracts the recipient from the signing request -> Recipient checked against the current condition set -> No match: sign; facilitator settles -> Match: DENY, nothing is signed ``` ## Why the policy engine is the enforcement point * **DENY takes precedence.** If any rule evaluates to DENY, the policy engine denies the request even if another rule would allow it. A denylist can therefore extend an existing policy without reworking its allow rules. * **Unmatched methods default to DENY.** If no rule returns an action for a requested method, the policy engine denies the request. Pair the denylist with ALLOW rules for the methods the app legitimately uses. * **Condition sets update independently of the policy.** Addresses live in a condition set referenced by ID, so new designations sync without redeploying or re-signing the policy. * **Privy enforces policies per wallet.** A policy applies only to the wallets that list it in `policy_ids`. Every wallet the app creates must carry the policy from creation. An app-side pre-check can improve the user experience, but clients can be modified and server paths can be bypassed. Keep the Privy policy as the authoritative control and treat any pre-check as advisory. ## 1. Define the authoritative list and its owner Use a sanctions-data source approved by the compliance team, for example the U.S. Treasury OFAC SDN list, and establish: * the team responsible for reviewing and approving list updates * the refresh cadence, at least daily and more often if the risk posture requires it * an audit trail recording source, fetch time, version or hash, and addresses added or removed * an escalation path for potential matches and false positives Store the chain family alongside every address. An address should never be treated as globally sanctioned without chain context. Create an [owner](/controls/authorization-keys/using-owners/overview) for the condition set and the policy. Condition sets require an owner, and updates to both require an [authorization signature](/api-reference/authorization-signatures#usage). Without an owner, the app secret alone can modify the denylist. ## 2. Create the condition set Create one condition set per chain family, one for EVM and one for Solana. Each policy targets a single `chain_type`, and separate sets keep the audit trail per chain clean and allow independent refreshes. ```shell theme={"system"} curl -X POST https://api.privy.io/v1/condition_sets \ -u ":" \ -H "privy-app-id: " \ -H "Content-Type: application/json" \ -d '{ "name": "OFAC sanctioned addresses (EVM)", "owner_id": "" }' ``` ```json theme={"system"} { "id": "qvah5m2hmp9abqlxdmfiht95", "name": "OFAC sanctioned addresses (EVM)", "owner_id": "", "created_at": 1761271537642 } ``` Keep the returned `id`. The policy references the denylist by ID, not by name. ## 3. Load the addresses into the condition set Fetching and batching the addresses is general code, so it is not reproduced here. At a high level: * Pull digital-currency addresses from the machine-readable [SDN enhanced XML export](https://sanctionslistservice.ofac.treas.gov/api/PublicationPreview/exports/SDN_ENHANCED.XML) on a schedule. * Branch on address format, not on the asset label. USDC and USDT appear on multiple chains, and one address can carry several asset labels. EVM addresses start with `0x`, and Solana addresses are base58. * POST them to the condition set in batches of up to 100 items per request. The only Privy-specific call is adding items: ```shell theme={"system"} curl -X POST https://api.privy.io/v1/condition_sets/qvah5m2hmp9abqlxdmfiht95/condition_set_items \ -u ":" \ -H "privy-app-id: " \ -H "privy-authorization-signature: " \ -H "Content-Type: application/json" \ -d '[ { "value": "" }, { "value": "" } ]' ``` For the signing conditions in this guide, the policy engine compares EVM addresses case-insensitively, so each address needs a single entry in either checksummed or lowercase form. Solana base58 addresses are matched exactly, so store those verbatim. The high-level `transfer` action is also matched exactly, even for EVM addresses. See [the transfer action](#the-transfer-action). ## 4. Create the policy ### Base and Tempo (EVM) The x402 signing path is `eth_signTypedData_v4`. Screen the `to` field of the `TransferWithAuthorization` message against the condition set. An `ethereum_typed_data_message` condition only evaluates when the `types` map declared in the policy matches the `types` map in the signing request **exactly**, including every type the client sends and the field order within each type. On a mismatch the condition evaluates to `false`, which means a DENY rule never fires and a permissive ALLOW rule signs the request anyway. #### Determine the types map the client sends The x402 library defines only `TransferWithAuthorization`. Whether `EIP712Domain` also reaches Privy depends on how the wallet is wired up: | Client | `types` in the signing request | | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------- | | `createX402Client` from `@privy-io/node` | `TransferWithAuthorization` only | | `useX402Fetch` from `@privy-io/react-auth` | `TransferWithAuthorization` only | | A viem `WalletClient` wired to Privy directly, or any integration that builds the `eth_signTypedData_v4` call itself, including a platform that signs on a developer's behalf such as AgentCore Payments | `EIP712Domain` **and** `TransferWithAuthorization` | Both Privy clients hand x402 a viem `LocalAccount`, which x402 calls directly, so its types map is forwarded untouched. A `WalletClient` instead routes through viem's `signTypedData` action, which inserts an `EIP712Domain` entry derived from the domain fields. The example below uses the shape both Privy clients produce. To screen requests that carry `EIP712Domain`, add it to the same `types` map, listing only the domain fields the client actually sends, in the order `name`, `version`, `chainId`, `verifyingContract`: ```ts {skip-check} theme={"system"} EIP712Domain: [ {name: 'name', type: 'string'}, {name: 'version', type: 'string'}, {name: 'chainId', type: 'uint256'}, {name: 'verifyingContract', type: 'address'} ], ``` If an app signs through more than one of these paths, give each shape its own DENY rule. A rule only screens requests whose `types` map it matches exactly, so one rule cannot cover both. Confirm the shape against a real request rather than inferring it. Send one payment and inspect the `typed_data.types` the app submits, then mirror it exactly. If the policy declares a map the client does not send, both rules stop matching and the request is denied by default, so the symptom is every payment failing rather than a sanctioned payment slipping through. `EIP712Domain` also lists only the domain fields actually present, so a client that omits `name` or `version` produces a different map again. ```ts {skip-check} theme={"system"} const SANCTIONS_SET_ID = 'qvah5m2hmp9abqlxdmfiht95'; // The types map the Privy x402 clients send for an EIP-3009 authorization. // This must match the signing request exactly, including field order. Add an // EIP712Domain entry here if the app wires up its own viem WalletClient. const transferWithAuthorization = { types: { TransferWithAuthorization: [ {name: 'from', type: 'address'}, {name: 'to', type: 'address'}, {name: 'value', type: 'uint256'}, {name: 'validAfter', type: 'uint256'}, {name: 'validBefore', type: 'uint256'}, {name: 'nonce', type: 'bytes32'} ] }, primary_type: 'TransferWithAuthorization' }; const policy = await privy.policies().create({ version: '1.0', name: 'OFAC sanctions denylist (x402)', chain_type: 'ethereum', owner_id: '', rules: [ // Deny x402 payments whose recipient is on the denylist. { name: 'Deny sanctioned x402 recipients', method: 'eth_signTypedData_v4', action: 'DENY', conditions: [ { field_source: 'ethereum_typed_data_message', typed_data: transferWithAuthorization, field: 'to', operator: 'in_condition_set', value: SANCTIONS_SET_ID } ] }, // Allow x402 authorizations that clear the denylist. Pinning the same types map here // means a client-side schema change fails closed: both rules stop matching, no rule // returns an action, and the request is denied by default. { name: 'Allow x402 authorizations on Base', method: 'eth_signTypedData_v4', action: 'ALLOW', conditions: [ { field_source: 'ethereum_typed_data_domain', field: 'chainId', operator: 'eq', value: '8453' }, { field_source: 'ethereum_typed_data_domain', field: 'verifyingContract', operator: 'eq', // The USDC contract on the target chain, as sent in the EIP-712 domain. value: '' }, // Trivially true. Present so the rule is bound to the same message schema // as the DENY rule above. { field_source: 'ethereum_typed_data_message', typed_data: transferWithAuthorization, field: 'value', operator: 'gte', value: '0' } ] } ] }); await privy.wallets().update('', {policy_ids: [policy.id]}); ``` Add one ALLOW rule per typed-data message shape the app signs. A rule pinned to `TransferWithAuthorization` denies every other message type, which is the intended behavior for a payments-only wallet but breaks apps that also sign permits or logins. For Tempo, keep the same rule shape and set the domain conditions to Tempo's chain ID and USDC contract. If the app also moves value through ordinary transactions, add matching DENY rules on `eth_sendTransaction` and `eth_signTransaction` that screen `ethereum_transaction.to` for native transfers and the decoded calldata recipient for ERC-20 `transfer` and `transferFrom`. ### Solana ```ts {skip-check} theme={"system"} const SANCTIONS_SET_ID = ''; const SIGNING_METHODS = ['signTransaction', 'signAndSendTransaction']; const policy = await privy.policies().create({ version: '1.0', name: 'OFAC sanctions denylist (x402)', chain_type: 'solana', owner_id: '', rules: [ // Deny SPL (USDC) transfers to sanctioned destinations. Rule names must be // 50 characters or fewer, so keep generated names short. ...SIGNING_METHODS.flatMap((method) => ['Transfer.destination', 'TransferChecked.destination'].map((field) => ({ name: `Deny sanctioned ${field}`, method, action: 'DENY', conditions: [ { field_source: 'solana_token_program_instruction', field, operator: 'in_condition_set', value: SANCTIONS_SET_ID } ] })) ), // Allow the rest of the app's legitimate signing. Both signing methods need an ALLOW // rule: a method with no rule that returns an action is denied by default. Solana // rules must declare at least one condition, so this uses a trivially true one. ...SIGNING_METHODS.map((method) => ({ name: 'Allow other instructions', method, action: 'ALLOW', conditions: [ { field_source: 'system', field: 'current_unix_timestamp', operator: 'gte', value: '0' } ] })) ] }); ``` Solana evaluation requires **every** instruction in the transaction to be allowed by at least one rule. An x402 payment usually carries more than the token transfer, such as a compute budget instruction, so narrowing the ALLOW rule to `instructionName in ['Transfer', 'TransferChecked']` rejects the whole transaction. Narrow the ALLOW rule only after confirming which instructions the x402 client actually emits. The policy engine evaluates every instruction in a Solana transaction, so a DENY on any single instruction rejects the whole transaction. ### The transfer action Rules are scoped to a method, so the rules above only screen the signing methods they name. An app that also moves funds through the high-level [transfer](/wallets/actions/transfer/policies) action needs its own rules, screening `destination.address` from the request body. ```ts {skip-check} theme={"system"} const rules = [ { name: 'Deny sanctioned transfer destinations', method: 'transfer', action: 'DENY', conditions: [ { field_source: 'action_request_body', field: 'destination.address', operator: 'in_condition_set', value: SANCTIONS_SET_ID } ] }, { name: 'Allow USDC transfers on Base', method: 'transfer', action: 'ALLOW', conditions: [ {field_source: 'action_request_body', field: 'source.asset', operator: 'eq', value: 'usdc'}, {field_source: 'action_request_body', field: 'source.chain', operator: 'eq', value: 'base'} ] } ]; ``` Unlike the signing conditions above, `action_request_body` comparisons are **exact**. A denylist holding a checksummed address does not match the same address sent in lowercase, and the DENY rule silently fails to fire. Normalize destination addresses to a single form before calling `transfer`, or store both forms in the condition set. A policy that omits `transfer` rules entirely does not leave transfers unscreened: an unmatched method is denied by default, so every `transfer` call fails until a rule allows it. ## 5. Attach the policy to wallets Attach the policy at creation so no wallet ever exists without the denylist: ```shell theme={"system"} curl -X POST https://api.privy.io/v1/wallets \ -u ":" \ -H "privy-app-id: " \ -H "Content-Type: application/json" \ -d '{ "chain_type": "ethereum", "policy_ids": [""] }' ``` For existing wallets, patch them. If the wallet has an `owner_id`, the update must be authorized by that owner: ```shell theme={"system"} curl -X PATCH https://api.privy.io/v1/wallets/ \ -u ":" \ -H "privy-app-id: " \ -H "privy-authorization-signature: " \ -H "Content-Type: application/json" \ -d '{ "policy_ids": [""] }' ``` To apply different denylists to different signers on the same wallet, attach the policy as an override policy on the signer instead. See [conditional signer policies](/recipes/wallets/conditional-signer-policies). When a third party creates wallets on an app's behalf, the policy ID must reach that integration. Pass it alongside the app ID, app secret, and authorization key so wallets are created with `policy_ids` already set. ## 6. Reject consistently and safely When the policy denies a request, Privy does not sign it and returns a `policy_violation` error. Map it to a stable, client-safe response: * return a consistent 4xx, either 403 or 422 * use a generic message that does not reveal screening details * record an internal audit event with policy version, matched address, chain, timestamp, and request identifier * route the event to the compliance or risk-review workflow ```json theme={"system"} { "error": "transaction_blocked_by_policy", "message": "This payment cannot be completed under your organization's transfer policy." } ``` Do not echo the matched address, condition set ID, or rule name to end users. Detailed rejection reasons let a caller enumerate the denylist by probing. ## 7. Decide behavior when the sanctions source is unavailable Enforcement does not depend on the sanctions source being reachable at signing time, because the condition set is the versioned local cache. What the app must decide is how the sync job behaves when the source is unavailable, and how stale a list it tolerates. Document one of: * **Fail closed**: if the list cannot be refreshed within the maximum tolerated age, tighten the policy to deny the affected methods. * **Fail open with alerting**: keep enforcing against the last known-good set and alert immediately. * **Recommended**: enforce continuously against the condition set, refresh asynchronously, and continue on the last known-good contents up to a defined maximum age, then apply the chosen fail mode. Refresh the list in place. The policy never changes, so this requires no redeploy and no re-signature: ```shell theme={"system"} curl -X PUT https://api.privy.io/v1/condition_sets/qvah5m2hmp9abqlxdmfiht95/condition_set_items \ -u ":" \ -H "privy-app-id: " \ -H "privy-authorization-signature: " \ -H "Content-Type: application/json" \ -d '[{ "value": "0x..." }, { "value": "0x..." }]' ``` If a condition set is deleted, every condition referencing it evaluates to `false`. The denylist silently stops blocking while ALLOW rules keep passing traffic. Give the set an owner, and alert on an unexpectedly empty, shrunken, or missing set. ## 8. Test before enabling Cover at least these cases in a non-production environment: | Case | Expected | | :------------------------------------------------------------------ | :------------------------------------- | | x402 payment (`eth_signTypedData_v4`) to an unlisted recipient | Allowed | | x402 payment to a listed recipient (`to` in the message) | Rejected | | Listed EVM address submitted in the opposite casing (signing paths) | Rejected | | `transfer` to a listed `destination.address`, exact casing | Rejected | | `transfer` to that address in the opposite casing | Rejected only if both forms are stored | | Request whose `types` map differs from the policy's | Rejected by default DENY | | Solana SPL transfer to a listed destination | Rejected | | Stale list or sync outage | Follows the documented fail mode | | Every rejection | Auditable, exposes no internal detail | Run the allowed and rejected cases against the same wallet and policy. A DENY rule that never fires looks identical to a correctly configured policy until a listed address is tested explicitly. ## Limitations * **Direct recipients only.** The engine screens the recipient in the signing request. Funds routed through a bridge, mixer, or intermediary that later reach a sanctioned address are not caught. Use a screening provider for indirect exposure. * **Typed-data screening needs the schema.** An `ethereum_typed_data_message` condition only matches messages whose `types` map matches the declared one exactly. Screen every message type the app signs over. * **Solana SPL destinations are token accounts.** `TransferChecked.destination` is an associated token account, not the owner's wallet address. To screen owners, derive and store the associated token accounts for each sanctioned address and each supported mint. * **Solana address lookup tables.** Policy evaluation cannot resolve addresses stored in an address lookup table. Keep screened addresses in the transaction's static account keys. * **Wallets without the policy are unprotected.** Enforcement is per wallet. Audit that every wallet carries the policy. ## Further reading * [Policies overview](/controls/policies/overview) * [Condition sets](/controls/policies/condition-sets) * [Ethereum policy examples](/controls/policies/example-policies/ethereum) * [Solana policy examples](/controls/policies/example-policies/solana) * [Using x402 payments with Privy](/recipes/agent-integrations/x402) * [EIP-3009 standard](https://eips.ethereum.org/EIPS/eip-3009) * [OFAC SDN list](https://sanctionslist.ofac.treas.gov/Home/SdnList) and the [machine-readable SDN enhanced XML export](https://sanctionslistservice.ofac.treas.gov/api/PublicationPreview/exports/SDN_ENHANCED.XML) # Migrating sign in with Apple users for an Apple team transfer Source: https://docs.privy.io/recipes/apple-team-migration When transferring an iOS app between Apple Developer Teams (e.g., selling an app or moving to a new organization), Sign in with Apple user identifiers (`sub`) change because they are team-scoped. Users who chose "Hide My Email" also receive new private relay email addresses under the new team. Without migrating these identifiers in Privy, affected users will lose access to their existing accounts. Throughout this guide, **Team A** refers to the current team that owns the app today, and **Team B** refers to the destination team receiving the app. You have **60 days** from the date of the app transfer to complete the migration. After that, Apple's migration endpoints become inactive. If you miss this window, the app must be transferred back and the process restarted. See [TN3159: Migrating users after the 60-day app transfer period](https://developer.apple.com/documentation/technotes/tn3159-migrating-sign-in-with-apple-users-for-an-app-transfer#Migrating-users-after-the-60-day-app-transfer-period). ## What changes during an Apple team transfer | What changes | Impact on Privy | | ------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | | Every user gets a new team-scoped `sub` (subject identifier) | Privy matches users by subject — logins will fail if subjects aren't updated | | "Hide My Email" users get a new `@privaterelay.appleid.com` address | Email-based fallback matching fails, causing duplicate accounts | | Both teams' credentials remain valid during the 60-day window | No immediate login disruption, but credentials must be updated before the window closes | ## Before the transfer (Team A) ### Step 1: Export your Apple users from Privy Use the Privy API to [fetch all users](/user-management/users/managing-users/querying-users) for your app, then filter for users with `apple_oauth` linked accounts. For each matching user, extract their Privy ID, Apple subject, and email. For each Apple OAuth account, you will need: * **Privy ID** (`privy_id`) * **Apple subject** (the current `sub` stored in Privy) * **Email** (the email currently stored for the Apple OAuth account) Save this list — you will need each user's Apple `sub` in step 3 to generate transfer identifiers via Apple's API. ### Step 2: Disable Apple login (strongly recommended) Temporarily remove Apple as a login method in the Privy dashboard configuration. There is a race condition between when the app transfer completes (Apple starts issuing new `sub` values) and when Privy's subject migration is run. If a user signs in during this window, their new `sub` won't match any stored subject, and if they used "Hide My Email," their new relay email won't match either so we don't do any automatic account merging. This results in a **duplicate account** being created. This is especially important if: * Your users have wallets, balances, or other critical state tied to their accounts * A significant portion of your users use "Hide My Email" (private relay) If your app has very few Apple sign-in users and you can execute the migration quickly, it may be acceptable to skip this step and accept the risk of needing to manually resolve duplicates. ### Step 3: Generate transfer identifiers Using Team A's credentials, generate a `transfer_sub` for each user. Follow Apple's guide: [Transferring your apps and users to another team](https://developer.apple.com/documentation/sign_in_with_apple/transferring_your_apps_and_users_to_another_team). **Obtain an access token for Team A:** ```bash theme={"system"} curl -X POST "https://appleid.apple.com/auth/token" \ -H 'Content-Type: application/x-www-form-urlencoded' \ -d 'grant_type=client_credentials' \ -d 'scope=user.migration' \ -d 'client_id=YOUR_CLIENT_ID' \ -d 'client_secret=CLIENT_SECRET_SIGNED_BY_TEAM_A' ``` **For each user, generate a transfer identifier:** ```bash theme={"system"} curl -X POST "https://appleid.apple.com/auth/usermigrationinfo" \ -H 'Content-Type: application/x-www-form-urlencoded' \ -H 'Authorization: Bearer ACCESS_TOKEN_FOR_TEAM_A' \ -d 'sub=USERS_CURRENT_APPLE_SUB' \ -d 'target=TEAM_B_TEAM_ID' \ -d 'client_id=YOUR_CLIENT_ID' \ -d 'client_secret=CLIENT_SECRET_SIGNED_BY_TEAM_A' ``` Apple returns: ```json theme={"system"} { "transfer_sub": "760417.ebbf12acbc78e1be1668ba852d492d8a.1827" } ``` Save the `transfer_sub` alongside each user's `privy_id` and old `sub`. ### Step 4: Initiate the app transfer Transfer the app in [App Store Connect](https://developer.apple.com/help/app-store-connect/transfer-an-app/overview-of-app-transfer). Once Team B accepts, Apple begins issuing Team B-scoped identifiers. ## After the transfer (Team B) ### Step 5: Exchange transfer identifiers for new identifiers Using Team B's credentials, exchange each `transfer_sub` for the new team-scoped `sub` and (if applicable) the new private relay email. Follow Apple's guide: [Bringing new apps and users into your team](https://developer.apple.com/documentation/sign_in_with_apple/bringing_new_apps_and_users_into_your_team). **Obtain an access token for Team B:** ```bash theme={"system"} curl -X POST "https://appleid.apple.com/auth/token" \ -H 'Content-Type: application/x-www-form-urlencoded' \ -d 'grant_type=client_credentials' \ -d 'scope=user.migration' \ -d 'client_id=YOUR_CLIENT_ID' \ -d 'client_secret=CLIENT_SECRET_SIGNED_BY_TEAM_B' ``` **For each user, exchange the transfer identifier:** ```bash theme={"system"} curl -X POST "https://appleid.apple.com/auth/usermigrationinfo" \ -H 'Content-Type: application/x-www-form-urlencoded' \ -H 'Authorization: Bearer ACCESS_TOKEN_FOR_TEAM_B' \ -d 'transfer_sub=TRANSFER_SUB_FROM_TEAM_A' \ -d 'client_id=YOUR_CLIENT_ID' \ -d 'client_secret=CLIENT_SECRET_SIGNED_BY_TEAM_B' ``` Apple returns the new team-scoped identifiers: ```json theme={"system"} { "sub": "820417.faa325acbc78e1be1668ba852d492d8a.0219", "email": "ep9ks2tnph@privaterelay.appleid.com", "is_private_email": true } ``` * **`sub`** — the new Team B-scoped user identifier (present for all users) * **`email`** — the new private relay email (only present for users who used "Hide My Email") * **`is_private_email`** — indicates this is a relay address Save the new `sub` and `email` (when present) alongside each user's `privy_id`. ### Step 6: Build the migration CSV Prepare a CSV with the following columns: ```csv theme={"system"} privy_id,old_apple_sub,email,new_apple_sub,new_email did:privy:user1,001234.old-sub-a.5678,oldrelay@privaterelay.appleid.com,820417.new-sub-b.0219,newrelay@privaterelay.appleid.com did:privy:user2,001234.old-sub-c.9012,realuser@gmail.com,820417.new-sub-d.3456, ``` | Column | Required | Description | | --------------- | -------- | ------------------------------------------------------------------------------------ | | `privy_id` | Yes | The user's Privy ID (e.g., `did:privy:abc123`) | | `old_apple_sub` | Yes | The current Apple subject stored in Privy (Team A's `sub`) | | `email` | No | The email currently stored in Privy for this Apple account, used for verification | | `new_apple_sub` | Yes | The new Apple subject from step 5 (Team B's `sub`) | | `new_email` | No | The new private relay email from step 5 (include when `is_private_email` was `true`) | For users who shared their real email address (not "Hide My Email"), leave `new_email` blank. Real email addresses are not team-scoped and don't change during migration — Apple's exchange response won't include an `email` field for these users. ### Step 7: Submit the migration to Privy Provide the CSV to Privy support to run the Apple subject migration. This updates each user's stored subject and, where provided, their stored email to the new Team B values. We'll reach out once the migration is complete on our end. ### Step 8: Update Apple OAuth credentials in Privy This step can be done while waiting for the subject migration in step 7 to complete — they are independent. Update the app's Apple OAuth configuration with Team B's credentials: * **Key ID** — the Key ID for Team B's Sign in with Apple private key * **Private key** — Team B's `.p8` private key file * **Team ID** — Team B's 10-character Team ID * **Client ID** — this is typically the bundle ID and stays the same after transfer The **Key ID** and **private key** can be updated directly in the [Privy Dashboard](https://dashboard.privy.io). However, the **Team ID** and **Client ID** are read-only in the dashboard once users exist. Contact Privy support to update these fields. Both teams' credentials remain valid during the 60-day migration window, so this step doesn't need to happen before the subject migration. However, it **must** be completed before the 60-day window closes or Apple login will stop working. ### Step 9: Re-enable Apple login and verify Once the subject migration (step 7) and credential update (step 8) are both complete, re-enable Apple as a login method in the Privy dashboard. Then verify the migration by testing with a small number of users: 1. Have a user sign in with Apple through the app 2. Verify they land on their **existing account** — same Privy ID, same linked accounts, same wallets and data 3. If possible, also test with: * A user who used "Hide My Email" (private relay) — these are most likely to be affected * A user who shared their real email address * A brand new user signing up for the first time post-transfer If anything is wrong, you can disable Apple login again while investigating. ## Troubleshooting ### Duplicate accounts were created If users signed in during the migration window (between the transfer and the subject migration), they may have new duplicate accounts. Contact Privy support with the affected Privy IDs to resolve these. ### The 60-day window has passed If more than 60 days have elapsed since the transfer, Apple's migration endpoints are no longer active. The app must be transferred back to Team A, and the process restarted from step 3. See [TN3159: Migrating users after the 60-day app transfer period](https://developer.apple.com/documentation/technotes/tn3159-migrating-sign-in-with-apple-users-for-an-app-transfer#Migrating-users-after-the-60-day-app-transfer-period). ## Apple documentation * [TN3159: Migrating Sign in with Apple users for an app transfer](https://developer.apple.com/documentation/technotes/tn3159-migrating-sign-in-with-apple-users-for-an-app-transfer) * [Transferring your apps and users to another team](https://developer.apple.com/documentation/sign_in_with_apple/transferring_your_apps_and_users_to_another_team) * [Bringing new apps and users into your team](https://developer.apple.com/documentation/sign_in_with_apple/bringing_new_apps_and_users_into_your_team) * [TN3107: Resolving Sign in with Apple response errors](https://developer.apple.com/documentation/technotes/tn3107-resolving-sign-in-with-apple-response-errors) # Authentication Source: https://docs.privy.io/recipes/authentication/overview Privy authentication supports account creation and sign-in flows across social, messaging, and custom identity systems. Launch with instant sign-in and progressive account upgrades. Add one-tap Telegram sign-in with wallet provisioning. Authenticate users from Farcaster mini apps. Configure phone-based authentication in the dashboard. Connect Supabase sessions to Privy user authentication. Port custom JWT authentication flows to Privy. # Using Supabase as an authentication provider Source: https://docs.privy.io/recipes/authentication/using-supabase-for-custom-auth This guide demonstrates how to integrate Supabase's authentication system with Privy to create a custom authentication flow. This setup allows you to leverage Supabase's powerful authentication and backend features, including Row Level Security (RLS) for data access control, while managing user wallets in Privy. ## Configure your Supabase project Before integrating with Privy, you need to configure your Supabase project to use JWT tokens for authentication. Follow the [Supabase JWT signing keys documentation](https://supabase.com/docs/guides/auth/signing-keys) to: 1. Migrate your Supabase project to use the new JWT signing keys. 2. Get the JWKS endpoint URL, which will look like `https://[PROJECT_ID].supabase.co/auth/v1/.well-known/jwks.json`. 3. Ensure your Supabase project is using an asymmetric signing algorithm. Make sure to complete the JWT signing key migration in Supabase before proceeding with the Privy integration. This ensures your tokens will be properly validated. ## Configure your Privy project Navigate to your Privy dashboard and configure JWT-based authentication following the [custom authentication guide](/authentication/user-authentication/jwt-based-auth/setup). ## Configure your Next.js project ### 1. Create Supabase clients Create separate Supabase clients for server-side and client-side operations. ```typescript lib/supabase/server.ts theme={"system"} import { createServerClient } from '@supabase/ssr' import { cookies } from 'next/headers' export async function createSupabaseServer(token?: string) { const cookieStore = cookies() return createServerClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!, { cookies: { get(name: string) { return cookieStore.get(name)?.value }, set(name: string, value: string, options: any) { cookieStore.set({ name, value, ...options }) }, remove(name: string, options: any) { cookieStore.set({ name, value: '', ...options }) }, }, global: { headers: token ? { Authorization: `Bearer ${token}` } : {}, }, } ) } ``` ```typescript lib/supabase/client.ts theme={"system"} import { createBrowserClient } from '@supabase/ssr' export function createSupabaseClient(token?: string) { return createBrowserClient( process.env.NEXT_PUBLIC_SUPABASE_URL!, process.env.NEXT_PUBLIC_SUPABASE_PUBLISHABLE_KEY!, ) } ``` ### 2. Create a Supabase provider and hook Create a provider that integrates Privy authentication with Supabase: ```tsx components/SupabaseProvider.tsx theme={"system"} 'use client'; import {createContext, useContext, useEffect, useMemo, useState} from 'react'; import {SupabaseClient, Session, User} from '@supabase/supabase-js'; import {usePathname, useRouter} from 'next/navigation'; import {createClient} from '@/lib/supabase/client'; interface SupabaseContextType { supabase: SupabaseClient; session: Session | null; user: User | null; loading: boolean; } const SupabaseContext = createContext(undefined); export const SupabaseProvider = ({children}: {children: React.ReactNode}) => { const [session, setSession] = useState(null); const [user, setUser] = useState(null); const [loading, setLoading] = useState(true); const router = useRouter(); const pathname = usePathname(); const supabase = useMemo(() => createClient(), []); useEffect(() => { supabase.auth .getSession() .then(({data: {session}}) => { setSession(session); setUser(session?.user || null); setLoading(false); }) .catch(() => { setLoading(false); }); const {data: authListener} = supabase.auth.onAuthStateChange(async (event, currentSession) => { setSession(currentSession); setUser(currentSession?.user || null); setLoading(false); // Optional: Redirect based on auth state if (event === 'SIGNED_IN' || event === 'TOKEN_REFRESHED') { // console.log("User signed in or token refreshed"); // router.push("/dashboard"); // Example redirect } else if (event === 'SIGNED_OUT') { // console.log("User signed out"); router.push('/'); // Example redirect } }); return () => { authListener.subscription.unsubscribe(); }; }, [pathname]); return ( {children} ); }; export const useSupabase = () => { const context = useContext(SupabaseContext); if (context === undefined) { throw new Error('useSupabase must be used within a SupabaseProvider'); } return context; }; ``` ### 3. Create a Providers component and add to root layout Create a combined providers component and wrap your application with it in the root layout: ```tsx components/Providers.tsx theme={"system"} 'use client'; import {PrivyProvider} from '@privy-io/react-auth'; import {SupabaseProvider, useSupabase} from './SupabaseProvider'; export default function Providers({children}: {children: React.ReactNode}) { return ( {children} ); } function InnerPrivyProvider({children}: {children: React.ReactNode}) { const {loading, supabase, session} = useSupabase(); async function getCustomAuthToken() { if (!session) return undefined; const {data, error} = await supabase.auth.getSession(); if (error) { console.error('Error getting session:', error); return undefined; } return data.session?.access_token || undefined; } return ( {children} ); } ``` The `getCustomAuthToken` function retrieves the current session's access token from Supabase and passes it to Privy's `getCustomAccessToken` configuration. Privy uses this token to validate the user's authentication state through the JWKS endpoint configured in your Privy dashboard. ### 4. Just use Privy! You can now access the Privy user object, create wallets and sign messages! ## Conclusion With this setup complete, you now have a fully integrated Privy and Supabase authentication system. You can: * Use Supabase for user management, database operations, and real-time features. * Leverage Privy's wallet management capabilities. * Customize your authentication flow to match your brand and UI while taking advantage of Supabase RLS! # Sending batch transactions Source: https://docs.privy.io/recipes/batch-transactions Batch transactions allow your app to send multiple operations in a single request. This is useful for executing multi-step workflows — such as approving and swapping tokens, distributing funds to multiple recipients, or performing complex DeFi operations — without requiring separate transactions for each step. ## How it works Privy supports batch transactions on both EVM and Solana chains, but the mechanism differs for each. ### EVM On EVM chains, Privy uses the [`wallet_sendCalls`](/api-reference/wallets/ethereum/wallet-send-calls) RPC method to send multiple calls in a single atomic batch. Under the hood, Privy leverages [EIP-7702](https://eip7702.io/) to upgrade your user's wallet to a [Kernel smart contract](https://github.com/zerodevapp/kernel), which enables batched execution. * **Without sponsorship**: The wallet executes a `self.execute` call on the upgraded Kernel contract, bundling all calls into a single atomic transaction. * **With sponsorship**: Privy routes the batch through a **bundler** and **paymaster**, which covers the gas fees on behalf of the user. Set `sponsor` to `true` in the request body to enable this. In both cases, batch calls are **atomic** — either all calls succeed, or the entire batch reverts. This ensures consistent state and prevents partial execution. ### Solana On Solana, batch behavior is achieved by adding multiple instructions to a single `Transaction`. The Solana runtime processes all instructions in a transaction sequentially, and the transaction is **atomic** — if any instruction fails, the entire transaction is reverted. Learn more about sending Solana transactions in the [Solana recipes](/recipes/solana/send-sol). ## Sending batch transactions on EVM Use the `wallet_sendCalls` method via the Privy wallets RPC endpoint to send multiple calls in a single batch. ### Example: sending two transfers ```typescript {skip-check} theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID!, appSecret: process.env.PRIVY_APP_SECRET! }); const response = await privy .wallets() .ethereum() .sendCalls('insert-wallet-id', { caip2: 'eip155:8453', // Base params: { calls: [ { to: '0xRecipientAddress1', value: '0x2386F26FC10000' // 0.01 ETH in wei }, { to: '0xRecipientAddress2', value: '0x2386F26FC10000' // 0.01 ETH in wei } ] } }); ``` ```bash theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "wallet_sendCalls", "caip2": "eip155:8453", "chain_type": "ethereum", "params": { "calls": [ { "to": "0xRecipientAddress1", "value": "0x2386F26FC10000" }, { "to": "0xRecipientAddress2", "value": "0x2386F26FC10000" } ] } }' ``` ### Example: approve and swap in one batch A common use case is combining an ERC-20 approval with a swap in a single atomic batch: ```typescript theme={"system"} export {}; declare const swapRouterAbi: any; declare const swapParams: any; declare const privy: any; import {encodeFunctionData, erc20Abi} from 'viem'; // Encode the approval call const approveData = encodeFunctionData({ abi: erc20Abi, functionName: 'approve', args: ['0xSwapRouterAddress', BigInt(1000000)] // Approve 1 USDC (6 decimals) }); // Encode the swap call (example ABI) const swapData = encodeFunctionData({ abi: swapRouterAbi, functionName: 'exactInputSingle', args: [swapParams] }); const response = await privy .wallets() .ethereum() .sendCalls('insert-wallet-id', { caip2: 'eip155:8453', params: { calls: [ { to: '0xUSDCContractAddress', data: approveData }, { to: '0xSwapRouterAddress', data: swapData } ] } }); ``` ### With gas sponsorship To sponsor gas for the batch transaction, set `sponsor` to `true`: ```bash theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "wallet_sendCalls", "caip2": "eip155:8453", "chain_type": "ethereum", "sponsor": true, "params": { "calls": [ { "to": "0xRecipientAddress1", "value": "0x2386F26FC10000" }, { "to": "0xRecipientAddress2", "value": "0x2386F26FC10000" } ] } }' ``` ### Response A successful request returns a `transaction_id` and the `caip2` chain identifier: ```json theme={"system"} { "method": "wallet_sendCalls", "data": { "transaction_id": "b4966a89-8983-4b1b-a93a-b104799527f5", "caip2": "eip155:8453" } } ``` Use the `transaction_id` to [track the status of the transaction](/api-reference/transactions/get). ## Sending batch transactions on Solana On Solana, batching is done by adding multiple instructions to a single transaction. See the [Sending SOL](/recipes/solana/send-sol) recipe for a complete walkthrough. Here is an example of batching two SOL transfers in a single transaction: ```typescript theme={"system"} import {Connection, PublicKey, SystemProgram, Transaction, LAMPORTS_PER_SOL} from '@solana/web3.js'; const connection = new Connection('https://api.mainnet-beta.solana.com', 'confirmed'); const fromPubkey = new PublicKey('insert-wallet-address'); // Create a transaction with multiple instructions const transaction = new Transaction(); // First transfer transaction.add( SystemProgram.transfer({ fromPubkey, toPubkey: new PublicKey('0xRecipientAddress1'), lamports: 0.01 * LAMPORTS_PER_SOL }) ); // Second transfer transaction.add( SystemProgram.transfer({ fromPubkey, toPubkey: new PublicKey('0xRecipientAddress2'), lamports: 0.02 * LAMPORTS_PER_SOL }) ); const {blockhash} = await connection.getLatestBlockhash(); transaction.recentBlockhash = blockhash; transaction.feePayer = fromPubkey; ``` Then sign and send the transaction using any of the Privy SDKs as shown in the [Sending SOL](/recipes/solana/send-sol#2-send-the-transaction) recipe. ## Next steps * [API reference: `wallet_sendCalls`](/api-reference/wallets/ethereum/wallet-send-calls) — Full parameter and response documentation * [Gas sponsorship](/wallets/gas-and-asset-management/gas/overview) — Learn how to sponsor gas for batch transactions * [Sending SOL](/recipes/solana/send-sol) — Solana transaction recipes * [Sending USDC](/recipes/send-usdc) — Formatting ERC-20 token transfers # Onramping to MXNB with Juno Source: https://docs.privy.io/recipes/bitso-mxnb-onramp How to integrate Privy and Juno to enable onramping to MXNB, the Mexican Peso stablecoin [Bitso](https://bitso.com) is the largest crypto exchange in Mexico. The [Juno](https://docs.bitso.com/juno/docs/overview-of-basic-operations) platform lets businesses issue and redeem MXNB. MXNB is a stablecoin pegged 1:1 to the Mexican Peso, deployed on Arbitrum. Juno automatically triggers an issuance when a user deposits MXN via [SPEI](#spei-and-clabe) to a Juno-issued [CLABE](#spei-and-clabe). This recipe shows how to combine Privy embedded wallets with the Juno API to build an MXNB onramp flow. It is most useful for centralized exchanges or fintech apps serving Mexico-based users. Using Privy and Juno, your app can: * Provision an embedded wallet for each user * Guide users through Bitso's KYB process * Issue a CLABE to receive SPEI deposits * Automatically mint MXNB when Juno receives MXN * Withdraw MXNB to the user's Privy wallet on Arbitrum ## Prerequisites * A [Privy account](https://dashboard.privy.io) with an app configured * A [Juno](https://docs.bitso.com/juno/docs/overview-of-basic-operations) account with API credentials. See [Juno's guide to creating signed requests](https://docs.bitso.com/juno/docs/create-signed-requests) for how to generate your API key and Bearer token. * Users must complete Bitso's KYB process before minting or redeeming MXNB ## SPEI and CLABE **SPEI** (Sistema de Pagos Electrónicos Interbancarios) is Mexico's interbank electronic payment network, operated by Banco de México. It enables near-instant MXN transfers between any Mexican bank account. **CLABE** (Clave Bancaria Estandarizada) is the standardized 18-digit account number used for SPEI transfers. Juno issues each user a dedicated `AUTO_PAYMENT` CLABE. When MXN arrives at that CLABE, Juno automatically mints the equivalent MXNB to the user's Juno balance. All API examples use the Juno staging environment at `stage.buildwithjuno.com`. Switch to `buildwithjuno.com` for production. ## 1. Complete KYB with Bitso Before MXNB can be issued to a user, Bitso requires KYB (Know Your Business) verification. Your app should redirect users to the Bitso onboarding flow and track their verification status via the Juno API. Refer to [Bitso's KYB documentation](https://docs.bitso.com/juno/docs) for verification requirements and endpoints. KYB approval is required before any issuance or redemption. After KYB is approved, users must log in to the [Juno web platform](https://buildwithjuno.com) and accept the Terms & Conditions via the UI before your app can use the Juno API or UI on their behalf. ## 2. Create a CLABE for the user Juno issues `AUTO_PAYMENT` CLABEs — when MXN arrives via SPEI to a user's CLABE, Juno automatically mints MXNB to that user's Juno balance. ```bash theme={"system"} curl --request POST \ --url https://stage.buildwithjuno.com/mint_platform/v1/clabes \ --header 'Authorization: Bitso:::' \ --header 'BitsoAuth: Bearer ' ``` ```json theme={"system"} { "success": true, "payload": { "clabe": "710969000000329002", "type": "AUTO_PAYMENT" } } ``` Save the `clabe` value. Each CLABE is assigned to one user and must not be reused across different users. ## 3. Display the CLABE to the user Your app must show the user the CLABE so they can initiate a SPEI transfer from their bank. Once Bitso receives the MXN deposit (minimum 100 MXN), MXNB is automatically minted to the user's Juno balance. To retrieve an existing CLABE for a user, call `GET https://stage.buildwithjuno.com/spei/v1/clabes?clabe_type=AUTO_PAYMENT`. ## 4. Create a Privy wallet for the user Your app can create an embedded Ethereum wallet for the user via the Privy API. MXNB is deployed on Arbitrum, an EVM-compatible chain, so a standard Ethereum wallet can hold it. ```bash theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets \ -u ":" \ --header 'privy-app-id: ' \ --header 'Content-Type: application/json' \ --data '{ "owner": { "user_id": "" }, "chain_type": "ethereum" }' ``` Save the returned wallet `address`. Juno uses this address as the MXNB withdrawal destination. ## 5. Register the Privy wallet as a blockchain account in Juno Before withdrawing MXNB to the Privy wallet, your app must register the wallet address with Juno as a blockchain account. ```bash theme={"system"} curl --request POST \ --url https://stage.buildwithjuno.com/mint_platform/v1/accounts/blockchain \ --header 'Authorization: Bitso:::' \ --header 'BitsoAuth: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "tag": "", "network": "arbitrum", "address": "" }' ``` ```json theme={"system"} { "success": true, "payload": {} } ``` ## 6. Withdraw MXNB to the Privy wallet Once MXNB is in the user's Juno balance, your app can withdraw it to the user's Privy wallet on Arbitrum. ```bash theme={"system"} curl --request POST \ --url https://stage.buildwithjuno.com/mint_platform/v1/withdrawals \ --header 'Authorization: Bitso:::' \ --header 'BitsoAuth: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "blockchain": "ARBITRUM", "asset": "MXNB", "amount": "1000", "address": "", "compliance": {} }' ``` The `compliance` field is required. Pass an empty object `{}` for amounts under the travel rule threshold. Refer to [Bitso's travel rule documentation](https://docs.bitso.com/juno/docs/withdrawals-under-the-travel-rule-threshold) for larger amounts. Pass an `X-Idempotency-Key` header (UUID v1) to prevent duplicate withdrawals if the request is retried. Learn more about MXNB issuances, redemptions, and withdrawals API reference for provisioning embedded wallets # OAuth with Capacitor Source: https://docs.privy.io/recipes/capacitor-oauth **[Capacitor](https://capacitorjs.com/) is a cross-platform native runtime that makes it easy to build modern web apps that run natively on iOS, Android, and the web.** Capacitor apps can leverage native device capabilities while maintaining a single codebase. **Privy enables your Capacitor apps to easily integrate OAuth authentication with native mobile OAuth flows.** This provides a seamless authentication experience that feels native to each platform while maintaining the flexibility of web-based development. **Important**: OAuth for Capacitor requires Universal App Links (HTTPS URLs) and does not work with custom URL schemes like `com.capacitor-example.app`. You'll need to set up deep links using HTTPS domains. Here's how to set up OAuth authentication in your Capacitor app! Capacitor OAuth integration provides native authentication flows on mobile devices while falling back to web-based OAuth on other platforms.
How does Capacitor OAuth work? Capacitor OAuth leverages the native OAuth capabilities of each platform: * **iOS**: Uses `ASWebAuthenticationSession` for secure in-app browser authentication * **Android**: Uses Chrome Custom Tabs for secure authentication flows * **Web**: Falls back to standard web OAuth flows This approach provides the best user experience on each platform while maintaining code consistency across your app. ***
### 1. Install Capacitor OAuth dependencies First, install the necessary Capacitor plugins for OAuth authentication: ```bash theme={"system"} npm install @capacitor/browser npm install @capacitor/app ``` Then sync your Capacitor project: ```bash theme={"system"} npx cap sync ``` ### 2. Configure OAuth providers in your dashboard Go to your app in your [developer dashboard](https://dashboard.privy.io) and navigate to **User management > Authentication > Socials**. Enable the OAuth providers you want to support (Google, Apple, etc.). Configure your OAuth redirect URIs and allowed origins for your Capacitor app. #### Configure allowed origins Navigate to **App Settings > Domains** and add platform-specific origins for your Capacitor app: Dashboard settings showing allowed origins for Capacitor * **Android**: `https://localhost` (origin for Capacitor Android apps) * **iOS**: `capacitor://localhost` (origin for Capacitor iOS apps) * **Development**: `https://your-ngrok-url.ngrok.io` (must match your redirect URL domain) #### Configure allowed redirect URLs Navigate to **App Settings > Advanced** and add your ngrok URL to the allowed redirect URLs: Dashboard settings showing allowed redirect URLs configuration * Redirect URI: `https://your-ngrok-url.ngrok.io/redirect` For more information on configuring OAuth redirect URLs, see our [allowed OAuth redirects guide](/recipes/react/allowed-oauth-redirects). The allowed OAuth redirect URL domain must match your allowed origins. Both should use the same ngrok domain during development. #### Production deployment considerations When deploying to production, you'll need to update these settings: * Replace your ngrok URL with your production domain * Update both the allowed redirect URLs and allowed origins to use your production domain * The platform-specific origins (`https://localhost` for Android and `capacitor://localhost` for iOS) remain the same ### 3. Set up Capacitor configuration Configure your `capacitor.config.ts` file to handle OAuth redirects: ```typescript theme={"system"} import type {CapacitorConfig} from '@capacitor/cli'; const config: CapacitorConfig = { appId: 'com.yourcompany.yourapp', appName: 'your-app-name', webDir: 'dist', plugins: { App: { urlScheme: 'com.yourcompany.yourapp' } } }; export default config; ``` ### 4. Set up deep links with ngrok For OAuth to work properly in your Capacitor app, you need to set up deep links. During development, you can use ngrok to create a public URL that redirects to your local app. Follow the [Capacitor Deep Links guide](https://capacitorjs.com/docs/guides/deep-links) to set up deep linking in your app. Once you have ngrok running, note your ngrok URL (e.g., `https://abc123.ngrok.io`) as you'll need it for the Privy configuration. Remember to update your ngrok domain in both your Privy provider configuration and dashboard settings when the ngrok URL changes. ### 5. Add the AppUrlListener component Create an `AppUrlListener` component to handle deep link redirects for OAuth flows. Add this component before your `PrivyProvider`: This component is specifically for handling social login OAuth redirects. Other Privy authentication methods (email, SMS, etc.) don't require this setup. ```tsx theme={"system"} import {useEffect} from 'react'; import {App} from '@capacitor/app'; export const AppUrlListener = () => { useEffect(() => { App.addListener('appUrlOpen', (event) => { try { const deepLinkUrl = new URL(event.url); // Extract search params from deep link if ( deepLinkUrl.search && deepLinkUrl.searchParams.has('privy_oauth_code') && deepLinkUrl.searchParams.has('privy_oauth_state') && deepLinkUrl.searchParams.has('privy_oauth_provider') ) { const currentUrl = new URL(window.location.href); currentUrl.search = deepLinkUrl.search; window.location.assign(currentUrl.toString()); } } catch (error) { console.error('Failed to parse deep link URL:', error); } }); }, []); return null; }; ``` ### 6. Configure your Privy provider Set up your Privy provider with deep link support for Capacitor: ```tsx theme={"system"} import {PrivyProvider} from '@privy-io/react-auth'; import {Capacitor} from '@capacitor/core'; import {AppUrlListener} from './AppUrlListener'; function App() { return ( <> {/* Your app content */} ); } ``` ### 7. Platform-specific setup Configure your iOS app to handle OAuth redirects by adding URL schemes to your `Info.plist`: ```xml theme={"system"} CFBundleURLTypes CFBundleURLName com.yourcompany.yourapp CFBundleURLSchemes com.yourcompany.yourapp ``` Configure your Android app to handle OAuth redirects by adding intent filters to your `AndroidManifest.xml`: ```xml theme={"system"} ``` Your development server must be running and configured to serve the `assetlinks.json` file for Android and `apple-app-site-association` file for iOS. See the [Capacitor Deep Links guide](https://capacitorjs.com/docs/guides/deep-links) for more details on configuring these files. ### 8. Test your implementation Test your OAuth implementation across different platforms: 1. **Web**: Test in your browser during development 2. **iOS Simulator**: Test the native iOS OAuth flow 3. **Android Emulator**: Test the native Android OAuth flow 4. **Physical devices**: Test on real devices for the full experience That's it! Your Capacitor app now supports native OAuth authentication across all platforms while maintaining a single codebase. Remember to test your OAuth flows on actual devices, as the authentication experience can differ between simulators and real devices. # Earn yield with markets powered by Dolomite Source: https://docs.privy.io/recipes/community/dolomite-guide Dolomite powers token markets for the assets it supports. Apps can deposit USD1 into a Dolomite market to earn yield. ## Markets powered by Dolomite Dolomite organizes each supported asset into its own market, giving users a clear way to supply, borrow, and manage positions across a range of tokens. Each market is built around a specific supported asset, making it easy to understand what users are interacting with when they deposit or withdraw. In this guide, USD1 is accessed through its Dolomite market. ## Resources Explore Markets powered by Dolomite. Set up Privy and create embedded wallets for your app. ## Deposit USD1 into a Dolomite market Set up the USD1 token address, the Dolomite Deposit Router, and the USD1 market ID. The example below uses Ethereum Mainnet: ```tsx theme={"system"} const USD1_ADDRESS = '0x8d0D000Ee44948FC98c9B98A4FA4921476f08B0d'; const DOLOMITE_DEPOSIT_ROUTER = '0xf8b2c637A68cF6A17b1DF9F8992EeBeFf63d2dFf'; const USD1_MARKET_ID = 1n; // Dolomite market ID for USD1 const ACCOUNT_NUMBER = 0n; // Default account number const CHAIN_ID = 1; // Ethereum Mainnet ``` Use viem's `encodeFunctionData` to encode the approval, and Privy's `useSendTransaction` to send it: ```tsx theme={"system"} import {encodeFunctionData, maxUint256, erc20Abi} from 'viem'; import {useSendTransaction} from '@privy-io/react-auth'; const {sendTransaction} = useSendTransaction(); const data = encodeFunctionData({ abi: erc20Abi, functionName: 'approve', args: [DOLOMITE_DEPOSIT_ROUTER as `0x${string}`, maxUint256] }); const tx = await sendTransaction({ to: USD1_ADDRESS as `0x${string}`, data, chainId: CHAIN_ID }); ``` Use viem to encode the `depositWei` call and Privy's `useSendTransaction` to send it: ```tsx theme={"system"} import {encodeFunctionData, parseUnits} from 'viem'; import {useSendTransaction} from '@privy-io/react-auth'; const {sendTransaction} = useSendTransaction(); const depositRouterAbi = [ { type: 'function', name: 'depositWei', inputs: [ {name: '_isolationModeMarketId', type: 'uint256'}, {name: '_toAccountNumber', type: 'uint256'}, {name: '_marketId', type: 'uint256'}, {name: '_amountWei', type: 'uint256'}, {name: '_eventFlag', type: 'uint8'} ], outputs: [], stateMutability: 'nonpayable' } ] as const; const depositAmount = parseUnits('100', 18); // 100 USD1 (18 decimals) const data = encodeFunctionData({ abi: depositRouterAbi, functionName: 'depositWei', args: [ 0n, // _isolationModeMarketId: 0 for standard deposits ACCOUNT_NUMBER, // _toAccountNumber: 0 for the default account USD1_MARKET_ID, // _marketId: Dolomite market ID for USD1 depositAmount, // _amountWei: amount of USD1 in wei 0 // _eventFlag: 0 (None) ] }); const tx = await sendTransaction({ to: DOLOMITE_DEPOSIT_ROUTER as `0x${string}`, data, chainId: CHAIN_ID }); ``` *** ## Withdraw USD1 from a Dolomite market Call `withdrawWei` on the Dolomite Deposit Router to pull USD1 back from the market to the user's wallet. Pass the full wei amount to withdraw, or use a max value to exit entirely. ```tsx theme={"system"} import {encodeFunctionData, parseUnits} from 'viem'; import {useSendTransaction} from '@privy-io/react-auth'; const {sendTransaction} = useSendTransaction(); const depositRouterAbi = [ { type: 'function', name: 'withdrawWei', inputs: [ {name: '_isolationModeMarketId', type: 'uint256'}, {name: '_fromAccountNumber', type: 'uint256'}, {name: '_marketId', type: 'uint256'}, {name: '_amountWei', type: 'uint256'}, {name: '_balanceCheckFlag', type: 'uint8'} ], outputs: [], stateMutability: 'nonpayable' } ] as const; const withdrawAmount = parseUnits('100', 18); // 100 USD1 (18 decimals) const data = encodeFunctionData({ abi: depositRouterAbi, functionName: 'withdrawWei', args: [ 0n, // _isolationModeMarketId: 0 for standard withdrawals ACCOUNT_NUMBER, // _fromAccountNumber: 0 for the default account USD1_MARKET_ID, // _marketId: Dolomite market ID for USD1 withdrawAmount, // _amountWei: amount of USD1 in wei 1 // _balanceCheckFlag: 1 (From) — validates sender balance post-withdrawal ] }); const tx = await sendTransaction({ to: DOLOMITE_DEPOSIT_ROUTER as `0x${string}`, data, chainId: CHAIN_ID }); ``` *** ## Key integration tips 1. **Always approve first**: Your app must grant ERC-20 approval to the Dolomite Deposit Router before any deposit. 2. **USD1 uses 18 decimals**: Use `parseUnits('100', 18)` for amounts. 3. **Market IDs identify assets**: The `_marketId` parameter (`1` for USD1 in this example) tells Dolomite which market to deposit into or withdraw from. Dolomite uses ascending numerical market IDs rather than token addresses to identify markets. Use `_isolationModeMarketId = 0` for standard (non-isolated) assets. 4. **Account number**: Dolomite tracks balances per account number. Use `0` for the default account, or pass a custom value to support multiple sub-accounts per wallet. 5. **Balance check flag**: On withdrawal, `_balanceCheckFlag = 1` checks that the sender's balance remains non-negative after the operation. Use `0` to skip checks on both sides. 6. **Supported chains**: This guide uses Ethereum Mainnet. Dolomite is also deployed on Arbitrum — market IDs and router addresses differ per chain. *** ## Conclusion Privy makes it straightforward to build secure access to markets powered by Dolomite. For advanced use cases, refer to [Dolomite](https://dolomite.io), or reach out in [Slack](https://privy.io/slack). Your app is now ready to deposit USD1 into a Dolomite market using Privy embedded wallets! # Swapping crypto using Privy and Omniston Source: https://docs.privy.io/recipes/community/ton-omniston-swap
Author(s): STON.fi Team
This guide was contributed by the STON.fi Team, and has not been validated by Privy. Please review and test thoroughly before using in production. If you find a bug or problem with this resource, please let Privy support know and we'll alert STON.fi Team. This guide extends the [Getting started with Privy and TON](/recipes/community/ton-vite-react) application by adding token swap functionality using Omniston - a protocol that aggregates liquidity from multiple DEXes including STON.fi and DeDust. ## Prerequisites Before starting this guide, ensure you have: * Completed the [Getting started with Privy and TON](/recipes/community/ton-vite-react) guide * A working React app with Privy authentication for TON * A deployed and funded TON wallet (minimum 0.05 TON for gas fees) * The existing utilities and hooks from the previous guide already implemented ## Step 1: Install Omniston SDK and STON.fi API From your existing project root, install the Omniston SDK and STON.fi API: ```bash theme={"system"} pnpm add @ston-fi/omniston-sdk-react @ston-fi/api ``` ## Step 2: Add Omniston provider Update your `src/main.tsx` to add the Omniston provider: ```tsx theme={"system"} // src/main.tsx import {StrictMode} from 'react'; import {createRoot} from 'react-dom/client'; import {PrivyProvider} from '@privy-io/react-auth'; import {Omniston, OmnistonProvider} from '@ston-fi/omniston-sdk-react'; import './index.css'; import App from './App'; const omniston = new Omniston({apiUrl: 'wss://omni-ws.ston.fi'}); createRoot(document.getElementById('root')!).render( ); ``` ## Step 3: Create asset fetching hook Create a simple hook to fetch available tokens: ```tsx theme={"system"} // src/hooks/useAssets.ts import {useState, useEffect} from 'react'; import {StonApiClient, AssetTag, type AssetInfoV2} from '@ston-fi/api'; export const useAssets = () => { const [assets, setAssets] = useState([]); useEffect(() => { const fetchAssets = async () => { try { const client = new StonApiClient(); const condition = [ AssetTag.LiquidityVeryHigh, AssetTag.LiquidityHigh, AssetTag.LiquidityMedium ].join(' | '); const result = await client.queryAssets({condition}); setAssets(result); } catch (err) { console.error('Failed to fetch assets:', err); } }; fetchAssets(); }, []); return assets; }; ``` ## Step 3.5: Add utility files The swap hook needs some utility files to work with TON and Privy. Create these if you don't have them already: ```tsx theme={"system"} // src/utils/tonClient.ts import {TonClient} from '@ton/ton'; export const getTonClient = () => { const apiKey = import.meta.env.VITE_TON_API_KEY; return new TonClient({ endpoint: 'https://toncenter.com/api/v2/jsonRPC', apiKey: apiKey }); }; ``` ```tsx theme={"system"} // src/utils/tonAddress.ts import {WalletContractV4} from '@ton/ton'; export function maybeStripEd25519PublicKeyPrefix(publicKey: string) { if (publicKey.length === 66 && publicKey.startsWith('00')) { return publicKey.slice(2); } return publicKey; } export function deriveTonWalletFromPublicKey(publicKey: string) { const strippedKey = maybeStripEd25519PublicKeyPrefix(publicKey); const publicKeyBuffer = Buffer.from(strippedKey, 'hex'); const wallet = WalletContractV4.create({ workchain: 0, publicKey: publicKeyBuffer }); return { address: wallet.address.toString(), wallet }; } ``` ```tsx theme={"system"} // src/utils/tonWallet.ts import {WalletContractV4, type OpenedContract} from '@ton/ton'; export async function getWalletSeqno(contract: OpenedContract): Promise { try { return await contract.getSeqno(); } catch { return 0; } } export function normalizeOmnistonValue(sendAmount: string | number | bigint): bigint { if (typeof sendAmount === 'string') return BigInt(sendAmount); if (typeof sendAmount === 'number') return BigInt(sendAmount); return sendAmount as bigint; } export async function retry( fn: () => Promise, options: {retries?: number; delay?: number} = {} ): Promise { const {retries = 3, delay = 1000} = options; let lastError: unknown; for (let i = 0; i <= retries; i++) { try { return await fn(); } catch (error) { lastError = error; if (i < retries) await new Promise((r) => setTimeout(r, delay)); } } throw lastError; } ``` ## Step 4: Create the swap hook This hook handles quoting, building transactions, signing with Privy, and tracking: This hook integrates Omniston's RFQ (Request for Quote) system to find the best swap rates across multiple DEXes, then builds and signs transactions using Privy's embedded wallet. ```tsx theme={"system"} // src/hooks/useOmnistonSwap.ts import {useState, useCallback} from 'react'; import {usePrivy} from '@privy-io/react-auth'; import {useSignRawHash} from '@privy-io/react-auth/extended-chains'; import { useRfq, useOmniston, useTrackTrade, SettlementMethod, Blockchain, GaslessSettlement, type QuoteResponseEvent_QuoteUpdated } from '@ston-fi/omniston-sdk-react'; import {Address, Cell, internal, SendMode, toNano, WalletContractV4} from '@ton/ton'; import {toHex} from 'viem'; import type {AssetInfoV2} from '@ston-fi/api'; import {useTonWallet} from './useTonWallet'; import {getTonClient} from '../utils/tonClient'; import {getWalletSeqno, normalizeOmnistonValue, retry} from '../utils/tonWallet'; import {deriveTonWalletFromPublicKey} from '../utils/tonAddress'; interface UseOmnistonSwapProps { fromAsset?: AssetInfoV2; toAsset?: AssetInfoV2; amount: string; } function toBaseUnits(amount: string, decimals?: number) { return Math.floor(parseFloat(amount) * 10 ** (decimals ?? 9)).toString(); } export const useOmnistonSwap = ({fromAsset, toAsset, amount}: UseOmnistonSwapProps) => { const {user} = usePrivy(); const {signRawHash} = useSignRawHash(); const {address: walletAddress} = useTonWallet(); const omniston = useOmniston(); const [outgoingTxHash, setOutgoingTxHash] = useState(''); const [tradedQuote, setTradedQuote] = useState(null); const [isSwapping, setIsSwapping] = useState(false); const tonWallet = user?.linkedAccounts?.find( (account) => account.type === 'wallet' && 'chainType' in account && account.chainType === 'ton' ); // Get quote from Omniston const {data: quote, isLoading: quoteLoading} = useRfq( { settlementMethods: [SettlementMethod.SETTLEMENT_METHOD_SWAP], bidAssetAddress: fromAsset ? {blockchain: Blockchain.TON, address: fromAsset.contractAddress} : undefined, askAssetAddress: toAsset ? {blockchain: Blockchain.TON, address: toAsset.contractAddress} : undefined, amount: { bidUnits: fromAsset && amount ? toBaseUnits(amount, fromAsset.meta?.decimals) : '0' }, settlementParams: { gaslessSettlement: GaslessSettlement.GASLESS_SETTLEMENT_POSSIBLE, maxPriceSlippageBps: 500 } }, { enabled: !!fromAsset?.contractAddress && !!toAsset?.contractAddress && parseFloat(amount) > 0 && !outgoingTxHash && !isSwapping } ); // Track trade status const {data: tradeStatus} = useTrackTrade( { quoteId: tradedQuote?.quote?.quoteId || '', traderWalletAddress: { blockchain: Blockchain.TON, address: walletAddress || '' }, outgoingTxHash }, { enabled: !!tradedQuote?.quote?.quoteId && !!walletAddress && !!outgoingTxHash } ); const buildTransaction = useCallback( async (willTradedQuote: QuoteResponseEvent_QuoteUpdated) => { if (!walletAddress) throw new Error('Wallet not connected'); const tx = await omniston.buildTransfer({ quote: willTradedQuote.quote, sourceAddress: {blockchain: Blockchain.TON, address: walletAddress}, destinationAddress: {blockchain: Blockchain.TON, address: walletAddress}, gasExcessAddress: {blockchain: Blockchain.TON, address: walletAddress}, useRecommendedSlippage: true }); return tx.ton?.messages || []; }, [omniston, walletAddress] ); const getTxByBOC = useCallback(async (exBoc: string, walletAddr: string): Promise => { const client = getTonClient(); const myAddress = Address.parse(walletAddr); return retry( async () => { const transactions = await client.getTransactions(myAddress, {limit: 10}); for (const tx of transactions) { const inMsg = tx.inMessage; if (inMsg?.info.type === 'external-in') { try { if (inMsg.body) { const inBoc = inMsg.body.toBoc().toString('base64'); if (inBoc === exBoc) { return tx.hash().toString('hex'); } } } catch { const extHash = Cell.fromBase64(exBoc).hash().toString('hex'); if (inMsg.body) { const inHash = inMsg.body.hash().toString('hex'); if (extHash === inHash) { return tx.hash().toString('hex'); } } } } } throw new Error('Transaction not found'); }, {retries: 30, delay: 1000} ); }, []); const executeSwap = useCallback(async () => { if (!quote || quote.type !== 'quoteUpdated' || !walletAddress || !tonWallet || !user) { return; } setIsSwapping(true); try { setTradedQuote(quote); const messages = await buildTransaction(quote); if (!messages || messages.length === 0) { throw new Error('No transaction messages generated'); } const isEmbedded = tonWallet.type === 'wallet' && !tonWallet.imported && !tonWallet.delegated; if (!isEmbedded) { throw new Error('Only Privy embedded wallets are supported for swaps'); } if (!tonWallet.publicKey) throw new Error('Unable to find public key'); const {wallet: derivedWallet, address: derivedAddress} = deriveTonWalletFromPublicKey( tonWallet.publicKey ); let wallet: WalletContractV4; if (derivedAddress !== walletAddress) { const walletAtPrivyAddress = WalletContractV4.create({ workchain: 0, publicKey: derivedWallet.publicKey }); wallet = { ...walletAtPrivyAddress, address: Address.parse(walletAddress) } as WalletContractV4; } else { wallet = derivedWallet; } const client = getTonClient(); const contract = client.open(wallet); // Verify wallet is deployed const contractState = await client.getContractState(wallet.address); if (contractState.state !== 'active') { throw new Error('Wallet not deployed'); } // Check balance const walletBalance = await client.getBalance(wallet.address); const requiredAmount = messages.reduce((sum, msg) => { return sum + normalizeOmnistonValue(msg.sendAmount); }, 0n); const gasReserve = toNano('0.05'); const totalRequired = requiredAmount + gasReserve; if (walletBalance < totalRequired) { throw new Error('Insufficient balance for swap'); } const seqno = await getWalletSeqno(contract); // Embed createTonSigner logic const signer = async (msgCell: Cell) => { const hash = msgCell.hash(); const hexHash = toHex(hash) as `0x${string}`; const {signature} = await signRawHash({ address: walletAddress, chainType: 'ton' as const, hash: hexHash }); return Buffer.from(signature.slice(2), 'hex'); }; const internalMessages = messages.map((msg) => internal({ value: normalizeOmnistonValue(msg.sendAmount), to: Address.parse(msg.targetAddress), body: msg.payload ? Cell.fromBase64(msg.payload) : undefined, bounce: true }) ); const transfer = await contract.createTransfer({ seqno, messages: internalMessages, sendMode: SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS, signer }); await contract.send(transfer); const exBoc = transfer.toBoc().toString('base64'); const txHash = await getTxByBOC(exBoc, walletAddress); setOutgoingTxHash(txHash); } catch (err: unknown) { console.error('Swap failed:', err); setTradedQuote(null); } finally { setIsSwapping(false); } }, [quote, walletAddress, tonWallet, signRawHash, buildTransaction, getTxByBOC, user]); return { quote, quoteLoading, executeSwap, isSwapping, tradeStatus }; }; ``` ## Step 5: Create the swap interface component Build a simple swap interface: ```tsx theme={"system"} // src/components/SwapInterface.tsx import {useState, useEffect} from 'react'; import type {AssetInfoV2} from '@ston-fi/api'; import {useOmnistonSwap} from '../hooks/useOmnistonSwap'; import {useAssets} from '../hooks/useAssets'; export const SwapInterface: React.FC = () => { const [amount, setAmount] = useState(''); const [fromAsset, setFromAsset] = useState(); const [toAsset, setToAsset] = useState(); const assets = useAssets(); // Set default assets when loaded useEffect(() => { if (assets.length > 0 && !fromAsset) { setFromAsset(assets[0]); } if (assets.length > 1 && !toAsset) { setToAsset(assets[1]); } }, [assets, fromAsset, toAsset]); const {quote, quoteLoading, executeSwap, isSwapping, tradeStatus} = useOmnistonSwap({ fromAsset, toAsset, amount }); return (
{/* From Token */}
{/* To Token */}
{/* Amount */}
setAmount(e.target.value)} placeholder="0.0" className="w-full px-4 py-3 bg-gray-50 border border-gray-200 rounded-lg text-gray-700 placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 transition-all" />
{/* Quote Display */} {quoteLoading && (

Fetching best quote...

)} {quote && 'quote' in quote && (

Quote Details

Provider: {quote.quote.resolverName}

You send:{' '} {(parseInt(quote.quote.bidUnits) / 10 ** (fromAsset?.meta?.decimals ?? 9)).toFixed( 4 )}{' '} {fromAsset?.meta?.symbol}

You receive:{' '} {(parseInt(quote.quote.askUnits) / 10 ** (toAsset?.meta?.decimals ?? 9)).toFixed(4)}{' '} {toAsset?.meta?.symbol}

)} {/* Swap Button */} {quote && 'quote' in quote && ( )} {/* Trade Status */} {tradeStatus && (

{tradeStatus.status?.tradeSettled ? ( Trade completed successfully ) : ( Tracking trade... )}

)}
); }; ``` ## Step 6: Integrate with your TonWalletManager Add tabs to switch between wallet and swap functionality in your existing `TonWalletManager` component: ```tsx theme={"system"} // src/components/TonWalletManager.tsx import {useState} from 'react'; import {usePrivy} from '@privy-io/react-auth'; import {useTonWallet} from '../hooks/useTonWallet'; import {useTonBalance} from '../hooks/useTonBalance'; import {useWalletDeployment} from '../hooks/useWalletDeployment'; import {LoginButton} from './LoginButton'; import {CreateWalletButton} from './CreateWalletButton'; import {WalletDeployStatus} from './WalletDeployStatus'; import {SignMessageButton} from './SignMessageButton'; import {SendTransactionButton} from './SendTransactionButton'; import {SwapInterface} from './SwapInterface'; // Add this import export function TonWalletManager() { const {user, logout, authenticated} = usePrivy(); const {address, exists} = useTonWallet(); const {balance} = useTonBalance(); const {isDeployed} = useWalletDeployment(address); const [activeTab, setActiveTab] = useState<'wallet' | 'swap'>('wallet'); // Add tab state if (!authenticated) { return (

TON Wallet Manager

Please login to manage your TON wallet

); } return (

TON Wallet Manager

User: {user?.email?.address}

{!exists ? (
) : (
{/* Add tab navigation */}
{/* Tab content */} {activeTab === 'wallet' ? ( <> {/* Existing wallet content */} {address && }

Wallet Address:

{address}

Balance: {balance} TON

{isDeployed && (

✓ Wallet is deployed

)}
{isDeployed && ( <> )} ) : ( )}
)}
); } ``` ## Step 7: Test your integration 1. Start your development server: ```bash theme={"system"} pnpm dev ``` 2. Test the complete flow: * Log in with your email * Ensure your wallet is deployed and funded * Switch to the Swap tab * Select tokens to swap (e.g., TON → USDT) * Enter an amount * Review the quote * Execute the swap * Monitor the trade status ## Troubleshooting ### Common Issues * **"Only embedded wallets are supported"** * The swap functionality only works with Privy embedded wallets * Imported or delegated wallets are not supported * **"Wallet not deployed"** * Deploy your wallet first using the deployment flow from the previous guide * Ensure you have at least 0.05 TON for deployment * **"Insufficient balance"** * Swaps require the input amount plus gas fees (be sure to have at least \~0.2 TON for gas) * Top up your wallet and try again * **No quotes appearing** * Ensure both tokens are selected * Amount for swap must be greater than 0 * Some token pairs may have low liquidity * **Transaction tracking fails** * The helper retries automatically * If it persists, wait a moment and try again ## Summary This guide extends your Privy + TON application with: * Omniston SDK integration for DEX aggregation * Quote fetching from multiple liquidity sources * Transaction building and signing with Privy * Real-time trade tracking * A polished swap UI with error handling You can now seamlessly swap tokens on TON blockchain using Privy embedded wallets, with the best rates aggregated from multiple DEXes. # Getting started with Privy and TON (Vite + React) Source: https://docs.privy.io/recipes/community/ton-vite-react
Author(s): STON.fi Team
This guide was contributed by the STON.fi Team, and has not been validated by Privy. Please review and test thoroughly before using in production. If you find a bug or problem with this resource, please let Privy support know and we'll alert STON.fi Team. This guide demonstrates how to integrate Privy with the TON blockchain in a Vite + React app to enable wallet login as well as message and transaction signing. ### Resources Learn how to use Privy embedded wallets with TON for signing messages and transactions. Get started with Privy's React SDK. ## Creating your Privy app If you haven't set up Privy yet, follow our [React quickstart guide](/basics/react/installation) to get your app ID and configure your app. ## Installing with Vite Create a new Vite + React + TypeScript application and install the required dependencies: ```bash theme={"system"} npm create vite@latest privy-ton-app --template react-ts cd privy-ton-app ``` Install the required packages for TON integration: ```bash theme={"system"} npm install @privy-io/react-auth @ton/ton viem ``` Additionally, install the Node.js polyfills plugin for Vite, which is necessary to provide Buffer and other Node.js APIs in the browser environment (required by TON libraries): ```bash theme={"system"} npm install -D vite-plugin-node-polyfills ``` Next, install Tailwind CSS and its Vite plugin: ```bash theme={"system"} npm install -D tailwindcss @tailwindcss/vite ``` Configure the Vite plugin by updating `vite.config.js` file: ```javascript theme={"system"} import {defineConfig} from 'vite'; import react from '@vitejs/plugin-react'; import tailwindcss from '@tailwindcss/vite'; import {nodePolyfills} from 'vite-plugin-node-polyfills'; // https://vite.dev/config/ export default defineConfig({ plugins: [ react(), tailwindcss(), nodePolyfills({ include: ['buffer'], globals: { Buffer: true } }) ] }); ``` Then, import Tailwind CSS in your main CSS file. Open `src/index.css` and replace any existing code with: ```css theme={"system"} @import 'tailwindcss'; ``` You can also remove `src/App.css` (we don't need it), and remove the import statement `import './App.css'` from `src/App.tsx` if it exists. For simplicity, this guide uses Tailwind CSS for styling the components. The setup for Tailwind CSS is already included in the instructions above, so you don't need to set it up separately. ### Environment variables Vite requires environment variables to be prefixed with `VITE_`: ```bash theme={"system"} # .env (or .env.development) # Get your Privy App ID from https://dashboard.privy.io after creating an app VITE_PRIVY_APP_ID=your_privy_app_id # You can get a free API key from https://toncenter.com/ VITE_TON_API_KEY=your_toncenter_api_key ``` ## Setting up the app entry point Set up the main entry point with the Privy provider directly in your main.tsx file: ```tsx theme={"system"} // src/main.tsx import {StrictMode} from 'react'; import {createRoot} from 'react-dom/client'; import './index.css'; import App from './App.tsx'; import {PrivyProvider} from '@privy-io/react-auth'; createRoot(document.getElementById('root')!).render( ); ``` ## Using Privy in your app With Privy integrated, you can authenticate users, generate embedded wallets, and facilitate message and transaction signing. ### Log in with Privy To log in users with Privy, use the `useLogin` hook: ```tsx theme={"system"} // src/components/LoginButton.tsx import {useLogin} from '@privy-io/react-auth'; export function LoginButton() { const {login} = useLogin({ onComplete: (user) => { console.log('User logged in:', user); } }); return ( ); } ``` ### Creating a TON embedded wallet First, create a custom hook to access the TON wallet from Privy's linked accounts: ```tsx theme={"system"} // src/hooks/useTonWallet.ts import {usePrivy} from '@privy-io/react-auth'; export function useTonWallet() { const {user} = usePrivy(); const tonWallet = user?.linkedAccounts?.find( (account) => account.type === 'wallet' && 'chainType' in account && account.chainType === 'ton' ) as any; const address = tonWallet?.address; const walletId = tonWallet?.id; // Wallet ID needed for Privy API calls return { tonWallet, address, walletId, exists: !!tonWallet }; } ``` Now use the `useCreateWallet` hook from extended chains to create a wallet: ```tsx theme={"system"} // src/components/CreateWalletButton.tsx import {useCreateWallet} from '@privy-io/react-auth/extended-chains'; import {useTonWallet} from '../hooks/useTonWallet'; export function CreateWalletButton() { const {exists: hasTonWallet} = useTonWallet(); const {createWallet} = useCreateWallet(); const handleCreateWallet = async () => { try { const {wallet} = await createWallet({chainType: 'ton'}); console.log('TON wallet created:', wallet.address); } catch (error) { console.error('Error creating wallet:', error); } }; if (hasTonWallet) { return
TON wallet already created
; } return ( ); } ``` ### Getting wallet balance To check the balance of a TON wallet using our custom hooks and utilities: ```tsx theme={"system"} // src/hooks/useTonBalance.ts import {useTonWallet} from './useTonWallet'; import {Address, fromNano, TonClient} from '@ton/ton'; import {useEffect, useState} from 'react'; export function useTonBalance() { const {address} = useTonWallet(); const [balance, setBalance] = useState('0'); useEffect(() => { if (!address) return; const fetchBalance = async () => { const client = new TonClient({ endpoint: 'https://toncenter.com/api/v2/jsonRPC', apiKey: import.meta.env.VITE_TON_API_KEY }); const nano = await client.getBalance(Address.parse(address)); setBalance(fromNano(nano)); }; fetchBalance(); }, [address]); return {balance, address}; } ``` ### Checking wallet deployment status TON wallets are smart contracts that need to be deployed before they can be used. Create a hook to check the deployment status: ```tsx theme={"system"} // src/hooks/useWalletDeployment.ts import {useEffect, useState} from 'react'; import {TonClient, Address} from '@ton/ton'; export function useWalletDeployment(address: string | undefined) { const [isDeployed, setIsDeployed] = useState(null); useEffect(() => { if (!address) return; const checkDeployment = async () => { try { const client = new TonClient({ endpoint: 'https://toncenter.com/api/v2/jsonRPC', apiKey: import.meta.env.VITE_TON_API_KEY }); const state = await client.getContractState(Address.parse(address)); setIsDeployed(state.state === 'active'); } catch (error) { console.error('Failed to check wallet deployment:', error); setIsDeployed(false); } }; checkDeployment(); // Check periodically const interval = setInterval(checkDeployment, 10000); // Every 10 seconds return () => clearInterval(interval); }, [address]); return {isDeployed}; } ``` ### Deploying the wallet Ensure your wallet has sufficient balance (minimum 0.05 TON) before attempting deployment. The deployment transaction requires gas fees. ```tsx theme={"system"} // src/components/DeployWalletButton.tsx import {usePrivy} from '@privy-io/react-auth'; import {useSignRawHash} from '@privy-io/react-auth/extended-chains'; import {toNano, internal, SendMode, TonClient, WalletContractV4} from '@ton/ton'; import {useTonBalance} from '../hooks/useTonBalance'; import {useTonWallet} from '../hooks/useTonWallet'; import {toHex} from 'viem'; export function DeployWalletButton() { const {getAccessToken} = usePrivy(); const {signRawHash} = useSignRawHash(); const {balance, address} = useTonBalance(); const {walletId} = useTonWallet(); const handleDeploy = async () => { if (!address || !walletId || parseFloat(balance) < 0.05) return; // Fetch wallet public key from Privy const accessToken = await getAccessToken(); const res = await fetch(`https://auth.privy.io/api/v1/wallets/${walletId}`, { headers: { Authorization: `Bearer ${accessToken}`, 'privy-app-id': import.meta.env.VITE_PRIVY_APP_ID } }); const data = await res.json(); let publicKey = (data.public_key || data.publicKey).replace('0x', ''); // Strip Ed25519 prefix if present if (publicKey.length === 66 && publicKey.startsWith('00')) { publicKey = publicKey.slice(2); } // Create wallet and client const wallet = WalletContractV4.create({ workchain: 0, publicKey: Buffer.from(publicKey, 'hex') }); const client = new TonClient({ endpoint: 'https://toncenter.com/api/v2/jsonRPC', apiKey: import.meta.env.VITE_TON_API_KEY }); const contract = client.open(wallet); // Create deployment message const deployMessage = await wallet.createTransfer({ seqno: 0, messages: [ internal({ value: toNano('0.01'), to: wallet.address, body: 'Deploy' }) ], sendMode: SendMode.PAY_GAS_SEPARATELY + SendMode.IGNORE_ERRORS, signer: async (msgCell) => { const {signature} = await signRawHash({ address, chainType: 'ton' as const, hash: toHex(msgCell.hash()) as `0x${string}` }); return Buffer.from(signature.slice(2), 'hex'); } }); // Send the deployment message await contract.send(deployMessage); }; return ( ); } ``` ### Signing a message To sign a message with a TON embedded wallet, we use the browser's crypto.subtle API to hash the message to 32 bytes (SHA-256), which is then signed via signRawHash: ```tsx theme={"system"} // src/components/SignMessageButton.tsx import {useSignRawHash} from '@privy-io/react-auth/extended-chains'; import {useTonWallet} from '../hooks/useTonWallet'; import {toHex} from 'viem'; async function sha256Hex(message: string): Promise<`0x${string}`> { const data = new TextEncoder().encode(message); const digest = await crypto.subtle.digest('SHA-256', data); return toHex(new Uint8Array(digest)) as `0x${string}`; } export function SignMessageButton() { const {signRawHash} = useSignRawHash(); const {address} = useTonWallet(); const handleSignMessage = async () => { if (!address) return; try { const message = 'Hello from Privy!'; const hash = await sha256Hex(message); const {signature} = await signRawHash({ address, chainType: 'ton' as const, hash }); console.log('Message signature:', signature); alert('Message signed! Check console for signature.'); } catch (error) { console.error('Error signing message:', error); } }; return ; } ``` ### Sending a transaction To send TON from the embedded wallet using browser-compatible methods: ```tsx theme={"system"} // src/components/SendTransactionButton.tsx import {usePrivy} from '@privy-io/react-auth'; import {useSignRawHash} from '@privy-io/react-auth/extended-chains'; import {toNano, internal, SendMode, TonClient, WalletContractV4} from '@ton/ton'; import {useTonWallet} from '../hooks/useTonWallet'; import {useState} from 'react'; import {toHex} from 'viem'; export function SendTransactionButton() { const {getAccessToken} = usePrivy(); const {signRawHash} = useSignRawHash(); const {address, tonWallet, walletId} = useTonWallet(); const [recipientAddress, setRecipientAddress] = useState(''); const [sendAmount, setSendAmount] = useState('0.1'); const [isLoading, setIsLoading] = useState(false); const handleSendTransaction = async () => { if (!address || !tonWallet || !walletId || !recipientAddress) { alert('Please enter a recipient address'); return; } try { setIsLoading(true); // Get access token and fetch public key from Privy API using wallet ID const accessToken = await getAccessToken(); const res = await fetch(`https://auth.privy.io/api/v1/wallets/${walletId}`, { headers: { Authorization: `Bearer ${accessToken}`, 'privy-app-id': import.meta.env.VITE_PRIVY_APP_ID } }); if (!res.ok) throw new Error('Failed to fetch wallet public key'); const data = await res.json(); let publicKey = (data.public_key || data.publicKey).replace('0x', ''); // Strip Ed25519 prefix if present if (publicKey.length === 66 && publicKey.startsWith('00')) { publicKey = publicKey.slice(2); } const amount = toNano(sendAmount); // Create wallet contract const wallet = WalletContractV4.create({ workchain: 0, publicKey: Buffer.from(publicKey, 'hex') }); // Create TON client const tonApiKey = import.meta.env.VITE_TON_API_KEY as string | undefined; const client = new TonClient({ endpoint: 'https://toncenter.com/api/v2/jsonRPC', apiKey: tonApiKey }); const seqno = await client.open(wallet).getSeqno(); const transfer = await wallet.createTransfer({ seqno, messages: [ internal({ value: amount, to: recipientAddress, body: 'Transfer from Privy' }) ], sendMode: SendMode.PAY_GAS_SEPARATELY, signer: async (msgCell) => { const hash = msgCell.hash(); const hashHex = toHex(hash) as `0x${string}`; const {signature} = await signRawHash({ address, chainType: 'ton' as const, hash: hashHex }); return Buffer.from(signature.slice(2), 'hex'); } }); await client.open(wallet).send(transfer); console.log('Transaction sent'); alert('Transaction sent successfully!'); } catch (error) { console.error('Error sending transaction:', error); alert('Failed to send transaction'); } finally { setIsLoading(false); } }; return (
setRecipientAddress(e.target.value)} className="w-full p-2 border rounded" /> setSendAmount(e.target.value)} className="w-full p-2 border rounded" />
); } ``` ### Wallet deployment status component Create a component to inform users about wallet deployment: ```tsx theme={"system"} // src/components/WalletDeployStatus.tsx import {useWalletDeployment} from '../hooks/useWalletDeployment'; import {useTonBalance} from '../hooks/useTonBalance'; import {DeployWalletButton} from './DeployWalletButton'; export function WalletDeployStatus({address}: {address: string}) { const {isDeployed} = useWalletDeployment(address); const {balance} = useTonBalance(); // Don't show anything if wallet is deployed or still checking if (!address || isDeployed === null || isDeployed) { return null; } const hasSufficientBalance = balance && parseFloat(balance) >= 0.05; return (

Wallet Not Deployed

Your TON wallet needs to be deployed before you can send transactions or make swaps.

{hasSufficientBalance ? (

✓ Your wallet has {balance} TON (minimum 0.05 TON required)

) : (

To deploy your wallet:

  1. Send at least 0.05 TON to your wallet address
  2. Click "Deploy Wallet" once funded

Your wallet address:

{address}

)}
); } ``` ## Complete example container Here's the complete application that brings together all the components we've created: ```tsx theme={"system"} // src/components/TonWalletManager.tsx import {usePrivy} from '@privy-io/react-auth'; import {useTonWallet} from '../hooks/useTonWallet'; import {useTonBalance} from '../hooks/useTonBalance'; import {useWalletDeployment} from '../hooks/useWalletDeployment'; import {LoginButton} from './LoginButton'; import {CreateWalletButton} from './CreateWalletButton'; import {WalletDeployStatus} from './WalletDeployStatus'; import {SignMessageButton} from './SignMessageButton'; import {SendTransactionButton} from './SendTransactionButton'; export function TonWalletManager() { const {user, logout, authenticated} = usePrivy(); const {address, exists} = useTonWallet(); const {balance} = useTonBalance(); const {isDeployed} = useWalletDeployment(address); if (!authenticated) { return (

TON Wallet Manager

Please login to manage your TON wallet

); } return (

TON Wallet Manager

User: {user?.email?.address}

{!exists ? (
) : (
{/* Show deployment warning if wallet is not deployed */} {address && }

Wallet Address:

{address}

Balance: {balance} TON

{isDeployed &&

✓ Wallet is deployed

}
{/* Only show transaction functions if wallet is deployed */} {isDeployed && ( <>

Send TON

)}
)}
); } ``` ## Creating the main App component The App.tsx component will contain your main application logic: ```tsx theme={"system"} // src/App.tsx import {TonWalletManager} from './components/TonWalletManager'; export default function App() { return ; } ``` ## Run ```bash theme={"system"} npm run dev ``` Open the printed local URL (typically [http://localhost:5173](http://localhost:5173)). ## Summary 1. **Configure env** * In `.env` (or `.env.development`), set: * `VITE_PRIVY_APP_ID=` * `VITE_TON_API_KEY=` 2. **Start the app** * Run `npm run dev` and open the local URL. 3. **Log in** * Click **Log in with Privy** (email auth). 4. **Create a TON wallet** (once) * If you don't have one yet, click **Create TON Wallet**. 5. **Fund the wallet** * Send ≥ 0.05 TON to the wallet address shown in the UI. 6. **Deploy the wallet** * Click **Deploy Wallet** once funded. * Wait until you see **✓ Wallet is deployed**. 7. **Sign a message** * Click **Sign Message** to sign an example message (check the console for the signature). 8. **Send TON** * Enter **Recipient address** (EQ...) and **Amount in TON**. * Click **Send ... TON** and wait for the success notice. # Using the vanilla JavaScript SDK Source: https://docs.privy.io/recipes/core-js The `@privy-io/js-sdk-core` library is a vanilla JavaScript library for browser-like environments. It provides secure authentication, non-custodial embedded wallets, and user management without requiring React or any other UI framework. `@privy-io/js-sdk-core` is a low-level library. Please do not attempt to use this library without first reaching out to the Privy team to discuss your project and which Privy SDK options may be better suited to it. ## Prerequisites Before you begin: * [Set up your Privy app](/basics/get-started/dashboard/create-new-app) and obtain your **app ID** from the Privy Dashboard * Obtain your **client ID** from the Dashboard under **Settings → Clients** ## Installation ```bash npm theme={"system"} npm install @privy-io/js-sdk-core@latest ``` ```bash pnpm theme={"system"} pnpm install @privy-io/js-sdk-core@latest ``` ```bash yarn theme={"system"} yarn add @privy-io/js-sdk-core@latest ``` ## 1. Create the Privy client Import the `Privy` class and create a single instance for your application. The client accepts the following configuration: ```ts theme={"system"} import Privy, {LocalStorage} from '@privy-io/js-sdk-core'; const privy = new Privy({ appId: '', clientId: '', storage: new LocalStorage() }); ``` Only instantiate a single Privy client for your application. Creating multiple instances will cause unexpected behavior. Your Privy app ID from the Dashboard. Your Privy client ID from the Dashboard under **Settings → Clients**. A storage adapter for persisting session state. Use `LocalStorage` for browsers, or implement a custom adapter for other environments. A custom logger object to replace the default console-based logger. Useful for routing Privy logs to your observability stack. The `Storage` interface requires four methods. Implement this interface if `LocalStorage` is not suitable for your environment (e.g., encrypted storage, server-side rendering, or non-browser runtimes): ```ts {skip-check} theme={"system"} import type {Storage} from '@privy-io/js-sdk-core'; const myStorage: Storage = { get(key: string): Promise { /* return value for key, or null */ }, put(key: string, val: string): Promise { /* persist key-value pair */ }, del(key: string): Promise { /* delete key */ }, getKeys(): Promise { /* return all stored keys */ } }; ``` ## 2. Initialize the client After creating the client, call `initialize()` to establish a connection with the Privy backend and restore any existing session. This must complete before performing any other operations. ```ts {skip-check} theme={"system"} try { await privy.initialize(); } catch (e) { // Initialization can fail if storage access is blocked or network is unavailable console.error('Privy initialization failed:', e); } ``` After `initialize()` resolves, call `client.user.get()` to check for an existing authenticated session. If the user previously logged in and the session is still valid, this returns the user object without requiring re-authentication. ### Restoring a returning user's session ```ts {skip-check} theme={"system"} await privy.initialize(); // Check if a user is already logged in from a previous session const {user} = await privy.user.get(); if (user) { // Store the user object in your application state (e.g., a store, context, or signal) // This is the source of truth for the authenticated user throughout your app } else { // No active session — prompt the user to log in } ``` ## 3. Connect to the secure context The Privy secure context is an iframe that handles embedded wallet key material. Your app must mount this iframe and wire up bidirectional message passing. ### Mount the iframe ```ts {skip-check} theme={"system"} const iframe = document.createElement('iframe'); iframe.src = privy.embeddedWallet.getURL(); iframe.style.display = 'none'; // Track when the iframe is ready let isProxyReady = false; iframe.onload = () => { isProxyReady = true; }; document.body.appendChild(iframe); ``` ### Wire up message passing ```ts {skip-check} theme={"system"} // Allow the Privy client to post messages to the iframe privy.setMessagePoster(iframe.contentWindow); // Forward messages from the iframe to the Privy client window.addEventListener('message', (e) => { // Only process messages from the Privy iframe if (e.source !== iframe.contentWindow) return; const data = typeof e.data === 'string' ? JSON.parse(e.data) : e.data; privy.embeddedWallet.onMessage(data); }); ``` If you are using a UI rendering library or framework, render the iframe and register event listeners using that library's lifecycle methods instead of manipulating the DOM directly. ## 4. Authenticate a user The Privy core SDK supports email, SMS, OAuth, and JWT-based authentication. ```ts Email {skip-check} theme={"system"} const emailAddress = 'user@example.com'; await privy.auth.email.sendCode(emailAddress); // Collect the OTP from your UI const otp = '123456'; const session = await privy.auth.email.loginWithCode(emailAddress, otp); ``` ```ts SMS {skip-check} theme={"system"} // Format: '+1 555-555-5555' const phoneNumber = '+1 555-555-5555'; await privy.auth.phone.sendCode(phoneNumber); // Collect the OTP from your UI const otp = '123456'; const session = await privy.auth.phone.loginWithCode(phoneNumber, otp); ``` ```ts OAuth {skip-check} theme={"system"} // Note: OAuth flows use PKCE and require `window.crypto.subtle` to be available. // This is supported in all modern browsers but may not be available in non-secure // contexts (e.g., HTTP without TLS). const provider = 'google'; const redirectURI = `${window.location.origin}/login-callback`; const oauthURL = await privy.auth.oauth.generateURL(provider, redirectURI); // Redirect the user to the OAuth provider window.location.assign(oauthURL); // When the user returns to your app at the redirectURI const queryParams = new URLSearchParams(window.location.search); const oauthCode = queryParams.get('privy_oauth_code'); const oauthState = queryParams.get('privy_oauth_state'); const session = await privy.auth.oauth.loginWithCode(oauthCode, oauthState); ``` ```ts JWT (custom auth) {skip-check} theme={"system"} const authToken = 'your-jwt-access-or-identity-token'; const session = await privy.auth.customProvider.syncWithToken(authToken); ``` ```ts SIWE (Ethereum) {skip-check} theme={"system"} const [address] = await ethereum.request({method: 'eth_requestAccounts'}); const wallet = { address, chainId: 'eip155:1' }; const {message} = await privy.auth.siwe.init(wallet, window.location.host, window.location.origin); const signature = await ethereum.request({ method: 'personal_sign', params: [message, address] }); const {user} = await privy.auth.siwe.loginWithSiwe(signature, wallet, message, 'login-or-sign-up'); ``` ```ts SIWS (Solana) {skip-check} theme={"system"} const address = solana.publicKey.toBase58(); const {nonce} = await privy.auth.siws.fetchNonce({address}); const message = createSiwsMessage({ address, nonce, domain: window.location.host, uri: window.location.origin }); const encodedMessage = new TextEncoder().encode(message); const signed = await solana.signMessage(encodedMessage, 'utf8'); const signature = btoa(String.fromCharCode(...signed.signature)); const {user} = await privy.auth.siws.login({ message, signature, mode: 'login-or-sign-up' }); ``` ## 5. Get access tokens for your backend After authentication, use `getAccessToken()` to retrieve the user's access token. Include this token in requests to your backend to verify the user's identity. ```ts {skip-check} theme={"system"} const token = await privy.getAccessToken(); // Send authenticated requests to your backend const response = await fetch('https://your-server.com/api/protected', { headers: { Authorization: `Bearer ${token}` } }); ``` `getAccessToken()` automatically handles token refresh when the access token is near expiration. The returned token is always valid at the time of return. ## 6. Create an embedded wallet Your app can [**manually** create wallets](/wallets/wallets/create/create-a-wallet) for users when desired. Privy can provision wallets for your users on both **Ethereum** and **Solana**. ```ts Ethereum {skip-check} theme={"system"} import {getUserEmbeddedEthereumWallet, getEntropyDetailsFromUser} from '@privy-io/js-sdk-core'; const {user} = await privy.embeddedWallet.create({}); const wallet = getUserEmbeddedEthereumWallet(user); const {entropyId, entropyIdVerifier} = getEntropyDetailsFromUser(user); const provider = await privy.embeddedWallet.getEthereumProvider({ wallet, entropyId, entropyIdVerifier }); ``` ```ts Solana {skip-check} theme={"system"} import {getUserEmbeddedSolanaWallet, getEntropyDetailsFromUser} from '@privy-io/js-sdk-core'; const {user} = await privy.embeddedWallet.createSolana(); const account = getUserEmbeddedSolanaWallet(user); const {entropyId, entropyIdVerifier} = getEntropyDetailsFromUser(user); const provider = await privy.embeddedWallet.getSolanaProvider( account, entropyId, entropyIdVerifier ); ``` ## 7. Connect to an existing wallet When a user returns to your app on a subsequent visit, the wallet already exists but a provider must be obtained. ```ts {skip-check} theme={"system"} import {getUserEmbeddedEthereumWallet, getEntropyDetailsFromUser} from '@privy-io/js-sdk-core'; const {user} = await privy.user.get(); const wallet = getUserEmbeddedEthereumWallet(user); const {entropyId, entropyIdVerifier} = getEntropyDetailsFromUser(user); const provider = await privy.embeddedWallet.getEthereumProvider({ wallet, entropyId, entropyIdVerifier }); ``` ## 8. Use the embedded wallet Your wallet must have funds to pay for gas. Use a testnet [faucet](https://console.optimism.io/faucet) to test on Base Sepolia, or send funds to the wallet on your preferred network. With the embedded wallet provider, your app can prompt the user to sign messages and send transactions. ```ts Ethereum {skip-check} theme={"system"} const provider = await privy.embeddedWallet.getEthereumProvider({...args}); // Sign a message await provider.request({ method: 'personal_sign', params: ['hello', signingAddress] }); // Send a transaction await provider.request({ method: 'eth_sendTransaction', params: [ { to: '', value: '0x2386F26FC10000' // 0.01 ETH in wei } ] }); ``` ```ts Solana {skip-check} theme={"system"} const provider = await privy.embeddedWallet.getSolanaProvider(...args); // Sign a message await provider.request({ method: 'signMessage', params: {message: 'hello'} }); ``` [Learn more](/wallets/using-wallets/ethereum/send-a-transaction) about sending transactions with the embedded wallet. Privy enables you to take many actions on the embedded wallet, including [sign a message](/wallets/using-wallets/ethereum/sign-a-message), [sign typed data](/wallets/using-wallets/ethereum/sign-typed-data), and [sign a transaction](/wallets/using-wallets/ethereum/sign-a-transaction). ## 9. Log the user out To end the user's session and clean up resources: ```ts {skip-check} theme={"system"} const {user} = await privy.user.get(); await privy.auth.logout({userId: user.id}); // Clean up the iframe and event listeners window.removeEventListener('message', listener); iframe.remove(); ``` After logout, the user must authenticate again to access any protected resources or wallet functionality. ## Full integration example Below is a minimal end-to-end integration showing the complete lifecycle: ```ts {skip-check} theme={"system"} import Privy, { LocalStorage, getUserEmbeddedEthereumWallet, getEntropyDetailsFromUser } from '@privy-io/js-sdk-core'; // 1. Create the client (once, at app startup) const privy = new Privy({ appId: '', clientId: '', storage: new LocalStorage() }); // 2. Initialize and check for existing session await privy.initialize(); let {user} = await privy.user.get(); // 3. Set up the secure context const iframe = document.createElement('iframe'); iframe.src = privy.embeddedWallet.getURL(); iframe.style.display = 'none'; document.body.appendChild(iframe); privy.setMessagePoster(iframe.contentWindow); const listener = (e) => { if (e.source !== iframe.contentWindow) return; const data = typeof e.data === 'string' ? JSON.parse(e.data) : e.data; privy.embeddedWallet.onMessage(data); }; window.addEventListener('message', listener); // 4. Authenticate (if no existing session) if (!user) { await privy.auth.email.sendCode('user@example.com'); const session = await privy.auth.email.loginWithCode('user@example.com', '123456'); user = session.user; } // 5. Get the wallet provider const wallet = getUserEmbeddedEthereumWallet(user); const {entropyId, entropyIdVerifier} = getEntropyDetailsFromUser(user); const provider = await privy.embeddedWallet.getEthereumProvider({ wallet, entropyId, entropyIdVerifier }); // 6. Use the wallet await provider.request({ method: 'personal_sign', params: ['hello world', wallet.address] }); // 7. Get a token for backend requests const accessToken = await privy.getAccessToken(); ``` # Configure account transfer Source: https://docs.privy.io/recipes/dashboard/account-transfer ### Login method transfer If **User management > Authentication > Login method transfer** is enabled, if a user attempts to link a login method that is already linked to another account they own, they can choose to transfer the login method to their currently logged-in account. Sample account transfer flow **Once the login method is transferred to the current user, the previous account will then be deleted.** Please ensure that the embedded wallet associated with the previous account has either been exported or that its assets have been transferred out prior to the account deletion. Currently, login method transfer is only supported when the orphan account is associated with a single login method (the account to transfer). We are working to allow for transfers of login methods without full deletion of the orphan account to allow for login method transfer when multiple login methods are linked to the orphan account. # Managing your allowlist with Airtable Source: https://docs.privy.io/recipes/dashboard/airtable **If you use [Airtable](https://airtable.com/) or a similar tool to manage a waitlist of wallet addresses for your app, you can easily set up a [Privy allowlist](/user-management/users/managing-users/allowlist) and manage it from the same interface.** Just follow the steps outlined below. ## 0. Prerequisites This guide assumes: * You have an allowlist enabled for your Privy app. If you would like to enable an allowlist for your app, please [reach out](https://privy.io/slack)! * You are managing a waitlist of users with Airtable, and that the base at minimum includes the user's wallet address and a boolean/checkbox column for whether the user should be invited off the waitlist or not. ## 1. Create an automation In your Airtable with your waitlist, create a custom automation by clicking on "Automations" in the top right corner of your screen, and then clicking "Create a custom automation" from the dropdown. You can find Airtable's complete instructions around automations [here](https://support.airtable.com/docs/creating-an-airtable-automation#setup). ## 2. Configure a trigger for your automation Once you start creating your custom automation, click "Add a trigger", and select the "When a record is updated" option. Then, in the right sidebar that appears, under "Table", select the name of your Airtable base with your waitlist. Under "Fields", select the column from your base that indicates whether or not a user should be invited off your waitlist. This configures your automation to run whenever you update this column (whether the user is allowed or not) for any record. ## 3. Set up your script After you've configured your trigger, click "Add action" in the same automation. This will set up an action that runs whenever your automation is triggered. First, you need to configure the inputs for your action. In the right sidebar, under "Input Variables", create the following two variables: * the user's wallet address. Set the `Name` as 'address' and `Value` as the column in your table with the user's wallet address. * whether or not the user should be invited off the waitlist. Set the `Name` as 'isAllowed' and `Value` as the corresponding column in your table. Next, after you've configured your input variables, copy the following script into your code box and replace the variables at the top with your corresponding values. The script essentially parses the input variables you just configured, and makes a request to Privy's API to [add the corresponding user to your app's allowlist](/user-management/users/managing-users/allowlist). ```tsx theme={"system"} // These are variables to set up our script const appId = /* paste your Privy App ID here */; const appSecret = /* Paste your Privy App Secret */; const base64 = /* Paste a base-64 encoding of appId + ":" + appSecret */ const url = `https://auth.privy.io/api/v1/apps/${appId}/allowlist`; // This parses the input variables we configured earlier const inputConfig = input.config(); const isAllowed = inputConfig.isAllowed; const address = inputConfig.address; async function addToAllowlist() { const data = {'type': 'wallet', 'value': address}; try { const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Basic ${base64}`, 'privy-app-id': appId }, body: JSON.stringify(data) }) console.log(response); } catch (error) { console.log(error.data); } } if (isAllowed) { addToAllowlist(); } ``` ## 4. Enable your script! **That's it! You can now manage your app's allowlist directly from Airtable.** Just go ahead and enable the automation. If you’d like to test this automation before enabling it, in the right sidebar, you can scroll down and select a test record to use to make sure things work smoothly. # Configure allowed URLs Source: https://docs.privy.io/recipes/dashboard/allowed-domains ## Allowed domains Use the **Configuration > App settings** page > **Domains** tab of the Privy Dashboard to manage **allowed origins** for web and native mobile apps and to manage **HttpOnly cookies** in web apps. You should only use this setting when using Privy in a production website. #### Browser (web & mobile web) In a browser environment (web & mobile web), allowed origins restrict which **domains** are allowed to use your Privy app ID. In the **Allowed origins** section of this page, select the **Web & mobile web** option. In the input field, list any domains that will use your Privy app ID, separated by commas, spaces, or breaks. Please note the following requirements: * The protocol (`https`) is required. * Trailing paths (`/path`) are not supported. * Wildcards (`*`) are only supported as a subdomain (`*.domain.com`), but not as a domain alone (`*.com`). * Partial wildcards of the form `*-sometext.domain.com` are not supported. * Localhost (`http://localhost:port`) *is* supported but you *must* specify the `port` number. Though supported, we do **not** recommend listing `localhost` as an allowed domain for production apps. If you need to temporarily list `localhost` as an allowed domain for your production app ID, please take care to remove it when not developing. Many hosting providers and their corresponding DNS configurations treat `https://www.example.com` and `https://example.com` interchangeably. If these URLs are equivalent for your app setup, we recommend adding **both** (with and without the `www` subdomain) domains as allowed origins to the dashboard. Setting allowed domains restricts **client-side access** to your Privy app ID only. Privy's REST API endpoints that you would query from your backend are gated by your app secret, which should **never** be exposed on a user's client. #### Supporting preview URLs Many hosting providers (e.g. Vercel) support preview deployment URLs to make it easy to test changes, like: ``` // Matches the pattern *.netlify.app, which anyone with a free Netlify account can deploy to deploy-preview-id--yoursitename.netlify.app ``` For security reasons, **we do not allow whitelisting domains with a *generic* pattern** that are commonly used for these preview deployments, such as: * `https://*.netlify.app` / `https://*.vercel.app` * `https://*-projectname.netlify.app` / `https://*-projectname.vercel.app` Any project can deploy to a domain that matches `https://*.netlify.app`, `https://*.vercel.app`, or similar. If you were to whitelist this domain for your production App ID, any actor could set up any arbitrary deployment with your hosting provider and can use your production App ID within their site. **If you'd like to secure your Privy App ID on preview deployment URLs, please check if your hosting provider allows you to map preview deployments to a stable subdomain that only *you control***, like: ``` // Matches the pattern *.yoursitename.netlify.app, which only members of your Netlify account // (or hosting provider) can deploy to deploy-preview-42.yoursitename.netlify.app ``` This allows you to list `https://*.yoursitename.netlify.app` under allowed domains, which arbitrary actors cannot deploy to. See instructions to set this up with [Vercel](https://vercel.com/docs/deployments/generated-urls#preview-deployment-suffix) or [Netlify](https://docs.netlify.com/domains-https/custom-domains/automatic-deploy-subdomains/). ### Native mobile You should only use this setting if you use Privy in a native mobile app (e.g. via the [Expo SDK](/basics/react-native/quickstart). In a native mobile environment (e.g. iOS and Android apps), allowed origins request which **application identifiers** are allowed to use your Privy app ID. In the **Allowed origins** section of this page, select the **Native** option. In the input field, list any domains that will use your Privy app ID, separated by commas, spaces, or breaks. ### HttpOnly Cookies Set secure cookies that restrict access to client-side scripts, protecting sensitive data from XSS attacks. Once toggled on, you’ll be prompted to add an app domain which Privy to store user access tokens as a first-party cookie. This improves your app security and enhances your app with features like server-side rendering (SSR). Please see our [cookies guide](/recipes/react/cookies) for instructions on how to set an app domain in this field. # Customize your application Source: https://docs.privy.io/recipes/dashboard/customization Use the **Configuration > UI components** page of the dashboard to configure your app's brand settings, including name, logo, accent color, and legal policies. ## Name Use the **Name** input to set a name for your product as you'd like to present it to users. Privy will use this value to reference your product in OTP messages sent to users for login and various UIs throughout your app. ## Logo Use the **Logo** input to set a logo for your product. Provide the URL to a hosted image. We recommend a 2:1 aspect ratio with a size of 180px by 90px for best results. Please note that SVGs are not allowed, as they are incompatible with many major email clients. Privy will use this logo in two places: * in OTP emails sent to your users for passwordless email login * in the Privy modal shown to users when they login to your app If you'd like to remove the logo from the Privy modal or set a different logo instead, you can customize the logo via the SDK directly. You should still set a logo in the dashboard for use in OTP emails. ## Brand color Use the **Brand color** input to set an accent color for your application. Provide the color as a hexadecimal string. This will apply to links and buttons within Privy's UIs in your app. ## Legal ### Terms & conditions Use the **Terms & conditions** input to set the terms & conditions for your app. Please provide a hosted URL to a publicly viewable site. If set, users will be shown your terms & conditions as part of their login flow. ### Privacy policy Use the **Privacy policy** input to set the privacy policy for your app. Please provide a hosted URL to a publicly viewable site. If set, users will be shown your privacy policy as part of their login flow. ### Affirmative consent If your app requires affirmative consent for your users for your terms & conditions and privacy policy, enable the **Require affirmative consent** option. If enabled, users will be prompted for affirmative consent on your legal policies as part of their first login to your app. If disabled, users will be shown your legal policies without an explicit prompt. # Setting up SMS or WhatsApp login Source: https://docs.privy.io/recipes/dashboard/login-methods/sms-whatsapp Privy enables your users to log in to their account via SMS or WhatsApp. Your account can be set up for **either** SMS or WhatsApp, but **not both** at the same time. Your chosen configuration will apply to all applications under your Privy account. We recommend including alternative login options alongside SMS or enabling [Multi-Factor Authentication](/authentication/user-authentication/mfa) to ensure broad accessibility in regions without SMS coverage and to allow users to access their accounts in the event that they lose SMS access. Included on all plans. This enables SMS log in for only US and Canada. International SMS via BYO Twilio is available only for accounts on Scale and Enterprise plans. Reach out to [support@privy.io](mailto:support@privy.io) to request access. With your own Twilio account, you can configure geo-permissions for specific regions, implement custom fraud detection rules, set spending limits, and access comprehensive logging through Twilio's dashboard. 1. You can [sign up for a Twilio account here](https://www.twilio.com/try-twilio), if you don't already have one. Ensure that your Twilio account is a paid account, fully onboarded, and fully verified. 2. After you create an account, you'll have access to the Twilio Console, where you can configure a [Verify service](https://www.twilio.com/docs/verify) and [region coverage](https://www.twilio.com/docs/verify/preventing-toll-fraud/verify-geo-permissions). 3. Reach out to us at [support@privy.io](mailto:support@privy.io) and provide the following Twilio credentials: * Privy app ID * Twilio Account SID * Twilio Auth token * Twilio Verify service SID Out-of-the-box international SMS is only available for accounts on Enterprise plans. Reach out to [support@privy.io](mailto:support@privy.io) to request access. Privy uses Twilio to deliver international SMS, and underlying carrier costs are passed through to your account. Refer to [Twilio's pricing here](https://assets.cdn.prod.twilio.com/pricing-csv/SMSPricing.csv). **Privy supports these regions:** | Region | Region Code | | -------------- | ----------- | | Argentina | +54 | | Australia | +61 | | Canada | +1 | | Chile | +56 | | Czech Republic | +420 | | Germany | +49 | | Hong Kong | +852 | | Hungary | +36 | | Japan | +81 | | New Zealand | +64 | | Portugal | +351 | | Saudi Arabia | +966 | | Singapore | +65 | | South Korea | +82 | | Sweden | +46 | | Taiwan | +886 | | Thailand | +66 | | Turkey | +90 | | United Kingdom | +44 | | United States | +1 | To enable international SMS regions beyond our default coverage, you'll need to connect your own Twilio account to Privy, i.e., BYO Twilio. Available only for accounts on Scale and Enterprise plans. Reach out to [support@privy.io](mailto:support@privy.io) to request access. This enables WhatsApp log in for all regions that WhatsApp supports. # Optimize your setup Source: https://docs.privy.io/recipes/dashboard/optimizing For developers looking to optimize their Privy integration, we have a few key features that should help fine-tune the performance your setup. ## Manually set a verification key for authorization When verifying a Privy access token to authorize requests to your servers, by default the Privy Client's `verifyAuthToken` method will make a request to Privy's API to fetch the verification key for your app. Although it is cached for reuse, you can avoid this API request entirely by copying your verification key from the [Configuration > App settings > Basics tab of the Dashboard](https://dashboard.privy.io/apps?page=settings), under "Verify with key instead": ```ts @privy-io/node theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: 'your-privy-app-id', appSecret: 'your-privy-app-secret', // Set the verification key from the Dashboard when initializing the PrivyClient jwtVerificationKey: 'paste-your-verification-key-from-the-dashboard' }); ``` If you ever rotate your verification key, you will have to update this, but this will remove any network dependency on Privy for token verification. ## Get user data with identity tokens If you need access to the user object, especially on the server, this can be a costly action. To remove a network call from your critical path, we recommend using Privy's [identity tokens](/user-management/users/identity-tokens), which include the latest user information in token form. While it does not have the full user details (it omits certain lesser-needed fields for efficiency), it should have what you need to get started quickly. ## Set a custom API URL for `HttpOnly` cookies (`react-auth` only) In the case where you have set up and enabled `HttpOnly` cookies, on initial page load, the Privy SDK will start by making a call to fetch app details on our default `https://auth.privy.io` API URL. In `HttpOnly` cookie mode however, all your requests are routed through `https://privy.`. To avoid an occasional extra call on page load, we recommend explicitly setting the `apiUrl` in your `PrivyProvider`: ```tsx theme={"system"} return ( {children} ); ``` Note that this has a risk - if you are ever *disabling* `HttpOnly` cookies, you will need to remove this in order for your app to continue functioning properly. For a smooth transition, first remove the `apiUrl`, deploy, and then disable HttpOnly cookies. ## Handling rate limits When your application encounters rate limiting (HTTP 429 responses), implementing proper retry logic ensures a smooth user experience and optimal API usage. ### Understanding rate limit responses When you exceed a rate limit, Privy's API returns a `429 Too Many Requests` status code. Rate limits are applied per endpoint and are designed to ensure fair usage across all applications. ### Best practices for handling rate limits #### 1. Implement exponential backoff Exponential backoff is a standard error-handling strategy that gradually increases the wait time between retry attempts: ```typescript theme={"system"} export {}; declare const privy: any; async function makeRequestWithRetry( requestFn: () => Promise, maxRetries: number = 5, baseDelay: number = 1000 ): Promise { for (let attempt = 0; attempt < maxRetries; attempt++) { try { return await requestFn(); } catch (error: any) { // Check if it's a rate limit error if (error.status === 429 && attempt < maxRetries - 1) { // Calculate exponential backoff delay: 1s, 2s, 4s, 8s, 16s const delay = baseDelay * Math.pow(2, attempt); // Add jitter to prevent thundering herd const jitter = Math.random() * 1000; console.log(`Rate limited. Retrying in ${delay + jitter}ms...`); await new Promise((resolve) => setTimeout(resolve, delay + jitter)); } else { throw error; } } } throw new Error('Max retries exceeded'); } // Usage example const user = await makeRequestWithRetry(() => privy.users()._get('did:privy:xxxxx')); ``` #### 2. Batch your requests Instead of making individual API calls for each operation, batch multiple operations together when possible: ```typescript theme={"system"} export {}; declare const privy: any; const userIds = ['did:privy:xxxxx']; const processUser = (_user: unknown) => {}; // ❌ Avoid: Multiple individual requests for (const userId of userIds) { const user = await privy.users()._get(userId); processUser(user); } // ✅ Better: Use list endpoint with pagination for await (const user of privy.users().list()) { if (userIds.includes(user.id)) { processUser(user); } } ``` For bulk user operations, use the [batch user creation endpoint](/user-management/migrating-users-to-privy/create-or-import-a-batch-of-users) which allows creating up to 100 users per request. #### 3. Cache responses when appropriate For data that doesn't change frequently, implement caching to reduce API calls: ```typescript theme={"system"} declare const privy: any; const userCache = new Map(); const CACHE_TTL = 5 * 60 * 1000; // 5 minutes async function getCachedUser(userId: string) { const cached = userCache.get(userId); if (cached && Date.now() - cached.timestamp < CACHE_TTL) { return cached.user; } const user = await privy.users()._get(userId); userCache.set(userId, {user, timestamp: Date.now()}); return user; } ``` #### 4. Use identity tokens for authenticated users For getting user data about authenticated users, use [identity tokens](/user-management/users/identity-tokens) instead of making API calls. This approach is rate-limit-free and provides user information directly from the token. ```typescript theme={"system"} export {}; declare const privy: any; const idToken = 'insert-id-token'; const userId = 'did:privy:xxxxx'; // ✅ Preferred: Use identity token (no API call) const userFromToken = await privy.users().get({id_token: idToken}); // ❌ Avoid when possible: Direct API call (subject to rate limits) const userFromApi = await privy.users()._get(userId); ``` #### 5. Implement circuit breakers For production applications, consider implementing a circuit breaker pattern to temporarily stop making requests when rate limits are consistently hit: ```typescript theme={"system"} class CircuitBreaker { private failureCount = 0; private lastFailureTime = 0; private readonly threshold = 3; private readonly cooldown = 60000; // 1 minute async execute(fn: () => Promise): Promise { // Check if circuit is open if (this.failureCount >= this.threshold) { const timeSinceLastFailure = Date.now() - this.lastFailureTime; if (timeSinceLastFailure < this.cooldown) { throw new Error('Circuit breaker is open. Too many rate limit errors.'); } // Reset after cooldown this.failureCount = 0; } try { const result = await fn(); this.failureCount = 0; // Reset on success return result; } catch (error: any) { if (error.status === 429) { this.failureCount++; this.lastFailureTime = Date.now(); } throw error; } } } ``` ### Additional optimization tips * **Monitor your usage**: Track your API call patterns to identify optimization opportunities * **Use webhooks**: For real-time updates, consider using webhooks instead of polling endpoints * **Optimize query patterns**: Review your query logic to eliminate unnecessary or redundant API calls * **Parallelize independent requests**: Use `Promise.all()` for independent requests to reduce total execution time while staying within rate limits By following these practices, your application can handle rate limits gracefully and provide a reliable experience for your users. # Bot traffic mitigation Source: https://docs.privy.io/recipes/dashboard/preventing-bots The strongest bot mitigation setup combines several controls. You can start in the Privy dashboard and then add sitewide protections. ## 1. Enable invisible CAPTCHA Privy supports invisible CAPTCHA with [Cloudflare Turnstile](https://www.cloudflare.com/products/turnstile/) and [hCaptcha](https://www.hcaptcha.com/). Enable CAPTCHA in [App settings > Advanced](https://dashboard.privy.io/apps?page=settings\&setting=advanced). When using hCaptcha, configure the risk tolerance setting to define how strictly the system blocks suspicious attempts. When using a strict CSP, include CAPTCHA domains in policy directives. See the [CSP guide](/security/implementation-guide/content-security-policy#optional-features). ## 2. Block low-quality email signups On the [Authentication](https://dashboard.privy.io/apps?page=login-methods) page, enable email restrictions that reduce throwaway account creation: * Block temporary email domains * Disable `+` aliases in email addresses Privy uses [`mailchecker`](https://github.com/FGRibreau/mailchecker/) to identify temporary email domains. Blocking `+` aliases increases friction for abuse, but may also impact legitimate alias usage. Choose this setting based on your app's risk profile. ## 3. Block VOIP numbers for phone login On the [Authentication](https://dashboard.privy.io/apps?page=login-methods) page, enable VOIP blocking for phone login. When SMS or WhatsApp login is in enabled, block VOIP numbers to reduce disposable phone signups and OTP abuse. ## 4. Use the denylist for repeat offenders Use the [denylist](/user-management/users/managing-users/denylist) to block known bad users from logging in or creating new accounts. Supported denylist entries include: * Email addresses * Email domains * Phone numbers * EVM wallet addresses * Solana wallet addresses ## 5. Add sitewide Cloudflare protections Privy controls are strongest when paired with edge protection in front of your app. A practical Cloudflare setup usually includes: * Bot management or Super Bot Fight Mode * Managed Challenge on high-risk pages like sign up and login * Blocking or challenging high-risk traffic segments for your app's threat model ## 6. Add supporting controls For stronger defense in depth, also configure: * [Allowed domains](/recipes/dashboard/allowed-domains) to prevent unauthorized client usage of your app ID * [Allowed OAuth redirects](/recipes/react/allowed-oauth-redirects) to reduce OAuth abuse risk * [MFA](/authentication/user-authentication/mfa/overview) for sensitive or high-value actions * Minimum required login methods only, to reduce attack surface Anti-bot strategy should evolve with traffic patterns. Review signup quality, OTP volume, and conversion rates on a regular cadence. ## FAQ CAPTCHA providers do not share specific details about how they classify attempts as bot-like traffic. As a workaround, users can try:
  • Disabling VPN, proxy, or traffic filtering tools
  • Trying an incognito/private window to identify browser extension interference
  • Trying a different browser or device
  • Switching networks (for example, from public Wi-Fi to mobile data)
  • Retrying after a short wait
Privy does not recommend deleting users unless absolutely necessary. Blocking future access with the [denylist](/user-management/users/managing-users/denylist) is usually a better first step. When you need to delete users, follow [Deleting users](/user-management/users/managing-users/deleting-users). Enable Twilio [Fraud Guard](https://www.twilio.com/docs/verify/preventing-toll-fraud), and review Twilio [Verify geo-permissions](https://www.twilio.com/docs/verify/preventing-toll-fraud/verify-geo-permissions) to limit risky destination regions.
# Using Privy and Due for on/off ramping Source: https://docs.privy.io/recipes/due-on-off-ramp Due is an API platform for moving money between crypto and fiat. It lets you send, receive, and convert funds using stablecoins and bank accounts. You use Due to automate on/off ramping and track transfers programmatically. If you want to bridge crypto and traditional finance, Due makes it simple. ## Table of Contents * Prerequisites * Initial setup (required for all transfers) * Off ramping crypto to fiat * Option A: Signature-based transfer (via transfer intent) * Option B: Direct transfer (via funding address) * On ramping fiat to crypto * Virtual accounts (fiat on-ramp) Dedicated banking details for automatic fiat-to-crypto conversion. * Tracking transfer status ## Prerequisites You will need an **[API key](https://www.opendue.com/api)** for the Due Network APIs. ## Initial setup (required for all transfers) Before executing any transfers, you must create and link accounts for your user. ### 1. Create a customer account in Due Create an account for your user in Due. Save the returned `id` as `ACCOUNT_ID`. ```bash theme={"system"} curl --request POST \ --url https://api.due.network/v1/accounts \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --data '{ "type": "individual", "email": "user@example.com", "details": { "firstName": "John", "lastName": "Doe" } }' ``` ### 2. Create or get a Privy wallet Create an embedded wallet for the user via the [Privy API](/api-reference/wallets/create). Save the `id` as `wallet_id` and the wallet's `address`. ```bash theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets \ -u ":" \ --header 'privy-app-id: ' \ --header 'Content-Type: application/json' \ --data '{ "owner": { "user_id": "did:privy:clxduz8al00kql00fva24ggty" }, "chain_type": "ethereum" }' ``` ### 3. Link Privy wallet to Due account Link the Privy wallet address to the user's Due account. ```bash theme={"system"} curl --request POST \ --url https://api.due.network/v1/wallets \ --header 'Authorization: Bearer ' \ --header 'Content-Type: application/json' \ --header 'Due-Account-Id: ' \ --data '{ "address": "0xcF5AaaBe14Ba42d9D765C8f2b9099c3b69a25321" }' ``` *** ## Off ramping crypto to fiat This flow moves cryptocurrency from a user's Privy wallet to an external bank account. You can choose between two methods: * Signature based transfer: via Transfer Intent, more secure, requires signatures. * Direct transfer via funding address: simpler, no signatures needed. ### Option A: Signature-based transfer (via transfer intent) This method involves the user signing transaction data with their Privy wallet. #### Step 1: Create a recipient Define the destination bank account. ```bash theme={"system"} curl --request POST \ --url https://api.due.network/v1/recipients \ --header 'Authorization: Bearer ' \ --header 'Due-Account-Id: ' \ --data '{ "name": "Marie Dubois", "details": { "schema": "bank_sepa", "accountType": "individual", "firstName": "Marie", "lastName": "Dubois", "IBAN": "FR1420041010050500013M02606" } }' ``` #### Step 2: Get a quote Get a quote for the transfer. **Note:** Quotes are short-lived (2 mins), so get it right before creating the transfer. ```bash theme={"system"} curl --request POST \ --url https://api.due.network/v1/transfers/quote \ --header 'Authorization: Bearer ' \ --header 'Due-Account-Id: ' \ --data '{ "source": {"rail": "base", "currency": "USDC"}, "destination": {"rail": "sepa", "currency": "EUR", "amount": "1000"} }' ``` ```json theme={"system"} { "token": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...", "source": { "rail": "base", "currency": "USDC", "amount": "1177.171875", "fee": "5.296875" }, "destination": { "rail": "sepa", "currency": "EUR", "amount": "1000", "fee": "4.52" }, "fxRate": 1.1718750000000002, "fxMarkup": 5, "expiresAt": "2025-10-02T16:40:31.951762984Z" } ``` Be sure to take note of the `token` field in the response, as you'll need it to create the transfer. #### Step 3: Create a transfer Use the token returned by the quote to create the transfer. ```bash theme={"system"} curl --request POST \ --url https://api.due.network/v1/transfers \ --header 'Authorization: Bearer ' \ --header 'Due-Account-Id: ' \ --data '{ "quote": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...", "sender": "wlt_e3lLDBYiPMHxCv1Q", "recipient": "rcp_fRlKXtbmyzvRwmY9", "memo": "Invoice#1" }' ``` #### Step 4: Create & sign the transfer intent 1. **Create the Intent:** Request a transfer intent from Due for the transfer ID (`tf_...`) created above. ```bash theme={"system"} curl --request POST \ --url https://api.due.network/v1/transfers//transfer_intent \ --header 'Authorization: Bearer ' \ --header 'Due-Account-Id: ' ``` The response contains a `signables` array, typically with two objects to sign (`Permit` and `PayoutIntent`). 2. **Sign with Privy:** For *each object* in the `signables` array, call the [Privy API](/api-reference/wallets/ethereum/eth-signtypeddata-v4) to get a signature or send the signables to the client side for [signing](/wallets/using-wallets/ethereum/sign-typed-data#sign-typed-data). Use the `eth_signTypedData_v4` method with the `value` object from each signable. ```bash REST API theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets//rpc \ -u ":" \ --data '{ "method": "eth_signTypedData_v4", "params": { "typed_data": { // ... Paste the `value` object from a signable here ... } } }' ``` ```tsx React theme={"system"} import {useSignTypedData} from '@privy-io/react-auth'; const signables = [/* ... signables array from Due ... */]; const {signTypedData} = useSignTypedData(); const signatures = await Promise.all( signables.map(async (signable) => { const signature = await signTypedData({ typedData: signable.value, }); return { id: signable.id, signature, }; }) ); ``` #### Step 5: Submit the signed intent Submit the original transfer intent object back to Due, now including the `signature` for each object in the `signables` array. ```bash theme={"system"} curl --request POST \ --url https://api.due.network/v1/transfer_intents/submit \ --header 'Authorization: Bearer ' \ --header 'Due-Account-Id: ' \ --data '{ "id": "ti_24QbulYAT9nfjU", // ... entire transfer intent object from the previous step ... "signables": [ { // ... first signable object ... "signature": "0xd99802ab7a14b535ad0bf9c69a7cfd86..." }, { // ... second signable object ... "signature": "0xa1b2c3d4e5f678901234567890abcdef..." } ] // ... rest of transfer intent object ... }' ``` ### Option B: Direct transfer (via funding address) This simpler method provides a temporary address to send funds to, avoiding the signature flow. 1. **Create Funding Address:** After creating a transfer (Steps 1-3 above), request a funding address for it. ```bash theme={"system"} curl --request POST \ --url https://api.due.network/v1/transfers//funding_address \ --header 'Authorization: Bearer ' \ --header 'Due-Account-Id: ' ``` 2. **Send funds from the wallet:** Use the Privy API to sign and submit the onchain transfer for the *exact* amount to the funding `address` received. The transfer will process automatically once funds are received. *** ## On ramping fiat to crypto This flow lets users convert fiat currency from a bank account into cryptocurrency through Due, which deposits crypto into the user wallet. It involves obtaining a transfer quote, creating the transfer, and providing the user with banking details to complete the transaction. Due handles the fiat-to-crypto conversion and settlement, requiring no additional signatures or manual intervention. 1. **Get a quote:** ```bash theme={"system"} curl --request POST \ --url https://api.due.network/v1/transfers/quote \ --header 'Authorization: Bearer ' \ --header 'Due-Account-Id: ' \ --data '{ "source": {"rail": "ach", "currency": "USD", "amount": "100"}, "destination": {"rail": "base", "currency": "USDC"} }' ``` 2. **Create a transfer:** Use the quote token and specify your linked Due wallet ID as the `recipient`. ```bash theme={"system"} curl --request POST \ --url https://api.due.network/v1/transfers \ --header 'Authorization: Bearer ' \ --header 'Due-Account-Id: ' \ --data '{ "quote": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9...", "recipient": "wlt_e3lLDBYiPMHxCv1Q" }' ``` 3. **Provide banking details to the user:** The API response will include `bankingDetails` (account number, routing number, etc.). Share these details with the user so they can initiate the fiat transfer from their bank account. Due handles the conversion and deposits the cryptocurrency into the recipient wallet automatically. No additional signatures are required. *** ## Virtual accounts (fiat on-ramp) Virtual accounts provide dedicated banking details (e.g., an IBAN) that automatically convert incoming fiat deposits to a specified stablecoin and send them to your wallet. ### Example: Create a EUR → EURC virtual account ```bash theme={"system"} curl --request POST \ --url https://api.due.network/v1/virtual_accounts \ --header 'Authorization: Bearer ' \ --header 'Due-Account-Id: ' \ --data '{ "destination": "wlt_e3lLDBYiPMHxCv1Q", "schemaIn": "bank_sepa", "currencyIn": "EUR", "railOut": "base", "currencyOut": "EURC", "reference": "customer_x_eur_onramp" }' ``` The response provides an IBAN. Any EUR sent to this IBAN will be automatically converted to EURC and deposited into the destination wallet. *** ## Tracking transfer status Check the status of any transfer using its ID. ```bash theme={"system"} curl --request GET \ --url https://api.due.network/v1/transfers/ \ --header 'Authorization: Bearer ' \ --header 'Due-Account-Id: ' ``` **That's it! Your app can now move value between fiat bank accounts and stablecoins using Privy embedded wallets and the Due Network API, with Due handling conversion and settlement.** Here are some additional resources to help expand your integration: * [Privy API references](/api-reference/introduction) * [Due Network API documentation](https://due.readme.io/docs/privy-due#using-privy-x-due-to-move-between-fiat-and-stablecoins) # Abstract global wallet Source: https://docs.privy.io/recipes/ecosystem/abstract-global-wallet This recipe assumes you have already [created an app](https://docs.privy.io/basics/get-started/dashboard/create-new-app) on the Privy Dashboard and configured your Privy Provider. Refer to our [React SDK Setup](/basics/react/setup) or [React Native SDK Setup](/basics/react-native/setup) documentation to get started. From the Privy dashboard, navigate to [**Global Wallet > Integrations**](https://dashboard.privy.io/apps?page=ecosystem\&tab=integrations). Scroll down to find Abstract and toggle the switch to enable the integration. Note the provider app id, you will need it in the next step. To prompt users to log into your app with their Abstract Global Wallet, use the `loginWithCrossAppAccount` method from the `useCrossAppAccounts` hook: ```tsx theme={"system"} import {usePrivy, useCrossAppAccounts} from '@privy-io/react-auth'; function LoginButton() { const {ready, authenticated} = usePrivy(); const {loginWithCrossAppAccount} = useCrossAppAccounts(); return ( ); } ``` To prompt users to log into your app with an account from a provider app, use the `loginWithCrossApp` method from the `useLoginWithCrossApp` hook: ```tsx theme={"system"} import {usePrivy, useLoginWithCrossApp} from '@privy-io/expo'; function LoginButton() { const {ready, user} = usePrivy(); const {loginWithCrossApp} = useLoginWithCrossApp(); return ( ); } ``` To prompt users to sign a message with their Abstract Global Wallet, use the `signMessage` method from the `useCrossAppAccounts` hook: ```tsx theme={"system"} import {usePrivy, useCrossAppAccounts} from '@privy-io/react-auth'; function SignMessageButton() { const {user} = usePrivy(); const {signMessage} = useCrossAppAccounts(); const crossAppAccount = user.linkedAccounts.find((account) => account.type === 'cross_app'); const address = crossAppAccount.embeddedWallets[0].address; return ( ); } ``` Refer to [Using Global Wallets](/wallets/global-wallets/integrate-a-global-wallet/using-global-wallets) for more details. To prompt users to sign a message with their Abstract Global Wallet, use the `signMessage` method from the `useSignMessageWithCrossApp` hook: ```tsx theme={"system"} import {usePrivy, useSignMessageWithCrossApp} from '@privy-io/expo'; function SignMessageButton() { const {user} = usePrivy(); const {signMessage} = useSignMessageWithCrossApp(); const crossAppAccount = user.linked_accounts.find((account) => account.type === 'cross_app'); const address = crossAppAccount.embedded_wallets[0].address; return ( ); } ``` Refer to [Using Global Wallets](/wallets/global-wallets/integrate-a-global-wallet/using-global-wallets) for more details. # Integrating Base builder codes Source: https://docs.privy.io/recipes/evm/base-builder-codes [Base Builder Codes](https://base.dev) enable developers to unlock rewards by attributing onchain activity back to their applications. By appending an [ERC-8021](https://www.erc8021.com/) attribution suffix to transaction data, Base can identify which application originated each transaction and reward builders accordingly. Privy's `dataSuffix` plugin makes this integration seamless—once configured, the attribution suffix is automatically appended to all transactions sent through your app, including both EOA transactions and ERC-4337 smart wallet user operations. ## Prerequisites * A Privy app with `@privy-io/react-auth` v3.22.0+ configured * A registered Builder Code from [base.dev](https://base.dev) > Settings > Builder Codes ## Installation Install the `ox` library to generate ERC-8021 compliant data suffixes: ```bash theme={"system"} npm install ox ``` ## Integration Import the `dataSuffix` plugin from Privy and the `Attribution` utility from `ox`: ```tsx theme={"system"} import {PrivyProvider, dataSuffix} from '@privy-io/react-auth'; import {Attribution} from 'ox/erc8021'; // Generate the ERC-8021 attribution suffix with your Builder Code const ERC_8021_ATTRIBUTION_SUFFIX = Attribution.toDataSuffix({ codes: ['YOUR-BUILDER-CODE'] // Replace with your code from base.dev > Settings > Builder Codes }); function App() { return ( {/* your app's content */} ); } ``` **That's it!** Once configured, every transaction sent through your Privy-powered app will include the ERC-8021 attribution suffix, enabling you to unlock rewards from Base. ## How it works The `dataSuffix` plugin automatically appends your attribution data to: | Transaction Type | Where Suffix is Appended | | ------------------------ | ------------------------ | | EOA transactions | `transaction.data` field | | Smart wallets (ERC-4337) | `userOp.callData` field | The ERC-8021 suffix follows a standardized format that Base's sequencer can parse: ``` TX_DATA + [CODES_LENGTH][CODES] + [SCHEMA_ID] + [ERC_MARKER] ``` This allows Base to attribute transactions to your app without requiring any changes to your existing transaction logic or smart contracts. Base Builder Codes rewards are specific to transactions on Base mainnet and Base Sepolia. However, the `dataSuffix` plugin will append the suffix to transactions on **all chains**. If you need chain-specific suffix behavior, please [reach out to the Privy team](https://privy.io/slack). The `dataSuffix` plugin is not yet supported when using the [`@privy-io/wagmi`](/wallets/connectors/ethereum/integrations/wagmi) adapter. If you need wagmi support, please [contact the Privy team](https://privy.io/slack) for assistance. ## Resources Learn more about the ERC-8021 attribution standard. Register for a Builder Code and track your rewards. Documentation for the ox Ethereum utilities library. # Swapping crypto using Privy and Bebop Source: https://docs.privy.io/recipes/evm/bebop-swap-guide Learn how to integrate Bebop's swap functionality with Privy embedded wallets Bebop enables applications to execute gasless or self-executed token swaps using a request-for-quote (RFQ) model that eliminates slippage. This guide demonstrates how to integrate Bebop's swap functionality with Privy embedded wallets. ## Prerequisites Before implementing swaps, contact Bebop to receive the following credentials: * **Auth Key** – Enables authenticated API calls with improved rate limits and pricing * **Source ID** – Identifies the application as an integration partner for revenue tracking ## Setup token approvals Bebop requires token approvals before executing swaps. Applications can use either standard ERC20 approvals or Permit2. ### Standard ERC20 approvals To use standard ERC20 approvals, specify `approval_type=Standard` when requesting a quote from Bebop's API. Before executing a swap, the application must grant the Bebop settlement contract (`0xbbbbbBB520d69a9775E85b458C58c648259FAD5F`) an allowance to spend the user's tokens. ```tsx theme={"system"} import {maxUint256, erc20Abi, encodeFunctionData} from 'viem'; import {useWallets} from '@privy-io/react-auth'; const BEBOP_SETTLEMENT_ADDRESS = '0xbbbbbBB520d69a9775E85b458C58c648259FAD5F'; const WETH_ADDRESS = '0x4200000000000000000000000000000000000006'; // WETH on Base async function approveToken() { const {wallets} = useWallets(); const embeddedWallet = wallets.find((wallet) => wallet.walletClientType === 'privy'); // Get EIP-1193 provider from Privy embedded wallet const provider = await embeddedWallet.getEthereumProvider(); // Encode approval transaction data const data = encodeFunctionData({ abi: erc20Abi, functionName: 'approve', args: [BEBOP_SETTLEMENT_ADDRESS, maxUint256] }); // Submit approval transaction const txHash = await provider.request({ method: 'eth_sendTransaction', params: [ { from: embeddedWallet.address, to: WETH_ADDRESS, data, value: '0x0' } ] }); return txHash; } ``` ```tsx theme={"system"} import {maxUint256, erc20Abi, encodeFunctionData} from 'viem'; import {useEmbeddedEthereumWallet} from '@privy-io/expo'; const BEBOP_SETTLEMENT_ADDRESS = '0xbbbbbBB520d69a9775E85b458C58c648259FAD5F'; const WETH_ADDRESS = '0x4200000000000000000000000000000000000006'; // WETH on Base async function approveToken() { const wallet = useEmbeddedEthereumWallet(); // Get EIP-1193 provider from Privy embedded wallet const provider = await wallet.getEthereumProvider(); // Encode approval transaction data const data = encodeFunctionData({ abi: erc20Abi, functionName: 'approve', args: [BEBOP_SETTLEMENT_ADDRESS, maxUint256] }); // Submit approval transaction const txHash = await provider.request({ method: 'eth_sendTransaction', params: [ { from: wallet.address, to: WETH_ADDRESS, data, value: '0x0' } ] }); return txHash; } ``` ```typescript theme={"system"} import {maxUint256, erc20Abi, encodeFunctionData} from 'viem'; import {PrivyClient} from '@privy-io/node'; const BEBOP_SETTLEMENT_ADDRESS = '0xbbbbbBB520d69a9775E85b458C58c648259FAD5F'; const WETH_ADDRESS = '0x4200000000000000000000000000000000000006'; // WETH on Base const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID, appSecret: process.env.PRIVY_APP_SECRET }); async function approveToken(walletId: string) { // Encode approval transaction data const data = encodeFunctionData({ abi: erc20Abi, functionName: 'approve', args: [BEBOP_SETTLEMENT_ADDRESS, maxUint256] }); // Submit approval transaction const response = await privy .wallets() .ethereum() .sendTransaction(walletId, { caip2: 'eip155:8453', // Base params: { transaction: { to: WETH_ADDRESS, data, chain_id: 8453 } } }); return response.hash; } ``` Applications can also use Permit2 for approvals. Consult Bebop's documentation for implementation details. ## Request a quote After configuring approvals, request a quote from Bebop's API. The RFQ model guarantees the quoted price with zero slippage. The following example requests a quote to swap 1 WETH for USDC on Base: ```typescript theme={"system"} import axios from 'axios'; import {parseEther} from 'viem'; const BEBOP_SOURCE_ID = process.env.BEBOP_SOURCE_ID || ''; // Source ID issued by Bebop const BEBOP_AUTH_KEY = process.env.BEBOP_AUTH_KEY || ''; // Auth key issued by Bebop const tokensToSell = ['0x4200000000000000000000000000000000000006']; // WETH on Base const sellAmounts = [parseEther('1')]; // 1 WETH const tokensToBuy = ['0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913']; // USDC on Base interface Chain { chainId: number; name: string; } const chain: Chain = { chainId: 8453, name: 'base' }; async function getSwapQuote(walletAddress: string): Promise { const {data: quote} = await axios.get(`https://api.bebop.xyz/pmm/${chain.name}/v3/quote`, { params: { buy_tokens: tokensToBuy.toString(), sell_tokens: tokensToSell.toString(), sell_amounts: sellAmounts.toString(), taker_address: walletAddress, gasless: false, approval_type: 'Standard', source: BEBOP_SOURCE_ID }, headers: { 'source-auth': BEBOP_AUTH_KEY } }); if (quote.error) { throw new Error(`Quote error: ${quote.error}`); } return quote.tx; } ``` ## Execute the swap Once Bebop returns a quote, the application can execute the swap by broadcasting the transaction using Privy's embedded wallet provider. ```tsx theme={"system"} import {useWallets} from '@privy-io/react-auth'; async function executeSwap(rawTransaction) { const {wallets} = useWallets(); const embeddedWallet = wallets.find((wallet) => wallet.walletClientType === 'privy'); const provider = await embeddedWallet.getEthereumProvider(); const txHash = await provider.request({ method: 'eth_sendTransaction', params: [rawTransaction] }); return txHash; } // Complete swap flow async function performSwap() { try { const {wallets} = useWallets(); const embeddedWallet = wallets.find((wallet) => wallet.walletClientType === 'privy'); // Request quote from Bebop const transaction = await getSwapQuote(embeddedWallet.address); // Execute transaction onchain const txHash = await executeSwap(transaction); return txHash; } catch (error) { console.error('Swap failed:', error); throw error; } } ``` ```tsx theme={"system"} import {useEmbeddedEthereumWallet} from '@privy-io/expo'; async function executeSwap(rawTransaction, wallet) { const provider = await wallet.getEthereumProvider(); const txHash = await provider.request({ method: 'eth_sendTransaction', params: [rawTransaction] }); return txHash; } // Complete swap flow async function performSwap() { try { const wallet = useEmbeddedEthereumWallet(); // Request quote from Bebop const transaction = await getSwapQuote(wallet.address); // Execute transaction onchain const txHash = await executeSwap(transaction, wallet); return txHash; } catch (error) { console.error('Swap failed:', error); throw error; } } ``` ```typescript {skip-check} theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID, appSecret: process.env.PRIVY_APP_SECRET }); async function executeSwap(walletId: string, rawTransaction: any) { const response = await privy .wallets() .ethereum() .sendTransaction(walletId, { caip2: rawTransaction.caip2 || 'eip155:8453', // Base params: { transaction: { to: rawTransaction.to, data: rawTransaction.data, value: rawTransaction.value, chain_id: 8453 } } }); return response.hash; } // Complete swap flow async function performSwap(walletId: string) { try { // Get wallet address const wallet = await privy.wallets().get(walletId); // Request quote from Bebop const transaction = await getSwapQuote(wallet.address); // Execute transaction onchain const txHash = await executeSwap(walletId, transaction); return txHash; } catch (error) { console.error('Swap failed:', error); throw error; } } ``` ## Fee monetization Bebop embeds fees directly in quotes, which are collected by market makers and distributed to integration partners monthly. The source ID must be included in all requests to ensure proper revenue tracking. Bebop can optionally hedge collected fees to stablecoins (such as ETH to USDC) to prevent applications from accumulating unwanted token inventory. Contact Bebop to configure fee hedging preferences and review revenue distribution terms. # ERC-1271 signatures Source: https://docs.privy.io/recipes/evm/erc-1271-signatures Enable ERC-1271 compatible signatures for smart contract wallets and off-chain verifiers [ERC-1271](https://eips.ethereum.org/EIPS/eip-1271) is a standard interface that allows smart contracts to verify signatures on behalf of an address. When a Privy embedded wallet is upgraded to a smart contract wallet — via [EIP-7702](/wallets/using-wallets/ethereum/sign-7702-authorization) or Privy's [gas sponsorship](/wallets/gas-and-asset-management/gas/setup) — the wallet address has contract code. Some smart contracts and off-chain verifiers check signatures by calling the ERC-1271 `isValidSignature` method on the wallet address instead of using `ecrecover`. To ensure your signatures are verifiable by these ERC-1271 aware systems, Privy supports an `erc1271` signature mode. ERC-1271 signature mode is only needed when your wallet is an EIP-7702 delegated smart contract wallet, such as when using Privy's gas sponsorship. Standard embedded wallets (EOAs) do not need this mode. ## When to use ERC-1271 signatures Use ERC-1271 signatures when **all** of the following are true: * Your wallet uses [gas sponsorship](/wallets/gas-and-asset-management/gas/setup) or is otherwise delegated via EIP-7702 * A smart contract or off-chain service verifies signatures using the ERC-1271 `isValidSignature` interface Common use cases include: * **DeFi protocols** that support both EOA and smart account signers * **Off-chain payment authorization** (e.g. [x402 payments](/recipes/agent-integrations/x402) with gas-sponsored wallets) * **SIWE (Sign-In With Ethereum)** with smart contract wallets * Any integration where the verifier calls `IERC1271.isValidSignature` on the signer address ## Producing ERC-1271 signatures Privy exposes ERC-1271 signing through the `signatureOptions: {type: 'erc1271'}` option on signing methods. Pass `signatureOptions: {type: 'erc1271'}` to signing methods when the wallet is gas-sponsored or EIP-7702 delegated. ### Signing a message ```typescript theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({appId: 'your-app-id', appSecret: 'your-app-secret'}); const {signature} = await privy .wallets() .ethereum() .signMessage('your-wallet-id', { message: 'I authorize this action', signature_options: {type: 'erc1271'} }); ``` ### Signing typed data (EIP-712) ```typescript theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({appId: 'your-app-id', appSecret: 'your-app-secret'}); const {signature} = await privy .wallets() .ethereum() .signTypedData('your-wallet-id', { params: { typed_data: { domain: { name: 'My App', version: '1', chainId: 1, verifyingContract: '' }, types: { Order: [ {name: 'amount', type: 'uint256'}, {name: 'deadline', type: 'uint256'} ] }, primary_type: 'Order', message: { amount: '1000000', deadline: '1893456000' } } }, signature_options: {type: 'erc1271'} }); ``` ## How it works When a Privy embedded wallet is upgraded via EIP-7702 (either through gas sponsorship or a direct delegation), the wallet address has smart contract bytecode. The delegated smart contract implements `isValidSignature(bytes32 hash, bytes signature)`, which validates that the underlying EOA's private key produced the signature. Privy's `erc1271` signature mode produces signatures in the exact format that the delegated smart contract's `isValidSignature` implementation expects. Without this mode, the raw ECDSA signature bytes may not match the format the smart contract validates, causing verification to fail. # Using Flashblocks with Privy Source: https://docs.privy.io/recipes/evm/flashblocks [Flashblocks](https://docs.base.org/base-chain/flashblocks/apps) are a Base L2 feature that allows for faster transaction confirmation times, with most pre-confirmations happening close to 200ms. **Privy offers Flashblocks support on Base and Base Sepolia by default.** There is nothing you need to do to enable Flashblocks support – your application automatically leverages Flashblocks pre-confirmations for faster transaction experiences. If you'd like to integrate your **own** Flashblocks provider instead of using Privy's default offering, you can do so following the guide below. ### 0. Set up Privy in your app To start, if you haven't yet set up Privy, get your application on service [set up with Privy's basic functionality](/basics/get-started/platforms). Follow the linked quickstarts depending on your If using Privy's React or Expo SDKs, install the `@privy-io/chains` package as well: ### 1. Get your Flashblocks RPC URL Next, get your custom Flashblocks RPC URL from your own RPC provider. Most RPC providers like Quicknode, Alchemy, and Infura offer Flashblocks RPCs for Base. ### 2. Use your custom RPC URL for Base and/or Base Sepolia Next, configure Privy with your custom Flashblocks RPC URL. In addition to Privy's React or Expo SDKs, install the `@privy-io/chains` and `viem` packages: ```sh theme={"system"} npm i @privy-io/chains viem ``` Next, use the `addRpcUrlOverride` method from `@privy-io/chains` to set your RPC URL in a `viem/chains` object: ```tsx theme={"system"} import {base, baseSepolia} from 'viem/chains'; import {addRpcUrlOverride} from '@privy-io/chains'; const baseWithFlashblocks = addRpcUrlOverride(base, 'insert-flashblocks-rpc-url-for-base'); const baseSepoliaWithFlashblocks = addRpcUrlOverride( baseSepolia, 'insert-flashblocks-rpc-url-for-base-sepolia' ); ``` Then, pass the chain representations with your custom Flashblocks RPC URLs to the `PrivyProvider`'s `defaultChain` and `supportedChains` property: ```tsx theme={"system"} {/* your app's content */} ``` Use the NodeJS SDK's [`signTransaction`](/wallets/using-wallets/ethereum/sign-a-transaction) method to request a signature over your desired transaction on Base or Base Sepolia: ```ts @privy-io/node theme={"system"} const {signedTransaction} = await privy.wallets().ethereum().signTransaction('insert-wallet-id', { params: { // Replace with your desired transaction transaction: { to: '0xE3070d3e4309afA3bC9a6b057685743CF42da77C', value: '0x2386F26FC10000', chainId: 8453, } } }); ``` Next, create a viem public client, setting the `transport` to an HTTP transport with your custom Flashblocks URL: ```ts theme={"system"} import {createPublicClient, http} from 'viem'; import {base} from 'viem/chains'; const publicClient = createPublicClient({ chain: base, transport: http('insert-custom-flashblocks-RPC-URL') }); ``` Lastly, broadcast the `signedTransaction` using the public client's [`sendRawTransaction`](https://viem.sh/docs/actions/wallet/sendRawTransaction#sendrawtransaction) method. You can then wait for the transaction to be confirmed using the client's `waitForTransactionReceipt` method: ```ts theme={"system"} const hash = await publicClient.sendRawTransaction({serializedTransaction: signedTransaction}); const receipt = await publicClient.waitForTransactionReceipt({hash}); ``` Use the REST API's [`POST /v1/wallets/[wallet_id]/rpc`](/wallets/using-wallets/ethereum/sign-a-transaction) method with the `eth_signTransaction` RPC to request a signature over your desired transaction: ```sh theme={"system"} $ curl --request POST https://api.privy.io/v1/wallets//rpc \ -u ":" \ -H "privy-app-id: " \ -H "privy-authorization-signature: " \ -H 'Content-Type: application/json' \ -d '{ "method": "eth_signTransaction", "params": { # Replace with your desired transaction "transaction": { "to": "0xE3070d3e4309afA3bC9a6b057685743CF42da77C", "value": "0x2386F26FC10000", "chain_id": 8453, "type": 2, "gas_limit": "0x5208", "nonce": 1, "max_fee_per_gas": "0x14bf7dadac", "max_priority_fee_per_gas": "0xf4240" } } }' ``` Next, using the `signed_transaction` from the response, broadcast the `signed_transaction` to your custom Flashblocks RPC URL using the `eth_sendRawTransaction` RPC method, like so: Make sure to update the cURL below with any authentication or headers required by your RPC provider. ```sh theme={"system"} curl -X POST \ -H "Content-Type: application/json" \ -d '{ "jsonrpc": "2.0", "method": "eth_sendRawTransaction", "params": ["insert-signed-transaction"], "id": 1 }' ``` ### 3. Send transactions **That's it!** Privy will now route transactions on Base and/or Base Sepolia through the Flashblocks RPC URL you configured. Transactions should execute with near-200ms pre-confirmation times. # EVM transactions and signing Source: https://docs.privy.io/recipes/evm/overview EVM transaction recipes show how to send transfers, batch calls, manage pending transactions, and use modern account patterns. Send ERC-20 transfers from Privy-managed wallets. Combine multiple onchain actions into one execution flow. Replace stuck transactions with higher-fee submissions. Integrate low-latency block execution patterns. Upgrade EOAs with smart-account capabilities for app UX. # Login with Farcaster Source: https://docs.privy.io/recipes/farcaster/login [**Farcaster**](https://www.farcaster.xyz/) is a sufficiently decentralized social network whose core social graph is stored on-chain. Users can choose how content they create is stored and it enables unique, composable experiences by enabling users to link their accounts with a wallet of their choosing. **Privy enables your users to easily log in to your app using their Farcaster account.** This means you can easily integrate Privy with Farcaster to compose experiences with a user's existing social graph or network. Here's how! Log in with Farcaster enables log in and read access to a user's Farcaster account but does not provide write access to the account today.
How does Farcaster login work? Farcaster identifies users via a **signer**: this is an EdDSA keypair that is used by the client application to sign content like posts ("casts"), follows, etc on behalf of users. These Farcaster signers are managed through various clients such as [Farcaster](https://farcaster.xyz/), [Supercast](https://www.supercast.xyz/) and others. Privy uses a standard called **Sign in with Farcaster** ([FIP-11](https://github.com/farcasterxyz/protocol/discussions/110)) to issue a signature request to a user's Farcaster account via the client a user has. ***
### 1. Enable Farcaster login in your dashboard Go to your app in your [developer dashboard](https://dashboard.privy.io) and navigate to **User management > Authentication > Socials**. From here, enable **Farcaster** as a social option. This will enable you to configure Farcaster as a login and account linking option in your app. ### 2. Configure your app's Farcaster integration The following assumes you have set up Privy with your app. If you haven't, start by following the instructions in the [**Privy Quickstart**](/basics/get-started/dashboard/create-new-app) to get your app set up with Privy. From there, if you'd like users to be able to [**`login`**](/authentication/user-authentication/login-methods/farcaster) to your app with their Farcaster account, you can configure `'farcaster'` as an upfront login method in your **`PrivyProvider`**, like so: ```tsx theme={"system"} ``` You can also prompt existing users to link their Farcaster account to their existing account. ```tsx theme={"system"} import {usePrivy} from '@privy-io/react-auth'; function Page() { const {linkFarcaster} = usePrivy(); // You may replace this hook with any of the other `link-` hooks to // link a different account type. return ; } ``` ### 3. Use your Farcaster link to power custom logic Once a user has logged in with or linked their Farcaster account, you can find their **`Farcaster`** object, including their `fid`, `username`, `pfp` and more, in the [**`user`**](/user-management/users/the-user-object) object returned by the `usePrivy` hook. **That's it! Once you've linked a Farcaster account to a user object, you can use this to power composable experiences in your app.** You should also consider using toolkits like [Farcaster's APIs](https://docs.farcaster.xyz/) or [Neynar](https://neynar.com/) to query and interact with protocol data. ### 4. (Optional) Refresh Farcaster info Sometimes, a user may update their Farcaster profile information (`username`, `pfp`) - while this is publicly available using a public hub endpoint, Privy caches a version whenever the user logs in for convenience. To refresh the Privy cache, you can use our [REST API](/basics/rest-api/setup) (`server-auth` coming soon) and hitting the `/api/v1/users/farcaster/refresh` endpoint. ```tsx theme={"system"} const response = await fetch('https://auth.privy.io/api/v1/users/farcaster/refresh', { method: 'POST', body: JSON.stringify({ fid: 1 }), headers: { Authorization: `Basic ${btoa(`${'your-privy-app-id'}:${'your-privy-app-secret'}`)}`, 'privy-app-id': 'your-privy-app-id', 'content-type': 'application/json' } }); ``` If you're simply looking for the current Privy user object, we recommend using [`getUserByFarcasterId`](/user-management/users/managing-users/querying-users#by-farcaster-fid). To be considerate of public hubs, we only allow refreshing of user data once every 24 hours. If you need more frequent than daily freshness, we recommend that you query public hub data using the user's `fid`. # Building a Farcaster mini app Source: https://docs.privy.io/recipes/farcaster/mini-apps [**Farcaster**](https://www.farcaster.xyz/) is a sufficiently decentralized social network whose core social graph is stored on-chain. Users can choose how content they create is stored and it enables unique, composable experiences by enabling users to link their accounts with a wallet of their choosing. **Privy enables seamless login with your user's Farcaster account within a Mini App.** This means you can easily integrate Privy with Farcaster Mini Apps to compose experiences with a user's existing social graph or network. Here's how! Privy supports [Farcaster auth addresses](https://github.com/farcasterxyz/protocol/discussions/225), including authentication from The Base App. To authenticate a user, pass a Sign-In With Farcaster (SIWF) message signed by an auth address to `loginToMiniApp`. A starter repository for building a Farcaster Mini App with Privy and the Mini Apps SDK. Privy uses a standard called **Sign in with Farcaster** ([FIP-11](https://github.com/farcasterxyz/protocol/discussions/110)) to issue a signature request to a user's Farcaster account via the client the user has. The [Mini Apps spec](https://miniapps.farcaster.xyz/docs/specification) introduces a new `sdk.actions.signIn` action. This will produce the same [FIP-11](https://github.com/farcasterxyz/protocol/discussions/110) conformant signature automatically on the Farcaster mobile app. The `sdk.actions.signIn` action, in combination with the Privy `useLoginToMiniApp` hook, provides a seamless login experience that automatically and securely authenticates a user on Farcaster. ### 1. Enable Farcaster login in your dashboard Go to your app in your [developer dashboard](https://dashboard.privy.io/apps?page=login-methods) and navigate to **User management > Authentication > Socials**. From here, enable **Farcaster** as a social option. This will enable you to configure Farcaster as a login and account linking option in your app. ### 2. Configure your allowed domains and cookies When building a Farcaster Mini App, you must include `https://farcaster.xyz` as an allowed domain. Allowed domains is **required** for iframe-in-iframe which Farcaster uses, even for staging environments. Go to the `Domains` tab of your `Configuration > App settings` page in the developer dashboard and [configure allowed domains](/recipes/react/allowed-domains) for your app. This is the URL that your app is deployed to. To use the embedded wallet, your application must also include `https://farcaster.xyz` as an allowed domain. Including Farcaster as an allowed domain allows the Privy iframe, where the embedded wallet is hosted, to load in the Farcaster browser app. Use an [appClient](/basics/get-started/dashboard/app-clients) to override the default cookie settings. Currently Mini Apps do not support httpOnly cookies. If you have httpOnly cookies enabled for your app, it is recommended to set up an appClient to override the default cookie settings. Learn more about appClients [here](/basics/get-started/dashboard/app-clients#cookies). ### 3. Configure your app's Farcaster integration The following assumes you have set up Privy with your app. If you haven't, start by following the instructions in the [**Privy Quickstart**](/basics/react/quickstart) to get your app set up with Privy. Be sure to configure `'farcaster'` as an upfront login method in your **`PrivyProvider`**, like so: ```tsx theme={"system"} ``` ### 4. Setup seamless Farcaster login with the Mini Apps SDK Use `loginToMiniApp` from `useLoginToMiniApp` for proper Farcaster Mini App authentication (not Farcaster's quick auth). In your app, install the [@farcaster/miniapp-sdk](https://www.npmjs.com/package/@farcaster/miniapp-sdk): ```bash theme={"system"} npm install @farcaster/miniapp-sdk ``` Privy now supports authentication with Farcaster auth addresses, including from the new Base app! To authenticate with an auth address, pass a SIWF message signed by an auth address to `loginToMiniApp` from `useLoginToMiniApp`. This can be done by fetching a signature from an external wallet, the Farcaster Wallet, or in an app with mini app support (such as the Base app) by calling `miniappSdk.actions.signIn`. Automatic embedded wallet creation is currently not supported for Farcaster Mini Apps. You have two options: use the wallet that clients like the Farcaster app and The Base App automatically inject (recommended), or [manually create embedded wallets](/wallets/wallets/create/create-a-wallet) at your chosen onboarding point. ```tsx theme={"system"} import miniappSdk from '@farcaster/miniapp-sdk'; import {usePrivy} from '@privy-io/react-auth'; import {useLoginToMiniApp} from '@privy-io/react-auth/farcaster'; ... const {ready, authenticated} = usePrivy(); const {initLoginToMiniApp, loginToMiniApp} = useLoginToMiniApp(); // Login to Mini App with Privy automatically useEffect(() => { if (ready && !authenticated) { const login = async () => { // Initialize a new login attempt to get a nonce for the Farcaster wallet to sign const { nonce } = await initLoginToMiniApp(); // Request a signature from Farcaster const result = await miniappSdk.actions.signIn({nonce}); // Send the received signature from Farcaster to Privy for authentication // or pass a SIWF message signed by an auth address await loginToMiniApp({ message: result.message, signature: result.signature, }); }; login(); } }, [ready, authenticated]); ``` **The Base App (TBA) Special Requirement:** If your users are accessing your Mini App through The Base App, they must add their The Base App Wallet address as an auth address to their Farcaster account for authentication to work properly. Always check that `ready` and `authenticated` from the `usePrivy` hook are `true` before taking actions! Once a user has logged in with or linked their Farcaster account, you can find their **`Farcaster`** object, including their `fid`, `username`, `pfp` and more, in the [**`user`**](/user-management/users/the-user-object) object returned by the `usePrivy` hook. **That's it! You can now use this to power composable experiences in your new Mini App.** When building out your Mini App, be sure to visit [Farcaster's resources page](https://docs.farcaster.xyz/developers/frames/v2/resources) for help with testing and common issues! # Writing to Farcaster Source: https://docs.privy.io/recipes/farcaster/writes [**Farcaster**](https://www.farcaster.xyz/) is a sufficiently decentralized social network whose core social graph is stored on-chain. Users can choose how content they create is stored and it enables unique, composable experiences by enabling users to link their accounts with a wallet of their choosing. ## Getting started **Privy enables your users to easily log in to and write with their Farcaster account.** This means you can easily integrate Privy with Farcaster to compose experiences with a user's existing social graph or network. Generating a signer to power this is sponsored by the Privy team and is free to you and your users. You can see a demo here: [https://farcaster-demo.vercel.app](https://farcaster-demo.vercel.app). The example repo is at [https://github.com/privy-io/examples/tree/main/examples/privy-next-farcaster](https://github.com/privy-io/examples/tree/main/examples/privy-next-farcaster). Here's how to get started! ### 0. Install the latest version of @privy-io/react-auth In order to use embedded signers, we recommend you install the latest version of our SDK, as the interfaces have changed. ### 1. Login with Farcaster The following assumes you have set up Privy with your app. If you haven't, start by following the instructions in the [**Privy Quickstart**](/basics/react/quickstart) to get your app set up with Privy. You must also enable Farcaster as a sign-in method for your app from the dashboard. Please see the [**Farcaster Integration**](/authentication/user-authentication/login-methods/farcaster) recipe to get set up. ### 2. Create an embedded Farcaster signer This guide requires your Privy app to be configured with **on-device wallet mode**. Embedded Farcaster signers are not compatible with TEE mode. The first step to write to Farcaster is to create an embedded signer.
What is an embedded Farcaster signer? Farcaster data is shared across a network of servers called "hubs". Hubs are responsible for verifying and sharing messages on the protocol. In order to submit messages, you need to create a Farcaster signer. This is an Ed25519 key-pair that is authorized to sign messages on your user's behalf. Privy generates a new non-custodial signer for your user. A user can authorize their embedded Farcaster signer to post on their behalf via Farcaster's signer connect flow. This allows your app to post messages with your user's Farcaster account! ***
In order to do so, a user must go through an authorization flow, which grants their new Farcaster signer permission to submit casts on their behalf. Use the `useFarcasterSigner()` hook to request a new signer from a user's existing Farcaster account. ```tsx theme={"system"} import { useFarcasterSigner, usePrivy } from @privy-io/react-auth; const { user } = usePrivy(); const { requestFarcasterSignerFromWarpcast } = useFarcasterSigner(); const farcasterAccount = user.linkedAccounts.find((account) => account.type === 'farcaster'); ``` You can see if your user already has an embedded Farcaster signer authorized by checking if `user.linkedAccounts.find((account) => account.type === 'farcaster').signerPublicKey` is defined! ### 3. Create an external signer In order to interface with Farcaster libraries, we need to build a simple signer object. This can easily be constructed using the Privy Farcaster signer interfaces. This step requires that you install [@standard-crypto/farcaster-js](https://www.npmjs.com/package/@standard-crypto/farcaster-js). ``` npm install @farcaster/frame-sdk ``` First, define an ExternalEd25519Signer: ```tsx theme={"system"} import {ExternalEd25519Signer} from '@standard-crypto/farcaster-js'; const {getFarcasterSignerPublicKey, signFarcasterMessage} = useFarcasterSigner(); const privySigner = new ExternalEd25519Signer(signFarcasterMessage, getFarcasterSignerPublicKey); ``` ### 4. Build the hub client Now that we have a signer object built, we can build our [@standard-crypto/farcaster-js](https://www.npmjs.com/package/@standard-crypto/farcaster-js) client for interacting with Farcaster! ```tsx theme={"system"} import {HubRestAPIClient} from '@standard-crypto/farcaster-js'; const client = new HubRestAPIClient({ hubUrl: 'https://hub.farcaster.standardcrypto.vc:2281' }); ``` ### 5. Submit a cast Now that you have a client initialized, you can now begin submitting messages to the protocol! Luckily, farcaster-js makes submitting a cast as easy as: ```tsx theme={"system"} const submitCastResponse = await client.submitCast( {text: 'Hello world!'}, user.farcaster.fid, privySigner ); ``` ### 6. Interact with other Farcasters! Alright, your user has created a new cast, but how do they interact with other people? First off, we need our user to follow people to display casts on their feed! Let's go ahead and follow Vitalik. ```tsx theme={"system"} // Vitalik's Farcaster ID (FID) is 5650 const followUserResponse = await client.followUser(5650, user.farcaster.fid, privySigner); ``` Next, let's like and recast some of his casts. ```tsx theme={"system"} // Liking one of Vitalik's recent casts // https://farcaster.xyz/vitalik.eth/0x3e9b3734 const submitLikeResponse = await client.submitReaction( { type: 'like', target: { fid: 5650, hash: '0x3e9b3734a29ad341f1c73912c42343a21d5df75a' } }, user.farcaster.fid, privySigner ); // Recasting another one of Vitalik's recent casts // https://farcaster.xyz/vitalik.eth/0x6be44f32 const submitRecastResponse = await client.submitReaction( { type: 'recast', target: { fid: 5650, hash: '0x6be44f32011a59e239d5a00bb6302c3105ad3214' } }, user.farcaster.fid, privySigner ); ``` Awesome! We've already submitted, liked, and recasted a cast + followed a user. Now, your user wants to shake up their feed, so they are going to unfollow Vitalik. ```tsx theme={"system"} const unfollowUserResponse = await client.unfollowUser(5650, user.farcaster.fid, privySigner); ``` That's it! Your user has now logged in with Farcaster, authorized a new non-custodial signer, and started writing to the protocol. Wowow! ## Caveats Some Privy features cannot be used alongside Farcaster embedded signers. 1. A user must always have an embedded wallet to use Farcaster embedded signers. Before calling `requestFarcasterSignerFromWarpcast` be sure to either [manually or automatically](/wallets/wallets/create/create-a-wallet) create an embedded wallet. 2. **MFA** cannot be enabled when using Farcaster embedded signers. 3. **Passwords** on embedded wallets cannot be added when using Farcaster embedded signers. ## Resources If you're new to Farcaster, here are some great places to get started: * [https://docs.farcaster.xyz/](https://docs.farcaster.xyz/) * [https://www.thehubble.xyz/](https://www.thehubble.xyz/) Interacting with the Farcaster protocol requires reading existing data. The Farcaster team has a set of open source libraries for reading from hubs directly: [https://github.com/farcasterxyz](https://github.com/farcasterxyz). Alternatively, you can use data APIs built for Farcaster developers that drastically improve developer and user UX using [Neynar](https://neynar.com/). ## FAQ
**Q**: *Will my users have to pay Warps?* **A**: We are sponsoring all signers created so that your users will not have to spend any Warps! **Q**: *How do I revoke my embedded Farcaster signer?* **A**: If your account was created on Farcaster, you can go to Settings -> Advanced -> Manage connected apps. Then, delete the key you wish to revoke. Note that this will **delete all messages** posted by that signer. Revoking a signer without Farcaster is a somewhat involved process. Using your custody address (wallet from Farcaster), you can call `remove(mySignerPublicKey)` on the [Farcaster Key Registry](https://optimistic.etherscan.io/address/0x00000000fc1237824fb747abde0ff18990e59b7e) contract to permanently deauthorize an embedded Farcaster signer. [Here is an example](https://optimistic.etherscan.io/tx/0x49f4de7596eced1ac34abd2e78329ba8ff02569156cf4f5919250cca0144e783).
**Q**: *I really like/dislike 'X'. How do I tell someone?* **A**: We would love ANY feedback on your experience so far! Please reach out to us on [Farcaster](https://farcaster.xyz/privy) or [Slack](https://privy.io/slack). # Integrating Flashbots protect Source: https://docs.privy.io/recipes/flashbots-protect [Flashbots Protect](https://docs.flashbots.net/flashbots-protect/overview) is an RPC service that helps protect your users' Ethereum transactions from dangerous frontrunning attacks by submitting transactions to a private mempool that remains hidden from bots. You can also use the service to earn MEV refunds on any MEV realized through backrunning. To integrate Flashbots Protect with Privy, first configure the [Flashbots RPC URL](https://docs.flashbots.net/flashbots-protect/quick-start) for the chain your app needs: ```tsx theme={"system"} import {mainnet} from 'viem/chains'; import {addRpcUrlOverrideToChain} from '@privy-io/react-auth'; // Configure the Flashbots RPC URL for mainnet const mainnetWithFlashbotsProtect = addRpcUrlOverrideToChain( mainnet, 'https://rpc.flashbots.net/fast' ); ``` ```tsx theme={"system"} import {sepolia} from 'viem/chains'; import {addRpcUrlOverrideToChain} from '@privy-io/react-auth'; // Configure the Flashbots RPC URL for sepolia const sepoliaWithFlashbotsProtect = addRpcUrlOverrideToChain( sepolia, 'https://rpc-sepolia.flashbots.net/' ); ``` ```tsx theme={"system"} import {holesky} from 'viem/chains'; import {addRpcUrlOverrideToChain} from '@privy-io/react-auth'; // Configure the Flashbots RPC URL for holesky const holeskyWithFlashbotsProtect = addRpcUrlOverrideToChain( holesky, 'https://rpc-holesky.flashbots.net/' ); ``` Next, pass the chain configured with the Flashbots RPC URL to the `config.supportedChains` property of the `PrivyProvider`: ```tsx theme={"system"} {/* your app's content */} ``` ```tsx theme={"system"} {/* your app's content */} ``` ```tsx theme={"system"} {/* your app's content */} ``` **That's it!** Once you've configured Flashbots Protect as the RPC URL for your desired chain, Privy will route your users' transactions through the private Flashbots mempool. Flashbots Protect currently only supports Ethereum mainnet, the Ethereum Sepolia testnet, and the Ethereum Holesky testnet. The team is actively building support for other networks, including L2s, as well. # Custom gas sponsorship rate limits Source: https://docs.privy.io/recipes/gas-sponsorship-rate-limits Implement spending controls for gas-sponsored transactions Gas sponsorship allows your app to pay for transaction fees on behalf of users. Privy natively offers controls to limit total spend and allows for per-transaction control on whether to sponsor gas, but your app may implement finer controls on spend across wallets, users, or apps. This guide walks through an implementation of wallet-level, user-level, and app-level spending limits for Privy gas-sponsored transactions across a given timeframe. ## Prerequisites * A Privy app with [gas sponsorship enabled](/wallets/gas-and-asset-management/gas/setup) * Basic familiarity with [sending transactions](/wallets/using-wallets/ethereum/send-a-transaction) ## Overview The recipe implements a three-tier spending control system: * **Wallet-level**: Limit spending per individual wallet * **User-level**: Limit spending per user across all their wallets * **App-level**: Limit total spending across all users Privy exposes a `sponsor` parameter in a transaction request to conditionally enable gas sponsorship, which your app can integrate with. Based on whether a given meter is exceeded, the app can conditionally choose to send a gas sponsored transaction or fallback to a non-sponsored submission. This allows the app to create a stopgap in spend based on custom metering. ## Step 1: Configure custom spending limits To start, set up a simple policy configuration based on a daily spending limit. This strategy defines daily spending limits for each tier, resetting these limits every day, e.g. at midnight UTC. ```tsx theme={"system"} const LIMITS = { perWalletDailyCents: 200, // $2 per wallet per day perUserDailyCents: 500, // $5 per user per day perAppDailyCents: 10000 // $100 per app per day }; ``` ## Step 2: Set up spend tracking To actually know how much gas your app has consumed, set up a storage system to track spending. ```tsx theme={"system"} interface SpendTracker { date: string; // YYYY-MM-DD spentCents: number; } const walletSpend = new Map(); const userSpend = new Map(); let appSpend: SpendTracker = {date: '', spentCents: 0}; ``` ## Step 3: Estimate transaction costs Define cost estimates per chain. For production use, implement dynamic cost estimation based on current gas prices. In practice, your app could also adjust the cost multipliers dynamically based on the type of transaction and a given chain. For example, recording a higher multiple of gas spend for a transaction on Solana that is also an SPL token transfer. ```tsx theme={"system"} const CHAIN_COSTS: Record = { 'eip155:1': 100, // Ethereum: ~$1 'eip155:8453': 5, // Base: ~$0.05 'solana:mainnet': 1 // Solana: ~$0.01 }; function estimateCostCents(chainId: string): number { return CHAIN_COSTS[chainId] || 100; // Default $1 } ``` ## Step 4: Implement rate limit checks Check spending against all tiers based on current date before allowing sponsorship. ```tsx theme={"system"} function getToday(): string { return new Date().toISOString().split('T')[0]; } function canSponsor( walletAddress: string, userId: string, costCents: number ): {allowed: boolean; reason?: string} { const today = getToday(); // Level 1: Check wallet limit let walletTracker = walletSpend.get(walletAddress); if (!walletTracker || walletTracker.date !== today) { walletTracker = {date: today, spentCents: 0}; } if (walletTracker.spentCents + costCents > LIMITS.perWalletDailyCents) { return { allowed: false, reason: `Wallet daily limit of $${LIMITS.perWalletDailyCents / 100} reached` }; } // Level 2: Check user limit let userTracker = userSpend.get(userId); if (!userTracker || userTracker.date !== today) { userTracker = {date: today, spentCents: 0}; } if (userTracker.spentCents + costCents > LIMITS.perUserDailyCents) { return { allowed: false, reason: `User daily limit of $${LIMITS.perUserDailyCents / 100} reached` }; } // Level 3: Check app limit if (appSpend.date !== today) { appSpend = {date: today, spentCents: 0}; } if (appSpend.spentCents + costCents > LIMITS.perAppDailyCents) { return { allowed: false, reason: `App daily limit of $${LIMITS.perAppDailyCents / 100} reached` }; } return {allowed: true}; } ``` ## Step 5: Record spending after transactions Update all spending trackers when a sponsored transaction is allowed and is successfully submitted. ```tsx theme={"system"} function recordSpend(walletAddress: string, userId: string, costCents: number): void { const today = getToday(); // Update wallet tracker let walletTracker = walletSpend.get(walletAddress); if (!walletTracker || walletTracker.date !== today) { walletTracker = {date: today, spentCents: 0}; } walletTracker.spentCents += costCents; walletSpend.set(walletAddress, walletTracker); // Update user tracker let userTracker = userSpend.get(userId); if (!userTracker || userTracker.date !== today) { userTracker = {date: today, spentCents: 0}; } userTracker.spentCents += costCents; userSpend.set(userId, userTracker); // Update app tracker if (appSpend.date !== today) { appSpend = {date: today, spentCents: 0}; } appSpend.spentCents += costCents; } ``` ## Step 6: Send transactions with conditional sponsorship Integrate the rate limiting logic with Privy's transaction API. ```tsx theme={"system"} async function sendTransaction({userId, walletAddress, chainId, transaction}) { const estimatedCostCents = estimateCostCents(chainId); const {allowed, reason} = canSponsor(walletAddress, userId, estimatedCostCents); const response = await fetch(`https://api.privy.io/v1/wallets/${walletAddress}/rpc`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'privy-app-id': process.env.PRIVY_APP_ID!, Authorization: `Bearer ${process.env.PRIVY_APP_SECRET}` }, body: JSON.stringify({ method: 'eth_sendTransaction', params: { transaction: {transaction} }, caip2: chainId, sponsor: allowed // Conditionally sponsor based on limits }) }); if (response.ok) { const result = await response.json(); // Record spend if sponsored if (allowed) { recordSpend(walletAddress, userId, estimatedCostCents); } else { console.log(reason); } } } ``` ## Advanced considerations Some additional directions to explore for more advanced custom rate limiting: * Replace in-memory rate limit storage with persistent storage, e.g. Redis * For smoother limits, consider implementing sliding window rate limiting * Implement rate limit counters for number of total transactions sent in addition to transaction dollar volume. For example, a rate limit to allow at most 100 transactions per day * Implement per-transaction limits. For example, reject a transaction if its gas cost estimate prior to submission is over \$1 ## Related resources Learn about Privy's gas sponsorship engine Guide to sending transactions with Privy # Hierarchical deterministic (HD) wallets Source: https://docs.privy.io/recipes/hd-wallets Privy embedded wallets are **hierarchical deterministic (HD)** wallets. An HD wallet allows you to generate multiple addresses and private keys from a shared source of entropy: the wallet seed (or equivalently, a [BIP-39 mnemonic](https://github.com/bitcoin/bips/blob/master/bip-0039.mediawiki) encoding the seed, known as a seed *phrase*). In kind, **Privy can be used to provision multiple embedded wallets for a single user.** Read more below to learn how!
Read more about how HD wallets work. HD wallets use a shared source of entropy to derive the wallet seed; this entropy is protected by the Privy cryptosystem. Each wallet is generated from the seed and a unique path parameter, which has the format: ``` m / purpose' / coin_type' / account' / change / address_index ``` For Privy's embedded wallets, the path used for the `i`-th wallet is: ``` m/44'/60'/0'/0/i for Ethereum m/44'/501'/i/0' for Solana ``` where `i` is 0-indexed. An HD wallet is said to have an index of `i` if it is derived from the `i`-th path above. You can read more about these derivation paths [here](https://help.myetherwallet.com/en/articles/5867305-hd-wallets-and-derivation-paths).
## Creating multiple HD wallets To create multiple HD wallets for a user, use the `createWallet` method: ```tsx theme={"system"} import {useCreateWallet} from '@privy-io/react-auth'; const {createWallet} = useCreateWallet(); ``` ```tsx theme={"system"} import {useCreateWallet} from '@privy-io/react-auth/solana'; const {createWallet} = useCreateWallet(); ``` ### Creating the user's first wallet If this is the first wallet you are creating for the user (e.g. the 0th index), you may call **`createWallet`** with no parameters: ```tsx theme={"system"} // Creating the first wallet for a user await createWallet(); ``` ### Creating additional wallets There are two approaches to creating additional wallets. #### 1. Create an additional wallet with the next available index If the user already has an embedded wallet, and you are creating an additional embedded wallet, call `createWallet` with `createAdditional` set to `true`: If `true`, will allow the user to create a wallet regardless if it is their first wallet or an additional wallet. If `false`, createWallet will succeed *only if the use is creating their first wallet.* Defaults to `false`. Once invoked, **`createWallet`** will return a Promise that resolves to the **`Wallet`** created for the user at the specified index, if it was successful. This method will reject with an error if: * the user is not `authenticated` * the user already has an embedded wallet and `createAdditional` was not set to `true` * if there is another error during wallet creation, such as the user exiting prematurely ```tsx theme={"system"} // Creating additional embedded wallets for the user // You can also create the first wallet for the user using this syntax await createWallet({createAdditional: true}); ``` #### 2. Create an additional wallet with a specified HD wallet index To create a wallet at a specified HD wallet index, call `createWallet` with the preferred `walletIndex`. This method will either create a new wallet, or return the existing one if one already exists at the specified index. The specified HD wallet index. Must be a positive number, and must be `0` for the user's first wallet. A wallet with HD index 0 must be created before creating a wallet at greater HD indices. ```tsx theme={"system"} // Create an additional embedded wallet at index 5 await createWallet({walletIndex: 5}); ``` An error can be thrown if: * the user is not `authenticated` * wallet creation fails or the wallet cannot be added to the user's account. * an invalid HD wallet index is supplied, i.e. `walletIndex` is less than 0, or if `walletIndex` is greater than 0 while user has no wallet with HD index 0. ## Using multiple HD wallets ### Getting a specific embedded wallet Once a user has one or more embedded wallets, the wallets are added to both [`linkedAccounts`](/user-management/users/the-user-object) array of the **`user`** object and the array of connected wallets returned by [`useWallets`](/wallets/wallets/get-a-wallet/get-connected-wallet). To find a specific embedded wallet for the user, search the `useWallets` array for a wallet with `walletClientType: 'privy'` and an `address` that matches your desired address: ```tsx theme={"system"} import {useWallets} from '@privy-io/react-auth'; // Ensure the wallet address is checksummed per EIP55 const desiredAddress = 'insert-your-desired-address-in-EIP55-format'; const {wallets} = useWallets(); const desiredWallet = wallets.find( (wallet) => wallet.walletClientType === 'privy' && wallet.address === desiredAddress ); ``` ```tsx theme={"system"} import {useWallets} from '@privy-io/react-auth/solana'; const desiredAddress = 'insert-your-desired-address'; const {wallets} = useWallets(); const desiredWallet = wallets.find( (wallet) => wallet.walletClientType === 'privy' && wallet.address === desiredAddress ); ``` You can also get a list of all of the user's embedded wallets by filtering the `useWallets` array for entries with `walletClientType: 'privy'`: ```tsx theme={"system"} const embeddedWallets = wallets.filter((wallet) => wallet.walletClientType === 'privy'); ``` ### Requesting signatures and transactions Your app can then use Privy's native signature and transaction methods, the wallet's EIP1193 provider, or a third-party library like `viem` or `ethers`, per the instructions below. #### Using Privy's native signature and transaction methods To use Privy's native `signMessage`, `signTypedData`, and `sendTransaction` methods with a specific embedded wallet, simply pass the address for your desired wallet as the final optional parameter to these methods: #### `signMessage` ```tsx theme={"system"} const {signMessage} = usePrivy(); const signature = await signMessage( {message: 'insert-message-to-sign'}, { uiOptions: insertOptionalUIConfigOrUndefined, address: desiredWallet.address // Replace with the address of the desired embedded wallet } ); ``` #### `signTypedData` ```tsx theme={"system"} const {signTypedData} = usePrivy(); const signature = await signTypedData(insertTypedDataObject, { uiOptions: insertOptionalUIConfigOrUndefined, address: desiredWallet.address // Replace with the address of the desired embedded wallet }); ``` #### `sendTransaction` ```tsx theme={"system"} const {sendTransaction} = usePrivy(); const signature = await sendTransaction(insertTransactionRequest, { uiOptions: insertOptionalUIConfigOrUndefined, fundingConfig: insertOptionalUIConfigOrUndefined, address: desiredWallet.address // Replace with the address of the desired embedded wallet }); ``` #### `signMessage` ```tsx theme={"system"} import {useSignMessage} from '@privy-io/react-auth/solana'; const {signMessage} = useSignMessage(); const signature = await signMessage( {message: new TextEncoder().encode('insert-message-to-sign')}, { uiOptions: insertOptionalUIConfigOrUndefined, address: desiredWallet.address // Replace with the address of the desired embedded wallet } ); ``` #### `sendTransaction` ```tsx theme={"system"} import {useSendTransaction} from '@privy-io/react-auth/solana'; const {sendTransaction} = useSendTransaction(); const signature = await sendTransaction({ transaction, uiOptions: insertOptionalUIConfigOrUndefined, fundWalletConfig: insertOptionalUIConfigOrUndefined, address: desiredWallet.address // Replace with the address of the desired embedded wallet }); ``` #### Using the EIP1193 provider, viem, and ethers (EVM only) You can also request signatures and transactions from a specific embedded wallet using the wallet's [EIP1193 provider](/wallets/using-wallets/ethereum/web3-integrations) or a library like `viem` or `ethers`. To get the EIP1193 provider for a specific embedded wallet, first find the corresponding `ConnectedWallet` object from the `useWallets` array: ```tsx theme={"system"} // Ensure the wallet address is checksummed per EIP55 const address = 'insert-your-desired-address-in-EIP55-format'; const {wallets} = useWallets(); const wallet = wallets.find( (wallet) => wallet.walletClientType === 'privy' && wallet.address === address ); ``` Then, call the object's `getEthereumProvider` method to get an EIP1193 provider for that wallet: ```tsx theme={"system"} const provider = await wallet.getEthereumProvider(); ``` You can then easily pass that EIP1193 provider to a library like [`viem`](/wallets/using-wallets/ethereum/web3-integrations#viem) or [`ethers`](/wallets/using-wallets/ethereum/web3-integrations#ethers) to use those libraries' interfaces to send requests to the wallet. ## Exporting HD wallets To export the private key for a specific HD wallet, simply pass the address of the wallet you'd like to export as an `address` parameter to the `exportWallet` method: ```tsx theme={"system"} const {exportWallet} = usePrivy(); await exportWallet({address: 'insert-your-desired-address'}); ``` ```tsx theme={"system"} import {useExportWallet} from '@privy-io/react-auth/solana'; const {exportWallet} = useExportWallet(); await exportWallet({address: 'insert-your-desired-address'}); ``` If no `address` is passed to `exportWallet`, Privy will default to exporting the non-imported wallet at `walletIndex: 0`. ## Pregenerating multiple HD wallets (EVM only) Privy supports pregenerating multiple HD wallets in Ethereum when creating new users. With our user import endpoint, you can create a user with up to 10 pregenerated HD wallets. Simply call the import endpoint with `create_n_ethereum_wallets` set to the number of embedded wallets you want to generate for your user. Pregeneration endpoints have heavier rate limit of 240 users per minute. If you are being rate limited, responses will have status code 429. We suggest you setup exponential back-offs starting at 1 second to seamlessly recover. Below is a sample cURL command for pregenerating two new wallets for a user with Privy: ```bash theme={"system"} $ curl --request POST https://auth.privy.io/api/v1/users \ -u ":" \ -H "privy-app-id: " \ -H 'Content-Type: application/json' \ -d '{ "create_n_ethereum_wallets": 2, "linked_accounts": [ { "address": "batman@privy.io", "type": "email" } ] }' ``` A successful response will include the new user object along with their Privy user ID and embedded wallet addresses, like below. The generated wallets will be available to the user upon sign in. Below is a sample successful response for generating two new wallets for a user with Privy: ```json theme={"system"} { "id": "did:privy:clddy332f002tyqpq3b3lv327", "created_at": 1674788927, "linked_accounts": [ { "address": "batman@privy.io", "type": "email" }, { "address": "0x3DAF84b3f09A0E2092302F7560888dBc0952b7B7", "type": "wallet", "wallet_index": 0, "walletClient": "privy", "chain_type": "ethereum" }, { "address": "0x1a235d54C58d0B5E339c784Fd98d4D71125fEb1c", "type": "wallet", "wallet_index": 1, "walletClient": "privy", "chain_type": "ethereum" } ] } ``` ## Creating multiple HD wallets To create multiple Ethereum wallets for a user, use the `create` method from the `useEmbeddedEthereumWallet` hook: ```tsx theme={"system"} import {useEmbeddedEthereumWallet} from '@privy-io/expo'; const {create} = useEmbeddedEthereumWallet(); ``` As a parameter to `create`, pass an object containing a `createAdditional` boolean specifying if you would like to create an additional wallet, even if the user has an existing one. If `true`, will allow the user to create a Ethereum wallet regardless if it is their first wallet or an additional wallet. If `false`, createWallet will succeed *only if the use is creating their first wallet.* Defaults to `false`. Once invoked, **`create`** will return a Promise that resolves to the newly created [wallet](/wallets/wallets/get-a-wallet/get-connected-wallet), if it was successful. This method will reject with an error if: * the user is not `authenticated`. * the user already has an embedded Ethereum wallet and `createAdditional` was not set to `true`. * if there is another error during wallet creation, such as the user exiting prematurely. ```tsx theme={"system"} // Creating additional embedded wallets for the user // You can also create the first wallet for the user using this syntax await create({createAdditional: true}); ``` ## Using multiple HD wallets Once a user has one or more embedded wallets, the wallets are added to the `wallets` array returned by `useEmbeddedEthereumWallet`: ```tsx theme={"system"} import {useEmbeddedEthereumWallet} from '@privy-io/expo'; ... const {wallets} = useEmbeddedEthereumWallet(); ``` Refer to the [requests section](/wallets/wallets/get-a-wallet/get-connected-wallet) to learn how to interact with the wallets you create. ### Getting a specific embedded wallet To find a specific embedded wallet for the user, search the `wallets` array for a wallet with the `address` that matches your desired address: ```tsx theme={"system"} const desiredAddress = 'insert-your-desired-wallet-address'; const {wallets} = useEmbeddedEthereumWallet(); const desiredWallet = wallets.find((wallet) => wallet.address === desiredAddress); ``` You can alternatively search the wallets array by your desired HD index: ```tsx theme={"system"} // Replace this with your desired HD index const desiredHdIndex = 0; const {wallets} = useEmbeddedEthereumWallet(); const desiredWallet = wallets.find((wallet) => wallet.walletIndex === desiredHdIndex); ``` ## Creating multiple HD wallets To create multiple Solana wallets for a user, use the `create` method from the `useEmbeddedSolanaWallet` hook: ```tsx theme={"system"} import {useEmbeddedSolanaWallet} from '@privy-io/expo'; ... const {create} = useEmbeddedSolanaWallet(); ``` As an optional parameter to `create`, you may pass an object containing the following fields: If `true`, will allow the user to create a Solana wallet regardless if it is their first wallet or an additional wallet. If `false`, createWallet will succeed *only if the use is creating their first wallet.* Defaults to `false`. Once invoked, **`create`** will return a Promise that resolves to the [provider](/wallets/wallets/get-a-wallet/get-connected-wallet) for the wallet created for the user, if it was successful. This method will reject with an error if: * the user is not `authenticated` * the user already has an embedded Solana wallet and `createAdditional` was not set to `true` * if there is another error during wallet creation, such as the user exiting prematurely ### Creating the user's first wallet If this is the first wallet you are creating for the user (e.g. the 0th index), you may call **`create`** with no parameters: ```tsx theme={"system"} // Creating the first wallet for a user const provider = await create(); ``` ### Creating additional wallets If the user already has an embedded wallet, and you are creating an additional embedded wallet, **you must call `create` with `createAdditional` set to `true`**: ```tsx theme={"system"} // Creating additional embedded wallets for the user // You can also create the first wallet for the user using this syntax const provider = await create({createAdditional: true}); ``` ## Using multiple HD wallets Once a user has one or more embedded wallets, the wallets are added to the `wallets` array returned by `useEmbeddedSolanaWallet`: ```tsx theme={"system"} import {useEmbeddedSolanaWallet} from '@privy-io/expo'; ... const {wallets} = useEmbeddedSolanaWallet(); ``` Each entry in the `wallets` array is an object with the following fields: The address (base58-encoded public key) for the wallet. The address (base58-encoded public key) for the wallet. The HD index for the wallet. Method to get a [provider](/wallets/wallets/get-a-wallet/get-connected-wallet) for the wallet for requesting signatures and transactions. ### Getting a specific embedded wallet To find a specific embedded wallet for the user, search the `wallets` array for a wallet with the `address` that matches your desired address: ```tsx theme={"system"} const desiredAddress = 'insert-your-desired-wallet-address'; const {wallets} = useEmbeddedSolanaWallet(); const desiredWallet = wallets.find((wallet) => wallet.address === desiredAddress); ``` You can alternatively search the wallets array by your desired HD index: ```tsx theme={"system"} // Replace this with your desired HD index const desiredHdIndex = 0; const {wallets} = useEmbeddedSolanaWallet(); const desiredWallet = wallets.find((wallet) => wallet.walletIndex === desiredHdIndex); ``` ### Requesting signatures and transactions To request a signature or transaction from a specific embedded wallet, first find the corresponding wallet object from the `wallets` array: ```tsx theme={"system"} const desiredAddress = 'insert-your-desired-wallet-address'; const {wallets} = useEmbeddedSolanaWallet(); const wallet = wallets.find((wallet) => wallet.address === desiredAddress); ``` Then, call the object's `getProvider` method to get a [provider](/wallets/wallets/get-a-wallet/get-connected-wallet) for the wallet: ```tsx theme={"system"} const provider = await wallet.getProvider(); ``` You can then easily request signatures from the [`provider`](/wallets/wallets/get-a-wallet/get-connected-wallet) using its [`request`](/wallets/using-wallets/solana/sign-a-message) method, like so: ```tsx theme={"system"} const message = 'Hello world'; const {signature} = await provider.request({ method: 'signMessage', params: { message: message } }); ``` # Quickstart Source: https://docs.privy.io/recipes/hyperliquid-guide [Hyperliquid](https://hyperliquid.xyz/) is a high-performance blockchain designed specifically for decentralized derivatives trading. It offers incredibly fast transaction processing, low fees, and a fully onchain open financial system. This guide demonstrates how to programmatically access Hyperliquid through Privy's SDKs, covering essential operations including agent wallet creation, trade execution, subaccount management, and builder code integration for revenue sharing ## Prerequisites Before you begin, make sure you have: * Created a [Privy app](https://dashboard.privy.io) and [enabled gas sponsorship for Abritrum](/wallets/gas-and-asset-management/gas/setup) * Node.js 16+ installed * Basic familiarity with TypeScript/JavaScript ## Installation Install the required dependencies: ```bash theme={"system"} npm install @nktkas/hyperliquid @privy-io/node viem ``` ## Quickstart Here's a minimal example to get you started: ```javascript theme={"system"} import { PrivyClient } from '@privy-io/node'; import { createViemAccount } from '@privy-io/node/viem'; import * as hl from '@nktkas/hyperliquid'; // Initialize Privy client const privy = new PrivyClient({ appId: 'insert-your-app-id', appSecret: 'insert-your-app-secret', }); // Create a wallet const wallet = await privy.wallets().createWallet({ chain_type: 'ethereum', }); // Create a viem account const account = createViemAccount(privy, { walletId: wallet.id, address: wallet.address as `0x${string}`, }); // Initialize Hyperliquid client const transport = new hl.HttpTransport({ isTestnet: true, }); const client = new hl.ExchangeClient({ transport, wallet: account, }); ``` ## Placing Your First Order Once you have your client set up, you can place a limit order: ```javascript theme={"system"} // Place a limit buy order for BTC const order = await client.order({ orders: [ { a: 0, // Asset index (0 = BTC) b: true, // Buy side (true = buy, false = sell) p: '95000', // Limit price in USD s: '0.001', // Size (0.001 BTC) r: false, // Reduce-only (false = can open new position) t: {limit: {tif: 'Gtc'}} // Time in force: Good-til-canceled } ], grouping: 'na' // Order grouping (usually "na") }); console.log('Order placed:', order); ``` This example places a buy order for 0.001 BTC at \$95,000. The order will remain active until it's filled or you cancel it. Learn more about different order types and trading patterns in the [Trading Patterns guide](/recipes/hyperliquid/trading-patterns). ## Funding ### Deposit to HyperCore To start trading on Hyperliquid, you need to deposit funds to HyperCore. A minimum of **\$5 USDC on Arbitrum** is required to deposit. Funds will be credited to the address that makes the deposit. ```javascript theme={"system"} import {encodeFunctionData, parseUnits, erc20Abi} from 'viem'; const HYPERLIQUID_BRIDGE_ADDRESS = '0x2Df1c51E09aECF9cacB7bc98cB1742757f163dF7'; const ARBITRUM_USDC_ADDRESS = '0xaf88d065e77c8cC2239327C5EDb3A432268e5831'; // Deposit USDC from Arbitrum to HyperCore const transaction = await privy .wallets() .ethereum() .sendTransaction(wallet.id, { sponsor: true, caip2: 'eip155:42161', params: { transaction: { to: ARBITRUM_USDC_ADDRESS, data: encodeFunctionData({ abi: erc20Abi, functionName: 'transfer', args: [HYPERLIQUID_BRIDGE_ADDRESS, parseUnits('5', 6)] }) } } }); console.log(transaction); ``` Learn more about the Hyperliquid bridge in the [official documentation](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/bridge2). ### Withdraw from HyperCore To withdraw funds from Hyperliquid back to your wallet, use the `withdraw3` method. Funds will be credited to the `destination` address on Arbitrum as USDC. ```javascript theme={"system"} // Withdraw USDC from HyperCore to your wallet const withdraw = await client.withdraw3({ destination: wallet.address, amount: '5' }); console.log(withdraw); ``` Withdrawals are **User Signed Actions** and must be signed by the master wallet. Agent wallets cannot initiate withdrawals directly. ### Faucet (Testnet Only) To use the testnet faucet, your master account must first be activated on mainnet. Send at least **\$5 USDC on Arbitrum** to the bridge address from the master account to activate it. Once activated, you can claim testnet funds by making an API request: ```javascript Fetch theme={"system"} const response = await fetch('https://api.hyperliquid-testnet.xyz/info', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ type: 'claimDrip', user: wallet.address }) }); const result = await response.json(); console.log(result); ``` ```bash Curl theme={"system"} curl 'https://api.hyperliquid-testnet.xyz/info' \ -H 'Content-Type: application/json' \ --data-raw '{"type":"claimDrip","user":"YOUR_WALLET_ADDRESS"}' ``` The faucet provides **\$1000 USDC** for testnet trading. You can only claim from the faucet **once per address**. ### View Wallet Activity You can track all deposits, withdrawals, and trading activity by viewing your wallet on the [Hyperliquid explorer](https://app.hyperliquid.xyz/explorer): The explorer shows real-time transaction history, trading positions, and account balances for any Hyperliquid address. ## Next Steps Explore these guides to learn more about building with Hyperliquid and Privy: Learn how to set up API wallets for secure, programmatic trading operations. Discover common trading patterns and best practices for placing orders, managing positions, and more. Build React apps with external wallets like MetaMask and agent wallets. Implement secure trading policies and execute offline actions for advanced risk management. Develop smart contracts on Hyperliquid's EVM-compatible blockchain. ## Resources Official documentation explaining Hyperliquid's architecture, trading features, and API endpoints. Overview of Hyperliquid's EVM chain, including architecture and features. Learn how to send EVM transactions using Privy wallets. ## Why Use Privy with Hyperliquid? * **Security**: Private keys never leave Privy's secure infrastructure * **Simplicity**: No need to manage key storage or rotation * **Compatibility**: Full compatibility with Hyperliquid's SDK and API * **Flexibility**: Easily create and manage multiple wallets for different strategies You're ready to start building secure trading applications on Hyperliquid! # Agent wallets Source: https://docs.privy.io/recipes/hyperliquid/agents-and-subaccounts Learn how to use agent wallets (API wallets) to build secure, scalable trading systems on Hyperliquid with Privy. Before continuing with this guide, make sure you have initialized your Hyperliquid client as shown in the [Getting Started guide](/recipes/hyperliquid-guide). This guide assumes you have `client` and other basic setup completed. ## Overview [Agent wallets](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/nonces-and-api-wallets#api-wallets) (also known as **API wallets**) are permissioned signers that do not hold funds but can execute Hyperliquid actions for a master account and its subaccounts. This lets you: * Keep user keys isolated while trading programmatically * Give each bot or trading strategy its own nonce space * Simplify concurrent trading operations * Easily rotate keys without affecting trading * Preserve consolidated fee tiers and account-level PnL ## Creating an Agent Wallet ```javascript theme={"system"} import { PrivyClient } from '@privy-io/node'; import { createViemAccount } from '@privy-io/node/viem'; import * as hl from '@nktkas/hyperliquid'; // Initialize Privy client const privy = new PrivyClient({ appId: 'insert-your-app-id', appSecret: 'insert-your-app-secret', }); // Create your master trading wallet const masterWallet = await privy.wallets().createWallet({ chain_type: 'ethereum', }); const masterAccount = createViemAccount(privy, { walletId: masterWallet.id, address: masterWallet.address as `0x${string}`, }); // Initialize Hyperliquid client with master account const transport = new hl.HttpTransport({ isTestnet: true, }); const masterClient = new hl.ExchangeClient({ transport, wallet: masterAccount, }); // Create a new agent wallet const agentWallet = await privy.wallets().createWallet({ chain_type: 'ethereum', }); // Register the agent wallet with the master account await masterClient.registerAgent({ agentAddress: agentWallet.address as `0x${string}`, agentName: "Trading Bot 1", }); ``` ## Using an Agent Wallet Once registered, the agent wallet can execute trades on behalf of the master account: ```javascript theme={"system"} // Create a viem account for the agent const agentAccount = createViemAccount(privy, { walletId: agentWallet.id, address: agentWallet.address as `0x${string}`, }); // Create an exchange client using the agent wallet const agentClient = new hl.ExchangeClient({ transport, wallet: agentAccount, }); // The agent can now trade on behalf of the master account const orderResponse = await agentClient.order({ orders: [ { a: 0, // BTC b: true, // Buy s: "0.01", // Size r: false, // Not reduce-only p: "50000", // Price t: { limit: { tif: "Gtc" } }, }, ], grouping: "na", }); ``` ## Setting Agent Expiration You can set an expiration timestamp for agent wallets using the agent name: ```javascript theme={"system"} // Create an agent that expires in 24 hours const expirationTimestamp = Date.now() + 24 * 60 * 60 * 1000; await masterClient.registerAgent({ agentName: `Trading Bot valid_until ${expirationTimestamp}`, agentAddress: agentWallet.address as `0x${string}`, }); ``` ## Listing Agent Wallets Retrieve all registered agents for a master account: ```javascript theme={"system"} const infoClient = new hl.InfoClient({ transport }); const agents = await infoClient.extraAgents({ user: masterWallet.address as `0x${string}`, }); console.log("Registered agents:", agents); ``` ## Best Practices Create separate agent wallets for each trading strategy or bot. This isolates nonce management and makes it easier to track which system placed which orders. For automated trading systems, set reasonable expiration times on agent wallets to limit exposure if a key is compromised. Regularly audit which agent wallets are registered and revoke access for agents that are no longer needed. ## Next Steps Learn how to use subaccounts for risk isolation Learn common trading patterns and order types # Builder codes Source: https://docs.privy.io/recipes/hyperliquid/builder-codes Learn how to earn fees with builder codes using Privy's wallet infrastructure. Before continuing with this guide, make sure you have initialized your Hyperliquid client as shown in the [Getting Started guide](/recipes/hyperliquid-guide). This guide assumes you have `client` and other basic setup completed. ## Overview [Builder codes](https://hyperliquid.gitbook.io/hyperliquid-docs/trading/builder-codes) are an on-chain, per-order attribution mechanism that lets apps and interfaces earn fees on orders they route for users. Builder codes enable **order-level revenue sharing** - completely independent of which market the order is on: * Apps earn a small fee on every order fill they route through their interface * Works on **all markets** across Hyperliquid * Users must approve a max builder fee for each builder they want to use * Builder codes can override referral codes on specific orders ## How Builder Codes Work 1. **User Approval**: User approves your builder code and sets a max builder fee they're willing to pay 2. **Order Attribution**: When placing orders through your app, include your builder code in the order 3. **Fee Collection**: You earn a small fee on each filled order 4. **On-Chain Tracking**: Revenue is tracked on-chain and paid out automatically ## Obtaining a Builder Code To get started with builder codes, refer to the [builder codes documentation](https://hyperliquid.gitbook.io/hyperliquid-docs/trading/builder-codes) for registration details. ## Approving Builder Fees Before users can use your builder code, they must approve your builder address and set a max fee rate (one-time per user per builder): ```javascript theme={"system"} import * as hl from '@nktkas/hyperliquid'; import {createViemAccount} from '@privy-io/node/viem'; // Create exchange client with user's wallet const client = new hl.ExchangeClient({ transport, wallet: userAccount // User's viem account from Privy }); // User approves your builder and sets max fee await client.approveBuilderFee({ builder: '0xBuilderAddress', // Your builder address maxFeeRate: '0.05%' // Max fee rate user approves // Perps: max 0.10% (10 bps) // Spot: max 1.00% (100 bps) }); ``` The `maxFeeRate` is the maximum fee the user is willing to pay. Actual fees charged can be lower, but never higher than this approved rate. ## Applying Builder Codes to Orders Once approved, include your builder code when placing orders to earn fees: ```javascript theme={"system"} // Place an order with builder code attribution await client.order({ orders: [ { a: 0, // BTC index b: true, // Buy s: '0.001', // Size p: '100000', // Price r: false, // Not reduce-only t: {limit: {tif: 'Gtc'}} } ], builder: { b: '0xBuilderAddress', // Your builder address f: 50 // Fee in tenths of a bp (50 = 5 bps = 0.05%) } }); ``` **Builder fee format:** * `f` is specified in **tenths of a basis point** * Example: `f: 50` means 50 tenths of a bp = 5 bps = 0.05% * Must be ≤ the user's approved `maxFeeRate` ## Checking Builder Fee Approval Verify a user's builder fee approval before placing orders: ```javascript theme={"system"} const infoClient = new hl.InfoClient({transport}); // Check if user has approved your builder const approval = await infoClient.maxBuilderFee({ user: userWallet.address as `0x${string}`, builder: '0xBuilderAddress' }); console.log('Max approved fee:', approval.maxBuilderFee); // If maxBuilderFee is "0" or undefined, user hasn't approved yet if (!approval.maxBuilderFee || approval.maxBuilderFee === '0') { console.log('User needs to approve builder fee first'); } ``` ## Best Practices Track the fee revenue from your builder code to understand user engagement and optimize your application. Balance between earning revenue and providing value to users. Lower fees may attract more users. Always test your builder code integration on testnet before going live. ## Resources Official guide to order-level revenue sharing ## Next Steps Learn common trading patterns # Client-side SDKs Source: https://docs.privy.io/recipes/hyperliquid/client-side-usage Learn how to integrate Hyperliquid with client-side applications using React and external wallets like MetaMask. ## Overview When building client-side applications with Hyperliquid, you'll typically want to: 1. Allow users to connect with external wallets (MetaMask, Coinbase Wallet, etc.) 2. Create an embedded agent wallet for programmatic trading 3. Execute L1 actions (trading) through the agent wallet 4. Execute User Signed Actions (withdrawals, approvals) through the user's external wallet This pattern provides the best user experience - users maintain control through their external wallet while trading operations happen seamlessly through the agent wallet. ## Using External Wallets with Agent Wallets If you want to execute L1 actions (like placing orders, canceling orders, etc.) with external wallets like MetaMask, create an [Agent Wallet](/recipes/hyperliquid/agents-and-subaccounts) and execute all L1 actions through it. Agent wallets are designed specifically for programmatic trading and provide a better user experience - users won't see confusing chain switching prompts, and it creates a cleaner separation between user wallets and trading operations. ```javascript theme={"system"} import { usePrivy, useWallets, toViemAccount } from "@privy-io/react-auth"; import { useCallback } from "react"; import * as hl from "@nktkas/hyperliquid"; export function useInitializeAgent() { const { user, ready } = usePrivy(); const { wallets, ready: walletsReady } = useWallets(); const initializeAgent = useCallback(async () => { if (!ready || !walletsReady) { return null; } const externalWallet = wallets.find( (w) => ( w.walletClientType != "privy" && w.address === user?.wallet?.address ) ); if (!externalWallet) { throw new Error("External wallet not found"); } const embeddedWallet = wallets.find( (w) => w.walletClientType == "privy" ); if (!embeddedWallet) { throw new Error("Embedded wallet not found"); } const externalViemAccount = await toViemAccount({ wallet: externalWallet }); const embeddedViemAccount = await toViemAccount({ wallet: embeddedWallet }); const transport = new hl.HttpTransport(); const client = new hl.ExchangeClient({ wallet: externalViemAccount, transport, }); await client.approveAgent({ agentAddress: embeddedWallet.address as `0x${string}`, agentName: "Privy Agent", }); const agentClient = new hl.ExchangeClient({ wallet: embeddedViemAccount, transport, }); return agentClient; }, [user, wallets, ready, walletsReady]); return { initializeAgent }; } ``` **User Signed Actions** (like withdrawals and agent approvals) use the standard Ethereum mainnet (chain 1) and work normally with external wallets. ## Understanding Action Types ### L1 Actions (Trading Operations) These actions can be executed by agent wallets without requiring the master wallet's signature: * Placing orders * Canceling orders * Modifying leverage * Setting position sizes **Best Practice**: Execute these through an agent wallet for a seamless user experience. ### User Signed Actions These sensitive operations require the master wallet's signature: * Withdrawing funds * Approving agents * Account transfers * Approving builder fees **Best Practice**: Execute these through the user's connected external wallet. ## Best Practices Create and approve the agent wallet the first time a user wants to trade. Store the agent wallet reference for future sessions. Implement proper error handling for when users disconnect their external wallet. The agent wallet can continue operating, but User Signed Actions will fail. Communicate to users which wallet is being used for each action. For example, "Placing order with trading wallet" vs "Approve with MetaMask". ## Next Steps Learn more about agent wallets Explore trading patterns and order types # HyperEVM Source: https://docs.privy.io/recipes/hyperliquid/hyperevm Learn how to build on HyperEVM using Privy's wallet infrastructure. Before continuing with this guide, make sure you have set up your Privy client as shown in the [Getting Started guide](/recipes/hyperliquid-guide). ## Overview [HyperEVM](https://hyperliquid.gitbook.io/hyperliquid-docs/hyperevm) is Hyperliquid's EVM-compatible blockchain that enables: * Deploying smart contracts with Solidity * Building DeFi applications * Creating custom trading logic * Integrating with existing EVM tooling ## Setting Up HyperEVM Configure your application to work with HyperEVM: ```javascript NodeJS theme={"system"} import {PrivyClient} from '@privy-io/node'; import {hyperevmTestnet} from 'viem/chains'; const privy = new PrivyClient({ appId: 'insert-your-app-id', appSecret: 'insert-your-app-secret' }); // Send a transaction on HyperEVM testnet const {hash, caip2} = await privy .wallets() .ethereum() .sendTransaction('insert-wallet-id', { caip2: `eip155:${hyperevmTestnet.id}`, params: { transaction: { to: '0xE3070d3e4309afA3bC9a6b057685743CF42da77C', value: '0x2386F26FC10000', chain_id: hyperevmTestnet.id } } }); ``` ```javascript React theme={"system"} import {PrivyProvider} from '@privy-io/react-auth'; import {hyperliquidEvmTestnet} from 'viem/chains'; function App() { return ( {/* Your app components */} ); } ``` ## Gas Sponsorship ### Smart Wallets on HyperEVM HyperEVM supports [ERC-4337](https://eips.ethereum.org/EIPS/eip-4337) Smart Wallet accounts, allowing you to pay for your users' transaction fees using gas sponsorship. HyperEVM is compatible with popular smart wallet providers including Alchemy, ZeroDev, and Biconomy. You can use Privy's smart wallet infrastructure with any of these providers. ### Setting Up a Paymaster and Bundler Follow these steps to configure gas sponsorship for HyperEVM: #### 1. Navigate to Smart Wallets Settings In your [Privy Dashboard](https://dashboard.privy.io), go to **Wallet Infrastructure** → **Smart Wallets**. #### 2. Choose a Smart Wallet Provider Select one of the supported providers: * **ZeroDev** * **Alchemy** * **Biconomy** #### 3. Add HyperEVM as a Custom Chain Click **Add a new Chain** → **Custom Chain** and configure HyperEVM: Configure HyperEVM custom chain * **Name**: HyperEVM Testnet * **ID number**: 998 * **RPC URL**: Your HyperEVM RPC endpoint * **Bundler URL**: Your provider's bundler URL (e.g., `https://rpc.zerodev.app/api/v3/ZERO_DEV_API_KEY/chain/998`) * **Paymaster URL**: Your provider's paymaster URL (e.g., `https://rpc.zerodev.app/api/v3/ZERO_DEV_API_KEY/chain/998`) Make sure the smart wallet contract is deployed on HyperEVM for it to save and function correctly. #### 4. Save and Test After configuring the chain, click **Save and close**. Your app can now sponsor gas fees for users on HyperEVM using smart wallets. ## Resources Official HyperEVM smart contract documentation Learn more about using smart wallets, including how to send transactions, batch operations, and configure advanced features. ## Next Steps Return to the getting started guide Learn common trading patterns # Policies Source: https://docs.privy.io/recipes/hyperliquid/policies-and-offline-actions Learn how to implement secure trading policies using Privy's signers and policies to control Hyperliquid operations with multi-signature authorization. ## Overview Privy's [signers](/wallets/using-wallets/signers/overview) enable you to add additional signers to wallets, allowing you to. Combined with [policies](/controls/policies/overview), you can define granular controls over which Hyperliquid actions are allowed or denied. ## High-Level Steps 1. **Create policies** - Define which Hyperliquid actions are allowed or denied for your authorization keys 2. **Create authorization keys** - Generate signers that will be used to sign transactions on behalf of wallets 3. **Update wallet** - Attach the authorization key as a signer with your policies applied Once configured, your authorization keys can execute Hyperliquid operations within the boundaries defined by your policies. ## How It Works Policies enforce security controls on wallet operations by evaluating each transaction against a set of conditions. When a transaction is attempted with an authorization key: 1. The transaction is analyzed against all policies attached to that signer 2. If any DENY policy matches, the transaction is rejected 3. If an ALLOW policy matches and no DENY policies match, the transaction proceeds 4. Operations that don't match any policies follow the default behavior ## User Signed Actions vs L1 Actions Hyperliquid operations are divided into two categories: ### User Signed Actions These are sensitive operations that require the master account's signature. Policies can be applied to control any User Signed Action, including: * **Withdrawals** - Transferring funds out of Hyperliquid to external addresses * **Approving Agents** - Registering or managing agent wallets * **Account Transfers** - Moving funds between master account and subaccounts using `sendAsset` * **Approving Builder Fees** - Authorizing builder code fee arrangements These actions require explicit authorization and are ideal candidates for policy controls to protect user funds and account security. ### L1 Actions Other operations are L1 Actions that can be performed by any registered agent wallet without requiring master account approval. These include: * Placing orders * Canceling orders * Modifying orders * Setting leverage * Other trading operations This separation allows you to enable automated trading through agent wallets while maintaining strict control over sensitive account operations. ## Prerequisites Before implementing policies for Hyperliquid operations, you'll need to set up signers and create policies: Learn how to create additional signers for your wallets Learn how to define and create policies for wallet operations ## Creating Policies Policies determine which Hyperliquid operations are allowed or denied. Here are common policy examples for controlling User Signed Actions: ### DENY Withdrawal Attempts Only the master account for a Hyperliquid account can initiate withdrawal attempts. You can setup policies to DENY the additional signer on the master account from being able to withdraw funds without user consent. This is critical for protecting user funds - even if an authorization key is compromised, withdrawals cannot be executed without explicit user approval. ```json theme={"system"} { "name": "DENY Withdrawal from account", "method": "eth_signTypedData_v4", "action": "DENY", "conditions": [ { "field_source": "ethereum_typed_data_message", "field": "hyperliquidChain", "typed_data": { "types": { "EIP712Domain": [ { "name": "name", "type": "string" }, { "name": "version", "type": "string" }, { "name": "chainId", "type": "uint256" }, { "name": "verifyingContract", "type": "address" } ], "HyperliquidTransaction:Withdraw": [ { "name": "hyperliquidChain", "type": "string" }, { "name": "destination", "type": "string" }, { "name": "amount", "type": "string" }, { "name": "time", "type": "uint64" } ] }, "primary_type": "HyperliquidTransaction:Withdraw" }, "operator": "in", "value": ["Testnet", "Mainnet"] } ] } ``` ### DENY Account Transfers The master account can transfer funds between subaccounts using the `sendAsset` action. You can setup policies to DENY the additional signer on the master account from transferring funds between subaccounts without user authorization. This ensures that subaccount balances remain isolated and protected - an important security measure when managing multiple trading strategies or client accounts. ```json theme={"system"} { "name": "DENY Account Transfers", "method": "eth_signTypedData_v4", "action": "DENY", "conditions": [ { "field_source": "ethereum_typed_data_message", "field": "hyperliquidChain", "typed_data": { "types": { "EIP712Domain": [ { "name": "name", "type": "string" }, { "name": "version", "type": "string" }, { "name": "chainId", "type": "uint256" }, { "name": "verifyingContract", "type": "address" } ], "HyperliquidTransaction:SendAsset": [ { "name": "hyperliquidChain", "type": "string" }, { "name": "destination", "type": "string" }, { "name": "sourceDex", "type": "string" }, { "name": "destinationDex", "type": "string" }, { "name": "token", "type": "string" }, { "name": "amount", "type": "string" }, { "name": "fromSubAccount", "type": "string" }, { "name": "nonce", "type": "uint64" } ] }, "primary_type": "HyperliquidTransaction:SendAsset" }, "operator": "in", "value": ["Testnet", "Mainnet"] } ] } ``` ### ALLOW Approve Agent Permit the master account to register new agent wallets. This allows the additional signer to approve agent registrations on behalf of the user. By explicitly allowing agent approval, you can enable operational flexibility while still denying other sensitive operations like withdrawals and account transfers. ```json theme={"system"} { "name": "ALLOW Approve Agent", "method": "eth_signTypedData_v4", "action": "ALLOW", "conditions": [ { "field_source": "ethereum_typed_data_message", "field": "hyperliquidChain", "typed_data": { "types": { "EIP712Domain": [ { "name": "name", "type": "string" }, { "name": "version", "type": "string" }, { "name": "chainId", "type": "uint256" }, { "name": "verifyingContract", "type": "address" } ], "HyperliquidTransaction:ApproveAgent": [ { "name": "hyperliquidChain", "type": "string" }, { "name": "agentAddress", "type": "string" }, { "name": "agentName", "type": "string" }, { "name": "nonce", "type": "uint64" } ] }, "primary_type": "HyperliquidTransaction:ApproveAgent" }, "operator": "in", "value": ["Testnet", "Mainnet"] } ] } ``` ## Creating Authorization Keys Authorization keys are signers that allow you to execute actions on wallets within the constraints defined by your policies. These keys can be controlled by your server, stored securely, and used to sign transactions on behalf of wallets. To create an authorization key, you can use either the Privy Dashboard or the REST API. The process generates a keypair where: * The **private key** is generated on your device and only known to you (Privy never sees it) * The **public key** is registered with Privy's secure enclave to verify signatures Save your authorization private key securely - Privy does not store it and cannot help you recover it later. You'll need this key to sign transactions with your policies applied. For detailed instructions on creating authorization keys, see the [Authorization Keys documentation](https://docs.privy.io/controls/authorization-keys/keys/create/key). ## Applying Policies to Wallets Once you've created policies, you can apply them to wallets by adding additional signers with policy overrides: ```javascript theme={"system"} // Update a wallet to add additional signers with specific policies const wallet = await privy.wallets().update('WALLET_ID', { policy_ids: [], // Global policies (empty in this case) additional_signers: [ { signer_id: 'SIGNER_ID', // Authorization key ID override_policy_ids: ['POLICY_ID'] // Policies for this signer } ] }); ``` Once you've applied policies to a wallet, you can use the authorization private key to sign transactions on behalf of the wallet: ```javascript theme={"system"} import { createViemAccount } from '@privy-io/node/viem'; // Create a viem account with authorization context const account = createViemAccount(privy, { walletId: wallet.id, address: wallet.address as `0x${string}`, authorizationContext: { authorization_private_keys: [ "AUTHORIZATION_PRIVATE_KEY" ] } }); // Use the account with Hyperliquid const client = new hl.ExchangeClient({ transport, wallet: account, }); ``` The authorization private key (`wallet-auth:...`) allows the additional signer to sign transactions. Any operations attempted will be evaluated against the policies you've defined. If a policy denies an action (like withdrawal), the transaction will fail before execution. ## Best Practices Create dedicated authorization keys for trading operations vs. administrative functions. Begin with strict policies and gradually relax them as needed, rather than starting permissive. Always validate your policy configuration on Hyperliquid testnet before deploying to production. Periodically review and update policies to match current risk management needs. ## Next Steps Learn about managing multiple accounts Earn fees with builder codes on Hyperliquid orders Develop smart contracts on HyperEVM # Subaccounts Source: https://docs.privy.io/recipes/hyperliquid/subaccounts Learn how to use Hyperliquid subaccounts to isolate balances, positions, and PnL per strategy or user while maintaining consolidated fee tiers. Before continuing with this guide, make sure you have initialized your Hyperliquid client as shown in the [Getting Started guide](/recipes/hyperliquid-guide). This guide assumes you have `client` and other basic setup completed. ## Overview [Subaccounts](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/exchange-endpoint#subaccounts-and-vaults) let you isolate balances, positions, and PnL per strategy or user while still rolling up trading volume to the master account for fee tier benefits. You can create up to 10 subaccounts per master account. **Key Benefits:** * **Risk Isolation**: Separate balances and positions for different strategies * **Consolidated Fees**: All trading volume rolls up to the master account * **Organization**: Manage multiple strategies or users under one master account * **Flexibility**: Each subaccount can have its own risk parameters Subaccounts are only available for master accounts with at least **\$100,000** in total trading volume. ## Creating a Subaccount ```javascript theme={"system"} // Create a new subaccount await masterClient.createSubAccount({ name: "Strategy Alpha", }); // List all subaccounts const subAccounts = await infoClient.subAccounts({ user: masterWallet.address as `0x${string}`, }); console.log("Subaccounts:", subAccounts); ``` ## Trading with a Subaccount To execute trades for a specific subaccount, create a client with the `defaultVaultAddress` parameter: ```javascript theme={"system"} // Get the first subaccount const subAccount = subAccounts[0]; // Create a client for the subaccount const subAccountClient = new hl.ExchangeClient({ transport, wallet: masterAccount, // Use the master account signer defaultVaultAddress: subAccount.subAccountUser }); // Place an order using the subaccount const subAccountOrder = await subAccountClient.order({ orders: [ { a: 0, // BTC b: true, // Buy s: '0.01', // Size r: false, p: '50000', t: {limit: {tif: 'Gtc'}} } ], grouping: 'na' }); ``` ## Transferring Funds Between Accounts Transfer funds between the master account and subaccounts using the `sendAsset` method: ```javascript theme={"system"} // Transfer from master to subaccount await masterClient.sendAsset({ destination: subAccount.subAccountUser, sourceDex: "", token: "USDC", amount: "1000", fromSubAccount: "", // Empty string = from master account destinationDex: "" }); // Transfer from subaccount back to master await masterClient.sendAsset({ destination: masterWallet.address as `0x${string}`, sourceDex: "", token: "USDC", amount: "500", fromSubAccount: subAccount.subAccountUser, // Specify source subaccount destinationDex: "" }); ``` The `fromSubAccount` parameter determines the source: an empty string `""` transfers from the master account, while specifying a subaccount address transfers from that subaccount. ## Combining Agents and Subaccounts You can use agent wallets to trade on behalf of subaccounts, creating a flexible multi-account trading architecture: ```javascript theme={"system"} // Agent wallet can trade for any subaccount of the master const agentForSubAccountClient = new hl.ExchangeClient({ transport, wallet: agentAccount, // Agent wallet defaultVaultAddress: subAccount.subAccountUser // Subaccount }); // Place order using agent for the subaccount await agentForSubAccountClient.order({ orders: [ { a: 0, b: true, s: '0.01', r: false, p: '50000', t: {limit: {tif: 'Gtc'}} } ], grouping: 'na' }); ``` This pattern is ideal for managing multiple strategies or user accounts - each subaccount gets isolated risk, while a single agent wallet can manage trading operations across all of them. ## Best Practices Create separate subaccounts for different risk profiles. For example, maintain one subaccount for conservative strategies with tight stop losses, and another for high-risk, high-reward strategies. Regularly check subaccount balances and positions to ensure proper risk allocation across your strategies. Remember that all subaccount trading volume rolls up to the master account, so you maintain your fee tier benefits across all strategies. ## Next Steps Learn about agent wallets for programmatic trading Explore common trading patterns and order types # Executing trades Source: https://docs.privy.io/recipes/hyperliquid/trading-patterns Learn common trading patterns and best practices for placing orders, managing positions, and executing strategies on Hyperliquid with Privy. Before continuing with this guide, make sure you have initialized your Hyperliquid client as shown in the [Getting Started guide](/recipes/hyperliquid-guide). This guide assumes you have `client` and other basic setup completed. ## Overview This guide covers essential trading patterns and techniques for building robust trading applications on Hyperliquid using Privy's secure wallet infrastructure. ## Getting Market Data Before placing trades, you'll need to fetch asset metadata and current market conditions: ```javascript theme={"system"} import * as hl from '@nktkas/hyperliquid'; const transport = new hl.HttpTransport({ isTestnet: true }); const infoClient = new hl.InfoClient({transport}); // Get all available assets and their current context const [meta, contexts] = await infoClient.metaAndAssetCtxs(); // Find a specific asset (e.g., BTC) const btcIndex = meta.universe.findIndex((asset) => asset.name === 'BTC'); const btcMeta = meta.universe[btcIndex]; const btcContext = contexts[btcIndex]; console.log('BTC Mark Price:', btcContext.markPx); console.log('BTC Funding Rate:', btcContext.funding); ``` ## Understanding Tick Size and Lot Size Before placing orders, it's critical to understand how Hyperliquid formats prices and sizes. Using incorrect precision will cause your orders to be rejected. **Price Precision (Tick Size):** Prices can have up to **5 significant figures**, but no more than `MAX_DECIMALS - szDecimals` decimal places: * **Perpetuals**: `MAX_DECIMALS = 6` * **Spot**: `MAX_DECIMALS = 8` Integer prices are always allowed, regardless of significant figures. **Examples for Perps:** * ✅ `1234.5` is valid * ❌ `1234.56` is not valid (too many significant figures) * ✅ `0.001234` is valid * ❌ `0.0012345` is not valid (more than 6 decimal places) **If `szDecimals = 1`:** * ✅ `0.01234` is valid * ❌ `0.012345` is not valid (more than `6 - 1 = 5` decimal places) **Size Precision (Lot Size):** Sizes are rounded to the `szDecimals` of that asset. For example: * If `szDecimals = 3`, then `1.001` is valid but `1.0001` is not * If `szDecimals = 2`, then `10.25` is valid but `10.251` is not You can find `szDecimals` for each asset in the meta response: ```javascript theme={"system"} const [meta, contexts] = await infoClient.metaAndAssetCtxs(); const btcMeta = meta.universe[0]; // Assuming BTC is first console.log('BTC szDecimals:', btcMeta.szDecimals); ``` **Important**: When implementing signing, trailing zeroes should be removed from prices and sizes. See the [Hyperliquid tick and lot size documentation](https://hyperliquid.gitbook.io/hyperliquid-docs/for-developers/api/tick-and-lot-size) for more details. ## Order Types ### Market Orders Hyperliquid doesn't have traditional market orders, but you can achieve market-like execution by placing a limit order with `tif: "Ioc"` (Immediate-Or-Cancel) and a price that guarantees immediate execution: * **For buys**: Set limit price ≥ current best ask * **For sells**: Set limit price ≤ current best bid ```javascript theme={"system"} // Get current market price const [meta, contexts] = await infoClient.metaAndAssetCtxs(); const btcContext = contexts[0]; // BTC const currentPrice = parseFloat(btcContext.markPx); // Market buy: use a price above current market to ensure execution await client.order({ orders: [ { a: 0, // BTC b: true, // Buy p: String(currentPrice * 1.01), // 1% above mark price s: '0.01', // Size r: false, t: {limit: {tif: 'Ioc'}} // Immediate-or-cancel } ], grouping: 'na' }); // Market sell: use a price below current market to ensure execution await client.order({ orders: [ { a: 0, // BTC b: false, // Sell p: String(currentPrice * 0.99), // 1% below mark price s: '0.01', r: false, t: {limit: {tif: 'Ioc'}} } ], grouping: 'na' }); ``` Market orders execute at the best available price, which may differ significantly from the mark price in volatile or illiquid markets. Always validate that the execution price is acceptable before placing large orders. ### Limit Orders Place an order at a specific price. Limit orders let you control the exact price at which you're willing to buy or sell. ```javascript theme={"system"} // Place a limit buy order for BTC at $95,000 await client.order({ orders: [ { a: 0, // Asset index (0 = BTC from meta.universe) b: true, // Buy side (true = buy, false = sell) p: '95000', // Limit price in USD s: '0.01', // Size (0.01 BTC) r: false, // Reduce-only (false = can open new position) t: {limit: {tif: 'Gtc'}} // Time in force } ], grouping: 'na' }); ``` **Order Parameters:** * **`a` (asset)**: The asset index from `meta.universe`. For example, `0` is typically BTC. * **`b` (buy/sell)**: `true` for buy orders, `false` for sell orders. * **`p` (price)**: Limit price as a string. Must follow tick size rules (see above). * **`s` (size)**: Order size as a string. Must follow lot size rules based on `szDecimals`. * **`r` (reduce-only)**: If `true`, order can only reduce existing position, not open new positions. * **`t` (order type)**: Object specifying order type and parameters. * **`grouping`**: Order grouping strategy, typically `"na"` for standard orders. **Time In Force (TIF) Options:** The `tif` parameter in limit orders controls how long the order remains active: Fills what it can immediately; any remainder stays on the order book until filled or manually canceled. This is the most common option for limit orders. ```javascript theme={"system"} t: { limit: { tif: "Gtc" } } ``` Fills immediately up to your limit price; any unfilled remainder is automatically canceled. The order never rests on the book. Useful for ensuring immediate execution without leaving open orders. ```javascript theme={"system"} t: { limit: { tif: "Ioc" } } ``` Must add liquidity to the order book. If the order would cross the spread and immediately match (take liquidity), it's rejected/canceled instead. This guarantees you receive maker fees rather than paying taker fees. ```javascript theme={"system"} t: { limit: { tif: "Alo" } } ``` ### Stop Loss and Take Profit Orders You can create linked TP/SL (Take Profit/Stop Loss) orders that trigger when price reaches specific levels. Use `grouping: "normalTpsl"` to link the entry order with its stop-loss and take-profit exit orders. ```javascript theme={"system"} // Create an entry order with linked TP/SL const tpsl = await client.order({ grouping: 'normalTpsl', // Links the three orders as a TP/SL set orders: [ // (A) Entry order - limit GTC at $100,000 { a: 0, // Asset index (BTC) b: true, // Buy s: '0.0036', // Size (respect szDecimals for BTC) p: '100000', // Limit price r: false, // Not reduce-only t: {limit: {tif: 'Gtc'}} }, // (B) Stop-loss - triggers at $95,000, executes as limit at $94,000 { a: 0, b: false, // Sell to close s: '0.0036', // Must match entry size p: '94000', // Limit price once triggered (allows slippage) r: true, // Reduce-only t: { trigger: { isMarket: true, tpsl: 'sl', // Stop-loss type triggerPx: '95000' // Trigger price (when order activates) } } }, // (C) Take-profit - triggers at $105,000, executes as limit at $101,200 { a: 0, b: false, // Sell to close s: '0.0036', // Must match entry size p: '101200', // Limit price once triggered r: true, // Reduce-only t: { trigger: { isMarket: true, tpsl: 'tp', // Take-profit type triggerPx: '105000' // Trigger price (when order activates) } } } ] }); ``` **Understanding Trigger vs Limit Prices:** The `triggerPx` and `p` (limit price) serve different purposes in TP/SL orders: * **`triggerPx`**: The price level that activates the order * **`p` (limit price)**: The worst price you're willing to accept once triggered For stop-loss orders, set the limit price (`p`) **below** the trigger to allow for slippage during fast price movements. In the example above, the stop-loss triggers at \$95,000 but will execute at \$94,000 or better. For take-profit orders, you can set a more conservative limit price to ensure execution. The take-profit triggers at \$105,000 but will accept \$101,200 or better. This approach ensures your orders execute even in volatile conditions while still providing some price protection. When one of the TP/SL orders executes (either stop-loss or take-profit), the other is automatically canceled. The `r: true` (reduce-only) flag ensures these orders can only close positions, not open new ones. ### TWAP Orders TWAP (Time-Weighted Average Price) orders split large orders into smaller chunks executed over time to minimize market impact and reduce slippage. ```javascript theme={"system"} // Execute a TWAP buy order over 30 minutes const twap = await client.twapOrder({ twap: { a: 3, // Asset index b: true, // Buy side (true = buy, false = sell) s: '0.01252', // Total size to execute (respects szDecimals) r: false, // Reduce-only m: 30, // Duration in minutes t: false // Randomize timing between chunks (false = evenly spaced) } }); ``` **TWAP Parameters:** * **`a` (asset)**: The asset index from `meta.universe` * **`b` (buy/sell)**: `true` for buy orders, `false` for sell orders * **`s` (size)**: Total size to execute across all chunks * **`r` (reduce-only)**: If `true`, order can only reduce existing positions * **`m` (minutes)**: Duration over which to execute the order (e.g., 30 = 30 minutes) * **`t` (randomize)**: If `true`, randomizes timing between chunks to make execution less predictable; if `false`, chunks are evenly spaced TWAP orders are ideal for executing large orders without significantly moving the market. The order is automatically split into smaller chunks and executed at regular (or randomized) intervals over the specified duration. ## Risk Management Hyperliquid supports two margin modes: **cross margin** and **isolated margin**. * **Cross Margin**: Uses your total perps balance as collateral across all positions. If one position loses money, other positions can help cover the loss. * **Isolated Margin**: Margin is isolated to each individual position. If a position is liquidated, it won't affect your other positions. ```javascript theme={"system"} // Update leverage for an asset const updateLeverage = await client.updateLeverage({ asset: 0, // Asset index (BTC) isCross: true, // true = cross margin, false = isolated margin leverage: 12 // Leverage multiplier (e.g., 12x) }); console.log("Leverage updated:", updateLeverage); // Check active asset data to verify leverage settings const assetData = await infoClient.activeAssetData({ user: wallet.address as `0x${string}`, coin: "BTC", // Asset name (e.g., "BTC", "ETH", "SOL") }); console.log("Current leverage:", assetData.leverage); console.log("Margin mode:", assetData.marginMode); ``` Higher leverage amplifies both gains and losses. Always understand your liquidation price before increasing leverage. Cross margin provides more flexibility but risks your entire account balance. ## Monitoring and Analytics Get comprehensive information about your account balance, open positions, and risk metrics: ```javascript theme={"system"} const transport = new hl.HttpTransport(); const infoClient = new hl.InfoClient({ transport }); // Get user clearinghouse state (positions, balance, margin) const clearinghouseState = await infoClient.clearinghouseState({ user: wallet.address as `0x${string}`, }); console.log("Account balance:", clearinghouseState.marginSummary.accountValue); console.log("Total position value:", clearinghouseState.marginSummary.totalNtlPos); console.log("Unrealized PnL:", clearinghouseState.marginSummary.totalRawUsd); console.log("Open positions:", clearinghouseState.assetPositions); ``` **Checking Open Orders:** View all your active orders across all assets: ```javascript theme={"system"} // Get all open orders for the user const openOrders = await infoClient.openOrders({ user: wallet.address as `0x${string}`, }); console.log("Open orders:", openOrders); ``` ## Best Practices Before placing orders, verify that prices are within expected ranges to avoid fat-finger errors. When closing positions, use reduce-only orders to prevent accidentally opening new positions. Keep track of funding rates, especially for large positions, as they can significantly impact profitability. Add safeguards to stop trading if certain thresholds are hit (e.g., max daily loss, max position size). ## Next Steps Learn about agent wallets for programmatic trading Implement advanced security policies # Login with Lens Source: https://docs.privy.io/recipes/lens [Lens Protocol](https://www.lens.xyz/) is an open social network that allows users to own their content and connections. Developers can build on the network, leveraging its audience and infrastructure. Users can seamlessly switch between social apps without losing their profiles, content, or connections. Allowing users to log into Lens with Privy is fully supported and simple to integrate. In this recipe, you'll integrate Privy + wagmi with the Lens React SDK, then let users log in with their Lens account using an embedded or external wallet. ## Resources Official documentation for Lens protocol and SDK. Configure Privy with wagmi for EVM integrations. *** ## Integrate Lens login ```bash theme={"system"} pnpm add @privy-io/react-auth @lens-protocol/react@canary @privy-io/wagmi @tanstack/react-query wagmi viem ``` We use @lens-protocol/react\@canary to access the latest Lens features. This step assumes you have set up your project with Privy and integrated with wagmi. If not, follow the [Privy wagmi guide](https://docs.privy.io/wallets/connectors/ethereum/integrations/wagmi#complete-example). Once your Privy setup is complete, initialize the Lens provider and client. Lens Protocol runs on the Lens chain (mainnet and testnet). Ensure your wagmi and Privy configs include the Lens chains. Wrap your app with LensProvider to use the Lens SDK across your app. ```tsx {skip-check} theme={"system"} import {QueryClient, QueryClientProvider} from '@tanstack/react-query'; import {LensProvider, PublicClient, mainnet} from '@lens-protocol/react'; // [!code ++] const queryClient = new QueryClient(); // [!code ++:4] const lensClient = PublicClient.create({ environment: mainnet, storage: window.localStorage }); return ( {children} {/* [!code ++] */} ); ``` ```ts {skip-check} theme={"system"} import {lens, lensTestnet} from 'viem/chains'; export const privyConfig = { supportedChains: [lens, lensTestnet] }; ``` ```ts {skip-check} theme={"system"} import {createConfig} from '@privy-io/wagmi'; import {http} from 'wagmi'; import {lens, lensTestnet} from 'viem/chains'; export const wagmiConfig = createConfig({ chains: [lens, lensTestnet], transports: { [lens.id]: http(), [lensTestnet.id]: http() } }); ``` You’ve successfully set up the Lens SDK. Your app is now ready to use features like logging in with Lens and posting directly on Lens. Use `login` to prompt the user to connect a wallet. With Privy, you can automatically create wallets for users who don’t have one (for example, when signing in with Google or another social method). With this new wallet, you can onboard the user to your Lens‑powered app and keep the onboarding experience seamless. You can customize whether to automatically create an embedded wallet for the user. See the automatic wallet creation guide. ```tsx theme={"system"} import {usePrivy} from '@privy-io/react-auth'; const {login} = usePrivy(); ; ``` First, fetch all the Lens accounts associated with the connected user wallet. ```ts {skip-check} theme={"system"} import {useAccountsAvailable} from '@lens-protocol/react'; import {useAccount} from 'wagmi'; const {address} = useAccount(); const {data: lensAccounts} = useAccountsAvailable({ managedBy: address }); ``` If a user has multiple wallets connected, set the active wallet to ensure you log in with the correct Lens account. ```ts theme={"system"} import {useWallets} from '@privy-io/react-auth'; import {useSetActiveWallet} from '@privy-io/wagmi'; const {wallets} = useWallets(); const {setActiveWallet} = useSetActiveWallet(); await setActiveWallet(wallets[0]); ``` Create a hook that handles the Lens login process for the selected account. ```ts {skip-check} theme={"system"} import {AccountAvailable, EvmAddress, useLogin} from '@lens-protocol/react'; import {signMessageWith} from '@lens-protocol/react/viem'; import {useAccount, useWalletClient} from 'wagmi'; export function useLensLogin() { const {address} = useAccount(); const {data: signer} = useWalletClient(); const {execute: login} = useLogin(); return (item: AccountAvailable) => { if (!signer) return; const ownerOrManager = signer.account?.address ?? (address as EvmAddress | undefined); if (!ownerOrManager) return; const payload = item.__typename === 'AccountManaged' ? { accountManager: { account: item.account.address as EvmAddress, manager: ownerOrManager as EvmAddress } } : { accountOwner: { account: item.account.address as EvmAddress, owner: ownerOrManager as EvmAddress } }; return login({ ...payload, signMessage: signMessageWith(signer) }); }; } ``` Show the fetched accounts and let users pick one to log in. ```tsx {skip-check} theme={"system"} import {useAccountsAvailable} from '@lens-protocol/react'; import {useAccount} from 'wagmi'; import {useLensLogin} from './use-lens-login'; export function LensAccountsList() { const {address} = useAccount(); const {data: lensAccounts} = useAccountsAvailable({managedBy: address}); const loginWithLensAccount = useLensLogin(); if (!lensAccounts || lensAccounts.items.length === 0) { return

No Lens accounts found for {address}.

; } return (
{lensAccounts.items.map((item) => (
))}
); } ```
Use `useAuthenticatedUser` to check whether the user is authenticated with a Lens account. ```ts {skip-check} theme={"system"} import {useAuthenticatedUser} from '@lens-protocol/react'; const {data: isAuthenticated} = useAuthenticatedUser(); ```
*** ## Next steps Explore the full [Lens SDK documentation](https://lens.xyz/docs/protocol/getting-started/react). * Create posts and publications * React to publications * Manage profiles and metadata For wagmi setup with Privy, see [Integrating with wagmi](https://docs.privy.io/wallets/connectors/ethereum/integrations/wagmi). # Migrating embedded wallets from Alchemy to Privy Source: https://docs.privy.io/recipes/migrating-embedded-wallets-from-alchemy This guide walks through migrating your app and users' embedded wallets from Alchemy Account Kit to Privy. The migration SDK handles re-authenticating users with Alchemy, securely exporting their private keys, and importing them into Privy. After migration, users log in with Privy while keeping the same wallet addresses and assets. Transactions and sponsorship continue through Alchemy infrastructure. June 1 is the cutoff for new signups and sends through the Alchemy React package. After June 1, Alchemy only allows logins to support migration. ## Overview 1. Set up Privy and replace Alchemy auth by swapping `@account-kit/react` for `@privy-io/react-auth`. 2. Reconnect sending and gas sponsorship by wiring Privy wallets to Alchemy transaction infrastructure. 3. Install the migration SDK to migrate wallet keys when users log in. 4. Deploy the updated app, export users from Alchemy, and import users into Privy. ## Step 1: Set up Privy and replace Alchemy auth Replace Alchemy auth SDKs with Privy to support login and signup. 1. [Set up your organization in Privy](https://docs.privy.io/basics/get-started/organization). 2. Follow [React SDK setup instructions](https://docs.privy.io/basics/react/installation). 3. Enable the same login methods your users used with Alchemy, such as email, Google, and passkeys. ### Step 1a: Install Privy and remove Alchemy auth packages ```bash theme={"system"} npm install @privy-io/react-auth npm uninstall @account-kit/react ``` ### Step 1b: Replace the Alchemy provider with `PrivyProvider` Remove `AlchemyAccountProvider` (and `cookieToInitialState` for SSR), then wrap your app with `PrivyProvider`. Ensure you set createOnLogin to "user-without-wallet", and that the plugin for wallet creation is set as follows. ```tsx theme={"system"} import {PrivyProvider, createWalletCreationOnLoginPlugin, User} from '@privy-io/react-auth'; function Providers({children}) { // Add custom logic to only create a new embedded wallet const walletCreationPluginOptions = { shouldCreateWallet: ({user}: {user: User}) => user.customMetadata?.['alchemy_org_id'] === undefined }; return ( {children} ); } ``` ### Step 1c: Replace Alchemy hooks with Privy equivalents Update all imports from `@account-kit/react`. | Before (Alchemy) | After (Privy) | | -------------------------------- | ------------------------------------------------------- | | `useSignerStatus().isConnected` | `usePrivy().authenticated` | | `useAuthModal().openAuthModal()` | `usePrivy().login()` | | `useLogout()` | `usePrivy().logout` (function, not hook) | | `useUser()` | `usePrivy().user` (`email` is at `user.email?.address`) | | `useSmartAccountClient()` | Use `@alchemy/wallet-apis` in step 2 | | `useSendUserOperation()` | `client.sendCalls()` in step 2 | | `cookieToInitialState` (SSR) | Remove | At this point, your app should compile, and users can log in with Privy. Transaction sending is not yet wired. ## Step 2: Reconnect sending and gas sponsorship Wire Privy wallets to Alchemy transaction infrastructure so gasless transactions, batching, and existing send flows continue to work. This will now use Alchemy SDK v5 with Wallet APIs. Full guide for Privy + Alchemy: * [Alchemy: Privy signer integration guide](https://www.alchemy.com/docs/wallets/third-party/signers/privy) More on Alchemy SDK v5: * [Alchemy Wallets documentation](https://www.alchemy.com/docs/wallets) Notes: * The client defaults to EIP-7702 and delegates the Privy wallet at send time. * For ERC-4337 mode, request an account before sending and include the account address in `sendCalls`. * If your app previously used ERC-4337 (with assets directly in smart accounts), follow the EIP-7702 guide for non-7702 mode. Add an extra call to `wallet_requestAccount` before sending. ### Prerequisites * **Alchemy API key**: From the [Alchemy Dashboard](https://dashboard.alchemy.com/apps). This must be the same app that has your Smart Wallets configuration and gas policy linked. * **Gas sponsorship policy ID**: From the [Gas Manager dashboard](https://dashboard.alchemy.com/gas-manager). ### Step 2a: Install transaction SDK ```bash theme={"system"} npm install @alchemy/wallet-apis ``` ### Step 2b: Get the Privy signer Use `toViemAccount` to convert a Privy embedded wallet into a viem `LocalAccount`. ```tsx theme={"system"} import {toViemAccount, useWallets} from '@privy-io/react-auth'; import {useEffect, useState} from 'react'; import type {LocalAccount} from 'viem'; const usePrivySigner = () => { const { wallets: [wallet] } = useWallets(); const [signer, setSigner] = useState(); useEffect(() => { if (!wallet || signer) return; toViemAccount({wallet}).then(setSigner); }, [wallet, signer]); return signer; }; ``` What's happening: `useWallets()` returns the user's Privy wallets. `toViemAccount` converts the Privy wallet into a standard viem `LocalAccount`, which is what the Alchemy wallet client needs as a signer. The signer is `undefined` until the wallet is ready, so your UI should handle that loading state. ### Step 2c: Send gasless transactions ```tsx theme={"system"} import {useMemo, useCallback} from 'react'; import {zeroAddress} from 'viem'; import {createSmartWalletClient, alchemyWalletTransport} from '@alchemy/wallet-apis'; import {arbitrumSepolia} from 'viem/chains'; import type {LocalAccount} from 'viem'; function SendTransaction({signer}: {signer: LocalAccount}) { const client = useMemo( () => createSmartWalletClient({ signer, transport: alchemyWalletTransport({ apiKey: 'YOUR_ALCHEMY_API_KEY' }), chain: arbitrumSepolia, paymaster: { policyId: 'YOUR_GAS_MANAGER_POLICY_ID' } }), [signer] ); const handleSend = useCallback(async () => { // If using 4337 accounts and not 7702, add a call to request account // const { address } = await client.requestAccount({ creationHint: { accountType: "sma-b" } }); const {id} = await client.sendCalls({ // If non 7702, you'll need to add the address to the from: field in sendCalls calls: [{to: zeroAddress, value: BigInt(0), data: '0x'}] }); const result = await client.waitForCallsStatus({id}); console.log(`Transaction hash: ${result.receipts?.[0]?.transactionHash}`); }, [client]); return ; } ``` At this point, your app should fully work with Privy auth and Alchemy gas sponsorship. Users can log in, sign, and send gasless transactions. Next, migrate existing users' wallet keys. ## Step 3: Install the React Migration SDK The migration SDK is a drop-in React component that detects users who need wallet migration, prompts re-authentication through Alchemy, and transfers key material from Alchemy TEE to Privy TEE with end-to-end encryption. ### How it works When a user logs in, the SDK: 1. Detects migration need by checking Alchemy migration metadata and missing embedded wallets. 2. Shows a migration modal that prompts re-authentication with the original Alchemy method. 3. Migrates wallets by exporting keys from Alchemy TEE and importing into Privy TEE. 4. Confirms completion with a success view and auto-close. Users keep the same wallet addresses. No seed phrases and no manual steps are required. ### Supported authentication methods * Email (OTP) * Google * Twitter/X * GitHub * Discord * Passkey (non-anonymous, for example passkeys linked to email) ### Step 3a: Install migration packages ```bash theme={"system"} npm install @privy-io/alchemy-migration @account-kit/react @account-kit/infra ``` ### Step 3b: Import migration styles ```tsx theme={"system"} import '@privy-io/alchemy-migration/styles.css'; ``` ### Step 3c: Create the Alchemy config Use `createMigrationConfig` and include all auth methods your users used with Alchemy. The `auth.sections` field should include all login methods originally used with Alchemy. The SDK automatically detects which method each user needs and shows the right sign-in flow. This remains true even if your app used custom UI with `useAuthenticate`. Use the same config pattern below and include all auth methods your users used. Only pre-built components are supported for the migration flow. ```tsx theme={"system"} import {createMigrationConfig} from '@privy-io/alchemy-migration'; import {alchemy} from '@account-kit/infra'; import {sepolia} from '@account-kit/infra'; const alchemyConfig = createMigrationConfig( { transport: alchemy({apiKey: 'YOUR_ALCHEMY_API_KEY'}), chain: sepolia, // Use your app's chain ssr: true }, { auth: { sections: [ [{type: 'email'}], [ {type: 'passkey'}, {type: 'social', authProviderId: 'google', mode: 'popup'} // Add all other providers your users signed up with ] ] } } ); ``` ### Step 3d: Set up `MigrationProvider` Wrap `MigrationProvider` inside `PrivyProvider`. ```tsx theme={"system"} import {PrivyProvider} from '@privy-io/react-auth'; import {MigrationProvider} from '@privy-io/alchemy-migration'; function App({children}) { return ( {children} ); } ``` ### `MigrationProvider` props | Prop | Type | Required | Description | | --------------------- | ----------------------------- | -------- | ------------------------------------------- | | `alchemyConfig` | `AlchemyAccountsConfigWithUI` | Yes | Config created with `createMigrationConfig` | | `privyAppId` | `string` | Yes | Privy app ID | | `privyClientId` | `string` | Yes | Privy app client ID | | `showDebugButton` | `boolean` | No | Show debug buttons for testing (dev only) | | `skipOrgVerification` | `boolean` | No | Skip Alchemy org ID verification (dev only) | ### After migration is complete After users have migrated wallets (for example, after at least one login): * Remove `@privy-io/alchemy-migration`, `@account-kit/react`, and `@account-kit/infra`. * Keep `createOnLogin: 'users-without-wallets'` for standard new-user wallet creation. * Remove `MigrationProvider`. ## Step 4: Deploy the updated app, export users, and import into Privy To detect users who need key migration, Privy needs imported user data and linked auth methods from Alchemy. This import does not move keys. Critical sequencing: user export from Alchemy must happen at deployment time or immediately after deployment. If export runs first, users who sign up before deployment can be missing from Privy. Recommended sequence: **Deploy → Export → Import**. 1. Pause new signups briefly. 2. Deploy your updated app with Privy auth. 3. Immediately export users from Alchemy. 4. Import users into Privy. 5. Resume normal operations. ### Step 4a: Export users from Alchemy 1. Open the Alchemy dashboard and choose your app. 2. Go to **Wallets** → **Export**. 3. Select **Export users** and download the JSON file. ### Step 4b: Import users into Privy 1. Open the Privy dashboard and choose your target app. 2. Go to **User management** → **Users** → **Import users**. 3. Upload the JSON exported from Alchemy. Privy creates accounts with linked auth methods and migration metadata. The migration SDK then detects which users need key transfer. ## FAQ ### Do users need to do anything? Users only need to sign in once with their original Alchemy login method. ### Will wallet addresses change? No. Wallet addresses remain the same. ### Are private keys ever exposed? No. Keys move from Alchemy TEE to Privy TEE through end-to-end encryption. ### What if a user has both Ethereum and Solana wallets? Both are migrated automatically and independently. ### What if a user already has a Privy wallet? Only missing wallets are migrated. If a user already has an ETH wallet but not SOL, only SOL is migrated. ### Can migration be tested before launch? Yes. Set `showDebugButton: true` on `MigrationProvider` and validate with a test Alchemy app. ### What if deployment and export cannot happen at exactly the same time? Deploy first, then export. Once your app is on Privy, no new users are being created in Alchemy, so the export captures everyone. If export happens before deploy and there is a gap, run a second export after deploying to capture users who signed up in between. ### The migration modal never appears Common causes: 1. Users were not imported from Alchemy before first Privy login. 2. Wallet auto-creation ran before migration checks. 3. The user already has all required wallets in Privy. # Migrating JWT authentication from Alchemy to Privy Source: https://docs.privy.io/recipes/migrating-jwt-authentication-from-alchemy This guide is for developers who authenticate users with Alchemy Signer using JWT (JSON Web Token) authentication and need to migrate to Privy. If you use standard auth methods (email, Google, and passkeys) with the React SDK, use the [React migration guide](https://docs.privy.io/recipes/migrating-embedded-wallets-from-alchemy) instead. Before starting, read the [signer migration overview](https://www.alchemy.com/docs/wallets/wallet-integrations/privy/signer-migration-overview) to confirm your account type (Modular Account v2 vs. another implementation) and connection type (EIP-7702 vs. ERC-4337). Apps not on MAv2, or using pure 4337, need to adjust a step in this guide — the overview's [Edge cases](https://www.alchemy.com/docs/wallets/wallet-integrations/privy/signer-migration-overview#edge-cases) section walks through each. ## How JWT migration differs from standard migration With JWT auth, you control both sides of authentication — your server generates JWTs, and both Alchemy and your new provider accept them. This simplifies the migration because: * No user export/import needed (unless you also use other auth methods) — both providers authenticate via your JWT * JWTs are reusable — they're not one-time-use tokens, so the same JWT can authenticate with both Alchemy and Privy in a single session * Migration detection is your responsibility — since there's no Privy-side metadata, you track which users have migrated in your own database ## Choose your integration path This guide covers two paths. Both share the same Privy dashboard setup (Steps 1–2a), but differ in how authentication and wallet import happen: | | Client-side path | Server-side path | | -------------------- | --------------------------------------------------------------- | ------------------------------------------------------------ | | Best for | React / React Native apps where the user's browser handles auth | Backend services where your server controls auth and wallets | | Auth hook | `useSubscribeToJwtAuthWithFlag` (React SDK) | REST API: exchange JWT for a Privy `userSigner` | | Wallet import | `useImportWallet` hook (client-side) | `@privy-io/node` SDK or REST API `/v1/wallets/import/*` | | Private key exposure | Briefly in client memory during export→import | On your server during export→import | ## Step 1: Add a migration tracking field to your user database Add a field to your user/account model to track migration status. Every existing user starts as `needs_migration = true`. ```sql theme={"system"} ALTER TABLE users ADD COLUMN signer_migrated BOOLEAN DEFAULT FALSE; ``` Or use whatever mechanism fits your stack — a boolean field, a status enum, a separate migration table. The point is: you need a way to check on each login whether this user still needs their Alchemy wallet keys migrated. ## Step 2: Set up Privy with JWT auth ### 2a. Create a Privy app and configure JWT verification This setup is the same regardless of whether you choose the client-side or server-side path. 1. Go to the [Privy Dashboard](https://dashboard.privy.io/) and create an app 2. Request access to Custom Auth Support in the Integrations > Built-in tab 3. Navigate to User Management > Authentication > JWT-based auth 4. Select the environment: * Client side if JWT-authenticated requests will come from end-user devices (React / React Native) * Server side if requests will come from your backend servers 5. Provide your JWT verification key — either a JWKS endpoint URL or a public verification key (PEM certificate) 6. Enter the JWT claim that contains the user's unique ID (usually `sub`) See the [Privy JWT setup docs](https://docs.privy.io/authentication/user-authentication/jwt-based-auth/setup) for full details. ### 2b. Disable automatic wallet creation during migration While existing users still need migrating, you don't want Privy to auto-create a fresh wallet before you import their Alchemy wallet. If using a client SDK, set `createOnLogin: "off"`: ```tsx theme={"system"} ``` For new users (signing up after your migration is live): because you track migration status in your own database (Step 1), new users start with `signer_migrated = true` (no Alchemy wallet to migrate). Create a Privy wallet for them explicitly after login — either by calling [`createWallet`](https://docs.privy.io/wallets/using-wallets/ethereum/create-a-wallet) from the client SDK or via the `@privy-io/node` SDK server-side. Once all existing users have migrated, switch `createOnLogin` back to `"users-without-wallets"` and you can drop the explicit creation call. ## Step 3: Reconnect sending and gas sponsorship Install `@alchemy/wallet-apis` and wire up `createSmartWalletClient`. See the [Privy signer integration guide](https://www.alchemy.com/docs/wallets/third-party/signers/privy) for full setup details, or follow the [React migration guide Step 2](https://docs.privy.io/recipes/migrating-embedded-wallets-from-alchemy#step-2-reconnect-sending-and-gas-sponsorship) for a walkthrough. *** ## Client-side path Use this path if you have a React or React Native app and want the migration to happen in the user's browser/device. ### 4a. Integrate Privy auth (client-side) Instead of calling a Privy login function directly, you subscribe the Privy SDK to your existing auth provider's state using the `useSubscribeToJwtAuthWithFlag` hook. Privy will automatically authenticate when your provider reports the user is logged in. In a component that lives below both `PrivyProvider` and your auth provider: ```tsx theme={"system"} import {useAuth} from 'your-auth-provider'; import {useSubscribeToJwtAuthWithFlag} from '@privy-io/react-auth'; const AuthStateSync = () => { const {getToken, isLoading, isAuthenticated} = useAuth(); useSubscribeToJwtAuthWithFlag({ isAuthenticated, isLoading, getExternalJwt: async () => { if (isAuthenticated) { return await getToken(); } } }); return null; }; ``` Mount this component throughout the lifetime of your app to keep Privy in sync: ```tsx theme={"system"} import {AuthProvider} from 'your-auth-provider'; import {PrivyProvider} from '@privy-io/react-auth'; function App() { return ( ); } ``` You can check the user's Privy auth status with `usePrivy`: ```tsx theme={"system"} import {usePrivy} from '@privy-io/react-auth'; function MainContent() { const {user, ready, authenticated} = usePrivy(); if (!ready) return
Loading...
; if (!authenticated) return
Please log in through your authentication provider
; return
Welcome, {user.id}!
; } ``` Do not call Privy's `login` method (from `useLogin` or `usePrivy`) when using JWT-based auth. Let your auth provider handle login; Privy syncs automatically. See the [Privy JWT usage docs](https://docs.privy.io/authentication/user-authentication/jwt-based-auth/usage) for full details. ### 4b. Build the client-side migration flow The migration happens in a single session where the user is authenticated with both Alchemy and Privy simultaneously. ```tsx theme={"system"} // useImportWallet is a React hook — call it at the component/hook level, not inside async functions const {importWallet} = useImportWallet(); async function handleMigration(userId: string) { // 1. Get JWT from your auth provider (same token Privy is already using) const jwt = await getToken(); // 2. Check if user needs migration const user = await getUserFromDB(userId); if (!user.signer_migrated) { // 3. Authenticate with Alchemy using the SAME JWT const alchemySigner = new AlchemyWebSigner({ client: { connection: {apiKey: 'YOUR_ALCHEMY_API_KEY'}, iframeConfig: {iframeContainerId: 'alchemy-signer-iframe-container'} } }); await alchemySigner.authenticate({type: 'jwt', token: jwt}); // 4. Export private key from Alchemy const privateKey = await alchemySigner.exportPrivateKey(); // 5. Import into Privy await importWallet({privateKey}); // 6. Mark migration complete await updateUserDB(userId, {signer_migrated: true}); } } ``` ### 4c. Export details The snippet in 4b already authenticates with Alchemy using the same JWT and calls `exportPrivateKey()`. A note on when to use the encrypted variant: ```tsx theme={"system"} // Plaintext export — fine for the fully client-side flow above const privateKey = await alchemySigner.exportPrivateKey(); // If you need to ship the key to another process (e.g. your backend for the server-side path), // export it encrypted against a public key you control: const privateKeyEncrypted = await alchemySigner.exportPrivateKeyEncrypted(...); ``` ### 4d. Import into Privy (client-side) Use the `useImportWallet` hook from `@privy-io/react-auth`. It accepts a raw hex private key (with or without `0x` prefix) for Ethereum, or a base58-encoded key for Solana. Ethereum: ```tsx theme={"system"} import {useImportWallet} from '@privy-io/react-auth'; const {importWallet} = useImportWallet(); const wallet = await importWallet({privateKey: ethPrivateKey}); ``` Solana: ```tsx theme={"system"} import {useImportWallet} from '@privy-io/react-auth/solana'; const {importWallet} = useImportWallet(); const wallet = await importWallet({privateKey: solPrivateKey}); ``` ### 4e. Mark migration complete Update your database so the user isn't prompted again on next login. ```tsx theme={"system"} await db.users.update(userId, {signer_migrated: true}); ``` *** ## Server-side path (import only) Alchemy's JWT signer runs in the browser via `AlchemyWebSigner` — there is no supported path for authenticating an Alchemy JWT signer from a backend. That means the export from Alchemy must still happen client-side (as in 4a–4c above). This server-side path only covers the import side: your client exports the key from Alchemy, ships it to your server, and your server imports it into Privy using the `@privy-io/node` SDK. For most apps, the fully client-side path in 4a–4e is simpler. Use this path only if you have a specific reason to move the import to the server — for example, to attach policies, owners, or additional signers at import time. ### 5a. Set up the Privy Node SDK In the Privy dashboard JWT configuration (Step 2a), select "Server side" so Privy will accept JWTs from your backend. Then install the SDK: ```tsx theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: 'your-privy-app-id', appSecret: 'your-privy-app-secret' }); ``` ### 5b. Ship the exported key to your server On the client, export the private key from Alchemy as in 4c. To avoid sending plaintext key material over the network, use `exportPrivateKeyEncrypted` with a public key owned by your backend: ```tsx theme={"system"} // Client const encryptedKey = await alchemySigner.exportPrivateKeyEncrypted({ publicKey: serverPublicKey }); await fetch('/api/migrate-signer', { method: 'POST', body: JSON.stringify({encryptedKey}) }); ``` ### 5c. Import into Privy from your backend Decrypt the key with your server's private key, then pass it to the Node SDK. `@privy-io/node` re-encrypts the key with HPKE before sending it to Privy's TEE, so the plaintext key never leaves your server over the wire. ```tsx theme={"system"} async function handleMigrateSigner(userId: string, encryptedKey: string) { const user = await getUserFromDB(userId); if (user.signer_migrated) return; const privateKey = decryptWithServerKey(encryptedKey); const wallet = await privy.wallets().import({ wallet: { entropy_type: 'private-key', chain_type: 'ethereum', address: user.wallet_address, private_key: privateKey } }); await db.users.update(userId, {signer_migrated: true}); } ``` ### 5c. Import into Privy (server-side) The `@privy-io/node` SDK encrypts the key material automatically for secure transmission to the TEE. Ethereum (raw private key): ```tsx theme={"system"} const wallet = await privy.wallets().import({ wallet: { entropy_type: 'private-key', chain_type: 'ethereum', address: '', private_key: '' } }); ``` Solana (raw private key): ```tsx theme={"system"} const wallet = await privy.wallets().import({ wallet: { entropy_type: 'private-key', chain_type: 'solana', address: '', private_key: '' } }); ``` HD wallet (mnemonic): If you're migrating HD wallets with a BIP39 mnemonic: ```tsx theme={"system"} const wallet = await privy.wallets().import({ wallet: { entropy_type: 'hd', chain_type: 'ethereum', address: '', private_key: '', index: 0 } }); ``` You can optionally assign an owner, policies, or additional signers at import time: ```tsx theme={"system"} const wallet = await privy.wallets().import({ wallet: { entropy_type: 'private-key', chain_type: 'ethereum', address: '', private_key: '' }, owner_id: '', policy_ids: [''], additional_signers: [{signer_id: ''}] }); ``` ### REST API alternative If you're not using Node.js, use the REST API directly. The flow is three steps: 1. Initialize — call `/v1/wallets/import/init` to get an encryption public key: ```bash theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/import/init \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "address": "", "chain_type": "ethereum", "entropy_type": "private-key", "encryption_type": "HPKE" }' ``` 2. Encrypt — encrypt the private key using HPKE with the returned public key (KEM: `DHKEM_P256_HKDF_SHA256`, KDF: `HKDF_SHA256`, AEAD: `CHACHA20_POLY1305`). 3. Submit — call `/v1/wallets/import/submit` with the encrypted key: ```bash theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/import/submit \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "wallet": { "address": "", "chain_type": "ethereum", "entropy_type": "private-key", "encryption_type": "HPKE", "ciphertext": "", "encapsulated_key": "" } }' ``` See the [Privy key import docs](https://docs.privy.io/wallets/wallets/import-a-wallet/hd-wallets) for the full encryption example and HD wallet variant. ### 5d. Mark migration complete ```tsx theme={"system"} await db.users.update(userId, {signer_migrated: true}); ``` *** ## Step 6: Deploy Unlike the React migration, JWT migration does not require a user export/import step — unless your users also use other auth methods (email, Google, etc.) alongside JWT. If JWT is your only auth method: * Deploy your updated app * Users authenticate via JWT with Privy going forward * Migration happens automatically on each user's first login * No Alchemy dashboard export needed If you also use other auth methods: * You still need to export users from Alchemy and import into Privy (see [React migration guide Step 4](https://docs.privy.io/recipes/migrating-embedded-wallets-from-alchemy#step-4-deploy-the-updated-app-export-users-and-import-into-privy)) * Follow the same Deploy → Export → Import sequence ### Next steps After migration, follow the Privy guides for using embedded wallets: * [Send a transaction](https://docs.privy.io/wallets/using-wallets/ethereum/send-a-transaction) (generic wallet usage) * [Create authorization keys](https://docs.privy.io/controls/authorization-keys/keys/create/user/request) (for server-side usage of wallets) * [Prepare smart wallet operations](https://www.alchemy.com/docs/wallets/api-reference/smart-wallets/wallet-api-endpoints/wallet-api-endpoints/wallet-prepare-calls) * [Sign smart wallet operations](https://docs.privy.io/api-reference/wallets/ethereum/eth-sign-user-operation) ## Important notes ### No user export/import needed (JWT-only) Because JWT authentication is controlled by your server, both Alchemy and Privy can verify the same JWT. There's no user metadata to transfer — your server is the source of truth for user identity. The only thing being migrated is the private key material. ### Security considerations * Client-side path: The private key is briefly available in client memory during the export→import handoff. This is different from the React SDK which uses encrypted TEE-to-TEE transfer. * Server-side path: The key leaves the client encrypted against your server's public key, is decrypted briefly on your server, then re-encrypted with HPKE by `@privy-io/node` before being sent to Privy's TEE. The plaintext is only briefly available on your server, never over the wire. ### Migration tracking is on you The [React migration SDK](https://docs.privy.io/recipes/migrating-embedded-wallets-from-alchemy) detects migration need automatically via Privy metadata. In the JWT path, you own this logic entirely via your user database. ## FAQ ## Can I use the React migration SDK with JWT auth? The React migration SDK supports standard auth methods (email, Google, passkeys). If your users authenticated exclusively via JWT, use this guide instead. If you have a mix of JWT and standard auth users, you may need both paths. ## Are JWTs one-time use? No. JWTs are reusable until they expire. The same JWT can authenticate with both Alchemy and Privy in the same session. ## What if my JWT has a short expiration? Ensure the JWT is valid for the duration of the migration flow (authenticate with both providers + export + import). If your JWTs expire quickly, generate a fresh one at the start of the migration flow. ## Do I need to change my JWT signing/verification setup? You'll need to configure Privy to accept your JWTs (JWKS endpoint or public key, plus the user ID claim). Your JWT generation on the server side stays the same. ## Which path should I choose — client-side or server-side? If you already have a React or React Native app with Alchemy's client SDK, the client-side path is the most straightforward — it mirrors the flow you already have. If your architecture is backend-driven (e.g., your server holds keys or controls wallet operations), the server-side path keeps everything on the server without requiring a client SDK integration. # Exporting wallet keys from mobile apps Source: https://docs.privy.io/recipes/mobile-key-export Privy's `exportWallet` method requires a secure browser context and therefore can only be executed through the React SDK. Mobile apps built with React Native, Swift, Android, or Flutter can support key export by opening a hosted web page in a WebView. The user logs in on the hosted page and exports their key. ## How it works 1. The user taps "Export key" (or equivalent) in the native app. 2. The app opens the hosted export page URL in a WebView. 3. On the hosted page, the user logs in with Privy (email, OAuth, etc.) if they are not already logged in. 4. After logging in, the user taps "Export wallet" on the page, which calls `exportWallet()` and shows the export modal. 5. When export completes (or fails), the page posts a JSON result back to the native app via a messaging bridge. ## Prerequisites * Privy integrated into the mobile app with users authenticating and embedded wallets created. See the quickstart for [React Native](/basics/react-native/quickstart), [Swift](/basics/swift/quickstart), [Android](/basics/android/quickstart), or [Flutter](/basics/flutter/quickstart). * Familiarity with [exporting wallets](/wallets/wallets/export) via the React SDK. ## 1. Build the export web page Create a hosted web page where the user can log in with Privy (if needed) and then tap a button to export their wallet key. The page uses the Privy React SDK and posts the result back to the native app via a messaging bridge. ```html index.html theme={"system"} Export wallet
``` ```tsx App.tsx theme={"system"} import React, {useCallback} from 'react'; import ReactDOM from 'react-dom/client'; import {PrivyProvider, usePrivy} from '@privy-io/react-auth'; function postExportResult(message: object) { const json = JSON.stringify(message); if (typeof window.ReactNativeWebView !== 'undefined') { window.ReactNativeWebView.postMessage(json); } if (window.webkit?.messageHandlers?.exportResult) { window.webkit.messageHandlers.exportResult.postMessage(json); } if (typeof (window as any).AndroidBridge?.onExportResult === 'function') { (window as any).AndroidBridge.onExportResult(json); } // Flutter WebView (webview_flutter: window.exportResult.postMessage) const w = window as any; if (typeof w.exportResult?.postMessage === 'function') { w.exportResult.postMessage(json); } } function ExportPage() { const {ready, authenticated, login, exportWallet} = usePrivy(); const handleExport = useCallback(() => { exportWallet() .then(() => postExportResult({status: 'success'})) .catch((error) => postExportResult({status: 'error', error: String(error)})); }, [exportWallet]); if (!ready) return
Loading...
; if (!authenticated) { return (

Log in to export your wallet key.

); } return (

Export your wallet key to copy it to another wallet.

); } ReactDOM.createRoot(document.getElementById('root')!).render( ); ``` ```tsx App.tsx (Solana) theme={"system"} import React, {useCallback} from 'react'; import ReactDOM from 'react-dom/client'; import {PrivyProvider, usePrivy} from '@privy-io/react-auth'; import {useExportWallet} from '@privy-io/react-auth/solana'; function postExportResult(message: object) { const json = JSON.stringify(message); if (typeof window.ReactNativeWebView !== 'undefined') { window.ReactNativeWebView.postMessage(json); } if (window.webkit?.messageHandlers?.exportResult) { window.webkit.messageHandlers.exportResult.postMessage(json); } if (typeof (window as any).AndroidBridge?.onExportResult === 'function') { (window as any).AndroidBridge.onExportResult(json); } const w = window as any; if (typeof w.exportResult?.postMessage === 'function') { w.exportResult.postMessage(json); } } function ExportSolanaPage() { const {ready, authenticated, login} = usePrivy(); const {exportWallet} = useExportWallet(); const handleExport = useCallback(() => { exportWallet() .then(() => postExportResult({status: 'success'})) .catch((error) => postExportResult({status: 'error', error: String(error)})); }, [exportWallet]); if (!ready) return
Loading...
; if (!authenticated) { return (

Log in to export your wallet key.

); } return (

Export your Solana wallet key to copy it to another wallet.

); } ReactDOM.createRoot(document.getElementById('root')!).render( ); ```
Host this page on a domain listed in the app's allowed origins. Configure allowed origins in the [Privy Dashboard](https://dashboard.privy.io). The hosted page must use the same `appId` as the mobile app so that login and export work correctly for your app's users. ## 2. Load the WebView in the native app When the user taps "Export key", open the hosted export page URL in a WebView. Use incognito or non-persistent storage so that login and export data are not cached. ```tsx theme={"system"} import React, {useState, useRef} from 'react'; import {View, Modal, Button} from 'react-native'; import {WebView} from 'react-native-webview'; const EXPORT_PAGE_URL = 'https://your-domain.com/export'; function ExportWalletScreen() { const [visible, setVisible] = useState(false); const webViewRef = useRef(null); const openExport = () => setVisible(true); const handleMessage = (event: any) => { const data = JSON.parse(event.nativeEvent.data); if (data.status === 'success' || data.status === 'error') { setVisible(false); } }; return ( ; } ``` **Custom SMS enrollment:** ```tsx theme={"system"} import {useMfaEnrollment} from '@privy-io/react-auth'; const {initEnrollmentWithSms, submitEnrollmentWithSms} = useMfaEnrollment(); // Send enrollment code await initEnrollmentWithSms({phoneNumber: phoneNumber}); // Submit code to complete enrollment await submitEnrollmentWithSms({ phoneNumber: phoneNumber, mfaCode: mfaCode, }); ``` Learn more about [MFA enrollment](/authentication/user-authentication/mfa/default-ui). Create a policy that defines which transactions can be executed without MFA. For example, allow USDC transfers under 1000 USDC without MFA. Policies can also be created in the [Dashboard](https://dashboard.privy.io/apps?page=policies). When creating via the Dashboard, you'll receive a policy ID that you can reference in your code. Alternatively, you can create a policy programmatically using the [NodeJS SDK](/controls/policies/create-a-policy): ```tsx theme={"system"} import { PrivyClient } from "@privy-io/node"; import { erc20Abi, parseUnits } from "viem"; const USDC_SEPOLIA_ADDRESS = "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238"; const privy = new PrivyClient({ appId: PRIVY_APP_ID, appSecret: PRIVY_APP_SECRET, }); const policy = await privy.policies().create({ name: "USDC Transfer Policy", version: "1.0", chain_type: "ethereum", rules: [ { name: "USDC Transfer Policy", method: "eth_sendTransaction", action: "ALLOW", conditions: [ { field_source: "ethereum_transaction", field: "to", operator: "eq", value: USDC_SEPOLIA_ADDRESS, }, { field_source: "ethereum_calldata", field: "transfer.amount", abi: erc20Abi, operator: "lte", value: parseUnits("1000", 6).toString(), // 1000 USDC } ] }, ] }); ``` This policy allows transactions to the USDC contract with transfer amounts up to 1000 USDC. Transactions that satisfy this policy can be signed without MFA. Save the `policy.id` as you'll need it to attach the policy to a signer in the next step. Learn more about [creating policies](/controls/policies/create-a-policy). Create an authorization key in the Dashboard, then add it as a signer to the user's wallet with the policy you created: **1. Create an authorization key:** Go to the [Dashboard](https://dashboard.privy.io/apps?page=authorization-keys) and create a new authorization key. Save the private key securely. **2. Add the signer to the wallet:** ```tsx theme={"system"} import {usePrivy, useSigners} from '@privy-io/react-auth'; const { user } = usePrivy(); const { addSigners } = useSigners(); async function addSigner() { if (user && user.wallet && user.wallet.walletClientType === "privy") { await addSigners({ address: user.wallet.address, signers: [ { signerId: "", // Authorization key ID policyIds: [""], // Policy ID } ] }); } } ``` Learn more about [adding signers](/wallets/using-wallets/signers/add-signers). On your server, create an endpoint to send transactions using your authorization key: ```tsx theme={"system"} import { NextResponse, NextRequest } from "next/server"; import { PrivyClient } from "@privy-io/node"; import { AUTHORIZATION_KEY_SECRET } from "@/constants"; export async function POST(request: NextRequest) { const { walletId, transaction } = await request.json(); const privy = new PrivyClient({ appId: PRIVY_APP_ID, appSecret: PRIVY_APP_SECRET, }); const result = await privy.wallets().ethereum().sendTransaction(walletId, { caip2: 'eip155:11155111', params: { transaction, }, authorization_context: { authorization_private_keys: [AUTHORIZATION_KEY_SECRET], }, }); return NextResponse.json(result); } ``` **On the client, route transactions based on whether they satisfy the policy:** ```tsx theme={"system"} import {usePrivy, useSendTransaction} from '@privy-io/react-auth'; import {encodeFunctionData, parseUnits, erc20Abi} from 'viem'; import {sepolia} from 'viem/chains'; const USDC_SEPOLIA_ADDRESS = "0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238"; const { user } = usePrivy(); const { sendTransaction } = useSendTransaction(); const handleSignTransaction = async () => { if (!user || !user.wallet) { return; } const transaction = { to: USDC_SEPOLIA_ADDRESS, data: encodeFunctionData({ abi: erc20Abi, functionName: "transfer", args: [ "0xE3070d3e4309afA3bC9a6b057685743CF42da77C", parseUnits(amount, 6), ], }), chain_id: sepolia.id, }; // If transaction satisfies policy (USDC transfer under 1000), sign with additional signer that does not require MFA if ( transaction.to === USDC_SEPOLIA_ADDRESS && parseUnits(amount, 6) < parseUnits("1000", 6) ) { const response = await fetch("/api/sign-with-signer", { method: "POST", body: JSON.stringify({ walletId: user.wallet.id, transaction, }), }); const result = await response.json(); return result; } // Otherwise, use sendTransaction which requires MFA const result = await sendTransaction(transaction, { uiOptions: { showWalletUIs: false, }, }); return result; }; ``` Transactions that satisfy the policy (USDC transfers under 1000) are signed by your authorization key without prompting the user for MFA. All other transactions require MFA. ## Summary With policy-based MFA, you can: * **Enable MFA** for additional security on user wallets * **Create policies** that define transaction limits and conditions * **Add signers** with scoped permissions to execute policy-approved transactions * **Route transactions** based on whether they satisfy policy conditions, requiring MFA only when necessary This approach provides a balance between security and user experience, reducing friction for transactions within policy limits while maintaining strong protection for transactions outside those limits. # Polymarket builder codes Source: https://docs.privy.io/recipes/polymarket-guide Polymarket's Builder program enables apps to earn fees on trades they route through the platform. This guide demonstrates how to integrate Polymarket trading using Privy for authentication and embedded wallet provisioning—giving your users a seamless, gasless trading experience with web2-style onboarding. The world's largest prediction market platform. Official builder program documentation. Official Next.js example with Privy authentication and Polymarket trading. ## Getting started Sign up at [dashboard.privy.io](https://dashboard.privy.io) and create an app. Obtain your builder API key, secret, and passphrase from [Polymarket](https://polymarket.com/settings?tab=builder). Use any Polygon mainnet RPC provider (Alchemy, Infura, or a public RPC). ### Setup To get started, you'll need to set up: * **Privy authentication** — Handles login and provisions embedded wallets * **Builder signing endpoint** — Keeps your builder credentials secure server-side * **Polymarket clients** — RelayClient for Safe operations, ClobClient for trading Clone the example: ```bash theme={"system"} git clone https://github.com/Polymarket/privy-safe-builder-example.git cd privy-safe-builder-example npm install ``` Add your credentials to `.env.local`: ```bash theme={"system"} NEXT_PUBLIC_POLYGON_RPC_URL=your_rpc_url NEXT_PUBLIC_PRIVY_APP_ID=your_privy_app_id POLYMARKET_BUILDER_API_KEY=your_builder_api_key POLYMARKET_BUILDER_SECRET=your_builder_secret POLYMARKET_BUILDER_PASSPHRASE=your_builder_passphrase ``` Run `npm run dev` and open [localhost:3000](http://localhost:3000). ## Integration steps ### Step 1: Set up authentication First, wrap your app with `PrivyProvider` and use Privy's hooks to handle login. When users authenticate, Privy automatically provisions an embedded wallet: ```tsx theme={"system"} import {PrivyProvider, usePrivy, useWallets} from '@privy-io/react-auth'; function App() { const {login, authenticated} = usePrivy(); const {wallets} = useWallets(); const embeddedWallet = wallets.find((w) => w.walletClientType === 'privy'); return ( ); } ``` ### Step 2: Configure the builder signing endpoint Next, set up a server-side API route to keep your builder credentials secure. The example includes this at [`app/api/polymarket/sign/route.ts`](https://github.com/Polymarket/privy-safe-builder-example/blob/main/app/api/polymarket/sign/route.ts). This endpoint generates HMAC signatures for the `RelayClient` and `ClobClient`: ```typescript {skip-check} theme={"system"} import {NextRequest, NextResponse} from 'next/server'; import {BuilderApiKeyCreds, buildHmacSignature} from '@polymarket/builder-signing-sdk'; const BUILDER_CREDENTIALS: BuilderApiKeyCreds = { key: process.env.POLYMARKET_BUILDER_API_KEY!, secret: process.env.POLYMARKET_BUILDER_SECRET!, passphrase: process.env.POLYMARKET_BUILDER_PASSPHRASE! }; export async function POST(request: NextRequest) { const body = await request.json(); const {method, path, body: requestBody} = body; const sigTimestamp = Date.now().toString(); const signature = buildHmacSignature( BUILDER_CREDENTIALS.secret, parseInt(sigTimestamp), method, path, requestBody ); return NextResponse.json({ POLY_BUILDER_SIGNATURE: signature, POLY_BUILDER_TIMESTAMP: sigTimestamp, POLY_BUILDER_API_KEY: BUILDER_CREDENTIALS.key, POLY_BUILDER_PASSPHRASE: BUILDER_CREDENTIALS.passphrase }); } ``` This reference implementation exposes builder credentials to the client. For production, implement a proxy pattern where the server makes all CLOB/Relay requests, or add auth token validation before returning credentials. ### Step 3: Deploy a Safe wallet Polymarket uses Gnosis Safe wallets for trading. The [`hooks/useSafeDeployment.ts`](https://github.com/Polymarket/privy-safe-builder-example/blob/main/hooks/useSafeDeployment.ts) hook handles this. First, derive the Safe address from the user's EOA (this is deterministic—the same EOA always gets the same Safe): ```typescript {skip-check} theme={"system"} import {deriveSafe} from '@polymarket/builder-relayer-client/dist/builder/derive'; import {getContractConfig} from '@polymarket/builder-relayer-client/dist/config'; import {POLYGON_CHAIN_ID} from '@/constants/polymarket'; const config = getContractConfig(POLYGON_CHAIN_ID); const safeAddress = deriveSafe(eoaAddress, config.SafeContracts.SafeFactory); ``` Then check if the Safe exists and deploy it if needed: ```typescript {skip-check} theme={"system"} import {RelayClient, RelayerTransactionState} from '@polymarket/builder-relayer-client'; async function deploySafe(relayClient: RelayClient): Promise { // Prompts signer for a signature const response = await relayClient.deploy(); // Poll until the transaction is mined/confirmed (60s timeout, 3s interval) const result = await relayClient.pollUntilState( response.transactionID, [ RelayerTransactionState.STATE_MINED, RelayerTransactionState.STATE_CONFIRMED, RelayerTransactionState.STATE_FAILED ], '60', 3000 ); return result.proxyAddress; } ``` The Safe holds the user's USDC.e and outcome tokens. Deployment is gasless—Polymarket's relayer covers the cost. ### Step 4: Get user API credentials User API Credentials authenticate requests to the CLOB. The [`hooks/useUserApiCredentials.ts`](https://github.com/Polymarket/privy-safe-builder-example/blob/main/hooks/useUserApiCredentials.ts) hook handles this. For new users, create credentials: ```typescript {skip-check} theme={"system"} import {ClobClient} from '@polymarket/clob-client'; import {CLOB_API_URL, POLYGON_CHAIN_ID} from '@/constants/polymarket'; import {useWallet} from '@/providers/WalletContext'; const {eoaAddress, ethersSigner} = useWallet(); const tempClient = new ClobClient(CLOB_API_URL, POLYGON_CHAIN_ID, ethersSigner); const creds = await tempClient.createApiKey(); // Prompts for signature ``` For returning users, derive existing credentials: ```typescript {skip-check} theme={"system"} const creds = await tempClient.deriveApiKey(); // Prompts for signature ``` Both methods require the user to sign an EIP-712 message. The example stores credentials in localStorage for convenience, but production apps should use secure httpOnly cookies or server-side session management. ### Step 5: Set token approvals Before trading, the Safe must approve Polymarket's contracts. The [`hooks/useTokenApprovals.ts`](https://github.com/Polymarket/privy-safe-builder-example/blob/main/hooks/useTokenApprovals.ts) hook batches all approvals into a single transaction. **USDC.e (ERC-20) approvals for:** * CTF Contract: `0x4d97dcd97ec945f40cf65f87097ace5ea0476045` * CTF Exchange: `0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E` * Neg Risk Exchange: `0xC5d563A36AE78145C45a50134d48A1215220f80a` * Neg Risk Adapter: `0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296` **Outcome token (ERC-1155) approvals for:** * CTF Exchange: `0x4bFb41d5B3570DeFd03C39a9A4D8dE6Bd8B8982E` * Neg Risk Exchange: `0xC5d563A36AE78145C45a50134d48A1215220f80a` * Neg Risk Adapter: `0xd91E80cF2E7be2e162c6513ceD06f1dD0dA35296` ```typescript {skip-check} theme={"system"} const approvalStatus = await checkAllApprovals(safeAddress); if (!approvalStatus.allApproved) { const approvalTxs = createAllApprovalTxs(); const response = await relayClient.execute(approvalTxs, 'Set token approvals'); await response.wait(); } ``` Approvals persist across sessions—users only sign once. All approval transactions are gasless via the relayer. ### Step 6: Place an order With the session initialized, place orders through the CLOB client: ```typescript {skip-check} theme={"system"} const order = { tokenID: '0x...', // Outcome token from market price: 0.65, // 65 cents size: 10, // 10 shares side: 'BUY', feeRateBps: 0, expiration: 0, // Good-til-cancel taker: '0x0000000000000000000000000000000000000000' }; const response = await clobClient.createAndPostOrder(order, {negRisk: false}, OrderType.GTC); console.log('Order ID:', response.orderID); ``` Orders are signed by the user's Privy EOA and executed from their Safe address. Builder attribution is automatic via the builder config. ## Troubleshooting Verify `NEXT_PUBLIC_PRIVY_APP_ID` is set correctly. Check that your domain is allowed in the Privy dashboard. Check builder credentials in `.env.local`. Verify the `/api/polymarket/sign` endpoint is accessible. Ensure the Polygon RPC URL is valid. The user must approve the signature via Privy. The Safe must be deployed first. Verify the builder relay service is operational. Verify the trading session is complete and the Safe has a USDC.e balance. Wait 2-3 seconds for CLOB sync. ## Resources Order book API for placing and managing orders. Gasless Safe deployment and approvals. # Pregenerating wallets Source: https://docs.privy.io/recipes/pregenerate-wallets With Privy, you can **pregenerate non-custodial Ethereum and Solana wallets** for existing users, or create a new user with other login methods, like an email address or phone number, without requiring the user to login. You can even send assets to the wallet before the user logs in to your app for the first time. Once the user associated with the account logs in, they will be able to access the pregenerated wallet and any assets sent to them. Wallet pregeneration is useful for: * **Airdrops and rewards**: Distribute tokens or NFTs to users before they sign up * **Web2 to web3 migrations**: Import existing users from a database and provision wallets for them * **Frictionless onboarding**: Allow users to receive assets before creating an account * **Server-side provisioning**: Generate wallets programmatically based on off-chain events or criteria This recipe covers three scenarios: creating wallets for new users, adding wallets to existing users, and batch importing users with wallets. ## Pregenerating wallets for new users Create new users with pregenerated wallets in a single operation. This is useful for airdrops, migrations, or any scenario where wallets need to exist before authentication. To pregenerate embedded wallets for a new user, use the `create` method on the `users()` interface of the Privy client. ### Usage ```tsx [expandable] theme={"system"} const privy = new PrivyClient({ appId: 'your-app-id', appSecret: 'your-app-secret' }); const user = await privy.users().create({ linked_accounts: [ { type: 'email', address: 'batman@privy.io', }, ], wallets: [ { chain_type: 'ethereum', wallet_index: 0, additional_signers: [ { signer_id: '', // Policies specific to this signer override_policy_ids: [''] } ], // Policies for all transactions on the wallet, no matter the signer policy_ids: [''] }, { chain_type: 'solana', wallet_index: 0, additional_signers: [ { signer_id: '', override_policy_ids: [''] } ], policy_ids: [] } ], create_direct_signer: true }); ``` ### Params and returns Check out the [API reference](/api-reference/users/create) for more details. To pregenerate embedded wallets for a new user, use the `createUser` method. Take a look at the [guide on creating users](/user-management/migrating-users-to-privy/create-or-import-a-user) for more information on creating users with particular linked accounts and metadata before they sign in to the application. ```java theme={"system"} try { List createUserLinkedAccounts = List.of(/* Some linked accounts... */); // Pregenerate an Ethereum and Solana wallet for the user List wallets = List.of( UserWalletRequest.builder() .chainType(WalletChainType.ETHEREUM) .walletIndex(0) .build(), UserWalletRequest.builder() .chainType(WalletChainType.SOLANA) .walletIndex(0) .build() ); UserCreateRequestBody requestBody = UserCreateRequestBody.builder() .linkedAccounts(createUserLinkedAccounts) .wallets(wallets) .createDirectSigner(true) .build(); UserCreateResponse response = privyClient.users().create(requestBody); // Check if the user was created successfully. // The application can now use the address of their generated wallet. if (response.user().isPresent()) { User user = response.user().get(); // Extract Ethereum wallet from User response LinkedAccountEthereumEmbeddedWallet ethereumWallet = user.getFirstLinkedAccountByType( LinkedAccountEthereumEmbeddedWallet.class ); // Grab the generated wallet address String ethereumAddress = ethereumWallet.address(); // Extract Solana wallet from User response LinkedAccountSolanaEmbeddedWallet solanaWallet = user.getFirstLinkedAccountByType( LinkedAccountSolanaEmbeddedWallet.class ); // Grab the generated wallet address String solanaAddress = solanaWallet.address(); } } catch (APIException e) { String errorBody = e.bodyAsString(); System.err.println(errorBody); } catch (Exception e) { System.err.println(e.getMessage()); } ``` To pregenerate embedded wallets for a new user, make a POST request to `https://auth.privy.io/api/v1/users`. ### Usage Below is a sample cURL command for creating a new user with pregenerated wallets: ```sh theme={"system"} $ curl --request POST https://auth.privy.io/api/v1/users \ -u ":" \ -H "privy-app-id: " \ -H "Content-Type: application/json" \ -d '{ "linked_accounts": [ { "address": "batman@privy.io", "type": "email" } ], "wallets": [ { "chain_type": "ethereum", "wallet_index": 0, "policy_ids": [""], "additional_signers": [ { "signer_id": "", "override_policy_ids": [""] } ] }, { "chain_type": "solana", "wallet_index": 0, "additional_signers": [ { "signer_id": "", "override_policy_ids": [""] } ] } ], "create_direct_signer": true }' ``` A successful response will include the new user object along with their user ID and embedded wallet addresses: ```json theme={"system"} { "id": "did:privy:clddy332f002tyqpq3b3lv327", "created_at": 1674788927, "linked_accounts": [ { "address": "batman@privy.io", "type": "email" }, { "address": "0x3DAF84b3f09A0E2092302F7560888dBc0952b7B7", "type": "wallet", "wallet_client": "privy", "chain_type": "ethereum" }, { "address": "9KnvxKTx...", "type": "wallet", "wallet_client": "privy", "chain_type": "solana" } ] } ``` ### Parameters An array of linked accounts to associate with the user. Custom metadata to associate with the user. An array of wallets to create for the user. The chain type of the wallet to create. The HD wallet index to use for wallet generation. Defaults to `0`. List of policy IDs for policies that should be enforced on all transactions on the wallet, regardless of signer. The ID of the signer. List of policy IDs for policies specific to this signer. Currently, only one policy is supported per wallet. Set to `true` to create a smart account with the user's wallet as the signer. Can only be set on wallets where `chain_type` is `ethereum`. Set to `true` to create a UserSigner for custom JWT authentication for all wallets. Requires a custom JWT linked account. ## Creating wallets for existing users Add additional embedded wallets to users who already have Privy accounts. This is useful when expanding to support new chains or creating wallets based on in-app actions. To create embedded wallets for an existing user, use the `pregenerateWallets` method from the `users()` interface of the Privy client. ### Usage ```tsx theme={"system"} const user = await privy.users().pregenerateWallets('did:privy:clddy332f002tyqpq3b3lv327', { wallets: [ { chain_type: 'ethereum', wallet_index: 0, }, { chain_type: 'solana', wallet_index: 0, } ], create_direct_signer: true }); ``` Check out the [API reference](/api-reference/users/pregenerate-wallets) for more details. To pregenerate wallets for a user with the Go SDK, use the `PregenerateWallets` method on the `Users` service. ### Usage ```go theme={"system"} user, err := client.Users.PregenerateWallets( context.Background(), "did:privy:xxxxx", privy.UserPregenerateWalletsParams{ Wallets: []privy.UserPregenerateWalletsParamsWallet{{ ChainType: privy.WalletChainTypeEthereum, AdditionalSigners: []privy.UserPregenerateWalletsParamsWalletAdditionalSigner{{ SignerID: "signer_id", OverridePolicyIDs: []string{"string"}, }}, CreateSmartWallet: privy.Bool(true), PolicyIDs: []string{"string"}, }}, }, ) if err != nil { log.Fatalf("failed to pregenerate wallets: %v", err) } fmt.Println("Pregenerated wallets for user:", user.ID) ``` ### Parameters and Returns See the [API reference](/api-reference/users/pregenerate-wallets) for more details. To pregenerate wallets for a user with the Ruby SDK, use the `pregenerate_wallets` method on the `users` service. ### Usage ```ruby theme={"system"} user = client.users.pregenerate_wallets( "did:privy:xxxxx", wallets: [ { chain_type: :ethereum, additional_signers: [ {signer_id: "signer_id", override_policy_ids: ["string"]} ], create_smart_wallet: true, policy_ids: ["string"] } ] ) puts(user.id) ``` ### Parameters and Returns See the [API reference](/api-reference/users/pregenerate-wallets) for more details. To pregenerate embedded wallets for an existing user, make a POST request to `https://api.privy.io/v1/wallets`. ### Usage Below is a sample cURL command for creating wallets for an existing user: ```sh theme={"system"} $ curl --request POST \ --url https://api.privy.io/v1/wallets \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data ' { "chain_type": "ethereum", "owner": { "user_id": "did:privy:clddy332f002tyqpq3b3lv327" }, "policy_ids": [""] "additional_signers": [ { "signer_id": "", "override_policy_ids": [""] } ] } ' ``` A successful response will include the updated user object with their newly created wallet addresses: ```json theme={"system"} { "id": "did:privy:clddy332f002tyqpq3b3lv327", "created_at": 1674788927, "linked_accounts": [ { "address": "batman@privy.io", "type": "email" }, { "address": "0x3DAF84b3f09A0E2092302F7560888dBc0952b7B7", "type": "wallet", "wallet_client": "privy", "chain_type": "ethereum" }, { "address": "9KnvxKTx...", "type": "wallet", "wallet_client": "privy", "chain_type": "solana" } ] } ``` ### Parameters The Privy user ID to create wallets for. An array of wallets to create for the user. The chain type of the wallet to create. The HD wallet index to use for wallet generation. Defaults to `0`. List of policy IDs for policies that should be enforced on all transactions on the wallet, regardless of signer. The ID of the signer. List of policy IDs for policies specific to this signer. Currently, only one policy is supported per wallet. Set to `true` to create a smart account with the user's wallet as the signer. Can only be set on wallets where `chain_type` is `ethereum`. Set to `true` to create a UserSigner for custom JWT authentication for all wallets. Requires a custom JWT linked account. ## Batch importing users with wallets Import multiple users with pregenerated wallets in a single batch operation. This is more efficient than creating users individually when provisioning wallets for hundreds or thousands of users at once. To batch import multiple users with pregenerated wallets, make a POST request to `https://auth.privy.io/api/v1/apps//users/import`. ### Usage Below is a sample cURL command for batch importing users with pregenerated wallets: ```sh theme={"system"} $ curl --request POST https://auth.privy.io/api/v1/apps//users/import \ -u ":" \ -H "privy-app-id: " \ -H "Content-Type: application/json" \ -d '{ "users": [ { "linked_accounts": [ { "address": "user1@example.com", "type": "email" } ], "wallets": [ { "chain_type": "ethereum", "wallet_index": 0, "policy_ids": [""], "additional_signers": [ { "signer_id": "", "override_policy_ids": [""] } ] } ], "create_direct_signer": true }, { "linked_accounts": [ { "address": "user2@example.com", "type": "email" } ], "wallets": [ { "chain_type": "solana", "wallet_index": 0 } ], "create_direct_signer": false } ] }' ``` A successful response will include an array of the newly created users with their wallet addresses: ```json theme={"system"} { "users": [ { "id": "did:privy:abc123", "created_at": 1674788927, "linked_accounts": [ { "address": "user1@example.com", "type": "email" }, { "address": "0x3DAF84b3f09A0E2092302F7560888dBc0952b7B7", "type": "wallet", "wallet_client": "privy", "chain_type": "ethereum" } ] }, { "id": "did:privy:def456", "created_at": 1674788928, "linked_accounts": [ { "address": "user2@example.com", "type": "email" }, { "address": "9KnvxKTx...", "type": "wallet", "wallet_client": "privy", "chain_type": "solana" } ] } ] } ``` ### Parameters An array of user objects to import. Each object has the following fields: An array of linked accounts to associate with the user. Custom metadata to associate with the user. An array of wallets to create for the user. The chain type of the wallet to create. The HD wallet index to use for wallet generation. Defaults to `0`. List of policy IDs for policies that should be enforced on all transactions on the wallet, regardless of signer. The ID of the signer. List of policy IDs for policies specific to this signer. Currently, only one policy is supported per wallet. Set to `true` to create a smart account with the user's wallet as the signer. Can only be set on wallets where `chain_type` is `ethereum`. Set to `true` to create a UserSigner for custom JWT authentication for all wallets. Requires a custom JWT linked account. ## What happens when users log in When users log in for the first time after wallets have been pregenerated, the wallets automatically appear in their account. Users can immediately access these wallets and any assets that were sent to them. These wallets work identically to wallets created during login. Users can sign transactions, view balances, and manage their assets through the standard application interface. # Clearing state on fresh installs Source: https://docs.privy.io/recipes/react-native/clearing-state-on-fresh-installs The React Native SDK for Privy stores session state in the Expo Secure Store, powered by the Keychain on iOS. This means that when a user reinstalls your app, Privy is able to restore their existing session. This is great for user experience, and is standard behavior for iOS apps throughout. However, there are some cases where you may want to clear the state on a fresh install. ## Log out the user on a fresh install To do this, you should keep a flag in your app's storage, one that you can check to see if the app has been reinstalled, and set it on first launch. First, install the `@react-native-async-storage/async-storage` package, as a way to store the flag in your app's storage in a way that it does not persist across app reinstalls. ```sh theme={"system"} npx expo install @react-native-async-storage/async-storage ``` Then, in your app, you can check for a fresh install and clear the state on a fresh install. Make sure to run this code in the root of your app, so it runs as soon as the app is launched, like so: ```ts theme={"system"} import {useEffect} from 'react'; import AsyncStorage from '@react-native-async-storage/async-storage'; import {usePrivy} from '@privy-io/expo'; // ... const {logout} = usePrivy(); useEffect(() => { const checkForFreshInstall = async () => { const hasLaunchedBefore = await AsyncStorage.getItem('hasLaunchedBefore'); if (!hasLaunchedBefore) { await AsyncStorage.setItem('hasLaunchedBefore', 'true'); await logout(); } }; checkForFreshInstall(); }, [logout]); ``` This will clear the state on a fresh install, but ensure state is restored on subsequent launches. ### My app is already in production If your application is already deployed in production, note that this will also **log out every user the first time** they launch after this update, as if it were a fresh install. This can be circumvented by doing an intermediate release first, that includes the `setItem` logic only, but not the `logout` call yet. ```ts theme={"system"} import AsyncStorage from '@react-native-async-storage/async-storage'; const checkForFreshInstall = async () => { const hasLaunchedBefore = await AsyncStorage.getItem('hasLaunchedBefore'); if (!hasLaunchedBefore) { await AsyncStorage.setItem('hasLaunchedBefore', 'true'); // No logout in this release, add it in the next release. } }; ``` This ensures that no users are logged out on the first launch after this update, and that they will be ready for the next update. # Deeplinking Solana wallets in React Native applications Source: https://docs.privy.io/recipes/react-native/deeplinking-wallets If your Expo mobile app uses Privy, you can implement wallet deeplinking to allow your users to connect their existing mobile wallets (like Phantom) with a seamless experience. This guide will walk you through the steps to set up wallet deeplinking in your Privy Expo app. ## 0. Setup This guide assumes that you have already integrated Privy into your React native app. If you have not yet set up the basic integration, please first follow the [Privy quickstart](/basics/react-native/quickstart). ## 1. Install required packages First, make sure you have the necessary dependencies: ```bash theme={"system"} npm install @privy-io/expo @privy-io/expo/connectors ``` ## 2. Import the wallet connector hooks Import the wallet connector hooks in your component: ```tsx theme={"system"} import { useDeeplinkWalletConnector, usePhantomDeeplinkWalletConnector, useBackpackDeeplinkWalletConnector } from '@privy-io/expo/connectors'; ``` Privy Expo provides several hooks for wallet deeplinking: | Hook | Description | | ------------------------------------ | --------------------------------------------------------------------------------- | | `useDeeplinkWalletConnector` | A generic wallet deeplinking connector that you can configure for various wallets | | `usePhantomDeeplinkWalletConnector` | A pre-configured connector specifically for Phantom wallet | | `useBackpackDeeplinkWalletConnector` | A pre-configured connector specifically for Backpack wallet | ## 3. Set up the wallet connector To set up the Phantom wallet connector, use the `usePhantomDeeplinkWalletConnector` hook in your component: ```tsx theme={"system"} export default function LoginScreen() { const { address, connect, disconnect, isConnected, signMessage, signTransaction, signAllTransactions, signAndSendTransaction } = usePhantomDeeplinkWalletConnector({ appUrl: 'https://yourdapp.com', redirectUri: '/sign-in' }); // Your component code here } ``` ### Configuration options The `usePhantomDeeplinkWalletConnector` hook accepts the following configuration: | Parameter | Type | Description | | ------------- | -------- | ---------------------------------------------------------------------------------------------------------- | | `appUrl` | `string` | The URL of your app that will be displayed in the wallet app as the requesting dapp, for metadata purposes | | `redirectUri` | `string` | The path in your app that the wallet should redirect to after completing an action | To set up the Backpack wallet connector, use the `useBackpackDeeplinkWalletConnector` hook in your component: ```tsx theme={"system"} export default function LoginScreen() { const { address, connect, disconnect, isConnected, signMessage, signTransaction, signAllTransactions, signAndSendTransaction } = useBackpackDeeplinkWalletConnector({ appUrl: 'https://yourdapp.com', redirectUri: '/sign-in' }); // Your component code here } ``` ### Configuration options The `useBackpackDeeplinkWalletConnector` hook accepts the following configuration: | Parameter | Type | Description | | ------------- | -------- | ---------------------------------------------------------------------------------------------------------- | | `appUrl` | `string` | The URL of your app that will be displayed in the wallet app as the requesting dapp, for metadata purposes | | `redirectUri` | `string` | The path in your app that the wallet should redirect to after completing an action | If you want to integrate with wallets other than Phantom or Backpack, you can use the generic `useDeeplinkWalletConnector` hook and configure it for your specific wallet: ```tsx theme={"system"} export default function LoginScreen() { const { address, connect, disconnect, isConnected, signMessage, signTransaction, signAllTransactions, signAndSendTransaction } = useDeeplinkWalletConnector({ // Base URL for the wallet baseUrl: 'https://solflare.com', // The name of the public key used for encryption encryptionPublicKeyName: 'solflare_encryption_public_key', // Other wallet-specific configuration appUrl: 'https://yourdapp.com', redirectUri: '/sign-in' }); // Your component code here } ``` ### Configuration options The generic `useDeeplinkWalletConnector` hook accepts the following wallet-specific configuration parameters: | Parameter | Type | Description | | ------------------------- | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `baseUrl` | `string` | The base URL for the wallet's deeplink protocol. This is typically the wallet's website URL or a custom URL scheme (e.g., `https://solflare.com`). | | `encryptionPublicKeyName` | `string` | The name of the key in localStorage where the wallet stores its encryption public key. This is used for secure communication between your app and the wallet. | | `appUrl` | `string` | The URL of your app that will be displayed in the wallet app as the requesting dapp, for metadata purposes | | `redirectUri` | `string` | The path in your app that the wallet should redirect to after completing an action | Always refer to the wallet provider's documentation for the specific deeplink protocol implementation and required parameters. Each wallet may implement deeplinking differently and require specific configuration values. ## 4. Using the connector in your UI Now you can implement UI components to interact with the wallet: ```tsx theme={"system"} return (

Wallet Deeplinking

Connected: {isConnected ? 'true' : 'false'} {!isConnected && } {isConnected && ( <> Address: {address} )}
); ``` ## 5. Implementing the wallet functions Here are examples of how to implement the handler functions for various wallet actions: ```tsx theme={"system"} // Function to handle message signing const handleSignMsg = async () => { try { const message = 'Hello, Privy Expo!'; const signature = await signMessage(message); console.log('Message signed:', signature); } catch (error) { console.error('Error signing message:', error); } }; // Function to handle transaction signing const handleSignTx = async () => { try { // Create a transaction const transaction = new Transaction().add( SystemProgram.transfer({ fromPubkey: new PublicKey(address), toPubkey: new PublicKey('DESTINATION_ADDRESS'), lamports: LAMPORTS_PER_SOL * 0.01 }) ); const signedTx = await signTransaction(transaction); console.log('Transaction signed:', signedTx); } catch (error) { console.error('Error signing transaction:', error); } }; // Function to handle signing and sending a transaction const handleSignAndSendTx = async () => { try { const transaction = new Transaction().add( SystemProgram.transfer({ fromPubkey: new PublicKey(address), toPubkey: new PublicKey('DESTINATION_ADDRESS'), lamports: LAMPORTS_PER_SOL * 0.01 }) ); const signature = await signAndSendTransaction(transaction); console.log('Transaction sent:', signature); } catch (error) { console.error('Error sending transaction:', error); } }; // Function to handle signing multiple transactions const handleSignAllTxs = async () => { try { const transactions = [ new Transaction().add( SystemProgram.transfer({ fromPubkey: new PublicKey(address), toPubkey: new PublicKey('DESTINATION_ADDRESS_1'), lamports: LAMPORTS_PER_SOL * 0.01 }) ), new Transaction().add( SystemProgram.transfer({ fromPubkey: new PublicKey(address), toPubkey: new PublicKey('DESTINATION_ADDRESS_2'), lamports: LAMPORTS_PER_SOL * 0.01 }) ) ]; const signedTxs = await signAllTransactions(transactions); console.log('Transactions signed:', signedTxs); } catch (error) { console.error('Error signing transactions:', error); } }; ``` The configuration for the generic connector will depend on the specific wallet you're integrating with. Refer to each wallet's documentation for their deeplinking protocol. ## How deeplinking works When a user interacts with your app: 1. Your app initiates a connection request using the `connect()` function 2. The user is directed to their installed wallet app 3. The user approves or denies the action in their wallet 4. The wallet redirects back to your app with the result 5. Your app updates its state based on the wallet's response This flow provides a seamless experience for users, allowing them to interact with your dApp using their preferred wallet without having to switch contexts or manually copy addresses. **That's it! You've successfully integrated wallet deeplinking in your Privy React Native app 🎉** For the best user experience, consider implementing fallbacks for when a user doesn't have the wallet installed. You might prompt them to install the wallet or offer them an alternative login method. # Require MFA on every app session Source: https://docs.privy.io/recipes/react-native/mfa-on-app-session Clear MFA state when your app is backgrounded so users must re-verify on their next session By default, Privy caches the MFA token after verification and skips re-prompting until it expires. Apps that need fresh verification on every launch can clear the cached token whenever the app moves to the background. A trading app, for example, might treat each app open as a new security session. This recipe uses React Native's `AppState` API and Privy's `clear` method from `useMfa` to tie MFA verification to app session lifecycle. ## How it works 1. The user opens the app, authenticates, and proceeds normally. Wallet actions trigger MFA as usual. 2. When the user backgrounds the app, `clear()` invalidates the cached MFA token. 3. When the user returns and performs a wallet action, Privy finds no valid token and prompts for fresh verification. ## Implementation Register an `AppState` listener in a component near the root of your app. When the app leaves the `active` state — backgrounded or inactive — clear the MFA token: ```tsx theme={"system"} import {useEffect} from 'react'; import {AppState} from 'react-native'; import {useMfa} from '@privy-io/expo'; export default function AppSessionMfaGuard() { const {clear} = useMfa(); useEffect(() => { const subscription = AppState.addEventListener('change', (nextState) => { if (nextState !== 'active') { clear(); } }); return () => subscription.remove(); }, [clear]); return null; } ``` Render `` inside `PrivyProvider`, near the root of your app: ```tsx theme={"system"} import {PrivyProvider} from '@privy-io/expo'; import AppSessionMfaGuard from './AppSessionMfaGuard'; export default function App() { return ( {/* rest of your app */} ); } ``` ## Proactively prompting on app resume Wallet actions naturally re-trigger MFA after `clear()`. To prompt immediately on foreground instead, call `prompt()` when the app returns to `active`: ```tsx theme={"system"} import {useEffect, useRef} from 'react'; import {AppState, AppStateStatus} from 'react-native'; import {useMfa} from '@privy-io/expo'; export default function AppSessionMfaGuard() { const {clear, prompt} = useMfa(); const appState = useRef(AppState.currentState); useEffect(() => { const subscription = AppState.addEventListener('change', async (nextState) => { const previousState = appState.current; appState.current = nextState; if (nextState !== 'active') { // App is going to the background — clear the cached MFA token clear(); } else if (previousState !== 'active' && nextState === 'active') { // App is returning to the foreground — proactively prompt for MFA await prompt(); } }); return () => subscription.remove(); }, [clear, prompt]); return null; } ``` `prompt()` no-ops when the user has no MFA methods enrolled. Because `clear()` runs on every background event, there is never a cached token on resume. The user is always prompted. ## Extending the cache duration By default, MFA tokens stay valid for 15 minutes. For a session-based approach, a longer cache prevents mid-session expiry. `clear()` then handles explicit invalidation on background. Configure the MFA token duration in the [Privy Dashboard](https://dashboard.privy.io/admin/mfa-cache-duration). Setting a value of several hours ensures no mid-session re-prompts while the user is active. ## iOS background constraints On iOS, apps that move to the background have limited time before execution suspends. `clear()` makes no network requests and operates only on local state. It completes reliably within iOS background execution limits. # Configure allowed OAuth redirect URLs Source: https://docs.privy.io/recipes/react/allowed-oauth-redirects Similar to allowed domains, you can configure **allowed OAuth redirect URLs** to restrict where users can be redirected after they log in with an external OAuth provider. This is a **security best practice** that prevents users from being redirected to malicious sites with their authentication token. To configure allowed OAuth redirect URLs, navigate to **Configuration > App settings** > **Advanced** on the [dashboard](https://dashboard.privy.io?page=settings\&tab=advanced\&setting=advanced). Add the OAuth providers are allowed to redirect to after authentication. Please note: * The URL must be an exact match for the redirect URL; query params and trailing slashes will error. * The URL must be at a domain listed in allowed domains. * The protocol (`https`) is required. * Wildcards (`*`) are not supported. * If no URLs are listed, users can be redirected to any URL. # Chrome extension authentication Source: https://docs.privy.io/recipes/react/chrome-extension This guide shows you how to implement Privy authentication and wallets in your Chrome extension using Privy's React SDK. Chrome extensions offer a unique application experience for your users, but come with some unique nuances specifically around social login. ## Resources Complete starter repository with Privy authentication and wallet management. ## Set up your Chrome extension project First, create a React app and install Privy: ```bash theme={"system"} npm create react-app my-extension cd my-extension npm install @privy-io/react-auth ``` Create your `manifest.json` file in the `public` directory: ```json theme={"system"} { "manifest_version": 3, "name": "My Extension with Privy", "version": "1.0", "description": "Chrome extension with Privy authentication", "permissions": ["identity"], "host_permissions": ["https://api.privy.io/*"], "action": { "default_popup": "index.html", "default_title": "My Extension" }, "options_page": "options.html", "content_security_policy": { "extension_pages": "script-src 'self'; object-src 'self'; frame-ancestors 'none';" } } ``` The `identity` permission is required for OAuth flows, and `storage` is recommended for persisting user sessions. ### Security Guidelines Below are comprehensive security guidelines for Chrome extensions. You can find more information in the [Chrome extension security documentation](https://developer.chrome.com/docs/extensions/develop/security-privacy/stay-secure). #### Content Security Policy Add a strict CSP to your manifest to prevent code injection and framing attacks. You can see our broader CSP guidance [here](/security/implementation-guide/content-security-policy). ```json theme={"system"} { "content_security_policy": { "extension_pages": "script-src 'self'; object-src 'self'; frame-ancestors 'none';" } } ``` The `frame-ancestors 'none'` directive prevents your extension from being embedded in frames, protecting against clickjacking attacks. #### Minimal permissions Only request permissions your extension actually needs. Limiting permissions reduces attack surface if compromised: ```json theme={"system"} { "permissions": ["identity"], "host_permissions": ["https://api.privy.io/*"] } ``` **Cross-origin fetch() restrictions:** Extensions can only use `fetch()` and `XMLHttpRequest()` to access domains specified in `host_permissions`. If the extension were compromised, it would still only have permission to interact with websites that meet the match pattern. The attacker would only have limited ability to access sites not in this list. ```json theme={"system"} { "host_permissions": ["https://api.privy.io/*", "https://api.yourservice.com/*"] } ``` Remove unused permissions like `tabs`, `activeTab`, or broad host permissions to reduce your extension's attack surface and improve user trust. #### Externally connectable Restrict which external extensions and web pages can communicate with your extension: ```json theme={"system"} { "externally_connectable": { "ids": ["allowedextensionidheredata"], "matches": ["https://yourtrustedsite.com/*"], "accepts_tls_channel_id": false } } ``` Only include trusted sources in `externally_connectable`. This prevents malicious sites from communicating with your extension. #### Web-accessible resources Minimize web-accessible resources as they make your extension detectable and create attack vectors: ```json theme={"system"} { "web_accessible_resources": [ { "resources": ["images/icon.png"], "matches": ["https://yourtrustedsite.com/*"] } ] } ``` Keep web-accessible resources to a minimum. Each exposed resource increases potential attack surface. #### Secure DOM manipulation Avoid `document.write()` and `innerHTML` which can lead to script injection: #### Validate all inputs Always validate and sanitize inputs, especially from content scripts: ```javascript theme={"system"} chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { // Validate sender is from your extension if (sender.id !== chrome.runtime.id) return; // Validate and sanitize request data if (request.action === 'updateUser' && typeof request.userData === 'object') { // Process validated data updateUser(request.userData); } }); ``` Content scripts can be compromised by malicious websites, so treat all messages from content scripts as potentially malicious. *** ## Configure your Privy dashboard You'll get your extension ID after loading the extension in Chrome's developer mode at `chrome://extensions/`. In the [Privy dashboard](https://dashboard.privy.io/apps?setting=domains\&page=settings), configure OAuth settings for your extension: **1. Add allowed origins** Go to **App Settings > Domains** and add: ``` chrome-extension:// ``` **2. (Optional) Configure redirect URLs** Use `chrome.identity.getRedirectURL()` to get the exact redirect URL programmatically. If your extension uses social login, you'll need to configure redirect URLs. In your allowed domains, add the following redirect URL, and additionally in your [allowed redirect URLs](https://dashboard.privy.io/apps?setting=advanced\&page=settings) ``` https://.chromiumapp.org/ ``` *** ## Enabling social login in your extension Chrome extensions can't handle social OAuth flows directly in the popup due to security restrictions. **Social login requires opening either the options page or a popup window.** This provides the full browser context needed for OAuth redirects. Both approaches follow the same flow: 1. User clicks "Sign in with social" in extension 2. Open authentication context (options page or popup window) 3. Privy handles the OAuth flow 4. User is redirected back to the extension authenticated ### Approach 1: Options page **Setup:** Add to your manifest: ```json theme={"system"} {"options_page": "options.html"} ``` **Implementation:** ```tsx theme={"system"} // In your popup component const openOptionsForLogin = () => { chrome.tabs.create({ url: chrome.runtime.getURL('options.html') }); }; ``` ### Approach 2: Popup window **Implementation:** ```tsx theme={"system"} // In your popup component const openAuthWindow = () => { chrome.windows.create({ url: chrome.runtime.getURL('auth.html'), type: 'popup', width: 400, height: 600 }); }; ``` Both approaches use the same authentication logic: You can use the same `AuthComponent` for both approaches - just render it in different HTML files (options.html or auth.html). ```tsx theme={"system"} // src/auth/AuthComponent.tsx import {PrivyProvider, usePrivy, useLogin} from '@privy-io/react-auth'; import {useEffect} from 'react'; const AuthContent = () => { const {authenticated, ready} = usePrivy(); const {login} = useLogin({ onComplete: () => { // Open the extension popup after authentication chrome.tabs.query({active: true, currentWindow: true}, (tabs) => { if (tabs[0]) { // Open the extension popup chrome.action.openPopup(); } }); } }); // Auto-trigger login when opened for authentication useEffect(() => { if (ready && !authenticated) { login(); } }, [authenticated, ready]); return null; }; export const AuthComponent = () => ( ); ``` Redirect the user back to the extension after authentication. ```tsx theme={"system"} onComplete: () => { // Open the extension popup after authentication chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => { if (tabs[0]) { // Open the extension popup chrome.action.openPopup(); } }); }, ``` *** ## That's it! 🎉 You've now implemented Privy authentication in your Chrome extension. *** ## Avoiding Chrome Web Store rejection for remote code The Chrome Web Store prohibits extensions from loading remotely hosted code. By default, `@privy-io/react-auth` bundles dependencies that inject remote scripts at runtime, which can cause your extension to be rejected during review — even if those scripts never execute. ### Remote scripts in the React SDK The React SDK conditionally loads these remote scripts: | Script | Remote URL | When loaded | | -------------------- | ----------------------------------------------- | --------------------------------------- | | Cloudflare Turnstile | `challenges.cloudflare.com/turnstile/v0/api.js` | CAPTCHA enabled with Turnstile provider | | hCaptcha | `js.hcaptcha.com/1/api.js` | CAPTCHA enabled with hCaptcha provider | | Telegram login | `{apiOrigin}/js/telegram-login.js` | Telegram login enabled in app config | ### Step 1: Disable features that load remote scripts In the [Privy dashboard](https://dashboard.privy.io): 1. **Disable CAPTCHA** in [App settings > Advanced](https://dashboard.privy.io/apps?page=settings\&setting=advanced). This prevents the Turnstile and hCaptcha scripts from loading at runtime. 2. **Disable Telegram login** in [Authentication](https://dashboard.privy.io/apps?page=login-methods) if your extension does not need it. This prevents the Telegram login script from loading. ### Step 2: Remove bundled remote-code references Disabling these features prevents the scripts from loading at runtime, but the Chrome Web Store review scans your extension package statically. The CAPTCHA wrapper code may still appear in your bundled output as a separate chunk, even if it is never executed. To remove it from the bundle entirely, alias the underlying CAPTCHA dependencies to empty stubs in your bundler config: ```js theme={"system"} // webpack.config.js module.exports = { resolve: { alias: { '@marsidev/react-turnstile': false, '@hcaptcha/react-hcaptcha': false, }, }, }; ``` Create a stub file at `stubs/empty-captcha.js`: ```js theme={"system"} export default () => null; export const Turnstile = () => null; ``` Then add the aliases: ```js theme={"system"} // vite.config.js import {defineConfig} from 'vite'; import path from 'path'; export default defineConfig({ resolve: { alias: { '@marsidev/react-turnstile': path.resolve(__dirname, 'stubs/empty-captcha.js'), '@hcaptcha/react-hcaptcha': path.resolve(__dirname, 'stubs/empty-captcha.js') } } }); ``` This replaces the CAPTCHA libraries with inert stubs during bundling, so the remote Cloudflare and hCaptcha URLs do not appear anywhere in the extension package. These aliases depend on the internal dependency structure of `@privy-io/react-auth` and may need to be updated when upgrading SDK versions. Verify your built extension does not contain references to `challenges.cloudflare.com` or `js.hcaptcha.com` after each upgrade. *** ## Production considerations Before publishing to the Chrome Web Store: 1. **Remove unnecessary permissions** from manifest 2. **Limit host permissions** to only required domains 3. **Minimize web-accessible resources** to reduce attack surface 4. **Implement strict CSP** with `frame-ancestors 'none'` 5. **Validate all inputs** from content scripts and external sources 6. **Update OAuth configuration** with production URLs in Privy dashboard 7. **Review externally connectable** settings for trusted domains only Chrome extensions with OAuth require Google's review. Document your authentication flow and privacy practices clearly in your Web Store listing. Follow the [Chrome Web Store security best practices](https://developer.chrome.com/docs/extensions/develop/security-privacy/stay-secure) for faster approval. # Configuring external connectors Source: https://docs.privy.io/recipes/react/configuring-external-connectors Privy supports connecting external wallet on both EVM networks (e.g. MetaMask, Rainbow) and Solana (e.g. Phantom, Solflare) to your application to request signatures and transactions. ## Configuring connectors To connect external wallets on EVM networks, there is **no additional configuration** required for your app. Simply continue with the instructions below to prompt connections to external EVM wallets, like MetaMask or Coinbase Wallet. To connect external wallets on Solana, your application must first explicitly **configure Solana connectors** for Privy. To do so: 1. Import and configure the `toSolanaWalletConnectors` function from `@privy-io/react-auth/solana` 2. Enable `'solana-only'` or `'ethereum-and-solana'` as the `config.appearance.walletChainType` prop of your `PrivyProvider` You do not need to configure `config.solana.rpcs` for external wallets. RPC clients under `solana.rpcs` are only required when using Privy's embedded wallet UIs (UI `signTransaction` and `signAndSendTransaction`). As an example, you might set up your `PrivyProvider` like so: ```tsx theme={"system"} import {toSolanaWalletConnectors} from '@privy-io/react-auth/solana'; import {createSolanaRpc, createSolanaRpcSubscriptions} from '@solana/kit'; const solanaConnectors = toSolanaWalletConnectors({ // By default, shouldAutoConnect is enabled shouldAutoConnect: true }); const Provider: React.FC = ({children}) => { return ( {children} ); }; ``` Note that some Solana wallet connectors do not gracefully support `autoConnect`, which will result in an extension pop-up on page load. If you would like to disable this feature, set it to `false` in the `toSolanaWalletConnectors` function. ## Connecting the wallet To prompt a user to connect an external wallet (on EVM networks or Solana) to your app, use Privy's **`connectWallet`** method: ```tsx theme={"system"} import {usePrivy} from '@privy-io/react-auth'; const {connectWallet} = usePrivy(); ``` This method will prompt the user to select the wallet they want to connect, and will show users EVM and/or Solana external wallet options based off of the `config.appearance.walletChainType` configured in your app's `PrivyProvider`. You can prompt users to connect as many wallets as you'd like to your app. For example, you might have a "Connect" button in your app that prompts users to connect their wallet, like so: ```tsx theme={"system"} import {usePrivy} from '@privy-io/react-auth'; export default function ConnectWalletButton() { const {connectWallet} = usePrivy(); // Prompt user to connect a wallet with Privy modal return ; } ``` As an optional parameter to `connectWallet`, you may pass an object with the following optional fields: | Field | Type | Description | | ------------- | ------------------- | -------------------------------------------------------------------------------------- | | `description` | `string` | A description for the wallet connection prompt, which will be displayed in Privy's UI. | | `walletList` | `WalletListEntry[]` | A list of wallet optionsthat you would like Privy to display in the connection prompt. | ```tsx theme={"system"} connectWallet({ description: 'Connect your wallet to access the app', walletList: ['metamask', 'safe'] }); ``` ```tsx theme={"system"} connectWallet({ description: 'Connect your wallet to access the app', walletList: ['phantom', 'solflare'] }); ``` Once a user has connected their external wallet to your app, the wallet will appear in either of Privy's **`useWallets`** arrays, which you can then use to request signatures and transactions from the connected wallet. ## Connecting or creating a wallet You can also use Privy to connect a user's external wallet if they have one, or to create an embedded wallet for them if they do not. To do so, use the **`connectOrCreateWallet`** method of the **`usePrivy`** hook: ```tsx theme={"system"} const {connectOrCreateWallet} = usePrivy(); ``` This method will prompt the user to connect an external wallet, or log in with email, SMS, or socials, depending on your configured `loginMethods`, to create an embedded wallet. Privy's `connectOrCreate` interface currently only supports external and embedded wallets on EVM networks. For example, you might have a "Connect" button in your app that prompts users to connect their wallet, like so: ```tsx theme={"system"} import {usePrivy} from '@privy-io/react-auth'; export default function ConnectWalletButton() { const {connectOrCreateWallet} = usePrivy(); // Prompt user to connect a wallet with Privy modal return ; } ``` This method functions exactly the same as Privy's `login` method, except when users connect their external wallet, they will not automatically be prompted to authenticate that wallet by signing a message ## Authenticating a connected wallet Once a user has connected their wallet to your app, and the wallet is available in either of the **`useWallets`** arrays, you can also prompt them to **login** with that wallet or **link** that wallet to their existing account, instead of prompting the entire **`login`** or **`linkWallet`** flow. To do so, find the **`ConnectedWallet`** or **`ConnectedStandardSolanaWallet`** object from Privy, and call the object's **`loginOrLink`** method for EVM wallets and use the **`useLoginWithSiws`** or **`useLinkWithSiws`** hooks for the Solana wallets: ```tsx theme={"system"} import {useWallets} from '@privy-io/react-auth'; const {wallets} = useWallets(); wallets[0].loginOrLink(); ``` ```tsx theme={"system"} import {useWallets} from '@privy-io/react-auth/solana'; const {wallets} = useWallets(); const {generateSiwsMessage, loginWithSiws} = useLoginWithSiws(); const message = await generateSiwsMessage({address: wallets[0].address}); const encodedMessage = new TextEncoder().encode(message); const results = await wallets[0].signMessage({message: encodedMessage}); const signatureBase64 = Buffer.from(results.signature).toString('base64'); await loginWithSiws({message, signature: signatureBase64}); ``` When called, **`loginOrLink`** will directly request a [SIWE](https://docs.login.xyz/general-information/siwe-overview/eip-4361) signature from the user's connected wallet to authenticate the wallet. If the user was not **`authenticated`** when the method was called, the user will become **`authenticated`** after signing the message. If the user was already **`authenticated`** when the method was called, the user will remain **`authenticated`** after signing the message, and the connected wallet will become one of the user's **`linkedAccounts`** in their **`user`** object. You might use the methods above to "split up" the connect and sign steps of external wallet login, like so: ```tsx theme={"system"} import {useConnectWallet, useWallets} from '@privy-io/react-auth'; export default function WalletButton() { const {connectWallet} = useConnectWallet(); const {wallets} = useWallets(); // Prompt user to connect a wallet with Privy modal return ( <> {/* Button to connect wallet */} {/* Button to login with or link the most recently connected wallet */} ); } ``` ```tsx theme={"system"} import {useConnectWallet, useLoginWithSiws} from '@privy-io/react-auth'; import {useWallets} from '@privy-io/react-auth/solana'; export default function WalletButton() { const {connectWallet} = useConnectWallet(); const {wallets} = useWallets(); const {generateSiwsMessage, loginWithSiws} = useLoginWithSiws() // Prompt user to connect a wallet with Privy modal return ( {/* Button to connect wallet */} {/* Button to login with or link the most recently connected wallet */} ); } ``` # Configure cookies Source: https://docs.privy.io/recipes/react/cookies When a user logs in to your app, Privy issues that user an access token that stores their authenticated session. **You can configure Privy to store a user's access token either with a browser's [local storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) or as a [`HttpOnly` cookie](https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies) set on your app's base domain**. By default, Privy will store the user's access token in local storage. Configuring cookies requires that your app have a stable base domain and that you set a DNS record for this domain. In kind, **cookies are recommended for production applications only**. While developing your integration, you can use Privy's default setup of local storage to get started. ## Enabling cookies To configure your app to use cookies, follow the steps below: ### 1. Create separate development and production Privy app IDs In the [**Privy Dashboard**](https://dashboard.privy.io/), create (at minimum) **two** Privy apps. Concretely, you should create one app for use in **production** environments only, and one app for use in **development** environments only. This step is critical, as once you enable cookies, your production app ID will **only** work in your production environment, and will error in all other environments. The development process for all environments (production and development) will be the same on your end. The only difference is that you **must** use separate app IDs for each environment. Next, follow the steps below, **only for your production app ID**. Do not complete them for your development app. ### 2. For your production app, register your production domain in the Privy Dashboard In the [**Privy Dashboard**](https://dashboard.privy.io/), find your **production app** in the App Dropdown of the left sidebar. Then, navigate to the **Configuration > App settings** page > **Domains** tab for that app. Toggle on **HttpOnly cookies**. You'll be prompted to add an app domain. This is the domain root of your web app (e.g. example.com). Do not include the protocol or [www](http://www). **Do not list a domain that is not a production domain.** As a general rule, our team will not automatically approve domains that appear to be scoped to a sandbox environment. Example of such unsupported domains include `*.vercel.app`, `*.railway.app`, `*.herokuapp.com`, and `*.amazonaws.com`. ### 3. For your production domain, set the necessary DNS records Once you've set your app's domain in **Configuration >> App settings >> Domains**, Privy will display any required DNS records you must set for that for that domain. **Go to the admin dashboard of your domain registrar and set the required DNS records.** Once done, return to the **HttpOnly cookies** section in the **Privy Dashboard** and click the **Refresh** button on your domain. This will force Privy to re-verify if the correct DNS records have been set for that domain or not. Please note that it may take a few hours for DNS records to propagate before Privy can confirm that it is verified. This allows Privy's servers to set a first-party cookie on your production domain. **Once you've finished the steps above, Privy will review your request and update servers to begin setting cookies on your production app's domain.** Once your domain is verified, the corresponding App ID can only be used on that **exact** production domain. If using Cloudflare as your DNS records provider, make sure that the registered DNS record is **not** set to **Proxied**, and is set to **DNS Only** until the domain verification is complete. ### App clients and cookies Each app can only have one **cookie domain**. If you share an app ID across web and mobile environments, you can use app clients to conditionally enforce cookies depending on the environment. For example, you can have an app client that enforces cookies for your web app, and an app client that does not enforce it for your mobile app. To toggle cookie settings for different app clients, first set your **HttpOnly Cookies** and an app domain. Then, go to the **Configuration > App settings** page > **Clients** tab, and find the **App client** you’d like to enable cookies for. Select “edit”, set the cookies toggle to **Enabled**, and save. If you enable cookies in an app client, but no base domain is set on your app, no cookies will be set. Within an app client, you can choose to enable or disable cookies. If you enable cookies for any client, they will be set on the domain that is configured as your app’s **domain**. ### Debugging DNS issues #### CAA records block issuance Some providers may require extra configuration in order to set up SSL for your base domain to work with Privy. If you are seeing the error "CAA records block issuance" in the Privy dashboard or you keep trying to set an `acme_challenge` and state resets, you might either: 1. Already have CAA record that does not include one of the CAs Privy uses to issue SSL certs 2. Need to explicitly set a CAA record To resolve this, go to your provider and create a `CAA` record on your root domain (ie `example.com`, not including any subdomains). If there are already contents in the `CAA` record, append the following, otherwise create a new record containing the following: ``` # Let's Encrypt 0 issue "letsencrypt.org" 0 issuewild "letsencrypt.org" # Google Trust Services 0 issue "pki.goog; cansignhttpexchanges=yes" 0 issuewild "pki.goog; cansignhttpexchanges=yes" ``` #### The hostname is associated with a held zone If you use Cloudflare as a DNS provider and have "held" your zone for security reasons, you will need to temporarily [release the hold](https://developers.cloudflare.com/fundamentals/setup/account/account-security/zone-holds/#release-zone-holds). ## Using cookies in development In **both production and development** (local, preview, staging) environments, Privy will set a cookie with the name `privy-token` to store your user's session. **Your app logic for handling the cookie (e.g. in your authorization middleware) does not need to handle different environments differently.** The mechanics of *how the cookie is set* is the key difference between production and development environments. This is why you **must only use your production App ID within your production environment**. Concretely: * For your **production** app ID, once you have completed the steps above, Privy's **servers** will set a cookie, only on the domain you have verified and any subdomains. Cookies will not be set on localhost. * For your **development** app ID(s), Privy's **client** will automatically set a cookie on *any* domain you use this App ID on, including localhost. This allows you to use the same app logic around cookies across various environments. As a security precaution, client-set cookies for development have a shorter lifetime (7 days, versus 30 days for server-set cookies). We recommend maintaining two apps, one for development and one for production. However, if you need to develop with your production App ID in a `localhost` environment, you can do so by using [App Clients](/basics/get-started/dashboard/app-clients). ## Server-side rendering With cookies, when an authenticated user visits a page of your app, the request to fetch the page from your server will automatically include the user's access token as a **`privy-token`** cookie. If your app uses **server-side rendering (SSR)**, you can use the presence of this cookie (and other Privy cookies) to determine if the user is authenticated *before* your page is rendered on the client. ### When the `privy-token` is present Concretely, if the request to your server includes a valid **`privy-token`**, you should consider the user as authenticated and should handle them accordingly. ### When the `privy-token` is absent If the request to your server does *not* include a valid **`privy-token`**, the user might either: * be unauthenticated, and will need to **`login`** to become unauthenticated. * *appear* as unauthenticated, and will need to wait for the page to be rendered in their client before you can determine if they are authenticated. The latter case generally occurs when an authenticated user steps away from your app for more than an hour, allowing the access token to expire, and returns to your app for the first time. In this case, the request to fetch the page from your server will **not** include a valid **`privy-token`**, as it has expired, but the **`privy-token`** will be refreshed imminently as soon as the page loads in the user's browser. To handle this case, when the **`privy-token`** is missing in the request, you should **instead wait for your app to load in the client, to allow their user's authentication status to update correctly, before taking any actions based on their authentication status.** This most commonly occurs in middleware setups that perform server-side routing. One solution for handling this flow is to set up your app and middleware like so: #### Client-side setup In your client, add a new page (e.g. **`/refresh`**) that implements the following: 1. Call Privy’s **`getAccessToken`** method when the page loads. This ensures that whenever the user visits this page, their session is refreshed if they are authenticated. 2. If **`getAccessToken`** returns a valid token, redirect the user to the path specified in a **`redirect_uri`** query parameter. Your middleware will populate this query parameter later. 3. If **`getAccessToken`** returns `null`, redirect the user to your login page as they are not authenticated. #### Middleware setup In your middleware, when your backend receives a request to fetch a given page: 1. If the request includes a **`privy-token`** that is valid, you can consider the user authenticated and apply your normal middleware. 2. If the request does not include a **`privy-token`** but does include a **`privy-session`** cookie, the user may be authenticated, and you’ll need to refresh their session from the client before applying your middleware. 3. To refresh the user’s session from the client, you can redirect the user to the **`/refresh`** page you set up above. As part of this, you should also pass the original route the user intended to visit as a query param (e.g. **`redirect_url`**) when you redirect them to **`/refresh`**. Per the client-side setup, this allows the user's session to be refreshed and for them to be correctly redirected based on their authentication status. Make sure to exclude the following from the above redirect middleware: 1. The page at the `/refresh` path you setup: in this case, the user should be allowed to visit the `/refresh` page as their authentication status and redirect will be handled *on that page* in the client. Redirecting away from this page in your middleware may result in an infinite redirecting loop. 2. Any page that includes the query parameter `privy_oauth_code`, `privy_oauth_state`, or `privy_oauth_provider`: these parameters are a required component of Privy's [OAuth login flow](/authentication/user-authentication/login-methods/oauth) and applying a redirect will destructively erase them. As an example, if you're using NextJS, you might setup your middleware like so: ```tsx theme={"system"} // Replace this array with an array of paths for pages in your app that do not require the // user to be authenticated, e.g. a login page const UNAUTHENTICATED_PAGES = []; export const config = { // necessary to ensure that you are redirected to the refresh page matcher: '/' }; export async function middleware(req: NextRequest) { const cookieAuthToken = req.cookies.get('privy-token'); const cookieSession = req.cookies.get('privy-session'); // Bypass middleware when `privy_oauth_code` is a query parameter, as // we are in the middle of an authentication flow if (req.nextUrl.searchParams.get('privy_oauth_code')) return NextResponse.next(); // Bypass middleware when the /refresh page is fetched, otherwise // we will enter an infinite loop if (req.url.includes('/refresh')) return NextResponse.next(); // If the user has `privy-token`, they are definitely authenticated const definitelyAuthenticated = Boolean(cookieAuthToken); // If user has `privy-session`, they also have `privy-refresh-token` and // may be authenticated once their session is refreshed in the client const maybeAuthenticated = Boolean(cookieSession); if (!definitelyAuthenticated && maybeAuthenticated) { // If user is not authenticated, but is maybe authenticated // redirect them to the `/refresh` page to trigger client-side refresh flow return NextResponse.redirect(new URL('/refresh', req.url)); } return NextResponse.next(); } ``` By design, Privy does **not** permit apps to refresh a user's access token from the app's server via the user's refresh token. This is a standard security protection to limit the surface area of exposure of the refresh token. ### Setting SameSite to Lax Cookies set by Privy are by default set with the `SameSite` attribute set to `Strict`. This ensures that cookies are only sent on requests originating from the same site that set the cookie. However, you may wish to receive cookies on cross-site top-level navigations or [safe requests methods](https://developer.mozilla.org/en-US/docs/Glossary/Safe/HTTP) (e.g. `GET`, `HEAD`, `OPTIONS`). In this case, you can toggle setting the `SameSite` attribute to `Lax` in the [**Privy Dashboard**](https://dashboard.privy.io/?page=settings\&setting=domains). Setting SameSite=Lax sends your cookies on cross-site top-level navigations. If your app has any unprotected state-changing endpoints an attacker could leverage this to lure your users into making changes to their accounts. In the [**Privy Dashboard**](https://dashboard.privy.io/?page=settings\&setting=domains), find your **production app** in the App Dropdown of the left sidebar. Then, navigate to the **Configuration > App settings** page > **Domains** tab for that app. Check the box next to **Set SameSite to Lax** under **HttpOnly cookies**. # Integrating with EIP-7702 Source: https://docs.privy.io/recipes/react/eip-7702 [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702) is an upgrade to EVM blockchains that enables externally owned accounts (EOAs) to set their code to that of a smart contract. In practical terms, this means that EOA wallets will gain AA (account abstraction) capabilities such as transaction bundling, gas sponsorship, and custom permissions. Privy supports all low level interfaces required by 7702 - signing authorizations and sending type 4 transactions, allowing you to use any implementation of EIP-7702. Use the following guides to get started with EIP-7702 in your application: ### Signing EIP-7702 authorizations Privy provides methods to sign EIP-7702 authorizations using the user's embedded wallet. This authorization is a cryptographic signature that allows an EOA to set its code to that of a smart contract, enabling the EOA to behave like a smart account. Learn more about signing EIP-7702 authorizations in our [dedicated guide](/wallets/using-wallets/ethereum/sign-7702-authorization). Learn more about using the signed authorization in the integration guides below! ### Detect current 7702 authorization state and implementation address You can determine whether an EOA is currently delegated via EIP-7702 and read the authorized implementation address with a single `eth_getCode` call on the EOA address. Under EIP-7702, an authorized EOA temporarily exposes a small bytecode stub that begins with the magic prefix `0xef0100`, followed immediately by the 20-byte implementation address. If `eth_getCode` returns empty code (`0x` or `0x0`), the EOA is not currently delegated on that chain. The snippets below return the current implementation address or `null`. ```ts theme={"system"} import {createPublicClient, http} from 'viem'; import {mainnet} from 'viem/chains'; // replace with your chain const publicClient = createPublicClient({ chain: mainnet, transport: http('your RPC URL here') }); const address = '0x...'; // the EOA address here const code = (await publicClient.getCode({address}))?.toLowerCase() ?? '0x'; const prefixIndex = code.indexOf('0xef0100'); const authorizedImplementationAddress = prefixIndex === -1 ? null : (`0x${code.slice(prefixIndex + 8, prefixIndex + 48)}` as `0x${string}`); ``` ```ts theme={"system"} export function parseEip7702AuthorizedAddress( code: string | null | undefined ): `0x${string}` | null { if (!code || code === '0x' || code === '0x0') return null; const normalized = code.toLowerCase(); const MAGIC = '0xef0100'; const idx = normalized.indexOf(MAGIC); if (idx === -1) return null; return ('0x' + normalized.slice(idx + MAGIC.length, idx + MAGIC.length + 40)) as `0x${string}`; } ``` ```ts theme={"system"} import {ethers} from 'ethers'; const provider = new ethers.JsonRpcProvider('your RPC URL here'); const address = 'the EOA address here'; const code = (await provider.getCode(address))?.toLowerCase() ?? '0x'; const prefixIndex = code.indexOf('0xef0100'); const authorizedImplementationAddress = prefixIndex === -1 ? null : (`0x${code.slice(prefixIndex + 8, prefixIndex + 48)}` as `0x${string}`); ``` ```ts theme={"system"} function parseEip7702AuthorizedAddress(code?: string | null) { if (!code || code === '0x' || code === '0x0') return null; const MAGIC = '0xef0100'; const idx = code.toLowerCase().indexOf(MAGIC); if (idx === -1) return null; return ('0x' + code.slice(idx + MAGIC.length, idx + MAGIC.length + 40)) as `0x${string}`; } ``` Authorization state is per-chain. Under 7702, an authorized EOA will return non-empty code with the `0xef0100` prefix; other non-empty code indicates a deployed contract account. ### Using EIP-7702 capabilities In this guide, we will transform your Privy embedded wallet into a smart wallet with features like gas sponsorship, batch transactions, granular permissions, and more [using EIP-7702](https://www.alchemy.com/docs/wallets/react/using-7702) support from Alchemy. ### 0. Install dependencies In your app's repository, install the required dependencies from Privy, Alchemy Account Kit, and [`viem`](https://www.npmjs.com/package/viem): ```sh theme={"system"} npm i @privy-io/react-auth @account-kit/infra @account-kit/wallet-client @account-kit/smart-contracts @aa-sdk/core viem@2.29.3 ``` ### 1. Create an Alchemy account and get your API key * **Create an app:** visit the Alchemy [dashboard](https://dashboard.alchemy.com/apps) to create a new app, if you don't already have one. * Make sure to enable networks that support EIP-7702 such as Ethereum Mainnet or Sepolia. * Save the app's **API Key** that will be used later. * **Enable gas sponsorship:** visit the [Gas Manager](https://dashboard.alchemy.com/gas-manager) dashboard and create a new sponsorship policy for the app you created in step 1. This policy will be used to set rules on how much of user's gas you want to sponsor. * Make sure to enable gas sponsorship on a chain that support EIP-7702 such as Ethereum Mainnet or Sepolia. * Save the **Policy ID** that will be used later. Now that you have your API key and Policy ID, you can set up your smart wallets. ### 2. Configure Privy settings If you're already using a Privy embedded wallet, update your configuration to support EIP-7702 with embedded wallets. If you don't yet have authentication configured, you can follow [this](https://www.alchemy.com/docs/wallets/react/using-7702) guide to get set up. Make sure your `PrivyProvider` has the following `embeddedWallets` settings and ensure you set the `defaultChain` and `supportedChains` to the 7702 supported chain you chose in step 1. ```tsx theme={"system"} import { sepolia } from "@account-kit/infra"; ``` ### 3. Adapt Privy signer to a smart account signer Now that you have authentication working, adapt the Privy signer to be able to sign 7702 authorizations to upgrade to smart accounts. **1. Get the Privy embedded wallet** ```tsx theme={"system"} import {useWallets} from '@privy-io/react-auth'; const {wallets} = useWallets(); const embeddedWallet = wallets.find((x) => x.walletClientType === 'privy'); ``` **2. Create a `SmartAccountSigner` instance** ```tsx theme={"system"} import {sepolia, alchemy} from '@account-kit/infra'; // make sure to import your chain from account-kit, not viem import {useSign7702Authorization} from '@privy-io/react-auth'; import {SmartAccountSigner, WalletClientSigner} from '@aa-sdk/core'; import {createWalletClient, custom, Hex} from 'viem'; import {Authorization} from 'viem/experimental'; const {signAuthorization} = useSign7702Authorization(); async function create7702signer() { const baseSigner = new WalletClientSigner( createWalletClient({ account: embeddedWallet!.address as Hex, chain: sepolia, transport: custom(await embeddedWallet!.getEthereumProvider()) }), 'privy' ); const signer: SmartAccountSigner = { getAddress: baseSigner.getAddress, signMessage: baseSigner.signMessage, signTypedData: baseSigner.signTypedData, signerType: baseSigner.signerType, inner: baseSigner.inner, signAuthorization: async ( unsignedAuth: Authorization ): Promise> => { const signature = await signAuthorization(unsignedAuth); return { ...unsignedAuth, ...{ r: signature.r!, s: signature.s!, v: signature.v! } }; } }; return signer; } ``` ### 4. Upgrade to smart accounts and send sponsored transactions Now that you have a `SmartAccountSigner` instance, follow [this guide](https://www.alchemy.com/docs/wallets/transactions/using-eip-7702#third-party-signers) to create a smart account client (`createModularAccountV2Client` ) and start sending sponsored transactions. You'll need: * the `SmartAccountSigner` instance defined in step 3 * the API key and the policy ID from step 1 Once you define the client, you can send sponsored transactions with your embedded EOA and access other advanced smart account features! This client will handle all of the logic of delegating to a new smart account, if not already, and signing transactions. If you don't yet have a signer, follow [this guide](https://www.alchemy.com/docs/wallets/react/using-7702) to get set up. Looking for a complete working example? See this [working example integrating Privy embedded EOAs with EIP-7702 and Alchemy Smart Wallets](https://github.com/alchemyplatform/alchemy-wallets-7702-thirdparty-example). ### Next steps You just upgraded your EOA and sent your first sponsored transaction using EIP-7702! If you want to leverage other smart account capabilities such as batching and permissions, check out the [Alchemy docs](https://www.alchemy.com/docs/wallets). In this guide we'll see a quick example of using the [Biconomy's](https://biconomy.io/) Modular Execution Environment (MEE) stack with Privy! To keep things short, this guide will show you a simple example of *single-chain orchestration* with cross-chain gas. However, the Biconomy MEE stack supports multi-chain orchestration cases. If you're looking to build a multi-chain DeFi strategy - read the [Biconomy Docs](https://docs.biconomy.io) ## Project Setup Create your project using Vite: ```bash theme={"system"} bun create vite biconomy-mee-embedded-example --template react-ts cd biconomy-mee-embedded-example ``` Add the following to your `package.json`: ```json theme={"system"} "dependencies": { "@biconomy/abstractjs": "^1.0.17", "@privy-io/react-auth": "^2.14.2", "@privy-io/wagmi": "^1.0.4", "@tanstack/react-query": "^5.80.7", "react": "^19.1.0", "react-dom": "^19.1.0", "viem": "^2.31.3", "wagmi": "^2.15.6" } ``` Install dependencies: ```bash theme={"system"} bun install ``` Set up your `main.tsx`: ```tsx theme={"system"} import React from 'react'; import ReactDOM from 'react-dom/client'; import App from './App.tsx'; import './index.css'; import {PrivyProvider} from '@privy-io/react-auth'; import {WagmiProvider} from 'wagmi'; import {wagmiConfig} from './wagmi.ts'; import {QueryClient, QueryClientProvider} from '@tanstack/react-query'; const appId = 'your-privy-app-id'; const queryClient = new QueryClient(); ReactDOM.createRoot(document.getElementById('root')!).render( ); ``` In `wagmi.ts`: ```ts theme={"system"} import {createConfig} from '@privy-io/wagmi'; import {http} from 'wagmi'; import {baseSepolia, optimismSepolia} from 'viem/chains'; export const wagmiConfig = createConfig({ chains: [optimismSepolia, baseSepolia], transports: { [optimismSepolia.id]: http(), [baseSepolia.id]: http() } }); ``` ## ⬇Get the Embedded Wallet Instance After logging in the user with Privy, you can find the embedded wallet as follows: ```ts theme={"system"} import {useWallets} from '@privy-io/react-auth'; const {wallets} = useWallets(); const wallet = wallets.find((wallet) => wallet.walletClientType === 'privy'); ``` ## Authorizing with EIP-7702 To install the Biconomy Nexus 1.2.0 smart account on the address of your Privy embedded wallet EOA, you need to sign the following authorization. Note: This is using the `signAuthorization` method exposed by the `useSignAuthorization` Privy hook. ```ts theme={"system"} import {useSign7702Authorization} from '@privy-io/react-auth'; import {baseSepolia} from '@privy-io/chains'; const {signAuthorization} = useSign7702Authorization(); const NEXUS_V120 = '0x000000004F43C49e93C970E84001853a70923B03'; const authorization = await signAuthorization({ contractAddress: NEXUS_V120, chainId: baseSepolia.id, // or 0 for universal nonce: 0 }); ``` ## Execute a Cross-Chain Gas Abstracted Transaction After signing, you can submit a transaction through Biconomy MEE Relayers. Notice few things for this transaction: * Gas is paid with USDC * Gas is paid on a different chain than the instruction being executed * The `amount` arg in the ERC-20 `transfer` call is not fixed to a value, but uses `runtimeERC20BalanceOf` which will inject the full amount of USDC available. This demonstrates few key points of MEE: * Gas abstraction / gas sponsorship * Multi-chain execution/orchestration * Runtime parameter injection enabling multi-transaction composability ```ts {skip-check} theme={"system"} const orchestrator = await toMultichainNexusAccount({ chains: [optimismSepolia, baseSepolia], transports: [http(), http()], signer: await wallet.getEthereumProvider(), accountAddress: wallet.address as Address }); const meeClient = await createMeeClient({account: orchestrator}); const sendUSDCBase = await orchestrator.buildComposable({ type: 'default', data: { abi: erc20Abi, chainId: baseSepolia.id, to: usdcAddresses[baseSepolia.id], functionName: 'transfer', args: [ wallet.address, runtimeERC20BalanceOf({ tokenAddress: usdcAddresses[baseSepolia.id], targetAddress: orchestrator.addressOn(baseSepolia.id, true), constraints: [greaterThanOrEqualTo(1n)] }) ] } }); const quote = await meeClient.getQuote({ instructions: [sendUSDCBase], authorization, delegate: true, // Paying for gas with USDC on Optimism, while // executing a transaction on Base! feeToken: { address: usdcAddresses[optimismSepolia.id], chainId: optimismSepolia.id } }); const {hash} = await meeClient.executeQuote({quote}); ``` You can then link the user to MEE Scan to track: ```ts {skip-check} theme={"system"} const link = getMeeScanLink(hash); ``` ## Storing the **Authorization** If you've used the `chainId === 0` for your authorization you can store it (e.g. in localStorage or DB) and replay it for other chains in the future. This gives your users an even more seamless UX. In this guide, we'll demonstrate how to use Pimlico, a bundler and paymaster service for ERC-4337 accounts, to enable your users to send gasless transactions using EIP-7702 authorization. Want to see a full end to end example? Check out our starter repo [here](https://github.com/pimlicolabs/permissionless-privy-7702) ## 0. Install dependencies In your app's repository, install the required dependencies from Privy, Permissionless, and Viem: ```bash theme={"system"} npm i @privy-io/react-auth @privy-io/wagmi permissionless viem wagmi ``` ## 1. Sign up for a Pimlico account and get your API key Head to the Pimlico dashboard [here](https://dashboard.pimlico.io/) and create an account. Generate an API key and create a sponsorship policy for the network you plan to use (optional). Make note of your API key and sponsorship policy ID. ## 2. Configure Privy settings Configure your app to create embedded wallets for all users. ```jsx theme={"system"} ... ``` ## 3. Create a simple smart account with Permissionless SDK Permissionless provides a simple way to create a smart account client that can send user operations with EIP-7702 authorization. All you need is the user's embedded wallet and the Pimlico API key. ```jsx theme={"system"} import {useEffect} from 'react'; import {usePrivy, useSign7702Authorization, useWallets} from '@privy-io/react-auth'; import {useSetActiveWallet} from '@privy-io/wagmi'; import {useWalletClient} from 'wagmi'; import {createPublicClient, http, zeroAddress, Hex} from 'viem'; import {sepolia} from 'viem/chains'; import {createSmartAccountClient} from 'permissionless'; import {createPimlicoClient} from 'permissionless/clients/pimlico'; import {to7702SimpleSmartAccount} from 'permissionless/accounts'; // Get the Privy embedded wallet const {wallets} = useWallets(); const {data: walletClient} = useWalletClient(); const embeddedWallet = wallets.find((wallet) => wallet.walletClientType === 'privy'); // Set the embedded wallet as active const {setActiveWallet} = useSetActiveWallet(); useEffect(() => { if (embeddedWallet) { setActiveWallet(embeddedWallet); } }, [embeddedWallet, setActiveWallet]); // Create a public client for the chain const publicClient = createPublicClient({ chain: sepolia, transport: http(process.env.NEXT_PUBLIC_SEPOLIA_RPC_URL) }); // Create a Pimlico client const pimlicoApiKey = process.env.NEXT_PUBLIC_PIMLICO_API_KEY; const pimlicoUrl = `https://api.pimlico.io/v2/${sepolia.id}/rpc?apikey=${pimlicoApiKey}`; const pimlicoClient = createPimlicoClient({ chain: sepolia, transport: http(pimlicoUrl) }); // Create a 7702 simple smart account const simple7702Account = await to7702SimpleSmartAccount({ client: publicClient, owner: walletClient }); // Create the smart account client const smartAccountClient = createSmartAccountClient({ client: publicClient, chain: sepolia, account: simple7702Account, paymaster: pimlicoClient, bundlerTransport: http(pimlicoUrl) }); ``` ## 4. Sign the EIP-7702 authorization Privy provides methods to sign EIP-7702 authorizations using the user's embedded wallet. This authorization is a cryptographic signature that allows an EOA to set its code to that of a smart contract, enabling the EOA to behave like a smart account. ```jsx theme={"system"} const {signAuthorization} = useSign7702Authorization(); // Sign the EIP-7702 authorization const authorization = await signAuthorization({ contractAddress: '0xe6Cae83BdE06E4c305530e199D7217f42808555B', // Simple account implementation address chainId: sepolia.id, nonce: await publicClient.getTransactionCount({ address: walletClient.account.address }) }); ``` ## 5. Send a gas-sponsored transaction With the smart account client configured and the authorization signed, you can now send gasless UserOperations: ```jsx theme={"system"} const transactionHash = await smartAccountClient.sendTransaction({ to: zeroAddress, value: 0n, data: '0x', authorization, paymasterContext: { sponsorshipPolicyId: process.env.NEXT_PUBLIC_SPONSORSHIP_POLICY_ID } }); console.log(`Transaction hash: ${transactionHash}`); console.log(`View on Etherscan: https://sepolia.etherscan.io/tx/${transactionHash}`); ``` ## Conclusion That's it! You've just executed a gasless transaction from a normal EOA upgraded with EIP-7702 using Pimlico as the bundler and paymaster service. Explore the rest of the [Pimlico docs](https://docs.pimlico.io/) to learn about advanced features like batching transactions, gas estimation, and more. Want to see a full end to end example? Check out our starter repo [here](https://github.com/pimlicolabs/permissionless-privy-7702)! In this guide, we'll demonstrate how to use [Porto](https://porto.sh/), a universal blockchain account infrastructure with native cross-chain interoperability, together with Privy to upgrade your EOA wallets with EIP-7702. ### 0. Install dependencies In your app's repository, install the required dependencies from Privy, Porto, and [`viem`](https://www.npmjs.com/package/viem): ```sh theme={"system"} npm i @privy-io/react-auth porto viem ``` ### 1. Configure Privy settings Configure your app to create embedded wallets for all users. Update your `PrivyProvider` configuration: ```tsx theme={"system"} ... ``` ### 2. Set up Porto client Create a Viem client configured for Porto: ```tsx theme={"system"} import {createClient, http} from 'viem'; import {Chains} from 'porto/viem'; const client = createClient({ chain: Chains.baseSepolia, transport: http('https://rpc.porto.sh') }); ``` ### 3. Create Porto account with Privy's embedded wallet Get the Privy embedded wallet and create a Porto account instance with custom signing: ```tsx theme={"system"} import {useWallets} from '@privy-io/react-auth'; import {Account} from 'porto/viem'; import {Hex} from 'viem'; // Get the Privy embedded wallet const {wallets} = useWallets(); const embeddedWallet = wallets.find((wallet) => wallet.walletClientType === 'privy'); // Create a Porto account with secp256k1 signing const account = Account.from({ source: 'privateKey', address: embeddedWallet.address as Hex, async sign({hash}) { const provider = await embeddedWallet.getEthereumProvider(); const signature = await provider.request({ method: 'secp256k1_sign', params: [hash] }); return signature; } }); ``` ### 4. Upgrade account to Porto Use Porto's `RelayActions` to upgrade the account: ```tsx theme={"system"} import {RelayActions} from 'porto/viem'; // Upgrade the account to Porto const upgradedAccount = await RelayActions.upgradeAccount(client, { account }); ``` ### 5. Send transactions with Porto With the account upgraded, you can now send transactions through Porto's relay infrastructure with advanced features like gas sponsorship and cross-chain capabilities: ```tsx theme={"system"} import {encodeFunctionData} from 'viem'; // Example: Send a transaction to mint an NFT const result = await RelayActions.sendCalls(client, { account, chain: Chains.baseSepolia, calls: [ { to: nftContractAddress, data: encodeFunctionData({ abi: nftAbi, functionName: 'mint', args: [account.address] }) } ] }); console.log('Transaction sent:', result); ``` ### 6. Track transaction status Monitor the status of your transactions: ```tsx theme={"system"} // Get the status of a transaction const status = await RelayActions.getCallsStatus(client, { account, callHash: result.hash }); console.log('Transaction status:', status); ``` ### Next steps You've successfully upgraded your EOA with EIP-7702 and Porto! Your users can now: * Send transactions with gas sponsorship * Execute cross-chain transactions seamlessly * Batch multiple operations in a single transaction * Benefit from native interoperability across chains Explore the [Porto documentation](https://porto.sh/) to learn about additional features. In this guide, we demonstrate using [ZeroDev](https://zerodev.app/), a toolkit for creating smart accounts, together with Privy to enable your users to send gasless (sponsored) transactions. Want to see a full end to end example? Check out our starter repo [here](https://github.com/privy-io/create-next-app/tree/7702/zerodev)! ### 0. Install dependencies In your app's repository, install the required dependencies from Privy, ZeroDev and [`viem`](https://www.npmjs.com/package/viem): ```sh theme={"system"} npm i @privy-io/react-auth @zerodev/sdk viem ``` ### 1. Sign up for a ZeroDev account and create a project Head to the [**ZeroDev dashboard**](https://dashboard.zerodev.app/) and create a project on a chain that supports EIP-7702. Set up a [gas sponsorship policy](https://dashboard.zerodev.app/paymasters) to enable sending sponsored transactions. Copy the **Bundler RPC** and **Paymaster RPC** for the network you plan to use. ### 2. Configure Privy settings Configure your app to create embedded wallets for all users. Also configure Privy to not show its default wallet UIs. Instead, we recommend you use your own custom UIs for showing users the user operations they sign. Update your `PrivyProvider` configuration to include the following properties: ```tsx theme={"system"} ... ``` ### 3. Create a 7702 Kernel account with the ZeroDev SDK ZeroDev exposes helper functions that take care of generating the 7702 authorization for you. All you need to provide is the signer for the user's embedded wallet and the Kernel version you want to use. ```tsx theme={"system"} import { createZeroDevPaymasterClient, createKernelAccountClient, createKernelAccount } from '@zerodev/sdk'; import {KERNEL_V3_3} from '@zerodev/sdk/constants'; import {getEntryPoint} from '@zerodev/sdk/constants'; import {createWalletClient, createPublicClient, custom, http, zeroAddress, Hex} from 'viem'; import {odysseyTestnet} from 'viem/chains'; // Select the chain and Kernel version you want to use const chain = odysseyTestnet; const kernelVersion = KERNEL_V3_3; const kernelAddresses = KernelVersionToAddressesMap[kernelVersion]; const entryPoint = getEntryPoint('0.7'); // Grab the embedded wallet created by Privy const {wallets} = useWallets(); const embeddedWallet = wallets.find((wallet) => wallet.walletClientType === 'privy'); // Build viem clients for the wallet & public RPC const walletClient = createWalletClient({ account: embeddedWallet.address as Hex, chain, transport: custom(await embeddedWallet.getEthereumProvider()) }); const publicClient = createPublicClient({ chain, transport: http() }); // Sign the EIP-7702 authorization const authorization = await signAuthorization({ contractAddress: kernelAddresses.accountImplementationAddress, chainId: chain.id }); // Create the 7702 Kernel account (no deployment occurs!) const account = await createKernelAccount(publicClient, { eip7702Account: walletClient, entryPoint, kernelVersion, eip7702Auth: authorization }); ``` Behind the scenes ZeroDev generates the EIP-7702 authorization and binds the Kernel implementation code to the EOA, giving it smart-account super-powers while keeping the same address. ### 4. Configure the ZeroDev client for sponsored transactions ```tsx theme={"system"} const paymasterRpc = 'YOUR_PAYMASTER_RPC_URL'; const bundlerRpc = 'YOUR_BUNDLER_RPC_URL'; // Create a paymaster client so the user does not need ETH const paymasterClient = createZeroDevPaymasterClient({ chain, transport: http(paymasterRpc) }); // Build a Kernel client that will create & submit UserOperations const kernelClient = createKernelAccountClient({ account, chain, bundlerTransport: http(bundlerRpc), paymaster: paymasterClient, client: publicClient }); ``` ### 5. Send a gas-sponsored transaction With the client configured, you can now send gasless UserOperations. Below we send an empty call then wait for it to be mined: ```tsx theme={"system"} // Send a simple UserOperation const userOpHash = await kernelClient.sendUserOperation({ callData: await kernelClient.account.encodeCalls([ { to: zeroAddress, value: BigInt(0), data: '0x' } ]) }); // Wait for the operation to be included const {receipt} = await kernelClient.waitForUserOperationReceipt({ hash: userOpHash }); console.log( 'UserOp completed', `${chain.blockExplorers.default.url}/tx/${receipt.transactionHash}` ); ``` ### Conclusion That's it! You've just executed a gasless transaction from a normal EOA upgraded with EIP-7702. Explore the rest of the [ZeroDev docs](https://docs.zerodev.app/) to learn about batching, session keys, cross-chain actions and more. # Integrating the Base app Source: https://docs.privy.io/recipes/react/external-wallets/base-app The **Base App** is an ERC-4337-compatible smart wallet by Coinbase that users can connect to your application. The Base App supports a variety of extended capabilities, like [spend permissions](https://docs.base.org/base-account/improve-ux/spend-permissions), [gas sponsorship](https://docs.base.org/base-account/improve-ux/sponsor-gas/paymasters), [sub accounts](https://docs.base.org/base-account/improve-ux/sub-accounts) and more. Learn how to integrate the Base App with Privy in the guide below. ### 1. Set up your React app First, follow the [React Quickstart](/basics/react/quickstart) to get your app instrumented with Privy's basic functionality. Make sure you have updated your `@privy-io/react-auth` SDK to the latest version. Next, configure your React app to show the Base Account as one of the external wallet options that users can use to connect to your application. To do so, pass `'base_account'` to the [`config.appearance.walletList`](/wallets/connectors/setup/configuring-external-connector-wallets) array. ```tsx highlight={5} theme={"system"} {children} ``` ### 2. Access the Base Account SDK Next, in your React app, access the instance of the [Base Account SDK](https://github.com/base/account-sdk) using Privy's `useBaseAccountSdk` hook. ```tsx theme={"system"} import {useBaseAccountSdk} from '@privy-io/react-auth'; ... const {baseAccountSdk} = useBaseAccountSdk(); ``` This SDK instance is your app's entrypoint to the features of the Base Account. ### 3. Use Base Account methods Finally, use methods on the Base Account SDK such as `getProvider`, `pay`, `subaccount` methods, and more to leverage the capabilities of the Base App in your app. # Using Base sub accounts Source: https://docs.privy.io/recipes/react/external-wallets/base-sub-accounts [Base Sub Accounts](https://docs.base.org/base-account/improve-ux/sub-accounts) are a feature of the [Base Account](https://docs.base.org/base-account/overview/what-is-base-account) (formerly known as Coinbase Smart Wallet) that allow you to streamline the user experience of using a Base Account in your app. Follow the guide below to learn how to use Base Sub Accounts with Privy. ## Overview By default, when a user uses their Base Account within their app, the user must authorize every signature and transaction via a passkey prompt. This may be interruptive for your app's user experience, particularly for use cases that require a high-volume of signatures or transactions, such as gaming. [Sub Accounts](https://docs.base.org/base-account/improve-ux/sub-accounts) enable you to create an Ethereum account derived from the parent Base Account that is *specific to your app*, with its own address, signing capabilities, and transaction history. This Sub Account is owned by another wallet, such as an embedded wallet or a local account, and can be configured to *not* require an explicit (passkey) confirmation from the user on every signature and transaction. Sub accounts can even transact with the balance of the parent account using Spend Permissions, allowing users to spend this balance without explicit passkey prompts. ## Usage To set up Sub Accounts in your app that can be controlled by an embedded wallet, follow the guide below. ### 1. Set up your React app First, follow the [React Quickstart](/basics/react/quickstart) to get your app instrumented with Privy's basic functionality. Make sure you have updated your `@privy-io/react-auth` SDK to the latest version. Next, configure your React app to: * Show the Base Account as one of the external wallet options that users can use to connect to your application. To do so, pass `'base_account'` to the [`config.appearance.walletList`](/wallets/connectors/setup/configuring-external-connector-wallets) array. * Create embedded wallets automatically on login by setting [`config.embedded.ethereum.createOnLogin`](/basics/react/advanced/automatic-wallet-creation) to `'all-users'`. ```tsx theme={"system"} {children} ``` This will ensure that when users connect or login to your application, they have the option to use their Base Account. ### 2. Create or get a Sub Account Next, after the user logs in, create a new Sub Account or get the existing Sub Account for the user that is tied to your app's domain. To start, get the connected wallet instances for your user's embedded wallet and Base App by searching for the entries with `walletClientType: 'privy'` and `walletClientType: 'base_account'` respectively in your [`useWallets`](/wallets/wallets/get-a-wallet/get-connected-wallet#ethereum) array: ```tsx theme={"system"} import {useWallets} from '@privy-io/react-auth'; const {wallets} = useWallets(); const embeddedWallet = wallets.find((wallet) => wallet.walletClientType === 'privy'); const baseAccount = wallets.find((wallet) => wallet.walletClientType === 'base_account'); // `embeddedWallet` and `baseAccount` must be defined for users to use Sub Accounts ``` Next, switch the network of the Base Account to Base or Base Sepolia, and get the wallet's EIP-1193 provider: ```tsx theme={"system"} // Switching to Base Sepolia await baseAccount.switchChain(84532); const provider = await baseAccount.getEthereumProvider(); ``` Lastly, check if the user has an existing Sub Account using the [`wallet_getSubAccounts`](https://docs.base.org/base-account/improve-ux/sub-accounts#get-existing-sub-account) RPC method. If the user does not have an existing Sub Account, create a new one for them using the [`wallet_addSubAccount`](https://docs.base.org/base-account/improve-ux/sub-accounts#create-a-new-sub-account) RPC: ```tsx theme={"system"} // Get existing Sub Account if it exists const { subAccounts: [existingSubAccount] } = await provider.request({ method: 'wallet_getSubAccounts', params: [ { account: baseAccount.address as `0x${string}`, // The address of your user's Base Account domain: window.location.origin // The domain of your app } ] }); // Use the existing Sub Account if it exists, otherwise create a new sub account const subaccount = existingSubAccount ? existingSubAccount : await provider.request({ method: 'wallet_addSubAccount', params: [ { version: '1', account: { type: 'create', keys: [ { type: 'address', publicKey: embeddedWallet.address as Hex // Pass your user's embedded wallet address } ] } } ] }); ``` ### 3. Configure the SDK to use the embedded wallet for Sub Account operations Next, configure the Base Account SDK to use the embedded wallet to control Sub Account operations. This allows the embedded wallet to sign messages and transactions on behalf of the Sub Account, avoiding the need for a separate passkey prompt. Use the `useBaseAccountSdk` hook from Privy's React SDK to access the instance of the Base Account SDK directly, and use the SDK's `subAccount.setToOwnerAccount` method to configure the embedded wallet to sign on behalf of the Sub Account's operations. As a parameter to this method, pass a function that returns a `Promise` for a viem [`LocalAccount`](https://viem.sh/docs/accounts/local.html) representing the user's embedded wallet. You can use Privy's `toViemAccount` utility method to do so. ```tsx theme={"system"} import {useBaseAccountSdk, toViemAccount} from '@privy-io/react-auth'; ... const {baseAccountSdk} = useBaseAccountSdk(); const toOwnerAccount = async () => { const account = await toViemAccount({wallet: embeddedWallet}); return {account}; } baseAccountSdk.subAccount.setToOwnerAccount(toOwnerAccount); ``` The code below showcases how to create or get an existing Sub Account for your user, and set the embedded wallet as the Sub Account's owner. ```tsx theme={"system"} import {useWallets, useBaseAccountSdk, toViemAccount} from '@privy-io/react-auth'; // In your React component const {wallets} = useWallets(); const {baseAccountSdk} = useBaseAccountSdk(); const embeddedWallet = wallets.find((wallet) => wallet.walletClientType === 'privy'); const baseAccount = wallets.find((wallet) => wallet.walletClientType === 'base_account'); // Call this function when needed, e.g. in a button's `onClick` handler const createOrGetSubAccount = async () => { if (!embeddedWallet) throw new Error('User does not have an embedded wallet'); if (!baseAccount) throw new Error('User has not connected a Base Account'); if (!baseAccountSdk) throw new Error('Base Account SDK not initialized'); await baseAccount.switchChain(84532); // Use 8453 for Base Mainnet const provider = await baseAccount.getEthereumProvider(); // Get existing Sub Account const { subAccounts: [existingSubAccount] } = await provider.request({ method: 'wallet_getSubAccounts', params: [ { account: baseAccount.address as `0x${string}`, // The address of your user's Base Account domain: window.location.origin // The domain of your app } ] }); // Create new Sub Account if one does not exist const subaccount = existingSubAccount ? existingSubAccount : await provider.request({ method: 'wallet_addSubAccount', params: [ { version: '1', account: { type: 'create', keys: [ { type: 'address', publicKey: embeddedWallet.address as Hex // Pass your user's embedded wallet address } ] } } ] }); // Configure privy embedded wallets to power Sub Account operations const toOwnerAccount = async () => { const account = await toViemAccount({wallet: embeddedWallet}); return {account}; }; baseAccountSdk.subAccount.setToOwnerAccount(toOwnerAccount); }; ``` ### 4. Sign messages and send transactions with the Sub Account Lastly, you can sign and send transactions with the Sub Account using the Base Account's EIP1193 provider. To ensure that signatures and transactions come from the Sub Account, for each of the following RPCs: * `personal_sign`: pass the Sub Account's address, not the parent Base Account's address, as the second parameter. * `eth_signTypedData_v4`: pass the Sub Account's address, not the parent Base Account's address, as the first parameter. * `eth_sendTransaction`: set `from` in the transaction object to the Sub Account's address, not the parent Base Account's address. When these methods are invoked, the embedded wallet will sign on behalf of the Sub Account, avoiding the need for an explicit passkey prompt from the user. ```tsx theme={"system"} import {toHex} from 'viem'; const message = 'Hello world'; const signature = await baseProvider.request({ method: 'personal_sign', params: [toHex(message), subaccount.address] // Pass the Sub Account, not parent Base Account address }); ``` ```tsx theme={"system"} import {parseEther} from 'viem'; const txHash = await baseProvider.request({ method: 'eth_sendTransaction', params: [{ from: subaccount.address, // Use Sub Account address as sender to: 'insert-recipient-address', value: parseEther('0.01').toString(), data: '0x' }] }); ``` ```tsx theme={"system"} import {parseEther} from 'viem'; const userOpHash = await baseProvider.request({ method: 'wallet_sendCalls', params: [{ from: subaccount.address, // Use Sub Account address calls: [{ to: 'insert-recipient-address', value: parseEther('0.01').toString(), data: '0x' }] }] }); ``` You can combine Sub Accounts with [Spend Permissions](https://docs.base.org/base-account/improve-ux/spend-permissions) to allow the Sub Account to spend from the balance of the parent Base Account in `eth_sendTransaction` requests. # Guest accounts Source: https://docs.privy.io/recipes/react/guest-accounts Privy enables developers to create Guest accounts for users, so that users can immediately use your app without going through a login flow. Guest accounts are available in @privy-io/react-auth\@1.77.0 and above. Privy guest accounts have powerful features: * They are locally persisted, so guest users can leave and return to the same account on the same device. * They have **fully functioning** embedded wallets that can transact and mint on-chain. * They are upgradable to fully logged-in accounts by simply calling `login()`. * They have stable user IDs that do not change once a user is fully logged in. * They can be logged out and deleted as needed. ## Integration tips * **Guest account creation:** If a user is not logged in at all (via guest or normal user account), we recommend showing guest account creation and normal user login side-by-side. * e.g. a “Continue as Guest” button next to a “Login or Create Account” button. This is so users do not create guest accounts unintentionally when they mean to log in with an existing account. * **Guest account upgrade:** If a user is logged in as a guest, we recommend showing two options: upgrade or delete. * *Upgrade guest account*: When a guest upgrades to a full user, they must enter a new login credential. If they try to upgrade with a credential (e.g. email address) that already is associated with an existing account, they will see a “Could not link existing account” error message. * *Delete guest account*: If a guest prefers to use an existing account instead, they must delete their guest account first. We recommend surfacing a “delete” option explicitly so guests can opt-into abandoning their guest account in favor of an existing account. * You can make use of [login and error callbacks](/authentication/user-authentication/login-methods/email) to customize your desired behavior when a user upgrades out of guest-mode. ## Please note Guest accounts are **valid for 30 days**. If the guest does not upgrade to a full user account within 30 days, the guest session will expire. * User data and embedded wallets from guest sessions **cannot** be merged into an existing user account — guest accounts can only be *upgraded* into a new user account. If a guest user wants to log in with an existing account, you must delete the guest user session first. * Note that Telegram is not available as an upgrade login method for guest accounts. ## Configure guest accounts Enable guest accounts in the [Privy Dashboard](https://dashboard.privy.io/apps?page=login-methods\&logins=advanced) before implementing this feature. ### Create guest accounts client-side Use the `createGuestAccount` function from the `useGuestAccounts` hook in the React SDK to integrate guest accounts. The `createGuestAccount` function returns an [authenticated `User` object](/user-management/users/the-user-object). ```jsx theme={"system"} // createGuestAccount: () => Promise const {createGuestAccount} = useGuestAccounts(); ``` `createGuestAccount` is an asynchronous call that will create and authenticate users as guests. If the user is already a guest, this call is idempotent. If the user is already *logged in* as a non-guest user, this will throw an error indicating as such. ### Check if a user is a guest To check if a User is a guest account, use the `isGuest` property on the `User` object returned by the `usePrivy` hook. ```jsx theme={"system"} const {user} = usePrivy(); // isGuest: boolean user.isGuest; ``` ### Access a guest user ID To access the guest’s user data including their stable user ID and wallet address, access the user object from the `usePrivy` hook. ```tsx theme={"system"} const {user} = usePrivy(); // Get the user's stable User ID and their wallet address. user.id; user.wallet.address; ``` ### Access a guest user’s embedded wallet To transact with the guest user’s embedded wallet, [use the appropriate wallet from the connected `wallets` array.](/wallets/wallets/get-a-wallet) All embedded wallet functionality that is available for logged-in users is also available to guest users. ```tsx theme={"system"} const {wallets} = useWallets(); const embeddedWallet = getEmbeddedConnectedWallet(wallets); // Get the embedded wallet address or send a transaction. embeddedWallet.address; const provider = await embeddedWallet.getEthereumProvider() provider.request({method: 'eth_sendTransaction', params: [...]}); ``` ### Upgrade a guest user to a logged-in user Simply call `login()` to enable the guest user to upgrade their account to a logged-in account using any authentication method of their choice. ```tsx theme={"system"} // login: (options?) => void const {login} = usePrivy(); ``` ### Enable a guest user to delete their guest session Call `logout()` to enable the guest user to delete their guest session. This is an important interface to support so that users who start a guest session but would prefer to log in with a pre-existing account, are able to do so. ```tsx theme={"system"} // logout: (options?) => void const {logout} = usePrivy(); ``` # Configuring wallet confirmation modals Source: https://docs.privy.io/recipes/react/manage-wallet-UIs Privy allows you to customize showing wallet confirmation modals globally for your application in the Privy Dashboard or in your `PrivyProvider` configuration. This is a guide for configuring wallet confirmation modals for the `react-auth` SDK. ## Dashboard configuration To toggle displaying wallet confirmation modals navigate to the [Configuration > Authentication > Advanced](https://dashboard.privy.io/apps?logins=advanced\&page=login-methods) tab for your app. Here you can toggle the `Disable confirmation modals` option across the entire application. ## `PrivyProvider` configuration The `showWalletUIs` option will override the dashboard configuration if one is set. In your `PrivyProvider` configuration, you can toggle the `showWalletUIs` option to enable or disable wallet confirmation modals across the entire application. ```tsx theme={"system"} ``` ## Customizing wallet confirmation modals for individual function calls The `uiOptions.showWalletUIs` option will override the `PrivyProvider` configuration if one is set. Privy allows you to further customize showing wallet confirmation modals for individual function calls by passing the `uiOptions.showWalletUIs` option to the respective function. Learn more in the following sections: * Ethereum * [`signMessage`](/wallets/using-wallets/ethereum/sign-a-message#param-ui-options) * [`signTransaction`](/wallets/using-wallets/ethereum/sign-a-transaction#param-options-ui-options) * [`signTypedData`](/wallets/using-wallets/ethereum/sign-typed-data#param-ui-options) * [`sendTransaction`](/wallets/using-wallets/ethereum/send-a-transaction#param-options-ui-options) * Solana * [`signAndSendTransaction`](/wallets/using-wallets/solana/send-a-transaction#param-ui-options) * [`signTransaction`](/wallets/using-wallets/solana/sign-a-transaction#param-ui-options) * [`signMessage`](/wallets/using-wallets/solana/sign-a-message#param-ui-options) # Use tokens from OAuth providers Source: https://docs.privy.io/recipes/react/oauth-tokens OAuth token retrieval via the `useOAuthTokens` hook is currently available in the React (`@privy-io/react-auth`) and React Native (`@privy-io/expo`) SDKs only. It is not yet supported in the native Swift, Android, Flutter, or Unity SDKs, or in server-side environments. **To configure callbacks for whenever a user successfully authorizes a third-party OAuth account, use the `useOAuthTokens` hook:** ```tsx theme={"system"} import {useOAuthTokens, type OAuthTokens} from '@privy-io/react-auth'; const {reauthorize} = useOAuthTokens({ // Any logic you'd like to execute with the OAuth tokens onOAuthTokenGrant: ({oAuthTokens}) => { console.log( oAuthTokens.provider, oAuthTokens.accessToken, oAuthTokens.accessTokenExpiresInSeconds, oAuthTokens.refreshToken, oAuthTokens.refreshTokenExpiresInSeconds, oAuthTokens.scopes ); } }); // You may also call `getAccessToken` to get the user's current access token ``` As parameters to **`useOAuthTokens`**, you may include an **`onOAuthTokenGrant`** callback. The component where the **`useOAuthTokens`** hook is invoked **must** be mounted on the component/page the user returns to after authorizing an OAuth flow in order for this callback to execute. Note that having the page lazy load the component with the hook may interfere with execution of the callback. ### onAccessTokenGranted If set, the **`onOAuthTokenGrant`** callback will execute after a user returns to the application from an OAuth flow authorization. This happens in 3 cases: * When the user logs in via an OAuth/social login method, * When a user links a new OAuth account to their user account, * When a successful **`reauthorize`** call is invoked, and the user authorizes an existing OAuth account. Within this callback, you can access: * **`provider`**: the OAuth provider, is one of `'apple'`, `'discord'`, `'github'`, `'google'`, `'linkedin'`, `'spotify'`, `'tiktok'`, `'instagram'`, and `'twitter'`. * **`accessToken`**: the OAuth access token * **`accessTokenExpiresInSeconds`**: the number of seconds until the OAuth access token expires * **`refreshToken`**: the OAuth refresh token * **`refreshTokenExpiresInSeconds`**: the number of seconds until the OAuth refresh token expires. If the refresh token is present and this field is undefined, it is assumed that the refresh token does not have an expiration date * **`scopes`**: the list of OAuth scopes the access token is approved for. Learn more about how to use OAuth access and refresh tokens [here.](https://www.oauth.com/oauth2-servers/access-tokens/) Within this callback, you can also access a `reauthorize` method, which will allow a user to re-authorize an existing OAuth account in order to retrieve more up-to-date OAuth tokens and account metadata. **To configure callbacks for whenever a user successfully authorizes a third-party OAuth account in your React Native app, use the `useOAuthTokens` hook:** ```tsx theme={"system"} import {useOAuthTokens} from '@privy-io/expo'; // The hook takes a single callback that will be triggered when OAuth tokens are granted useOAuthTokens({ onOAuthTokenGrant: (tokens: OAuthTokens) => { console.log( tokens.provider, tokens.accessToken, tokens.accessTokenExpiresInSeconds, tokens.refreshToken, tokens.refreshTokenExpiresInSeconds, tokens.scopes ); } }); ``` The component where the **`useOAuthTokens`** hook is invoked **must** be mounted on the component/page the user returns to after authorizing an OAuth flow in order for this callback to execute. ### onOAuthTokenGrant The **`onOAuthTokenGrant`** callback will execute after a user returns to the application from an OAuth flow authorization. This happens in 3 cases: * When the user logs in via an OAuth/social login method, * When a user links a new OAuth account to their user account, * When a user re-authorizes an existing OAuth account. Within this callback, you can access: * **`provider`**: the OAuth provider, is one of `'apple'`, `'discord'`, `'github'`, `'google'`, `'linkedin'`, `'spotify'`, `'tiktok'`, `'instagram'`, and `'twitter'`. * **`accessToken`**: the OAuth access token * **`accessTokenExpiresInSeconds`**: the number of seconds until the OAuth access token expires * **`refreshToken`**: the OAuth refresh token * **`refreshTokenExpiresInSeconds`**: the number of seconds until the OAuth refresh token expires. If the refresh token is present and this field is undefined, it is assumed that the refresh token does not have an expiration date * **`scopes`**: the list of OAuth scopes the access token is approved for. In React Native, OAuth tokens are securely stored in the Expo Secure Store, which is backed by the Keychain on iOS and EncryptedSharedPreferences on Android. This ensures that sensitive OAuth tokens are properly protected on mobile devices. Unlike the React version, the React Native `useOAuthTokens` hook does not return a `reauthorize` method. To reauthorize an OAuth account in React Native, you should use the `login` method from the `usePrivy` hook. # Wallet list configuration recipes Source: https://docs.privy.io/recipes/react/wallet-list-configurations Common wallet list configurations with examples and screenshots This guide provides practical examples for configuring your `walletList` to achieve common desired wallet connection setups. Each recipe includes the configuration code and explains when to use it. The `walletList` controls which wallet options appear in Privy's connection modal and in what order. For detailed API reference, see [Configure wallet options](/wallets/connectors/setup/configuring-external-connector-wallets). ## Quick Decision Guide **What do you want to show users?** * **Show ALL WalletConnect wallets (100+)** → Use `wallet_connect` * **Show a QR code for Ethereum wallets** → Use `wallet_connect_qr` * **Show a QR code for Solana wallets** → Use `wallet_connect_qr_solana` * **Show only specific wallets** → List them explicitly (e.g., `['phantom', 'metamask']`) * **Show all detected browser extensions** → Use `detected_ethereum_wallets` / `detected_solana_wallets` *** ## Recipe 1: Ethereum Wallets with Popular Options **Use case**: Standard Ethereum app with popular wallet options plus WalletConnect fallback ```tsx theme={"system"} {children} ``` **What users see:** 1. MetaMask (if installed, shown first) 2. Coinbase Wallet 3. Rainbow 4. Any other detected Ethereum browser extensions (alphabetically) 5. WalletConnect button (shows QR code when clicked) **Best for:** * Ethereum-focused applications * Desktop users with browser extensions * Apps that want to support mobile wallets via QR codes *** ## Recipe 2: Solana Wallets Only **Use case**: Solana-focused application with popular Solana wallets ```tsx theme={"system"} import {toSolanaWalletConnectors} from '@privy-io/react-auth/solana'; const solanaConnectors = toSolanaWalletConnectors({ shouldAutoConnect: true }); {children} ; ``` **Required for Solana**: You must configure `externalWallets.solana.connectors` and set `walletChainType` to `'solana-only'` or `'ethereum-and-solana'` for Solana wallets to appear. **What users see:** 1. Phantom (prioritized first) 2. Solflare 3. Backpack 4. Any other detected Solana browser extensions 5. WalletConnect button for Solana wallets **Best for:** * Solana-only applications * NFT marketplaces on Solana * Solana DeFi applications *** ## Recipe 3: Multi-Chain (Ethereum + Solana) **Use case**: Cross-chain application supporting both Ethereum and Solana ```tsx theme={"system"} import {toSolanaWalletConnectors} from '@privy-io/react-auth/solana'; const solanaConnectors = toSolanaWalletConnectors({ shouldAutoConnect: true }); {children} ; ``` **What users see:** * Both Ethereum and Solana wallets in one list * Solana wallets display with a "Solana" badge * Two separate WalletConnect options (one for each chain) **Best for:** * Cross-chain applications * Portfolio tracking apps * Multi-chain DeFi platforms When `walletChainType` is set to `'ethereum-and-solana'`, Privy automatically adds badges to Solana wallets to help users distinguish between chain types. *** ## Recipe 4: Minimal Setup - Just Phantom and WalletConnect **Use case**: Solana app that primarily uses Phantom with WalletConnect fallback ```tsx theme={"system"} import {toSolanaWalletConnectors} from '@privy-io/react-auth/solana'; const solanaConnectors = toSolanaWalletConnectors({ shouldAutoConnect: true }); {children} ; ``` **What users see:** * Only Phantom and WalletConnect options * Clean, minimal UI * No other wallet options displayed **Best for:** * Apps with strong Phantom user base * Simplified onboarding flows * Mobile-first Solana applications **Warning**: Users with other wallets (like Solflare or Backpack) won't see connection options unless they use WalletConnect. *** ## Recipe 5: Show All WalletConnect Registry Wallets **Use case**: Maximum wallet compatibility - show every WalletConnect-supported wallet ```tsx theme={"system"} {children} ``` **`wallet_connect` shows 100+ wallets!** This entry displays ALL wallets from the WalletConnect registry as individual, searchable options. If you want a simple QR code instead, use `wallet_connect_qr`. **What users see:** * MetaMask, Coinbase Wallet, and Rainbow at the top * Followed by a long, searchable list of 100+ WalletConnect-supported wallets * Each wallet has its own connection button **Best for:** * Apps needing maximum wallet compatibility * Desktop applications where users browse wallet options * Supporting niche or regional wallets **Not recommended for:** * Mobile-first applications (UI becomes cluttered) * Simple onboarding flows * Apps with specific wallet preferences *** ## Recipe 6: Desktop-Optimized with Auto-Detection **Use case**: Desktop app that auto-detects all installed browser extensions ```tsx theme={"system"} {children} ``` **What users see:** 1. All detected browser extensions (both Ethereum and Solana) 2. Full WalletConnect registry for any wallet not installed 3. Completely dynamic wallet list based on what's installed **Best for:** * Desktop-focused applications * Power users with multiple wallets installed * Maximum flexibility without manual configuration **Note**: Wallets aren't injected in mobile web browser environments, with the exception of the in-app browser for a few wallets (see [In-App Browsers](#in-app-browsers) section). Thus, `detected_*_wallets` will show empty in mobile environments. Consider adding specific wallet names (like `'metamask'`, `'phantom'`) for mobile support. *** ## Recipe 7: Prioritize Specific Wallet + Show Others **Use case**: Promote a specific wallet but still support others ```tsx theme={"system"} import {toSolanaWalletConnectors} from '@privy-io/react-auth/solana'; const solanaConnectors = toSolanaWalletConnectors({ shouldAutoConnect: true }); {children} ; ``` **How ordering works:** * Phantom **always** appears first * Backpack **always** appears second * Other detected wallets appear after, alphabetically * If Phantom is detected, it appears at position 1 (not duplicated in detected list) **Best for:** * Apps with wallet partnerships * Promoting recommended wallets * Maintaining flexibility while guiding users *** ## Recipe 8: WalletConnect Fallback for Unlisted Wallets **Use case**: Support specific wallets but provide fallback for others ```tsx theme={"system"} {children} ``` **What users see:** 1. MetaMask, Rainbow, and Coinbase Wallet prominently displayed 2. WalletConnect option showing 100+ additional wallets 3. Support for niche wallets not in the detected list **Best for:** * Supporting wallets not yet in WalletConnect's detection system * International users with regional wallets * Providing comprehensive coverage without cluttering the main list This approach gives you the best of both worlds: prominent display of preferred wallets plus comprehensive fallback support through WalletConnect. *** ## Platform Considerations ### Mobile Browser Limitations Wallets aren't injected in mobile web browser environments, with the exception of the in-app browser for a few wallets (see [In-App Browsers](#in-app-browsers) section below). Thus, adding `detected_ethereum_wallets` or `detected_solana_wallets` will show empty in mobile environments. On mobile: * ✅ **Works**: Specific wallet names (like `'metamask'`, `'phantom'`), `wallet_connect` (shows full registry) * ❌ **Doesn't work**: `wallet_connect_qr`, `wallet_connect_qr_solana`, `detected_ethereum_wallets`, `detected_solana_wallets` **Recommended mobile configuration:** ```tsx theme={"system"} walletList: [ 'metamask', // Shows "Open in MetaMask" button 'phantom', 'rainbow' ]; ``` ### In-App Browsers On mobile, some wallets will connect via the in-app browser of that wallet's mobile app. These wallets include: * **Phantom** (Ethereum and Solana) * **Backpack** (Ethereum and Solana) * **OKX Wallet** (Ethereum and Solana) * **Solflare** (Solana only) * **Jupiter Wallet** (Solana only) When users access your app through one of these wallet's built-in browsers: * The wallet is **automatically detected and prioritized** * No additional configuration needed * Appears first regardless of `walletList` order ### Runtime Configuration You can dynamically change the wallet list based on platform: ```tsx theme={"system"} const {connectWallet} = usePrivy(); const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); const openWalletModal = () => { connectWallet({ walletList: isMobile ? ['metamask', 'phantom', 'rainbow', 'coinbase_wallet'] // Specific wallets for mobile : ['metamask', 'rainbow', 'coinbase_wallet', 'detected_ethereum_wallets', 'wallet_connect_qr'] // QR code for desktop }); }; ``` *** ## Common Patterns Summary | Pattern | Configuration | Use Case | | ------------------------- | ------------------------------------------------------- | ------------------------------- | | **Simple Ethereum** | `['metamask', 'coinbase_wallet', 'wallet_connect_qr']` | Standard Ethereum app (desktop) | | **Simple Solana** | `['phantom', 'solflare', 'wallet_connect_qr_solana']` | Standard Solana app (desktop) | | **Maximum Compatibility** | `['detected_ethereum_wallets', 'wallet_connect']` | Support all wallets | | **Mobile-First** | `['metamask', 'phantom', 'rainbow', 'coinbase_wallet']` | Mobile-optimized | | **Minimal** | `['phantom', 'metamask']` | Specific wallets only | | **Multi-Chain** | Both Ethereum and Solana entries + both chain types | Cross-chain apps | *** ## Need More Help? * **Can't find your wallet?** See [Troubleshooting wallet visibility](/wallets/connectors/setup/configuring-external-connector-wallets#why-isnt-my-wallet-showing-up) * **WalletConnect confusion?** See [WalletConnect FAQ](/wallets/connectors/setup/configuring-external-connector-wallets#walletconnect-configuration-faq) * **Custom configurations?** See [Configure wallet options](/wallets/connectors/setup/configuring-external-connector-wallets) # Whitelabel Source: https://docs.privy.io/recipes/react/whitelabel The Privy React SDK provides complete control over all interfaces for authentication, embedded wallets, and user management. You can customize the user experience to match your brand while maintaining the security and reliability of Privy's infrastructure. The fastest way to get started with whitelabeling is to fork our [whitelabel starter repository](https://github.com/privy-io/examples/tree/main/privy-react-whitelabel-starter). This template provides a fully customizable foundation that you can build upon. ## What you can customize Whitelabel login and MFA with your own UI and branding. Create seamless wallet interactions with your own UI components and styling. Manage user profiles and connect social accounts your way. ## Whitelabeling your app Privy allows developers to choose when to take advantage of Privy's UI and when to customize the experience with their own UI. This guide walks through how to whitelabel your app. ### Authentication All of Privy's authentication flows can be whitelabeled, from email and SMS passwordless flows to social logins and passkeys. To whitelabel Privy's passwordless email flow, use the `useLoginWithEmail` hook. Then, call `sendCode` and `loginWithCode` with the desired email address. ```tsx theme={"system"} import {useLoginWithEmail} from '@privy-io/react-auth'; ``` ```tsx theme={"system"} const {sendCode, loginWithCode} = useLoginWithEmail(); sendCode({email: 'test@test.com'}); loginWithCode({code: '123456'}); ``` Learn more about [email authentication and tracking login flow state](/authentication/user-authentication/login-methods/email). To whitelabel the passwordless SMS flow, use the `useLoginWithSms` hook. Then, call `sendCode` and `loginWithCode` with the desired phone number. ```tsx theme={"system"} import {useLoginWithSms} from '@privy-io/react-auth'; ``` ```tsx theme={"system"} const {sendCode, loginWithCode} = useLoginWithSms(); sendCode({phoneNumber: '+1234567890'}); loginWithCode({code: '123456'}); ``` Learn more about [SMS authentication and tracking login flow state](/authentication/user-authentication/login-methods/sms). To whitelabel social login, use the `useLoginWithOAuth` hook and call `initOAuth` with your desired social login provider. ```tsx theme={"system"} import {useLoginWithOAuth} from '@privy-io/react-auth'; ``` ```tsx theme={"system"} const {initOAuth} = useLoginWithOAuth(); initOAuth({provider: 'google'}); ``` Learn more about [social logins and tracking login flow state](/authentication/user-authentication/login-methods/oauth). To whitelabel passkeys, use the `useLoginWithPasskey` hook and call `loginWithPasskey`. ```tsx theme={"system"} import {useLoginWithPasskey} from '@privy-io/react-auth'; ``` ```tsx theme={"system"} const {loginWithPasskey} = useLoginWithPasskey(); loginWithPasskey(); ``` To sign up with a passkey: ```tsx theme={"system"} import {useSignupWithPasskey} from '@privy-io/react-auth'; ``` ```tsx theme={"system"} const {signupWithPasskey} = useSignupWithPasskey(); signupWithPasskey(); ``` To link a passkey to an existing user: ```tsx theme={"system"} import {useLinkWithPasskey} from '@privy-io/react-auth'; ``` ```tsx theme={"system"} const {linkWithPasskey} = useLinkWithPasskey(); linkWithPasskey(); ``` Learn more about [passkeys and tracking login flow state](/authentication/user-authentication/login-methods/passkey). To whitelabel the Telegram login flow, use the `useLoginWithOAuth` hook and call `initOAuth` with the `'telegram'` provider. ```tsx theme={"system"} import {useLoginWithOAuth} from '@privy-io/react-auth'; ``` ```tsx theme={"system"} const {initOAuth} = useLoginWithOAuth(); initOAuth({provider: 'telegram'}); ``` Learn more about [Telegram authentication and tracking login flow state](/authentication/user-authentication/login-methods/oauth). To whitelabel MFA with SMS, TOTP, or passkeys, follow the [custom UI guide](/authentication/user-authentication/mfa/custom-ui). ### Wallets Privy enables developers to whitelabel embedded wallet functionality. You can abstract away wallet UIs entirely or selectively use Privy's default UI for specific flows. To whitelabel embedded wallets, you can configure this globally across your app in the `PrivyProvider` config, or selectively for specific flows at runtime. In your `PrivyProvider` config you can control the default wallet UI for all flows in your app. ```tsx {5} theme={"system"} ``` For more granular control, you can also control wallet UIs for specific flows in the sections below. Privy supports whitelabeling wallet creation for Ethereum, Solana, and other chains. ```tsx theme={"system"} import {useCreateWallet} from '@privy-io/react-auth'; ``` ```tsx theme={"system"} const {createWallet} = useCreateWallet(); createWallet(); ``` ```tsx theme={"system"} import {useWallets} from '@privy-io/react-auth/solana'; ``` ```tsx theme={"system"} const {createWallet} = useWallets(); createWallet(); ``` ```tsx theme={"system"} import {useCreateWallet} from '@privy-io/react-auth/extended-chains'; ``` ```tsx theme={"system"} const {createWallet} = useCreateWallet(); const {user, wallet} = await createWallet({chainType: 'cosmos'}); // or 'stellar', 'sui', etc. ``` To whitelabel Privy's message signing functionality, use the `useSignMessage` hook and call `signMessage` with your desired message. ```tsx theme={"system"} import {useSignMessage} from '@privy-io/react-auth'; ``` ```tsx theme={"system"} const {signMessage} = useSignMessage(); const signature = await signMessage( {message: 'Hello, world!'}, {uiOptions: {showWalletUIs: false}} ); ``` ```tsx theme={"system"} import {useSignMessage} from '@privy-io/react-auth/solana'; ``` ```tsx theme={"system"} const {signMessage} = useSignMessage(); signMessage({ message: 'messageinUint8Array', options: {uiOptions: {showWalletUIs: false}} }); ``` ```tsx theme={"system"} import {useSignRawHash} from '@privy-io/react-auth/extended-chains'; ``` ```tsx theme={"system"} const {signature} = await signRawHash({ address: 'insert-wallet-address', chainType: 'cosmos', // or 'stellar', 'sui', etc. hash: '0x1acab030f479bda7829de07e9db4138cec5d38574df17d65af1617b7268541c0' }); ``` To whitelabel Privy's transaction sending functionality, use the `useSendTransaction` hook and call `sendTransaction` with your desired transaction. ```tsx theme={"system"} import {useSendTransaction} from '@privy-io/react-auth'; ``` ```tsx theme={"system"} const {sendTransaction} = useSendTransaction(); sendTransaction( { to: '0xE3070d3e4309afA3bC9a6b057685743CF42da77C', value: 100000 }, { uiOptions: {showWalletUIs: false} } ); ``` ```tsx theme={"system"} import {useSendTransaction} from '@privy-io/react-auth/solana'; ``` ```tsx theme={"system"} const {sendTransaction} = useSendTransaction(); sendTransaction({ transaction: 'insert-solana-transaction', uiOptions: {showWalletUIs: false} }); ``` ### User management Privy supports whitelabeling user management for linking and unlinking accounts. To whitelabel linking social accounts, use the `useLinkAccount` hook and call `link`. ```tsx theme={"system"} import {useLinkAccount} from '@privy-io/react-auth'; ``` ```tsx theme={"system"} const {linkGoogle, linkTwitter} = useLinkAccount(); linkGoogle(); linkTwitter(); ``` To link [additional OAuth providers](/authentication/user-authentication/login-methods/custom-oauth) that are not natively supported by Privy, use the `linkOAuth` method from the `useLinkAccount` hook. For built-in providers like Google or Twitter, use the dedicated methods (e.g., `linkGoogle`, `linkTwitter`). ```tsx theme={"system"} import {useLinkAccount} from '@privy-io/react-auth'; ``` ```tsx theme={"system"} const {linkOAuth} = useLinkAccount(); linkOAuth({provider: 'custom:twitch'}); ``` ### Parameters The `linkOAuth` method accepts an object with the following fields: The additional OAuth provider to link, in the format `'custom:'` (e.g., `'custom:twitch'`). ### Usage ```tsx theme={"system"} import {useLinkAccount} from '@privy-io/react-auth'; function LinkTwitchButton() { const {linkOAuth} = useLinkAccount({ onSuccess: ({user, linkMethod, linkedAccount}) => { console.log('Linked account:', linkedAccount); }, onError: (error) => { console.error('Failed to link account:', error); } }); return ( ); } ``` To whitelabel linking wallets, use the `useLinkWithSiwe` hook for Ethereum wallets or `useLinkWithSiws` hook for Solana wallets. These hooks allow you to generate messages, request signatures, and link wallets without using Privy's modal UI. To link an Ethereum wallet to a user via [SIWE](https://eips.ethereum.org/EIPS/eip-4361), use the React SDK's `useLinkWithSiwe` hook. ### Generate SIWE message ```tsx theme={"system"} generateSiweMessage({ address: string, chainId: string }) => Promise ``` EIP-55 checksum-encoded wallet address performing the signing. The chain ID to which the session is bound, in [CAIP-2 format](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md), e.g. `'eip155:1'`. ### Sign the SIWE message Request an EIP-191 `personal_sign` signature for the `message` returned by `generateSiweMessage` from the wallet. ```tsx theme={"system"} import {useWallets} from '@privy-io/react-auth'; const {wallets} = useWallets(); const signature = await wallets[0].sign(message); ``` Alternatively, you can request a signature from any external wallet or smart account: ```tsx theme={"system"} const signature = await wallet.signMessage({message}); ``` ### Link with SIWE ```tsx theme={"system"} linkWithSiwe({ signature: string, message: string, chainId: string, walletClientType?: string, connectorType?: string }) => Promise ``` The EIP-191 signature corresponding to the message. The EIP-4361 message returned by `generateSiweMessage`. The same [CAIP-2 formatted](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md) chain ID you passed to `generateSiweMessage`, e.g. `'eip155:1'`. Optional. The wallet client of the external wallet (e.g., `'metamask'`, `'coinbase_wallet'`). Defaults to `null` if not specified. Optional. The method used to connect the wallet to the application (e.g., `'injected'`, `'wallet_connect_v2'`). Defaults to `null` if not specified. ### Usage ```tsx theme={"system"} import {useLinkWithSiwe, useWallets} from '@privy-io/react-auth'; export function LinkWalletButton() { const {generateSiweMessage, linkWithSiwe} = useLinkWithSiwe(); const {wallets} = useWallets(); const handleLink = async () => { if (!wallets?.length) return; const activeWallet = wallets[0]; const message = await generateSiweMessage({ address: activeWallet.address, chainId: 'eip155:1' }); const signature = await activeWallet.sign(message); await linkWithSiwe({ message, chainId: 'eip155:1', signature }); }; return ; } ``` ### Callbacks You can optionally pass callbacks into `useLinkWithSiwe`: ```tsx theme={"system"} const {generateSiweMessage, linkWithSiwe} = useLinkWithSiwe({ onSuccess: ({user, linkMethod, linkedAccount}) => { console.log('Wallet linked successfully', linkedAccount); }, onError: (error) => { console.error('Failed to link wallet', error); } }); ``` To link a Solana wallet to a user via [SIWS](https://github.com/phantom/sign-in-with-solana), use the React SDK's `useLinkWithSiws` hook. ### Generate SIWS message ```tsx theme={"system"} generateSiwsMessage({ address: string }) => Promise ``` The Solana wallet address performing the signing. ### Sign the SIWS message Request a signature for the `message` returned by `generateSiwsMessage` from the Solana wallet. The message needs to be encoded as Uint8Array for signing. ```tsx theme={"system"} import {useWallets} from '@privy-io/react-auth/solana'; const {wallets} = useWallets(); const encodedMessage = new TextEncoder().encode(message); const results = await wallets[0].signMessage({message: encodedMessage}); ``` ### Link with SIWS ```tsx theme={"system"} linkWithSiws({ message: string, signature: string, walletClientType?: string, connectorType?: string }) => Promise<{ user: User; linkedAccount: LinkedAccountWithMetadata | null }> ``` The SIWS message returned from `generateSiwsMessage`. The signature corresponding to the message. Convert the signature bytes from the wallet's `signMessage` method to a base64-encoded string using `Buffer.from(results.signature).toString('base64')`. Optional. A string indicating the wallet client you'd like to associate with the wallet. Defaults to `'privy'`. Optional. A string indicating the connector type you'd like to associate with the wallet. Defaults to `'privy'`. ### Usage ```tsx theme={"system"} import {useLinkWithSiws} from '@privy-io/react-auth'; import {useWallets} from '@privy-io/react-auth/solana'; export function LinkSolanaWalletButton() { const {generateSiwsMessage, linkWithSiws} = useLinkWithSiws(); const {wallets} = useWallets(); const handleLink = async () => { if (!wallets?.length) return; const activeWallet = wallets[0]; const message = await generateSiwsMessage({ address: activeWallet.address }); const encodedMessage = new TextEncoder().encode(message); const results = await activeWallet.signMessage({message: encodedMessage}); // Convert signature bytes to string (base64) const signatureBase64 = Buffer.from(results.signature).toString('base64'); await linkWithSiws({ message, signature: signatureBase64 }); }; return ; } ``` ### Callbacks You can optionally pass callbacks into `useLinkWithSiws`: ```tsx theme={"system"} const {generateSiwsMessage, linkWithSiws} = useLinkWithSiws({ onSuccess: ({user, linkMethod, linkedAccount}) => { console.log('Solana wallet linked successfully', linkedAccount); }, onError: (error) => { console.error('Failed to link Solana wallet', error); } }); ``` To whitelabel updating a user's email address, use the `useUpdateEmail` hook: ```tsx theme={"system"} import {useUpdateEmail} from '@privy-io/react-auth'; const {state, sendCode, verifyCode} = useUpdateEmail(); ``` ### Send an OTP First, use the `sendCode` method to send an OTP verification code to the user's new email address: ```tsx theme={"system"} sendCode: ({newEmailAddress: string}) => Promise; ``` The new email address to send the verification code to. This sends a one-time passcode to the new email address, which the user must enter to verify and confirm the update. ### Verify the OTP Prompt the user for the OTP they received and verify it using the `verifyCode` method: ```tsx theme={"system"} verifyCode: ({code: string}) => Promise<{user: User} | undefined>; ``` The one-time code received on the new email address. The updated user object if the update was successful. ### State The `state` property provides the current state of the OTP flow: | Status | Description | | ----------------------- | --------------------------------------------- | | `'initial'` | The flow has not started | | `'sending-code'` | The code is being sent | | `'awaiting-code-input'` | Waiting for the user to enter the code | | `'submitting-code'` | The code is being verified | | `'done'` | The email was updated successfully | | `'error'` | An error occurred (includes an `error` field) | ### Usage ```tsx theme={"system"} import {useState} from 'react'; import {useUpdateEmail} from '@privy-io/react-auth'; function UpdateEmailForm() { const {state, sendCode, verifyCode} = useUpdateEmail(); const [newEmailAddress, setNewEmailAddress] = useState(''); const [code, setCode] = useState(''); if (state.status === 'initial' || state.status === 'sending-code') { return (
setNewEmailAddress(e.target.value)} placeholder="New email address" />
); } return (
setCode(e.target.value)} placeholder="Enter verification code" />
); } ``` ### Callbacks You can optionally pass callbacks into `useUpdateEmail`: ```tsx theme={"system"} const {state, sendCode, verifyCode} = useUpdateEmail({ onSuccess: ({user, updateMethod, updatedAccount}) => { console.log('Email updated successfully', user); }, onError: (error, details) => { console.error('Failed to update email', error, details); } }); ``` Optional callback that executes after a successful email update. Receives the updated user object, the update method (`'email'`), and the updated account. Optional callback that executes if there is an error during the email update flow.
To link a custom JWT account to an existing user, use the `useLinkJwtAccount` hook. This is useful for integrating with external authentication systems that issue JWTs. ```tsx theme={"system"} import {useLinkJwtAccount} from '@privy-io/react-auth'; ``` ```tsx theme={"system"} const {linkWithCustomJwt, state} = useLinkJwtAccount(); ``` ### Link with custom JWT ```tsx theme={"system"} linkWithCustomJwt(jwt: string) => Promise<{user: User}> ``` The JWT token from your external authentication system to link to the user's account. ### State The `state` property tracks the current state of the JWT linking flow: | Status | Description | | --------------- | ------------------------------------------- | | `'initial'` | The flow has not started | | `'loading'` | The JWT is being verified and linked | | `'not-enabled'` | Custom JWT auth is not enabled for this app | | `'done'` | The account was linked successfully | | `'error'` | An error occurred | ### Callbacks You can optionally pass callbacks into `useLinkJwtAccount`: ```tsx theme={"system"} const {linkWithCustomJwt, state} = useLinkJwtAccount({ onSuccess: ({user, linkMethod, linkedAccount}) => { console.log('JWT account linked successfully', linkedAccount); }, onError: (error) => { console.error('Failed to link JWT account', error); } }); ``` ### Usage ```tsx theme={"system"} import {useLinkJwtAccount} from '@privy-io/react-auth'; function LinkJwtAccountButton() { const {linkWithCustomJwt, state} = useLinkJwtAccount({ onSuccess: ({user, linkedAccount}) => { console.log('Account linked:', linkedAccount); }, onError: (error) => { console.error('Link failed:', error); } }); const handleLink = async () => { const jwt = await getJwtFromExternalAuth(); await linkWithCustomJwt(jwt); }; return ( ); } ``` Custom JWT authentication must be enabled in your Privy Dashboard before using this hook. See the [JWT-based authentication documentation](/authentication/user-authentication/jwt-based-auth/setup) for setup instructions. To whitelabel unlinking an account, use the dedicated unlink hooks from `@privy-io/react-auth`: ```tsx theme={"system"} import {useUnlinkEmail, useUnlinkWallet, useUnlinkOAuth} from '@privy-io/react-auth'; ``` ```tsx theme={"system"} const {unlink: unlinkEmail} = useUnlinkEmail(); const {unlink: unlinkWallet} = useUnlinkWallet(); const {unlink: unlinkOAuth} = useUnlinkOAuth(); // Unlink by passing the relevant identifier unlinkEmail({address: 'user@example.com'}); unlinkOAuth({provider: 'google', subject: 'google-subject-id'}); unlinkWallet({address: '0x...'}); ``` See the [unlinking accounts guide](/user-management/users/unlinking-accounts) for the full list of available hooks and parameters. To unlink any OAuth provider, including built-in providers (e.g., `'google'`, `'twitter'`) and [additional OAuth providers](/authentication/user-authentication/login-methods/custom-oauth) (e.g., `'custom:twitch'`), use the `useUnlinkOAuth` hook. ```tsx theme={"system"} import {useUnlinkOAuth} from '@privy-io/react-auth'; ``` ```tsx theme={"system"} const {unlink: unlinkOAuth} = useUnlinkOAuth(); unlinkOAuth({provider: 'custom:twitch', subject: '12345'}); ``` ### Parameters The `unlink` method from `useUnlinkOAuth` accepts an object with the following fields: The OAuth provider to unlink. Use a built-in provider (e.g., `'google'`, `'twitter'`) or a custom provider in the format `'custom:'` (e.g., `'custom:twitch'`). The provider-specific subject identifier that uniquely identifies the user for the selected OAuth provider. This can be found in the linked account's `subject` field. ### Usage ```tsx theme={"system"} import {usePrivy} from '@privy-io/react-auth'; import {useUnlinkOAuth} from '@privy-io/react-auth'; function UnlinkTwitchButton() { const {user} = usePrivy(); const {unlink: unlinkOAuth} = useUnlinkOAuth(); // Find the custom OAuth account to unlink const twitchAccount = user?.linkedAccounts?.find((account) => account.type === 'custom:twitch'); const handleUnlink = () => { if (twitchAccount) { unlinkOAuth({ provider: 'custom:twitch', subject: twitchAccount.subject }); } }; return ( ); } ``` ## Resources Fork our starter repository to begin building your custom Privy integration. See a live demo of a whitelabeled app. # Worldcoin mini app SIWE with Privy Source: https://docs.privy.io/recipes/react/worldcoin-siwe-guide Privy offers a seamless integration with Worldcoin Mini Apps. This guide will walk you through integrating Sign-In With Ethereum (SIWE) using Privy in a Worldcoin Mini App. With this setup, you can offer secure wallet authentication for your users—combining the power of World App's native wallet with Privy's flexible authentication and wallet infrastructure. ### Resources Official documentation for building Worldcoin Mini Apps. Register your mini app and manage API keys. Learn how to set up Privy in your React app. ## Configure your Worldcoin developer portal Create a Worldcoin developer account and mini app, learn more [here](https://docs.world.org/mini-apps/quick-start/installing). 1. Go to the [Worldcoin Developer Portal](https://developer.worldcoin.org/). 2. Sign in and create a new team if you haven't already. 3. Create a new mini app for your project. 1. In your team dashboard, select your mini app. 2. Copy your **App ID** (you'll need this for your app config). 3. Go to the [API Keys page](https://developer.worldcoin.org/teams/\{TEAM_ID}/api-keys) and create a new API key for your app. 4. Save your API key securely. ## Get set up with Privy If you haven't set up Privy yet, follow our [React quickstart guide](/basics/react/installation) to get your app ID and configure your app. Privy's React SDK provides a secure way to authenticate users and manage wallets in your frontend application. Learn more about [getting started with React](/basics/react/installation). ## Set up with Worldcoin Mini App Scaffold a new Worldcoin Mini App using the official template: ``` npx @worldcoin/create-mini-app@latest my-mini-app ``` Follow the prompts in the README to set up your app. Use the env variables from your Worldcoin developer portal to configure your app. Your new app will have a file at `src/components/AuthButton/index.tsx`—this is where you'll add Privy SIWE support. ## SIWE into mini app with Privy Use Privy's `useLoginWithSiwe` hook to authenticate users with their World App wallet. The SIWE flow works as follows: Generate a unique nonce using `generateSiweNonce()` to ensure the SIWE message is secure. Pass the nonce to World MiniKit's `walletAuth()` command, which prompts the user to sign a SIWE message in their World App wallet. Send the signed message and signature back to Privy using `loginWithSiwe()` to complete the authentication flow. ### Implementation Use Privy's `useLoginWithSiwe` hook to authenticate users via their World wallet: ```tsx theme={"system"} import {useLoginWithSiwe} from '@privy-io/react-auth'; import MiniKit from '@worldcoin/minikit-js'; const {generateSiweNonce, loginWithSiwe} = useLoginWithSiwe(); const handleLogin = async () => { // Get nonce from Privy const privyNonce = await generateSiweNonce(); // Request signature from World wallet const {finalPayload} = await MiniKit.commandsAsync.walletAuth({ nonce: privyNonce }); // Log in with Privy await loginWithSiwe({ message: finalPayload.message, signature: finalPayload.signature }); }; ``` ### Access user data Once logged in, you can access the user's World information and wallet data: ```typescript {skip-check} theme={"system"} const userInfo = await MiniKit.getUserByAddress(user.wallet.address); ``` Your Worldcoin Mini App now supports SIWE authentication with Privy and World App! Enjoy seamless, secure onboarding for your users 🚀 # Send to Africa with Zuba Source: https://docs.privy.io/recipes/send-to-africa How to integrate Privy and Zuba to pay out USDC and USDT to bank accounts and mobile money across Africa [Zuba](https://zuba.com) is Privy's preferred partner for African-corridor payouts. With Privy powering your wallets and Zuba handling the last mile, you can move stablecoins into local currency across Africa. This recipe walks through the full flow: 1. Create a wallet with Privy 2. Fund your Zuba account with USDC/USDT from that wallet 3. Call the Zuba Payout API to pay out across Africa 4. Track the payout to completion ## Prerequisites * A Privy app with your **App ID** and **App Secret** from the [Privy Dashboard](https://dashboard.privy.io) * A Zuba account with API credentials (see [Zuba Authentication](https://docs.zuba.com/authentication)) Zuba uses OAuth 2.0 client credentials. Exchange your Client ID and Secret for a bearer token (valid 24 hours): ```bash cURL theme={"system"} curl -X POST "https://zuba-test.us.auth0.com/oauth/token" \ -H "Content-Type: application/json" \ -d '{ "client_id": "YOUR_ZUBA_CLIENT_ID", "client_secret": "YOUR_ZUBA_CLIENT_SECRET", "audience": "https://api.zuba.com", "grant_type": "client_credentials" }' ``` Use the returned `access_token` as `Authorization: Bearer ` on every Zuba request below. ## 1. Create a wallet with Privy If your users already have Privy embedded wallets, skip ahead. Any Privy wallet holding USDC or USDT works. To create a server wallet: ```bash cURL theme={"system"} curl -X POST "https://api.privy.io/v1/wallets" \ -u "PRIVY_APP_ID:PRIVY_APP_SECRET" \ -H "privy-app-id: PRIVY_APP_ID" \ -H "Content-Type: application/json" \ -d '{"chain_type": "ethereum"}' ``` See [Privy wallet docs](/wallets/overview) for embedded and user-owned wallet setups. ## 2. Fund your Zuba account Provision a deposit address on the network you want to fund from: ```bash cURL theme={"system"} curl -X POST "https://api.zuba.com/v1/deposits/crypto-address" \ -H "Authorization: Bearer YOUR_ZUBA_TOKEN" \ -H "Content-Type: application/json" \ -d '{"network": "eip155:1"}' ``` ```json Response theme={"system"} { "network": "eip155:1", "address": "0x1234567890abcdef1234567890abcdef12345678" } ``` The address is reusable, so you can send to it as many times as you like. For EVM networks, the same address is valid on all supported EVM chains. Supported networks and tokens: | Network | `network` value | Tokens | | -------- | ----------------------------------------- | ---------- | | Ethereum | `eip155:1` | USDC, USDT | | Base | `eip155:8453` | USDC | | Solana | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | USDC, USDT | | Tron | `tron:mainnet` | USDT | Now send USDC/USDT from your Privy wallet to that address using Privy's transaction APIs. See [sending transactions on Ethereum](/wallets/using-wallets/ethereum/send-a-transaction) or [Solana](/wallets/using-wallets/solana/send-a-transaction). Once the transfer confirms on-chain, the funds appear on your Zuba balance: ```bash cURL theme={"system"} curl -X GET "https://api.zuba.com/ledger/balances" \ -H "Authorization: Bearer YOUR_ZUBA_TOKEN" ``` ## 3. Pay out across Africa One request does it all: pass the beneficiary's local payment details inline. `inputCurrency` is the balance you're paying from (your USDC/USDT), while `currency` and `amount` are what the beneficiary receives. Zuba handles the conversion: ```bash cURL theme={"system"} curl -X POST "https://api.zuba.com/v1/payouts" \ -H "Authorization: Bearer YOUR_ZUBA_TOKEN" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: 8f7d2a4e-payout-001" \ -d '{ "clientRef": "PAYOUT-001", "amount": "150000.00", "inputCurrency": "USDC", "currency": "NGN", "route": "bank_transfer", "beneficiary": { "name": "Amara Okafor", "country": "NG", "accounts": [ { "type": "bank_account", "currency": "NGN", "data": { "bankCode": "044", "crAccount": "1234567890" } } ] }, "reference": "Salary March", "description": "Monthly payroll" }' ``` For repeat payouts, create the beneficiary once via [`POST /v1/beneficiaries`](https://docs.zuba.com/concepts/payouts#beneficiaries) and reference it with `"beneficiary": { "id": "..." }`. Use `route: "mobile_money"` for mobile money corridors (GHS, XOF, XAF). Field requirements per corridor are available at [`GET /v1/payouts/requirements`](https://docs.zuba.com/api-reference) and in the [Zuba payout docs](https://docs.zuba.com/concepts/payouts). ## 4. Track the payout Register a webhook endpoint and listen for payout lifecycle events: ```bash cURL theme={"system"} curl -X POST "https://api.zuba.com/v1/webhooks" \ -H "Authorization: Bearer YOUR_ZUBA_TOKEN" \ -H "Content-Type: application/json" \ -d '{"url": "https://yourapp.com/webhooks/zuba", "events": ["payout.paid", "payout.failed"]}' ``` ```json Event payload theme={"system"} { "id": "evt_a1b2c3d4-e5f6-7890-abcd-ef1234567890", "type": "payout.paid", "createdAt": "2026-03-23T14:30:00.000Z", "data": { "id": "pay_abc123", "clientRef": "PAYOUT-001", "amount": "150000.00", "currency": "NGN", "status": "paid" } } ``` Verify deliveries with the `X-Zuba-Signature` header (see [Zuba webhook docs](https://docs.zuba.com/guides/webhooks)). You can also poll `GET /v1/payouts/{id}`. ## Test it in sandbox Everything above works against `https://api.sandbox.zuba.com` with sandbox credentials. Magic beneficiary account numbers give deterministic outcomes (`0000000000` → `paid`, `0000000001` → `failed`). See the [sandbox testing guide](https://docs.zuba.com/guides/sandbox-testing). That's it: a Privy wallet, one deposit address, and one API call to reach bank accounts and mobile money wallets across Africa. # Sending USDC (or other ERC-20s) Source: https://docs.privy.io/recipes/send-usdc Sending USDC, or other ERC-20 tokens, is one of the most common actions taken by wallet users on Ethereum-based chains. This guide will walk you through how to format the transaction input data for these tokens, using USDC as an example. ## 1. Get the USDC contract address To send USDC, you'll need the contract address for USDC. The address is different for each network, so make sure you get the correct address for the network you're targeting. You can go to Circle's website to look up the USDC address on both [main networks](https://developers.circle.com/stablecoins/usdc-on-main-networks) and [test networks](https://developers.circle.com/stablecoins/usdc-on-test-networks). ## 2. Format the transaction send input data USDC, and other ERC-20 tokens, are smart contracts. In order to send these tokens, your transaction needs to call the `transfer` function on the contract, which takes in two parameters: * `to`: The address of the recipient * `value`: The amount of tokens to send When formatting the transaction input data, you must define the expected interface of the function you're calling by providing an ABI. You can use helper packages like `viem` to provide the ABI and encode the function parameters. Additionally, each ERC-20 token defines a `decimals` value, which is the number of decimal places for the token. For USDC, the `decimals` value is usually 6, but for most other ERC-20 tokens, it's 18. First, install the `viem` package if it is not installed yet. ```bash theme={"system"} npm install viem ``` Then, build the transaction input data. ```typescript theme={"system"} import {encodeFunctionData, erc20Abi} from 'viem'; const recipientAddress = '0x...'; const amountToSend = 1; // Sender wants to send 1 USDC const decimals = 6; // USDC has 6 decimals const encodedData = encodeFunctionData({ abi: erc20Abi, functionName: 'transfer', args: [recipientAddress, BigInt(amountToSend * 10 ** decimals)] }); ``` ## 3. Send the transaction You can send the transaction using the Privy API. Below are examples for React, React Native, and NodeJS; you can find other SDKs' send transaction examples in the [Send a transaction](/wallets/using-wallets/ethereum/send-a-transaction) guide. ```typescript theme={"system"} import {useSendTransaction} from '@privy-io/react-auth'; const {sendTransaction} = useSendTransaction(); const {hash} = await sendTransaction({ to: '$USDC_CONTRACT_ADDRESS', data: '0x', // from the previous step chainId: 8453 // Base's chainId }); ``` ```typescript theme={"system"} import {useEmbeddedEthereumWallet} from '@privy-io/expo'; const {wallets} = useEmbeddedEthereumWallet(); const wallet = wallets[0]; const provider = await wallet.getProvider(); const accounts = await provider.request({ method: 'eth_requestAccounts' }); // Send transaction (will be signed and populated) const response = await provider.request({ method: 'eth_sendTransaction', params: [ { from: accounts[0], to: '$USDC_CONTRACT_ADDRESS', data: '0x' // from the previous step } ] }); ``` ```typescript {skip-check} theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: 'insert-your-app-id', appSecret: 'insert-your-app-secret' }); const usdcContractAddress = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; // on Base const {hash} = await privy .wallets() .ethereum() .sendTransaction('insert-wallet-id', { caip2: 'eip155:8453', // Base's caip2 params: { transaction: { to: usdcContractAddress, data: encodedData, // from the previous step chain_id: 8453 // Base's chainId } } }); ``` You've successfully sent USDC! # Integrating Solana mobile wallet adapter Source: https://docs.privy.io/recipes/solana/adding-solana-mwa The Solana Mobile Wallet Adapter (MWA) is a library that allows apps to connect to mobile Solana wallets. This guide will walk you through the steps to integrate MWA into your Privy app. ## Resources Learn how to set up Solana in your React app with Privy. ## Install dependencies Install the required dependencies: ```bash theme={"system"} npm i @solana-mobile/wallet-standard-mobile ``` ## Register the MWA adapter In your app's root component, register the MWA adapter with Privy. ```typescript theme={"system"} import { createDefaultAuthorizationCache, createDefaultChainSelector, createDefaultWalletNotFoundHandler, registerMwa } from '@solana-mobile/wallet-standard-mobile'; registerMwa({ appIdentity: { name: 'My app', uri: 'https://myapp.io', icon: 'relative/path/to/icon.png' // resolves to https://myapp.io/relative/path/to/icon.png }, authorizationCache: createDefaultAuthorizationCache(), chains: ['solana:mainnet'], chainSelector: createDefaultChainSelector(), onWalletNotFound: createDefaultWalletNotFoundHandler() }); ``` Solana Mobile Wallet Adapter is only supported on Android devices. ## Resources Official documentation for integrating Solana Mobile Wallet Adapter in React Native apps. ## Install dependencies Install the required dependencies: ```bash theme={"system"} npm install @solana-mobile/mobile-wallet-adapter-protocol-web3js @solana-mobile/mobile-wallet-adapter-protocol ``` ## Create a MWA session In order to connect and log in users with MWA wallets, you need to create a MWA session by calling the `transact` and `authorize` methods from the `@solana-mobile/mobile-wallet-adapter-protocol-web3js` package, you'll then need to log in the user with Privy using the `useLoginWithSiws` hook. You can do this in a custom login button component. ```tsx theme={"system"} import {transact} from '@solana-mobile/mobile-wallet-adapter-protocol-web3js'; import {useLoginWithSiws} from '@privy-io/expo'; import {toByteArray} from 'react-native-quick-base64'; import {PublicKey} from '@solana/web3.js'; import {Buffer} from 'buffer'; const MwaLoginButton = () => { const {generateMessage, login} = useLoginWithSiws(); const handleLogin = async () => { try { await transact(async (wallet) => { // 1. Create a MWA session const authorizationResult = await wallet.authorize({ chain: 'mainnet-beta', identity: { name: 'My app', uri: 'https://myapp.io', icon: 'https://myapp.io/icon.png' } }); const desiredAccount = authorizationResult.accounts[0]; // 2. Convert base64 address to base58 (Privy expects base58) const addressBytes = toByteArray(desiredAccount.address); const publicKey = new PublicKey(addressBytes); const base58Address = publicKey.toBase58(); // 3. Generate a Privy SIWS message for the wallet address const siwsMessage = await generateMessage({ wallet: {address: base58Address}, // Use base58 for Privy from: {domain: 'com.myapp.app', uri: 'https://myapp.io'} }); // 4. Convert the SIWS message to Uint8Array for signing const encodedSiwsMessage = new TextEncoder().encode(siwsMessage.message); // 5. Request user to sign the SIWS message with their wallet const [signatureBytes] = await wallet.signMessages({ addresses: [desiredAccount.address], // Use base64 for MWA payloads: [encodedSiwsMessage] }); const signatureBase64 = Buffer.from(signatureBytes).toString('base64'); // 6. Authenticate with Privy using the signed message const user = await login({ signature: signatureBase64, message: siwsMessage.message }); return {mwaResult: authResult, user}; }); } catch (error) { console.error('Login failed', error); } }; return ; } ``` ### Creating a Solana embedded wallet To create a Solana embedded wallet, you can use the `useWallets` hook from `@privy-io/react-auth/solana`. This hook provides a `createWallet` function to create an embedded wallet. ```tsx theme={"system"} // components/createWalletButton.tsx 'use client'; import {useWallets, useCreateWallet} from '@privy-io/react-auth/solana'; export function CreateWalletButton(props: {createAdditional: boolean}) { const {ready} = useWallets(); const {createWallet} = useCreateWallet(); if (!ready) { return
Loading...
; } const handleCreateWallet = async () => { try { // If createAdditional is true, it will create an additional HD wallet for the user. const wallet = await createWallet({createAdditional: props.createAdditional}); console.log('Embedded wallet created:', wallet); } catch (error) { console.error('Error creating embedded wallet:', error); } }; return ; } ``` ### Using wallets Privy provides the `useSignMessage`, `useSignTransaction`, and `useSignAndSendTransaction` hooks to sign messages and transactions with embedded wallets. You can also use linked EOA wallets directly for signing messages and transactions. #### Signing a message To sign a message with an embedded wallet, use the `useSignMessage` hook: ```tsx theme={"system"} import {useSignMessage, useWallets} from '@privy-io/react-auth/solana'; const {signMessage} = useSignMessage(); const {wallets} = useWallets(); // This hook provides access to both embedded and EOA wallets const wallet = wallets.find((w) => w.standardWallet.name === 'Privy'); // Find the first embedded wallet const handleSignMessage = async () => { try { if (!wallet) throw new Error('No embedded wallet found'); const signature = await signMessage({ message: new TextEncoder().encode('Hello from Privy!'), // Solana messages are typically encoded as Uint8Array wallet }); console.log('Message signed:', signature); } catch (error) { console.error('Error signing message:', error); } }; ``` This function signs a message using the first embedded wallet. #### Preparing a transaction Before signing or sending a transaction, you need to prepare it. Here's how you can create a simple SOL transfer transaction: ```tsx theme={"system"} import type {ConnectedStandardSolanaWallet} from '@privy-io/react-auth/solana'; import { pipe, createSolanaRpc, getTransactionEncoder, createTransactionMessage, setTransactionMessageFeePayer, setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstructions, compileTransaction, address, createNoopSigner } from '@solana/kit'; import {getTransferSolInstruction} from '@solana-program/system'; const generateTransaction = async (wallet: ConnectedStandardSolanaWallet) => { // Simple SOL transfer transaction const amount = 1; const transferInstruction = getTransferSolInstruction({ amount: BigInt(parseFloat(amount) * 1_000_000_000), // Convert SOL to lamports destination: address('RecipientAddressHere'), source: createNoopSigner(address(wallet.address)) }); // Configure your RPC connection to point to the correct Solana network const {getLatestBlockhash} = createSolanaRpc('https://api.mainnet-beta.solana.com'); // Replace with your Solana RPC endpoint const {value: latestBlockhash} = await getLatestBlockhash().send(); // Create transaction using @solana/kit const transaction = pipe( createTransactionMessage({version: 0}), (tx) => setTransactionMessageFeePayer(address(wallet.address), tx), // Set the message fee payer (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), // Set recent blockhash (tx) => appendTransactionMessageInstructions([transferInstruction], tx), // Add your instructions to the transaction (tx) => compileTransaction(tx), // Compile the transaction (tx) => new Uint8Array(getTransactionEncoder().encode(tx)) // Finally encode the transaction ); return transaction; }; ``` This code creates a transaction object that can be signed or sent later. Replace the placeholder values with actual addresses. #### Signing a transaction To sign a transaction, use the `useSignTransaction` hook: ```tsx theme={"system"} import {useSignTransaction, useWallets} from '@privy-io/react-auth/solana'; const {signTransaction} = useSignTransaction(); const {wallets} = useWallets(); // This hook provides access to both embedded and EOA wallets const handleSignTransaction = async () => { try { const wallet = wallets.find((w) => w.standardWallet.name === 'Privy'); // Find the first embedded wallet if (!wallet) throw new Error('No wallet found'); const transaction = await generateTransaction(wallet); // The transaction prepared earlier const transactionSignature = await signTransaction({ transaction, wallet }); console.log('Transaction signed:', transactionSignature); } catch (error) { console.error('Error signing transaction:', error); } }; ``` This function signs the prepared transaction using the embedded wallet and a specified Solana RPC endpoint. #### Sending a transaction To send a signed transaction to the Solana network, use the `useSignAndSendTransaction` hook: ```tsx theme={"system"} import {useSignAndSendTransaction, useWallets} from '@privy-io/react-auth/solana'; const {signAndSendTransaction} = useSignAndSendTransaction(); const {wallets} = useWallets(); // This hook provides access to both embedded and EOA wallets const handleSendTransaction = async () => { try { const wallet = wallets.find((w) => w.standardWallet.name === 'Privy'); // Find the first embedded wallet if (!wallet) throw new Error('No wallet found'); const transaction = await generateTransaction(); // The transaction prepared earlier const transactionSignature = await signAndSendTransaction({ transaction, wallet }); console.log('Transaction sent:', transactionSignature); } catch (error) { console.error('Error sending transaction:', error); } }; ``` This function sends the signed transaction to the Solana network and logs the transaction signature. ### Conclusion This guide has shown you how to integrate Privy with Solana into an application. You can now log in users, create embedded wallets, and sign messages and transactions using the Privy React SDK. # Solana transactions and signing Source: https://docs.privy.io/recipes/solana/overview Solana recipes cover wallet setup, SOL transfers, and SPL token flows using Privy-managed wallets. Set up core Solana wallet and transaction primitives. Transfer native SOL with managed wallet infrastructure. Build token transfer flows for SPL assets. # Sending a SOL transaction Source: https://docs.privy.io/recipes/solana/send-sol Complete example of sending SOL from a Privy embedded wallet using useSignAndSendTransaction and @solana/kit Sending SOL is the most common transaction on the Solana blockchain. This recipe walks you through creating and sending SOL transfer transactions using `@solana/web3.js` with Privy wallets. Before following this recipe, make sure you have [configured Privy for Solana](/recipes/solana/getting-started-with-privy-and-solana) in your app. ## Overview This recipe demonstrates how to: * Create a SOL transfer transaction using `@solana/web3.js` * Sign and send the transaction using Privy wallets ## Prerequisites Install the required dependencies: `bash npm install @solana/web3.js ` ## 1. Create the SOL transfer transaction Create a SOL transfer transaction using your preferred language: ```typescript theme={"system"} import {Connection, PublicKey, SystemProgram, Transaction, LAMPORTS_PER_SOL} from '@solana/web3.js'; const createSOLTransferTransaction = async ( fromAddress: string, toAddress: string, amount: number // Amount in SOL ) => { // Set up connection to Solana network const connection = new Connection('https://api.devnet.solana.com', 'confirmed'); // Create public key objects const fromPubkey = new PublicKey(fromAddress); const toPubkey = new PublicKey(toAddress); // Convert SOL to lamports (1 SOL = 1,000,000,000 lamports) const lamports = amount * LAMPORTS_PER_SOL; // Create transfer instruction const transferInstruction = SystemProgram.transfer({ fromPubkey, toPubkey, lamports }); // Create transaction and add instruction const transaction = new Transaction().add(transferInstruction); // Get recent blockhash const {blockhash} = await connection.getLatestBlockhash(); transaction.recentBlockhash = blockhash; transaction.feePayer = fromPubkey; return {transaction, connection}; }; ``` ## 2. Send the transaction You can send the transaction using Privy's different SDKs. Below are examples for React, React Native, and NodeJS: ```typescript {skip-check} theme={"system"} import {useSignAndSendTransaction, useWallets} from '@privy-io/react-auth/solana'; const {wallets} = useWallets(); const {signAndSendTransaction} = useSignAndSendTransaction(); const {transaction, connection} = await createSOLTransferTransaction( wallets[0].address, // fromAddress 'recipient-wallet-address', // toAddress 0.01 // amount in SOL ); // Assuming you have a transaction created from the previous step const signature = await signAndSendTransaction({ transaction.serialize(), // from createSOLTransferTransaction wallet: wallets[0] }); ``` ```typescript {skip-check} theme={"system"} import {useEmbeddedSolanaWallet} from '@privy-io/expo'; const {wallets} = useEmbeddedSolanaWallet(); const wallet = wallets[0]; const provider = await wallet.getProvider(); const {transaction, connection} = await createSOLTransferTransaction( wallet.address, // fromAddress 'recipient-wallet-address', // toAddress 0.01 // amount in SOL ); // Send transaction using the provider's request method const {signature} = await provider.request({ method: 'signAndSendTransaction', params: { transaction: transaction, // from createSOLTransferTransaction connection: connection // from createSOLTransferTransaction } }); ``` ```typescript {skip-check} theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID!, appSecret: process.env.PRIVY_APP_SECRET! }); const {transaction} = await createSOLTransferTransaction( 'insert-wallet-address', // fromAddress 'recipient-wallet-address', // toAddress 0.01 // amount in SOL ); // Send transaction using Privy API const response = await privy .wallets() .solana() .signAndSendTransaction('insert-wallet-id', { // Devnet's caip2 caip2: 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1', // from createSOLTransferTransaction transaction: Buffer.from(transaction.serialize()).toString('base64') }); ``` You've successfully sent SOL! ## Next steps Now that you can send SOL, you might want to explore: * [Sending SPL tokens](/recipes/solana/send-spl-tokens) - Learn how to send other tokens on Solana * [Web3 integrations](/wallets/using-wallets/solana/web3-integrations) - Advanced integration patterns with Solana libraries # Sending SPL tokens Source: https://docs.privy.io/recipes/solana/send-spl-tokens Complete example of sending SPL tokens (USDC, custom tokens) from a Privy embedded wallet on Solana Sending SPL tokens is a common transaction on the Solana blockchain. This recipe walks you through creating and sending SPL token transfer transactions using `@solana/spl-token` and `@solana/web3.js` with Privy wallets. In this example, we'll use USDC as the SPL token, but you can adapt it for any SPL token by changing the mint address and decimals. Before following this recipe, make sure you have [configured Privy for Solana](/recipes/solana/getting-started-with-privy-and-solana) in your app. ## Overview This recipe demonstrates how to: * Create an SPL token transfer transaction using `@solana/spl-token` * Handle token accounts and decimals properly * Sign and send the transaction using Privy wallets ## Prerequisites Install the required dependencies: `bash npm install @solana/web3.js @solana/spl-token ` ## 1. Create the SPL token transfer transaction Create an SPL token transfer transaction using your preferred language: ```typescript theme={"system"} import {Connection, PublicKey, Transaction} from '@solana/web3.js'; import {getAssociatedTokenAddress, createTransferInstruction} from '@solana/spl-token'; const createSPLTransferTransaction = async ( fromAddress: string, toAddress: string, tokenMintAddress: string, amount: number, decimals: number = 6 // Default for USDC, adjust for your token ) => { // Set up connection to Solana network const connection = new Connection('https://api.mainnet-beta.solana.com', 'confirmed'); // Create public key objects const fromPubkey = new PublicKey(fromAddress); const toPubkey = new PublicKey(toAddress); const mintPubkey = new PublicKey(tokenMintAddress); // Get associated token accounts const fromTokenAccount = await getAssociatedTokenAddress(mintPubkey, fromPubkey); const toTokenAccount = await getAssociatedTokenAddress(mintPubkey, toPubkey); // Convert amount to token units (considering decimals) const tokenAmount = amount * Math.pow(10, decimals); // Create transfer instruction const transferInstruction = createTransferInstruction( fromTokenAccount, toTokenAccount, fromPubkey, tokenAmount ); // Create transaction and add instruction const transaction = new Transaction().add(transferInstruction); // Get recent blockhash const {blockhash} = await connection.getLatestBlockhash(); transaction.recentBlockhash = blockhash; transaction.feePayer = fromPubkey; return {transaction, connection}; }; ``` ## 2. Send the transaction You can send the transaction using Privy's different SDKs. Below are examples for React, React Native, and NodeJS: ```typescript {skip-check} theme={"system"} import {useSignAndSendTransaction, useWallets} from '@privy-io/react-auth/solana'; const {wallets} = useWallets(); const {signAndSendTransaction} = useSignAndSendTransaction(); const {transaction, connection} = await createSPLTransferTransaction( wallets[0].address, 'recipient-wallet-address', // Replace with recipient's token account address 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC mint address 10 // Amount to send ); // Assuming you have a transaction created from the previous step const signature = await sendTransaction({ transaction.serialize(), // from createSPLTransferTransaction wallet: wallets[0], }); ``` ```typescript {skip-check} theme={"system"} import {useEmbeddedSolanaWallet} from '@privy-io/expo'; const {wallets} = useEmbeddedSolanaWallet(); const wallet = wallets[0]; const provider = await wallet.getProvider(); const {transaction, connection} = await createSPLTransferTransaction( wallet.address, 'recipient-wallet-address', // Replace with recipient's token account address 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC mint address 10 // Amount to send ); // Send transaction using the provider's request method const {signature} = await provider.request({ method: 'signAndSendTransaction', params: { transaction: transaction, connection: connection // from createSPLTransferTransaction } }); ``` ```typescript {skip-check} theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID!, appSecret: process.env.PRIVY_APP_SECRET! }); const {transaction} = await createSPLTransferTransaction( 'insert-wallet-address', 'recipient-wallet-address', // Replace with recipient's token account address 'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC mint address 10 // Amount to send ); // Send transaction using Privy API const response = await privy .wallets() .solana() .signAndSendTransaction('insert-wallet-id', { // Mainnet's caip2 caip2: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', // from createSPLTransferTransaction transaction: Buffer.from(transaction.serialize()).toString('base64') }); ``` You've successfully sent SPL tokens! ## Token account considerations Before sending SPL tokens, ensure that the recipient has a token account for the specific token mint. ### Getting token account information You can check if token accounts exist and get their addresses: ```typescript theme={"system"} import {Connection, PublicKey} from '@solana/web3.js'; import {getAssociatedTokenAddress} from '@solana/spl-token'; const connection = new Connection('https://api.mainnet-beta.solana.com', 'confirmed'); // Check if token account exists const tokenAccountAddress = await getAssociatedTokenAddress( new PublicKey('tokenMintAddress'), new PublicKey('walletAddress') ); const accountInfo = await connection.getAccountInfo(tokenAccountAddress); const accountExists = accountInfo !== null; ``` Token mint addresses are different on each network, so make sure you're using the correct addresses for your target environment. ## Next steps Now that you can send SPL tokens, you might want to explore: * [Sending SOL](/recipes/solana/send-sol) - Learn how to send native SOL tokens * [Web3 integrations](/wallets/using-wallets/solana/web3-integrations) - Advanced integration patterns with Solana libraries # Using Solana standard wallets Source: https://docs.privy.io/recipes/solana/standard-wallets A comprehensive guide to integrating and using Solana standard wallets in your application # Using Solana Standard Wallets This guide will help you integrate and use Solana standard wallets in your application. We'll cover everything from basic setup to advanced features like message signing and transaction handling. To learn more about the wallet standard, you can read more about it [here!](https://docs.phantom.com/developer-powertools/wallet-standard) ## Basic Setup First, import the necessary hooks and types from Privy: ```typescript {skip-check} theme={"system"} import {useSolanaStandardWallets, type SolanaStandardWallet} from '@privy-io/react-auth/solana'; ``` ## Available Features Standard wallets provide these core features: * `standard:connect`: Connect the wallet * `standard:disconnect`: Disconnect the wallet * `solana:signMessage`: Sign messages * `solana:signTransaction`: Sign transactions * `solana:signAndSendTransaction`: Sign and send transactions ## Core Wallet Features Here's how to use the core features of any Solana standard wallet: ```typescript {skip-check} theme={"system"} function WalletComponent() { const {ready, wallets} = useSolanaStandardWallets(); // Connect/Disconnect const connect = (wallet: SolanaStandardWallet) => wallet.features['standard:connect']!.connect(); const disconnect = (wallet: SolanaStandardWallet) => wallet.features['standard:disconnect']!.disconnect(); // Sign Message const signMessage = async ( wallet: SolanaStandardWallet, address: string, message: Uint8Array ) => { const account = wallet.accounts.find((a) => a.address === address)!; const [result] = await wallet.features['solana:signMessage']!.signMessage({ account, message }); return result; }; // Sign Transaction const signTransaction = async ( wallet: SolanaStandardWallet, address: string, transaction: Uint8Array ) => { const account = wallet.accounts.find((a) => a.address === address)!; const [result] = await wallet.features['solana:signTransaction']!.signTransaction({ transaction, chain: 'solana:devnet', account }); return result; }; // Sign and Send Transaction const signAndSendTransaction = async ( wallet: SolanaStandardWallet, address: string, transaction: Uint8Array ) => { const account = wallet.accounts.find((a) => a.address === address)!; return wallet.features['solana:signAndSendTransaction']!.signAndSendTransaction({ transaction, chain: 'solana:devnet', account }); }; } ``` ## Registering the Privy Embedded Wallet To make your Privy embedded wallet compatible with other Solana applications, register it with the window object. The registration function dispatches the appropriate events to make the wallet available to other applications: ```typescript theme={"system"} // This is copied from @wallet-standard/wallet import type { Wallet, WalletEventsWindow, WindowRegisterWalletEvent, WindowRegisterWalletEventCallback } from '@wallet-standard/base'; class RegisterWalletEvent extends CustomEvent implements WindowRegisterWalletEvent { readonly #detail: WindowRegisterWalletEventCallback; get detail() { return this.#detail; } get type() { return 'wallet-standard:register-wallet' as const; } constructor(callback: WindowRegisterWalletEventCallback) { super('wallet-standard:register-wallet', { bubbles: false, cancelable: false, detail: callback }); this.#detail = callback; } preventDefault(): never { throw new Error('preventDefault is not supported'); } stopPropagation(): never { throw new Error('stopPropagation is not supported'); } stopImmediatePropagation(): never { throw new Error('stopImmediatePropagation is not supported'); } } export function registerWallet(wallet: Wallet): void { const callback: WindowRegisterWalletEventCallback = ({register}) => register(wallet); try { (window as WalletEventsWindow).dispatchEvent(new RegisterWalletEvent(callback)); } catch (error) { console.error('wallet-standard:register-wallet event could not be dispatched\n', error); } try { (window as WalletEventsWindow).addEventListener('wallet-standard:app-ready', ({detail: api}) => callback(api) ); } catch (error) { console.error('wallet-standard:app-ready event listener could not be added\n', error); } } ``` After this code is implemented in your application, you can then register the Privy embedded wallet by calling: ```typescript {skip-check} theme={"system"} registerWallet(wallets.find((wallet) => wallet.name === 'Privy' && 'privy:' in wallet.features)); ``` **That's it!** You now have a fully functional Solana standard wallet integration in your application. You can use these features to connect wallets, sign messages, and handle transactions in a standardized way. # Using Spark BTC with Privy wallets Source: https://docs.privy.io/recipes/spark-btc-guide Spark is a Bitcoin scaling solution that enables instant, low-cost transfers while maintaining Bitcoin's security. Privy offers BTC support. This guide walks through creating a Spark wallet, signing and submitting BTC transfers, and claiming pending transfers using Privy's API. Privy's Spark integration utilizes the [Spark Wallet SDK](https://github.com/buildonspark/spark/blob/63c51c9b15d8ce8498365f9f471c57eae5608007/sdks/js/packages/spark-sdk/src/spark-wallet/spark-wallet.ts). For more information on the Spark wallet methods, check out the [Spark docs](https://docs.spark.money/wallet/introduction). ## 1. Create a Spark wallet Create a Spark wallet by calling the [wallet creation endpoint](/api-reference/wallets/create) with `chain_type: 'spark'`. Learn more about creating wallets [here](/wallets/wallets/create/create-a-wallet). ```tsx theme={"system"} import {useCreateWallet} from '@privy-io/react-auth/extended-chains'; const {createWallet} = useCreateWallet(); const {user, wallet} = await createWallet({chainType: 'spark'}); ``` ```tsx theme={"system"} import {useCreateWallet} from '@privy-io/expo/extended-chains'; const {createWallet} = useCreateWallet(); const {user, wallet} = await createWallet({chainType: 'spark'}); ``` ```bash theme={"system"} curl --request POST https://api.privy.io/v1/wallets \ -u ":" \ -H "privy-app-id: " \ -H 'Content-Type: application/json' \ -d '{ "owner": { "user_id": "did:privy:xxxxxx" }, "chain_type": "spark" }' ``` The response will include your new Spark wallet with a unique Spark address format (e.g., `spark1pgss...`). Spark supports both mainnet and testnet environments: * `MAINNET`: Production Bitcoin network * `REGTEST`: Test network for development When you create a wallet, the `MAINNET` address is automatically returned. To get the `REGTEST` address, you can use the returned public key and the [`encodeSparkAddress`](https://github.com/buildonspark/spark/blob/63c51c9b15d8ce8498365f9f471c57eae5608007/sdks/js/packages/spark-sdk/src/utils/address.ts#L44) method from the Spark SDK. On subsequent wallet requests, your request will need to include the network you want to take the operation on. ## 2. Transfer BTC Transfer Bitcoin to another Spark address using the [`transfer`](/api-reference/wallets/spark/transfer) endpoint. The amount is specified in satoshis. Most Spark wallet operations (transferring BTC and checking balances) require authorization signatures using user keys. Only wallet creation can be done without authorization signatures. Learn more about [signing requests with user keys](/controls/authorization-keys/using-owners/sign#react%2C-expo). ```typescript theme={"system"} import {useAuthorizationSignature} from '@privy-io/react-auth'; const {generateAuthorizationSignature} = useAuthorizationSignature(); // Build the request input for authorization signature const input = { version: 1, url: `https://api.privy.io/v1/wallets/${'$WALLET_ID'}/rpc`, method: 'POST', headers: { 'privy-app-id': 'your-app-id' }, body: { method: 'transfer', network: 'MAINNET', params: { receiver_spark_address: 'spark1pgss8z35rpycv4duqdk5u3sclhjnztjunv5yajlwk69tyv5fsvwwe9mgwmxfkx', amount_sats: 16 } } } as const; // Generate authorization signature const {signature: authorizationSignature} = await generateAuthorizationSignature(input); // Make the transfer request const transferResponse = await fetch(input.url, { method: input.method, headers: { ...input.headers, Authorization: `Bearer ${'$ACCESS_TOKEN'}`, 'Content-Type': 'application/json', 'privy-authorization-signature': authorizationSignature }, body: JSON.stringify(input.body) }); const transfer = await transferResponse.json(); ``` ```typescript theme={"system"} import {useAuthorizationSignature} from '@privy-io/expo'; const {generateAuthorizationSignature} = useAuthorizationSignature(); // Build the request input for authorization signature const input = { version: 1, url: `https://api.privy.io/v1/wallets/${'$WALLET_ID'}/rpc`, method: 'POST', headers: { 'privy-app-id': 'your-app-id' }, body: { method: 'transfer', network: 'MAINNET', params: { receiver_spark_address: 'spark1pgss8z35rpycv4duqdk5u3sclhjnztjunv5yajlwk69tyv5fsvwwe9mgwmxfkx', amount_sats: 16 } } } as const; // Generate authorization signature const {signature: authorizationSignature} = await generateAuthorizationSignature(input); // Make the transfer request const transferResponse = await fetch(input.url, { method: input.method, headers: { ...input.headers, Authorization: `Bearer ${'$ACCESS_TOKEN'}`, 'Content-Type': 'application/json', 'privy-authorization-signature': authorizationSignature }, body: JSON.stringify(input.body) }); const transfer = await transferResponse.json(); ``` The transfer response includes the transfer ID, status, and detailed transaction information. ## 3. Check balance and claim transfers Use the [`getBalance`](/api-reference/wallets/spark/get-balance) endpoint to retrieve your wallet balance and automatically claim any pending transfers. ```typescript theme={"system"} import {useAuthorizationSignature} from '@privy-io/react-auth'; const {generateAuthorizationSignature} = useAuthorizationSignature(); // Build the request input for authorization signature const input = { version: 1, url: `https://api.privy.io/v1/wallets/${'$WALLET_ID'}/rpc`, method: 'POST', headers: { 'privy-app-id': 'your-app-id' }, body: { method: 'getBalance', network: 'MAINNET' } } as const; // Generate authorization signature const {signature: authorizationSignature} = await generateAuthorizationSignature(input); // Make the balance request const balanceResponse = await fetch(input.url, { method: input.method, headers: { ...input.headers, Authorization: `Bearer ${'$ACCESS_TOKEN'}`, 'Content-Type': 'application/json', 'privy-authorization-signature': authorizationSignature }, body: JSON.stringify(input.body) }); const balance = await balanceResponse.json(); console.log(`Balance: ${balance.data.balance} satoshis`); ``` ```typescript theme={"system"} import {useAuthorizationSignature} from '@privy-io/expo'; const {generateAuthorizationSignature} = useAuthorizationSignature(); // Build the request input for authorization signature const input = { version: 1, url: `https://api.privy.io/v1/wallets/${'$WALLET_ID'}/rpc`, method: 'POST', headers: { 'privy-app-id': 'your-app-id' }, body: { method: 'getBalance', network: 'MAINNET' } } as const; // Generate authorization signature const {signature: authorizationSignature} = await generateAuthorizationSignature(input); // Make the balance request const balanceResponse = await fetch(input.url, { method: input.method, headers: { ...input.headers, Authorization: `Bearer ${'$ACCESS_TOKEN'}`, 'Content-Type': 'application/json', 'privy-authorization-signature': authorizationSignature }, body: JSON.stringify(input.body) }); const balance = await balanceResponse.json(); console.log(`Balance: ${balance.data.balance} satoshis`); ``` The balance response includes: * Your native Spark balance in satoshis * Any token balances with metadata * Automatically claims pending incoming transfers ## 4. Execute more wallet requests This guide demonstrates how to use `transfer` and `getBalance`, but Privy supports many Spark wallet methods including: * [`transfer`](/api-reference/wallets/spark/transfer) - Transfer satoshis from a Spark wallet to another Spark address * [`getBalance`](/api-reference/wallets/spark/get-balance) - Retrieve wallet balance and token holdings, automatically claims pending transfers * [`transferTokens`](/api-reference/wallets/spark/transfer-tokens) - Transfer Spark tokens to another Spark address * [`createLightningInvoice`](/api-reference/wallets/spark/create-lightning-invoice) - Create a Lightning invoice to receive funds via Lightning Network * [`payLightningInvoice`](/api-reference/wallets/spark/pay-lightning-invoice) - Pay a Lightning Network invoice * [`getStaticDepositAddress`](/api-reference/wallets/spark/get-static-deposit-address) - Get a static Bitcoin address for deposits * [`getClaimStaticDepositQuote`](/api-reference/wallets/spark/get-static-deposit-quote) - Get a quote for claiming a static deposit * [`claimStaticDeposit`](/api-reference/wallets/spark/claim-static-deposit) - Claim funds from a static Bitcoin deposit * [`signMessageWithIdentityKey`](/api-reference/wallets/spark/sign-message-with-identity-key) - Sign a message using the wallet's identity key All methods (except wallet creation) require authorization signatures using the same pattern shown in the examples above. For additional Spark functionality and advanced features, see the [Spark developer documentation](https://docs.spark.money/wallet/developer-guide/send-receive-spark). # Speeding up transactions on EVM chains Source: https://docs.privy.io/recipes/speeding-up-transactions Learn how to use webhooks to trigger replacement transactions to speed up transaction confirmation. # Overview When you send a transaction using Privy, we broadcast it to the specified blockchain. However, in periods of high network congestion, the transaction may take longer than expected to be confirmed, or occasionally even never make it to confirmation. This is due to transaction fee ("gas") estimates becoming outdated as high blockchain activity puts upward pressure on fees. In order to increase the likelihood of your transaction being confirmed, you can send a transaction that replaces the original one and sets higher gas fees. **This guide will show you how to use the webhooks feature to know when a transaction is taking longer than expected, and trigger a replacement transaction to speed up confirmation.** ## Prerequisites * A Privy EVM wallet * Webhooks enabled for your app ## Step 1: Set up webhooks for `transaction.still_pending` To start, follow the setup instructions for [Webhooks](/wallets/gas-and-asset-management/assets/transaction-event-webhooks) From the dropdown menu, select the `transaction.still_pending` event. ## Step 2: Implement the webhook handler When a transaction is taking longer than expected, Privy will emit a webhook to the destination URL you provided. Here is an example of the webhook payload: ```json theme={"system"} { "caip2": "eip155:8453", "transaction_hash": "0x28f0ae628c08b7a341cd49ea40225d54ddd5acfe5f7ccfb44ee0be154d17bab0", "transaction_id": "b2ua14lrsfj2kq8r8mlm9z07", "transaction_request": { "chain_id": 8453, "gas_limit": "0x5208", "max_fee_per_gas": "0xadc0e", "max_priority_fee_per_gas": "0xf4240", "data": "0x....", "nonce": 0, "to": "0x38Bc05d7b69F63D05337829fA5Dc4896F179B5fA", "type": 2, "value": "0x0" }, "type": "transaction.still_pending", "wallet_id": "" } ``` ## Step 3: Trigger a replacement transaction Once you receive the webhook, you can trigger a replacement transaction by submitting a new transaction with the same nonce as the still pending transaction. If you leave out the gas fields, Privy will automatically set them based on the current network conditions. Below is an example of sending a replacement transaction using Privy's Node SDK. For other ways to send a transaction, see the [guide](/wallets/using-wallets/ethereum/send-a-transaction). Although you are sending a new transaction, there is **no risk of both transactions getting executed** (unintentionally) if the nonce is set to be the same in each. On EVM chains, the nonce is an internal counter that is incremented for each transaction sent by a wallet to avoid replay and other attack vectors. ```ts @privy-io/node {skip-check} theme={"system"} // payload is from the webhook const {hash, transactionId, caip2} = await privy .wallets() .ethereum() .sendTransaction(payload.wallet_id, { caip2: payload.caip2, params: { transaction: { to: payload.transaction_request.to, value: payload.transaction_request.value, data: payload.transaction_request.data, nonce: payload.transaction_request.nonce } } }); ``` This transaction will replace the original transaction and return a new transaction hash and transaction id. Optionally, to increase the likelihood of the replacement transaction being confirmed, you can set the `max_priority_fee_per_gas` to be a higher value than the original transaction. ## \[Optional] Step 4: Monitor the status of the replacement via webhooks Subscribe to `transaction.replaced`, `transaction.failed`, and `transaction.confirmed` webhooks to get notified on the success of the speedup. Once the replacement succeeds, the following webhooks will trigger: * `transaction.replaced` for the original transaction ID * `transaction.confirmed` for the speedup transaction If the replacement failed, a `transaction.failed` webhook would trigger. For more information on transaction webhooks, see the [correspondent docs](/wallets/gas-and-asset-management/assets/transaction-event-webhooks). # Accept stablecoin payments with automatic fiat conversion Source: https://docs.privy.io/recipes/stablecoin-payments-bridge-stripe Accept USDC from Privy wallets and automatically convert payments to USD with Bridge and Stripe Financial Accounts Accept stablecoin payments without holding or manually converting crypto. This recipe uses Privy wallets to send USDC to a Bridge liquidation address. Bridge automatically converts the payment to USD and deposits it into a Stripe Financial Account through ACH. This flow works well for: * **Loyalty and rewards programs** that let users spend tokenized balances at checkout. The user pays in tokens, while the business receives USD. * **Fintech and neobank apps adding a spending layer** that hold user balances in stablecoins for yield, foreign exchange flexibility, or by design. Users can pay with their balance at checkout, while the merchant receives fiat transparently. ## How it works ```text theme={"system"} User's Privy wallet → Privy Transfer API → Bridge liquidation address → automatic conversion to USD → Stripe Financial Account ``` 1. Create a Bridge USD external account for the Stripe Financial Account, then create a liquidation address that uses it. This is a one-time setup. 2. When a user pays, call Privy's Transfer API with the liquidation address as the destination. 3. Bridge detects the incoming USDC, converts it to USD, and sends the USD to the Stripe Financial Account through ACH. ## Prerequisites * A [Privy app](https://dashboard.privy.io) with server-side API credentials and an embedded wallet that can authorize server-initiated transfers * A [Bridge account](https://apidocs.bridge.xyz/get-started/introduction/quick-start/get-set-up-with-bridge) with a verified customer ID * A [Stripe account with Treasury enabled](https://docs.stripe.com/treasury) * The routing number and account number for the Stripe Financial Account that receives payments ## Set up the payment destination ### Create a Stripe Financial Account Create a Stripe Financial Account if the business does not already have one. Enable ACH inbound transfers, then save the routing and account numbers from its financial address. ```bash theme={"system"} curl https://api.stripe.com/v1/treasury/financial_accounts \ -u "{{STRIPE_SECRET_KEY}}:" \ -d "supported_currencies[]=usd" ``` The Financial Account's financial address includes the ACH details needed by Bridge: ```json theme={"system"} { "routing_number": "110000000", "account_number": "000123456789" } ``` ### Create a Bridge USD external account Create a USD [Bridge external account](https://apidocs.bridge.xyz/api-reference/external-accounts/create-a-new-external-account) for the Stripe Financial Account. Use the business's legal name and address as the account-owner details, and use the Financial Account's routing and account numbers. ```bash theme={"system"} curl -X POST https://api.bridge.xyz/v0/customers//external_accounts \ -H "Api-Key: " \ -H "Content-Type: application/json" \ -H "Idempotency-Key: " \ -d '{ "currency": "usd", "bank_name": "", "account_owner_name": "", "account_type": "us", "account": { "routing_number": "110000000", "account_number": "000123456789", "checking_or_savings": "checking" }, "address": { "street_line_1": "", "city": "", "state": "", "postal_code": "", "country": "USA" } }' ``` Bridge returns an external account ID. Store it with the business's payment configuration: ```json theme={"system"} { "id": "", "customer_id": "", "account_owner_name": "", "currency": "usd", "account_type": "us", "active": true } ``` ### Create a Bridge liquidation address Create a [Bridge liquidation address](https://apidocs.bridge.xyz/api-reference/liquidation-addresses/create-a-liquidation-address) for the business. Pass the external account ID at the top level so Bridge can send converted USD to the Stripe Financial Account through ACH. ```bash theme={"system"} curl -X POST https://api.bridge.xyz/v0/customers//liquidation_addresses \ -H "Api-Key: " \ -H "Content-Type: application/json" \ -H "Idempotency-Key: " \ -d '{ "chain": "tempo", "currency": "usdc", "external_account_id": "", "destination_payment_rail": "ach", "destination_currency": "usd", "return_instructions": { "address": "0x" } }' ``` `return_instructions.address` is the Tempo address that receives the crypto if Bridge returns or fails the drain. The business must control this address, and it must be valid for the source chain. Bridge returns a liquidation address that automatically liquidates received USDC and routes USD to the configured external account: ```json theme={"system"} { "id": "liq_addr_...", "customer_id": "", "external_account_id": "", "address": "0xAbCdEf1234567890...", "chain": "tempo", "currency": "usdc", "destination_payment_rail": "ach", "destination_currency": "usd", "state": "active" } ``` Store the liquidation address with the business's payment configuration. A single address can accept payments from multiple users, so your app does not need to create an address for each transaction. ## Send a payment from a Privy wallet When a user confirms a payment, call the Transfer API with the Bridge liquidation address as the destination. ```bash theme={"system"} curl -X POST https://api.privy.io/v1/wallets//transfer \ -u ":" \ -H "privy-app-id: " \ -H "Content-Type: application/json" \ -d '{ "source": { "asset": "usdc", "amount": "12.50", "chain": "tempo" }, "destination": { "address": "0xAbCdEf1234567890..." } }' ``` The Transfer API creates an asynchronous wallet action. Store its `id` to track the payment: ```json theme={"system"} { "id": "action-id", "status": "pending", "wallet_id": "wallet-id", "created_at": "2026-07-22T20:09:11.929Z", "type": "transfer", "source_asset": "usdc", "source_amount": "12.50", "source_chain": "tempo", "destination_address": "0xAbCdEf1234567890..." } ``` Poll the wallet action until its status is `succeeded`, or use your existing wallet-action monitoring flow: ```bash theme={"system"} curl https://api.privy.io/v1/wallets//actions/?include=steps \ -u ":" \ -H "privy-app-id: " ``` ## Reconcile fiat settlement A `succeeded` Privy wallet action confirms the onchain USDC transfer. It does not confirm that the USD ACH payment has settled in the Stripe Financial Account. Bridge processes ACH payouts in daily batches, not in real time. Reconcile each drain through [Bridge drain history](https://apidocs.bridge.xyz/api-reference/liquidation-addresses/get-drain-history-of-a-liquidation-address) and treat `payment_processed` as the successful settlement state. Handle every non-success state before marking the payment as settled. Subscribe to Bridge's real-time [`liquidation_address.drain` webhooks](https://apidocs.bridge.xyz/platform/additional-information/webhooks/structure) to update the payment status as the drain progresses. The [drain lifecycle](https://apidocs.bridge.xyz/platform/orchestration/liquidation_address/drains) documents the available states. ## Next steps Review transfer parameters, supported assets, and error handling. Track a transfer from pending to a terminal state. # Swapping crypto using Privy and 0x Source: https://docs.privy.io/recipes/swap-with-0x To enable crypto asset swapping (e.g. convert USDC to ETH), you can integrate with the exchange of your choice. In this case, we use [0x](https://0x.org/) which offers a huge amount of swapping pairs and great rates. This guide will enable a Privy wallet to convert USDC to ETH. This guide assumes that the Privy wallet has already been created and funded with ETH to pay for transaction fees. Code examples are in Javascript. ## Step 1: Register with 0x and retrieve API keys Go the 0x dashboard and create an account. Save your API keys in your local `.env` file. ## Step 2: Approve the Permit2 contract to enable asset movement from your wallet In order to facilitate a sale of USDC, 0x needs to be able to move USDC from your wallet to the buyer based on the trade. To do so, the wallet owner (user) must approve of this via a signature, which is then verified onchain. All of this can be done invisibly for the user. This is a one time action that won’t have to be done again for any future swapping for this wallet. ```tsx theme={"system"} import {maxUint256, erc20Abi, encodeFunctionData, createPublicClient, http} from 'viem'; const PERMIT2_ADDRESS = '0x000000000022D473030F116dDEE9F6B43aC78BA3'; // USDC contract on Base const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; // Provider is an instance of an EIP-1193 provider exposed from a Privy SDK const provider = await wallet.getProvider(); // get Privy wallet // prepre transaction to give USDC approval const data = encodeFunctionData({ abi: erc20Abi, functionName: 'approve', args: [PERMIT2_ADDRESS, maxUint256] }); // execute transaction const tx = await provider.request({ method: 'eth_sendTransaction', params: [ { from: wallets[0].address, to: USDC_ADDRESS, data } ] }); ``` ## Step 3: Get quote from 0x Next, you want to fetch a quote from 0x to sell your USDC for ETH. ```tsx theme={"system"} const USDC_ADDRESS = '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913'; const ETH_ADDRESS = '0xEeeeeEeeeEeEeeEeEeEeeEEEeeeeEeeeeeeeEEeE'; // Base network chain ID const CHAIN_ID = 8453; const USDC_DECIMALS = 6; const amountInUSD = 100; const formattedAmount = 100 * 10 ** 6; const quoteResponse = await fetch( `https://api.0x.org/swap/permit2/quote?chainId=${CHAIN_ID}&sellToken=${USDC_ADDRESS}&buyToken=${ETH_ADDRESS}&sellAmount=${formattedAmount}&taker=${wallet.address}`, { headers: { '0x-api-key': process.env.API_KEY_0X, '0x-version': 'v2' } } ); ``` ## Step 4: Prepare the transaction and execute Finally, you will prepare the transaction to fulfill the order and execute the swap. The user should see additional ETH in their wallet in exchange for their USDC. ```tsx theme={"system"} import {numberToHex, concat} from 'viem'; // Provider is an instance of an EIP-1193 provider exposed from a Privy SDK const provider = await wallet.getProvider(); // Sign an off-chain signature for the permit const signature = await provider.request({ method: 'eth_signTypedData_v4', params: [wallet.address, quoteResult.permit2.eip712] }); const signatureLengthInHex = numberToHex(size(signature), { signed: false, size: 32 }); // Pack the transaction for the trade fulfillment, including the off-chain signature const transactionData = concat([quoteResult.transaction.data, signatureLengthInHex, signature]); const params = { from: wallet.address, to: quoteResult.transaction.to, data: transactionData, gas: !!quoteResult.transaction.gas ? BigInt(quoteResult.transaction.gas) : undefined }; // send the signed permit to be on-chain const tx = await provider.request({ method: 'eth_sendTransaction', params: [params] }); ``` # Login with Apple Source: https://docs.privy.io/recipes/swift/apple Privy supports native [Apple login](https://developer.apple.com/sign-in-with-apple/) on iOS. Apple is an OAuth2.0 compliant authentication provider, but requires a specific implementation of Apple sign-in within iOS apps. Prior to integrating Sign in with Apple, make sure your app's `Bundle ID` rather than the `Service ID`, is configured as the `Client ID` within the [Privy Dashboard](/basics/get-started/dashboard/app-clients) for the Apple Social login credentials. ## The "Sign in with Apple" button Apple's [Human Interface Guidelines](https://developer.apple.com/design/human-interface-guidelines/sign-in-with-apple) has a clear definition of how the Sign in with Apple button should look. In order to ensure compliance with Apple's guidelines, we recommend you use the `ASAuthorizationAppleIDButton` class from Apple's `AuthenticationServices` framework. **Take a look at the snippet below** if you wish to use the `ASAuthorizationAppleIDButton` in SwiftUI. ```swift theme={"system"} struct SignInWithApple: UIViewRepresentable { typealias UIViewType = ASAuthorizationAppleIDButton func makeUIView(context: Context) -> ASAuthorizationAppleIDButton { return ASAuthorizationAppleIDButton() } func updateUIView(_ uiView: ASAuthorizationAppleIDButton, context: Context) { } } ``` Apple's `AuthenticationServices` framework also offers the [`SignInWithAppleButton` SwiftUI View](https://developer.apple.com/documentation/authenticationservices/signinwithapplebutton/), but relying on `ASAuthorizationAppleIDButton` instead allows the PrivySDK to handle the whole authentication process for you. ## Initializing Apple login Privy automatically implements and launches Apple's native `ASAuthorizationController` after calling `privy.oAuth.login(with: OAuthProvider.apple)`. Add the `SignInWithApple` button to your view as described above, and trigger the login method when tapped. ```swift theme={"system"} // Add the SignInWithApple button with your view and register the tap gesture SignInWithApple() .onTapGesture { // Ideally this is called in a view model, but showcasing logic here for brevity Task { do { // The `appUrlScheme` param is not necessary for using Sign in with Apple. // Privy will use the first valid app URL scheme from your app's info.plist. // Ensure your client's url schemes are registered in the Privy dashboard let authSession = try await privy.oAuth.login(with: OAuthProvider.apple) } catch { debugPrint("Error: \(error)") // Handle errors } } } ``` # Read auth state from a background process Source: https://docs.privy.io/recipes/swift/background-auth-state Use getAuthStateWithoutRefresh to safely check authentication state in background tasks and app extensions without triggering a network refresh In a background task or app extension, triggering a network refresh is unsafe. iOS can suspend or terminate background processes at any time, and an in-flight token refresh can be interrupted, leaving auth state inconsistent. `getAuthStateWithoutRefresh()` is a synchronous method that reads the cached auth state without making any network calls. Use it any time you need to check whether a user is authenticated from outside the main app foreground context. ## How it works * `getAuthState() async` — the standard method. Waits for the SDK to be ready and may trigger a token refresh. **Do not call this from a background process.** * `getAuthStateWithoutRefresh()` — synchronous. Reads cached state only. Safe for background tasks, `BGTask` handlers, background `URLSession` callbacks, and app extensions. ## Implementation Call `getAuthStateWithoutRefresh()` on the `Privy` instance you obtained from `PrivySdk.initialize(config:)`: ```swift theme={"system"} // In a BGTask, background URLSession handler, or app extension: func handleBackgroundTask() { let privy = // your shared Privy instance let authState = privy.getAuthStateWithoutRefresh() switch authState { case .authenticated(let user): // Safe to proceed — use user.id, user.linkedAccounts, etc. let linkedAccounts = user.linkedAccounts performBackgroundWork(for: user) case .authenticatedUnverified: // A session exists in cache but hasn't been verified with Privy's backend. // Defer any work that requires a valid token until the app returns to foreground. scheduleWorkForForeground() case .unauthenticated: // No session. Nothing to do in the background. break case .notReady: // SDK was never initialized in this process context. // Common in app extensions that share a Privy reference but never called PrivySdk.initialize. break } } ``` ## AuthState cases | Case | Meaning | What to do | | -------------------------- | ----------------------------------- | ---------------------------------------- | | `.authenticated(user)` | Fully verified session | Safe to proceed with background work | | `.authenticatedUnverified` | Cached session, not yet verified | Defer token-dependent work to foreground | | `.unauthenticated` | No session | Nothing to do | | `.notReady` | SDK not initialized in this process | Do not proceed | ## Key caveats **Do not call `user.getAccessToken()` in a background process.** It will attempt a network refresh and is as unsafe as `getAuthState()`. Only call it once the app has returned to the foreground. **`.authenticatedUnverified` is the most common background case.** The user's session is present in Keychain but hasn't been confirmed with Privy's backend. This is expected — it occurs when there is no network connectivity or when the background task runs before a foreground refresh has completed. Treat it as "probably logged in, verify later." **`.notReady` indicates the SDK was never initialized** in the current process context. This is common in app extensions that hold a reference to a `Privy` instance but never called `PrivySdk.initialize(config:)`. Handle this case gracefully and do not attempt any SDK operations. ## Returning to the foreground When the app returns to the foreground, verify the session and promote state from `.authenticatedUnverified` to `.authenticated` if the session is still valid: ```swift theme={"system"} // Call from your scene or app delegate when the app becomes active Task { let authState = await privy.getAuthState() // authState is now verified against Privy's backend } ``` If the device was offline, call `privy.onNetworkRestored()` to trigger re-verification once connectivity is available. # Setting a system theme for the Privy modal Source: https://docs.privy.io/recipes/system-theme **With Privy, you can style your login modal to match your user's system preferences for light or dark mode.** Below is a short guide for how to configure your login modal to match your user's system settings. ### 1. Get your user's system preferences To start, you should determine if your user's system preferences are configured for light mode, or for dark mode.Based on their system preferences, your user's device will automatically set the [`prefers-color-scheme`](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme) CSS media feature in your website to indicate their light/dark preference. You can simply query this media feature to determine if your user prefers light or dark mode. Below is a sample React hook to determine if your user prefers light or dark mode; feel free to use this directly in your app! ```tsx theme={"system"} import {useEffect, useState} from 'react'; // Returns true if the user prefers dark mode, and false otherwise export default function useDarkMode() { const [darkMode, setDarkMode] = useState(false); const modeMe = (e: MediaQueryListEvent) => { setDarkMode(!!e.matches); }; useEffect(() => { // Query the `prefers-color-scheme` media feature const matchMedia = window.matchMedia('(prefers-color-scheme: dark)'); setDarkMode(matchMedia.matches); // Listen to changes in the `prefers-color-scheme` media feature matchMedia.addEventListener('change', modeMe); return () => matchMedia.removeEventListener('change', modeMe); }, []); return darkMode; } ``` ### 2. Construct a light and dark theme Next, in your frontend code, [create an `appearance` configuration object](/basics/react/advanced/configuring-appearance) for both light *and* dark mode. You can use the default Privy light and dark themes, e.g.: ```tsx theme={"system"} const lightModeConfig = { appearance: { theme: 'light', logo: 'light-logo-url' } }; ``` ```tsx theme={"system"} const darkModeConfig = { appearance: { theme: 'dark', logo: 'dark-logo-url' } }; ``` Or, you can create custom `lightModeConfig` and `darkModeConfig` objects (with custom [`theme`](/basics/react/advanced/configuring-appearance#theme)s and [`accentColor`](/basics/react/advanced/configuring-appearance#accent-color)s) to match the modal's light and dark modes with your app's branding. ### 3. Conditionally set your theme based on the user's system preferences Lastly, in your `PrivyProvider`, conditionally pass in the `lightModeConfig` (your light mode configuration object) or `darkModeConfig` (your dark mode configuration object) depending on the user's system preferences from step 1. ```tsx theme={"system"} // 1. Query the user's system preferences for light or dark mode const darkMode = useDarkMode(); // 2. Construct your light mode and dark mode configuration objects const lightModeConfig = { /* your light mode configuration object */ }; const darkModeConfig = { /* your dark mode configuration object */ }; // 3. When you render your `PrivyProvider`, change the `config` property based on the user's system preferences return ( {children} ); ``` **That's it!** Your Privy modal's theme will now automatically match the user's system preference for light or dark mode. # Migrating wallets from on-device to TEEs Source: https://docs.privy.io/recipes/tee-wallet-migration-guide Privy's security architecture leverages secure execution environments to protect your users' assets. Wallet private keys are only temporarily reconstructed within these strictly isolated, secure execution environments when needed for sensitive operations. Privy provides two types of secure execution environments: 1) via TEEs and 2) on the user's device. * With **[TEE execution](/security/wallet-infrastructure/architecture)**, wallets are reassembled within trusted execution environments (TEEs), also known as secure enclaves. * With **[on-device execution](/security/wallet-infrastructure/advanced/user-device)**, wallets are reassembled directly on user devices. Each environment ensures that private keys are never stored in complete form and are only temporarily reconstructed when needed. ### Feature support Your app must enable TEE execution in order to access the following features: * Support for **[Tier 1 and Tier 2 chains](/wallets/overview/chains)**, such as Bitcoin, Sui, Cosmos, and more. * **[Policy engine](/controls/policies/overview)** in order to restrict Ethereum and Solana transactions. * Server-side access to wallets, using **[signers](/wallets/using-wallets/signers)**. ## Migration guide The following guide details how to migrate from on-device to TEE execution. When you enable TEE execution, all new wallets will be created within **trusted execution environments (TEEs)**. Existing on-device wallets will be migrated to TEEs when users next log in to your app. This is a one-way change. **To be eligible to migrate from on-device to TEE execution:** * Your app cannot be using Farcaster signers * If your app is using delegated actions, migrating to TEE execution will reset all delegations and you will need to re-enable these delegated permissions using signers * Your app cannot be built on the Flutter SDK or Unity SDK ### Step 1: Identify your app's execution environment Navigate to the [Privy Dashboard](https://dashboard.privy.io/apps?tab=advanced\&page=wallets) to verify your app's wallet execution environment. On the **Wallets** page, navigate to the **Advanced** tab. Your app's wallet environment will be shown here as either "On-device" (with an option to "Request access to migrate to TEE") or "TEE enabled". Apps may only enable one execution environment. ### Step 2: Upgrade your SDKs TEE execution is supported by the following SDK versions or later: **Client SDKs**: * **React**: `@privy-io/react-auth@3.13.0` * **Expo**: `@privy-io/expo@0.54.0` * **iOS (Swift)**: `2.0.0-beta.11` * **Android (Kotlin)**: `0.1.0-beta.1` * **Flutter**: `0.1.0-beta.1` * **Unity**: `0.6.0` Note that while the Flutter SDK and Unity SDK support TEE execution, they do NOT support migrating accounts from on-device to TEEs. If you have existing on-device wallets on these SDKs and want to migrate them to TEEs, please reach out to [support@privy.io](mailto:support@privy.io). **Server SDKs**: * **Node**: `@privy-io/node@0.1.0` ### Step 3: Enable TEE execution in the Dashboard and contact us In the [Privy Dashboard](https://dashboard.privy.io/apps?tab=advanced\&page=wallets), navigate to the **Wallets** page and then the **Advanced** tab. Select **"Request access to migrate to TEE"** and follow the instructions. When you enable TEE execution, all new wallets will be created within trusted execution environments (TEEs). Existing user wallets will be migrated from on-device to TEEs via end-to-end encryption as users next log in to your app. All client-side features will be immediately available after the user logs in and their wallet is migrated. Please note that: * All client-side features will be immediately available once a user logs in and their wallet is migrated. * Only wallets that have been migrated to TEEs will support server-side features. Some users may not return to your app, so their wallets will remain on-device. * User-managed recovery (like cloud recovery) will no longer be prompted on new devices and will be disabled after migration. * This is a one-way change, and on-device execution is disabled once migration occurs. By default, wallets are migrated automatically when users log in. If you prefer to manually control when wallets are migrated, you can disable automatic migration and trigger the migration process manually. Automatic migration is enabled by default and is the recommended approach. If you are opting for manual migration, make sure that the migration is triggered as soon as possible. Avoid giving the option to opt-out to ensure a uniform experience across your userbase. Begin by disabling automatic migration by setting the `disableAutomaticMigration` flag to `true` in the provider config. ```tsx theme={"system"} {/* your app's content */} ``` Then, manually trigger the migration process by calling the `migrate` method returned by the `useMigrateWallets` hook. ```ts theme={"system"} import {useMigrateWallets} from '@privy-io/react-auth'; const {migrate} = useMigrateWallets(); await migrate(); ``` ### Returns A promise that resolves to an object with a success property indicating if the user's wallets were migrated successfully. The promise will reject otherwise. Begin by disabling automatic migration by setting the `disableAutomaticMigration` flag to `true` in the provider config. ```tsx theme={"system"} {/* your app's content */} ``` Then, manually trigger the migration process by calling the `migrate` method returned by the `useMigrateWallets` hook. ```ts theme={"system"} import {useMigrateWallets} from '@privy-io/expo'; const {migrate} = useMigrateWallets(); await migrate(); ``` ### Returns A promise that resolves to an object with a success property indicating if the user's wallets were migrated successfully. The promise will reject otherwise. Begin by disabling automatic migration by setting the `disableAutomaticMigration` flag to `true` in the Privy config. ```kotlin theme={"system"} val privy = Privy.init( context = applicationContext, config = PrivyConfig( appId = "your-privy-app-id", appClientId = "your-privy-app-client-id", logLevel = PrivyLogLevel.DEBUG, disableAutomaticMigration = true ) ) ``` Then, manually trigger the migration process by calling the `migrateWalletsIfNeeded` method on the authenticated user. ```kotlin theme={"system"} val user = privy.getUser() val result = user?.migrateWalletsIfNeeded() ``` ### Returns A Result that resolves to success if the user's wallets were migrated successfully. The Result will contain a failure otherwise. Begin by disabling automatic migration by setting the `disableAutomaticMigration` flag to `true` in the Privy config. ```dart theme={"system"} final privyConfig = PrivyConfig( appId: "your-privy-app-id", appClientId: "your-privy-app-client-id", logLevel: PrivyLogLevel.verbose, disableAutomaticMigration: true, ); final privy = Privy(config: privyConfig); ``` Then, manually trigger the migration process by calling the `migrateWalletsIfNeeded` method on the authenticated user. ```dart theme={"system"} final user = await privy.getUser(); final result = await user?.migrateWalletsIfNeeded(); ``` ### Returns A Result that resolves to success if the user's wallets were migrated successfully. The Result will contain a failure otherwise. Begin by disabling automatic migration by setting the `disableAutomaticMigration` flag to `true` in the embedded wallet config. ```swift theme={"system"} let config = PrivyConfig( appId: "your-privy-app-id", appClientId: "your-privy-app-client-id", embeddedWalletConfig: PrivyEmbeddedWalletConfig( disableAutomaticMigration: true ) ) ``` Then, manually trigger the migration process by calling the `migrateWalletsIfNeeded` method on the authenticated user. ```swift theme={"system"} let user = privy.getUser() try await user?.migrateWalletsIfNeeded() ``` ### Returns An async function that completes if the user's wallets were migrated successfully. The function will throw otherwise. ## Breaking changes Enabling TEE-based execution involves a limited set of breaking changes. These breaking changes do not apply to most apps. ### useSessionSigners TEE execution enables deeper configurability for clients to provision server-side access to user wallets. In particular, the advanced interface enables your app to specify policies or multiple signers on a wallet. To provision server-side access to user wallets, use the `useSessionSigner` hook instead of the previous `useDelegatedActions` hook. #### Adding signers (previous) ```javascript {1,4,7} theme={"system"} import { useHeadlessDelegatedActions, ConnectedWallet } from '@privy-io/react-auth'; function SessionSignersButton(wallet: ConnectedWallet) { const { delegateWallet } = useHeadlessDelegatedActions(); const handleAddSessionSigner = async () => { await delegateWallet({ address: wallet.address, chainType: wallet.type }); } return (
); } ``` Learn more about the previous interface [here](/wallets/using-wallets/signers/delegate-wallet). The previous interface for revoking access can be found [here](/wallets/using-wallets/signers/revoke-wallets). #### Adding signers (updated) The updated interface allows a signer ID to be specified and for policies to be set which constrain that signer. ```javascript {1, 4,7-15} theme={"system"} import {useSigners, ConnectedWallet} from '@privy-io/react-auth'; function SessionSignersButton(wallet: ConnectedWallet) { const {addSigners} = useSigners(); const handleAddSessionSigner = async () => { await addSigners({ address: wallet.address, signers: [ { signerId: "", policyIds: [" ); } ``` Learn more about the updated interface [here](/wallets/using-wallets/signers/add-signers). The updated interface for revoking access can be found [here](/wallets/using-wallets/signers/remove-signers). ## New advanced interfaces ### Server-side wallet creation TEE execution enables deeper configurability for server-side wallet creation. In particular, the advanced interface enables your app to specify policies and signers when you create a wallet from your server. The following interface update applies to [importing a single user](/user-management/migrating-users-to-privy/create-or-import-a-user), [batch importing users](/user-management/migrating-users-to-privy/create-or-import-a-batch-of-users), and [pregenerating wallets for existing users](/recipes/pregenerate-wallets) via the [`/users`](/api-reference/users), `/users/import` and `/v1/wallets` endpoints. #### Wallet creation (previous) (Optional) Whether to create an Ethereum wallet for the user. (Optional) Whether to create a Solana wallet for the user. (Optional) Whether to create an Ethereum smart wallet for the user. (Optional) The number of Ethereum wallets to pregenerate for the user. Defaults to `1`. #### Wallet creation (updated) The wallets to create for the user. Chain type of the wallet. "ethereum" supports any EVM-compatible network. List of policy IDs for policies that should be enforced on the wallet. Currently, only one policy is supported per wallet. The key ID of the signer. List of policy IDs for policies that should be enforced on the wallet. Currently, only one policy is supported per wallet. Set to `true` to create a smart wallet with the user's wallet as the signer. Can only be set on wallets where `chain_type` is `ethereum`. #### Example usage The following request body: ```json theme={"system"} { "create_ethereum_wallet": true, "create_smart_wallet": true, "create_solana_wallet": true } ``` can be updated to: ```json theme={"system"} { "wallets": [{"chain_type": "ethereum", "create_smart_wallet": true}, {"chain_type": "solana"}] } ``` In the updated interface, wallets may also set policies and signers, e.g.: ```json theme={"system"} { "wallets": [ {"chain_type": "ethereum", "policy_ids": [""], "create_smart_wallet": true}, { "chain_type": "solana", "additional_signers": [ {"signer_id": "", "override_policy_ids": ["policy-id-1"]}, {"signer_id": ""} ] } ] } ``` # Building a Telegram trading bot Source: https://docs.privy.io/recipes/telegram-bot Privy can power **Telegram trading bots** that trade on behalf of users. These bots can be controlled via commands in the Telegram app, natural language commands to LLMs, or purely agentic trading. ## Resources Complete starter repository showcasing a Telegram trading bot with Privy and Solana integration. At a high-level, there are two approaches to building a Telegram trading bot: * [**Bot-first**](/recipes/telegram-bot#bot-first-setup): Users first create and interact with their wallet via Telegram commands to the bot in the Telegram app itself. Later, the user can "claim" their wallet by logging into a web or mobile app to send transactions and export their private key from that interface. Users can also eventually revoke permissions for the bot to transact on their behalf. * [**App-first**](/recipes/telegram-bot#app-first-setup): Users first create and interact with their wallet by logging into a web or mobile app with their Telegram account, or logging in with an alternate method and then linking their Telegram account. They can send transactions and export their private key from the app, and can also grant permissions to the bot to transact on their behalf. Follow the guide below to learn how to build Telegram trading bots with Privy. Make sure to follow the appropriate section depending on if your app uses the **bot-first** or **app-first** setup. ## Configuring your bot To start, we'll cover the basics of creating and setting up your Telegram bot that can send transactions. ### Creating a bot First, create a new Telegram bot if you haven't already following the instructions below. 1. Create a new chat with @BotFather 2. Create a new bot with the command `/newbot` 3. Choose a name for your bot 4. Choose a username for your bot 5. Safely store your bot token. It should be in the format of, `BOT_ID:BOT_SECRET` 1. For this example, we will use Node.js to create a bot server, and specifically use the `node-telegram-bot-api` library. 2. Install the Telegram bot API library: ```bash npm theme={"system"} npm install node-telegram-bot-api ``` ```bash pnpm theme={"system"} pnpm install node-telegram-bot-api ``` ```bash yarn theme={"system"} yarn add node-telegram-bot-api ``` 3. Create a new file called `bot.js` and add the following code: ```ts @privy-io/node theme={"system"} const TelegramBot = require('node-telegram-bot-api'); const {PrivyClient} = require('@privy-io/node'); // replace the value below with the Telegram token you receive from @BotFather const token = 'YOUR_TELEGRAM_BOT_TOKEN'; // Create a bot that uses 'polling' to fetch new updates const bot = new TelegramBot(token, { polling: true }); const privy = new PrivyClient({ appId: 'insert-app-id', appSecret: 'insert-app-secret' }); ``` 4. Your app is now ready to receive messages from Telegram! ### Setting up commands Next, enable your users to interact with the bot via the Telegram app by configuring your bot to respond to Telegram commands. Use the bot's `onText` interface to register Telegram commands and the actions they should execute. For example, you might register a `/createwallet` command for users to create wallets via the Telegram app, or a `/transact` command for users to be able to transact. You can create a new command via the bot's `onText` method like so: ```ts @privy-io/node {skip-check} theme={"system"} bot.onText(/\/insert_command_name/, async (msg) => { await privy.wallets().ethereum().sendTransaction(...) }); ``` As an example, you might have a `/transact` command that allows users to send transactions like so. ```ts @privy-io/node {skip-check} theme={"system"} bot.onText(/\/transact/, async (msg) => { // Custom logic to infer the transaction to send from the user's message const transaction = getTransactionDetailsFromMsg(msg); // Get the Privy user object using the user's Telegram user ID (`msg.from.id`) const user = await privy.users().getByTelegramUserID({ telegram_user_id: msg.from.id }); // Search the user's linked accounts for their wallet const wallet = user.linked_accounts.find( (account) => account.type === 'wallet' && 'id' in account ); // Get the wallet ID from the wallet const walletId = wallet?.id; if (!walletId) throw new Error('Cannot determine wallet ID for user'); // Send transaction await privy.wallets().ethereum().sendTransaction(walletId, { caip2: 'eip155:1', params: {transaction} }); }); ``` ### Associating your user's wallet ID with their Telegram user ID In order for the Telegram bot to interact with a user's wallet, the bot must be able to determine what the user's wallet ID is. Within the command, you can access the user's Telegram user ID via the message's `from.id` property. You can then use the Privy client's `getByTelegramUserID` method to get their full Privy user object, as well as their wallet ID. ```ts @privy-io/node {skip-check} theme={"system"} bot.onText(/\/log_wallet_id/, async (msg) => { // Get the Privy user object using the user's Telegram user ID (`msg.from.id`) const user = await privy.users().getByTelegramUserID({ telegram_user_id: msg.from.id }); // Search the user's linked accounts for their wallet const wallet = user.linked_accounts.find( (account) => account.type === 'wallet' && 'id' in account ); // Get the wallet ID from the wallet const walletId = wallet?.id; console.log('Wallet ID', walletId); }); ``` ## Bot-first setup At a high-level, the **bot-first** setup works by creating a wallet associated with your user's Telegram handle, allowing them to transact with the wallet via commands made to your bot, and enabling users to claim their wallet or control it from a web or mobile app if desired. Follow the steps below for more concrete guidance. First, create a user in Privy and a wallet owned by that user. To allow your bot to transact on behalf of the user, create an [authorization key](/controls/authorization-keys/keys/create/key) and add it as an [additional signer](/wallets/using-wallets/signers/overview) on the wallet. ```ts @privy-io/node theme={"system"} bot.onText(/\/start/, async (msg) => { const telegramUserId = msg.from.id; // Create Privy user with Telegram user ID const privyUser = await privy.users().create({ linked_accounts: [ {type: 'telegram', telegram_user_id: telegramUserId} ] }); // Create wallet with user owner and the bot as an additional signer const wallet = await privy.wallets().create({ chain_type: 'ethereum', owner: { user_id: privyUser.id }, additional_signers: [{ signer_id: 'insert-signer-id', override_policy_ids: [] }], }); }); ``` Next, allow the user to transact with commands send to the bot. You might implement a `/transact` command that takes input on the user to transact on their behalf. Make sure to [configure your Privy client](/controls/authorization-keys/using-owners/sign) with the private key for the authorization key you created in the Dashboard. ```ts @privy-io/node theme={"system"} bot.onText(/\/transact/, async (msg) => { // Custom logic to infer the transaction to send from the user's message const transaction = getTransactionDetailsFromMsg(msg); // Determine user's wallet ID from their telegram ID const user = await privy.users().getByTelegramUserID({ telegram_user_id: msg.from.id }); const wallet = user.linked_accounts.find( (account) => account.type === 'wallet' && 'id' in account ); const walletId = wallet?.id; if (!walletId) throw new Error('Cannot determine wallet ID for user'); // Send transaction await privy.wallets().ethereum().sendTransaction(walletId, { caip2: 'eip155:1', params: {transaction} }); }); ``` If you'd like users to be able to claim their wallet via a web or mobile app, configure your web app with Privy's [React SDK](/basics/react/quickstart) or your mobile app with Privy's [React Native SDK](/basics/react-native/quickstart) and enable [Telegram login](/authentication/user-authentication/login-methods/oauth). Then, when users login to your app via Telegram, they can [send transactions](/wallets/using-wallets/ethereum/send-a-transaction) or [export their private keys](/wallets/wallets/export). ## App-first setup At a high-level, the **app-first** setup works by creating a wallet associated with your user when they login to your app with Telegram (or alternatively, link a Telegram account) and then adding a [signer](/wallets/using-wallets/signers/overview) to the wallet to allow the bot to transact on behalf of your user. Follow the steps below for more concrete guidance. If you have not already done so, instrument your web app with Privy's [React SDK](/basics/react/quickstart) or your mobile app with Privy's [React Native SDK](/basics/react-native/quickstart) and enable [Telegram login](/authentication/user-authentication/login-methods/oauth). When your users login to your app with Telegram or link a Telegram account, [create a wallet](/wallets/wallets/create/create-a-wallet) for them. Store a mapping between the ID of the created wallet and the user's Telegram ID so that you can determine the user's wallet within the bot's code. After the wallet has been created, add a signer to the user's wallet, which the bot can use to transact on the user's behalf. Make sure to store the private key(s) associated with your signer ID securely in your server. Your Telegram bot or agent will need this to execute transaction requests. Follow the linked quickstart below to learn how to add a signer to the user's wallet. Request access to user wallets with signers. Finally, the bot can use the signer to execute transactions on the user's behalf when prompted. For instance, you might implement a `/transact` command that takes input on the user to transact on their behalf. Make sure to [configure your Privy client](/controls/authorization-keys/using-owners/sign) with the private key for the signer (authorization key) you created in the Dashboard. ```ts @privy-io/node theme={"system"} import {isEmbeddedWalletLinkedAccount} from '@privy-io/node'; bot.onText(/\/transact/, async (msg) => { // Custom logic to infer the transaction to send from the user's message const transaction = getTransactionDetailsFromMsg(msg); // Determine user's wallet ID from their Telegram user ID const user = await privy.users().getByTelegramUserID({ telegram_user_id: msg.from.id }); const wallet = user.linked_accounts.find(isEmbeddedWalletLinkedAccount); const walletId = wallet?.id; if (!walletId) throw new Error('Cannot determine wallet ID for user'); // Send transaction await privy.wallets().ethereum().sendTransaction(walletId, { caip2: 'eip155:1', params: {transaction} }); }); ``` Authorize requests to the Privy API with your signer. Take actions on EVM chains with Privy's NodeJS SDK or REST API. Take actions on Solana with Privy's NodeJS SDK or REST API. # Overview Source: https://docs.privy.io/recipes/tempo/overview Overview of integrating Tempo with Privy for stablecoin transactions and digital payments. Build payment and deposit flows on [Tempo](https://tempo.xyz), a low-cost, high-throughput EVM-compatible blockchain optimized for payments, using Privy embedded wallets. Create embedded wallets and send transactions on Tempo. Configure policies to control fee tokens, sponsorship, and time windows on Tempo. # Using Tempo with Privy Source: https://docs.privy.io/recipes/tempo/send-transactions [Tempo](https://tempo.xyz) is a low-cost, high-throughput EVM-compatible blockchain optimized for payments. Just like with other [chains](/wallets/overview/chains), your app can use Privy to create wallets for users to send and receive payments on Tempo. This guide demonstrates how to create an embedded wallet and send transactions on Tempo. ## Prerequisites Before implementing Tempo support, ensure the following: * A Privy app configured with the application * Basic familiarity with [sending transactions](/wallets/using-wallets/ethereum/send-a-transaction) * (Optional) [Gas sponsorship enabled](/wallets/gas-and-asset-management/gas/setup) to sponsor transaction fees ## 1. Create an embedded wallet Privy embedded wallets support Tempo by default. To create an embedded wallet for a user on Tempo, use the same "Ethereum" `chain_type` as any other EVM-compatible chain. This recipe includes examples of creating an EVM-compatible wallet in React, Node, or via the REST API. See [Create a wallet](/wallets/wallets/create/create-a-wallet) for more guidance across all SDKs. ```typescript theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID, appSecret: process.env.PRIVY_APP_SECRET }); async function createTempoWallet(userId: string) { const wallet = await privy.wallets().create({ chain_type: 'ethereum' }); console.log('Created wallet:', wallet.address); return wallet; } ``` ```tsx theme={"system"} import {useCreateWallet} from '@privy-io/react-auth'; function CreateWallet() { const {createWallet} = useCreateWallet(); const handleCreateWallet = async () => { const wallet = await createWallet(); // creates an Ethereum-type wallet for the logged-in user console.log('Created wallet:', wallet.address); }; return ; } ``` Once created, the wallet address receives payments on Tempo. ## 2. Transfer tokens on Tempo The [`/transfer`](/wallets/actions/transfer/usage) endpoint provides the simplest integration path for sending tokens on Tempo. Privy handles Tempo transaction construction, token decimal precision, and gas sponsorship automatically. ```typescript {skip-check} theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID, appSecret: process.env.PRIVY_APP_SECRET }); const response = await privy.wallets().transfer('insert-wallet-id', { amount: '10.0', source: { asset: 'usdc', chain: 'tempo' }, destination: { address: '0xRecipientAddress' } }); console.log('Transfer action:', response.id, response.status); ``` ```bash theme={"system"} curl -X POST https://api.privy.io/v1/wallets/{wallet_id}/transfer \ -u ":" \ -H "privy-app-id: " \ -H "Content-Type: application/json" \ -d '{ "source": { "asset": "usdc", "amount": "10.0", "chain": "tempo" }, "destination": { "address": "0xRecipientAddress" } }' ``` ```json Example response theme={"system"} { "id": "action-id", "status": "pending", "wallet_id": "wallet-id", "created_at": "2026-04-14T20:09:11.929Z", "type": "transfer", "source_asset": "usdc", "source_amount": "10.0", "source_chain": "tempo", "destination_address": "0xRecipientAddress" } ``` If [gas sponsorship](/wallets/gas-and-asset-management/gas/setup) is enabled in the Privy Dashboard, the `/transfer` endpoint sponsors gas automatically. No additional parameters are needed. The `/transfer` endpoint also supports [cross-chain bridging](/wallets/actions/transfer/bridging) to and from Tempo. See the [transfer API reference](/wallets/actions/transfer/usage) for full parameter documentation. ## 3. Send custom transactions on Tempo For use cases beyond simple transfers — such as batched calls, custom contract interactions, specifying fee tokens, or using viem directly — use the low-level transaction APIs. To send transactions on Tempo mainnet, specify the CAIP-2 identifier `eip155:4217` when making transaction requests. This routes transactions to the Tempo network. To send transactions on Tempo Moderato (testnet), specify the CAIP-2 identifier `eip155:42431`. If no transaction type is specified, Privy sends a standard EIP-1559 transaction. Use Tempo transaction type `118` to access Tempo-native features like specifying the fee token. Tempo has no native gas token. To pay fees directly, set `feeToken` or `fee_token` to a supported TIP-20 token. If omitted, Tempo's [fee-token preference rules](https://docs.tempo.xyz/protocol/fees/spec-fee#fee-lifecycle) apply. Privy SDKs and REST API support Tempo transactions natively. Specify `type: 118` in the transaction params to access Tempo-native features like fee token control and gas sponsorship. ```typescript {skip-check} theme={"system"} import {PrivyClient} from '@privy-io/node'; import {encodeFunctionData, parseAbi} from 'viem'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID, appSecret: process.env.PRIVY_APP_SECRET }); const walletId = 'insert-wallet-id'; const USDC_E = '0x20c000000000000000000000b9537d11c60e8b50'; const data = encodeFunctionData({ abi: parseAbi(['function transfer(address to, uint256 amount) public returns (bool)']), functionName: 'transfer', args: ['0xRecipientAddress', 1_000_000n] // 1 USDC (6 decimals) }); const {hash} = await privy .wallets() .ethereum() .sendTransaction(walletId, { caip2: 'eip155:4217', // use 'eip155:42431' for Tempo Moderato (testnet) params: { transaction: { type: 118, fee_token: USDC_E, // pay Tempo fees in USDC calls: [ { to: USDC_E, data } ] } } }); console.log('Transaction hash:', hash); ``` ```tsx {skip-check} theme={"system"} import {useWallets} from '@privy-io/react-auth'; import {useSendTransaction} from '@privy-io/react-auth/tempo'; import {encodeFunctionData, parseAbi} from 'viem'; const usdc = '0x20c000000000000000000000b9537d11c60e8b50'; function SendTempoToken() { const {wallets} = useWallets(); const {sendTransaction} = useSendTransaction(); const handleSend = async () => { const wallet = wallets[0]; const {hash} = await sendTransaction({ transaction: { type: 118, chainId: 4217, // use 42431 for Tempo Moderato (testnet) feeToken: usdc, // pay Tempo fees in USDC calls: [ { to: usdc, data: encodeFunctionData({ abi: parseAbi([ 'function transfer(address to, uint256 amount) public returns (bool)' ]), functionName: 'transfer', args: ['0xRecipientAddress', 1_000_000n] // 1 USDC (6 decimals) }) } ] }, wallet }); console.log('Transaction hash:', hash); }; return ; } ``` ```bash theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "eth_sendTransaction", "caip2": "eip155:4217", "params": { "transaction": { "type": 118, "fee_token": "0x20c000000000000000000000b9537d11c60e8b50", "calls": [ { "to": "0x20c000000000000000000000b9537d11c60e8b50", "data": "" } ] } } }' ``` This approach uses viem's `tempoActions` extension to send native Tempo transactions. Use this path when building with viem directly, particularly for React embedded wallets. ```typescript {skip-check} theme={"system"} import {PrivyClient} from '@privy-io/node'; import {createViemAccount} from '@privy-io/node/viem'; import {createWalletClient, http, padHex, stringToHex} from 'viem'; import {tempo} from 'viem/chains'; import {tempoActions} from 'viem/tempo'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID, appSecret: process.env.PRIVY_APP_SECRET }); const account = createViemAccount(privy, { walletId: 'insert-wallet-id', address: 'insert-wallet-address' }); const walletClient = createWalletClient({ account, chain: tempo, transport: http() }).extend(tempoActions()); const usdc = '0x20c000000000000000000000b9537d11c60e8b50'; const receipt = await walletClient.token.transferSync({ to: '0xRecipientAddress', amount: 1_000_000n, // 1 USDC (6 decimals) token: usdc, memo: padHex(stringToHex('Hello, world!'), {size: 32}), feeToken: usdc // pay Tempo fees in USDC }); console.log(receipt); ``` ```tsx {skip-check} theme={"system"} import {useWallets, toViemAccount} from '@privy-io/react-auth'; import {createWalletClient, http, padHex, stringToHex} from 'viem'; import {tempo} from 'viem/chains'; import {tempoActions} from 'viem/tempo'; const usdc = '0x20c000000000000000000000b9537d11c60e8b50'; function SendTempoToken() { const {wallets} = useWallets(); const handleSend = async () => { const account = await toViemAccount({wallet: wallets[0]}); const walletClient = createWalletClient({ account, chain: tempo, // use tempoModerato from 'viem/chains' for testnet transport: http() }).extend(tempoActions()); const hash = await walletClient.token.transfer({ to: '0xRecipientAddress', amount: 1_000_000n, // 1 USDC (6 decimals) token: usdc, memo: padHex(stringToHex('Hello, world!'), {size: 32}), feeToken: usdc // pay gas fees in USDC }); console.log('Transaction hash:', hash); }; return ; } ``` ```bash theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "eth_sendTransaction", "caip2": "eip155:4217", "params": { "transaction": { "type": 118, "fee_token": "0x20c000000000000000000000b9537d11c60e8b50", "calls": [ { "to": "0x20c000000000000000000000b9537d11c60e8b50", "data": "" } ] } } }' ``` This approach sends a Tempo transaction with Privy paying the gas fee. [Gas sponsorship](/wallets/gas-and-asset-management/gas/setup) must be enabled in the Privy Dashboard before use, and requires TEE execution. Privy adds the Tempo fee payer signature before broadcasting the transaction. ```typescript {skip-check} theme={"system"} import {PrivyClient} from '@privy-io/node'; import {encodeFunctionData, parseAbi} from 'viem'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID, appSecret: process.env.PRIVY_APP_SECRET }); const USDC_E = '0x20c000000000000000000000b9537d11c60e8b50'; const data = encodeFunctionData({ abi: parseAbi(['function transfer(address to, uint256 amount) public returns (bool)']), functionName: 'transfer', args: ['0xRecipientAddress', 1_000_000n] // 1 USDC (6 decimals) }); const {hash} = await privy .wallets() .ethereum() .sendTransaction('insert-wallet-id', { caip2: 'eip155:4217', // use 'eip155:42431' for Tempo Moderato (testnet) params: { transaction: { type: 118, calls: [ { to: USDC_E, data } ] } }, sponsor: true }); console.log('Transaction hash:', hash); ``` ```tsx {skip-check} theme={"system"} import {useWallets} from '@privy-io/react-auth'; import {useSendTransaction} from '@privy-io/react-auth/tempo'; import {encodeFunctionData, parseAbi} from 'viem'; const usdc = '0x20c000000000000000000000b9537d11c60e8b50'; function SendSponsoredTempoTransaction() { const {wallets} = useWallets(); const {sendTransaction} = useSendTransaction(); const handleSend = async () => { const {hash} = await sendTransaction( { transaction: { type: 118, chainId: 4217, // use 42431 for Tempo Moderato (testnet) calls: [ { to: usdc, data: encodeFunctionData({ abi: parseAbi([ 'function transfer(address to, uint256 amount) public returns (bool)' ]), functionName: 'transfer', args: ['0xRecipientAddress', 1_000_000n] // 1 USDC (6 decimals) }) } ] }, wallet: wallets[0] }, {sponsor: true} ); console.log('Transaction hash:', hash); }; return ; } ``` ```bash theme={"system"} curl --request POST https://api.privy.io/v1/wallets//rpc \ -u ":" \ -H "privy-app-id: " \ -H "privy-authorization-signature: " \ -H "Content-Type: application/json" \ -d '{ "method": "eth_sendTransaction", "caip2": "eip155:4217", "sponsor": true, "params": { "transaction": { "type": 118, "calls": [ { "to": "0x20c000000000000000000000b9537d11c60e8b50", "data": "" } ] } } }' ``` ## Controlling transactions with policies Privy's policy engine supports Tempo-specific transaction fields via the `tempo_transaction` field source. Your app can require a specific fee token, enforce or block transaction sponsorship, and time-bound validity windows, all without affecting standard EVM policy rules. See [Tempo policy examples](/controls/policies/example-policies/tempo) for ready-to-use policy configurations. ## Related resources Complete guide to sending transactions with Privy Learn about Privy's gas sponsorship engine Subscribe to deposit and withdrawal events. Learn more about creating embedded wallets Configure policies for fee tokens, sponsorship, and time windows on Tempo # Tier 1 wallet integration Source: https://docs.privy.io/recipes/tier-1-wallet-integration Create wallets and sign with Privy on chains with Tier 1 support Tier 1 provides wallet creation, key export, and low-level signing. Your app builds and broadcasts transactions using the chain's SDK or RPC provider. For the complete support model and current chain list, see the [chain support overview](/wallets/overview/chains). ## Integration steps 1. [Create a wallet](/wallets/wallets/create/create-a-wallet) for the chain's supported key type. 2. Build the transaction or message with the chain's SDK. 3. Produce the payload that the chain expects the wallet to sign. 4. Sign the payload with Privy's [low-level signing interface](/wallets/using-wallets/other-chains). 5. Attach the signature and broadcast the transaction through the chain's RPC provider. Your app is responsible for chain-specific serialization, hashing, submission, and transaction tracking. Include any prefixes, suffixes, or domain separators required by the chain before signing. ## Chain examples ### Bitcoin (segwit) Bitcoin (segwit) supports the ECDSA signing algorithm using the secp256k1 curve. Use Privy's raw sign functionality to sign each input UTXO for your Bitcoin segwit transaction. For more details, see the [Bitcoin signing guide](/wallets/using-wallets/bitcoin/sign-transaction-inputs). ```typescript theme={"system"} import {p2wpkh, OutScript, getInputType, Transaction} from '@scure/btc-signer'; import {getPrevOut} from '@scure/btc-signer/transaction.js'; import {concatBytes} from '@scure/btc-signer/utils.js'; import secp256k1 from 'secp256k1'; const publicKey = ""; const publicKeyBuffer = Buffer.from(publicKey, 'hex'); const tx = new Transaction({version: 1, allowLegacyWitnessUtxo: true}); // add as many outputs as needed, in this example there is only one // note that the relay fee is sum(input amounts) - sum(output amounts) const outputAddress = ''; const outputAmount = 0n; tx.addOutputAddress(outputAddress, outputAmount); const inputAmount = 0n; tx.addInput({ txid: '', // buffer of utxo txid index: 0, // index of the output in the tx witnessUtxo: { amount: inputAmount, // this must match the amount of the input exactly script: p2wpkh(publicKeyBuffer).script } }); for (let i = 0; i < tx.inputsLength; i++) { const input = tx.getInput(i); const inputType = getInputType(input, tx.opts.allowLegacyWitnessUtxo); const prevOut = getPrevOut(input); let script = inputType.lastScript; // P2WPKH sighash uses the "pkh" script for signing if (inputType.last.type === 'wpkh') { script = OutScript.encode({type: 'pkh', hash: inputType.last.hash}); } const hash = tx.preimageWitnessV0(i, script, inputType.sighash, prevOut.amount); const signature = ''; // call Privy's raw sign function with bytesToHex(hash), returns '0x...' const signatureBuffer = Buffer.from(signature.slice(2), 'hex'); // convert to DER format const derSig = secp256k1.signatureExport(signatureBuffer); tx.updateInput( i, { partialSig: [[publicKeyBuffer, concatBytes(derSig, new Uint8Array([inputType.sighash]))]] }, true ); } tx.finalize(); // return tx ``` ### Bitcoin (taproot) Bitcoin (taproot) uses the Schnorr signing algorithm (BIP-340) with the secp256k1 curve. Privy automatically applies the BIP-341 key tweak when signing with a taproot wallet, producing signatures valid for key-path spends against the wallet's P2TR output. For more details, see the [Bitcoin signing guide](/wallets/using-wallets/bitcoin/sign-transaction-inputs). Privy's taproot support uses key-path spending only. The applied tweak assumes no custom Merkle roots or scripts, so script-path spends (e.g., custom Tapscript trees) are not supported. ```typescript {skip-check} theme={"system"} import {p2tr} from '@scure/btc-signer/payment'; import {Transaction, getInputType, getPrevOut} from '@scure/btc-signer/transaction'; // The wallet's `public_key` from the Privy Wallet object (33-byte compressed secp256k1 key, hex) const publicKey = wallet.public_key; // Extract the 32-byte x-only public key (strip the compression prefix byte) const pubKeyBytes = Buffer.from(publicKey, 'hex'); const xOnlyPubKey = pubKeyBytes.subarray(1); // Build the P2TR output for this wallet const taprootOutput = p2tr(xOnlyPubKey); const tx = new Transaction({allowLegacyWitnessUtxo: true}); // Add outputs const outputAddress = ''; const outputAmount = 0n; tx.addOutputAddress(outputAddress, outputAmount); // Add a taproot input const inputAmount = 0n; tx.addInput({ txid: '', // buffer of UTXO txid index: 0, // index of the output in the funding tx witnessUtxo: { amount: inputAmount, // must match the UTXO amount exactly script: taprootOutput.script }, tapInternalKey: xOnlyPubKey }); for (let i = 0; i < tx.inputsLength; i++) { const input = tx.getInput(i); const inputType = getInputType(input, true); const prevOut = getPrevOut(input); // Compute the BIP-341 sighash (witness v1) const sighash = tx.preimageWitnessV1(i, [prevOut.script], inputType.sighash, [prevOut.amount]); const signature = ''; // call Privy's raw sign function with bytesToHex(sighash), returns '0x...' // Attach the 64-byte Schnorr signature as tapKeySig const signatureBytes = Buffer.from(signature.slice(2), 'hex'); tx.updateInput(i, {tapKeySig: signatureBytes}, true); } // finalize() validates the Schnorr signature against the tweaked output key tx.finalize(); ``` ### Cosmos Cosmos utilizes the ECDSA signing algorithm with the secp256k1 curve. Below is an implementation example for signing hashes on Cosmos: ```typescript theme={"system"} import {Secp256k1, Secp256k1Signature} from '@cosmjs/crypto'; // Prepare the hash for signing const hash = '0x6503b027a625549f7be691646404f275f149d17a119a6804b855bac3030037aa'; // Obtain the raw signature from Privy's raw_sign endpoint const rawSignature = '...'; // call privy raw_sign on `hash` // Retrieve the wallet's public key from Privy const publicKey = '...'; // the wallet's public key from Privy // Verify the signature const signatureBytes = Secp256k1Signature.fromFixedLength( Buffer.from(rawSignature.slice(2), 'hex') ); const verified = await Secp256k1.verifySignature( signatureBytes, Buffer.from(hash.slice(2), 'hex'), Buffer.from(publicKey, 'hex') ); console.log('Signature valid?', verified); // true ``` ### Ton All wallets on Ton are smart contract accounts, and Ed25519 keypairs are used to sign transactions on behalf of the smart contracts. When creating a wallet via Privy, Privy will generate the Ed25519 keypair and predetermine the address of the wallet contract, assuming that the wallet uses `WalletContractV4` with a `workchain` of `0`. Privy will *not* deploy the contract itself; that is the responsibility of the developer. If you'd like to deploy a different wallet contract with the same keypair, the address will be different, but the request to Privy's API will remain the same. ```typescript theme={"system"} import {Cell, TonClient, WalletContractV4, internal} from '@ton/ton'; import {toHex} from 'viem'; // Create Client const client = new TonClient({ endpoint: 'https://toncenter.com/api/v2/jsonRPC' }); const walletId = ""; const publicKey = ""; const trimmedPublicKey = Buffer.from(publicKey.slice(2), 'hex'); // Create wallet contract let workchain = 0; // Usually you need a workchain 0 let wallet = WalletContractV4.create({ workchain, publicKey: trimmedPublicKey }); let contract = client.open(wallet); // Create a transfer let seqno: number = await contract.getSeqno(); const transfer = await contract.createTransfer({ seqno, messages: [ internal({ value: '1', to: 'to_address', body: 'Hello world' }) ], signer: async (msg: Cell) => { let hash = msg.hash(); let signature = '...'; // call Privy's raw sign function with toHex(hash), returns '0x...' return Buffer.from(signature.slice(2), 'hex'); } }); ``` ### Starknet On Starknet, all wallets are smart contract accounts. The wallet address is the contract address, and therefore is derived from account-specific data--namely, the account class hash, the constructor data, and the public key returned from the Privy API. The address returned from the Privy API assumes the use of [Ready's v0.5.0 account](https://github.com/argentlabs/argent-contracts-starknet/blob/6243bcf39fac0df25cff183056a9bc8f1e15ef28/deployments/account.txt#L1) class hash and the constructor call data, as shown below in the example. After creating a starknet wallet with Privy, STRK tokens must be sent to the address for the wallet. Then, the developer must deploy the account. If you wish to use a different account contract than Ready 0.5.0, we suggest maintaining the address-to-Privy-wallet mapping yourself at this time and ignoring the address returned from the Privy API. ```typescript theme={"system"} import { RpcProvider, SignerInterface, hash, CallData, CairoOption, CairoOptionVariant, CairoCustomEnum, Account, cairo, TypedData, Signature, Call, InvocationsSignerDetails, DeployAccountSignerDetails, DeclareSignerDetails } from 'starknet'; // connect RPC 0.8 provider const provider = new RpcProvider({ nodeUrl: 'https://starknet-sepolia.public.blastapi.io/rpc/v0_8' }); //new Argent X account v0.5.0 const ARGENT_X_ACCOUNT_CLASS_HASH_V0_5_0 = '0x073414441639dcd11d1846f287650a00c60c416b9d3ba45d31c651672125b2c2'; const publicKey = 'your public key'; // Calculate future address of the ArgentX account const axSigner = new CairoCustomEnum({Starknet: {pubkey: publicKey}}); const axGuardian = new CairoOption(CairoOptionVariant.None); const AXConstructorCallData = CallData.compile({ owner: axSigner, guardian: axGuardian }); const AXcontractAddress = hash.calculateContractAddressFromHash( publicKey, ARGENT_X_ACCOUNT_CLASS_HASH_V0_5_0, AXConstructorCallData, 0 ); // Use a RawSigner wrapper class around Signer. Example: https://github.com/argentlabs/argent-contracts-starknet/blob/6243bcf39fac0df25cff183056a9bc8f1e15ef28/lib/signers/signers.ts#L38 export abstract class RawSigner extends SignerInterface { abstract signRaw(messageHash: string): Promise; public async getPubKey(): Promise { throw new Error('Example'); } public async signMessage( typedDataArgument: TypedData, accountAddress: string ): Promise { throw new Error('Example'); } public async signTransaction( transactions: Call[], details: InvocationsSignerDetails ): Promise { throw new Error('Example'); } public async signDeployAccountTransaction( details: DeployAccountSignerDetails ): Promise { throw new Error('Example'); } public async signDeclareTransaction(details: DeclareSignerDetails): Promise { throw new Error('Example'); } } const account = new Account( provider, AXcontractAddress, new (class extends RawSigner { public async signRaw(messageHash: string): Promise { console.log('messageHash=', messageHash); // Get the signature using the privy raw sign method const sig = '..'; const sigWithout0x = sig.slice(2); const r = `0x${sigWithout0x.slice(0, 64)}`; const s = `0x${sigWithout0x.slice(64)}`; return [r, s]; } })() ); // The account address must hold STRK tokens to deploy the account. const accountDeployResult = await account.deployAccount({ classHash: ARGENT_X_ACCOUNT_CLASS_HASH_V0_5_0, contractAddress: AXcontractAddress, constructorCalldata: AXConstructorCallData, addressSalt: publicKey }); console.log('accountDeployResult=', accountDeployResult); // Transfer 1 STRK unit to your recipient address const STRK_TOKEN_ADDRESS = '0x04718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d'; const amount = cairo.uint256(1); // Simple transfer call using account.execute const transferCall = { contractAddress: STRK_TOKEN_ADDRESS, entrypoint: 'transfer', calldata: CallData.compile({ recipient: 'your recipient address', amount: amount }) }; const result = await account.execute(transferCall); await provider.waitForTransaction(result.transaction_hash); ``` # Tier 2 wallet integration Source: https://docs.privy.io/recipes/tier-2-wallet-integration Sign decoded transactions with Privy on chains with Tier 2 support Tier 2 adds transaction-aware signing. Privy decodes and validates supported transactions before signing, which enables transaction-level policy controls. Your app broadcasts signed transactions through the chain's RPC provider. For the complete support model and current chain list, see the [chain support overview](/wallets/overview/chains). ## Integration steps 1. [Create a wallet](/wallets/wallets/create/create-a-wallet) for the target chain. 2. Build and serialize a transaction with the chain's SDK. 3. Request a signature through the chain's supported Privy signing interface. 4. Attach the signature to the transaction. 5. Broadcast the signed transaction through the chain's RPC provider and track its status. When configuring [policies](/controls/policies/overview), use the transaction fields and policy methods supported by the chain's signing interface. ## Aptos example Below is a complete example of how to create a wallet, build a transaction, sign it with Privy's `rawSign` and broadcast it onchain. We use **Aptos** here, but the pattern is the same for all chains integrated through raw signing. ```typescript theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID, appSecret: process.env.PRIVY_APP_SECRET }); // Create an Aptos wallet for a user const wallet = await privy.wallets().create({ user_id: 'your-user-id', chain_type: 'aptos' }); console.log('Wallet ID:', wallet.id); console.log('Wallet Address:', wallet.address); console.log('Public Key:', wallet.public_key); ``` ```typescript theme={"system"} import { Aptos, AptosConfig, Network, AccountAddress, generateSigningMessageForTransaction } from '@aptos-labs/ts-sdk'; // Connect to Aptos network const aptos = new Aptos( new AptosConfig({ network: Network.MAINNET }) ); const address = AccountAddress.from(wallet.address); // Build a transaction (e.g., transfer APT tokens) const transaction = await aptos.transaction.build.simple({ sender: address, data: { function: '0x1::coin::transfer', typeArguments: ['0x1::aptos_coin::AptosCoin'], functionArguments: [ '0xRecipientAddress...', // recipient 100000000 // amount in Octas (0.1 APT) ] } }); // Generate the message that needs to be signed const message = generateSigningMessageForTransaction(transaction); ``` ```typescript theme={"system"} import {toHex} from 'viem'; // Sign the transaction using Privy's raw sign endpoint const signatureResponse = await privy.wallets().rawSign(wallet.id, { params: { hash: toHex(message) } }); const signature = signatureResponse as unknown as string; console.log('Signature:', signature); ``` ```typescript theme={"system"} import { AccountAuthenticatorEd25519, Ed25519PublicKey, Ed25519Signature } from '@aptos-labs/ts-sdk'; // Create the authenticator with public key and signature const authenticator = new AccountAuthenticatorEd25519( new Ed25519PublicKey(wallet.public_key), new Ed25519Signature(signature.slice(2)) ); // Submit the transaction const pendingTransaction = await aptos.transaction.submit.simple({ transaction, senderAuthenticator: authenticator }); // Wait for confirmation const executedTransaction = await aptos.waitForTransaction({ transactionHash: pendingTransaction.hash }); console.log('Transaction hash:', executedTransaction.hash); console.log('Transaction status:', executedTransaction.success); ``` ## Chain-specific implementation examples ### Sui Sui supports multiple cryptographic schemes, with Privy's implementation utilizing the Ed25519 curve and EdDSA signing algorithm. The following example demonstrates transaction signing for Sui, please note that the transaction bytes should be the full [intentMessage](https://docs.sui.io/concepts/transactions/transaction-auth/intent-signing): ```typescript theme={"system"} import {messageWithIntent, toSerializedSignature, PublicKey} from '@mysten/sui/cryptography'; import {Transaction} from '@mysten/sui/transactions'; import {verifyTransactionSignature, publicKeyFromRawBytes} from '@mysten/sui/verify'; import {toHex} from '@mysten/sui/utils'; import {base58} from '@scure/base'; const tx = new Transaction(); // ... add some transactions... const rawBytes = new Uint8Array(); // build transaction bytes with your configured Sui client const intentMessage = messageWithIntent('TransactionData', rawBytes); const bytes = Buffer.from(intentMessage).toString('hex'); const address = ''; // get public key from privy wallet and decode to Uint8Array const publicKey = publicKeyFromRawBytes('ED25519', base58.decode('')); // Obtain the raw signature from Privy's raw_sign endpoint // call privy raw_sign on `bytes`, `encoding` (`hex` or `base64`) and `hash_function` (`blake2b256`) and decode as Uint8Array const rawSignature = new Uint8Array(); // Create and verify the transaction signature const txSignature = toSerializedSignature({ signature: rawSignature, signatureScheme: 'ED25519', publicKey }); const signer = await verifyTransactionSignature(rawBytes, txSignature, {address}); console.log(signer.toSuiAddress() === address); // true ``` Privy's ["raw sign"](/wallets/using-wallets/other-chains) endpoint supports policy evaluation for `field_source` of `sui_transaction_command` and `sui_transfer_objects_command` with `bytes`, `encoding`, and `hash_function`. Configure those decoded transaction rules with the `signTransactionBytes` policy method. Message signing policies are also supported using `field_source` of `message` with the `signRawMessageBytes` method when signing with `hash`. See [example of Sui policies](/controls/policies/example-policies/sui). When an `amount` condition is configured on the `sui_transfer_objects_command` field\_source, always configure the `sui_transaction_command` to allow `MergeCoins`, `SplitCoins` and `TransferObjects` only. Transactions containing commands like `MakeMoveVec`, `MoveCall`, `Publish`, or `Upgrade` are not supported for now. ### Tron Tron implements the ECDSA signing algorithm using the secp256k1 curve. Privy's implementation returns 64-byte ECDSA signatures (r || s), while Tron requires 65-byte signatures that include a recovery ID (v) as the final byte. The recovery ID is essential because a 64-byte signature could correspond to two different addresses/private keys. The 65th byte, which can be either 0x1b or 0x1c (derived from 0 or 1 plus 27, following Ethereum standards), resolves this ambiguity. The following example demonstrates message signing and verification for Tron: ```typescript theme={"system"} import {TronWeb} from 'tronweb'; import {hashMessage} from 'tronweb/utils'; // Initialize with the wallet's Tron address const address = ""; // Determine the recovery ID for signature verification const getRecoveryId = async ({message, rawSignature}: {message: string; rawSignature: string}) => { return (await tronWeb.trx.verifyMessageV2(message, rawSignature + '1b')) === address ? '1b' : '1c'; }; // Initialize TronWeb const tronWeb = new TronWeb({ fullHost: 'xxx' }); // Prepare and sign the message const message = 'Hello world'; const hash = hashMessage(message); // Obtain the raw signature from Privy's raw_sign endpoint const rawSignature = '...'; // call privy raw_sign on `hash` // Verify the signature with the recovery ID const signerAddress = await tronWeb.trx.verifyMessageV2( message, rawSignature + (await getRecoveryId({message, rawSignature})) ); console.log(signerAddress === address); // true ``` ```typescript theme={"system"} import {TronWeb, Types} from 'tronweb'; const tronWeb = new TronWeb({ fullHost: 'https://api.shasta.trongrid.io' }); const walletId = ""; const from = ""; const to = ""; const amount = 1; const tx = (await tronWeb.transactionBuilder.sendTrx( to, amount, from )) as Types.SignedTransaction; const rawTxBytes = tronWeb.utils.code.hexStr2byteArray(tx.txID); const rawTxHex = '0x' + tronWeb.utils.code.byteArray2hexStr(rawTxBytes); const signature = '...'; // call Privy's raw sign function with rawTxHex, returns '0x...' (tx as Types.SignedTransaction).signature = [signature + '1b']; if (tronWeb.trx.ecRecover(tx) !== from) { (tx as Types.SignedTransaction).signature = [signature + '1c']; } const result = await tronWeb.trx.sendRawTransaction(tx); console.log('result', result); ``` Privy's ["raw sign"](/wallets/using-wallets/other-chains) endpoint supports policy evaluation for `TransferContract` and `TriggerSmartContract` transactions with `bytes`, `encoding`, and `hash_function`. Configure those decoded transaction rules with the `signTransactionBytes` policy method. Use the `signRawMessageBytes` policy method for unparsed raw signing requests that evaluate only system conditions. For structured Tron RPC transaction requests, it's preferable to configure `tron_signTransaction` or `tron_sendTransaction` policy rules instead. See [example of Tron policies](/controls/policies/example-policies/tron). ### Stellar Stellar implements the EdDSA signing algorithm using the Ed25519 curve. The following example demonstrates hash signing for Stellar transactions: ```typescript theme={"system"} import {Keypair} from '@stellar/stellar-sdk'; // Initialize with the wallet's Stellar address const address = ""; const keypair = Keypair.fromPublicKey(address); // Prepare the hash for signing const hash = '0x6503b027a625549f7be691646404f275f149d17a119a6804b855bac3030037aa'; // Obtain the raw signature from Privy's raw_sign endpoint const rawSignature = '...'; // call privy raw_sign on `hash` // Verify the signature const hashBytes = Buffer.from(hash.slice(2), 'hex'); const signatureBytes = Buffer.from(rawSignature.slice(2), 'hex'); const verified = keypair.verify(hashBytes, signatureBytes); console.log(verified); // true ``` ### Aptos Aptos is a Move VM chain which uses ed25519 keypairs for signing transactions. Below is an example of how to sign and send a transaction using Privy. See more developer docs [here](https://aptos.dev/build/sdks/ts-sdk/building-transactions). ```typescript theme={"system"} import { Aptos, AptosConfig, Network, AccountAddress, AccountAuthenticatorEd25519, Ed25519PublicKey, Ed25519Signature, generateSigningMessageForTransaction } from '@aptos-labs/ts-sdk'; import {toHex} from 'viem'; // 1) Wire up the client for the chain const aptos = new Aptos( new AptosConfig({ network: Network.MAINNET }) ); const walletId = ''; const publicKey = ''; // 32-byte ed25519 public key hex const address = AccountAddress.from(''); // 2) Build the raw transaction (SDK fills in seq#, chainId, gas if you let it) const rawTxn = await aptos.transaction.build.simple({ sender: address, data: { function: '0x1::coin::transfer', typeArguments: ['0x1::aptos_coin::AptosCoin'], functionArguments: ['', 1] // amount in Octas } }); const message = generateSigningMessageForTransaction(rawTxn); const signature = '...'; // call Privy's raw sign function with txHash, returns '0x...' // 5) Wrap pk + signature in an authenticator and submit const senderAuthenticator = new AccountAuthenticatorEd25519( new Ed25519PublicKey(publicKey), new Ed25519Signature(signature.slice(2)) ); const pending = await aptos.transaction.submit.simple({ transaction: rawTxn, senderAuthenticator }); const executed = await aptos.waitForTransaction({ transactionHash: pending.hash }); console.log('Executed:', executed.hash); ``` ### Near With Privy, you can create [Near-implicit accounts](https://docs.near.org/protocol/account-model) and sign over arbitrary data. Below is an example of how to create, sign, and send a Near transaction using Privy. (Note that Near requires accounts to be funded sending transactions.) ```typescript theme={"system"} import { JsonRpcProvider, baseDecode, PublicKey, parseNearAmount, actions, createTransaction, SignedTransaction, Signature } from 'near-api-js'; import {sha256} from '@noble/hashes/sha256'; import {base58} from '@scure/base'; import {toHex} from 'viem'; const nodeUrl = 'https://rpc.mainnet.near.org'; const provider = new JsonRpcProvider({url: nodeUrl}); const receiverId = 'receiver.near'; const amount = '1.5'; const nonce = 0; // If this is not the wallet's first transaction, set as current nonce const { header: {hash} } = await provider.viewBlock({finality: 'final'}); const blockHash = baseDecode(hash); const accountId = ""; const base58PublicKey = base58.encode(Buffer.from(accountId, 'hex')); const publicKey = PublicKey.fromString(`ed25519:${base58PublicKey}`); const amountYocto = parseNearAmount(Number(amount)); const transferActions = [actions.transfer(BigInt(amountYocto ?? 0))]; const tx = createTransaction(accountId, publicKey, receiverId, nonce, transferActions, blockHash); const serializedTx = tx.encode(); const txHash = toHex(sha256(serializedTx)); const signature = '...'; // call Privy's raw sign function with txHash, returns '0x...' const signedTx = new SignedTransaction({ transaction: tx, signature: new Signature({ keyType: tx.publicKey.keyType, data: Uint8Array.from(Buffer.from(signature.slice(2), 'hex')) }) }); const signedSerializedTx = signedTx.encode(); const result = await provider.sendJsonRpc('broadcast_tx_commit', [ Buffer.from(signedSerializedTx).toString('base64') ]); ``` ### Movement Movement is a Move VM chain that uses the Aptos chain standards. Below is an example of how to sign and send a transaction using Privy. See more developer docs [here](https://docs.movementnetwork.xyz/devs/interactonchain/wallet-adapter/aptos_wallet_standard#13-supporting-multiple-accounts-the-advanced-route). ```typescript theme={"system"} import { Aptos, AptosConfig, Network, AccountAddress, AccountAuthenticatorEd25519, Ed25519PublicKey, Ed25519Signature, generateSigningMessageForTransaction } from '@aptos-labs/ts-sdk'; import {toHex} from 'viem'; import {PrivyClient} from '@privy-io/node'; // Initialize your Privy client const privy = new PrivyClient({ appId: 'your-privy-app-id', appSecret: 'your-privy-app-secret' }); // 1) Wire up the client for the Movement chain const aptos = new Aptos( new AptosConfig({ network: Network.TESTNET, fullnode: 'https://full.testnet.movementinfra.xyz/v1' }) ); const walletId = ''; const publicKey = ''; // 32-byte ed25519 public key hex const address = AccountAddress.from(''); // 2) Build the raw transaction (SDK fills in seq#, chainId, gas if you let it) const rawTxn = await aptos.transaction.build.simple({ sender: address, data: { function: '0x1::coin::transfer', typeArguments: ['0x1::aptos_coin::AptosCoin'], functionArguments: ['', 1] // amount in Octas } }); const message = generateSigningMessageForTransaction(rawTxn); const signatureResponse = await privy.wallets().rawSign(walletId, {params: {hash: toHex(message)}}); const signature = signatureResponse as unknown as string; // 5) Wrap pk + signature in an authenticator and submit const senderAuthenticator = new AccountAuthenticatorEd25519( new Ed25519PublicKey(publicKey), new Ed25519Signature(signature.slice(2)) ); const pending = await aptos.transaction.submit.simple({ transaction: rawTxn, senderAuthenticator }); const executed = await aptos.waitForTransaction({ transactionHash: pending.hash }); console.log('Executed:', executed.hash); ``` # Trading apps resource page Source: https://docs.privy.io/recipes/trading-apps-homepage Privy gives you all the tools you need to build world-class trading experiences—securely, seamlessly, and at scale. Whether you're launching a new DEX, building a trading bot, or integrating on-chain swaps, Privy's infrastructure lets you focus on building great user experiences. ## Trusted by leading trading apps Privy powers leading trading apps such as [Hyperliquid](https://hyperliquid.xyz), [pump.fun](https://pump.fun), [dYdX](https://dydx.trade), [Vector](https://apps.apple.com/us/app/vector-buy-memecoins-crypto/id6502968192), [Jupiter](https://jup.ag), and [BananaGun](https://www.bananagun.io/). From instant wallet creation and flexible authentication to robust transaction controls and automated gas management, Privy's platform powers every step of the trading journey. With support for embedded and external wallets, cross-chain transactions, and granular transaction policy controls, you can deliver the features your users expect—without compromise. *** ## Common trading flows * **[Create a wallet](/wallets/wallets/create/create-a-wallet)**: Learn how to create and manage wallets for your users on any chain. * **[On-ramp funds](/financial-flows/deposits/overview)**: Let users buy crypto with fiat, Apple Pay, or Google Pay. * **[Send a transaction](/wallets/using-wallets/ethereum/send-a-transaction)**: Sign and send transactions from your app backend or client. * **[Off-ramp](/recipes/off-ramp-guide)**: Let users cash out to fiat with top providers. Our battle-tested infrastructure, rich on-chain integrations, and developer-friendly APIs make it easy to build, customize, and scale your trading experience. Security and compliance are built in from day one, so you can launch with confidence. *** ## Build your own trading app Privy makes it easy to build, customize, and scale your trading experience. Check out some of our guides to help you get started building your own trading app. Guide to building a Solana trading bot for Telegram. Learn how to integrate with Flashbots to avoid bot attacks. Integrate with the Hyperliquid SDKs to make trades on Hyperliquid. Learn how to integrate with Morpho lending protocol using Privy wallets. Configure custom control models where both users and your server can execute transactions with specific permissions. Request server-side access to user wallets to execute limit orders when users are offline. Enable users to fund wallets with Stripe's crypto onramp. Learn how to disable transaction confirmations for faster trading. Set up transaction policies to protect users and control spending limits on Ethereum and Solana. # Trading Source: https://docs.privy.io/recipes/trading/overview Trading recipes highlight patterns for market integrations, automated execution, and server-managed trading flows. Explore architecture and implementation patterns for trading apps. Build prediction market experiences with wallet-backed users. Launch automated trading interactions through Telegram. Execute trading actions with controlled server-side signers. Implement delegated limit-order execution workflows. Connect Privy wallets to perpetual trading on Hyperliquid. # Sponsor gas on Tron with Transatron Source: https://docs.privy.io/recipes/tron/transatron Use Transatron to cover Tron transaction fees so users can transact without holding TRX. [Transatron](https://transatron.io) is a Tron RPC provider that delegates the energy and bandwidth required to process a transaction, so end users can transact on Tron without holding TRX. Apps integrate Transatron as a drop-in replacement for the Tron full node and select a fee-payment mode that fits their UX. This recipe combines Privy's [Tron wallet support](/recipes/tier-2-wallet-integration#tron) with Transatron's gas sponsorship to broadcast TRX-less transfers from a user's embedded wallet. ## Resources Official Transatron integration documentation. How Privy signs Tron transactions via `raw_sign`. *** ## Prerequisites * A funded [Transatron account](https://te.transatron.io) and a **Spender** API key issued from the dashboard. The Spender key authorizes fee deduction from the app's prepaid TFN/TFU balance on every broadcast. Keep the Transatron API key on the server. Exposing it to a browser or mobile binary lets any caller spend the app's Transatron balance. ## How fee payment works Transatron supports four fee-payment modes: | Mode | When to use | User holds TRX? | | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------- | | **Internal account** | Default, simplest integration. Broadcasting through the Transatron RPC with a Spender key auto-deducts fees from the app's prepaid TFN/TFU balance. No per-transaction setup required. | No | | **Instant payment** | The user wallet holds a small TRX or USDT balance to cover Transatron's per-tx fee. Primarily for non-custodial wallet integrations. | Yes (small) | | **Coupon** | Per-transaction spend cap. The backend issues a coupon backed by its Transatron balance. Typically used for promos and individual discounts. | No | | **Bypass** | Fallback mode. The sender's wallet burns TRX directly for fees as on a vanilla Tron node. | Yes | All modes except bypass rely on broadcasting through the Transatron RPC. Submitting the same signed transaction to a vanilla Tron node bypasses Transatron's resource delegation logic, and Tron charges fees as usual. This recipe focuses on the **internal account** mode — the simplest integration path. Broadcasting through the RPC with a Spender key is all that's needed to sponsor fees. For other modes, see the [Transatron integration guide](https://docs.transatron.io/category/integration-guidelines). *** ## 1. Create a Tron wallet for the user Privy supports Tron at the Tier 2 level. Create a wallet with `chain_type: 'tron'`. The wallet address can receive TRX, USDT, and other TRC-20 assets. Use `useCreateWallet` from the extended-chains entrypoint to provision a Tron wallet for a logged-in user. ```tsx theme={"system"} import {useCreateWallet} from '@privy-io/react-auth/extended-chains'; function CreateTronWallet() { const {createWallet} = useCreateWallet(); const handleCreate = async () => { const {wallet} = await createWallet({chainType: 'tron'}); console.log('Tron address:', wallet.address); }; return ; } ``` ```typescript theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID, appSecret: process.env.PRIVY_APP_SECRET }); const wallet = await privy.wallets().create({ chain_type: 'tron' }); console.log('Wallet ID:', wallet.id); console.log('Tron address:', wallet.address); ``` ```bash theme={"system"} curl --request POST https://api.privy.io/v1/wallets \ -u ":" \ -H "privy-app-id: " \ -H 'Content-Type: application/json' \ -d '{ "owner": {"user_id": "did:privy:xxxxxx"}, "chain_type": "tron" }' ``` Newly created Tron addresses may require on-chain activation before some sending flows work reliably. Fund the address with a small TRX amount first if activation related errors appear. ## 2. Configure TronWeb to use the Transatron RPC Point TronWeb at `https://api.transatron.io` and attach the Spender API key as a header. All subsequent operations — fee estimation, simulation, and broadcasts — flow through Transatron. The Spender key on the connection authorizes automatic fee deduction from the app's prepaid balance on every broadcast. ```typescript theme={"system"} import {TronWeb, providers} from 'tronweb'; const TRANSATRON_RPC = 'https://api.transatron.io'; const TRANSATRON_TIMEOUT = 60_000; const headers = {'TRANSATRON-API-KEY': process.env.TRANSATRON_API_KEY!}; const tronWeb = new TronWeb({ fullNode: new providers.HttpProvider(TRANSATRON_RPC, TRANSATRON_TIMEOUT, '', '', headers), solidityNode: new providers.HttpProvider(TRANSATRON_RPC, TRANSATRON_TIMEOUT, '', '', headers), eventServer: new providers.HttpProvider(TRANSATRON_RPC, TRANSATRON_TIMEOUT, '', '', headers) }); ``` ## 3. Sign Tron transactions with Privy Privy's `raw_sign` returns a 64-byte ECDSA signature, but Tron expects 65 bytes — the trailing recovery byte is either `0x1b` or `0x1c`. Signing is the only step in the flow that can run on the client; building and broadcasting still happen on the server, since both require the Transatron API key. Call `useSignRawHash` with `chainType: 'tron'` to have the user's embedded wallet sign the transaction's `txID`. Send the returned 64-byte signature back to your server, which will attach the recovery byte and broadcast. ```tsx theme={"system"} import {useSignRawHash} from '@privy-io/react-auth/extended-chains'; function useSignTronTxId() { const {signRawHash} = useSignRawHash(); return async ({address, txId}: {address: string; txId: string}) => { const txIdHex = txId.startsWith('0x') ? txId : `0x${txId}`; const {signature} = await signRawHash({ address, chainType: 'tron', hash: txIdHex as `0x${string}` }); return signature; }; } ``` The corresponding server helper attaches the recovery byte after receiving the signature from the client: ```typescript theme={"system"} import type {TronWeb, Types} from 'tronweb'; function attachTronSignature({ tronWeb, walletAddress, transaction, signature }: { tronWeb: TronWeb; walletAddress: string; transaction: Types.SignedTransaction; signature: string; }): Types.SignedTransaction { const baseSig = signature.replace(/^0x/, ''); transaction.signature = [`${baseSig}1b`]; if (tronWeb.trx.ecRecover(transaction) !== walletAddress) { transaction.signature = [`${baseSig}1c`]; } return transaction; } ``` When signing happens entirely on the server (e.g. for backend bots or schedulers), call `privy.wallets().rawSign()` directly and probe both recovery bytes inline. ```typescript theme={"system"} import {PrivyClient} from '@privy-io/node'; import type {TronWeb, Types} from 'tronweb'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID, appSecret: process.env.PRIVY_APP_SECRET }); async function signTronTransaction({ tronWeb, walletId, walletAddress, transaction }: { tronWeb: TronWeb; walletId: string; walletAddress: string; transaction: Types.SignedTransaction; }): Promise { const txIdHex = transaction.txID.startsWith('0x') ? transaction.txID : `0x${transaction.txID}`; const {signature} = await privy.wallets().rawSign(walletId, { params: {hash: txIdHex} }); const baseSig = (signature as string).replace(/^0x/, ''); transaction.signature = [`${baseSig}1b`]; if (tronWeb.trx.ecRecover(transaction) !== walletAddress) { transaction.signature = [`${baseSig}1c`]; } return transaction; } ``` ## 4. Broadcast a sponsored transaction With the internal-account mode, broadcasting through the Transatron RPC is all that's needed to sponsor fees. The Spender key on the connection authorizes the charge — no coupon creation or additional per-transaction setup is required. The code below runs entirely on the server. If signing happens in a React client (per step 3), replace the inline `signTronTransaction(...)` call with a roundtrip — return the unsigned transaction's `txID` to the client, receive the 64-byte signature back, and finalize via `attachTronSignature(...)` before broadcasting. Tron rejects transactions whose `fee_limit` is lower than the energy required to execute them. Use `triggerConstantContract` to estimate `energy_used` and multiply by the live `energy_fee` from chain parameters. This is the Tron-side fee limit that gets baked into the signed transaction. This example uses USDT on Tron mainnet. ```typescript theme={"system"} const USDT_CONTRACT = 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t'; // USDT on Tron mainnet async function estimateFeeLimit({ ownerAddress, recipientAddress, amountBaseUnits }: { ownerAddress: string; recipientAddress: string; amountBaseUnits: string; }): Promise { const ownerHex = tronWeb.address.toHex(ownerAddress); const contractHex = tronWeb.address.toHex(USDT_CONTRACT); const constant = await tronWeb.transactionBuilder.triggerConstantContract( contractHex, 'transfer(address,uint256)', {}, [ {type: 'address', value: recipientAddress}, {type: 'uint256', value: amountBaseUnits} ], ownerHex ); const params = await tronWeb.trx.getChainParameters(); const energyFee = params.find((p) => p.key === 'getEnergyFee')?.value ?? 420; const energyUsed = constant.energy_used ?? 0; return Math.ceil(energyUsed * energyFee * 1.1); // 10% buffer } ``` Build the TRC-20 transfer locally, sign the `txID` with Privy, and broadcast through the Transatron RPC. The Spender key on the connection handles fee payment automatically. ```typescript theme={"system"} async function sendSponsoredTransfer({ walletId, walletAddress, recipientAddress, amountBaseUnits, feeLimit }: { walletId: string; walletAddress: string; recipientAddress: string; amountBaseUnits: string; feeLimit: number; }) { const ownerHex = tronWeb.address.toHex(walletAddress); const contractHex = tronWeb.address.toHex(USDT_CONTRACT); const built = await tronWeb.transactionBuilder.triggerSmartContract( contractHex, 'transfer(address,uint256)', {feeLimit, callValue: 0}, [ {type: 'address', value: recipientAddress}, {type: 'uint256', value: amountBaseUnits} ], ownerHex ); if (!built.transaction) { throw new Error('Failed to build transfer transaction'); } const signed = await signTronTransaction({ tronWeb, walletId, walletAddress, transaction: built.transaction as Types.SignedTransaction }); return tronWeb.fullNode.request('wallet/broadcasttransaction', signed, 'post'); } ``` The broadcast response includes a nested `transatron` object. Fields can vary by mode and API version (for example `tx_fee_rtrx_account`, `code`, or zero-fee counters), but the key signal is a sponsored/free-fee outcome instead of a Tron burn from the sender wallet. Chain the helpers from the previous steps to send a sponsored TRC-20 transfer. With internal-account sponsorship, transaction fees can be covered by Transatron instead of burning TRX from the sender wallet. ```typescript theme={"system"} const feeLimit = await estimateFeeLimit({ ownerAddress, recipientAddress, amountBaseUnits }); const result = await sendSponsoredTransfer({ walletId, walletAddress: ownerAddress, recipientAddress, amountBaseUnits, feeLimit }); ``` *** ## Operating the business account Use the following Transatron API endpoints to check account balance, review spending history, and fetch current pricing. Query the current TFN/TFU balance available for sponsoring transactions: ```typescript theme={"system"} const config = await tronWeb.fullNode.request<{ balance_rtrx: number; balance_rusdt: number; }>('api/v1/config', {}, 'get'); console.log('TFN balance:', config.balance_rtrx); console.log('TFU balance:', config.balance_rusdt); ``` Retrieve recent fee charges against the account to audit per-transaction costs: ```typescript theme={"system"} const ordersResponse = await tronWeb.fullNode.request<{ orders?: Array<{ order_id: string; order_date: string; amount_trx: number; charge_token: 'RTRX' | 'RUSDT'; }>; pagination?: {limit: number; offset: number; total: number}; }>( 'api/v1/orders', { limit: 50, offset: 0, from_date: '2026-01-01T00:00:00Z', to_date: '2026-03-01T00:00:00Z' }, 'get' ); const orders = ordersResponse.orders ?? []; for (const order of orders) { console.log( `Order ${order.order_id}: ${order.amount_trx} sun (${order.charge_token}) at ${order.order_date}` ); } ``` Read pricing from the same `GET /api/v1/config` response: ```typescript theme={"system"} const config = await tronWeb.fullNode.request<{ activation_price: number; energy_price_per_unit: number; bandwidth_price_per_unit: number; }>('api/v1/config', {}, 'get'); console.log('Activation price:', config.activation_price, 'sun'); console.log('Energy price:', config.energy_price_per_unit, 'sun per unit'); console.log('Bandwidth price:', config.bandwidth_price_per_unit, 'sun per unit'); ``` These account-management endpoints require the ADMIN (spender) API key. For full endpoint details, see the [Transatron account management docs](https://docs.transatron.io/category/account-management). # Integrating with tRPC Source: https://docs.privy.io/recipes/trpc **[tRPC](https://trpc.io) is an end-to-end typesafe API built in Typescript.** This guide shows how to integrate Privy into any tRPC application. There are two steps to enable auth in tRPC with Privy: * in your [client](/recipes/trpc.mdx#configuring-your-client), include the user's access token on requests * in your [server](/recipes/trpc.mdx#protecting-routes-on-your-server), secure procedures by validating the token included on requests If you're using tRPC with [zod](https://github.com/colinhacks/zod), check out [this transformation tool](https://transform.tools/typescript-to-zod) to automatically generate zod schemas from Privy's types (e.g. **`user.email`**). ## Configuring your client **When your client (frontend) makes a request to one of your tRPC procedures, you should include the Privy auth token, so that your server can verify that the user is authenticated.** The following works for both [`createTRPCProxyClient`](https://trpc.io/docs/typedoc/client/functions/createTRPCProxyClient-1) (vanilla) or [`createTRPCNextClient`](https://trpc.io/docs/nextjs#createtrpcnext-options) (Next.js). Note that while the configuration method signature is different between the two, the inner configuration object/strategy will remain the same. The example shown is for NextJS. When [scaffolding the tRPC client](https://trpc.io/docs/vanilla), pass the Privy auth token through the header of every request, via an [`httpBatchLink`](https://trpc.io/docs/links/httpBatchLink) within the `links` configuration. Below is an example: ```tsx theme={"system"} import {httpBatchLink} from '@trpc/client'; import {createTRPCNext} from '@trpc/next'; import {getAccessToken} from '@privy-io/react-auth'; export const api = createTRPCNext({ config() { return { links: [ httpBatchLink({ url: `your_base_url`, // apply the privy token to each request async headers() { return { Authorization: `Bearer ${(await getAccessToken()) || ''}` }; } }) ] }; } }); ``` ## Protecting routes on your server **When your server receives a request from the client, it should validate the Privy auth token to confirm included in the request to ensure that it is authenticated.** First, parse the passed token using jose where you create your tRPC context: ```ts @privy-io/node theme={"system"} import * as trpc from '@trpc/server'; import {inferAsyncReturnType} from '@trpc/server'; import * as trpcNext from '@trpc/server/adapters/next'; import {PrivyClient, VerifyAuthTokenResponse} from '@privy-io/node'; // configure your privy server auth client const privy = new PrivyClient({ appId: process.env.NEXT_PUBLIC_PRIVY_APP_ID || '', appSecret: process.env.PRIVY_APP_SECRET || '' }); export async function createContext({req, res}: trpcNext.CreateNextContextOptions) { const authToken = req.headers.authorization.replace('Bearer ', ''); let userClaim: VerifyAuthTokenResponse | undefined = undefined; if (authToken) { try { userClaim = await privy.utils().auth().verifyAuthToken(authToken); // the claim contains all details about the validated privy token and can be passed // via the context for use in all server routes // if you want to pull additional details about the user via your api / db, such as whether the user is an // admin, here's your chance! } catch (_) { // this is an expected error for tRPC procedures that don't need to be authenticated // if privy is expected, we will throw a 403 at the middleware level, shown in the next step } } return { userClaim }; } export type Context = inferAsyncReturnType; ``` Next, create a middleware procedure for protecting routes: ```typescript {skip-check} theme={"system"} const isPrivyAuthed = t.middleware(async ({ctx, next}) => { // check to make sure that the token was valid. // you can add further logic here, such as checking if the user is an admin, // if you added more user context within `createContext` above. if (!ctx.userClaim) { throw new TRPCError({ code: 'UNAUTHORIZED', message: 'Not authenticated' }); } return next({ ctx }); }); export const privyProtectedProcedure = t.procedure.use(isPrivyAuthed); ``` Finally, when defining routes, you can use your procedure middleware to ensure the user is properly authenticated. ```typescript {skip-check} theme={"system"} t.router({ // this is accessible for everyone hello: t.procedure .input(z.string().nullish()) .query(({input, ctx}) => `hello ${input ?? ctx.user?.name ?? 'world'}`), admin: t.router({ // this is accessible only to admins secret: privyProtectedProcedure.query(({ctx}) => { return { secret: 'sauce' }; }) }) }); ``` # Using stateful policies with Privy Source: https://docs.privy.io/recipes/using-stateful-policies Daily transfer limits are a standard fraud and risk control in finance. Stateful aggregation policies enforce these limits at the wallet layer without a separate rate limiting system. Once a wallet reaches its rolling cap, Privy rejects signing requests until the window resets. This recipe walks through enforcing a 24-hour USDC transfer cap on a server wallet. The same pattern applies to user withdrawal limits, hot wallet circuit breakers, or per-account spend controls. This approach has two parts: * **Aggregation**: Tracks the running sum of USDC `transfer` amounts from `eth_signTransaction` requests over a rolling time window * **Policy**: References the aggregation and rejects signing if the running total would exceed the cap Aggregation values are updated **after** a request is successfully signed. This means multiple concurrent requests may all pass policy evaluation before any of their values are recorded. Stateful policies are designed for **disaster prevention**, not strict real-time enforcement. For tighter control, combine aggregation-based caps with per-transaction limits and rate limiting in the application layer. Create an aggregation that sums the `amount` field from ERC-20 `transfer` calldata over a rolling 24-hour window. Scope it to the USDC contract so only USDC transfers count toward the cap. Aggregation creation is via the [REST API](/api-reference/aggregations/create). The Node SDK `aggregations` interface does not yet expose a `create` method. ```typescript {skip-check} theme={"system"} const PRIVY_APP_ID = process.env.PRIVY_APP_ID!; const PRIVY_APP_SECRET = process.env.PRIVY_APP_SECRET!; // USDC on Sepolia. Replace with the target chain's USDC address. const USDC_ADDRESS = '0x1c7D4B196Cb0C7B01d743Fbc6116a902379C7238'; const credentials = Buffer.from(`${PRIVY_APP_ID}:${PRIVY_APP_SECRET}`).toString('base64'); const response = await fetch('https://api.privy.io/v1/aggregations', { method: 'POST', headers: { 'Content-Type': 'application/json', 'privy-app-id': PRIVY_APP_ID, Authorization: `Basic ${credentials}` }, body: JSON.stringify({ name: 'USDC daily transfer tracker', method: 'eth_signTransaction', metric: { field: 'transfer.amount', field_source: 'ethereum_calldata', function: 'sum', abi: [ { type: 'function', name: 'transfer', stateMutability: 'nonpayable', inputs: [ {type: 'address', name: 'to'}, {type: 'uint256', name: 'amount'} ], outputs: [{type: 'bool'}] } ] }, window: { type: 'rolling', seconds: 86400 // 24 hours }, conditions: [ { field_source: 'ethereum_transaction', field: 'to', operator: 'eq', value: USDC_ADDRESS } ] }) }); if (!response.ok) { throw new Error(`Failed to create aggregation: ${await response.text()}`); } const {id: aggregationId} = await response.json(); ``` Save the returned `aggregationId`. The next step uses it in the policy condition. Each Privy app supports a maximum of **10 aggregations**. If your app provisions many wallets, consider using `group_by` to partition a single aggregation by wallet address rather than creating one aggregation per wallet. Create a policy with an `eth_signTransaction` rule that allows USDC transfers only while the 24-hour rolling sum stays at or below the cap. Set `field_source: 'reference'` and prefix the aggregation ID with `aggregation.` to reference it. ```typescript {skip-check} theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: PRIVY_APP_ID, appSecret: PRIVY_APP_SECRET }); // 100 USDC cap: 100 × 10^6 base units = 100,000,000 = 0x5F5E100 const SPENDING_CAP_HEX = '0x5F5E100'; const policy = await privy.policies.create({ name: 'USDC 100/24h transfer cap', version: '1.0', chain_type: 'ethereum', rules: [ { name: 'Allow USDC transfers within 24h rolling cap', method: 'eth_signTransaction', action: 'ALLOW', conditions: [ { field_source: 'ethereum_transaction', field: 'to', operator: 'eq', value: USDC_ADDRESS }, { field_source: 'reference', field: `aggregation.${aggregationId}`, operator: 'lte', value: SPENDING_CAP_HEX } ] } ] }); ``` USDC uses 6 decimal places. Convert a human-readable amount to base units before setting it as a hex cap value: `100 USDC = 100 × 10^6 = 100,000,000 = 0x5F5E100`. A policy denies any RPC method not explicitly covered by a rule. If this wallet also needs to sign typed data, call other contracts, or use other RPC methods, add explicit `ALLOW` rules for those methods. Save `policy.id`. The wallet creation step and the spending-cap update step both need it. Create a server wallet and attach the policy using `policy_ids`: ```typescript {skip-check} theme={"system"} const wallet = await privy.wallets.create({ chain_type: 'ethereum', policy_ids: [policy.id] }); // Save wallet.id (used to sign transactions) and wallet.address (the on-chain address) ``` The policy is now active on the wallet. Each `eth_signTransaction` request checks the rule before signing. Use `eth_signTransaction` to sign the transaction. This is where Privy checks the policy and updates the aggregation. After signing, send the raw transaction via any RPC node. ```typescript {skip-check} theme={"system"} import {encodeFunctionData, erc20Abi, parseUnits, createPublicClient, http} from 'viem'; import {sepolia} from 'viem/chains'; const recipientAddress = '0x...'; const amount = '25'; // USDC to send // Encode the ERC-20 transfer calldata const data = encodeFunctionData({ abi: erc20Abi, functionName: 'transfer', args: [recipientAddress as `0x${string}`, parseUnits(amount, 6)] }); // Sign via Privy. Policy and aggregation are evaluated here. const signResponse = await privy.wallets._rpc(wallet.id, { method: 'eth_signTransaction', chain_type: 'ethereum', params: { transaction: { from: wallet.address, to: USDC_ADDRESS, data, chain_id: sepolia.id // 11155111 } } }); // signResponse.data contains the signed transaction const {signed_transaction} = (signResponse.data as any).data; // Broadcast using viem or any Ethereum RPC client const publicClient = createPublicClient({ chain: sepolia, transport: http(process.env.RPC_URL) }); const txHash = await publicClient.sendRawTransaction({ serializedTransaction: signed_transaction as `0x${string}` }); ``` The policy check is **forward-looking**: the engine checks whether the running total plus the current request amount would exceed the cap. For example, a wallet that has spent 90 USDC is blocked from a 15 USDC transfer, even though the cap is 100 USDC. When a signing request would push the running total past the cap, Privy returns a `400` error with code `policy_violation`. Handle it and return a clear error: ```typescript {skip-check} theme={"system"} import {BadRequestError} from '@privy-io/node'; async function sendUsdcWithBudgetGuard( walletId: string, walletAddress: string, recipient: string, amountUsdc: string ) { const data = encodeFunctionData({ abi: erc20Abi, functionName: 'transfer', args: [recipient as `0x${string}`, parseUnits(amountUsdc, 6)] }); try { const signResponse = await privy.wallets._rpc(walletId, { method: 'eth_signTransaction', chain_type: 'ethereum', params: { transaction: { from: walletAddress, to: USDC_ADDRESS, data, chain_id: sepolia.id } } }); const {signed_transaction} = (signResponse.data as any).data; const txHash = await publicClient.sendRawTransaction({ serializedTransaction: signed_transaction as `0x${string}` }); return {status: 'submitted', txHash}; } catch (error) { if (error instanceof BadRequestError && (error.error as any)?.code === 'policy_violation') { // Spending cap reached. 24-hour window has not yet rolled over. return {status: 'blocked', reason: 'spending_limit_reached'}; } throw error; } } ``` ## Common pitfalls **Aggregation conditions and policy conditions are separate.** The aggregation's `conditions` array controls what gets tracked. The policy rule's `conditions` control what gets allowed or denied. If these diverge, the cap can reset without warning. For example, if the aggregation tracks USDC transfers to one contract but the policy rule covers all USDC transfers, spend to other contracts is excluded from the cap. Keep both condition sets in sync. **`eth_sendTransaction` spend is invisible to aggregations.** Aggregations only track `eth_signTransaction` and `eth_signUserOperation` requests. If the wallet also handles `eth_sendTransaction` calls, that spend does not count toward the cap. This recipe uses `eth_signTransaction` throughout to track all outflow. **`group_by` extraction failures deny the request.** When `group_by` is set and Privy cannot extract the grouping field from a transaction, the policy denies the request rather than falling back to a global bucket. This can happen when the calldata does not match the expected function signature or the ABI is absent. When the group key source is optional or variable, omit `group_by` and use per-wallet aggregations instead. **ABI mismatch silently passes.** If the transaction calldata does not decode against the aggregation metric's ABI, the extracted value defaults to `0`. The transaction passes the policy check as if nothing was sent. An incorrect ABI means Privy never enforces the cap for those transactions. Verify ABI decoding before relying on the cap in production. **Reverted transactions still count.** The aggregation updates when signed, not when confirmed on-chain. If a signed transaction reverts on-chain, the spend still counts. Gas failures and reverts do not cancel the aggregation increment. Plan for gas and slippage. **The rolling window is continuous, not clock-based.** A 24-hour window means the last 86,400 seconds from the exact moment of the request. There is no midnight reset. A wallet that sends 100 USDC at 11:59 PM is blocked until 11:59 PM the following day, not until midnight. ## Patch behavior Privy applies policy rule updates **in place**. The policy ID does not change, and wallets do not need updates after a rule change. Privy recommends updating each rule with `_updateRule` rather than replacing the full policy with `_update`. Updating each rule avoids race conditions from concurrent changes. See [updating policy rules](/controls/policies/update-a-policy#updating-policy-rules-individually) for more detail. If the policy has an `owner_id`, each update needs the owner's signature. See [authorization signatures](/api-reference/authorization-signatures) for details. ## Cleanup When a wallet is retired, delete its policy and aggregation to stay within the 10-aggregation limit. Delete the policy **before** deleting the aggregation. If the aggregation is deleted while a policy still references it, all conditions referencing that aggregation evaluate to `false`, which denies signing requests. ```typescript {skip-check} theme={"system"} // 1. Delete the policy await privy.policies._delete(policy.id); // 2. Delete the aggregation via the REST API const deleteResponse = await fetch(`https://api.privy.io/v1/aggregations/${aggregationId}`, { method: 'DELETE', headers: { 'privy-app-id': PRIVY_APP_ID, Authorization: `Basic ${credentials}` } }); if (!deleteResponse.ok) { throw new Error(`Failed to delete aggregation: ${await deleteResponse.text()}`); } ``` # Using test accounts Source: https://docs.privy.io/recipes/using-test-accounts Test accounts can be used to build automated tests, for local development, or to reduce friction during Apple's [App Store review](https://developer.apple.com/app-store/review/) process for mobile apps. A new set of credentials are created each time a test account is enabled, and the old set is revoked to keep the app secure. Enabling a test account registers a set of **test credentials** (a hardcoded email or phone number and OTP code). It does **not** create a Privy user record. The user record is created on the first successful login with those credentials, just like any real user. To use test accounts, the app must support email or SMS login. Testing other login flows can either be automated with a library like [Playwright](https://playwright.dev/), or by completing the flow manually, as it requires authorization with other APIs *(such as social providers)*. ## Enabling test accounts To enable a test account for an app and get its login credentials: 1. Go to the **User management > Authentication > Advanced** tab of the Privy Dashboard 2. Turn on the **Enable test accounts** toggle Once enabled, the Dashboard displays the test credentials for the app's test account. All test credentials follow the same format, where `XXXX`/`XXXXXX` in the credentials below should be substituted with the values shown on the **User management > Authentication > Advanced** page of the Dashboard. Arbitrary values cannot be substituted for `XXXX`/`XXXXXX`, and [plus addressing](https://learn.microsoft.com/en-us/exchange/recipients-in-exchange-online/plus-addressing-in-exchange-online) cannot be used; the credentials from the Dashboard must be used exactly. | email | phone | OTP *(for either)* | | -------------------- | ----------------- | ------------------ | | `test-XXXX@privy.io` | `+1 555 555 XXXX` | `XXXXXX` | After a test account is enabled, a user can log into the app with the provided email or phone number and OTP code to review and test the app. This first login creates the Privy user record. Depending on when the Privy app was created, a legacy test account may be enabled with the login credentials `test@privy.io` or `+1 555 555 5555`. See the **User management > Authentication > Advanced** page of the Privy Dashboard to determine if this is the case for the app. Test accounts have a lighter authentication rate limit for apps in development. While all accounts in production apps and non-test accounts in development apps are limited to 5 requests every 5 minutes for email and 5 requests every 10 minutes for SMS, test accounts in development apps are limited to 10 requests every 10 seconds for either. ## Testing login-only flows If the app sets `disableSignup: true` and the test account has never been used to log in, login attempts fail with a `user_does_not_exist` error and the **Account not found** modal. This is expected behavior: no Privy user record exists yet, so the login is treated the same as a real user attempting to sign in without an existing account. To test this flow, attempt login with the test credentials before completing a first login. To test a successful login, complete one login with the test credentials first to create the user record. ## Getting a test access token programmatically The app can programmatically get an access token for its test account using the `getTestAccessToken` method. `getTestAccessToken` will throw an error if: * Test accounts have not been enabled in the Privy Dashboard * Allowed origins or base domain are enabled for the app Use the `getTestAccessToken` method from the `apps()` interface to get an access token for a test account. To select a specific test account, pass an object with either an `email` or `phone_number` field. ```ts theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID!, appSecret: process.env.PRIVY_APP_SECRET! }); // Uses the first test account by default const {access_token} = await privy.apps().getTestAccessToken(); // Or, select a test account by email const {access_token: tokenByEmail} = await privy.apps().getTestAccessToken({ email: 'test-XXXX@privy.io' }); // Or, select a test account by phone number const {access_token: tokenByPhone} = await privy.apps().getTestAccessToken({ phone_number: '+1 555 555 XXXX' }); ``` The method returns a `Promise` for an object containing the `access_token` string. If no test account matches the provided parameters, the method throws an error. # Wallet infrastructure Source: https://docs.privy.io/recipes/wallet-infrastructure/overview Privy wallet infrastructure recipes cover wallet creation, key controls, policy enforcement, and account abstraction patterns for production apps. Create wallets in advance for low-latency onboarding. Manage user wallets with server-side authorization. Derive scalable wallet trees from managed roots. Apply dynamic policy checks before transaction execution. Provision programmable wallets for autonomous agents. Implement custom smart-account flows and signer controls. # Pay with WalletConnect pay Source: https://docs.privy.io/recipes/walletconnect-pay Privy embedded wallets give every user a wallet at signup, with no extensions or seed phrases required. [WalletConnect Pay](https://pay.walletconnect.com) lets merchants generate payment links that any wallet can fulfill. This recipe shows how to combine them so users can pay for anything in-app, with a single tap. Using Privy and WalletConnect Pay together, your app can: * Let users paste a WalletConnect Pay link and complete the payment with their Privy wallet * Support multiple chains (Ethereum, Base, Polygon, Arbitrum, Optimism, and more) * Handle ERC-20 token transfers (like USDC) alongside native ETH payments WalletConnect Pay offers interchange revenue for wallets and `$WCT` cashback for users. See the [WalletConnect Pay overview](https://docs.walletconnect.com/payments/wallets/overview) for details on earning revenue through your integration. *** # Setup ## 1. Install dependencies ```bash theme={"system"} npm install @privy-io/react-auth @reown/walletkit @walletconnect/core ``` ## 2. Get project IDs Your app needs two project IDs: * **Privy App ID** — from the [Privy Dashboard](https://dashboard.privy.io) * **WalletConnect Project ID** — from [WalletConnect Cloud](https://dashboard.walletconnect.com) (select your project and click **Get Started** to obtain your WCP ID). Add them to your environment: ```bash theme={"system"} VITE_PRIVY_APP_ID=your-privy-app-id VITE_WALLETCONNECT_PROJECT_ID=your-walletconnect-project-id ``` ## 3. Configure the `PrivyProvider` Wrap your app with `PrivyProvider` and enable automatic embedded wallet creation. This ensures every user gets a wallet on login. ```tsx theme={"system"} import {PrivyProvider} from '@privy-io/react-auth'; ; ``` Setting `createOnLogin` to `'users-without-wallets'` means Privy automatically provisions an Ethereum wallet the first time a user logs in. No extra steps needed. ## 4. Initialize WalletKit Create a shared module that initializes `WalletKit` for the payment link flow. ```ts {skip-check} theme={"system"} import {Core} from '@walletconnect/core'; import {WalletKit} from '@reown/walletkit'; let core: InstanceType | null = null; let walletkit: Awaited> | null = null; const projectId = process.env.VITE_WALLETCONNECT_PROJECT_ID; const metadata = { name: 'My Pay App', description: 'Pay with crypto', url: window.location.origin, icons: ['https://your-app.com/icon.png'] }; function getCore() { if (!core) { core = new Core({projectId}); } return core; } export async function getWalletKit() { if (walletkit) return walletkit; walletkit = await WalletKit.init({ core: getCore() as any, metadata, payConfig: { appId: projectId, apiKey: 'your-wc-pay-api-key' } }); return walletkit; } ``` *** # Pay with link This flow lets users paste a WalletConnect Pay link (like `https://pay.walletconnect.com/...`) and complete the payment entirely in your app. ## 1. Get the user's Privy wallet Use Privy's `useWallets` hook to access the embedded wallet: ```ts theme={"system"} import {useWallets} from '@privy-io/react-auth'; const {wallets} = useWallets(); const embeddedWallet = wallets.find((w) => w.walletClientType === 'privy'); ``` ## 2. Validate the payment link Use `isPaymentLink` from `@reown/walletkit` to verify the link before making API calls: ```ts theme={"system"} import {isPaymentLink} from '@reown/walletkit'; const uri = 'https://pay.walletconnect.com/...'; if (!isPaymentLink(uri)) { throw new Error('Not a valid WalletConnect Pay link'); } ``` ## 3. Fetch payment options Build the user's account list from supported chain prefixes and their wallet address, then call `getPaymentOptions`: ```ts theme={"system"} import {useWallets} from '@privy-io/react-auth'; declare function getWalletKit(): Promise; declare const uri: string; const SUPPORTED_CHAINS = [ 'eip155:1', // Ethereum 'eip155:8453', // Base 'eip155:137', // Polygon 'eip155:42161', // Arbitrum 'eip155:10' // Optimism ]; const {wallets} = useWallets(); const embeddedWallet = wallets.find((w) => w.walletClientType === 'privy'); const walletkit = await getWalletKit(); const address = embeddedWallet.address; const accounts = SUPPORTED_CHAINS.map((prefix) => `${prefix}:${address}`); try { const options = await walletkit.pay.getPaymentOptions({ paymentLink: uri, accounts, includePaymentInfo: true }); } catch (error) { if (error.message.includes('payment not found')) { // The payment link is invalid or has been cancelled } else if (error.message.includes('expired')) { // The payment has expired } else { throw error; } } ``` The response contains: * **`paymentId`** — unique identifier for this payment session * **`options`** — array of payment methods (different tokens and chains the user can pay with) * **`info`** — merchant name, requested amount, and expiry (`expiresAt`) Check `options.info.expiresAt` and warn users when time is running low. Payments expire after a set period, and signing after expiry results in a failed payment. ## 4. Handle data collection (if required) Some payment options require additional user information (e.g. shipping address). Check the selected option for a `collectData` object and render the provided URL in an iframe: ```ts {skip-check} theme={"system"} const collectData = selectedOption.collectData; if (collectData?.url) { // Render collectData.url in an iframe. // WalletConnect handles the form UI inside the iframe. // Listen for postMessage events from the iframe: // { type: 'IC_COMPLETE' } → data collected successfully, proceed to signing // { type: 'IC_ERROR' } → collection failed, show an error // To pre-populate known user data, append a base64-encoded JSON query param: // const prefill = btoa(JSON.stringify({ email: 'user@example.com' })); // const url = `${collectData.url}?prefill=${prefill}`; } ``` The iframe submits collected data directly to WalletConnect. Do not pass `collectedData` to `confirmPayment()` when using this flow — it is handled automatically. ## 5. Get required actions and sign Once the user selects a payment option, fetch the transaction actions and sign them with the Privy wallet. The API can return multiple actions (e.g. a token approval followed by a Permit2 signature), and different RPC methods require different parameter handling: ```ts theme={"system"} import {useWallets} from '@privy-io/react-auth'; declare function getWalletKit(): Promise; declare const options: any; declare const selectedOption: {id: string}; const {wallets} = useWallets(); const embeddedWallet = wallets.find((w) => w.walletClientType === 'privy'); const walletkit = await getWalletKit(); const actions = await walletkit.pay.getRequiredPaymentActions({ paymentId: options.paymentId, optionId: selectedOption.id }); const provider = await embeddedWallet.getEthereumProvider(); const signatures = await Promise.all( actions.map(async (action) => { const {chainId, method, params} = action.walletRpc; const parsedParams = JSON.parse(params); const numericChainId = parseInt(chainId.split(':')[1], 10); await embeddedWallet.switchChain(numericChainId); switch (method) { case 'eth_sendTransaction': return await provider.request({method, params: [parsedParams[0]]}); case 'eth_signTypedData_v4': return await provider.request({method, params: parsedParams}); case 'personal_sign': return await provider.request({method, params: parsedParams}); default: throw new Error(`Unsupported RPC method: ${method}`); } }) ); ``` The three methods above (`eth_sendTransaction`, `eth_signTypedData_v4`, `personal_sign`) are the most common, but the API can return any wallet RPC method. Add additional cases to the `switch` statement as needed for your integration. ## 6. Confirm the payment Submit all signatures to finalize the payment. The response may indicate the payment is still processing — poll until `isFinal` is `true`: ```ts {skip-check} theme={"system"} import {WalletKit} from '@reown/walletkit'; declare const walletkit: Awaited>; declare const options: {paymentId: string}; declare const selectedOption: {id: string}; declare const signatures: string[]; async function confirmAndPoll( wk: Awaited>, paymentId: string, optionId: string, sigs: string[] ) { let result = await wk.pay.confirmPayment({ paymentId, optionId, signatures: sigs }); while (!result.isFinal && result.pollInMs) { await new Promise((resolve) => setTimeout(resolve, result.pollInMs)); result = await wk.pay.confirmPayment({ paymentId, optionId, signatures: sigs }); } return result; } try { const result = await confirmAndPoll(walletkit, options.paymentId, selectedOption.id, signatures); if (result.status === 'succeeded') { // Payment confirmed } else { // Payment failed — check result.status for details // Possible statuses: 'failed', 'expired', 'cancelled' } } catch (error) { // Network error or unexpected failure } ``` Key types from the WalletConnect Pay API: ```ts {skip-check} theme={"system"} interface PaymentOptionsResponse { paymentId: string; info?: PaymentInfo; options: PaymentOption[]; collectData?: CollectDataAction; } interface PaymentOption { id: string; amount: PayAmount; etaS: number; actions: Action[]; collectData?: CollectDataAction; } interface WalletRpcAction { chainId: string; method: string; params: string; // JSON-encoded string } interface ConfirmPaymentResponse { status: 'requires_action' | 'processing' | 'succeeded' | 'failed' | 'expired' | 'cancelled'; isFinal: boolean; pollInMs?: number; info?: PaymentResultInfo; } ``` For the full API reference, see the [WalletConnect Pay SDK documentation](https://docs.walletconnect.com/payments/wallets/overview). *** # Supported chains Configure which chains your app supports. The `SUPPORTED_CHAINS` array in [step 3](#3-fetch-payment-options) determines which payment options WalletConnect Pay returns. | Chain | Chain ID | CAIP-2 prefix | USDC address | | -------- | -------- | -------------- | -------------------------------------------- | | Ethereum | 1 | `eip155:1` | `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48` | | Base | 8453 | `eip155:8453` | `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` | | Polygon | 137 | `eip155:137` | `0x3c499c542cEF5E3811e1192ce70d8cC03d5c3359` | | Arbitrum | 42161 | `eip155:42161` | `0xaf88d065e77c8cC2239327C5EDb3A432268e5831` | | Optimism | 10 | `eip155:10` | `0x0b2C639c533813f4Aa9D7837CAf62653d097Ff85` | WalletConnect Pay matches available payment options to the chains your app lists. Add more chains to `SUPPORTED_CHAINS` to offer users additional payment methods. For testnet development, use the [WalletConnect Dashboard POS tool](https://dashboard.walletconnect.com) to create test payments on supported testnets. *** # Testing Go to the [WalletConnect Dashboard](https://dashboard.walletconnect.com) and use the POS tool to create a test payment. Copy the payment link. Log in to your app with Privy so an embedded wallet is provisioned. Paste the link into your app and complete the payment flow. *** ## Learn more Official WalletConnect Pay integration guide for wallets Create test payments and manage your WCP ID # Conditional policies per signer Source: https://docs.privy.io/recipes/wallets/conditional-signer-policies Some applications need different transaction policies depending on which party authorizes a transaction. For example, a restricted signer should only be allowed to send small transfers, while a full-access signer can send transactions of any size. Privy supports this through **additional signers with override policies**. Each signer added to a wallet can have its own policy, so the constraints applied to a transaction depend on which signer authorizes it. ## How it works * Each wallet can have multiple **additional signers** (authorization keys or key quorums) * Each signer can have an **override policy** that defines what policies that signer is subject to * When a signer submits a transaction, Privy evaluates only that signer's override policy — not the policies of other signers * If the transaction satisfies the signer's policy, Privy signs it. Otherwise, Privy denies the request This gives the same wallet different levels of access depending on which signer acts, without needing separate wallets. ## Setup At a high level: Define a policy for each access level (e.g., a restrictive policy and a permissive policy). Create an authorization key (or key quorum) for each signer that needs access to the wallet. Add each signer to the wallet with its corresponding policy. Your server selects which authorization key to sign with based on the context of the request. ## 1. Create policies Define a policy for each signer. In this example, the restricted signer can only send small USDC transfers, while the full-access signer can send transactions to any address. ```ts {skip-check} theme={"system"} import {PrivyClient} from '@privy-io/node'; import {erc20Abi, parseUnits} from 'viem'; const privy = new PrivyClient({ appId: 'insert-your-app-id', appSecret: 'insert-your-app-secret' }); const USDC_ADDRESS = '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48'; // Restrictive policy: only USDC transfers under 1,000 const restrictivePolicy = await privy.policies().create({ name: 'Small USDC transfers only', version: '1.0', chain_type: 'ethereum', rules: [ { name: 'Allow small USDC transfers', method: 'eth_sendTransaction', action: 'ALLOW', conditions: [ { field_source: 'ethereum_transaction', field: 'to', operator: 'eq', value: USDC_ADDRESS }, { field_source: 'ethereum_calldata', field: 'transfer.amount', abi: erc20Abi, operator: 'lte', value: parseUnits('1000', 6).toString() } ] } ] }); // Permissive policy: allow any eth_sendTransaction const permissivePolicy = await privy.policies().create({ name: 'Allow all transactions', version: '1.0', chain_type: 'ethereum', rules: [ { name: 'Allow all sends', method: 'eth_sendTransaction', action: 'ALLOW', conditions: [] } ] }); ``` Make `POST` requests to: ```sh theme={"system"} https://api.privy.io/v1/policies ``` **Restrictive policy** (small USDC transfers only): ```json theme={"system"} { "name": "Small USDC transfers only", "version": "1.0", "chain_type": "ethereum", "rules": [ { "name": "Allow small USDC transfers", "method": "eth_sendTransaction", "action": "ALLOW", "conditions": [ { "field_source": "ethereum_transaction", "field": "to", "operator": "eq", "value": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48" }, { "field_source": "ethereum_calldata", "field": "transfer.amount", "abi": "", "operator": "lte", "value": "1000000000" } ] } ] } ``` **Permissive policy** (allow all transactions): ```json theme={"system"} { "name": "Allow all transactions", "version": "1.0", "chain_type": "ethereum", "rules": [ { "name": "Allow all sends", "method": "eth_sendTransaction", "action": "ALLOW", "conditions": [] } ] } ``` Save the `id` from each policy response. These are needed when adding signers to the wallet. Learn more about defining policies with rules and conditions. ## 2. Create authorization keys Create a separate authorization key for each signer. Each key corresponds to a different party or service that needs wallet access. Generate keypairs and register them in the Privy Dashboard or via the SDK. Store each private key securely (e.g., in a secrets manager). In the examples below, these are referenced as `restrictedSignerPrivateKey` and `fullAccessSignerPrivateKey`. ## 3. Add signers with override policies Add each authorization key as a signer on the wallet, attaching the appropriate override policy. The override policy scopes what that specific signer can authorize. ```tsx theme={"system"} import {useSigners} from '@privy-io/react-auth'; const {addSigners} = useSigners(); await addSigners({ address: walletAddress, signers: [ { signerId: '', policyIds: [''] }, { signerId: '', policyIds: [''] } ] }); ``` Update the wallet with the desired `additional_signers`. The wallet owner must [sign](/controls/authorization-keys/using-owners/sign) the request. ```ts theme={"system"} export {}; declare const privy: any; const walletId = 'insert-wallet-id'; const wallet = await privy.wallets().update(walletId, { additional_signers: [ { signer_id: '', override_policy_ids: [''] }, { signer_id: '', override_policy_ids: [''] } ] }); ``` Make a `PATCH` request to: ```sh theme={"system"} https://api.privy.io/v1/wallets/ ``` with the body: ```json theme={"system"} { "additional_signers": [ { "signer_id": "", "override_policy_ids": [""] }, { "signer_id": "", "override_policy_ids": [""] } ] } ``` ## 4. Route transactions through the appropriate signer Your server selects which authorization key to use based on the request context. For example, an automated service signs with the restricted signer, while an admin endpoint signs with the full-access signer. ```ts theme={"system"} export {}; declare const privy: any; const walletId = 'insert-wallet-id'; const botTransaction = {}; const adminTransaction = {}; const restrictedSignerPrivateKey = 'insert-restricted-signer-private-key'; const fullAccessSignerPrivateKey = 'insert-full-access-signer-private-key'; async function sendTransaction(walletId: string, transaction: object, signerPrivateKey: string) { const result = await privy .wallets() .ethereum() .sendTransaction(walletId, { caip2: 'eip155:1', params: {transaction}, authorization_context: { authorization_private_keys: [signerPrivateKey] } }); return result; } // Automated bot uses the restricted signer await sendTransaction(walletId, botTransaction, restrictedSignerPrivateKey); // Admin uses the full-access signer await sendTransaction(walletId, adminTransaction, fullAccessSignerPrivateKey); ``` Include the appropriate signer's authorization signature in the `privy-authorization-signature` header. The enclave evaluates the override policy attached to whichever signer produced the signature. ```bash theme={"system"} # Restricted signer request (restrictive policy applies) curl --request POST https://api.privy.io/v1/wallets//rpc \ -u ":" \ -H "privy-app-id: " \ -H "privy-authorization-signature: " \ -H "Content-Type: application/json" \ -d '{ "caip2": "eip155:1", "method": "eth_sendTransaction", "chain_type": "ethereum", "params": { "transaction": { "to": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "data": "0xa9059cbb..." } } }' ``` If the transaction violates the signer's override policy, Privy denies the request. Transactions do not fall back to another signer's policy — Privy evaluates each signer independently. ## Learn more Add signers to wallets and manage their permissions. Define rules that constrain which transactions are allowed. Create and manage server-controlled authorization keys. # Execution wallets Source: https://docs.privy.io/recipes/wallets/execution-wallets Privy enables teams to add a programmable execution layer on top of existing treasury infrastructure, without moving funds out of custody. Execution wallets are Privy wallets upgraded via [EIP-7702](/recipes/react/eip-7702) to operate as smart accounts. Each wallet sits alongside external wallet providers, interacting with onchain protocols on behalf of the treasury. EIP-7702 enables batched transactions, so each execution wallet bundles multiple calls into a single transaction. A fleet of execution wallets operates in parallel — each with its own nonce — eliminating the sequential bottleneck that limits throughput in single-wallet custody setups. Execution wallets do not need to hold a working balance. The EIP-7702 smart account layer handles gas sponsorship, so treasury funds stay in custody throughout. Execution wallets work alongside existing external wallet setups. ## Architecture Execution wallets architecture diagram Execution wallets operate as a delegation layer between treasury custody and onchain protocols: * **Treasury funds remain in custody** (e.g., Bridge or another provider) * **Execution wallets are EIP-7702 smart accounts** authorized to act on approved funds under policy constraints * **Batched transactions** allow each wallet to execute multiple protocol interactions atomically in a single transaction * **A fleet of execution wallets can run in parallel** across different strategies, with no shared nonce or ordering constraint across wallets * **No working balance required** — gas is sponsored via the EIP-7702 smart account layer ## Setup Your backend controls execution wallets via authorization keys. These keys sign requests to Privy's API on behalf of each wallet, enabling programmatic execution without manual approvals. Create [authorization keys](/controls/authorization-keys/owners/types#authorization-keys) in the Privy Dashboard and securely store the corresponding private keys. For higher-value wallets, register the authorization keys in a [key quorum](/controls/authorization-keys/owners/types#key-quorums). This requires multiple parties to sign before a wallet action is executed. Create authorization keys in your Privy Dashboard. Set up a key quorum for multi-party governance. Policies constrain what each execution wallet can do. Well-scoped policies ensure that execution wallets can only interact with approved protocols and move funds within defined limits. Common policy configurations for execution wallets include: * **Allowlisted contracts**: Restrict the wallet to interact only with specific onchain protocols (e.g., a particular lending pool or liquidity venue) * **Transfer limits**: Cap the amount that can be moved per transaction or within a time window * **Recipient restrictions**: Limit where funds can be sent, such as only back to the treasury address * **Chain restrictions**: Restrict execution to specific networks Follow the guide below to create a policy. Save the `id` after creation to assign it to the wallets you provision later. Learn how to construct policies with Privy's policy language. Create a policy for your execution wallets. Create one or more Privy wallets, each owned by the authorization key (or key quorum) and subject to the policies you defined. Provisioning multiple wallets allows your application to distribute transactions across the fleet and execute in parallel. Each wallet is an independent EOA with its own nonce, so there is no ordering constraint between wallets. When creating each wallet: * Set `owner_id` to the `id` of the authorization key or key quorum * Set `policy_ids` to include the `id` of the policy you created Provision wallets for your execution fleet. Reuse the same policy ID across the fleet so every wallet is subject to the same constraints. To apply different permissions to different wallets — for example, separate wallets for separate protocols — create distinct policies for each. Upgrade each execution wallet to a smart account using EIP-7702. This gives each wallet the ability to send batched transactions and enables gas sponsorship, so the wallets do not need to hold a native token balance for gas. Use Privy's `sign7702Authorization` method to generate the delegation authorization for each wallet, then submit the upgrade transaction via your preferred account abstraction provider. Learn how to upgrade EOAs with EIP-7702. Sign an EIP-7702 authorization with a Privy wallet. Each upgraded execution wallet can now send multiple protocol interactions atomically in a single batched transaction. Distribute transactions across the fleet to parallelize execution — for example, separate wallets can interact with different protocols simultaneously, or multiple wallets can execute the same strategy concurrently. Execute transactions on Solana. Follow the batch transactions guide for the transaction submission pattern. Privy provides webhooks for transaction lifecycle events and balance changes. Use these to observe execution activity, trigger downstream workflows, and reconcile with your treasury system. Monitor transaction status and completion events. Track deposits and withdrawals across the fleet. ## Learn more Set up an organization wallet with key quorums and policies. Learn how EIP-7702 enables batch transactions and gas sponsorship. Configure fine-grained execution controls. Add human review for high-value or sensitive transactions. # Organization wallets Source: https://docs.privy.io/recipes/wallets/organization-wallets Create and manage wallets for businesses and teams with Privy. Use an organization wallet when multiple people need to manage funds, share wallet access, or approve financial operations together. This recipe creates an organization, assigns a wallet to it, and retrieves the organization's wallets. Before you begin, [create Privy users](/organizations/setup/users) for the organization members and [create a default key quorum](/organizations/setup/organizations#create-default-key-quorum). Save the default key quorum ID for the steps below. ## Create an organization [Create an organization](/api-reference/organizations/create) with a display name and the default key quorum ID: ```ts @privy-io/node {skip-check} theme={"system"} const organization = await privy.organizations().create({ display_name: 'Acme Corporation', default_key_quorum_id: 'rkiz0ivz254drv1xw982v3jq' }); ``` You can retrieve the organization later by its ID: ```ts @privy-io/node {skip-check} theme={"system"} const organization = await privy.organizations().get('cm7zx4k9a0000l308abcd1234'); ``` ## Create an organization wallet When [creating a wallet](/api-reference/wallets/create), assign its `entity` to the organization: ```ts @privy-io/node {skip-check} theme={"system"} const wallet = await privy.wallets().create({ chain_type: 'ethereum', entity: { id: organization.id, type: 'organization' } }); ``` When you omit `owner` and `owner_id`, Privy automatically sets the organization's `default_key_quorum_id` as the wallet owner. You can override this default for a specific wallet by passing either field when creating it. A wallet's entity cannot be changed after it is set. You can assign up to 150 wallets to an organization. ## List the organization's wallets [Filter wallets](/api-reference/wallets/get-all) by the organization ID: ```ts @privy-io/node {skip-check} theme={"system"} for await (const wallet of privy.wallets().list({ entity_id: organization.id })) { console.log(wallet); } ``` ## Assign an existing wallet If a wallet does not already have an entity, you can [assign it to the organization](/api-reference/wallets/entity): ```ts @privy-io/node {skip-check} theme={"system"} await privy.wallets().assignEntity('insert-wallet-id', { id: organization.id, type: 'organization' }); ``` Assigning an organization after wallet creation does not change the wallet's owner. Update the owner separately if the organization's default key quorum should administer the wallet. ## Next steps * [Configure policies](/controls/policies/overview) to restrict wallet actions. * [Provision scoped access](/organizations/setup/signers) to additional organization members. * [Use intents](/organizations/actions/intents) to collect asynchronous approvals. # Server-side user wallets Source: https://docs.privy.io/recipes/wallets/server-side-user-wallets Create and control user wallets from your backend using the Privy Node.js SDK (PrivyClient) for server-to-server wallet operations Privy enables creating **non-custodial** wallets for your users that can be used from your servers. Privy enforces self-custody of the wallet by requiring a valid access token from the user for any wallet actions, ensuring the user is authenticated in your app. This guarantees that the **user must be in the loop** for any transaction invoked by your server. At a high-level, you can create non-custodial wallets that can be used from your servers by: Configure the authentication settings from your existing authentication provider in the Privy Dashboard. Privy will use these settings to verify a user's access token. [Create a user](/user-management/migrating-users-to-privy/create-or-import-a-user) user in Privy using the user ID from your authentication provider. This user will be assigned as the owner of their wallet. Create a wallet [owned](/controls/authorization-keys/owners/overview) by your user using their Privy user ID. When creating the wallet, you can optionally attach [policies](/controls/policies/overview) to the wallet to configure which kinds of transactions can be sent by your user. While your user is authenticated in your app, use your user's access token to request an ephemeral [user key](/controls/authorization-keys/keys/create/user/overview) for your user. This key is required to sign requests to execute transactions, ensuring the user stays in the loop for all transactions. Compute the user key's signature over your API request and execute transactions from your server. Follow the guide below for more concrete instructions. ## 1. Configure authentication settings Privy ensures that users are in the loop for all wallet actions by requiring a valid **access token** for your user issued by your authentication provider. This ensures your user is authenticated for all transactions executed by your server. To verify a user's access token, Privy requires that your app register details of your authentication setup in the Privy Dashboard. Namely: 1. Get your **JWKS.json** endpoint from your authentication provider (e.g. Auth0, Firebase, Stytch). Privy will use this endpoint to verify access tokens for your users. 2. In the **Authentication** page of the **Configuration** section of the Privy Dashboard, enable **JWT-based authentication**. 3. Once JWT-based authentication has been enabled: 1. Determine whether your app will be authenticating requests that contain your provider's JWTs from a **server side or client side environment**. 2. Register the **JWKS.json** endpoint from your authentication provider and the name of the **JWT claim** that specifies the user's ID (typically `sub`). Privy can now verify access tokens issued by your authentication provider to authenticate users, and issue user keys for users. ## 2. Create your user Next, [create a user](/user-management/migrating-users-to-privy/create-or-import-a-user) in Privy that will own your wallet. Pass the user ID from your authentication provider in the request to associate the user in Privy with the user in your authentication provider. Use the Privy client's `create` method on the `users()` interface to create a user. ```ts theme={"system"} export {}; declare const privy: any; const user = await privy.users().create({ linked_accounts: [ { type: 'custom_auth', custom_user_id: 'insert-user-id-from-authentication-provider' } ] }); // Save the Privy user ID const id = user.id; ``` Make sure to save the `id` of the returned Privy user for the next step. Make a `POST` request to: ```sh theme={"system"} https://auth.privy.io/api/v1/users ``` with the body: ```json theme={"system"} { "linked_accounts": [ { "type": "custom_auth", "custom_user_id": "insert-user-id-from-authentication-provider" } ] } ``` Below is a **sample cURL command** for importing a new user into Privy: ```bash theme={"system"} $ curl --request POST https://auth.privy.io/api/v1/users \ -u ":" \ -H "privy-app-id: " \ -H 'Content-Type: application/json' \ -d '{ "linked_accounts": [{ "type": "custom_auth", "custom_user_id": "insert-user-id-from-authentication-provider" }] }' ``` Make sure to save the `id` field returned in the response body for the next step ## 3. Create a wallet owned by your user Next, given your Privy user ID from step 3, create a wallet [owned](/controls/authorization-keys/owners/overview) by your user. This ensures that the user is the only party that is allowed to authorize transactions from the wallet. When creating a wallet, you can also associate [policies](/controls/policies/overview) with the wallet to configure which kinds of transactions are allowed to be sent. Use the Privy client's `create` method on the `wallets()` interface to create a wallet. ```ts theme={"system"} export {}; declare const privy: any; const {id, address} = await privy.wallets().create({ chain_type: 'ethereum', owner: {user_id: 'insert-privy-user-id'}, policy_ids: ['insert-any-policy-ids-to-associate-with-wallet'] }); ``` Make sure to save the `id` of the returned wallet, to allow your server to transact with this wallet in the next step. To create a new wallet for a user, make a `POST` request to: ```sh theme={"system"} https://api.privy.io/v1/wallets ``` with the body: ```json theme={"system"} { "owner": [ { "user_id": "insert-privy-user-id" } ], "chain_type": "specify-'ethereum'-or-'solana'" } ``` Below is a **sample cURL command** for creating a wallet owned by your user. ```bash theme={"system"} curl --request POST https://api.privy.io/v1/wallets \ -u ":" \ -H "privy-app-id: " \ -H 'Content-Type: application/json' \ -d '{ "owner": { "user_id": "did:privy:xxxxxx" }, "chain_type": "ethereum" }' ``` Make sure to save the `id` of the wallet in the response body, to execute transactions with the wallet in your next step. ## 4. Request a user key When your user is authenticated in your application and wants to take action with their wallet, first make a request from your frontend to your server with the user's access token. You will use this access token to request an ephemeral [user key](/controls/authorization-keys/keys/create/user/overview). This key is required to sign requests to the Privy API to ensure that users authorize transactions that are being sent. Next, once your server has the user's access token, make a request to Privy to get the user key. The NodeJS SDK will automatically handle requesting the user key when required whenever you set a `user_jwt` on the [authorization context](/controls/authorization-keys/using-owners/sign/signing-on-the-server) object. ```ts theme={"system"} type AuthorizationContext = {user_jwts: string[]}; const authorizationContext: AuthorizationContext = { user_jwts: ['insert-user-jwt'] }; ``` Make a `POST` request to: ```sh theme={"system"} https://api.privy.io/v1/wallets/authenticate ``` with the body: ```json theme={"system"} { "user_jwt": "insert-user-jwt-from-authentication-provider" } ``` Below is a sample cURL command for requesting a user key. ```bash theme={"system"} curl -X POST "https://api.privy.io/v1/wallets/authenticate" \ -H "Authorization: Basic " \ -H "Content-Type: application/json" \ -H "privy-app-id: " \ -d '{ "user_jwt": , }' ``` Save the `authorization_key` returned in the response body to be used in the next step. In production environments, we strongly recommend using asymmetric encryption when requesting user keys from Privy's API. View [this guide](/controls/authorization-keys/keys/create/user/request) to learn more. ## 5. Execute transactions from your server Lastly, execute transactions from your server with the user key. All requests to the Privy API to execute a transaction must be signed by the user key to ensure the user authorizes the transaction. Follow the steps below to sign a request and execute a transaction with the REST API. You can also enable [key export of a user's wallet](/wallets/wallets/export) with a valid access token from the user. Provided you've set the `user_jwt` on the [authorization context](/controls/authorization-keys/using-owners/sign/signing-on-the-server) object as shown in step 4, the Privy client will automatically sign requests to the Privy API with user's key. You can simply use the [`privy.wallets().ethereum()`](/wallets/using-wallets/ethereum/send-a-transaction) and [`privy.wallets().solana()`](/wallets/using-wallets/solana/send-a-transaction) interfaces to take actions with wallets, and the SDK will automatically sign requests under the hood. Follow [this guide](/controls/authorization-keys/using-owners/sign) to learn how to sign requests to the Privy API. Make sure to include the signature as a `privy-authorization-signature` header on all transaction requests. Then, follow the respective guides for executing transactions on [Ethereum](/wallets/using-wallets/ethereum/send-a-transaction) and [Solana](/wallets/using-wallets/solana/send-a-transaction). # Using signers to execute limit orders with wallets Source: https://docs.privy.io/recipes/wallets/session-signer-use-cases/limit-orders Signers allow your app to execute limit orders or other transactions while a user is offline. You can also configure signers such that you can only execute certain orders restricted by specific [policies](/controls/policies/overview) that you define. At a high-level, you can use signers to execute limit orders. Follow the "adding signers quickstart" to first request access to a user's wallet. Store the private key(s) associated with your signer ID securely in your server. Your Telegram bot or agent will need this to execute transaction requests. Request access to user wallets with signers. Next, use Privy's NodeJS SDK or REST API to execute the limit order from your server. When making the request to Privy's API, you must sign the request with the private key(s) associated with your signer ID. Follow the guides below to learn how to sign requests and execute transactions on EVM and Solana. Sign requests to the Privy API with your signer. Take actions on EVM chains with Privy's NodeJS SDK or REST API. Take actions on Solana with Privy's NodeJS SDK or REST API. # Enabling server-side access to user wallets Source: https://docs.privy.io/recipes/wallets/session-signer-use-cases/server-side-access Signers allow your app to request server-side access to user wallets. This enables your app to execute transactions from user wallets from your servers directly, giving you more control and the ability to execute transactions even when the user is offline (e.g., for [limit orders](/recipes/wallets/session-signer-use-cases/limit-orders) or [Telegram bot trading](/recipes/wallets/session-signer-use-cases/telegram-bot)). You can also configure signers to have specific permissions via [policies](/controls/policies/overview), such that you can request server-side access for only certain transaction types. At a high-level, you can use signers to request server-side access to user wallets. Follow the "adding a signer quickstart" to first request access to a user's wallet. Store the private key(s) associated with your signer ID securely in your server. Request access to user wallets with signers. Next, execute actions with your signer by signing requests with the private key(s) of your signer ID. Follow the guides below to learn how to sign requests and execute actions with wallets. Sign requests to the Privy API with your signer. Take actions on EVM chains with Privy's NodeJS SDK or REST API. Take actions on Solana with Privy's NodeJS SDK or REST API. # Treasury Source: https://docs.privy.io/recipes/wallets/treasury-overview Privy's treasury system lets you manage the digital assets your business uses to facilitate core operations, from payment orchestration to funds disbursement, to balance sheet capital and more. * **Automated operations with human escalation**: programmatic execution for routine transactions, with key quorum sign-off for sensitive actions * **Multi-party treasury management**: distributed control with m-of-n authorization and policy constraints * **High-throughput protocol execution**: parallel wallet fleets for fast onchain execution, including wallets not hosted within Privy Designed for treasury and operations teams, this enables you to codify authorization rules and execution constraints into your wallet infrastructure so funds only move within the boundaries you define. Privy handles key management, policy evaluation, and transaction delivery powering Privy's 120M+ user wallets so you can focus on your treasury operations rather than wallet infrastructure. ## Get started ### Automated operations with human escalation Wallets can be configured with two approval paths: * **Programmatic execution** — a server-controlled key handles routine transactions (payroll, vendor payments, liquidity rebalancing) under controlled policies * **Manual approval** — a key quorum owns the wallet for sensitive operations, requiring human sign-off for high-value transfers or unusual recipients Configure dual approval paths. ### Multi-party treasury management Corporate treasuries, trading desks, and payment orchestration teams use key quorums to enforce distributed control over onchain funds. A quorum of authorized signers must provide m-of-n signatures for every transaction. Policies layer additional constraints on top, such as transfer amount caps and recipient restrictions. Every action passes through both authorization and policy checks before Privy processes it. No single party can move funds unilaterally. Set up organization wallets. ### High-throughput protocol execution Single-wallet architectures create nonce bottlenecks when interacting with DeFi protocols, bridges, or settlement contracts at scale. Privy enables you to provision a fleet of wallets, upgrade them to smart accounts via EIP-7702, and distribute transactions across the fleet in parallel. This includes enabling fast execution on wallets not hosted within Privy (e.g., Fireblocks). Each wallet handles batched multi-call transactions atomically, with policies constraining what contracts and operations it can touch. Provision a wallet fleet. ## Features Configurable key quorums. Enforce transfer limits, contract allowlists, and more. Routine operations run automatically while sensitive actions require human sign-off, all on a single wallet. Wallet fleets with independent nonces eliminate sequential bottlenecks. EIP-7702 smart account upgrades bundle multiple contract calls into a single atomic operation. Execution wallets operate without maintaining working balances. Fund and withdraw from treasury via Bridge. # 2-of-2 quorum: user and server as co-signers Source: https://docs.privy.io/recipes/wallets/two-of-two-server-in-the-loop Many apps want to give users non-custodial wallets while ensuring the server must approve every transaction, wallet update, and key export. A **2-of-2 key quorum** achieves this: one quorum member is the user, the other is an authorization key controlled by your server. Both must sign every request to Privy's API. This means that even if a user's account is compromised, an attacker cannot take unilateral action with the wallet. Equally, your server alone cannot move funds without the user's consent. At a high-level, you will: Generate a P-256 keypair and register the public key with Privy. Your server holds the private key and uses it to co-sign every request. Register a key quorum that contains the user ID and the server authorization key, with an `authorization_threshold` of 2. Create a wallet whose owner is the key quorum. All subsequent actions on this wallet require both signatures. For each request, collect the user's JWT, obtain a user signing key, sign with the server authorization key, and send both signatures to the Privy API. *** ## 1. Create a server authorization key Your server needs a P-256 keypair. The private key stays on your server; the public key is registered with Privy so it can verify your server's signatures. Generate a keypair and register it in the Privy Dashboard or via SDK. Save the private key securely (e.g. in an environment variable or secrets manager). You will reference it as `serverAuthorizationPrivateKey` in later steps. *** ## 2. Create a 2-of-2 key quorum Once you have the server authorization key's public key and the user's Privy user ID, register a key quorum that requires both to sign. Key quorums containing both user IDs and authorization keys must be created via the SDK or REST API. The Dashboard only supports pure authorization-key quorums. ```ts theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: 'insert-your-app-id', appSecret: 'insert-your-app-secret' }); const userId = 'insert-privy-user-id'; const keyQuorum = await privy.keyQuorums().create({ display_name: `2-of-2 quorum for user ${userId}`, public_keys: ['insert-server-authorization-public-key'], user_ids: ['insert-privy-user-id'], authorization_threshold: 2 }); const keyQuorumId = keyQuorum.id; ``` Save the returned `id` — this is the `owner_id` you will assign to the wallet. Make a `POST` request to: ```sh theme={"system"} https://api.privy.io/v1/key_quorums ``` with the body: ```json theme={"system"} { "display_name": "2-of-2 quorum for user ", "public_keys": [""], "user_ids": [""], "authorization_threshold": 2 } ``` Sample cURL command: ```bash theme={"system"} curl --request POST https://api.privy.io/v1/key_quorums \ -u ":" \ -H "privy-app-id: " \ -H "Content-Type: application/json" \ -d '{ "display_name": "2-of-2 quorum for user did:privy:xxxxxx", "public_keys": [""], "user_ids": ["did:privy:xxxxxx"], "authorization_threshold": 2 }' ``` Save the `id` field from the response body. *** ## 3. Create a wallet owned by the quorum Create a wallet and set its `owner_id` to the key quorum ID from step 2. ```ts theme={"system"} export {}; declare const privy: any; const keyQuorumId = 'insert-key-quorum-id'; const {id: walletId, address} = await privy.wallets().create({ chain_type: 'ethereum', owner_id: keyQuorumId }); ``` Make a `POST` request to: ```sh theme={"system"} https://api.privy.io/v1/wallets ``` with the body: ```json theme={"system"} { "chain_type": "ethereum", "owner_id": "" } ``` Save the `id` of the wallet returned in the response. Attach [policies](/controls/policies/overview) to the wallet when creating it to further restrict which transactions are allowed, independent of the co-signing requirement. *** ## 4. Execute transactions with both signatures Every request to the Privy API that acts on this wallet must include signatures from both the user and the server. The flow below applies to transactions, wallet updates, and key export. ### How it works ``` Client Your server Privy API | | | |-- (1) Build request payload | | |-- (2) Sign with user key | | | (useAuthorizationSignature) | | | | | |-- (3) Send payload + user sig -->| | | |-- (4) Sign with server key | | |-- (5) Send request + | | | both sigs ------------->| | |<-- response ------------------| ``` ### Step-by-step Construct the JSON payload describing the request your app intends to make to the Privy API. This payload includes the target URL, HTTP method, required headers, and request body. ```tsx theme={"system"} const requestPayload = { version: 1, url: `https://api.privy.io/v1/wallets/${walletId}/rpc`, method: 'POST', headers: { 'privy-app-id': 'insert-your-app-id' }, body: { caip2: 'eip155:1', method: 'eth_sendTransaction', chain_type: 'ethereum', params: { transaction: { to: '0xRecipientAddress', value: '0x2386f26fc10000', data: '0x' } } } } as const; ``` ```tsx theme={"system"} const requestPayload = { version: 1, url: `https://api.privy.io/v1/wallets/${walletId}/rpc`, method: 'POST', headers: { 'privy-app-id': 'insert-your-app-id' }, body: { caip2: 'eip155:1', method: 'eth_sendTransaction', chain_type: 'ethereum', params: { transaction: { to: '0xRecipientAddress', value: '0x2386f26fc10000', data: '0x' } } } } as const; ``` Use the `useAuthorizationSignature` hook to sign the payload with the authenticated user's signing key. The hook handles key retrieval and signing entirely on the client — the user's private key never leaves the device. ```tsx theme={"system"} import {useAuthorizationSignature} from '@privy-io/react-auth'; const {generateAuthorizationSignature} = useAuthorizationSignature(); const {signature: userSignature} = await generateAuthorizationSignature(requestPayload); ``` ```tsx theme={"system"} import {useAuthorizationSignature} from '@privy-io/expo'; const {generateAuthorizationSignature} = useAuthorizationSignature(); const {signature: userSignature} = await generateAuthorizationSignature(requestPayload); ``` Forward both the request payload and the user's signature to your server. Your server will add its own signature before proxying the request to the Privy API. ```tsx theme={"system"} await fetch('https://your-server.com/api/wallet-action', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ requestPayload, userSignature }) }); ``` On your server, generate the server's authorization signature over the same payload, then send the request to the Privy API with both signatures as a comma-delimited value in the `privy-authorization-signature` header. ```ts theme={"system"} import {generateAuthorizationSignature, type WalletApiRequestSignatureInput} from '@privy-io/node'; // `requestPayload` and `userSignature` are received from the client const [serverSignature] = await generateAuthorizationSignature(privyClient, { input: requestPayload as WalletApiRequestSignatureInput, authorizationContext: { authorization_private_keys: ['insert-server-authorization-private-key'], } }); const response = await fetch(requestPayload.url, { method: requestPayload.method, headers: { ...requestPayload.headers, Authorization: `Basic ${Buffer.from('app-id:app-secret').toString('base64')}`, 'Content-Type': 'application/json', 'privy-authorization-signature': `${userSignature},${serverSignature}` }, body: JSON.stringify(requestPayload.body) }); ``` Generate a P-256 signature over the formatted payload using your server's authorization private key. Follow [this guide](/controls/authorization-keys/using-owners/sign/utility-functions) for formatting and signing details. Then send the request with both signatures: ```bash theme={"system"} curl --request POST https://api.privy.io/v1/wallets//rpc \ -u ":" \ -H "privy-app-id: " \ -H "privy-authorization-signature: ," \ -H "Content-Type: application/json" \ -d '{ "caip2": "eip155:1", "method": "eth_sendTransaction", "chain_type": "ethereum", "params": { "transaction": { "to": "0xRecipientAddress", "value": "0x2386f26fc10000", "data": "0x" } } }' ``` Privy validates that both signatures are present, valid, and correspond to members of the wallet's key quorum. If either signature is missing or invalid, the request is rejected. *** ## Learn more Learn how key quorums define ownership and authorization thresholds. Create and manage server-controlled authorization keys. Restrict which transactions are allowed with wallet policies. # Enabling users or servers to execute transactions Source: https://docs.privy.io/recipes/wallets/user-and-server-signers A common setup for Privy apps is to configure wallets such that both users and apps themselves can execute transactions from user wallets. This serves a variety of use cases: * Allowing apps to execute limit orders on behalf of a user, even when a user is offline * Allowing apps to rebalance user portfolios based on market data, even when the user is offline * Creating Telegram trading bots or other agents controlled by your app's server that can execute transactions on behalf of users You can accomplish these use cases via [signers](/wallets/using-wallets/signers/overview), which enable user to grant specific permissions to your app to transact on their behalf. Follow the guide below to learn how to integrate signers for your use case. View an [implementation of session signers](https://github.com/privy-io/examples/blob/main/privy-next-starter/src/components/sections/session-signers.tsx) in Privy's NextJS starter repo to learn about how to use signers end-to-end. ## 0. Prerequisites Prior to following this guide, follow the quickstart for Privy's [React SDK](/basics/react/quickstart) or [React Native SDK](/basics/react-native/quickstart) to get your app instrumented with Privy's basic functionality. If you plan to use this setup as part of a Telegram trading bot, check out the guide to integrate [Telegram seamless login](/authentication/user-authentication/login-methods/oauth) with the React SDK for a smoother user experience when signing into Telegram mini-apps. ## 1. Create an app authorization key To allow your app to be send transactions from user wallets, you must first create an **app authorization key**. Your app's server will sign API requests with this key to authorize sending transactions from user wallets. Create an authorization key locally on your machine like so: ```sh theme={"system"} openssl ecparam -name prime256v1 -genkey -noout -out private.pem && \ openssl ec -in private.pem -pubout -out public.pem ``` Retrieve the public key from the `public.pem` file and the private key from the `private.pem` file in your working directory. **Make sure to save both files securely.** Privy does not store your private key and cannot help you recover it. ## 2. Register the app authorization key in a key quorum Next, register the public key you created with Privy so that Privy can appropriately verify signed requests from your app. To do so, visit the [**Authorization keys**](https://dashboard.privy.io/apps?authorization-keys) page of the Privy Dashboard and click the **New key** button in the top right. Then, click the **Register key quorum instead** option. In the modal that pops up, enter the public key you generated in step 1 in the **Public keys** field. Set the **Authorization threshold** to 1, to allow that single key to sign on behalf of the key quorum, and set the **Quorum name** to a human readable name of your choice. Save the `id` of the key quorum that is created. You will need this value later. This creates a 1-of-1 [key quorum](/controls/quorum-approvals/overview) that can be granted permission to execute actions from a user's wallet. You can also register the public key with Privy programmatically via the [REST API](/api-reference/key-quorums/create). ## 3. Configure your Privy app to create embedded wallets on login Next, configure your Privy app to automatically create embedded wallets when users login. This ensures that all users have an embedded wallet, regardless of whether they login via a web app, a Telegram mini app, or a native mobile app. In your `PrivyProvider` component, set the `config.embeddedWallets.ethereum.createOnLogin` property to `'all-users'` to automatically create embedded wallets for users, regardless of what login method they use. ```tsx theme={"system"} {children} ``` In your `PrivyProvider` component, set the `config.embeddedWallets.ethereum.createOnLogin` property to `'all-users'` to automatically create embedded wallets for users, regardless of what login method they use. ```tsx theme={"system"} {children} ``` ## 4. (Optional) Create a policy for your signer If you'd like your signer to only have specific permissions on users' wallets, [create a policy](/controls/policies/overview) for your signer based on the transaction it needs to execute. For example, you might create a policy that [expires the signer's permissions after a certain date](/controls/policies/example-policies/timebound), or limits only allows transacting under a certain amount with a specific contract, in order to execute a limit order when a user is offline. You can also create multiple policies to allow your signer to execute a set of actions. Once you've created your desired policy for the signer, make sure to save the policy ID. You will need this when adding your signer to users' wallets. See example policies for [Ethereum](/controls/policies/example-policies/ethereum) and [Solana](/controls/policies/example-policies/solana) that you can modify for your use case. ## 5. Add a signer to the user's wallet Once a user logs in, an embedded wallet will automatically be created for them. Once a user has an embedded wallet, add the key quorum you created in step 3 as a [signer](/wallets/using-wallets/signers/overview) to the user's wallet. This allows your app to sign transaction requests from the user's wallet via your app's authorization key. Once a user logs in, you can use the [`addSigners`](/wallets/using-wallets/signers/add-signers) method of `useSigners` hook to add your app's authorization key as a signer on the wallet. ```tsx theme={"system"} import {useSigners} from '@privy-io/react-auth'; ... const {addSigners} = useSigners(); // Call this method after a user logs in and has an embedded wallet await addSigners({ address: 'insert-user-embedded-wallet-address', signers: [{ signerId: 'insert-key-quorum-id-from-step-2', // Replace the `policyIds` array with an array of valid policy IDs if you'd like the signer to only be able to execute certain transaction requests allowed by a policy. If you'd like the signer to have full permission, pass an empty array ([]). policyIds: ['insert-policy-id-1', 'insert-policy-id-2'] }] }) ``` If you'd like to immediately add your signer to a user's wallet when they login, use the `onComplete` callback of the `useLogin` hook: ```tsx theme={"system"} import {useLogin} from '@privy-io/react-auth'; const {login} = useLogin({ onComplete: () => { console.log( "Execute any logic you'd like to run after a user logs in, such as adding a signer" ); } }); ``` All together, you can add a signer after a user logs in like so: ```tsx theme={"system"} import {useLogin} from '@privy-io/react-auth'; import {useSigners} from '@privy-io/react-auth'; ... const {addSigners} = useSigners(); const {login} = useLogin({ onComplete: (user, isNewUser) => { if (isNewUser) { await addSigners({ address: user.wallet.address, signers: [{ signerId: 'insert-key-quorum-id-from-step-2', // Replace the empty `policyIds` array with an array of valid policy IDs if you'd like the signer to only be able to execute certain transaction requests allowed by a policy policyIds: [] }] }); } } }) // Call login somewhere in your app ``` If your app offers limit orders to users, we recommend the following flow for using signers to execute limit orders. Create a policy that allows your signer to execute the limit order from the user's wallet. Add your signer to the user's wallet via the `addSessionSigner` method, using the policy from step 1. When the conditions to execute the limit order are met, see step 6 of this guide to learn how to send transactions with your signer. Once a user logs in, you can use the [`addSigners`](/wallets/using-wallets/signers/add-signers#react-native) method of `useSigners` hook to add your app's authorization key as a signer on the wallet. ```tsx theme={"system"} import {useSigners} from '@privy-io/expo'; ... const {addSigners} = useSigners(); // Call this method after a user logs in and has an embedded wallet await addSigners({ address: 'insert-user-embedded-wallet-address', signers: [{ signerId: 'insert-key-quorum-id-from-step-2', // Replace the empty `policyIds` array with an array of valid policy IDs if you'd like the signer to only be able to execute certain transaction requests allowed by a policy policyIds: [] }] }) ``` If your app offers limit orders to users, when a user places an order, we recommend: 1. Create a policy that allows your signer to execute the limit order from the user's wallet. 2. Add your signer to the user's wallet via the `addSessionSigner` method, using the policy from step 1. 3. When the conditions to execute the limit order are met, execute the transaction with your signer. See step 6 of this guide to learn how to use signers to execute transactions via the NodeJS SDK or REST API. ## 6. Send transactions from the user's wallet That's it! Now, both users and your app can send transactions from a user's wallet. ### User-initiated transactions Users can send transactions from your app's frontend by taking actions in a web app (via Privy's React SDK), a mobile app (via Privy's React Native SDK), or a Telegram mini-app (via Privy's React SDK). Follow the guides below to learn how to send [transactions](/wallets/using-wallets/ethereum/send-a-transaction) from these environments. #### Ethereum Send Ethereum transactions from a web app or a Telegram mini-app using Privy's React SDK. Send Ethereum transactions from a mobile app or a Telegram mini-app using Privy's React Native SDK. #### Solana Send Solana transactions from a web app or a Telegram mini-app using Privy's React SDK. Send Solana transactions from a mobile app or a Telegram mini-app using Privy's React Native SDK. ### App-initiated transactions Your app can also now initiate transactions from users' wallets via Privy's NodeJS SDK or REST API. This allows your app to send transactions from users' wallets even when the user is offline, allowing for various use cases: * Executing limit orders * Rebalancing portfolios * Having a Telegram trading bot execute transactions on behalf of users Follow the guides below to send transactions from your app's server using your app's authorization key. Using Privy's REST API directly is an advanced integration. If your app uses a JavaScript or TypeScript backend, we strongly recommend using Privy's [NodeJS SDK](/basics/nodeJS/setup). Since your app is a signer on the user's wallet, your app must sign transaction requests to Privy's API with the private key of the authorization key you generated in step 1. Your app must then include this signature in the `privy-authorization-signature` header of the request. Follow [this guide](/controls/authorization-keys/using-owners/sign) to learn how to sign requests to Privy's API with your app's authorization key. Once you've learned and implemented how to sign requests, you can now follow the guides to send transactions on [Ethereum](/wallets/using-wallets/ethereum/send-a-transaction#rest-api) and [Solana](/wallets/using-wallets/solana/send-a-transaction#rest-api) with the NodeJS SDK. **Building a Telegram trading bot?** Check out the [Telegram trading bot recipe](/recipes/telegram-bot) to learn how to have your app initiate transactions on behalf of users with a trading bot. # Treasury setup Source: https://docs.privy.io/recipes/wallets/wallet-infrastructure Privy enables developers to set up wallet infrastructure with flexible approval flows — from fully automated server-side execution to human-in-the-loop approvals requiring multiple parties. This recipe walks through setting up a complete wallet infrastructure with: * **Key quorums** to require multiple authorizations for sensitive actions * **Policies** to restrict what transactions a wallet can execute automatically * **Wallets** configured with both human-approved and server-approved transaction paths * **Transaction execution** across both approval flows For a simpler setup without dual approval paths, see the [organization wallets recipe](/recipes/wallets/organization-wallets). For execution-focused setups with EIP-7702, see the [execution wallets recipe](/recipes/wallets/execution-wallets). ### Prerequisites Before starting, ensure your Privy app is configured with at least one [Dashboard admin](/basics/get-started/dashboard/overview) and that you are familiar with [authorization keys](/controls/authorization-keys/keys/create/key) and [key quorums](/controls/key-quorum/create). Manual approvals is an Enterprise feature. Reach out to [sales@privy.io](mailto:sales@privy.io) to request access for your app. ## How it works A wallet in Privy can support multiple approval flows simultaneously: * The **owner** (typically an admin, or a quorum of admins) can execute transactions subject to a set of policies. Since the owner is a human quorum, these transactions require human-in-the-loop approvals. * **Additional signers** (server held authorization key) can also execute transactions subject to their own set of policies. Since the additional signer is a server-controlled key, these transactions can be executed programmatically without human approvals. This dual-path setup means most transactions can be automated via your server, while sensitive operations still require manual approval from your team. ## Setup Key quorums require multiple authorization signatures before taking critical actions — such as updating wallet configurations, changing policies, or executing high-value transactions. For each approver group in your organization: 1. Create a key quorum using Dashboard admin users of all members. 2. Set the `authorization_threshold` to the number of signatures required (e.g., 2-of-3). The key quorum will serve as the **owner** of your wallets and policies, ensuring that updates and sensitive transactions require human approval. Create a key quorum in the Privy Dashboard. Learn how to manually approve transactions. Authorization keys allow your server to sign requests to Privy's API automatically for permissioned actions. Create an [authorization key](/controls/authorization-keys/keys/create/key) in the Privy Dashboard and securely store the corresponding private key. Generate authorization keys in the Privy Dashboard. Policies restrict what actions a signer can take. Create separate policies for your human-approved and server-approved transaction paths. For each policy: 1. Set the `owner_id` to the admin key quorum that should be able to update the policy. 2. Define the `rules` for the policy (e.g., transfer limits, allowlists, calldata restrictions). 3. Set a human-readable `name` to identify the policy. A typical policy configuration applies stricter limits to server-approved transactions (for example, capping transfers at 1,000 USDC). Any transaction above that threshold instead requires human approval through the admin flow. Learn how to construct policies with Privy's policy language. Create a policy. Create wallets with the following configuration to support both human and server approval flows: * Set `owner_id` to the key quorum that should control the wallet. The owner can update the wallet's configuration, execute transactions (subject to the key quorum threshold). * Set `policy_ids` to the policies that apply when the **owner** executes transactions. These are the human-approval policies. * In the `additional_signers` array, add an entry with: * `signer` set to the authorization key your server uses for automated transactions. * `override_policy_ids` set to the policies that apply for **server-approved** transactions. Create a wallet. The `policy_ids` on the wallet restrict what the owner can do. The `override_policy_ids` on an additional signer restrict what that specific signer can do. This allows the same wallet to have different constraints depending on who authorizes the transaction. Your wallet now supports two transaction flows: **Server-approved transactions** (automated, no human approval required): Sign the request with your authorization key and include the signature in the `privy-authorization-signature` header. Privy verifies the signature, evaluates the additional signer's override policies, and executes the transaction. **Human-approved transactions** (requires key quorum approval): Propose an intent to execute a transaction and Privy queues the approval for the Dashboard admin key quorum corresponding to the wallet's owner. Once a sufficient number of approvals are collected, Privy executes the transaction and emits a webhook with the result. Privy strongly recommends using Privy's [server-side SDKs](/controls/authorization-keys/using-owners/sign/signing-on-the-server) to generate and include authorization signatures in your requests automatically. For human-approved transactions, Privy emits webhooks when intents are created, authorized, executed, or failed. Use [intent webhooks](/transaction-management/intents/intent-webhooks) to track approval progress and retrieve transaction results asynchronously. Learn how to send an EVM transaction. Learn how to send a Solana transaction. Sign requests to the Privy API. # Build World mini apps with Privy Source: https://docs.privy.io/recipes/world/mini-apps World Mini Apps are native-like applications that run inside World App, giving you access to millions of verified human users. With the new **World Chat** (powered by XMTP), your mini apps can integrate with messaging—opening directly in conversations, accessing group context, and sharing content back to chat. This guide shows how to integrate Privy with World Mini Apps for authentication and wallet management. If you're building mini apps across multiple platforms, Privy's account linking enables users to access your app on World, Farcaster, and Base App with the same account and embedded wallet. Official documentation for building World Mini Apps. Register your mini app and manage API keys. Learn about XMTP, the protocol powering World Chat. **Building for multiple platforms?** If you're building on World, Farcaster, and Base App, Privy's account linking lets users connect their accounts across platforms and access the same embedded wallet everywhere. ## Setup First, configure a World developer account, scaffold a mini app project, and integrate Privy. Create a World developer account and mini app at the [World Developer Portal](https://developer.worldcoin.org/). Get your App ID and API key for your mini app. Scaffold a new World Mini App using the official template: ```bash theme={"system"} npx @worldcoin/create-mini-app@latest my-mini-app ``` Follow the prompts to configure your app with your World App ID and API key. If you haven't set up Privy yet, follow our [React quickstart guide](/basics/react/installation). ## Authenticate users with World wallet Use Privy's `useLoginWithSiwe` hook to authenticate users via their World wallet: ```tsx theme={"system"} import {useLoginWithSiwe} from '@privy-io/react-auth'; import MiniKit from '@worldcoin/minikit-js'; const {generateSiweNonce, loginWithSiwe} = useLoginWithSiwe(); const handleLogin = async () => { // Get nonce from Privy const privyNonce = await generateSiweNonce(); // Request signature from World wallet const {finalPayload} = await MiniKit.commandsAsync.walletAuth({ nonce: privyNonce }); // Log in with Privy await loginWithSiwe({ message: finalPayload.message, signature: finalPayload.signature }); }; ``` ## Link accounts (optional) If you're building mini apps for other platforms like Farcaster and Base App, account linking helps users access the same embedded wallet from any platform. Users can link their login methods and maintain access to their embedded wallet and data across all platforms. If a user already has a Privy account (from Farcaster, email, etc.) and wants to access it from your World mini app, they can link their World wallet: ```tsx theme={"system"} import {useLinkWithSiwe} from '@privy-io/react-auth'; import MiniKit from '@worldcoin/minikit-js'; const {generateSiweNonce, linkWithSiwe} = useLinkWithSiwe(); const handleLinkWorldWallet = async () => { // Get nonce from Privy const privyNonce = await generateSiweNonce(); // Request signature from World wallet const {finalPayload} = await MiniKit.commandsAsync.walletAuth({ nonce: privyNonce, }); // Link wallet await linkWithSiwe({ message: finalPayload.message, signature: finalPayload.signature, }); }; ``` Users can link additional login methods to their account: ```tsx theme={"system"} import {useLinkAccount} from '@privy-io/react-auth'; const {linkFarcaster, linkEmail, linkGoogle} = useLinkAccount(); // Link additional login methods linkFarcaster(); linkEmail(); linkGoogle(); ``` ## World Chat features The new **World Chat** (powered by XMTP) adds messaging capabilities to mini apps. You can detect when your mini app is opened from a chat and share content back to conversations. The World MiniKit SDK provides features like: * `sdk.location` - Detect where your app was opened (chat, home, app store, etc.) * `sdk.context.getGroupMembers` - Get group member info when opened from chat * `sdk.commands.chat` - Share content back to chat with link previews **Example:** Detect chat context and share results: ```tsx theme={"system"} import MiniKit from '@worldcoin/minikit-js'; // Check where app was opened if (MiniKit.location === 'chat') { const members = await MiniKit.context.getGroupMembers(); console.log(`Opened in chat with ${members.length} members`); } // Share to chat await MiniKit.commands.chat({ message: 'Check out my score!', to: ['username1'] // Optional }); ``` For full World Chat SDK docs, see [World's documentation](https://docs.world.org/mini-apps). The SDK is currently in preview. ## Example use cases With World Chat's social features, you can build: * **Group betting** - See members' bets, share results to chat * **Collaborative tools** - Shared lists, planning, games with group context * **Commerce** - Split bills, group purchases in chat * **Gaming** - Multiplayer games that share achievements to chat # Integrating Aave with Privy Source: https://docs.privy.io/recipes/yield/aave-guide Create a seamless DeFi lending experience with Privy's embedded wallets and Aave protocol. This guide shows you how to build an app where users can supply tokens directly to Aave, deploy yield-bearing vaults, and manage deposits—all without external wallets or complex onboarding. ## Resources Official documentation for Aave protocol and smart contracts. Privy Wallets are a powerful tool for helping users interact with DeFi. *** ## Integrate with Aave protocol There are two ways to integrate Aave into your application: * **Supply directly to Aave**: Directly supply tokens into Aave's liquidity pools to earn interest. Your tokens become available for borrowers and you earn yield from interest payments. * **Create a managed Aave vault**: Create ERC-4626 compliant vaults that hold aTokens (Aave's interest-bearing tokens). Vaults allow applications to manage supplied tokens on behalf of users and earn a percentage of the yield generated. For this walkthrough, we'll demonstrate using **Base Sepolia** and the **WETH lending pool**. The same patterns work across all Aave-supported networks and assets—explore the complete list of available pools and addresses in the [BGD Labs Address Book](https://github.com/bgd-labs/aave-address-book). ### Install and configure the Aave SDK ### Installation ```bash theme={"system"} npm install @aave/react@latest @privy-io/react-auth@latest ``` ### Setup Below is a minimal setup for Privy provider with Aave provider setup. To customize your Privy provider, follow the instructions in the [Privy Quickstart](/basics/get-started/dashboard/create-new-app) to get your app set up with Privy. ```tsx theme={"system"} // App.tsx import {PrivyProvider} from '@privy-io/react-auth'; import {AaveProvider, AaveClient} from '@aave/react'; const client = AaveClient.create(); export function App() { return ( {/* Your application components */} ); } ``` ### Supply directly to Aave protocol This approach lets users deposit tokens directly into Aave's lending pools to earn interest. When you supply tokens, they become available for other users to borrow, and you earn yield from the borrowing fees. This is the simplest way to start earning on idle assets. The Aave SDK returns transaction objects that you execute with Privy's `sendTransaction`: ```tsx theme={"system"} import {useSupply, bigDecimal} from '@aave/react'; import {useSendTransaction, useWallets} from '@privy-io/react-auth'; const {wallets} = useWallets(); const {sendTransaction} = useSendTransaction(); const [supply] = useSupply(); const supplyToken = async () => { const result = await supply({ market: '0x8bAB6d1b75f19e9eD9fCe8b9BD338844fF79aE27', amount: {native: bigDecimal(0.1)}, // Supply 0.1 ETH sender: wallets[0].address, chainId: 84532 }); if (result.isErr()) { throw new Error(`Supply failed: ${result.error}`); } const plan = result.value; // Handle approval if required if (plan.__typename === 'ApprovalRequired') { await sendTransaction( { to: plan.approval.to, value: BigInt(plan.approval.value), data: plan.approval.data, chainId: plan.approval.chainId }, {address: wallets[0].address} ); // Execute supply transaction return await sendTransaction( { to: plan.originalTransaction.to, value: BigInt(plan.originalTransaction.value), data: plan.originalTransaction.data, chainId: plan.originalTransaction.chainId }, {address: wallets[0].address} ); } // Direct supply transaction if (plan.__typename === 'TransactionRequest') { return await sendTransaction( { to: plan.to, value: BigInt(plan.value), data: plan.data, chainId: plan.chainId }, {address: wallets[0].address} ); } throw new Error(`Unhandled plan type: ${plan.__typename}`); }; ``` *** ### Create a managed Aave vault Aave Vaults are ERC-4626 compliant yield-bearing vaults that allow users to supply and withdraw ERC-20 tokens supported by Aave V3. Vaults enable applications to manage supplied tokens on behalf of users and earn a percentage of revenue. ```tsx theme={"system"} import {useAaveReserve, useVaultDeploy, bigDecimal} from '@aave/react'; import {useSendTransaction, useWallets} from '@privy-io/react-auth'; const {wallets} = useWallets(); const {sendTransaction} = useSendTransaction(); const [deployVault] = useVaultDeploy(); // Get reserve data for WETH on Base Sepolia (needed for vault deployment) const {data: reserve} = useAaveReserve({ market: '0x8bAB6d1b75f19e9eD9fCe8b9BD338844fF79aE27', // Base Sepolia Pool underlyingToken: '0x4200000000000000000000000000000000000006', // WETH chainId: 84532, suspense: true }); ``` ```tsx theme={"system"} const deploy = async () => { const result = await deployVault({ market: reserve.market.address, chainId: 84532, underlyingToken: reserve.underlyingToken.address, deployer: wallets[0].address, initialFee: bigDecimal(3), // 3% performance fee shareName: 'Aave WETH Vault Shares', shareSymbol: 'avWETH', initialLockDeposit: bigDecimal(1) // 1 WETH initial deposit }); if (result.isErr()) { throw new Error(`Deployment failed: ${result.error}`); } const plan = result.value; // Handle approval if required if (plan.__typename === 'ApprovalRequired') { await sendTransaction( { to: plan.approval.to, value: BigInt(plan.approval.value), data: plan.approval.data, chainId: plan.approval.chainId }, {address: wallets[0].address} ); return await sendTransaction( { to: plan.originalTransaction.to, value: BigInt(plan.originalTransaction.value), data: plan.originalTransaction.data, chainId: plan.originalTransaction.chainId }, {address: wallets[0].address} ); } if (plan.__typename === 'TransactionRequest') { return await sendTransaction( { to: plan.to, value: BigInt(plan.value), data: plan.data, chainId: plan.chainId }, {address: wallets[0].address} ); } throw new Error(`Unhandled plan type: ${plan.__typename}`); }; ``` ```tsx theme={"system"} import {useVault, useVaultDeposit} from '@aave/react'; const {data: vault} = useVault({ by: {address: '0x36b22e03bc9f8d08109ca4bb36241e3bfb7077fa'}, // vault address to deposit tokens chainId: 84532 }); const [deposit] = useVaultDeposit(); const depositTokens = async () => { const result = await deposit({ chainId: vault.chainId, vault: vault.address, amount: { currency: vault.usedReserve.underlyingToken.address, value: bigDecimal(100) // 100 tokens }, depositor: wallets[0].address }); if (result.isErr()) { throw new Error(`Deposit failed: ${result.error}`); } const plan = result.value; // Handle approval if required if (plan.__typename === 'ApprovalRequired') { await sendTransaction( { to: plan.approval.to, value: BigInt(plan.approval.value), data: plan.approval.data, chainId: plan.approval.chainId }, {address: wallets[0].address} ); return await sendTransaction( { to: plan.originalTransaction.to, value: BigInt(plan.originalTransaction.value), data: plan.originalTransaction.data, chainId: plan.originalTransaction.chainId }, {address: wallets[0].address} ); } if (plan.__typename === 'TransactionRequest') { return await sendTransaction( { to: plan.to, value: BigInt(plan.value), data: plan.data, chainId: plan.chainId }, {address: wallets[0].address} ); } throw new Error(`Unhandled plan type: ${plan.__typename}`); }; ``` ## Key integration tips 1. **In NodeJS**: the `sendWith` method from the Aave SDK is feature-rich and streamlines complex transaction flows. It automatically handles token approvals when required and then sends the main Aave transaction, making the overall process more seamless. 2. **Handle transaction plans**: The Aave SDK returns various plan types (actions) like `TransactionRequest` and `ApprovalRequired` which can be used to handle different transaction scenarios accordingly. This allows for flexible handling of different approval and execution patterns. 3. **Add error handling**: Production applications should wrap all async functions in try/catch blocks to handle common blockchain errors like user rejection, insufficient funds, network issues, and contract failures. Consider implementing user-friendly error messages and retry mechanisms for failed transactions. *** ## Conclusion With Privy and the Aave, building powerful DeFi lending experiences becomes seamless and secure. Users can interact with Aave protocol seamlessly through embedded wallets without needing external wallet management. # Integrating Ethena with Privy Source: https://docs.privy.io/recipes/yield/ethena-guide Ethena allows apps to earn yield by staking USDe into the sUSDe vault. The sUSDe vault follows the ERC-4626 standard — your app deposits USDe and receives sUSDe shares that automatically accrue yield over time. There is no minimum staking period, and rewards are distributed every 8 hours. ## Resources Official documentation for staking USDe and the sUSDe contract. Set up Privy and create embedded wallets for your app. ## Getting USDe Your app can swap any supported token for USDe on any major decentralized exchange using a DEX aggregator or router. ## Stake USDe into the sUSDe vault Set up the sUSDe vault and USDe token addresses. The example below uses Ethereum Mainnet: ```tsx theme={"system"} const SUSDE_VAULT_ADDRESS = '0x9D39A5DE30e57443BfF2A8307A4256c8797A3497'; const USDE_ADDRESS = '0x4c9EDD5852cd905f086C759E8383e09bff1E68B3'; const CHAIN_ID = 1; // Ethereum Mainnet ``` Use viem's `encodeFunctionData` to encode the approval, and Privy's `useSendTransaction` to send it: ```tsx theme={"system"} import {encodeFunctionData, maxUint256, erc20Abi} from 'viem'; import {useSendTransaction} from '@privy-io/react-auth'; const {sendTransaction} = useSendTransaction(); const data = encodeFunctionData({ abi: erc20Abi, functionName: 'approve', args: [SUSDE_VAULT_ADDRESS as `0x${string}`, maxUint256] }); const tx = await sendTransaction({ to: USDE_ADDRESS as `0x${string}`, data, chainId: CHAIN_ID }); ``` Use viem to encode the deposit and Privy's `useSendTransaction` to stake USDe: ```tsx theme={"system"} import {encodeFunctionData, parseUnits} from 'viem'; import {useSendTransaction} from '@privy-io/react-auth'; const {sendTransaction} = useSendTransaction(); const erc4626Abi = [ { type: 'function', name: 'deposit', inputs: [ {name: 'assets', type: 'uint256'}, {name: 'receiver', type: 'address'} ], outputs: [{name: 'shares', type: 'uint256'}], stateMutability: 'nonpayable' } ] as const; const depositAmount = parseUnits('100', 18); // 100 USDe (18 decimals) const data = encodeFunctionData({ abi: erc4626Abi, functionName: 'deposit', args: [depositAmount, address] }); const tx = await sendTransaction({ to: SUSDE_VAULT_ADDRESS as `0x${string}`, data, chainId: CHAIN_ID }); ``` ### Check balance Call the vault's `balanceOf` function to get the user's sUSDe shares, then call `convertToAssets` to see the current USDe value including accrued yield. ```tsx theme={"system"} import {publicClient} from './viem'; const erc4626Abi = [ { type: 'function', name: 'balanceOf', inputs: [{name: 'account', type: 'address'}], outputs: [{name: '', type: 'uint256'}], stateMutability: 'view' }, { type: 'function', name: 'convertToAssets', inputs: [{name: 'shares', type: 'uint256'}], outputs: [{name: '', type: 'uint256'}], stateMutability: 'view' } ] as const; const userShares = await publicClient.readContract({ address: SUSDE_VAULT_ADDRESS as `0x${string}`, abi: erc4626Abi, functionName: 'balanceOf', args: [address] }); const usdeValue = await publicClient.readContract({ address: SUSDE_VAULT_ADDRESS as `0x${string}`, abi: erc4626Abi, functionName: 'convertToAssets', args: [userShares] }); ``` ### Unstake from the vault Ethena has a **7-day cooldown period** for unstaking. Your app must first request an unstake, which places the USDe in the USDeSilo contract. After the cooldown completes, your app can withdraw the USDe. Unlike instant withdrawal vaults, Ethena requires a 7-day cooldown period after requesting an unstake before USDe can be withdrawn. Call `cooldownShares` on the sUSDe vault to initiate the unstaking process. The USDe is placed in the USDeSilo contract during the cooldown period. ```tsx theme={"system"} import {encodeFunctionData} from 'viem'; import {useSendTransaction} from '@privy-io/react-auth'; const {sendTransaction} = useSendTransaction(); const cooldownAbi = [ { type: 'function', name: 'cooldownShares', inputs: [{name: 'shares', type: 'uint256'}], outputs: [{name: 'assets', type: 'uint256'}], stateMutability: 'nonpayable' } ] as const; const data = encodeFunctionData({ abi: cooldownAbi, functionName: 'cooldownShares', args: [userShares] }); const tx = await sendTransaction({ to: SUSDE_VAULT_ADDRESS as `0x${string}`, data, chainId: CHAIN_ID }); ``` After the 7-day cooldown period has passed, call `unstake` on the sUSDe vault to withdraw USDe from the USDeSilo contract: ```tsx theme={"system"} import {encodeFunctionData} from 'viem'; import {useSendTransaction} from '@privy-io/react-auth'; const {sendTransaction} = useSendTransaction(); const unstakeAbi = [ { type: 'function', name: 'unstake', inputs: [{name: 'receiver', type: 'address'}], outputs: [], stateMutability: 'nonpayable' } ] as const; const data = encodeFunctionData({ abi: unstakeAbi, functionName: 'unstake', args: [address] }); const tx = await sendTransaction({ to: SUSDE_VAULT_ADDRESS as `0x${string}`, data, chainId: CHAIN_ID }); ``` ## Key integration tips 1. **Always approve first**: Your app must grant ERC-20 approval to the sUSDe vault before any deposit. 2. **USDe uses 18 decimals**: Use `parseUnits('100', 18)` for amounts. 3. **Use `convertToAssets` for real-time balances**: This ERC-4626 method converts sUSDe shares to their current USDe value, including accrued yield. 4. **Plan for the 7-day cooldown**: Unlike other yield protocols, Ethena requires a cooldown period before withdrawing staked USDe. During this period, the USDe is held in the USDeSilo contract. 5. **No minimum staking period**: Your app can stake and unstake in consecutive blocks, though the 7-day cooldown still applies for withdrawals. 6. **sUSDe value only increases**: Rewards can only be positive or zero — stakers cannot lose USDe by staking. 7. **Supported chains**: Ethena sUSDe staking is available on Ethereum Mainnet. *** ## Conclusion Privy makes it straightforward to build secure, user-friendly access to Ethena yield. For advanced use cases, refer to the [Ethena Staking Docs](https://docs.ethena.fi/solution-design/staking-usde), or reach out in [Slack](https://privy.io/slack). Your app is now ready to interact with Ethena using Privy embedded wallets! # Integrating Kamino with Privy Source: https://docs.privy.io/recipes/yield/kamino-guide Kamino Earn vaults enable users to generate yield on their Solana assets. This guide walks through setting up backend developer-owned wallets using [NodeJS](/basics/nodeJS/quickstart) and building and sending a Solana transaction that deposits funds into a Kamino Earn vault. ## Deposit flow Import the required packages for Privy client, Solana RPC communication, and Kamino SDK operations. Set up your Privy credentials and initialize the Privy client and RPC connection. ```typescript theme={"system"} import {PrivyClient} from '@privy-io/node'; import { createSolanaRpc, address, pipe, createTransactionMessage, setTransactionMessageFeePayerSigner, setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstructions, createNoopSigner, getBase64EncodedWireTransaction, compileTransaction } from '@solana/kit'; import {KaminoVault} from '@kamino-finance/klend-sdk'; import {Decimal} from 'decimal.js'; const PRIVY_APP_ID = 'put-your-privy-app-id-here'; const PRIVY_APP_SECRET = 'put-your-privy-app-secret-here'; const RPC_ENDPOINT = 'https://api.mainnet-beta.solana.com'; const VAULT_ADDRESS = 'put-your-vault-address-here'; const AUTH_KEY_ID = 'put-your-auth-key-id-here'; const AUTH_KEY_PRIVATE = 'put-your-auth-key-private-here'; const USER_EMAIL = 'put-your-email-here'; const privy = new PrivyClient({ appId: PRIVY_APP_ID, appSecret: PRIVY_APP_SECRET }); const rpc = createSolanaRpc(RPC_ENDPOINT); ``` Create a Privy user, generate a Solana wallet linked to the user, and set your authorization key as the wallet owner. We recommend funding the wallet with SOL and the tokens you will lend (likely USDC) so that your wallet can submit transactions successfully. ```typescript {skip-check} theme={"system"} await privy.users().create({ linked_accounts: [{type: 'email', address: USER_EMAIL}] }); const {id: walletId, address: walletAddress} = await privy.wallets().create({ chain_type: 'solana', owner_id: AUTH_KEY_ID }); ``` Initialize the vault and generate deposit instructions using a noop signer. A noop signer is used to build instructions without requiring the actual private key. Privy will handle signing later. Additionally fetch the latest blockhash and construct the transaction message. ```typescript {skip-check} theme={"system"} const vault = new KaminoVault(rpc, address(VAULT_ADDRESS)); await vault.getState(); const noopSigner = createNoopSigner(address(walletAddress)); const bundle = await vault.depositIxs(noopSigner, new Decimal(1.0)); const instructions = [...(bundle.depositIxs || [])]; if (!instructions.length) throw new Error('No instructions returned'); const {value: latestBlockhash} = await rpc.getLatestBlockhash().send(); const depositTransactionMessage = pipe( createTransactionMessage({version: 0}), (tx) => setTransactionMessageFeePayerSigner(noopSigner, tx), (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), (tx) => appendTransactionMessageInstructions(instructions, tx) ); ``` Compile the transaction, serialize it, and sign using Privy’s wallet API. Privy handles the signing securely using the embedded wallet. The private key never leaves Privy’s infrastructure. ```typescript {skip-check} theme={"system"} const compiledTransaction = compileTransaction(depositTransactionMessage); const serializedTx = getBase64EncodedWireTransaction(compiledTransaction); const signResponse = await privy .wallets() .solana() .signTransaction(walletId, { transaction: serializedTx, authorization_context: { authorization_private_keys: [AUTH_KEY_PRIVATE] } }); ``` Submit the signed transaction to the Solana network. ```typescript {skip-check} theme={"system"} const signature = await rpc .sendTransaction(signResponse.signed_transaction as any, { encoding: 'base64', skipPreflight: true }) .send(); console.log('Deposit successful! Signature:', signature); ``` Your deposit is complete! The user’s funds are now deposited in the Kamino Earn vault using their Privy embedded wallet. ## Additional resources Official documentation for Kamino protocol. Configure embedded Solana wallets for users. # Borrow with Morpho Source: https://docs.privy.io/recipes/yield/morpho-borrow [Morpho](https://morpho.org/) is a decentralized lending protocol deployed on Tempo that enables capital-efficient borrowing. Using the [cbBTC/pathUSD market](https://app.morpho.org/tempo/market/0x75add2f66f5c81917f6fd3b9603481defc1098359515d91c652913437ada14f5/cbbtc-pathusd), your app can let users supply cbBTC as collateral and borrow pathUSD against it. This recipe demonstrates how to integrate Morpho lending into your app using Privy embedded wallets and the Morpho SDK. ## Install dependencies ```bash theme={"system"} npm install @privy-io/node viem @morpho-org/morpho-sdk @morpho-org/blue-sdk ``` ## 1. Configure the Morpho market Define the market parameters for the cbBTC/pathUSD market on Tempo. These parameters uniquely identify the Morpho Blue market your app will interact with. ```typescript {skip-check} theme={"system"} import {MarketParams} from '@morpho-org/blue-sdk'; const TEMPO_CHAIN_ID = 4217; const MORPHO_BLUE_ADDRESS = '0x10EE9AAC980A180dd4DcFc96C746d60B0EA88f97'; const CBBTC_ADDRESS = '0x20C000000000000000000000c412Ec89D0c08be5'; const PATHUSD_ADDRESS = '0x20C0000000000000000000000000000000000000'; const cbbtcPathUsdMarket = new MarketParams({ loanToken: PATHUSD_ADDRESS, collateralToken: CBBTC_ADDRESS, oracle: '0xa59e9ACD8499343ae7b944df898fc20388ACD245', irm: '0x112fd4042E442C3C12C67AD23587b0afe36eB74E', lltv: 770000000000000000n // 77% loan-to-value }); ``` These parameters are fixed for each Morpho Blue market — they are set by the market creator at deployment and cannot be changed. The `lltv` (liquidation loan-to-value) determines the maximum ratio of debt to collateral before a position becomes liquidatable. To find these values for any market, query the [Morpho Blue contract](https://docs.morpho.org/contracts/morpho-blue/) or look them up on the [Morpho app](https://app.morpho.org/tempo/market/0x75add2f66f5c81917f6fd3b9603481defc1098359515d91c652913437ada14f5/cbbtc-pathusd). Both cbBTC and pathUSD use 6 decimals on Tempo. When specifying amounts, `1_000_000n` equals 1 cbBTC or 1 pathUSD. Note that cbBTC uses 8 decimals on other chains but 6 on Tempo. ## 2. Set up the Privy wallet and viem client Create a viem wallet client from the Privy embedded wallet and extend it with the Morpho SDK. The `morphoViemExtension` adds Morpho market methods directly to the client. ```typescript {skip-check} theme={"system"} import {PrivyClient} from '@privy-io/node'; import {createViemAccount} from '@privy-io/node/viem'; import {createWalletClient, createPublicClient, http} from 'viem'; import {tempo} from 'viem/chains'; import {morphoViemExtension} from '@morpho-org/morpho-sdk'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID!, appSecret: process.env.PRIVY_APP_SECRET! }); const account = createViemAccount(privy, { walletId: 'insert-wallet-id', address: 'insert-wallet-address' }); const client = createWalletClient({ account, chain: tempo, transport: http() }).extend(morphoViemExtension({supportSignature: true})); const publicClient = createPublicClient({ chain: tempo, transport: http() }); ``` ## 3. Supply collateral and borrow Use the Morpho SDK to construct and send transactions atomically via [Bundler3](https://docs.morpho.org/learn/concepts/bundlers/). The SDK handles ERC-20 approvals, Morpho `setAuthorization` for the GeneralAdapter1, and Permit/Permit2 signatures automatically through its requirements system. ```typescript {skip-check} theme={"system"} const market = client.morpho.marketV1(cbbtcPathUsdMarket, TEMPO_CHAIN_ID); const collateralAmount = 100_000n; // 0.1 cbBTC (6 decimals on Tempo) const borrowAmount = 4_000_000_000n; // 4,000 pathUSD (6 decimals) — well under 77% LLTV const userAddress = client.account.address; const positionData = await market.getPositionData(userAddress); const {buildTx, getRequirements} = market.supplyCollateralBorrow({ amount: collateralAmount, borrowAmount, userAddress, positionData }); // Resolve requirements (ERC-20 approvals, Morpho authorizations, Permit signatures) let signature; for (const req of await getRequirements()) { if ('sign' in req) { signature = await req.sign(client, userAddress); } else { const hash = await client.sendTransaction(req); await publicClient.waitForTransactionReceipt({hash}); } } // Execute the atomic supply + borrow transaction const hash = await client.sendTransaction(buildTx(signature)); const receipt = await publicClient.waitForTransactionReceipt({hash}); console.log('Supply + borrow tx:', receipt.transactionHash); ``` The SDK bundles `supplyCollateral` and `borrow` into a single atomic transaction via Bundler3. It also includes an LLTV buffer check to prevent the new position from being instantly liquidatable. Always verify that the borrow amount stays safely below the liquidation threshold. The market LLTV is 77%, meaning a position is liquidated when the debt value exceeds 77% of the collateral value. Use a safety buffer (e.g., borrow at most 70% of max) to avoid liquidation from price movements. ## 4. Repay and withdraw collateral To close the position, repay the borrowed pathUSD and withdraw the cbBTC collateral. The SDK bundles repay and withdraw into a single atomic transaction (repay executes first, then withdraw). ```typescript {skip-check} theme={"system"} const fresh = await market.getPositionData(userAddress); // Repay full debt and withdraw all collateral const {buildTx, getRequirements} = market.repayWithdrawCollateral({ shares: fresh.borrowShares, // repay full debt using shares withdrawAmount: fresh.collateral, // withdraw all collateral userAddress, positionData: fresh }); let signature; for (const req of await getRequirements()) { if ('sign' in req) { signature = await req.sign(client, userAddress); } else { const hash = await client.sendTransaction(req); await publicClient.waitForTransactionReceipt({hash}); } } const hash = await client.sendTransaction(buildTx(signature)); const receipt = await publicClient.waitForTransactionReceipt({hash}); console.log('Repay + withdraw tx:', receipt.transactionHash); ``` Repaying by `shares` instead of a fixed asset amount ensures the full debt is repaid, including any interest that accrued since the borrow. ## 5. Check position health Monitor a position's health factor to alert users before liquidation risk becomes critical. The `AccrualPosition` returned by `getPositionData` includes a built-in `healthFactor` getter (WAD-scaled). These values may be `undefined` if the oracle is unavailable. ```typescript {skip-check} theme={"system"} const positionData = await market.getPositionData(userAddress); const healthFactor = positionData.healthFactor; const ltv = positionData.ltv; const maxBorrowableAssets = positionData.maxBorrowableAssets; console.log( 'Health factor:', healthFactor === undefined ? 'Oracle unavailable' : positionData.borrowAssets === 0n ? 'No debt' : Number(healthFactor) / 1e18 ); console.log( 'Max additional borrow (pathUSD):', maxBorrowableAssets === undefined ? 'Oracle unavailable' : Number(maxBorrowableAssets) / 1e6 ); console.log( 'Current LTV:', ltv === undefined ? 'Oracle unavailable' : ltv === null ? 0 : Number(ltv) / 1e18 ); ``` ## Market parameters | Parameter | Value | | --------------- | ---------------------------------------------------------------- | | **Collateral** | cbBTC (`0x20C000000000000000000000c412Ec89D0c08be5`) | | **Loan token** | pathUSD (`0x20C0000000000000000000000000000000000000`) | | **LLTV** | 77% | | **Oracle** | ChainlinkOracleV2 (`0xa59e9ACD8499343ae7b944df898fc20388ACD245`) | | **Chain** | Tempo Mainnet (chain ID 4217) | | **Morpho Blue** | `0x10EE9AAC980A180dd4DcFc96C746d60B0EA88f97` | ## Related resources TypeScript SDK reference for Morpho protocol interactions Create embedded wallets and send transactions on Tempo # Overview Source: https://docs.privy.io/recipes/yield/overview Overview of yield integration recipes with Privy wallets across DeFi protocols. Build earning and borrowing flows by combining Privy embedded wallets with DeFi protocol integrations across Ethereum, Solana, and Tempo. Looking for a simple DeFi solution? [Earn](/wallets/actions/earn/overview) provides direct access to leading yield-generating protocols, revenue sharing, and position tracking out of the box, with a single API. ## Earn Deposit into Morpho vaults, collect fees, and track positions with Privy's earn feature. Build lending and vault flows on Aave. Offer yield on Veda BoringVault strategies. Earn yield by staking USDe in sUSDe vaults. Earn the Sky Savings Rate by depositing USDS into sUSDS vaults. Build a Solana-based yield flow with Kamino Earn. Access diversified yield strategies across multiple DeFi protocols. Deposit and withdraw from Jupiter Earn vaults using Privy embedded wallets. Use Privy embedded wallets with Yield.xyz AgentKit. Deposit USD1 into markets powered by Dolomite to earn yield. ## Borrow Supply collateral and borrow against it using Morpho Blue markets. # Integrating Pods with Privy Source: https://docs.privy.io/recipes/yield/pods-guide Pods allows apps to access diversified DeFi yield strategies across multiple protocols through a single API. Apps fetch strategies from Pods and execute the returned transaction bytecodes using Privy wallets. This enables deposits into lending, staking, and protocol-native yield strategies without managing complex transaction flows. ## Resources Official documentation for Pods strategies and API. Full working example of Pods + Privy integration. *** ## Using Pods with Privy For this walkthrough, the examples use the Aave USDT strategy on Polygon. The same patterns work across all Pods-supported strategies and networks—explore the complete list via the [Pods API](https://docs.pods.finance). ### Setup Below is a minimal setup for the Privy provider. To customize the provider, follow the [Privy Quickstart](/basics/get-started/dashboard/create-new-app). ```tsx theme={"system"} import {PrivyProvider} from '@privy-io/react-auth'; export function App() { return ( {/* Your application components */} ); } ``` ### Configure Pods API access Obtain your API credentials from the Pods Dashboard: ```tsx theme={"system"} const PODS_API_URL = 'https://api.pods.finance'; const PODS_API_KEY = 'your-pods-api-key'; ``` ### List available strategies Fetch the yield strategies available for users: ```tsx theme={"system"} const fetchStrategies = async () => { const response = await fetch(`${PODS_API_URL}/strategies?limit=100`, { method: 'GET', headers: { 'x-api-key': PODS_API_KEY } }); const {data: strategies} = await response.json(); return strategies; }; ``` Each strategy includes fields like `id`, `protocol`, `assetName`, `network`, and `availableActions`. *** ### Deposit into a yield strategy ```tsx theme={"system"} import {useWallets} from '@privy-io/react-auth'; const {wallets} = useWallets(); const walletAddress = wallets[0]?.address; ``` Request the bytecodes needed to execute a deposit action: ```tsx theme={"system"} const strategyId = 'Aave-USDT-polygon'; const action = 'lend'; const amount = '1500000'; // 1.5 USDT (6 decimals) const response = await fetch( `${PODS_API_URL}/strategies/${strategyId}/bytecode?action=${action}&wallet=${walletAddress}&amount=${amount}`, { method: 'GET', headers: { 'x-api-key': PODS_API_KEY } } ); const bytecodeResponse = await response.json(); ``` The response contains an array of transactions to execute: ```tsx theme={"system"} type PodsBytecodeResponse = { feeCharged: string; metadata: { isCrossChain: boolean; isSameChainSwap: boolean; crossChainQuoteId: string; }; bytecode: { to: string; value: string; data: string; chainId: string; }[]; }; ``` Use Privy's `useSendTransaction` to execute each bytecode: ```tsx theme={"system"} import {useSendTransaction} from '@privy-io/react-auth'; const {sendTransaction} = useSendTransaction(); const executePodsStrategy = async (bytecodeResponse: PodsBytecodeResponse) => { for (const tx of bytecodeResponse.bytecode) { await sendTransaction({ to: tx.to as `0x${string}`, data: tx.data as `0x${string}`, value: BigInt(tx.value), chainId: Number(tx.chainId) }); } }; ``` *** ### Check strategy position Fetch the user's current position and APY for a strategy: ```tsx theme={"system"} const fetchPosition = async (strategyId: string, walletAddress: string) => { const response = await fetch(`${PODS_API_URL}/strategies/${strategyId}?wallet=${walletAddress}`, { method: 'GET', headers: { 'x-api-key': PODS_API_KEY } }); const strategy = await response.json(); return { apy: strategy.apy, avgApy: strategy.avgApy, position: strategy.position }; }; ``` *** ### Withdraw from a strategy To withdraw, use the `withdraw` action and execute the returned bytecodes: ```tsx theme={"system"} import {useSendTransaction} from '@privy-io/react-auth'; const {sendTransaction} = useSendTransaction(); const withdrawFromStrategy = async (strategyId: string, walletAddress: string, amount: string) => { const response = await fetch( `${PODS_API_URL}/strategies/${strategyId}/bytecode?action=withdraw&wallet=${walletAddress}&amount=${amount}`, { method: 'GET', headers: { 'x-api-key': PODS_API_KEY } } ); const bytecodeResponse = await response.json(); for (const tx of bytecodeResponse.bytecode) { await sendTransaction({ to: tx.to as `0x${string}`, data: tx.data as `0x${string}`, value: BigInt(tx.value), chainId: Number(tx.chainId) }); } }; ``` *** ## Pods EarnWidget For a faster integration, Pods provides a pre-built `EarnWidget` component that handles strategy selection, deposits, and withdrawals with a built-in UI. ### Install the Pods SDK ```bash theme={"system"} npm install pods-sdk @pods-sdk/components @reduxjs/toolkit react-redux redux permissionless ``` If your app uses Vite, install the Node polyfills plugin and add it to `vite.config.ts`: ```bash theme={"system"} npm install --save-dev vite-plugin-node-polyfills ``` ```ts theme={"system"} import {defineConfig} from 'vite'; import {nodePolyfills} from 'vite-plugin-node-polyfills'; export default defineConfig({ plugins: [nodePolyfills()] }); ``` ### Configure the widget ```tsx theme={"system"} import {PodsProvider, EarnWidget} from 'pods-sdk'; import {useSmartWallets} from '@privy-io/react-auth/smart-wallets'; import {usePrivy} from '@privy-io/react-auth'; const {user} = usePrivy(); const {client: smartWalletClient, getClientForChain} = useSmartWallets(); const config = { PODS_API_URL: 'https://api.pods.finance', PODS_API_KEY: 'your-pods-api-key', walletAddress: smartWalletClient?.account.address, userId: user?.id, globalCurrency: 'USD', globalCurrencyExchangeRate: 1, theme: {mode: 'dark', preset: 'default'} }; ``` ### Implement the bytecode processor The widget returns bytecode for the app to execute using Privy smart wallets: ```tsx theme={"system"} import type {BytecodeTransaction, UpdateTxStatus} from 'pods-sdk'; const processBytecode = async ( payload: {clientTxId: string; bytecodes: BytecodeTransaction[]; simulateError?: boolean}, ctx: {updateTxStatus: UpdateTxStatus} ) => { ctx.updateTxStatus({type: 'HOST_ACK', clientTxId: payload.clientTxId}); const chainId = Number(payload.bytecodes[0].chainId); const chainClient = await getClientForChain({id: chainId}); const calls = payload.bytecodes.map((b) => ({ to: b.to as `0x${string}`, data: b.data as `0x${string}`, value: BigInt(b.value) })); ctx.updateTxStatus({type: 'SIGNATURE_PROMPTED', clientTxId: payload.clientTxId}); const txHash = await chainClient.sendTransaction({calls}); ctx.updateTxStatus({type: 'TX_SUBMITTED', clientTxId: payload.clientTxId, txHash}); }; ``` ### Render the widget ```tsx theme={"system"} ``` The EarnWidget requires [Smart Wallets](/wallets/using-wallets/evm-smart-wallets/overview) for transaction execution. Enable Smart Wallets in the [Privy Dashboard](https://dashboard.privy.io/) under Wallet infrastructure. *** ## Key integration tips 1. **Handle token decimals**: The `amount` parameter must respect the token's decimal places. USDT and USDC use 6 decimals; most other tokens use 18. 2. **Execute transactions in order**: Pods may return multiple bytecodes for a single action. Execute them sequentially. 3. **Verify available actions**: Each strategy has an `availableActions` array. Check that the action exists before requesting bytecodes. 4. **Handle errors gracefully**: Wrap transaction execution in try/catch blocks to handle user rejection, insufficient funds, and network issues. For advanced use cases, refer to the [Pods Docs](https://docs.pods.finance), or reach out in [Slack](https://privy.io/slack). # Integrating Sky savings with Privy Source: https://docs.privy.io/recipes/yield/sky-savings-guide Sky Savings allows apps to earn the Sky Savings Rate by depositing USDS into the sUSDS vault. The sUSDS vault follows the ERC-4626 standard — your app deposits USDS and receives sUSDS shares that automatically accrue yield. ## Resources Official documentation for the Sky protocol and smart contracts. Privy Wallets are a powerful tool for helping users interact with DeFi. *** ## Getting USDS ### Swap on a DEX Your app can swap any supported token for USDS on any major decentralized exchange using a DEX aggregator or router. ### Convert DAI to USDS On Ethereum, your app can convert DAI to USDS at a 1:1 rate through the [DaiUsds converter contract](https://etherscan.io/address/0x3225737a9bbb6473cb4a45b7244aca2befdb276a#writeContract). First, approve the converter to spend DAI, then call `daiToUsds`. ```tsx theme={"system"} import {encodeFunctionData, maxUint256, erc20Abi, parseUnits} from 'viem'; import {useSendTransaction} from '@privy-io/react-auth'; const {sendTransaction} = useSendTransaction(); const DAI_ADDRESS = '0x6B175474E89094C44Da98b954EedeAC495271d0F'; const DAI_USDS_CONVERTER = '0x3225737a9Bbb6473CB4a45b7244ACa2BEFdB276A'; const CHAIN_ID = 1; // Ethereum Mainnet // Step 1: Approve the converter to spend DAI const approveData = encodeFunctionData({ abi: erc20Abi, functionName: 'approve', args: [DAI_USDS_CONVERTER as `0x${string}`, maxUint256] }); await sendTransaction({ to: DAI_ADDRESS as `0x${string}`, data: approveData, chainId: CHAIN_ID }); // Step 2: Convert DAI to USDS const converterAbi = [ { type: 'function', name: 'daiToUsds', inputs: [ {name: 'usr', type: 'address'}, {name: 'wad', type: 'uint256'} ], outputs: [], stateMutability: 'nonpayable' } ] as const; const amount = parseUnits('100', 18); // 100 DAI const convertData = encodeFunctionData({ abi: converterAbi, functionName: 'daiToUsds', args: [address, amount] }); await sendTransaction({ to: DAI_USDS_CONVERTER as `0x${string}`, data: convertData, chainId: CHAIN_ID }); ``` *** ## Using Sky Savings with Privy ### Deposit USDS into Sky Savings Set up the sUSDS vault and USDS token addresses. The example below uses Ethereum Mainnet: ```tsx theme={"system"} const SUSDS_VAULT_ADDRESS = '0xa3931d71877C0E7a3148CB7Eb4463524FEc27fbD'; const USDS_ADDRESS = '0xdC035D45d973E3EC169d2276DDab16f1e407384F'; const CHAIN_ID = 1; // Ethereum Mainnet ``` Use viem's `encodeFunctionData` to encode the approval, and Privy's `useSendTransaction` to send it: ```tsx theme={"system"} import {encodeFunctionData, maxUint256, erc20Abi} from 'viem'; import {useSendTransaction} from '@privy-io/react-auth'; const {sendTransaction} = useSendTransaction(); const data = encodeFunctionData({ abi: erc20Abi, functionName: 'approve', args: [SUSDS_VAULT_ADDRESS as `0x${string}`, maxUint256] }); const tx = await sendTransaction({ to: USDS_ADDRESS as `0x${string}`, data, chainId: CHAIN_ID }); ``` Use viem to encode the deposit and Privy's `useSendTransaction` to fund the vault: ```tsx theme={"system"} import {encodeFunctionData, parseUnits} from 'viem'; import {useSendTransaction} from '@privy-io/react-auth'; const {sendTransaction} = useSendTransaction(); const erc4626Abi = [ { type: 'function', name: 'deposit', inputs: [ {name: 'assets', type: 'uint256'}, {name: 'receiver', type: 'address'} ], outputs: [{name: 'shares', type: 'uint256'}], stateMutability: 'nonpayable' } ] as const; const depositAmount = parseUnits('100', 18); // 100 USDS (18 decimals) const data = encodeFunctionData({ abi: erc4626Abi, functionName: 'deposit', args: [depositAmount, address] }); const tx = await sendTransaction({ to: SUSDS_VAULT_ADDRESS as `0x${string}`, data, chainId: CHAIN_ID }); ``` *** ### Check balance Call the vault's `balanceOf` function to get the user's sUSDS shares, then call `convertToAssets` to see the current USDS value including accrued yield. ```tsx theme={"system"} import {publicClient} from './viem'; const erc4626Abi = [ { type: 'function', name: 'balanceOf', inputs: [{name: 'account', type: 'address'}], outputs: [{name: '', type: 'uint256'}], stateMutability: 'view' }, { type: 'function', name: 'convertToAssets', inputs: [{name: 'shares', type: 'uint256'}], outputs: [{name: '', type: 'uint256'}], stateMutability: 'view' } ] as const; const userShares = await publicClient.readContract({ address: SUSDS_VAULT_ADDRESS as `0x${string}`, abi: erc4626Abi, functionName: 'balanceOf', args: [address] }); const usdsValue = await publicClient.readContract({ address: SUSDS_VAULT_ADDRESS as `0x${string}`, abi: erc4626Abi, functionName: 'convertToAssets', args: [userShares] }); ``` *** ### Withdraw from the vault Your app can withdraw a specific amount of USDS or redeem all shares for a full exit. ```tsx theme={"system"} import {encodeFunctionData, parseUnits} from 'viem'; import {useSendTransaction} from '@privy-io/react-auth'; const {sendTransaction} = useSendTransaction(); const withdrawAbi = [ { type: 'function', name: 'withdraw', inputs: [ {name: 'assets', type: 'uint256'}, {name: 'receiver', type: 'address'}, {name: 'owner', type: 'address'} ], outputs: [{name: 'shares', type: 'uint256'}], stateMutability: 'nonpayable' } ] as const; const withdrawAmount = parseUnits('50', 18); // 50 USDS const data = encodeFunctionData({ abi: withdrawAbi, functionName: 'withdraw', args: [withdrawAmount, address, address] }); const tx = await sendTransaction({ to: SUSDS_VAULT_ADDRESS as `0x${string}`, data, chainId: CHAIN_ID }); ``` ```tsx theme={"system"} import {encodeFunctionData} from 'viem'; import {useSendTransaction} from '@privy-io/react-auth'; const {sendTransaction} = useSendTransaction(); const redeemAbi = [ { type: 'function', name: 'redeem', inputs: [ {name: 'shares', type: 'uint256'}, {name: 'receiver', type: 'address'}, {name: 'owner', type: 'address'} ], outputs: [{name: 'assets', type: 'uint256'}], stateMutability: 'nonpayable' } ] as const; const data = encodeFunctionData({ abi: redeemAbi, functionName: 'redeem', args: [userShares, address, address] }); const tx = await sendTransaction({ to: SUSDS_VAULT_ADDRESS as `0x${string}`, data, chainId: CHAIN_ID }); ``` *** ## Key integration tips 1. **Always approve first**: Your app must grant ERC-20 approval to the sUSDS vault before any deposit. 2. **USDS uses 18 decimals**: Unlike USDC (6 decimals), USDS uses 18 decimals — use `parseUnits('100', 18)` for amounts. 3. **Use `convertToAssets` for real-time balances**: This ERC-4626 method converts sUSDS shares to their current USDS value, including accrued yield. 4. **Supported chains**: Sky Savings supports Ethereum, Base, Arbitrum, OP Mainnet, and Unichain. *** ## Conclusion Privy makes it straightforward to build secure, user-friendly access to Sky Savings. For advanced use cases, refer to the [Sky Developer Docs](https://developers.skyeco.com), or reach out in [Slack](https://privy.io/slack). Your app is now ready to interact with Sky Savings using Privy embedded wallets! # Using Privy server wallets with yield.xyz AgentKit Source: https://docs.privy.io/recipes/yield/yield-agentkit-guide Give AI agents access to 3,300+ yield opportunities across 80+ blockchain networks. Yield.xyz AgentKit discovers and constructs unsigned transactions for staking, lending, vaults, liquid staking, and restaking. Privy handles wallet creation, policy enforcement, and transaction signing. The two systems never overlap: Yield.xyz never signs, Privy never builds transactions. ## Resources GitHub repository for the Yield.xyz AgentKit MCP server. Official documentation for the Yield.xyz AgentKit. Set up Privy server wallets for autonomous agents. Intents API reference for semi-autonomous workflows. *** ## What is Yield.xyz AgentKit? Yield.xyz AgentKit is an MCP server that provides AI agents with tools to search yield opportunities, build deposit/withdraw/claim transactions, and check balances across 80+ networks. All yield operations go through MCP tools — agents never call the Yield.xyz REST API directly. | Tool | Purpose | | -------------------------------- | -------------------------------------------------------------------- | | `yields_get_all` | Search and filter yields by network, token, type, or provider | | `yields_get` | Retrieve full metadata for a specific yield | | `yields_get_validators` | Fetch validator options for delegation-based yields | | `yields_get_balances` | Check balances and pending actions | | `actions_enter` | Build transactions to deposit or stake into a yield | | `actions_exit` | Build transactions to withdraw or unstake from a yield | | `actions_manage` | Build transactions for pending actions (claim rewards, restake) | | `networks_get_all` | List all supported networks | | `providers_get_all` | List all supported yield protocols | | `yields_get_risk` | Fetch the aggregate risk rating for a yield | | `yields_get_reward_rate_history` | Get the historical APY/reward-rate time series for a yield | | `yields_get_tvl_history` | Get the historical TVL time series for a yield | | `actions_get` | Fetch the status and transactions for a single action | | `actions_get_all` | List past and pending actions for a wallet | | `get_transaction` | Poll the status and details of a single transaction | | `submit_hash` | Register a broadcast hash for an unsigned transaction | | `yields_get_kyc_status` | Check a wallet's KYC and eligibility status for a permissioned yield | Yield.xyz AgentKit is maintained by the Yield.xyz team. For AgentKit-specific issues, refer to their [GitHub repository](https://github.com/stakekit/agentkit). ## Use cases * Search and compare yield opportunities across networks * Enter and exit yield opportunities (staking, lending, vaults, RWAs) * Rebalance and rotate strategies * Claim rewards and manage positions * Monitor portfolios *** ### DeFi yields Access a broad range of yield opportunities across the DeFi ecosystem, including lending protocols, vaults, yield aggregators, and liquidity provisioning on DEXs. Agents can compare protocols using live metrics like TVL, historic APY, and risk scores to make optimal allocation decisions. Supported protocols include Aave, Spark, Sky, Morpho, Curve, Ethena, Lido, Upshift, Marinade, SummerFi, EtherFi, Compound, Maple, Euler, Yo Protocol, Fluid, and more. ### RWA yields Access tokenized real-world assets — US Treasuries, private credit, CLOs, and institutional funds — on-chain. RWA yields are supported across both permissionless and KYC-gated providers, with built-in eligibility handling so agents never submit a transaction to a non-allowlisted wallet. Supported providers include Superstate, Midas, Ondo, Dinari, Nest, and Securitize. **Eligibility gates:** Before entering any RWA position, the agent automatically runs an eligibility check. * **Permissioned providers:** The agent checks KYC status. If the wallet is approved, it proceeds to build and submit the transaction. If the wallet is not yet approved, the agent surfaces the issuer's onboarding URL to complete KYC and stops — no transaction is submitted until the wallet is eligible. * **Open-access providers:** No allowlist gate applies. The agent confirms jurisdiction eligibility and proceeds directly to build and submit the transaction. ### Staking yields Agents can stake across multiple networks, delegate to any active validator, compare commissions and reward rates, and perform all staking actions — stake, redelegate, unstake, and claim rewards. Supported staking opportunities include Ethereum, Hyperliquid, Solana, Polygon, BNB, Avalanche, and others. *** ## Demos **Autonomous — Portfolio Rebalance on Base**
Network: Base · Workflow: Autonomous
Pre-condition: USDC deposited into Aave V3, Morpho, and Fluid via the Privy agent wallet. **Semi-Autonomous — Split Deposit on Solana**
Network: Solana · Workflow: Semi-Autonomous (Enterprise)
Pre-condition: Wallet funded with USDC on Solana, SOL for rent. Key quorum and approver configured. **RWA Autonomous Flow - Dinari Midas Rebalancing** Network: Base · Workflow: Autonomous
Pre-condition: An active position on Midas base *** ## Prerequisites * A Privy account with API credentials ([dashboard.privy.io](https://dashboard.privy.io/)) * `PRIVY_APP_ID` and `PRIVY_APP_SECRET` set as environment variables * Optionally `PRIVY_WALLET_ID` if a wallet already exists * [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude-code/overview) installed * Privy Enterprise plan (required for the semi-autonomous workflow only) This skill does not set up or manage Privy credentials. Privy must already be configured in the agent environment before activation. To set up Privy before continuing: [Agentic wallets guide](/recipes/agent-integrations/agentic-wallets) · [Privy skill repository](https://github.com/stakekit/agentkit) · [Privy dashboard](https://dashboard.privy.io/) *** ## Installation ### Recommended: Claude Code plugin Add the marketplace: ```bash theme={"system"} /plugin marketplace add stakekit/agentkit ``` Install the Privy plugin: ```bash theme={"system"} /plugin install yield-xyz-agentkit-privy@agentkit ``` ### Alternative: skill ```bash theme={"system"} npx skills add https://github.com/stakekit/agentkit ``` The installer prompts for a skill selection (`yield-xyz-agentkit-privy`) and an agent choice (Claude Code, Cursor, or Codex CLI). Once complete, the skill is symlinked into the project at `.agents/skills/yield-xyz-agentkit-privy`. Open the agent in the project directory and run: ``` Set up the yield-xyz-agentkit-privy skill ``` The skill verifies Privy credentials and prompts for a workflow selection: **Autonomous** or **Semi-Autonomous**. *** ## Workflows Privy supports two control models for Yield.xyz AgentKit depending on approval requirements: * **Autonomous** — The agent signs and broadcasts transactions directly via Privy's wallet RPC endpoint. Policies are enforced automatically within the TEE. No manual approval is required. Available on any Privy plan. * **Semi-autonomous (Enterprise)** — Every transaction is submitted as an intent via Privy's Intents API. A designated approver must review and approve each transaction on the Privy dashboard before it executes. Intents expire after 72 hours. ### Autonomous The agent signs and broadcasts transactions directly via Privy's wallet RPC endpoint. Policies are enforced automatically within the TEE. No manual approval is required. Available on any Privy plan. The skill checks that `PRIVY_APP_ID`, `PRIVY_APP_SECRET`, and optionally `PRIVY_WALLET_ID` are present. The skill registers the Yield.xyz AgentKit MCP server: ```bash theme={"system"} claude mcp add --transport http yield-agentkit https://mcp.yield.xyz/mcp ``` The skill offers to attach a policy (conservative, balanced, or skip). Policies restrict per-transaction spend, allowed chains, or allowlisted protocols. All enforcement happens inside the TEE. The skill creates a wallet and attaches the policy if one was configured. Send assets to the displayed wallet address. The agent is ready. Example prompt: `"Deposit USDC into Aave V3 on Base."` ### Semi-autonomous (Enterprise) Every transaction is submitted as an intent via Privy's [Intents API](/transaction-management/overview). A designated approver must review and approve each transaction on the Privy dashboard before it executes. Intents expire after 72 hours if not approved. **Intent statuses:** `pending` (awaiting approval), `executed` (broadcast), `failed` (execution error), `expired` (72h elapsed), `rejected` (cancelled), `dismissed` (resource changed). Follow the credential verification, MCP registration, and policy configuration steps from the autonomous workflow above. Verify the Enterprise plan is active and the approver has been invited and completed MFA. Create a [key quorum](/controls/key-quorum/create) in the Privy dashboard under **Wallet Infrastructure → Authorization → New Key → Register Key Quorum Instead**. Copy the Quorum ID and provide it to the agent. The agent verifies it via `GET /v1/key_quorums/{id}`. The skill creates the wallet with the key quorum as owner: ```bash theme={"system"} POST /v1/wallets chain_type: "ethereum" owner_id: policy_ids: [] ``` Register an HTTPS endpoint under **Configuration → Webhooks** in the Privy dashboard and enable all four intent events: `intent.created`, `intent.authorized`, `intent.executed`, and `intent.failed`. See [Privy intent webhooks](/transaction-management/intents/intent-webhooks). For testing without a backend, use [webhook.site](https://webhook.site/). Once setup is complete, every transaction follows this cycle: agent builds via MCP → submits as intent → approver reviews on dashboard → Privy signs and broadcasts → agent polls until `executed` → reads hash. *** ## Transaction flow Every yield action — enter, exit, or manage — follows the same sequence: 1. Call `yields_get` to inspect the yield schema 2. Call `actions_enter`, `actions_exit`, or `actions_manage` to build unsigned transactions 3. MCP returns an array of transactions with `stepIndex`, `type`, and `unsignedTransaction` 4. For each transaction in `stepIndex` order: * **Autonomous:** `POST /v1/wallets/{id}/rpc` → immediate hash * **Semi-autonomous:** `POST /v1/intents/wallets/{id}/rpc` → poll until `executed` 5. Report results with on-chain confirmation hashes ### EVM transactions Extract only the fields Privy accepts from the unsigned transaction: ```bash theme={"system"} PRIVY_TX=$(echo "$UNSIGNED_TX" | jq '{from, to, value, data, nonce, type} | with_entries(select(.value != null))') ``` Submit to Privy: ```bash theme={"system"} # Example uses Base (chainId: 8453) curl -s -X POST "https://api.privy.io/v1/wallets/$PRIVY_WALLET_ID/rpc" \ --user "$PRIVY_APP_ID:$PRIVY_APP_SECRET" \ -H "privy-app-id:$PRIVY_APP_ID" \ -H "Content-Type: application/json" \ -d "{ \"method\":\"eth_sendTransaction\", \"caip2\":\"eip155:8453\", \"params\": { \"transaction\":$PRIVY_TX } }" ``` ### Solana transactions Convert hex to base64 before submitting: ```bash theme={"system"} TX_BASE64=$(echo "$UNSIGNED_TX" | xxd -r -p | base64) curl -s -X POST "https://api.privy.io/v1/wallets/$PRIVY_WALLET_ID/rpc" \ --user "$PRIVY_APP_ID:$PRIVY_APP_SECRET" \ -H "privy-app-id:$PRIVY_APP_ID" \ -H "Content-Type: application/json" \ -d "{ \"method\":\"signAndSendTransaction\", \"caip2\":\"solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp\", \"params\": { \"transaction\":\"$TX_BASE64\", \"encoding\":\"base64\" } }" ``` ### Multi-transaction ordering Transactions are processed sequentially in `stepIndex` order. For EVM chains, the nonce increments for each subsequent transaction. If any transaction fails, execution stops immediately and the failure is returned. For detailed transaction guides, see [Send EVM transaction](/wallets/using-wallets/ethereum/send-a-transaction) and [Send Solana transaction](/wallets/using-wallets/solana/send-a-transaction). The skill never modifies values inside an `unsignedTransaction`. Amounts, recipient addresses, fees, and calldata are passed through exactly as returned by the MCP. Only structural fields are adjusted for Privy's signing format. *** ## Error handling | Error | Cause | Action | | -------------------- | --------------------------------- | --------------------------------------------------------------------------------- | | `POLICY_VIOLATION` | Transaction exceeds policy limits | Report which rule triggered. Do not retry without user instruction. | | `INSUFFICIENT_FUNDS` | Wallet lacks gas | Ask the user to add native token to the wallet address. | | `FAILED` | Transaction failed on-chain | Provide the hash for block explorer inspection. Stop all subsequent transactions. | *** ## What cannot be automated | Action | Reason | | ---------------------------- | -------------------------------------------------------------------- | | Fund the agent wallet | Requires an existing external wallet | | Invite approver to Privy app | Tied to authenticated user account | | Complete MFA | Device-bound | | Create key quorum | API requires user ID input — kept manual for better UX | | Approve pending intent | By design — this is the point of the workflow | | Upgrade to Enterprise | Billing — done on the Privy dashboard | | Webhook setup | No API available — done in Configuration → Webhooks in the dashboard | *** ## Learn more Skill repository and MCP server source. AgentKit reference documentation. MCP server endpoint and setup. Tokenized real-world asset reference. Wallet setup guide for autonomous agents. Intents API reference. Webhooks overview for transaction events. Wallet and policy management. # User authorization keys Source: https://docs.privy.io/security/authentication/authenticated-signers [Authorization keys](/security/wallet-infrastructure/policy-and-controls) are the core primitive for control of Privy's [Wallet API](/security/wallet-infrastructure/architecture). Authorization key signatures prove that requests are authorized directly by the permitted user. **Self-custodial** wallets are those owned directly by a user. Privy enables users to fully control their wallets by issuing time-bound authorization keys to users who authenticate via a verified JWT. Once users retrieve a time-bound authorization key, they can make requests with the key. This configuration results in cryptographically-enforced user custody of wallets. All Privy client-side SDKs enable **fully user non-custodial wallets by default**. ### Authentication methods Privy integrates directly with any OIDC or JWT-based authentication system and also offers [dozens of login methods natively](/security/authentication/user-authentication), including email, SMS, social login, passkeys, and more. If a user is logged in, they always have access to their wallet. ### Multi-factor authentication Privy also enables multi-factor authentication for access to user authorization keys. Supported additional factors include: * Authenticator apps (TOTP) * Biometric verification (passkeys) * SMS confirmation * Hardware security keys This means your app can require additional user verification for sensitive wallet operations. [Learn more](/authentication/user-authentication/mfa#mfa) ## Direct access via API Directly managing user authorization keys via the API is an advanced setting. We recommend using Privy’s SDKs, which internally manage user authorization keys. Privy enables users to retrieve a **time-bound authorization key directly via a REST API**. This API can be called from either your app's frontend or backend. Privy infrastructure issues authorization keys from within trusted execution environments (TEEs)—see [TEE architecture](/security/wallet-infrastructure/architecture) for more information. Privy integrates with any asymmetric JWT-based authentication system, such as Privy's native authentication system, Auth0, Firebase, or any OIDC or OAuth authentication provider. The architecture works as follows: 1. Your app makes a request to the Privy API using the authentication token from your JWT-based authentication system. 2. The TEE issues a time-bound user authorization key in response. 3. Use the authorization key to authorize requests to the Wallet API. The following diagram illustrates an server-side integration. Note that Privy client-side SDKs fully manage direct client-side integrations. Server-side user authorization keys When you use a Privy SDK to provision and transact with user wallets, the SDK fully manages user authorization keys internally. ### Encryption The returned time-bound authorization key is encrypted from the TEE to the client using HPKE (Hybrid Public Key Encryption), using the same method used by the [wallet export API](/api-reference/wallets/export). # User authentication Source: https://docs.privy.io/security/authentication/user-authentication **Privy's embedded wallets are fully compatible with any authentication provider that supports JWT-based, stateless authentication.** If you're looking to add embedded wallets to your app, you can either: * use Privy as your authentication provider (easy to set up out-of-the-box) * use a custom authentication provider (easy to integrate alongside your existing stack) ## Integrating with any OIDC/JWT-based authentication system Privy integrates with any authentication system that relies on asymmetric JWT tokens. This includes popular authentication providers such as Auth0, AWS Cognito, Firebase, as well as **all** OIDC (OAuth) social providers such as Google, Apple, and Twitter. See the [JWT-based authentication guide](/authentication/user-authentication/jwt-based-auth/overview) for more information. ## Integrating with Privy's native authentication methods Privy's authentication system provides secure user verification out of the box while maintaining a seamless experience. Privy supports multiple verification methods to accommodate different user needs and security requirements: * Email and phone verification using one-time passwords (OTPs) [Learn more](/authentication/user-authentication/login-methods/email) * Social authentication through OAuth2.0 with providers like Google, Apple, Twitter, Discord, Github, TikTok, LinkedIn, Spotify, and Instagram [Learn more](/authentication/user-authentication/login-methods/oauth) * Sign In With Ethereum (SIWE) and Sign in with Solana (SIWS) for web3-native users [Learn more](/authentication/user-authentication/login-methods/wallet) * Custom authentication methods to match your specific needs We do not support regular password-based verification given [users' tendencies to use and reuse easy-to-guess passwords](https://blog.lastpass.com/2021/09/breaking-the-cycle-of-password-reuse/), and the [high incidence of password database breaches](https://haveibeenpwned.com/). ## Token architecture Upon successful authentication, Privy issues two types of tokens that work together to maintain secure user sessions. **Access token** The access token is a [JWT (JSON Web Token)](https://jwt.io/introduction) signed by an asymmetric key specific to your app. This signature cryptographically ensures that only Privy could have produced the token - it cannot be spoofed or tampered with. The token has a **one-hour lifetime**, limiting the impact of potential token exposure and enabling quick session revocation if needed. Your backend can use this token to [validate authenticated requests](/authentication/user-authentication/access-tokens) from users, and the Privy SDK uses it to determine authentication status in your frontend. **Refresh token** To provide longer sessions without compromising security, the refresh token has a **30-day lifetime** but can only be used once. When used to obtain a new access token, it's automatically rotated. This ensures refresh tokens can only renew existing sessions, never create new ones. If the Privy SDK detects any token tampering, it immediately invalidates the session and requires re-authentication. This destroys the corresponding session in Privy's backend. ## Session security Our authentication system includes several security enhancements to protect user sessions. When using a verified domain, tokens can be stored in **HttpOnly cookies** for enhanced protection against XSS attacks. All tokens are cryptographically signed and verified on both client and server. The Privy SDK manages this complexity for you, handling token rotation, renewal, and invalidation automatically. Your backend can easily [validate authenticated requests](/authentication/user-authentication/access-tokens) using the provided access tokens. Learn more about configuring secure authentication for your application in our [security checklist](/security/implementation-guide/security-checklist). # Activity logs Source: https://docs.privy.io/security/implementation-guide/activity-logs Review organization-wide activity logs for Privy Dashboard activity Activity logs provide an organization-wide record of actions taken in the Privy Dashboard. Use them to review changes to organization settings and any app in the organization. ## View activity logs 1. Sign in to the [Privy Dashboard](https://dashboard.privy.io/) 2. Open **Account** 3. Select **Activity logs** Activity logs are also available at [Activity logs](https://dashboard.privy.io/organization?organization=activity). ## What activity logs record Activity logs include account-level activity and activity from every app in the organization. Recorded activity includes: * Team member invitations, removals, and role changes * App creation and promotion * Changes to authentication and security configuration, such as SSO, MFA, allowlists, denylists, and OAuth settings * User and wallet management actions, including wallet archival and restoration * Webhook endpoint creation, updates, deletion, test sends, verification key retrieval, and message resends Each entry identifies the event type, related app when applicable, timestamp, and event details. When available, opening an entry also shows the actor's email address and Dashboard role. Access to activity logs depends on the team member's Dashboard permissions. ## Review app-specific webhook history In addition to organization-level Activity logs, Privy provides **Webhooks history** for apps with a configured webhook endpoint. It provides an app-specific record of outbound webhook messages. The events shown depend on the endpoint's subscriptions. Security-relevant event types include `user.authenticated`, `mfa.enabled`, `mfa.disabled`, `wallet.private_key_export`, `wallet.seed_phrase_export`, and `wallet.recovered`. To view it, select the app in the Privy Dashboard and open **Webhooks history** from the sidebar. Each message shows its event type, Svix message ID, timestamp, and delivery status. Opening a message shows its payload. Filter by event type or search by message ID. Use Activity logs to investigate Dashboard configuration changes. Use **Webhooks history** to investigate app-specific outbound webhook messages. # Guidance for content security policies (CSPs) Source: https://docs.privy.io/security/implementation-guide/content-security-policy New to CSPs? [Skip to CSP Basics](#csp-basics) for an introduction. If you are using Privy in a web client environment, we recommend setting a strict [Content Security Policy (CSP)](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) as a defense-in-depth strategy to mitigate XSS, clickjacking, and cross-site leak vulnerabilities. ## Quick start Remember to add your own domain to the relevant directives (e.g., add your domain to `connect-src`, `script-src`, etc.) ### Base CSP configuration ``` Content-Security-Policy: default-src 'self'; script-src 'self' https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; child-src https://auth.privy.io https://verify.walletconnect.com https://verify.walletconnect.org; frame-src https://auth.privy.io https://verify.walletconnect.com https://verify.walletconnect.org https://challenges.cloudflare.com; connect-src 'self' https://auth.privy.io wss://relay.walletconnect.com wss://relay.walletconnect.org wss://www.walletlink.org https://*.rpc.privy.systems https://explorer-api.walletconnect.com; worker-src 'self'; manifest-src 'self' ``` ```js theme={"system"} const nextConfig = { async headers() { return [ { source: "/:path*", headers: [ { key: "Content-Security-Policy", value: ` default-src 'self'; script-src 'self' https://challenges.cloudflare.com; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; font-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'; child-src https://auth.privy.io https://verify.walletconnect.com https://verify.walletconnect.org; frame-src https://auth.privy.io https://verify.walletconnect.com https://verify.walletconnect.org https://challenges.cloudflare.com; connect-src 'self' https://auth.privy.io wss://relay.walletconnect.com wss://relay.walletconnect.org wss://www.walletlink.org https://*.rpc.privy.systems https://explorer-api.walletconnect.com; worker-src 'self'; manifest-src 'self' `, }, ], }, ]; }, }; ``` ```js theme={"system"} const helmet = require("helmet"); app.use( helmet.contentSecurityPolicy({ directives: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "https://challenges.cloudflare.com"], styleSrc: ["'self'", "'unsafe-inline'"], imgSrc: ["'self'", "data:", "blob:"], fontSrc: ["'self'"], objectSrc: ["'none'"], baseUri: ["'self'"], formAction: ["'self'"], frameAncestors: ["'none'"], childSrc: [ "https://auth.privy.io", "https://verify.walletconnect.com", "https://verify.walletconnect.org", ], frameSrc: [ "https://auth.privy.io", "https://verify.walletconnect.com", "https://verify.walletconnect.org", "https://challenges.cloudflare.com", ], connectSrc: [ "'self'", "https://auth.privy.io", "wss://relay.walletconnect.com", "wss://relay.walletconnect.org", "wss://www.walletlink.org", "https://*.rpc.privy.systems", "https://explorer-api.walletconnect.com", ], workerSrc: ["'self'"], manifestSrc: ["'self'"], }, }) ); ``` ```ruby theme={"system"} Rails.application.config.content_security_policy do |policy| policy.default_src :self policy.script_src :self, "https://challenges.cloudflare.com" policy.style_src :self, :unsafe_inline policy.img_src :self, :data, :blob policy.font_src :self policy.object_src :none policy.base_uri :self policy.form_action :self policy.frame_ancestors :none policy.child_src "https://auth.privy.io", "https://verify.walletconnect.com", "https://verify.walletconnect.org" policy.frame_src "https://auth.privy.io", "https://verify.walletconnect.com", "https://verify.walletconnect.org", "https://challenges.cloudflare.com" policy.connect_src :self, "https://auth.privy.io", "wss://relay.walletconnect.com", "wss://relay.walletconnect.org", "wss://www.walletlink.org", "https://*.rpc.privy.systems", "https://explorer-api.walletconnect.com" policy.worker_src :self policy.manifest_src :self end ``` ## CSP recommendations ### CSP directives for @privy-io/react-auth As part of enforcing a CSP, you will need to allow certain trusted resources that your site needs to load as part of normal operation: If you have a base domain enabled, you must **also** add your domain-specific Privy instance, e.g. `https://privy.your-base-domain.com`. #### Required domains * `child-src` * [https://auth.privy.io](https://auth.privy.io) (Privy iframe) * [https://verify.walletconnect.com](https://verify.walletconnect.com) (WalletConnect iframe) * [https://verify.walletconnect.org](https://verify.walletconnect.org) (WalletConnect fallback iframe) * `frame-src` * [https://auth.privy.io](https://auth.privy.io) (Privy iframe) * [https://verify.walletconnect.com](https://verify.walletconnect.com) (WalletConnect iframe) * [https://verify.walletconnect.org](https://verify.walletconnect.org) (WalletConnect fallback iframe) * `connect-src` * [https://auth.privy.io](https://auth.privy.io) (Privy API) * wss\://relay.walletconnect.com (WalletConnect API) * wss\://relay.walletconnect.org (WalletConnect fallback API) * wss\://[www.walletlink.org](http://www.walletlink.org) (Coinbase Wallet API) * https\://\*.rpc.privy.systems (Privy RPC provider) * [https://explorer-api.walletconnect.com](https://explorer-api.walletconnect.com) (WalletConnect Explorer API) #### Optional features If your app uses Telegram login or linking, add: * `frame-src`: [https://oauth.telegram.org](https://oauth.telegram.org) (Telegram OAuth domain) * `script-src`: [https://telegram.org](https://telegram.org) (Telegram login domain) If your app uses Privy's [funding kit](/financial-flows/deposits/overview), add: * `connect-src`: * [https://api.relay.link](https://api.relay.link) (bridging provider) * [https://api.testnets.relay.link](https://api.testnets.relay.link) (bridging provider, testnets) If your app is on Solana, please add the [Solana cluster endpoints](https://solana.com/docs/core/clusters#on-a-high-level) if an override is not provided: * `connect-src`: * [https://api.mainnet-beta.solana.com](https://api.mainnet-beta.solana.com) * [https://api.devnet.solana.com](https://api.devnet.solana.com) * [https://api.testnet.solana.com](https://api.testnet.solana.com) If your app uses CAPTCHA, please add (based on provider): * `frame-src` * [https://challenges.cloudflare.com](https://challenges.cloudflare.com) (Cloudflare Turnstile) * [https://hcaptcha.com](https://hcaptcha.com) (hCaptcha) * https\://\*.hcaptcha.com (hCaptcha) * `connect-src` * [https://hcaptcha.com](https://hcaptcha.com) (hCaptcha) * https\://\*.hcaptcha.com (hCaptcha) * `script-src` * [https://challenges.cloudflare.com](https://challenges.cloudflare.com) (Cloudflare Turnstile) * [https://hcaptcha.com](https://hcaptcha.com) (hCaptcha) * https\://\*.hcaptcha.com (hCaptcha) * `style-src` * [https://hcaptcha.com](https://hcaptcha.com) (hCaptcha) * https\://\*.hcaptcha.com (hCaptcha) ## Best practices 1. **Start strict**: Begin with restrictive policies, and loosen only as needed. Document all exceptions. 2. **Test regularly**: Test your CSP after dependency updates and validate during deployments. Check compatibility across different browsers. 3. **Monitor**: Track violation reports and monitor performance impact. Watch for bypass attempts. 4. **Document changes and procedures**: Record all CSP changes and document allowed sources. Document testing procedures for your app. ## Testing and deployment We highly recommend testing your CSP thoroughly before deploying and enforcing in production. ### Test your CSP in a staging environment Run through your standard user flows in a **staging** environment with CSP enforcement. This may mean connecting to browser extension wallets / mobile app wallets, transacting, logging out, etc. It is possible that directives need to be updated after Privy SDK upgrades. **Whenever upgrading the Privy SDK, always test your CSP again before deploying the update to production.** Other software you use, such as [MetaMask](https://docs.metamask.io/wallet/how-to/get-started-building/secure-dapp/), may document their own guidance on CSP usage. ### Using Report-Only mode Most browsers support a [`Content-Security-Policy-Report-Only`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy-Report-Only) header, which sends violation reports without actually enforcing policies. This allows the developer to judge whether a modification to their CSP will impact their site's expected functionality. If your policy is strict, you will see many reported violations due to extensions trying to inject scripts into the browser. This is completely normal. It's best to filter these out to avoid the noise. ### Deployment We recommend that you first deploy your CSP in ` report-only` mode with the header `Content-Security-Policy-Report-Only`. Once it has been validated in production, you can migrate to `Content-Security-Policy`, which will enforce directive violations. Going forward, you can deploy with both `Content-Security-Policy-Report-Only` and `Content-Security-Policy` headers set simultaneously. This will allow you to test on the report only header and A/B test against your existing policy. ### Monitoring We recommend that you configure the `report-uri` to see violation/enforcement reports and set up a monitoring dashboard so you can review reports. ## CSP basics A Content Security Policy (CSP) is a set of rules that tell the browser **what sources of content are valid.** CSPs help prevent the browser from executing malicious scripts. They can be used to increase the security of any website. To enable a CSP, you need to configure your web server or backend application to return the `Content-Security-Policy` HTTP header. In that header, you specify a policy. A policy is described using a set of policy directives, each of which tells the browser what to do with respect to a given resource type. ### Example: `img-src` directive For example, the `img-src` directive tells the browser sources of images are valid. If you set this CSP header: ``` Content-Security-Policy: img-src https://my-website.com/ ``` Then any `` from other sites will be blocked: ``` {/* Error! This won't load! */} ``` ### Important directives Policy directives tell the browser what to do for a given resource type. * Keep `script-src` as locked down as possible to prevent malicious code execution * Set `frame-ancestors` to `none` unless you expect your website to be embedded * Keep `connect-src` as locked down as possible to prevent unauthorized data exfiltration * Use `child-src` and `frame-src` to control iframe loading and execution * Consider `worker-src` if using web workers * Implement `default-src` as a fallback for unlisted directives ### Read the following guides to learn more: * [https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP](https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP) * [https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Security-Policy) * [https://cheatsheetseries.owasp.org/cheatsheets/Content\_Security\_Policy\_Cheat\_Sheet.html](https://cheatsheetseries.owasp.org/cheatsheets/Content_Security_Policy_Cheat_Sheet.html) # IP allowlist Source: https://docs.privy.io/security/implementation-guide/ip-allowlist The IP allowlist restricts server-to-server API access to specific IP addresses and CIDR ranges. When enabled, only requests from allowlisted IP addresses can authenticate using the app secret. This protects against unauthorized API access if credentials are compromised. The IP allowlist only applies to server-to-server requests using Basic authentication with your app secret. User authentication and dashboard access are not affected by this setting. ## How it works When your server makes an API request using Basic authentication (app ID and app secret), Privy validates the request's source IP address against your configured allowlist: * If the allowlist is **empty**, all IP addresses are permitted (feature disabled) * If the allowlist **contains entries**, only matching IP addresses can complete the request * Non-matching requests receive a `403 Forbidden` error ## Supported formats The IP allowlist supports three types of entries: | Format | Example | Description | | ------------ | ------------- | ------------------------- | | IPv4 address | `192.168.1.1` | Single IPv4 address | | IPv6 address | `2001:db8::1` | Single IPv6 address | | CIDR range | `10.0.0.0/8` | IP range in CIDR notation | Use CIDR notation to allowlist entire IP ranges. For example, `192.168.1.0/24` allows all addresses from `192.168.1.0` to `192.168.1.255`. ### IPv6-mapped IPv4 addresses IPv6-mapped IPv4 addresses (e.g., `::ffff:192.168.1.1`) are automatically normalized to their standard IPv4 format for comparison. This ensures consistent matching regardless of how the client IP is reported. ## Configure the IP allowlist Configure the IP allowlist in the [Privy Dashboard](https://dashboard.privy.io/apps?page=settings) under **Configuration > App settings**. ### Add IP addresses 1. Navigate to the **IP allowlist** section in your app settings 2. Enter IP addresses or CIDR ranges, one per line 3. Save your changes Before enabling the IP allowlist, ensure your server's IP addresses are added. Adding entries to an empty allowlist immediately enables IP restrictions, which may block your existing integrations. ## Error handling When a request originates from a non-allowlisted IP address, the API returns a `403 Forbidden` error with a generic message. This prevents IP enumeration attacks by not revealing whether the IP allowlist is enabled or which IPs are allowed. ## Best practices ### Use CIDR ranges for cloud providers Cloud infrastructure often uses dynamic IP addresses. Configure CIDR ranges for your cloud provider's IP ranges rather than individual addresses: * For AWS, use the published [IP address ranges](https://docs.aws.amazon.com/general/latest/gr/aws-ip-ranges.html) * For Google Cloud, use [Cloud NAT](https://cloud.google.com/nat/docs/overview) with static IPs * For Azure, configure [outbound IP addresses](https://learn.microsoft.com/en-us/azure/virtual-network/ip-services/default-outbound-access) ### Test before enabling Before adding entries to an empty allowlist: 1. Identify all IP addresses your servers use for outbound requests 2. Test the IP addresses using a staging environment 3. Add all required IP addresses before enabling ### Monitor for blocked requests After enabling the IP allowlist, monitor your application logs for unexpected authentication failures. Blocked requests may indicate: * Missing IP addresses in the allowlist * Infrastructure changes that modified your outbound IP # Security checklist Source: https://docs.privy.io/security/implementation-guide/security-checklist Privy is a powerful library that enables you to provision powerful non-custodial embedded wallets in order to create delightful user experiences. Privy protects your users' accounts and wallets via secure account verification, session management, and key sharding cryptography. See the [architecture security documentation](/security/wallet-infrastructure/architecture) for more information. Before deploying Privy in production, there are several important security configurations to consider. Beyond this, security is a comprehensive topic that touches every part of your stack. ## Secure your client environment Because your application client provides the context in which users access their accounts, it is an essential environment to keep secure. Follow client-side security best practices, including limiting what is able to inject Javascript into your site. **You should make sure only the code you intend runs in your app.** ### Web integrations If you use Privy in your web application, including mobile web, we recommend configuring the following security settings. #### Restrict allowed domains Configure your allowed domains to prevent unauthorized access to your Privy integration. * Add your production domain in the [Configuration > App settings page](https://dashboard.privy.io/apps?setting=domains\&page=settings) of the Privy Dashboard. [Learn more](/recipes/dashboard/allowed-domains) * Remove any test or development domains Using domains not configured in your allowed domains list will cause your integration to fail. This is an important security measure that protects your users. #### Configure HttpOnly cookies To enable HttpOnly cookies for enhanced security, you can verify your domain ownership through a simple setup process in the Privy dashboard. [Learn more](/recipes/dashboard/allowed-domains) #### Security headers Configure proper security headers: * Implement a strict [Content Security Policy](/security/implementation-guide/content-security-policy) * Configure appropriate CORS settings * Set secure cookie attributes when using HttpOnly cookies ### Mobile integrations If you use Privy in your native mobile application, we recommend configuring the following security settings. #### Restrict allowed native app IDs Set your mobile project's bundle identifier as the required native app identifier. ## Set up authentication If you have integrated user authentication with Privy wallet infrastructure, we recommend the following authentication settings. **Authentication security starts with choosing appropriate methods for your application.** Consider your users' needs and security requirements when configuring these settings. Read more about our [authentication architecture](/security/authentication/user-authentication). ### **Login methods** For high-value applications, we recommend that you: * Require [MFA](/authentication/user-authentication/mfa/overview) with either a passkey or authenticator app for any app using delegated login (OAuth, email OTP, SMS) as the primary auth method. Account access is wallet access, so a compromised delegated credential directly exposes wallet funds. * Disable SMS-based authentication to prevent SIM-swapping attacks * Configure appropriate session duration. The default is 30 days. You can do this using [app clients](/basics/get-started/dashboard/app-clients) These security settings can be configured in your Privy dashboard. The defaults are chosen to balance security and user experience, but you may want to adjust them based on your specific needs. ### **OAuth configuration** If using social login, ensure proper configuration: * Set up [allowed OAuth redirect URLs](/recipes/react/allowed-oauth-redirects) * Review OAuth scopes and permissions * Enable only necessary social providers * Monitor OAuth token security ## Protect your wallets Wallet security requires careful consideration of your specific use case and threat model. Learn more about our [wallet security architecture](/security/wallet-infrastructure/architecture). ### Embedded wallets For wallets that users interact with directly through your application, we recommend enabling increasingly strict security settings as account value increases. #### **High-value assets** When protecting significant value, implement multiple security layers: * Require [MFA](/authentication/user-authentication/mfa#mfa) for all sensitive operations * Enable user-managed [recovery](/wallets/advanced-topics/new-devices/provision-new-devices) through password or cloud backup * Set up emergency contacts and procedures #### **Standard use cases** For typical wallet usage: * Enable users to optionally configure [MFA](/authentication/user-authentication/mfa#mfa) * Configure automatic recovery with appropriate login methods * Implement user education about security best practices ### Secure server-controlled wallets #### Authorization keys Set an owner on the wallet to add an additional layer of security for transaction signing. This means your transaction requests must be authorized with two factors: 1) your Privy app secret and 2) a signature from an [authorization key](/controls/authorization-keys/overview), which is a cryptographic key that only your service has access to. Once this is set up, implement key management controls: * Use a hardware-backed KMS (key management system) such as AWS KMS to secure authorization keys. Hardware-backed KMS systems disallow any export of keys. * We recommend rotating authorization keys regularly, by updating the owner on wallets to a new key on a regular basis (every 90-180 days). * You can further segregate wallets by setting different keys as the owner on different wallets. This means a given authorization key may only transact on one or a subset of wallets. * You can require additional authorization for transactions, by requiring a quorum of authorization keys to approve a transaction. For example, you may set a 2-of-2 key quorum as the owner of a wallet: one key held in a KMS, and one key held by your service. * We strongly recommend backing up authorization keys for redundancy. Privy does not have access to authorization keys and cannot recover your authorization key if you lose it. #### Least privilege access **Recommended Security Practice**: A powerful security control your application can implement is to separate the keys used for transaction signing from the keys used for [policy management](/controls/policies/overview). This ensures that even if your backend is compromised and transaction signing keys are exposed, the attacker cannot modify or remove the policies that constrain the wallet's behavior. To implement this separation: 1. Create two different signing keys: one for **managing wallets and policies** and one for **transaction signing** * The management key should be used rarely and only be accessed in a very restricted environment * The transaction signing key will be used frequently and will accessed by your core application 2. When creating a wallet or policy, set the management key as the `owner`, which will make its signature required for any updates 3. On your wallet, set your transaction signing key and the policy it is subject to as an [`additional_signer`](/api-reference/wallets/create#body-additional-signers) This creates a robust security boundary where: * Transaction signing keys can only operate within policy constraints * Policy management keys are rarely used and can be stored with higher security * A compromise of transaction signing infrastructure cannot escalate to policy modification The keys above can be quorums (e.g., 2-of-3 keys), providing additional security through multi-party authorization requirements. Learn more about implementing policies in our [policy overview documentation](/controls/policies/overview). #### Other security recommendations In addition to securing your server-controlled wallets with authorization keys, we also recommend the following: * Set a [policy](https://docs.privy.io/controls/policies/overview) on the wallet to limit the types of transactions that may be processed. * Monitor API usage and implement rate limiting * Set up alerts for unusual activity * Use separate development and production credentials * Implement proper logging and audit trails ### Secret scanning We recommend using a secret scanning tool to detect accidental disclosure of sensitive credentials in from your project. Privy authorization private keys generated since January 2025 match the regex `wallet-auth:[A-Za-z0-9+/]{16,}`. Older private keys may have a prefix of `wallet-api:` or no prefix. Privy app secrets generated since mid December 2025 match `privy_app_secret_[A-Za-z0-9]{16,}`. Older app secrets have no prefix. # Security Source: https://docs.privy.io/security/overview **The security of your users' data and digital assets is our top priority at Privy.** We secure over 120 million users' wallets and enable over 15 billion dollars in monthly transaction value through our secure, flexible infrastructure. Privy wallets are non-custodial and have a fully programmable control model. Privy's flexible configuration enables the full custody spectrum from user-custodial wallets to powerful service-controlled accounts. ## Our security approach At Privy, we've built our security foundation on unwavering principles. Our systems are non-custodial by design, ensuring that only authorized users can access their keys through sophisticated key splitting and secure execution environments. We implement defense in depth, with multiple independent security boundaries protecting your users' assets—from cryptographic guarantees to hardware-level isolation. We believe security requires constant vigilance. We maintain continuous validation through regular third-party audits, an active bug bounty program, and 24/7 security monitoring to ensure our systems remain secure as threats evolve. ## Core architecture The strength of Privy's security comes from our battle-tested approach to protecting sensitive operations and data: **Trusted execution environments (secure enclaves)** Sensitive wallet operations take place within Trusted Execution Environments (TEEs), also known as secure enclaves. TEEs are highly restricted compute environments that offer deep system isolation guaranteed by the processor itself. In particular, Privy uses [AWS Nitro Enclaves](https://docs.aws.amazon.com/enclaves/latest/user/nitro-enclave.html). **Key sharding and cryptography** We use robust, scalable cryptographic techniques to shard private keys across separate security boundaries, ensuring they are never stored in complete form and can only be accessed by authorized parties. Privy's cryptosystem design ensures sensitive operations remain protected even if the surrounding system is compromised. ## Security validation We regularly validate our security through comprehensive assessments: * Multiple independent security audits from firms including Cure53, Zellic, and Doyensec * SOC2 Type I and Type II compliant * Active bug bounty program on HackerOne * 24/7 incident response with rapid response SLAs Our commitment to security extends to transparency—our cryptographic implementations are open-source and have undergone dedicated third-party audits, available on our [GitHub repository](https://github.com/privy-io/shamir-secret-sharing). ## Getting started Our documentation will guide you through implementing Privy securely in your application. We recommend starting with our [security checklist](/security/implementation-guide/security-checklist) for a complete overview of security best practices, or diving into our [architecture details](/security/wallet-infrastructure/architecture) to learn more about our security model. Security researchers can learn more about our vulnerability disclosure program at [privy.io/vulnerability-disclosure](https://www.privy.io/vulnerability-disclosure) or reach out to [security@privy.io](mailto:security@privy.io). # Threat models & security FAQ Source: https://docs.privy.io/security/security-faqs Threat models are an essential part of building secure systems. Establishing a threat model means understanding the robustness of a system against a given attacker and context. At Privy, we work to communicate these threat models clearly so developers and users can protect themselves and their assets effectively. We break down some threat models below. Please reach out to us at [security@privy.io](mailto:security@privy.io) if you have any questions. As a reminder, Privy works to secure user assets and data in three main ways: * **Proactive security**: Privy systems are engineered and built with security in mind. This means resource isolation and [cryptographic architecture](/security/overview) layered with a defense-in-depth approach, designed to protect your wallets. This also means doing cryptographic and infrastructure audits on a quarterly basis, as well as running a Vulnerability Disclosure Program and active Bug Bounty Program. * **Active monitoring**: Privy systems are instrumented for active monitoring. This means automated alerts triggered by unexpected or abnormal activity and an on-call engineering team on standby 24/7. As our customers deploy apps, we work to monitor activity across the threat landscape online and collaborate with service providers to take down malicious threats. * **Defensive measures**: Privy is built with failsafes to enable developers and their users to cut off access to key material in the event of an emergency. We work on pre-approved procedures for such instances with our enterprise customers and are always at the ready to protect user assets in the case of attack. If you're a researcher interested in participating in our Bug Bounty or you believe you've detected a malicious threat relevant to Privy's work, please reach out to [security@privy.io](mailto:security@privy.io). ## Security philosophy Security is continuous work, not a one-time achievement. We recognize that [wallets are not one size fits all](https://www.privy.io/blog/metrocards-and-bank-vaults), and we build highly configurable, flexible wallet infrastructure so you can configure the system appropriate for your use-case. Moreover, security needs evolve as asset value grows. We give developers flexibility to build appropriate experiences while guiding them toward security best practices. We support the full spectrum from email-based embedded wallets to hardware-secured cold storage, recognizing the [inherent tradeoffs](https://www.privy.io/blog/embedded-wallet-architecture-breakdown) in any cryptosystem. ## Understanding threat models The below summarizes some key questions but is not exhaustive. Please reach out for a deeper discussion on threat modeling or other attack strategies. ### Cross-application security #### Q: Can unauthorized applications access the Privy iframe? No, as long as your administrators configure your app with the proper [allowed origins](/recipes/dashboard/allowed-domains) in the Privy dashboard. Privy has a permissive default to bootstrap the rapid development of apps, but when taking an app to production and the allowed origins are set, the served iframe enforces that all frame ancestors must be within those allowed origins. This is enforced by both frame ancestor CSP checks and in-code origin validation. #### Q: Can unauthorized applications send messages to the Privy iframe? No, as long as your administrators configure your app with the proper [allowed origins](/recipes/dashboard/allowed-domains) in the Privy dashboard. The Privy iframe only accepts messages from its parent frame, which is usually under the control of the application developer. The iframe message handler checks the origin of messages received and confirms they are from an approved parent origin. Additionally, the Privy iframe requires a valid access token to authenticate messages received from its parent frame. #### Q: Can a Privy customer's application interfere with another customer's iframe? No. Browser security controls and DOM boundaries isolate one customer's iframe from another customer's application context. Iframes are separated by the browser's same-origin policy and process isolation mechanisms, so they do not share DOM or memory across origins. ### User security #### Q: Can an unauthorized user access another user's wallet? No. A valid access token is required to access a wallet. Specifically, the user's access token is required to retrieve the auth share needed to reconstruct the wallet. Access tokens are only granted to authenticated users and as per the application's configuration. Here's an [example recipe](/recipes/react/cookies) of how to configure a React-based application to use cookies. #### Q: How are users protected if their browser is compromised? A compromised browser cannot reconstruct a private key unilaterally. For TEE-based embedded wallets, no complete share exists on the device: one share is only accessible inside our Amazon Nitro Enclave-based TEE, and the other is held encrypted by Privy's authentication service, each useless without the other. Additional layers of protection include: * **MFA for wallet operations**: developers can require users to re-authenticate before any signing operation, raising the bar for session hijack attacks * **Access token revocation**: compromised sessions can be invalidated immediately, preventing further signing requests from completing * **Key never persisted in full**: the private key is only transiently available inside the TEE during signing and is never written to disk or local storage in any form * If a compromise is detected, developers should revoke the user's access token and, for high-value wallets, prompt the user to re-authenticate and rotate their recovery configuration. ### Browser security #### Q: Can bookmarklets and browser extensions inject malicious Javascript into the iframe? In certain cases, yes. There is a CSP nonce on the embedded wallet iframe and the embedded wallet key export page. This means browsers are able to verify the iframe code via a server-set nonce, and additionally reject unauthorized code. We block extensions with CSPs that violate the unsafe eval directive. However, it's important to understand that bookmarklets and extensions have elevated permissions and may have access to things such as browser requests and responses. According to the W3C CSP standard, browser implementations should allow user-agent features to override policies. Browsers enable bookmarklets and extensions to bypass CSP settings and inject Javascript code onto pages. We recommend educating users to not install untrusted bookmarkets and browser extensions. Furthermore, we recommend enabling wallet MFA which requires the user to MFA to approve transactions. #### Q: What happens if browser security is compromised? We maintain multiple layers of protection: * Emergency kill switches for immediate response * Access token revocation capabilities * Geographic access restrictions * Rapid incident response procedures * Regular security updates ### Infrastructure security #### Q: Can a compromised Privy team member access user keys? No. Keys exist only as encrypted shares distributed across security boundaries. Wallet actions are only accessible within secure execution environments. #### Q: Can a compromised engineer deploy unauthorized code? No. Privy maintains a robust deployment security system with multiple independent controls. Code deployed to secure execution environments undergo extensive review and security controls, including strict multi-party approvals. All code changes require review from multiple designated owners, must pass automated security testing, and go through staged deployments with additional approvals. The Privy CI/CD pipeline ensures build artifacts are deployed directly from protected source code, with branch protection rules and signing requirements. This process is regularly audited and monitored to prevent unauthorized modifications. ### Custody and control #### Q: Does Privy hold my users' private keys? No. Privy never stores a complete private key. When a wallet is created, the private key is generated inside a trusted execution environment (TEE), immediately split into two shares via Shamir's Secret Sharing, and discarded. One share (the enclave share) is sealed to Amazon Nitro Enclave hardware and cannot be decrypted outside it. The other (the auth share) is encrypted at rest and released only when the user authenticates. Neither share alone is sufficient to reconstruct the key, and Privy cannot do so unilaterally. #### Q: Who controls the private keys in a Privy embedded wallet? The user does. Privy's embedded wallets use a 2-of-2 key-share architecture where both shares are required to produce a signature. The enclave share is inaccessible outside the TEE hardware, and the auth share is gated behind the user's authenticated session. In this default configuration, a signature can only be produced when the user provides valid authentication, which triggers time-bound key reconstruction inside the TEE. The key is wiped immediately after signing. Note: developers can optionally configure session signers or agent signers that allow a server to sign within policy constraints without per-transaction user approval -- in those configurations, signing authority is shared between the user and the developer's backend according to the policies set. #### Q: Is Privy a custodial or non-custodial wallet solution? Privy supports both models depending on wallet type and configuration. Embedded wallets are non-custodial (self-custodial) by default -- user custody is cryptographically enforced by the 2-of-2 key-share architecture, and neither Privy nor any single infrastructure provider can sign a transaction unilaterally. However, Privy also offers server wallets (developer-controlled, no end-user in the signing loop) and agent-controlled wallets where the developer's backend operates autonomously within policy constraints. The custody model is a developer choice, not a platform-wide absolute. #### Q: Can Privy access or move funds from my users' wallets? Not unilaterally. For TEE-executed embedded wallets, Privy stores an app share and a TEE share encrypted to the enclave's public key; the encrypted TEE share can be decrypted only by the authorized TEE. When necessary, key material is reconstructed transiently inside the TEE to sign a transaction. The TEE enforces the wallet's configured authorization and policy controls, so Privy cannot retrieve a usable private key or sign arbitrary transactions. If a developer configures session signers, agent signers, or other delegated permissions, those signers can execute transactions within their granted scope without per-transaction user approval. This is delegated authority configured for the wallet, not a general ability for Privy to move funds at will. ### Centralization #### Q: Does Privy create a single point of failure for key management? No. The 2-of-2 Shamir share architecture means compromising any single component -- Privy's storage, the TEE provider, or the user's authentication -- is insufficient to reconstruct a private key. An attacker would need to simultaneously compromise both the Amazon Nitro Enclave hardware and Privy's auth share storage while also possessing valid authentication credentials. For wallets configured with session signers or agent signers, the developer's backend holds additional signing authority scoped by policies -- but this authority is explicitly granted by the developer and constrained by Privy's policy engine, not a bypass of the key-share model. #### Q: How does Privy prevent unauthorized access to private keys, including by Privy itself? By design, no single Privy system or employee can reconstruct a private key. For TEE-backed wallets, key material is split across separate trust boundaries: Privy stores the app/auth share and an encrypted TEE share, but the plaintext TEE share can only be decrypted inside an attested AWS Nitro Enclave. Full private keys are reconstructed only temporarily inside the enclave after authentication and policy checks pass. Privy uses both code-level and hardware-level controls for this path. Enclave code changes go through multi-party review, automated security testing, signed build controls, and staged rollout. Nitro attestation then verifies the enclave's measured boot state before secrets are available, ensuring key material is only handled by approved code running in the expected hardware-isolated environment. #### Q: What is Privy's trust model -- does it require trusting Privy as an intermediary? Privy's trust model is "trust but verify with cryptographic guarantees." You trust that the TEE is running attested code (verifiable via Amazon Nitro attestation) and that Shamir's Secret Sharing is correctly implemented. You do not need to trust that Privy employees will choose not to access keys -- the architecture makes it impossible for them to do so, regardless of intent. ### Portability and lock-in #### Q: Can users export their private keys from Privy? Yes. Users can export their full private key via the client SDK key export flow or the REST API (POST /wallets//export). The exported key is a standard elliptic-curve private key (secp256k1 for EVM chains, ed25519 for Solana) that works with any wallet provider, hardware wallet, or self-hosted solution. #### Q: Can I migrate my users' wallets away from Privy? Yes. Because Privy wallets use standard key formats, exported keys are portable to any provider. For server wallets, developers can export keys programmatically via the API. The wallet addresses remain the same after export since they are derived from the same underlying keypair. #### Q: What data and key material can developers port if they leave Privy? Developers can export: private keys (full elliptic-curve keys in standard format), wallet addresses, and user identity mappings. Transaction history lives onchain and is inherently portable. Authentication data (linked emails, social accounts) can be mapped to a new provider during migration using Privy's user export capabilities. #### Q: Am I locked into Privy's infrastructure once I integrate? No. Privy uses standard key cryptography (secp256k1, ed25519), standard chain protocols, and standard authentication patterns. There is no proprietary key format, no custom chain requirement, and no contractual lock-in on key material. The key export API exists specifically to ensure developers and users can leave at any time with full control of their assets. For security questions not covered here or to report a security concern, contact us at [security@privy.io](mailto:security@privy.io). If you're a security researcher interested in our Bug Bounty Program, please reach out to the same address. # On device execution environment Source: https://docs.privy.io/security/wallet-infrastructure/advanced/user-device Privy's security architecture leverages secure execution environments to protect your users' assets. Wallet private keys are only temporarily reconstructed within these strictly isolated, secure execution environments when needed for specific operations, under the wallet owner's control. Privy provides two types of secure execution environments: 1) via TEEs and 2) on the user's device. Each environment ensures that private keys are never stored in complete form and are only temporarily reconstructed when needed. By default, Privy uses [trusted execution environments (TEEs)](/security/wallet-infrastructure/architecture), also known as secure enclaves, for secure wallet operations. As an advanced setting, Privy also enables wallets to be reassembled **directly on user devices**. On-device execution is an advanced configuration. Please [reach out](https://privy.io/slack) to enable this setting. * On-device execution enables the fastest-possible signing speed (5 ms), but involves a more limited feature set. * If you have on-device execution enabled, you will see "On-device" as the Wallet environment in your app's Wallet > Advanced settings page. Otherwise, your app uses TEE execution. * You can [migrate from on-device to TEE execution](/recipes/tee-wallet-migration-guide). Apps may only operate in one environment. ## Browser-isolated execution environments on user devices With on-device execution, Privy secures wallets directly on user devices using browser-enforced isolation via iframes. This relies on the same browser security boundaries that have been battle-tested for decades, securing billions of dollars in daily financial transactions across the modern internet. The Privy iframe runs in a separate process with its own isolated memory space, completely separated from your application. This isolation is enforced by: * Hardware-level memory protection * Browser process separation * Strict origin and frame ancestor validation * Content Security Policy controls that strictly lock down network access Browser security boundaries have been battle-tested for decades, securing billions of dollars in daily financial transactions across the modern internet. ## Key shares Privy's security model is based on distributed key sharding. This means critical key entropy is split into encrypted shares, protected by separate security boundaries. With on-device execution, there are three share types: * **Device share**, which is persisted on the user's device. In a browser environment, this is stored in the browser's domain-partitioned local storage via the iframe. * **Auth share**, which is encrypted and stored by Privy. This share is accessible only with valid user authentication. * **Recovery share** is used to provision the wallet on new user devices. This share is encrypted and secured either through user-managed methods (password or cloud backup) or Privy's recovery key management system. **Two shares** must be present to reconstruct the private key, which only happens temporarily within the iframe on the user's device. Typical operation involves sets of **2-of-2 shares**, where a device-specific share and an auth share are provisioned for each device on which a wallet is used. Similarly, a recovery share and recovery-specific auth share are provisioned to enable recovery on new devices. Wallet key shares in on-device execution ### Securing the recovery share Privy offers two approaches to securing the recovery share: **Automatic recovery** Privy's key management system secures the encrypted recovery share, allowing users to provision their wallet on new devices through normal authentication. Privy infrastructure ensures only the user can decrypt their recovery share on their device. When using automatic recovery, you are trusting Privy's infrastructure to secure the user's recovery share, and the user's authentication token as the sole root of trust for their wallet. **User-managed recovery** With user-managed recovery, the recovery share is encrypted via a recovery factor managed by the user. This takes two forms: * **Passwords**: users can set a strong memorable password to secure the recovery share for their wallet. Privy has no knowledge of the user's password and cannot decrypt the recovery share. * **Cloud-backup**: the recovery share is secured by a recovery decryption key that is backed up to the user's cloud storage account (e.g. Google Drive or iCloud). Privy cannot access this backup and cannot decrypt the recovery share. ## Key management operations ### Creating a wallet When a user creates a wallet, the secure execution environment generates strong entropy (128 bits) from a cryptographically secure random number generator (CSPRNG). This is converted to a mnemonic using BIP-39, from which Privy derives the wallet's public key and private key. All Privy wallets are [hierarchical deterministic (HD) wallets](https://help.myetherwallet.com/en/articles/5867305-hd-wallets-and-derivation-paths). Immediately after creation, the wallet entropy is sharded into key shares, and the key shares are encrypted and distributed across separate security boundaries. This ensures that wallets can never be accessed outside of the secure execution environment. ### Signing a transaction Two shares must be present to reconstruct the private key. During regular operation, Privy reassembles the wallet using a **device share** and **auth share**. A device-specific share and an auth share are provisioned for each device on which a wallet is used. In other words, when signing a transaction: 1. Your application passes the transaction data through the Privy SDK 2. The secure iframe validates authentication and retrieves necessary encrypted shares 3. Key reconstruction occurs only in the iframe's isolated memory 4. The key is used temporarily in-memory for cryptographic signing 5. Only the signature is returned to your application Because Privy wallets are provisioned directly on user devices, cryptographic signing is extremely fast (5 ms). Signing a transaction ### Provision new devices Users provision their wallet on a new device using the **recovery share** and **auth share**. This set of recovery shares is created on initialization of a new wallet. When a user accesses your app on a new device, the iframe will retrieve the **auth share** for your user during the login process. Then, depending on how you've configured recovery, the iframe will decrypt the **recovery share** for your user by: * requesting the recovery decryption key using the user's auth token, if using **automatic** recovery * having the user decrypt the key using their recovery factor (password or cloud account), if using **user-managed** recovery With the **auth share** and the **recovery share**, the iframe provisions a new **device share** for the new device. This device share allows your user to continue using the wallet on that device. Learn how to provision new devices in our [docs](/wallets/advanced-topics/new-devices/overview). Provision a new device ## External key recovery With Privy's architecture, a user is able to recover their private key even if they lose their device or if they lose access to your app. * If the user loses access to their device and is unable to retrieve their **device share**, they can combine their **auth share** and decrypt their **recovery share** to reconstitute the full private key. * If the user loses access to your app and is unable to retrieve their **auth share**, Privy enables an external recovery service so that **users are always able to export their wallet**. In all of these cases, Privy rotates keys to ensure compromised devices or authentication methods cannot be combined to maliciously reconstitute the private key. # Security architecture Source: https://docs.privy.io/security/wallet-infrastructure/architecture Privy's security architecture combines trusted execution environments (TEEs) with distributed key sharding to protect your users' assets. Simply put: * Keys are only stored as **encrypted shares distributed across separate security boundaries.** * Keys are only **temporarily reconstructed within trusted execution environments** when needed for specific operations, under the wallet owner's control. ## Concepts ### Trusted execution environments Trusted execution environments (TEEs), also known as secure enclaves, are highly restricted, isolated compute environments that allow for secure code execution and cryptographic verification (attestation) of the code being executed. In particular, Privy uses [AWS Nitro Enclaves](https://docs.aws.amazon.com/enclaves/latest/user/nitro-enclave.html). Privy uses TEEs to support private key reconstruction for the following processor-level guarantees: * Enclaves have no persistent storage, no interactive access, and no network connectivity, and so provide a secure, isolated compute environment for sensitive data. Private keys for wallets are only accessible within the enclave, and can only be used to produce signatures compliant with the policies attached to the wallet. * Attestations are cryptographic verifications of the computation run on a TEE. They are signed hashes of code on an enclave that can be verified with the corresponding public key, and can be used to verify actions run within the TEE. ### Key shares Privy's security model is based on distributed key sharding. This means critical key entropy is split into encrypted shares stored across separate security boundaries. Key sharding enables future-proof flexibility, strict security isolation, and built-in redundancy. In particular, key sharding enables separate authentication and encryption of each distributed share, enforcing control by wallet owners. Key sharding and assembly only ever occur within the trusted execution environment. Private keys are split into encrypted shares using a reliable, battle-tested, and fast cryptographic algorithm called [Shamir's secret sharing (SSS)](https://en.wikipedia.org/wiki/Shamir%27s_secret_sharing). No share in isolation provides any information or access to the wallet. Privy's [`shamir-secret-sharing`](https://github.com/privy-io/shamir-secret-sharing) cryptography library is open-source, heavily audited, and used to secure millions of wallets. It is the most widely used open-source Typescript library for Shamir's secret sharing. When a wallet is created, it is split into two shares, protected by different security boundaries: 1. **Enclave share**, also referred to as the TEE share, which is secured directly by the trusted execution environment and encrypted with the TEE's cryptographic key. The enclave share can only be decrypted within the TEE. 2. **Auth share**, which is encrypted and stored by Privy. This share is accessible only with valid authentication credentials, e.g. a bearer token or secret, and is sent to the enclave whenever an action is requested from the wallet. Trusted execution environment key shares This is a **2-of-2** share set, which means that *both* shares are required in order to generate signatures. Neither the auth share nor the enclave share in isolation provide any information or access to the wallet. Only the TEE can decrypt the enclave share and combine it with the auth share to temporarily reconstitute the wallet and execute actions. ## Key management operations ### Wallet creation When a wallet is created, the trusted execution environment generates strong entropy (128 bits) from a cryptographically secure random number generator (CSPRNG). This is converted to a mnemonic using BIP-39, from which Privy derives the wallet's public key and private key. All Privy wallets are [hierarchical deterministic (HD) wallets](https://help.myetherwallet.com/en/articles/5867305-hd-wallets-and-derivation-paths). Immediately after creation, the wallet entropy is sharded into key shares, and the key shares are encrypted and distributed across separate security boundaries. This ensures that wallets can never be accessed outside of the TEE. Private keys only exist in complete form temporarily within the trusted execution environment during signing operations. At all other times, they remain split into encrypted shares stored across separate security boundaries. ### Wallet transaction When a wallet transaction is requested, the wallet private key is reconstituted temporarily in-memory within the trusted execution environment. Two shares must be present to reconstruct the private key, the **enclave share** and **auth share**. The private key does not persist beyond usage for the wallet operation. This process ensures: * Keys exist only as encrypted shares stored across separate security boundaries * Shares are only combined temporarily within the secure environment for specific operations * Network access is strictly controlled * Every operation requires proper authentication In more detail, when signing a transaction: 1. Your app or service makes a `POST` request to the Privy API with the appropriate API credential (bearer token or app secret) and an authorization key signature. 2. The Privy API authenticates the API credential. If the request is valid, the request is forwarded to the TEE, along with the auth share. 3. The TEE verifies authorization and policies. The authorization signature from the request is verified against the authorization public key. 4. The TEE decrypts the encrypted device share and combines it with the auth share to reconstruct the wallet's private key. 5. The key is used temporarily in-memory for cryptographic signing. 6. The transaction signature is returned to the caller. Privy also supports broadcasting the signed transaction to the blockchain, directly from the API. Transaction flow ## Protecting code deployments to the trusted execution environment Privy enforces strict controls of the code deployments within the trusted execution environment. Code deployed to the TEE undergoes extensive review and security controls, including strict multi-party approvals and hardware security key requirements. All code changes require review from multiple designated owners, must pass automated security testing, and go through staged deployments with additional approvals. The Privy CI/CD pipeline ensures build artifacts are deployed directly from protected source code, with branch protection rules, security scanning, and signing requirements. This process is regularly audited and monitored to prevent unauthorized modifications. # Wallet policies and controls Source: https://docs.privy.io/security/wallet-infrastructure/policy-and-controls Privy's wallet API is secured with tamper-proof cryptographic authorization and a powerful, intuitive policy engine. This means that the trusted execution environment (secure enclave) will only act on requests issued by authorized parties, and only specific permitted actions may be processed. Privy wallets are non-custodial and have a fully programmable control model. Privy's flexible configuration enables the full custody spectrum from user-custodial wallets to powerful service-controlled accounts. ## Authentication and authorization ### Authorization keys Requests to Privy [wallet API](/security/wallet-infrastructure/architecture) endpoints are protected by **authorization keys**. This requires the secure enclave to verify a signature from the required authorization key before executing any requests. [Learn more](/controls/authorization-keys/overview) Privy uses [P-256](https://neuromancer.sk/std/nist/P-256) (also known as secp256r1) asymmetric keys for authorization keys. When you register a key: * The private key is generated on your device, and is only ever known to your app. **Neither Privy nor the enclave ever sees the P-256 private key, and cannot sign payloads with it.** * The public key is registered with the enclave, and is used to verify signatures produced by your servers. The authorization signature is a signature generated over the body and all critical parameters of each request. This authorization signature guarantees the enclave only processes verified requests. The enclave verifies the signature against the corresponding authorization public key registered for the wallet before executing any wallet actions. ### Powerful, flexible controls Authorization keys enable a fully configurable control model for wallets. This includes the full spectrum from user-custodial wallets to powerful service-controlled accounts. When you create a wallet, you specify its **owner**, which is the key (or key quorum) that controls the wallet. By default, this key is also required to authorize wallet actions, such as generating signatures or transacting funds. This wallet ownership model is extremely flexible. It enables you to configure, e.g: * **Fully user non-custodial wallets**, using an [authorization key tied to the user's authentication method](/security/authentication/authenticated-signers) as the authorization key * **Fully user non-custodial wallets**, using the user's passkey as the authorization key * **Service-controlled wallets**, using an authorization key that is held by your service * **Multi-sig wallets**, using a quorum of authorization keys held by different parties ### Key quorums Privy enables your app to require quorum approvals on wallet actions, so that signatures from m-of-n authorization keys are required in order to take action using the wallet. Key quorums are defined by a list of authorization public keys and a threshold required for approval. [Learn more](/controls/quorum-approvals/overview) ### Multi-factor authentication Privy enables native multi-factor authentication for wallet actions. This means your app can require additional verification for sensitive wallet operations using: * Authenticator apps (TOTP) * Biometric verification (passkeys) * SMS confirmation * Hardware security keys Multi-factor authentication is enforced via [authenticated signers](/security/authentication/authenticated-signers). Learn more about configuring multi-factor authentication for your app [here](/authentication/user-authentication/mfa). ## Policies Privy's policy engine allow your application to **restrict the actions that can be taken with wallets**. This is important for features such as payment subscriptions, stop and limit orders, or scheduled transactions. Policies allows you to configure transfer limits, allowlists and denylists of transfer recipients, allowlists and denylists of smart contracts and programs, and even constraints around calldata that can be passed to smart contracts. By default, the trusted execution environment (secure enclave) enforces policies when processing wallet actions, such as signature requests, transactions, and key export. The enclave evaluates policy rules in a tamper-proof environment before any operations proceed. Privy enforces some policies at the API level. For example, limiting transfer sizes requires transaction simulation which runs outside the enclave today. This ensures that wallets can only ever be used to take actions your application intends to take. Learn more about configuring policies [here](/controls/policies/overview). Managing policies in the Privy Dashboard # Secure enclaves Source: https://docs.privy.io/security/wallet-infrastructure/secure-enclaves **Privy’s wallet infrastructure is designed so that the only place a full private key ever exists is inside a secure enclave.** We rely on hardened trusted execution environments to wrap the most sensitive parts of the system: key generation, policy enforcement, and signing with hardware-backed isolation. ## Why secure enclaves matter * **Hardware isolation:** Our secure enclave runtime provides a CPU-level sandbox with no persistent storage, no interactive access, and memory that is encrypted while in use. Even if another component is compromised, keys remain protected inside the enclave. * **Defense in depth:** Every wallet uses Shamir secret sharing to produce split, encrypted key shares, along with authorization signatures and policy checks. The two shares live on separate pieces of infrastructure: one protected by the enclave, the other by the API. Compromising a single provider is not enough to recover a key. The enclave is a critical layer, but it works in tandem with upstream controls to make attacks impractical. * **Measured boot:** Each enclave boots from a signed image. We verify attestation reports before provisioning secrets so only approved code can handle wallet operations, and those secrets are sealed so they can only be unwrapped by that approved image. ## How requests are processed 1. Your service calls the Privy API from infrastructure you control. 2. The API, running on isolated infrastructure outside the enclave, retrieves the encrypted share that corresponds to its shard of the wallet. 3. Only the enclave can combine that incoming shard with its own encrypted shard. Reconstruction happens in-memory just long enough to execute the requested action. 4. [Authorization signatures](/security/authentication/authenticated-signers) and [policies](/security/wallet-infrastructure/policy-and-controls) are verified inside the enclave before any signing can occur. Only after every control passes does the enclave produce the minimal response (for example, a transaction signature) and discard the reconstituted key material. Because the API and the enclave sit in different trust boundaries, compromising one provider is not enough to access private keys. Both encrypted shares and policy validation must succeed within the enclave for any action to complete. ## What enclave attestation gives you * **Sealed secrets:** Sensitive configuration and the enclave’s key shard are encrypted to the enclave image. They only decrypt after the image identity is verified through the attestation flow. * **Independent validation:** Our build pipeline and enclave attestation are issued by separate hardened systems. Images are signed in one environment, and attestation materials are generated and verified in another, creating an extra layer of assurance that only approved code can boot. * **Operational transparency:** Coming soon: customers will be able to access enclave attestation documents and measurements to verify that their workloads are running on the expected image. ## Frequently asked questions **Do Privy engineers have access to user keys?**\ No. Key shares are intentionally separated. Privy team members cannot reconstruct keys or bypass policy checks, and the enclave never exposes its shard. **What if a cloud provider is compromised?**\ Both shares are encrypted end to end. One stays sealed inside the trusted execution environment; the other is stored on separate infrastructure and only released to an attested enclave. A breach in one environment is insufficient to assemble a key. **Can anyone update the wallet database outside the enclave?**\ No. All sensitive state transitions are signed inside the enclave. The trusted execution environment is the only component that can authorize or sign changes before they reach persistent storage. **How are updates handled?**\ Enclave images undergo multi-party review, automated testing, and attestation validation before deployment. Updates roll out gradually, with builds signed in one hardened system and attestation issued in a separate environment before the enclave can accept traffic. **How does disaster recovery work?**\ Enclaves are stateless. If an enclave is replaced, it rehydrates from sealed secrets only after attestation. Key shards remain encrypted and are never stored together. Looking for more detail on how wallets are constrained once inside the enclave? Review our [policy and controls documentation](/security/wallet-infrastructure/policy-and-controls) and the guide to [authenticated signers](/security/authentication/authenticated-signers). Still have questions? Reach out to [security@privy.io](mailto:security@privy.io) and our security team will be happy to help. # Authorize a transaction Source: https://docs.privy.io/transaction-management/intents/create/execute-rpc Propose an intent to authorize and execute a signature or transaction. This endpoint accepts the same request body as the synchronous [RPC](/wallets/using-wallets/rpc) endpoint but does **not** require authorization signatures in the request. Instead, the intent is [signed](/transaction-management/intents/sign-intents) separately and executes once enough signatures are collected. Intents expire 72 hours after creation by default. Signers must [authorize](/transaction-management/intents/sign-intents) them within this window. ```bash theme={"system"} curl -X POST https://api.privy.io/v1/intents/wallets//rpc \ -u ":" \ -H "privy-app-id: " \ -H "Content-Type: application/json" \ -d '{ "method": "eth_sendTransaction", "caip2": "eip155:8453", "sponsor": "true", "params": { "transaction": { "to": "0xE3070d3e4309afA3bC9a6b057685743CF42da77C", "value": "0x2386F26FC10000" } } }' ``` ```typescript {skip-check} theme={"system"} import {PrivyClient, type EthereumSendTransactionRpcInput} from '@privy-io/node'; const client = new PrivyClient({ appId: 'your-app-id', appSecret: 'your-app-secret' }); const rpcRequest: EthereumSendTransactionRpcInput = { method: 'eth_sendTransaction', caip2: 'eip155:8453', sponsor: true, params: { transaction: { to: '0xE3070d3e4309afA3bC9a6b057685743CF42da77C', value: '0x2386F26FC10000' } } }; const intent = await client.intents().rpc('insert-wallet-id', rpcRequest); console.log(intent.intent_id, intent.status, intent.authorization_details); ``` ```go theme={"system"} import privy "github.com/privy-io/go-sdk" client := privy.NewPrivyClient(privy.PrivyClientOptions{ AppID: "your-app-id", AppSecret: "your-app-secret", }) transaction := privy.UnsignedStandardEthereumTransaction{ To: privy.String("0xE3070d3e4309afA3bC9a6b057685743CF42da77C"), Value: privy.QuantityUnion{ OfString: privy.String("0x2386F26FC10000"), }, } rpcInput := &privy.EthereumSendTransactionRpcInput{ Method: privy.EthereumSendTransactionRpcInputMethodEthSendTransaction, Caip2: "eip155:8453", Params: privy.EthereumSendTransactionRpcInputParams{ Transaction: transaction, }, } intent, err := client.Intents.Rpc(ctx, "wallet-id", privy.IntentRpcParams{ WalletRpcRequestBody: privy.WalletRpcRequestBodyUnion{ OfEthSendTransaction: rpcInput, }, }) ``` ```ruby theme={"system"} require "privy" client = Privy::PrivyClient.new( app_id: "your-app-id", app_secret: "your-app-secret" ) intent = client.intents.rpc( "wallet-id", wallet_rpc_request_body: { method: "eth_sendTransaction", caip2: "eip155:8453", params: { transaction: { to: "0xE3070d3e4309afA3bC9a6b057685743CF42da77C", value: "0x2386F26FC10000" } } } ) puts(intent.intent_id, intent.status, intent.authorization_details) ``` ```java theme={"system"} import io.privy.api.PrivyClient; import io.privy.api.models.components.*; import io.privy.api.models.operations.IntentRpcResponse; PrivyClient client = PrivyClient.builder() .appId("your-privy-app-id") .appSecret("your-app-secret") .build(); IntentRpcResponse intent = client.intents().rpc( "insert-wallet-id", EthereumSendTransactionRpcInput.builder() .method(EthereumSendTransactionRpcInputMethod.ETH_SEND_TRANSACTION) .caip2("eip155:8453") .sponsor(true) .params(EthereumSendTransactionRpcInputParams.builder() .transaction(UnsignedEthereumTransaction.of(UnsignedStandardEthereumTransaction.builder() .to("0xB00F0759DbeeF5E543Cc3E3B07A6442F5f3928a2") .value(Quantity.of("0x2386F26FC10000")) .build())) .build()) .chainType(RpcChainType.ETHEREUM) .build(), null); ``` From the response, note the returned `intent_id`. View the [API reference](/api-reference/intents/rpc) for submitting an RPC intent. ## Next steps Add authorization signatures to execute a proposed intent. Track an intent from proposal to execution. # Transfer funds Source: https://docs.privy.io/transaction-management/intents/create/execute-transfer Propose an intent to transfer funds from a wallet to a destination address. Intents expire 72 hours after creation by default. Signers must [authorize](/transaction-management/intents/sign-intents) them within this window. ```bash theme={"system"} curl -X POST https://api.privy.io/v1/intents/wallets//transfer \ -u ":" \ -H "privy-app-id: " \ -H "Content-Type: application/json" \ -d '{ "source": { "asset": "usdc", "amount": "10.0", "chain": "tempo" }, "destination": { "address": "0xB00F0759DbeeF5E543Cc3E3B07A6442F5f3928a2" } }' ``` ```typescript {skip-check} theme={"system"} import {PrivyClient, type IntentTransferParams} from '@privy-io/node'; const client = new PrivyClient({ appId: 'your-app-id', appSecret: 'your-app-secret' }); const transfer: IntentTransferParams = { source: { asset: 'usdc', amount: '10.0', chain: 'tempo' }, destination: { address: '0xB00F0759DbeeF5E543Cc3E3B07A6442F5f3928a2' } }; const intent = await client.intents().transfer('insert-wallet-id', transfer); console.log(intent.intent_id, intent.status, intent.authorization_details); ``` ```ruby theme={"system"} require "privy" client = Privy::PrivyClient.new( app_id: "your-app-id", app_secret: "your-app-secret" ) intent = client.intents.transfer( "wallet-id", source: { asset: "usdc", amount: "10.0", chain: "tempo" }, destination: { address: "0xB00F0759DbeeF5E543Cc3E3B07A6442F5f3928a2" } ) puts(intent.intent_id, intent.status, intent.authorization_details) ``` ```java theme={"system"} import io.privy.api.PrivyClient; import io.privy.api.models.components.*; import io.privy.api.models.operations.IntentTransferResponse; PrivyClient client = PrivyClient.builder() .appId("your-privy-app-id") .appSecret("your-app-secret") .build(); IntentTransferResponse intent = client.intents().transfer( "insert-wallet-id", TransferRequestBody.builder() .source(TokenTransferSource.of(NamedTokenTransferSource.builder() .asset("usdc") .amount("10.0") .chain("tempo") .build())) .destination(TokenTransferDestination.builder() .address("0xB00F0759DbeeF5E543Cc3E3B07A6442F5f3928a2") .build()) .build(), null); ``` From the response, note the returned `intent_id`. Use this ID to [sign the intent](/transaction-management/intents/sign-intents), check approval progress, and retrieve execution results. View the [API reference](/api-reference/intents/transfer) for submitting a transfer intent. ## Next steps Add authorization signatures to execute a proposed intent. Track an intent from proposal to execution. # Update key quorum Source: https://docs.privy.io/transaction-management/intents/create/update-key-quorum Propose an intent to update a key quorum itself -- changing its name, members, or authorization threshold. This intent must be authorized by a sufficient number of members of the existing quorum. This endpoint accepts the same request body as the synchronous [Update key quorum](/api-reference/key-quorums/update) endpoint but does **not** require authorization signatures in the request. Intents expire 72 hours after creation by default. Signers must [authorize](/transaction-management/intents/sign-intents) them within this window. ```bash theme={"system"} curl -X PATCH https://api.privy.io/v1/intents/key_quorums/ \ -u ":" \ -H "privy-app-id: " \ -H "Content-Type: application/json" \ -d '{ "authorization_threshold": 2 }' ``` ```typescript {skip-check} theme={"system"} import {PrivyClient, type IntentUpdateKeyQuorumParams} from '@privy-io/node'; const client = new PrivyClient({ appId: 'your-app-id', appSecret: 'your-app-secret' }); const quorumUpdate: IntentUpdateKeyQuorumParams = { authorization_threshold: 2 }; const intent = await client.intents().updateKeyQuorum('insert-key-quorum-id', quorumUpdate); console.log(intent.intent_id, intent.status, intent.authorization_details); ``` ```go theme={"system"} import privy "github.com/privy-io/go-sdk" client := privy.NewPrivyClient(privy.PrivyClientOptions{ AppID: "your-app-id", AppSecret: "your-app-secret", }) intent, err := client.Intents.UpdateKeyQuorum(ctx, "key-quorum-id", privy.IntentUpdateKeyQuorumParams{ KeyQuorumUpdateRequestBody: privy.KeyQuorumUpdateRequestBody{ AuthorizationThreshold: privy.Float(2), }, }) ``` ```ruby theme={"system"} require "privy" client = Privy::PrivyClient.new( app_id: "your-app-id", app_secret: "your-app-secret" ) intent = client.intents.update_key_quorum( "key-quorum-id", authorization_threshold: 2 ) puts(intent.intent_id, intent.status, intent.authorization_details) ``` ```java theme={"system"} import io.privy.api.PrivyClient; import io.privy.api.models.components.KeyQuorumUpdateRequestBody; import io.privy.api.models.operations.IntentKeyQuorumUpdateResponse; PrivyClient client = PrivyClient.builder() .appId("your-privy-app-id") .appSecret("your-app-secret") .build(); IntentKeyQuorumUpdateResponse intent = client.intents().updateKeyQuorum( "insert-key-quorum-id", KeyQuorumUpdateRequestBody.builder() .authorizationThreshold(2d) .build(), null); ``` View the [API reference](/api-reference/intents/update-key-quorum) for submitting a key quorum intent. ## Next steps Add authorization signatures to execute a proposed intent. Track an intent from proposal to execution. # Update policy Source: https://docs.privy.io/transaction-management/intents/create/update-policy Propose an intent to update a policy. This endpoint accepts the same request body as the synchronous [Update policy](/api-reference/policies/update) endpoint but does **not** require authorization signatures in the request. Intents expire 72 hours after creation by default. Signers must [authorize](/transaction-management/intents/sign-intents) them within this window. ```bash theme={"system"} curl -X PATCH https://api.privy.io/v1/intents/policies/ \ -u ":" \ -H "privy-app-id: " \ -H "Content-Type: application/json" \ -d '{ "name": "Updated policy name" }' ``` ```typescript {skip-check} theme={"system"} import {PrivyClient, type IntentUpdatePolicyParams} from '@privy-io/node'; const client = new PrivyClient({ appId: 'your-app-id', appSecret: 'your-app-secret' }); const policyUpdate: IntentUpdatePolicyParams = { name: 'Updated policy name' }; const intent = await client.intents().updatePolicy('insert-policy-id', policyUpdate); console.log(intent.intent_id, intent.status, intent.authorization_details); ``` ```go theme={"system"} import privy "github.com/privy-io/go-sdk" client := privy.NewPrivyClient(privy.PrivyClientOptions{ AppID: "your-app-id", AppSecret: "your-app-secret", }) intent, err := client.Intents.UpdatePolicy(ctx, "policy-id", privy.IntentUpdatePolicyParams{ Name: privy.String("updated-policy-name"), }) ``` ```ruby theme={"system"} require "privy" client = Privy::PrivyClient.new( app_id: "your-app-id", app_secret: "your-app-secret" ) intent = client.intents.update_policy( "policy-id", name: "updated-policy-name" ) puts(intent.intent_id, intent.status, intent.authorization_details) ``` ```java theme={"system"} import io.privy.api.PrivyClient; import io.privy.api.models.operations.IntentPolicyUpdateRequestBody; import io.privy.api.models.operations.IntentPolicyUpdateResponse; PrivyClient client = PrivyClient.builder() .appId("your-privy-app-id") .appSecret("your-app-secret") .build(); IntentPolicyUpdateResponse intent = client.intents().updatePolicy( "insert-policy-id", IntentPolicyUpdateRequestBody.builder() .name("Updated policy name") .build(), null); ``` View the [API reference](/api-reference/intents/update-policy) for submitting a policy intent. ## Next steps Add authorization signatures to execute a proposed intent. Track an intent from proposal to execution. # Update policy rules Source: https://docs.privy.io/transaction-management/intents/create/update-policy-rules Propose an intent to add, edit, or remove rules for a policy. Each rule action uses a different HTTP method and endpoint: | Action | Method | Endpoint | | ------------- | -------- | -------------------------------------------------- | | Add a rule | `POST` | `/v1/intents/policies/{policy_id}/rules` | | Update a rule | `PATCH` | `/v1/intents/policies/{policy_id}/rules/{rule_id}` | | Delete a rule | `DELETE` | `/v1/intents/policies/{policy_id}/rules/{rule_id}` | The example below shows how to add a new rule. Intents expire 72 hours after creation by default. Signers must [authorize](/transaction-management/intents/sign-intents) them within this window. ```bash theme={"system"} curl -X POST https://api.privy.io/v1/intents/policies//rules \ -u ":" \ -H "privy-app-id: " \ -H "Content-Type: application/json" \ -d '{ "name": "Restrict destination address", "method": "eth_sendTransaction", "action": "ALLOW", "conditions": [ { "field": "to", "field_source": "ethereum_transaction", "operator": "eq", "value": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" } ] }' ``` ```typescript {skip-check} theme={"system"} import {PrivyClient, type IntentCreatePolicyRuleParams} from '@privy-io/node'; const client = new PrivyClient({ appId: 'your-app-id', appSecret: 'your-app-secret' }); const rule: IntentCreatePolicyRuleParams = { name: 'Restrict destination address', method: 'eth_sendTransaction', action: 'ALLOW', conditions: [ { field: 'to', field_source: 'ethereum_transaction', operator: 'eq', value: '0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913' } ] }; const intent = await client.intents().createPolicyRule('insert-policy-id', rule); console.log(intent.intent_id, intent.status, intent.authorization_details); ``` ```go theme={"system"} import privy "github.com/privy-io/go-sdk" client := privy.NewPrivyClient(privy.PrivyClientOptions{ AppID: "your-app-id", AppSecret: "your-app-secret", }) intent, err := client.Intents.NewPolicyRule(ctx, "policy-id", privy.IntentNewPolicyRuleParams{ PolicyRuleRequestBody: privy.PolicyRuleRequestBody{ Name: "Restrict destination address", Action: privy.PolicyActionAllow, Method: privy.PolicyMethodEthSendTransaction, Conditions: []privy.PolicyConditionUnion{ { OfEthereumTransaction: &privy.EthereumTransactionCondition{ Field: privy.EthereumTransactionConditionFieldTo, FieldSource: privy.EthereumTransactionConditionFieldSourceEthereumTransaction, Operator: privy.ConditionOperatorEq, Value: privy.ConditionValueUnion{OfString: privy.String("0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913")}, }, }, }, }, }) ``` ```ruby theme={"system"} require "privy" client = Privy::PrivyClient.new( app_id: "your-app-id", app_secret: "your-app-secret" ) intent = client.intents.create_policy_rule( "policy-id", name: "Restrict destination address", method_: "eth_sendTransaction", action: "ALLOW", conditions: [ { field: "to", field_source: "ethereum_transaction", operator: "eq", value: "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913" } ] ) puts(intent.intent_id, intent.status, intent.authorization_details) ``` ```java theme={"system"} import io.privy.api.PrivyClient; import io.privy.api.models.components.*; import io.privy.api.models.operations.IntentCreateRuleResponse; import java.util.List; PrivyClient client = PrivyClient.builder() .appId("your-privy-app-id") .appSecret("your-app-secret") .build(); IntentCreateRuleResponse intent = client.intents().createRule( "insert-policy-id", PolicyRuleRequestBody.builder() .name("Restrict destination address") .method(PolicyMethod.ETH_SEND_TRANSACTION) .action(PolicyAction.ALLOW) .conditions(List.of(EthereumTransactionCondition.builder() .field(EthereumTransactionConditionField.TO) .fieldSource(EthereumTransactionConditionFieldSource.ETHEREUM_TRANSACTION) .operator(ConditionOperator.EQ) .value(ConditionValue.of("0xB00F0759DbeeF5E543Cc3E3B07A6442F5f3928a2")) .build())) .build(), null); ``` Each endpoint accepts the same request body as its synchronous counterpart ([create](/api-reference/policies/rules/create), [update](/api-reference/policies/rules/update), [delete](/api-reference/policies/rules/delete)) but does **not** require authorization signatures in the request. View the [API reference](/api-reference/intents/create-rule) for submitting a rule intent. ## Next steps Add authorization signatures to execute a proposed intent. Track an intent from proposal to execution. # Update wallet Source: https://docs.privy.io/transaction-management/intents/create/update-wallet Propose an intent to update a wallet -- for example, to change its owner, additional signers, or policies. This endpoint accepts the same request body as the synchronous [Update wallet](/api-reference/wallets/update) endpoint but does **not** require authorization signatures in the request. Intents expire 72 hours after creation by default. Signers must [authorize](/transaction-management/intents/sign-intents) them within this window. ```bash theme={"system"} curl -X PATCH https://api.privy.io/v1/intents/wallets/ \ -u ":" \ -H "privy-app-id: " \ -H "Content-Type: application/json" \ -d '{ "policy_ids": ["new-policy-id"] }' ``` ```typescript {skip-check} theme={"system"} import {PrivyClient, type IntentUpdateWalletParams} from '@privy-io/node'; const client = new PrivyClient({ appId: 'your-app-id', appSecret: 'your-app-secret' }); const walletUpdate: IntentUpdateWalletParams = { policy_ids: ['new-policy-id'] }; const intent = await client.intents().updateWallet('insert-wallet-id', walletUpdate); console.log(intent.intent_id, intent.status, intent.authorization_details); ``` ```go theme={"system"} import privy "github.com/privy-io/go-sdk" client := privy.NewPrivyClient(privy.PrivyClientOptions{ AppID: "your-app-id", AppSecret: "your-app-secret", }) intent, err := client.Intents.UpdateWallet(ctx, "wallet-id", privy.IntentUpdateWalletParams{ WalletUpdateRequestBody: privy.WalletUpdateRequestBody{ PolicyIDs: []string{"new-policy-id"}, }, }) ``` ```ruby theme={"system"} require "privy" client = Privy::PrivyClient.new( app_id: "your-app-id", app_secret: "your-app-secret" ) intent = client.intents.update_wallet( "wallet-id", policy_ids: ["new-policy-id"] ) puts(intent.intent_id, intent.status, intent.authorization_details) ``` ```java theme={"system"} import io.privy.api.PrivyClient; import io.privy.api.models.components.WalletUpdateRequestBody; import io.privy.api.models.operations.IntentWalletUpdateResponse; import java.util.List; PrivyClient client = PrivyClient.builder() .appId("your-privy-app-id") .appSecret("your-app-secret") .build(); IntentWalletUpdateResponse intent = client.intents().updateWallet( "insert-wallet-id", WalletUpdateRequestBody.builder() .policyIds(List.of("new-policy-id")) .build(), null); ``` View the [API reference](/api-reference/intents/update-wallet) for submitting a wallet intent. ## Next steps Add authorization signatures to execute a proposed intent. Track an intent from proposal to execution. # Get intent by ID Source: https://docs.privy.io/transaction-management/intents/fetch-intent Your app can fetch an intent from the Privy API to check its current status, view approval progress, and retrieve execution results. ## Usage To fetch an intent, make a `GET` request to `/v1/intents/{intent_id}`. ```sh Request theme={"system"} curl https://api.privy.io/v1/intents/{intent_id} \ -H "Authorization: Basic " \ -H "privy-app-id: " ``` ```json Response theme={"system"} { "intent_id": "clpq1234567890abcdefghij", "intent_type": "RPC", "created_by_display_name": "developer@example.com", "created_by_id": "did:privy:clabcd123", "created_at": 1741834854578, "resource_id": "xs76o3pi0v5syd62ui1wmijw", "authorization_details": [ { "members": [ { "type": "user", "user_id": "did:privy:clabcd123", "display_name": "admin@example.com", "signed_at": null } ], "threshold": 1, "display_name": "Admin Key Quorum" } ], "status": "pending", "expires_at": 1741921254578, "request_details": { "method": "POST", "url": "https://api.privy.io/v1/wallets/xs76o3pi0v5syd62ui1wmijw/rpc", "body": { "method": "eth_sendTransaction", "caip2": "eip155:8453", "chain_type": "ethereum", "params": { "transaction": { "to": "0x0000000000000000000000000000000000000000", "value": 1 } } } } } ``` ```typescript {skip-check} theme={"system"} import {PrivyClient} from '@privy-io/node'; const client = new PrivyClient({ appId: 'your-app-id', appSecret: 'your-app-secret' }); const intent = await client.intents().get('insert-intent-id'); console.log(intent.status, intent.authorization_details); ``` ```go theme={"system"} import privy "github.com/privy-io/go-sdk" client := privy.NewPrivyClient(privy.PrivyClientOptions{ AppID: "your-app-id", AppSecret: "your-app-secret", }) intent, err := client.Intents.Get(ctx, "intent-id") ``` ```ruby theme={"system"} require "privy" client = Privy::PrivyClient.new( app_id: "your-app-id", app_secret: "your-app-secret" ) intent = client.intents.get("intent-id") puts(intent.status) ``` ```java theme={"system"} import io.privy.api.PrivyClient; import io.privy.api.models.operations.IntentRetrieveResponse; PrivyClient client = PrivyClient.builder() .appId("your-privy-app-id") .appSecret("your-app-secret") .build(); IntentRetrieveResponse response = client.intents().get("insert-intent-id"); ``` ## Response fields Unique identifier for the intent. The type of action the intent proposes. Email address of the team member who proposed the intent. May be `null` for intents proposed via the API. The Privy DID of the team member who proposed the intent. UNIX timestamp (ms) for when the intent was created. ID of the resource the intent targets (e.g., a wallet ID or policy ID). List of key quorums assigned to the intent. Name of the key quorum. Number of approvals required from this quorum. Members of the quorum. The member type (e.g., `'user'`). The Privy DID of the member. Email address of the member. Timestamp of when this member signed. Null if they have yet to sign. Current status of the intent. See [intent lifecycle](/transaction-management/intents/lifecycle) for details on each status. UNIX timestamp (ms) for when the intent expires. The full request for the proposed action, including HTTP method, URL, and body. Response body from execution, if the intent has completed. For transaction intents, this contains the transaction hash. For transaction intents, your app can read the transaction hash from `action_result` to track the transaction on-chain. ## API reference View the full API reference for fetching an intent. # Intent webhooks Source: https://docs.privy.io/transaction-management/intents/intent-webhooks Privy emits webhooks when the status of an intent changes, enabling your application to react to approval events in real time. Use these webhooks to notify reviewers, trigger automations, or keep external systems in sync with the approval flow. Webhooks can be tested at no cost in development environments. To enable webhooks in production, upgrade to the Enterprise plan in the Privy Dashboard. ## Setup Follow the [webhooks setup guide](/api-reference/webhooks/overview#registering-an-endpoint) to register an endpoint, then enable the intent events your app needs. Privy will emit a signed webhook to your endpoint whenever the status of an intent changes, and will retry delivery if the endpoint does not successfully respond. See the [webhooks reference](/api-reference/webhooks/overview#webhook-delivery) for details on delivery, idempotency, and retries. ## Events ### `intent.created` Fired when an intent is proposed, either via the Privy Dashboard or the REST API. The payload includes the list of **authorizers** eligible to approve the request and an `expires_at` timestamp indicating when the intent will expire if the required approvals are not satisfied. Use this event to notify reviewers that a new intent is awaiting their approval. List of key quorums assigned to the intent. Name of the key quorum. Number of approvals required from this quorum. Members of the quorum. The member type (e.g., `'user'`). The Privy DID of the member. Email address of the member. Timestamp of when this member signed. Null if they have yet to sign. UNIX timestamp after which the intent expires and can no longer be executed. Intents expire 72 hours after creation by default. Build alerting around the `expires_at` field to remind reviewers before the window closes. View the complete `intent.created` webhook payload. ### `intent.authorized` Fired when a team member approves an intent. This event indicates that one reviewer has submitted their authorization. *It does not guarantee the intent has been executed.* If the approval meets the authorization threshold, the intent executes automatically. If more approvals are still required, the intent remains in the **Pending** state until the threshold is reached or the intent expires. Use this event to track approval progress or trigger follow-up actions once a specific reviewer has signed off. The team member who authorized the intent. The member type (e.g., `'user'`). The Privy DID of the member. Email address of the member. Timestamp of when this member signed. UNIX timestamp when the team member signed the intent. View the complete `intent.authorized` webhook payload. ### `intent.rejected` Fired when an intent is rejected. A rejected intent is in a terminal state and cannot be approved or executed. Use this event to notify the intent proposer that their request was denied, or to trigger cleanup logic in external systems. UNIX timestamp when the intent was rejected. A rejected intent is in a terminal state. To retry the action, propose a new intent with the same parameters. View the complete `intent.rejected` webhook payload. ### `intent.executed` Fired when an intent has been fully approved and the proposed action executes successfully. For example, a transaction was sent and broadcasted. Use this event to confirm that the action is complete and to retrieve execution results such as a transaction hash. Result of the failed intent execution. HTTP status code from the action execution. UNIX timestamp when the action was executed. Display name of the key quorum that authorized execution. ID of the key quorum that authorized execution. Response from the execution. For transaction intents, this contains the transaction hash. View the complete `intent.executed` webhook payload. ### `intent.failed` Fired when an intent has been fully approved but the proposed action fails during execution. Common causes include insufficient gas or balance, a policy blocking the transaction, or an on-chain revert. A failed intent is in a terminal state. To retry the action, propose a new intent with the same parameters. Result of the failed intent execution. HTTP status code from the action execution. UNIX timestamp when the action was executed. Display name of the key quorum that authorized execution. ID of the key quorum that authorized execution. Response from the execution, if available. View the complete `intent.failed` webhook payload. # Lifecycle of an intent Source: https://docs.privy.io/transaction-management/intents/lifecycle Every intent moves through a series of statuses from creation to resolution. Understanding these statuses helps your app track proposals and build automation around intent outcomes. ## Status overview | Status | Description | | ----------------------------- | ------------------------------------------------------------------------------------------- | | [**Pending**](#pending) | Awaiting authorization. The collected signatures have not yet met the threshold. | | [**Granted**](#granted) | Authorized by the current signer, but still awaiting more signatures to meet the threshold. | | [**Processing**](#processing) | The threshold is met and the action is executing. | | [**Executed**](#executed) | The threshold was met and the action completed successfully. | | [**Failed**](#failed) | The threshold was met, but the action failed during execution. | | [**Rejected**](#rejected) | The intent was cancelled. | | [**Expired**](#expired) | The authorization window elapsed without enough signatures. | | [**Dismissed**](#dismissed) | The underlying resource changed or was deleted, invalidating the intent. | ### Pending When an intent is proposed, via the API or the Dashboard, it starts in the **Pending** status. The intent remains pending until one of the following occurs: * Enough signers authorize it to meet the threshold (transitions to **Processing**, **Executed**, or **Failed**). * The intent is cancelled (transitions to **Rejected**). * The authorization window elapses without enough signatures (transitions to **Expired**). * The underlying resource is modified or deleted (transitions to **Dismissed**). ### Granted An intent moves to **Granted** when the current signer has authorized it but the threshold is not yet met. *This state is specific to the current signer and is not shared across all authorizers.* The intent remains pending, awaiting authorization from more owners or signers. ### Processing An intent moves to **Processing** when the authorization threshold is met and execution begins. This applies to asynchronous actions such as transfers. ### Executed An intent moves to **Executed** when the authorization threshold is met and execution succeeds. The proposed action completes automatically -- the transaction is signed and broadcasted, or the wallet or policy is updated. After execution, the intent's `action_result` field contains the response. For transaction intents, this includes the transaction hash. [Fetch the intent](/transaction-management/intents/fetch-intent) or listen to the `intent.executed` webhook to retrieve these results. ### Failed An intent moves to **Failed** when the authorization threshold is met, but execution fails. Intent execution can fail for various reasons, such as a policy blocking the transaction or insufficient gas or balance to transact. At this point, the intent is in a terminal state and must be recreated to try again. ### Rejected An intent can be cancelled before it executes, moving it to **Rejected**. In the Dashboard, cancel an intent from the [Approvals](https://dashboard.privy.io/apps?page=approvals) page by selecting **Cancel proposal**. Once rejected, the intent is in a terminal state and cannot execute. ### Expired Intents expire 72 hours after creation by default. If the authorization threshold has not been met within that window, the intent moves to **Expired** and can no longer accept authorizations. Once expired, the intent is in a terminal state and cannot execute. To retry an expired intent, propose a new intent with the same parameters. ### Dismissed An intent is automatically dismissed when the underlying resource changes or is deleted. This prevents stale proposals from executing against a resource that no longer matches the original intent. When this may happen: * Updating a wallet dismisses all pending intents for that wallet. * Deleting a policy dismisses all pending policy and rule intents for that policy. ## Next steps Add authorization signatures to execute a proposed intent. Retrieve intent status and execution results via the API. # Intents Source: https://docs.privy.io/transaction-management/intents/overview Intents are Privy's asynchronous signing mechanism. They let your app propose an action on a wallet or resource, then collect the authorization signatures needed to execute it separately, over time and, if needed, from multiple parties. ## Asynchronous signing Privy's wallet API was originally synchronous: a request to use a wallet had to include every authorization signature in that same request. Intents split this into distinct steps, so signatures can be added independently: 1. **Propose an intent** that describes the action to perform, such as a transfer, a transaction, or a resource update. 2. **Sign the intent** by adding authorization signatures one at a time from the resource's owners or signers. 3. **Privy executes** the action automatically once the signatures satisfy the resource's authorization threshold. Without intents, an action and all of its authorization signatures are sent to the Privy API in a single request. With intents, the action is proposed first and each authorization signature is added over time. For the synchronous flow, where signatures accompany the request, see [Signing and RPC](/wallets/using-wallets/rpc). ## Supported actions An intent can propose any of the following actions: | Action | Description | API reference | | --------------------- | ---------------------------------------------------------------- | ------------------------------------------------------------- | | **Transfer** | Transfer funds from a wallet to a destination address. | [Create transfer](/api-reference/intents/transfer) | | **RPC** | Execute a signature or transaction with a wallet. | [Create RPC transaction](/api-reference/intents/rpc) | | **Update wallet** | Change a wallet's owner, additional signers, or policies. | [Update wallet](/api-reference/intents/update-wallet) | | **Update policy** | Update a policy or add, edit, or remove its rules. | [Update policy](/api-reference/intents/update-policy) | | **Update key quorum** | Change a key quorum's name, members, or authorization threshold. | [Update key quorum](/api-reference/intents/update-key-quorum) | ## Use cases ### Embed arbitrary logic in transaction flows Intents create a window between proposing an action and executing it. Your app can use that window to run additional steps such as compliance or risk screening, funding or onramping, or a user confirmation before the action executes. For example, an app lets a user buy a token with their fiat balance. The app proposes an intent to execute the swap, runs compliance screening while the intent is pending, onramps funds into the user's wallet, and only then adds the authorization signature to execute. Intents provide a generic way to introduce additional steps into an authorization flow without blocking on a single synchronous request. ### Orchestrate complex approval workflows Because signatures can be added asynchronously by different parties, intents are a natural fit for multi-party approvals. Your app can provision wallets for organizations and collect authorization signatures from multiple members of the organization at their convenience, and Privy orchestrates the collected signatures and executes once the threshold is met. This lets your app offer its own white-labeled approval experience. For example, build an internal dashboard for a treasury team with out-of-the-box signature collection and execution orchestration. ## Relationship to manual approvals [Manual approvals](/controls/dashboard/overview) is Privy's Dashboard-native implementation of intents. Team members review and approve intents directly in the Privy Dashboard, secured by biometric or TOTP MFA. Use manual approvals for a ready-made, Dashboard-based approval flow, or use the Intents API to build your own asynchronous signing flow. ## Next steps Propose an intent to transfer funds, run a transaction, or update a resource. Add authorization signatures to execute a proposed intent. Track an intent from proposal to execution. Explore the full Intents REST API. # Reject intent Source: https://docs.privy.io/transaction-management/intents/reject-intents A pending intent can be rejected to cancel it before it is authorized and executed. Rejecting is useful when an intent was proposed in error or is no longer needed. ## Reject an intent To cancel a pending intent, use the reject endpoint: ```sh theme={"system"} POST https://api.privy.io/v1/intents/{intent_id}/reject ``` The endpoint can be called with your app secret or with the intent creator's user token, and takes no request body. Rejecting an intent moves it to the **Rejected** status. Rejected intents cannot be authorized or executed in the future. Only a pending intent can be rejected. An intent that is already in a terminal or `Processing` state cannot be rejected. ## API reference View the full API reference for rejecting an intent. ## Next steps Authorize a pending intent so Privy can execute it. Track an intent from proposal to execution. # Authorize intent Source: https://docs.privy.io/transaction-management/intents/sign-intents After an intent is proposed, it must be authorized before Privy executes it. Each eligible owner or signer authorizes the intent by adding an **authorization signature**. Signatures can be added independently, over time, until the resource's authorization threshold is met. ## Authorize an intent To authorize an intent, submit an authorization signature to the authorize endpoint: ```sh theme={"system"} POST https://api.privy.io/v1/intents/{intent_id}/authorize ``` The endpoint can be called with your app secret or with a wallet owner's user token, and accepts an object with the following fields: An [authorization signature](/api-reference/authorization-signatures) over the intent's action. Unix timestamp, in milliseconds, when the signature was created. Privy uses it to verify the signing key was valid at signing time. ```bash theme={"system"} curl -X POST https://api.privy.io/v1/intents//authorize \ -u ":" \ -H "privy-app-id: " \ -H "Content-Type: application/json" \ -d '{ "signature": "", "timestamp": 1741834854578 }' ``` ```java theme={"system"} import io.privy.api.PrivyClient; import io.privy.api.models.components.AuthorizationContext; import io.privy.api.models.operations.IntentAuthorizeResponse; import java.util.Arrays; PrivyClient client = PrivyClient.builder() .appId("your-privy-app-id") .appSecret("your-app-secret") .build(); // The authorize endpoint accepts a single signature, so include exactly one signing mechanism. AuthorizationContext authorizationContext = AuthorizationContext.builder() .addAuthorizationPrivateKeys(Arrays.asList("your-authorization-private-key")) .build(); // Fetches the intent, builds and signs the authorization payload, and submits it. IntentAuthorizeResponse response = client.intents().authorize("insert-intent-id", authorizationContext); ``` The `signature` is an [authorization signature](/api-reference/authorization-signatures) over the intent's underlying request. Generate it with the resource owner's authorization key, following the [signing guides](/controls/authorization-keys/using-owners/sign/overview). The authorize endpoint accepts a single signature per call. To satisfy a threshold greater than one, each owner or signer calls the endpoint with their own signature. ## Execution When authorizations meet the resource's authorization threshold, Privy executes the action automatically. The intent moves from **Pending** to **Processing** for asynchronous actions such as transfers, and then to **Executed** or **Failed**. Retrieve the outcome by [fetching the intent](/transaction-management/intents/fetch-intent) or by listening to the `intent.executed` webhook. For transaction intents, the `action_result` field contains the transaction hash. See [intent status](/transaction-management/intents/lifecycle) for details on each status. ## Idempotency Privy records one authorization per signer. Re-submitting the same signer's authorization is safe: it does not add a duplicate approval or advance the intent past its threshold more than once, and the action executes only once when the threshold is met. ## Errors Authorizing an intent returns an error in the following cases: | Condition | Description | | ----------------------- | ------------------------------------------------------------------------------------------------------------- | | Intent not found | No intent matches the provided `intent_id`. | | Intent not authorizable | The intent is already **Executed**, **Failed**, **Rejected**, **Expired**, **Dismissed**, or **Processing**. | | Invalid signature | The `signature` is malformed, or the `timestamp` falls outside the window in which the signing key was valid. | | Ineligible signer | The signer is not an owner or signer eligible to authorize this intent. | ## API reference View the full API reference for authorizing an intent. ## Next steps Propose an intent to transfer funds, run a transaction, or update a resource. Track an intent from proposal to execution. Cancel a pending intent before it is authorized and executed. # Set a reference ID Source: https://docs.privy.io/transaction-management/transactions/reference-id The following functionality exists for [wallets reconstituted server-side](/wallets/wallets/create/create-a-wallet). More on [Privy architecture here](/security/wallet-infrastructure/architecture) A `reference_id` is an optional, developer-provided identifier that can be attached to a transaction for reconciliation with your own internal records. It must be unique per transaction and can be up to 64 characters. The `reference_id` is included in all transaction payloads, including [webhook events](/wallets/gas-and-asset-management/assets/transaction-event-webhooks), and can be used to [fetch a transaction by its reference ID](/api-reference/transactions/external-id). ## Supported methods The `reference_id` parameter is supported on the following RPC methods: * [`eth_sendTransaction`](/api-reference/wallets/ethereum/eth-send-transaction) for EVM chains * [`signAndSendTransaction`](/api-reference/wallets/solana/sign-and-send-transaction) for Solana Pass the `reference_id` field in the request body when calling either method. ```bash theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "eth_sendTransaction", "caip2": "eip155:8453", "chain_type": "ethereum", "reference_id": "order-abc-123", "params": { "transaction": { "to": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045", "value": "0x2386F26FC10000" } } }' ``` ```bash theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/rpc \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "method": "signAndSendTransaction", "caip2": "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", "reference_id": "order-abc-123", "params": { "transaction": "", "encoding": "base64" } }' ``` ## Looking up transactions by reference ID Once a `reference_id` has been set, your app can look up the associated transaction using the [get transaction by reference ID](/api-reference/transactions/external-id) endpoint: ```bash theme={"system"} curl --request GET \ --url 'https://api.privy.io/v1/transactions?reference_id=order-abc-123' \ --header 'Authorization: Basic ' \ --header 'privy-app-id: ' ``` Your app can also retrieve the transaction directly by its Privy-assigned ID using the [get transaction](/api-reference/transactions/get) endpoint. The `reference_id` is included in the response. ## Webhooks All [transaction webhook events](/wallets/gas-and-asset-management/assets/transaction-event-webhooks) include the `reference_id` field in their payload when one was provided. See the individual webhook event references for the full payload schema: * [Transaction broadcasted](/api-reference/webhooks/transaction/broadcasted) * [Transaction confirmed](/api-reference/webhooks/transaction/confirmed) * [Transaction still pending](/api-reference/webhooks/transaction/still_pending) * [Transaction execution reverted](/api-reference/webhooks/transaction/execution_reverted) * [Transaction replaced](/api-reference/webhooks/transaction/replaced) * [Transaction failed](/api-reference/webhooks/transaction/failed) * [Transaction provider error](/api-reference/webhooks/transaction/provider_error) This allows your app to match incoming webhook notifications to your internal records without an additional API call. # Create or import a batch of users Source: https://docs.privy.io/user-management/migrating-users-to-privy/create-or-import-a-batch-of-users To import existing users, Privy allows you to create users with their linked accounts (wallet, email, etc.) in batches via REST API to simplify the migration process. To create users, pass in an array of user objects which each represent a new user. You can also create wallets with wallet pregeneration. Once a user has been created, all of their accounts (wallet, email, etc.) will be included in their user object when they log in. If the user has an embedded wallet, that wallet will be available to the user upon sign in. Make a `POST` request to: ```sh theme={"system"} https://auth.privy.io/api/v1/users/batch ``` In the body of the request, include a `users` field with an array of up to 20 user objects. Below is a **sample cURL command** for creating multiple new users: ```bash theme={"system"} $ curl --request POST https://auth.privy.io/api/v1/users/batch \ -u ":" \ -H "privy-app-id: " \ -H "Content-Type: application/json" \ -d '{ "users": [ { "linked_accounts": [ { "type": "email", "address": "joker@gmail.com" } ] }, { "linked_accounts": [ { "type": "wallet", "chain_type": "ethereum", "address": "0xd8da6bf26964af9d7eed9e03e53415d37aa96045" } ] }, { "linked_accounts": [ { "type": "email", "address": "robin@gmail.com" } ] } ] }' ``` ### Parameters An array including all of the user's linked accounts. These objects are in the same shape as the linked accounts returned by [`getUser`](/user-management/users/managing-users/querying-users). For each linked account, you must specify the `type` and must not include a `verifiedAt` timestamp. | Field | Type | Description | | --------- | --------------- | ------------------------------------------------------- | | `type` | `'apple_oauth'` | N/A | | `email` | `string` | Email address associated with the user's Apple account. | | `subject` | `number` | ID of user from Apple's user API. | | Field | Type | Description | | ------------------------------------------------ | --------------- | ------------------------------------- | | `type` | `'custom_auth'` | N/A | | API: `custom_user_id`
SDK: `customUserId` | `string` | ID of user from custom auth provider. |
| Field | Type | Description | | ---------- | ----------------- | --------------------------------------------------------------------------------------------------- | | `type` | `'discord_oauth'` | N/A | | `subject` | `string` | ID of user from Discord user API response. | | `email` | `string` | Email of user from Discord user API response | | `username` | `string` | Username of user from Discord user API response. Include the 4-digit discriminator prefixed by '#'. | (See [Discord docs](https://discord.com/developers/docs/resources/user)) | Field | Type | Description | | --------- | --------- | ------------------------------ | | `type` | `'email'` | N/A | | `address` | `string` | Email address of user account. | | Field | Type | Description | | ---------------------------------------------------------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------- | | `type` | `'farcaster'` | N/A | | `fid` | `number` | FID of the user from Farcaster user API response. | | API: `owner_address`
SDK: `ownerAddress` | `string` | Wallet address of the user from Farcaster user API response. Note that this is the Farcaster wallet address, and not the Privy embedded wallet address. | | `username` | `string` | (Optional) Username of user from Farcaster user API response. Do not include the '@'. | | API: `display_name`
SDK: `displayName` | `string` | (Optional) Display name of user from Farcaster user API response. | | `bio` | `string` | (Optional) Bio of user from Farcaster user API response. | | API: `profile_picture_url`
SDK: `profilePictureUrl` | `string` | (Optional) Profile picture URL of the user from Farcaster user API response. Must be a valid image URL. | | API: `homepage_url`
SDK: `homepageUrl` | `string` | (Optional) Profile URL of the user from Farcaster user API response. | (See [Farcaster docs](https://docs.farcaster.xyz/reference/hubble/httpapi/userdata#userdata-api). Note that the Privy import interface differs slightly from the Farcaster public interface in order to maintain consistency with other Privy **`LinkedAccount`** types.)
| Field | Type | Description | | ---------- | ---------------- | ---------------------------------------------- | | `type` | `'github_oauth'` | N/A | | `subject` | `string` | ID of user from GitHub user API response. | | `email` | `string` | Email of user from GitHub user API response | | `name` | `string` | Name of user from GitHub user API response | | `username` | `string` | Username of user from GitHub user API response | (See [GitHub docs](https://docs.github.com/en/rest/users/users?apiVersion=2022-11-28#get-the-authenticated-user)) | Field | Type | Description | | --------- | ---------------- | ---------------------------------------------------------- | | `type` | `'google_oauth'` | N/A | | `subject` | `string` | `sub` pulled from Google-provided JWT with "openid" scope. | | `email` | `string` | `email` from Google-provided JWT with "email" scope. | | `name` | `string` | `name` from Google-provided JWT with "profile" scope. | | Field | Type | Description | | ---------- | ------------------- | --------------------------------------------------------------------------- | | `type` | `'instagram_oauth'` | N/A | | `subject` | `string` | ID of user from Instagram user API response. | | `username` | `string` | The name displayed on a user's profile from Instagram's `/me` API response. | (See [Instagram docs](https://developers.facebook.com/docs/instagram-basic-display-api/reference/me/)) | Field | Type | Description | | --------- | ------------------ | --------------------------------------------------------------------- | | `type` | `'linkedin_oauth'` | N/A | | `subject` | `string` | ID of user from LinkedIn user API response. | | `email` | `string` | Email of user from LinkedIn user API response | | `name` | `string` | Name of user from LinkedIn user API response. Do not include the '@'. | (See [Linkedin docs](https://learn.microsoft.com/en-us/linkedin/consumer/integrations/self-serve/sign-in-with-linkedin-v2#api-request-to-retreive-member-details)) | Field | Type | Description | | -------- | --------- | ----------------------------------------------------------------------- | | `type` | `'phone'` | N/A | | `number` | `string` | Phone number of user account (non-international numbers default to US). | While `number` is accepted as input, `phoneNumber` is returned in the response. | Field | Type | Description | | --------- | ----------------- | ------------------------------------------------------------------------------- | | `type` | `'spotify_oauth'` | N/A | | `subject` | `string` | ID of user from Spotify user API response. | | `email` | `string` | Email of user from Spotify user API. | | `name` | `string` | The name displayed on a user's profile from Spotify display\_name API response. | (See [Spotify docs](https://developer.spotify.com/documentation/web-api/reference/get-current-users-profile)) | Field | Type | Description | | ---------------- | ------------ | ---------------------------------------------------------------- | | `type` | `'telegram'` | N/A | | `telegramUserId` | `string` | ID of a user's telegram account. | | `firstName` | `string` | The first name displayed on a user's telegram account. | | `lastName` | `string` | (Optional) The last name displayed on a user's telegram account. | | `username` | `string` | (Optional) The username displayed on a user's telegram account. | | `photo_url` | `string` | (Optional) The url of a user's telegram account profile picture. | (See [Telegram docs](https://core.telegram.org/widgets/login#checking-authorization)) | Field | Type | Description | | ------------------------------------------------------- | ----------------- | ----------------------------------------------------------------------------------------------------- | | `type` | `'twitter_oauth'` | N/A | | `subject` | `string` | ID of user from Twitter user API response. | | `name` | `string` | Name of user from Twitter user API response | | `username` | `string` | Username of user from Twitter user API response. Do not include the '@'. | | API: profile\_picture\_url`
`SDK: profilePictureUrl | `string` | (Optional) Profile picture URL of the user from Twitter user API response. Must be a valid image URL. | (See [Twitter docs](https://developer.twitter.com/en/docs/twitter-api/users/lookup/api-reference/get-users-me#tab0))
| Field | Type | Description | | ------------------- | ----------------- | ------------------------------------------------------------------------------------------------------- | | `type` | `'smart_wallet'` | N/A | | `address` | `string` | Checksummed smart wallet address. | | `smart_wallet_type` | `SmartWalletType` | One of `'kernel'`, `'safe'`, `'biconomy'`, `'thirdweb'`, `'light_account'` or `'coinbase_smart_wallet'` | | Field | Type | Description | | ---------------------------------------- | ------------------------ | -------------------------------------------------------------------------------------------------------- | | `type` | `'wallet'` | N/A | | API:`chain_type`
SDK: `chainType` | `'ethereum' \| 'solana'` | Type of chain for the wallet. EVM chains (`'ethereum'`) and Solana (`'solana'`) are currently supported. | | `address` | `string` | Checksummed wallet address. |
(Optional) An array of wallets to create for the user. The chain type of the wallet to create. The ID of the signer. List of policy IDs for policies that should be enforced on the wallet. Currently, only one policy is supported per wallet. Set to `true` to create a smart wallet with the user's wallet as the signer. Can only be set on wallets where `chain_type` is `ethereum`. ### Response Format A successful response will include a list of results along with details about which succeeded and which failed: ```json theme={"system"} { "results": [ { "action": "create", "index": 0, "success": true, "id": "did:privy:clfn2wysq01ijykc8gyq2j2t1" }, { "action": "create", "index": 1, "success": false, "code": 101, "error": "Account conflict caused by an existing user. Multiple users cannot share the same account.", "cause": "did:privy:clfmxole300rmykc89nojp3v2" }, { "action": "create", "index": 2, "success": true, "id": "did:privy:clfn2wysq01ijykc8gyq2j2t3" } ] } ``` Each result in the response includes: The action taken ("create"). The index of the user in the request array. Whether the user creation succeeded. The Privy DID of the user (if successful). Error code (if unsuccessful). Error message (if unsuccessful). The conflicting DID (if there was an account conflict).
User creation endpoints have a rate limit of 240 users per minute. If you are being rate limited, responses will have status code 429. We suggest you set up exponential back-offs starting at 1 second to seamlessly recover. # Create or import a user Source: https://docs.privy.io/user-management/migrating-users-to-privy/create-or-import-a-user To import an existing user, Privy allows you to create a user with their linked accounts (wallet, email, etc.) as part of the user creation request. You can also generate a wallet when you create a user. When the user logs in, all of their linked accounts will be included in the user object. If the user has a pregenerated embedded wallet, that wallet will be available to the user upon sign in. You can create a user by calling the `.users().create()` method on the `PrivyClient`. ```ts theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: 'insert-your-app-id', appSecret: 'insert-your-app-secret' }); try { const user = await privy.users().create({ linked_accounts: [{type: 'email', address: 'batman@privy.io'}], wallets: [{chain_type: 'ethereum'}], custom_metadata: {key: 'value'} }); } catch (error) { console.error(error); } ``` Refer to the [API reference](/api-reference/users/create) for more details on the available parameters and returns. You can create a user by calling the `.users().create()` method on the `PrivyClient`. ```java theme={"system"} try { List createUserLinkedAccounts = List.of( LinkedAccountInput.email("batman@privy.io") ); Map customMetadata = Map.of("username", CustomMetadata.of("name")); // Pregenerate an Ethereum wallet for the user List wallets = List.of( UserWalletRequest.builder() .chainType(WalletChainType.ETHEREUM) .build() ); UserCreateRequestBody requestBody = UserCreateRequestBody.builder() .linkedAccounts(createUserLinkedAccounts) .customMetadata(customMetadata) .wallets(wallets) .build(); UserCreateResponse response = privyClient .users() .create(requestBody); if (response.user().isPresent()) { User user = response.user().get(); } } catch (APIException e) { String errorBody = e.bodyAsString(); System.err.println(errorBody); } catch (Exception e) { System.err.println(e.getMessage()); } ``` ### Parameters You can specify the following values on the `UserCreateRequestBody` builder: A list of linked accounts to create for the user. An object containing any custom metadata you want to associate with the user. This metadata will be returned in the user object when the user logs in. A list of wallets to create for the user. ### Returns The `UserCreateResponse` object contains an optional `user()` field, present if the user was created successfully. The created `User` object. See the [user object](/user-management/users/the-user-object) for more details. You can create a user by calling the `.users().create()` method on the `PrivyClient`. ```rust theme={"system"} use privy_rs::{PrivyClient, generated::types::*}; use std::collections::HashMap; let client = PrivyClient::new(app_id, app_secret)?; // Create custom metadata with proper typing let mut metadata_map = HashMap::new(); metadata_map.insert("key".to_string(), CustomMetadataValue::String("value".to_string())); let custom_metadata = CustomMetadata::from(metadata_map); let user = client .users() .create(&CreateUserBody { linked_accounts: vec![ LinkedAccountInput::EmailInput(LinkedAccountEmailInput { address: "batman@privy.io".to_string(), type_: LinkedAccountEmailInputType::Email, }), ], wallets: vec![ CreateWalletBody { chain_type: WalletChainType::Ethereum, additional_signers: None, owner: None, owner_id: None, policy_ids: vec![], }, ], custom_metadata: Some(custom_metadata), }) .await?; println!("Created user: {}", user.id); ``` ### Parameters and Returns See the Rust SDK documentation for detailed parameter and return types, including embedded examples: * [UsersClient::create](https://docs.rs/privy-rs/latest/privy_rs/subclients/struct.UsersClient.html#method.create) For REST API details, see the [API reference](/api-reference/users/create). To create or import a user with the Go SDK, use the `New` method on the `Users` service. ### Usage ```go theme={"system"} user, err := client.Users.New(context.Background(), privy.UserNewParams{ LinkedAccounts: []privy.LinkedAccountInputUnion{ { OfCustomAuth: &privy.LinkedAccountCustomJwtInput{ Type: privy.LinkedAccountCustomJwtInputTypeCustomAuth, CustomUserID: "your-subject-id", }, }, { OfEmail: &privy.LinkedAccountEmailInput{ Type: privy.LinkedAccountEmailInputTypeEmail, Address: "user@example.com", }, }, }, }) if err != nil { log.Fatalf("failed to create user: %v", err) } fmt.Println("Created user:", user.ID) ``` ### Parameters and Returns See the [API reference](/api-reference/users/create) for more details. To create or import a user with the Ruby SDK, use the `create` method on the `users` service. ### Usage ```ruby theme={"system"} user = client.users.create( user_create_params: { linked_accounts: [ {type: :custom_auth, custom_user_id: "your-subject-id"}, {type: :email, address: "user@example.com"} ] } ) puts(user.id) ``` ### Parameters and Returns See the [API reference](/api-reference/users/create) for more details. Make a `POST` request to: ```sh theme={"system"} https://auth.privy.io/api/v1/users ``` Below is a **sample cURL command** for creating a new user: ```bash theme={"system"} $ curl --request POST https://auth.privy.io/api/v1/users \ -u ":" \ -H "privy-app-id: " \ -H 'Content-Type: application/json' \ -d '{ "linked_accounts": [ { "address": "batman@privy.io", "type": "email" } ] }' ``` ### Parameters An array including all of the user's linked accounts. These objects are in the same shape as the linked accounts returned by [`getUser`](/user-management/users/managing-users/querying-users). For each linked account, you must specify the `type` and must not include a `verifiedAt` timestamp. An object containing any custom metadata you want to associate with the user. This metadata will be returned in the user object when the user logs in. (Optional) An array of wallets to create for the user. The chain type of the wallet to create. The ID of the signer. The array of policy IDs that will be applied to wallet requests. If specified, this will override the base policy IDs set on the wallet. Currently, only one policy is supported per signer. List of policy IDs for policies that should be enforced on the wallet. Currently, only one policy is supported per wallet. Set to `true` to create a smart wallet with the user's wallet as the signer. Can only be set on wallets where `chainType` is `ethereum`. A successful response will include the new user object along with their DID: ```json theme={"system"} { "id": "did:privy:clddy332f002tyqpq3b3lv327", "created_at": 1674788927, "linked_accounts": [ { "address": "batman@privy.io", "type": "email", "verified_at": 1674788927 } ] } ``` User creation endpoints have a rate limit of 240 users in total per minute. If you are being rate limited, responses will have status code 429. We suggest you set up exponential back-offs starting at 1 second to seamlessly recover. # Migrating existing users to Privy Source: https://docs.privy.io/user-management/migrating-users-to-privy/overview Privy makes it easy for you to import existing user accounts from your existing auth setup by creating new users with Privy. At a high-level, your migration workflow involves two key components: **creating user accounts** and **ensuring users have continuous ownership over any assets stored in their wallets, including embedded wallets.** From these two pieces, you can easily switch over from a custom provider or add Privy to your existing auth flow. ## Importing user data **You can easily create users and their accounts with Privy and can even pre-generate Privy embedded wallets for them.** Privy supports both: * [just-in-time migration](#just-in-time-migration) so you can map your existing users to new Privy users as they log in * [proactive migration](#proactive-migration) to import user data into Privy all at once ### Just-in-time migration The simplest option is to "lazily" transfer your existing users to Privy. When an existing user logs in to your app via Privy for the first time, add their Privy DID to your internal users database to create a mapping between your existing user entry and [their Privy user object](/user-management/users/the-user-object). Namely, we suggest: 1. In your internal users database, add a `PrivyDID` column. 2. In the [**`onComplete`**](/authentication/user-authentication/ui-component) callback from Privy's `useLogin` hook, if the **`isNewUser`** flag is `true`, make a request to your backend with the user's Privy DID (`user.id`) and any account data you need to identify that user from your existing DB. For example, the request to your backend for a new user might include a body like: ```json theme={"system"} { "address": user.wallet?.address, "email": user.email?.address, "privyDID": user.id } ``` 3. When your backend receives the request from step (2), find the corresponding entry in your internal user database, matching on their wallet address, email address, or any other relevant account data. If entry does not have a `PrivyDID`, add the Privy DID from your request to the `PrivyDID` column in your database. If the entry already has a "Privy DID" in your database, it should match the Privy DID included in your request. There is nothing more to do. If there is no user matching the account information in the request, you can assume it is a new user in your internal database, and create an entry for them with their `PrivyDID`. In this way, you can maintain a mapping between your existing user data and the corresponding Privy user object. Your user data will be updated as your users login to your app using Privy. ### Proactive migration If your existing users database associates multiple linked accounts (e.g. email, wallet, Discord, etc.) to a single user, we recommend that you proactively migrate them to Privy using the [**create a batch of users**](/user-management/migrating-users-to-privy/create-or-import-a-batch-of-users) endpoint. This ensures you can migrate your users and preserve the links between their different accounts in Privy. Please see the instructions [here](/user-management/migrating-users-to-privy/create-or-import-a-batch-of-users) for more. ## Ensuring continuous asset ownership Once you've migrated your user data to Privy, you should next migrate user assets if necessary to ensure the transition is seamless for your users. This can be done by transferring over user addresses to Privy (migrating the wallet) or having them transfer assets to their new accounts (migrating the assets). The best path depends on your current setup and whether you need users to keep their existing wallets. We generally recommend transferring assets if you can. In most cases, migrating assets and/or wallets is only necessary if you are coming from another **embedded wallet** provider. If your users currently use external wallets to store their assets, you can simply import their address to Privy. ### If you are able to transfer assets If you are able to submit transactions on behalf of your users, you can set up batch transactions on your backend, sponsoring gas on behalf of your users to transfer their assets into pregenerated Privy wallets. When they next log in, prompt your users to run a one-time transfer to migrate their assets over to their new account. ### If you need to transfer wallets instead of transferring assets You can import user keys to Privy easily if you have access to them. This enables you to smoothly move your users' keys so they are managed by Privy's non-custodial system. We recommend you prompt your user to export their keys so they can use them with an external wallet (like MetaMask). Privy's system is non-custodial. This means neither you nor Privy will have any access to your user's private keys after the migration. # Custom metadata Source: https://docs.privy.io/user-management/users/custom-metadata Privy allows you to set custom metadata on the `user` object to store any app-specific metadata. This field is a generic JSON object up to 1KB in size. The JSON can contain arbitrary key-value pairs where the key is a `string` and value is a `string`, `integer`, or `boolean` (ie `{username: 'name', isVerified: true, age: 23}`). Privy supports two modes for writing custom metadata: * **Set (replace):** Replaces the entire `custom_metadata` object with the provided value. Any existing keys not included in the request are removed. * **Update (partial):** Performs a shallow merge of the provided keys into the existing `custom_metadata` object. Only the top-level keys you specify are updated; all unspecified keys are preserved. Partial updates are currently only supported via the REST API. The Node.js, Java, Rust, and Go SDKs do not yet support this operation. SDK support is coming soon. Use the **`PrivyClient`**'s **`setCustomMetadata`** method from the `users()` interface to set the custom metadata field for a user by their ID. As parameters, pass the user's ID as a `string` and the JSON object that you wish to set as custom metadata: ```ts theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID!, appSecret: process.env.PRIVY_APP_SECRET! }); try { const user = await privy.users().setCustomMetadata('insert-user-id', { custom_metadata: { username: 'name' } }); } catch (error) { console.error(error); } ``` If a matching user is found for the ID and the custom metadata object is valid, the method will return the corresponding **`User`** object with updated custom metadata. If no matching user is found, or the custom metadata input is malformed or too large (>1KB), the method will throw an error. You can set the custom metadata for a user by their ID using the `users().setCustomMetadata()` method. ```java theme={"system"} try { UserCustomMetadataSetRequestBody customMetadataRequestBody = UserCustomMetadataSetRequestBody.builder() .customMetadata(Map.of("username", CustomMetadata.of("name"))) .build(); UserCustomMetadataSetResponse customMetadataResponse = privyClient .users() .setCustomMetadata() .userId("did:privy:XXXXXX") .requestBody(customMetadataRequestBody) .call(); if (customMetadataResponse.user().isPresent()) { User updatedUser = customMetadataResponse.user().get(); } } catch (APIException e) { String errorBody = e.bodyAsString(); System.err.println(errorBody); } catch (Exception e) { System.err.println(e.getMessage()); } ``` ### Parameters When setting the custom metadata for a user, you may specify the following values on the `UserCustomMetadataSetRequestBody` builder: A map of custom metadata key-value pairs. The key is a `String` and value is `CustomMetadata` object, which can be a `String`, `double`, or `boolean`. ### Returns The `UserCustomMetadataSetResponse` object contains an optional `user()` field that contains the updated user object if the custom metadata was set successfully. The updated user object. See the [user object](/user-management/users/the-user-object) for more details. Use the **`PrivyClient`**'s **`set_custom_metadata`** method from the `users()` interface to set the custom metadata field for a user by their ID. As parameters, pass the user's ID as a `string` and the metadata object: ```rust theme={"system"} use privy_rs::{PrivyClient, generated::types::*}; use std::collections::HashMap; let client = PrivyClient::new(app_id, app_secret)?; // Create custom metadata using the typed enum values let mut metadata_map = HashMap::new(); metadata_map.insert("username".to_string(), CustomMetadataValue::String("name".to_string())); metadata_map.insert("isVerified".to_string(), CustomMetadataValue::Boolean(true)); metadata_map.insert("age".to_string(), CustomMetadataValue::Number(23.0)); let custom_metadata = CustomMetadata::from(metadata_map); let user = client .users() .set_custom_metadata("insert-user-id", &UserCustomMetadataSetRequestBody { custom_metadata, }) .await?; println!("Updated user: {}", user.id); ``` If a matching user is found for the ID and the custom metadata object is valid, the method will return the corresponding **`User`** object with updated custom metadata. If no matching user is found, or the custom metadata input is malformed or too large (>1KB), the method will return an error. ### Type Safety The Rust SDK provides type-safe custom metadata through the `CustomMetadataValue` enum: * `CustomMetadataValue::String(String)` for string values * `CustomMetadataValue::Number(f64)` for numeric values * `CustomMetadataValue::Boolean(bool)` for boolean values To set custom metadata for a user with the Go SDK, use the `SetCustomMetadata` method on the `Users` service. ### Usage ```go theme={"system"} user, err := client.Users.SetCustomMetadata( context.Background(), "did:privy:xxxxx", privy.UserSetCustomMetadataParams{ CustomMetadata: privy.CustomMetadata{ "role": privy.CustomMetadataItemUnion{OfString: privy.String("admin")}, "plan": privy.CustomMetadataItemUnion{OfString: privy.String("premium")}, "signupDate": privy.CustomMetadataItemUnion{OfString: privy.String("2024-01-15")}, }, }, ) if err != nil { log.Fatalf("failed to set custom metadata: %v", err) } fmt.Println("Updated user:", user.ID) ``` To set custom metadata for a user with the Ruby SDK, use the `set_custom_metadata` method on the `users` service. ### Usage ```ruby theme={"system"} user = client.users.set_custom_metadata( "did:privy:xxxxx", custom_metadata: { role: "admin", plan: "premium", signupDate: "2024-01-15" } ) puts(user.id) ``` ### Set custom metadata (replace) To replace the entire custom metadata object for a user with a given DID, make a `POST` request to: ```bash theme={"system"} https://auth.privy.io/api/v1/users//custom_metadata ``` Replace `` with your desired Privy DID. It should have the format `did:privy:XXXXXX`. Below is a sample cURL command for this request: ```bash theme={"system"} curl --request POST https://auth.privy.io/api/v1/users//custom_metadata \ -u ":" \ -H "privy-app-id: " \ -d '{ "custom_metadata": {"username": "name", "isVerified": true, "age": 23} }' ``` A successful response will include the user object associated with the DID, with the replaced custom\_metadata: ```json theme={"system"} { "id": "did:privy:cfbsvtqo2c22202mo08847jdux2z", "created_at": 1667165891, "custom_metadata": {"username": "name", "isVerified": true, "age": 23}, "linked_accounts": [ { "type": "email", "address": "user@gmail.com", "verified_at": 1667350653 } ] } ``` The `POST` method replaces the entire `custom_metadata` object. Any existing keys not included in the request body will be removed. ### Update custom metadata (partial) To partially update custom metadata for a user, make a `PATCH` request to the same endpoint: ```bash theme={"system"} https://auth.privy.io/api/v1/users//custom_metadata ``` Only the top-level keys you provide are updated. All unspecified keys are preserved. Below is a sample cURL command for this request: ```bash theme={"system"} curl --request PATCH https://auth.privy.io/api/v1/users//custom_metadata \ -u ":" \ -H "privy-app-id: " \ -d '{ "custom_metadata": {"username": "new-name"} }' ``` If the user previously had `{"username": "name", "isVerified": true, "age": 23}`, the response will include the merged result: ```json theme={"system"} { "id": "did:privy:cfbsvtqo2c22202mo08847jdux2z", "created_at": 1667165891, "custom_metadata": {"username": "new-name", "isVerified": true, "age": 23}, "linked_accounts": [ { "type": "email", "address": "user@gmail.com", "verified_at": 1667350653 } ] } ``` The `PATCH` method uses shallow merge behavior. Only top-level keys in the provided object are updated — nested objects are replaced entirely, not deep-merged. If there is no user associated with the provided DID, or the custom metadata input is malformed or the merged result exceeds 1KB, the API will return an error. # Identity tokens Source: https://docs.privy.io/user-management/users/identity-tokens Access user data securely with Privy identity tokens Identity tokens provide a secure and efficient way to access user data, especially on the server side. These tokens are JSON Web Tokens (JWTs) whose claims contain information about the currently authenticated user, including their linked accounts, metadata, and more. Privy strongly recommends using identity tokens when you need user-level data on your server. They allow you to easily pass a signed representation of the current user's linked accounts from your frontend to your backend directly, letting you verifiably determine which accounts (wallet address, email address, Farcaster profile, etc.) are associated with the current request. Enable identity tokens in the [Privy Dashboard](https://dashboard.privy.io/apps?page=login-methods\&logins=advanced) before implementing this feature. ## Enabling identity tokens To enable identity tokens for your application: 1. Navigate to your application dashboard's [User management > Authentication > Advanced](https://dashboard.privy.io/apps?logins=advanced\&page=login-methods) section 2. Toggle on **Return user data in an identity token** 3. Make sure you're using the latest version of the Privy SDK ## Token format Privy identity tokens are [JSON Web Tokens (JWT)](https://jwt.io/introduction), signed with the ES256 algorithm. These JWTs include the following claims: A stringified array containing a lightweight version of the current user's `linkedAccounts` A stringified version of the current user's `customMetadata` The user's Privy DID The token issuer, which should always be `privy.io` Your Privy app ID The timestamp of when the JWT was issued The timestamp of when the JWT will expire (generally 1 hour after issuance) ## Retrieving identity tokens Once you've enabled identity tokens, Privy will **automatically** include the identity token as a cookie on every request from your frontend to your server. For setups where you cannot use cookies, you can retrieve the identity token using the `useIdentityToken` hook or the `getIdentityToken` method: ```tsx theme={"system"} import { useIdentityToken, getIdentityToken } from '@privy-io/react-auth'; function MyComponent() { const { identityToken } = useIdentityToken(); // Use the token in your API requests const callApi = async () => { const response = await fetch('/api/your-endpoint', { headers: { 'privy-id-token': identityToken // or await getIdentityToken() if you need to get the token outside of the useIdentityToken hook } }); }; return ( ); } ``` We strongly recommend setting a base domain for your application, so that Privy can set the identity token as a more secure **HttpOnly** cookie. In React Native applications, you can get the current user's Privy token using the `getIdentityToken` method from the `useIdentityToken` hook: ```tsx theme={"system"} import { useIdentityToken } from '@privy-io/expo'; function MyComponent() { const { getIdentityToken } = useIdentityToken(); const callApi = async () => { const idToken = await getIdentityToken(); // For authenticated users, idToken will be a valid token // For unauthenticated users, idToken will be null if (idToken) { const response = await fetch('https://your-api.com/endpoint', { method: 'POST', headers: { 'privy-id-token': idToken, 'Content-Type': 'application/json' }, body: JSON.stringify({ /* your data */ }) }); } }; return ( ; } ``` To programmatically refresh the identity token, call `client.user.get()` from the `usePrivyClient` hook: ```tsx theme={"system"} import {usePrivyClient} from '@privy-io/expo'; import {Button, View} from 'react-native'; function RefreshButton() { const client = usePrivyClient(); const refreshUser = async () => { // Refresh and get the updated user data const user = await client.user.get(); }; return ( ); } ``` ### Callbacks You can optionally register an `onSuccess` or `onError` callback on the `useLinkAccount` hook. ```tsx theme={"system"} const {linkGoogle} = useLinkAccount({ onSuccess: ({user, linkMethod, linkedAccount}) => { console.log('Linked account to user ', linkedAccount); }, onError: (error) => { console.error('Failed to link account with error ', error) } }) ``` Optional callback to run after a user successfully links an account. Optional callback to run after there is an error during account linkage. **Looking for whitelabel `link` methods?** Our [`useLoginWith`](/authentication/user-authentication/login-methods/email) hooks allow will link an account to a user, provided that the user is already logged in whenever the authentication flow is completed. For headless wallet linking with `useLinkWithSiwe` or `useLinkWithSiws`, see the [whitelabel user management documentation](/user-management/users/whitelabel#react). ### Linking additional OAuth accounts The `linkOAuth` method allows your app to link [additional OAuth providers](/authentication/user-authentication/login-methods/custom-oauth) that are not natively supported by Privy. For built-in providers like Google or Twitter, use the dedicated methods (e.g., `linkGoogle`, `linkTwitter`). ```tsx theme={"system"} import {useLinkAccount} from '@privy-io/react-auth'; function LinkOAuthButton() { const {linkOAuth} = useLinkAccount(); return ( ); } ``` #### Parameters The `linkOAuth` method accepts an object with the following fields: The additional OAuth provider to link, in the format `'custom:'` (e.g., `'custom:twitch'`). ### Linking passkeys The `linkPasskey` method accepts an optional object with the following fields: ```tsx theme={"system"} import {useLinkAccount} from '@privy-io/react-auth'; function LinkPasskeyButton() { const {linkPasskey} = useLinkAccount(); return ( ); } ``` #### Parameters An optional display name to associate with the passkey. This name is shown to the user in their password manager (e.g. Google Password Manager, iCloud Keychain) when selecting which passkey to use for authentication. If not provided, the passkey defaults to your app name configured in the Privy Dashboard. ### Linking custom JWT accounts If your app uses an [external JWT-based authentication provider](/authentication/user-authentication/jwt-based-auth/setup), use the `useLinkJwtAccount` hook to link a custom JWT account to an already-authenticated Privy user. Unlike the methods above, this hook is headless: you provide the JWT yourself and Privy verifies it on the server. ```tsx theme={"system"} import {useLinkJwtAccount} from '@privy-io/react-auth'; function LinkJwtAccountButton() { const {linkWithCustomJwt, state} = useLinkJwtAccount(); const handleLink = async () => { const jwt = await getJwtFromExternalAuth(); await linkWithCustomJwt(jwt); }; return ( ); } ``` #### Parameters The JWT issued by your external authentication provider to link to the user's account. #### State The `state` property tracks the current state of the JWT linking flow: | Status | Description | | --------------- | ------------------------------------------- | | `'initial'` | The flow has not started | | `'loading'` | The JWT is being verified and linked | | `'not-enabled'` | Custom JWT auth is not enabled for this app | | `'done'` | The account was linked successfully | | `'error'` | An error occurred | #### Callbacks You can optionally pass `onSuccess` and `onError` callbacks into `useLinkJwtAccount`: ```tsx theme={"system"} const {linkWithCustomJwt, state} = useLinkJwtAccount({ onSuccess: ({user, linkMethod, linkedAccount}) => { console.log('JWT account linked successfully', linkedAccount); }, onError: (error) => { console.error('Failed to link JWT account', error); } }); ``` Custom JWT authentication must be enabled in your Privy Dashboard before using this hook. See the [JWT-based authentication setup docs](/authentication/user-authentication/jwt-based-auth/setup) for instructions. **To prompt a user to link an account, use the respective hooks:** | Account type | Description | Hook to invoke | | ------------ | ----------------------- | -------------------------------------------------- | | `Email` | Links email address | `useLinkEmail` | | `Phone` | Links phone number | `useLinkSMS` | | `Wallet` | Links external wallet | `useLinkWithSiwe`, `useLinkWithSiws` | | `Google` | Links Google account | `useLinkWithOAuth` | | `Apple` | Links Apple account | `useLinkWithOAuth` | | `Twitter` | Links Twitter account | `useLinkWithOAuth` | | `Discord` | Links Discord account | `useLinkWithOAuth` | | `Github` | Links Github account | `useLinkWithOAuth` | | `LinkedIn` | Links LinkedIn account | `useLinkWithOAuth` | | `TikTok` | Links TikTok account | `useLinkWithOAuth` | | `Spotify` | Links Spotify account | `useLinkWithOAuth` | | `Instagram` | Links Instagram account | `useLinkWithOAuth` | | `Telegram` | Links Telegram account | `useLinkWithOAuth` | | `Farcaster` | Links Farcaster account | `useLinkWithFarcaster` | | `Passkey` | Links passkey | `useLinkWithPasskey` from `@privy-io/expo/passkey` | Users are only permitted to link **a single account** for a given account type, except for wallets and passkeys. Concretely, a user may link at most one email address, but can link as many wallets and passkeys as they'd like. Use the `useLinkEmail` hook to link an email address to an existing user. ```tsx theme={"system"} import {useLinkEmail} from '@privy-io/expo'; const {sendCode, linkWithCode, state} = useLinkEmail(); ``` ### Send code ```tsx theme={"system"} sendCode({email: string}) => Promise<{success: boolean}> ``` The email address to send the verification code to. ### Link with code ```tsx theme={"system"} linkWithCode({code: string, email?: string}) => Promise ``` The one-time passcode received at the email address. Optional. The email address to link. If omitted, uses the email from `sendCode`. ### State The `state` property tracks the current state of the OTP flow: | Status | Description | | ----------------------- | -------------------------------------- | | `'initial'` | The flow has not started | | `'sending-code'` | The code is being sent | | `'awaiting-code-input'` | Waiting for the user to enter the code | | `'submitting-code'` | The code is being verified | | `'done'` | The email was linked successfully | | `'error'` | An error occurred | ### Callbacks ```tsx theme={"system"} const {sendCode, linkWithCode, state} = useLinkEmail({ onSendCodeSuccess: ({email}) => console.log('Code sent to', email), onLinkSuccess: ({user}) => console.log('Email linked', user), onError: (error) => console.error(error), }); ``` ### Usage ```tsx theme={"system"} import {useLinkEmail} from '@privy-io/expo'; function LinkEmailButton() { const {sendCode, linkWithCode, state} = useLinkEmail({ onLinkSuccess: ({user}) => console.log('Email linked!', user), onError: (error) => console.error('Link failed:', error), }); const handleSendCode = async () => { await sendCode({email: 'user@example.com'}); }; const handleVerify = async (code: string) => { await linkWithCode({code}); }; return ( ); } ``` ### Unlinking OAuth accounts The `useUnlinkOAuth` hook supports unlinking any OAuth provider, including built-in providers (e.g., `'google'`, `'twitter'`) and [additional OAuth providers](/authentication/user-authentication/login-methods/custom-oauth) (e.g., `'custom:twitch'`). ```tsx theme={"system"} import {usePrivy} from '@privy-io/react-auth'; import {useUnlinkOAuth} from '@privy-io/react-auth'; function UnlinkOAuthButton() { const {user} = usePrivy(); const {unlink: unlinkOAuth} = useUnlinkOAuth(); // Find the OAuth account to unlink const twitchAccount = user?.linkedAccounts?.find( (account) => account.type === 'custom:twitch' ); const handleUnlink = () => { if (twitchAccount) { unlinkOAuth({ provider: 'custom:twitch', subject: twitchAccount.subject }); } }; return ( ); } ``` #### Parameters The `unlink` method from `useUnlinkOAuth` accepts an object with the following fields: The OAuth provider to unlink. Use a built-in provider (e.g., `'google'`, `'twitter'`) or a custom provider in the format `'custom:'` (e.g., `'custom:twitch'`). The provider-specific subject identifier that uniquely identifies the user for the selected OAuth provider. This can be found in the linked account's `subject` field. The React Native SDK supports unlinking all supported account types via our modal-guided unlink methods. **To unlink an account, use the respective functions and hooks:** | Account type | Description | Hook to invoke | | ------------ | ------------------------- | -------------------- | | `Email` | Unlinks email address | `useUnlinkEmail` | | `Wallet` | Unlinks external wallet | `useUnlinkWallet` | | `Google` | Unlinks Google account | `useUnlinkOAuth` | | `Apple` | Unlinks Apple account | `useUnlinkOAuth` | | `Twitter` | Unlinks Twitter account | `useUnlinkOAuth` | | `Discord` | Unlinks Discord account | `useUnlinkOAuth` | | `Github` | Unlinks Github account | `useUnlinkOAuth` | | `LinkedIn` | Unlinks LinkedIn account | `useUnlinkOAuth` | | `TikTok` | Unlinks TikTok account | `useUnlinkOAuth` | | `Spotify` | Unlinks Spotify account | `useUnlinkOAuth` | | `Instagram` | Unlinks Instagram account | `useUnlinkOAuth` | | `Farcaster` | Unlinks Farcaster account | `useUnlinkFarcaster` | Users are only permitted to unlink **an account** so long as they have at least one more linked account. ### Usage ```tsx theme={"system"} import {useUnlinkEmail} from '@privy-io/expo'; const {unlinkEmail} = useUnlinkEmail(); await unlinkEmail({ email: 'user@example.com' }); ``` ### Parameters The email address to unlink from the current user. ### Callbacks You can optionally register an `onSuccess` or `onError` callback on the `useUnlinkEmail` hook. ```tsx theme={"system"} import {useUnlinkEmail} from '@privy-io/expo'; const {unlinkEmail} = useUnlinkEmail({ onSuccess: (user) => { console.log('Email unlinked', user); }, onError: (err) => console.error(err), }); ``` Optional callback to run after a user successfully unlinks an email address. Optional callback to run after there is an error during email unlinking. ### Usage ```tsx theme={"system"} import {useUnlinkWallet} from '@privy-io/expo'; const {unlinkWallet} = useUnlinkWallet(); await unlinkWallet({ address: 'wallet_address' }); ``` ### Parameters The wallet address to unlink from the current user. ### Callbacks You can optionally register an `onSuccess` or `onError` callback on the `useUnlinkWallet` hook. ```tsx theme={"system"} import {useUnlinkWallet} from '@privy-io/expo'; const {unlinkWallet} = useUnlinkWallet({ onSuccess: (user, isNewUser) => { console.log('Wallet unlinked', user, isNewUser); }, onError: (err) => console.error(err), }); ``` Optional callback to run after a user successfully unlinks a wallet. Optional callback to run after there is an error during wallet unlinking. ### Usage ```tsx theme={"system"} import {useUnlinkOAuth} from '@privy-io/expo'; const {unlinkOAuth} = useUnlinkOAuth(); await unlinkOAuth({ provider: 'google', subject: 'subject_identifier' }); ``` ### Parameters The OAuth provider for the account to unlink. The provider-specific subject ("sub" claim) that uniquely identifies the user for the selected OAuth provider. ### Usage ```tsx theme={"system"} import {useUnlinkFarcaster} from '@privy-io/expo'; const {unlinkFarcaster} = useUnlinkFarcaster(); await unlinkFarcaster({ fid: 123 }); ``` ### Parameters The Farcaster ID to unlink from the current user. ### Callbacks You can optionally register an `onSuccess` or `onError` callback on the `useUnlinkFarcaster` hook. ```tsx theme={"system"} import {useUnlinkFarcaster} from '@privy-io/expo'; const {unlinkFarcaster} = useUnlinkFarcaster({ onSuccess: (user) => console.log('Farcaster unlinked', user), onError: (err) => console.error(err), }); ``` Optional callback to run after a user successfully unlinks a Farcaster account. Optional callback to run after there is an error during Farcaster unlinking. Users are only permitted to unlink **an account** so long as they have at least one more linked account. Use the following method from the `email` handler to unlink an email address: ```swift theme={"system"} func unlink(email: String) async throws -> PrivyUser ``` ### Returns The updated user object with the email removed. ### Usage ```swift theme={"system"} do { // unlink email address let user = try await privy.email.unlink(email: email) // successfully unlinked email } catch { // error unlinking email } ``` Use the following method from the `sms` handler to unlink a phone number: ```swift theme={"system"} func unlink(phoneNumber: String) async throws -> PrivyUser ``` ### Returns The updated user object with the phone number removed. ### Usage ```swift theme={"system"} do { // unlink phone number let user = try await privy.sms.unlink(phoneNumber: phoneNumber) // successfully unlinked phone number } catch { // error unlinking phone number } ``` Use the following method from the `siwe` handler to unlink an Ethereum wallet: ```swift theme={"system"} func unlink(address: String) async throws -> PrivyUser ``` ### Returns The updated user object with the wallet removed. ### Usage ```swift theme={"system"} do { // unlink Ethereum wallet with address let user = try await privy.siwe.unlink(address: address) // successfully unlinked Ethereum wallet } catch { // error unlinking Ethereum wallet } ``` Use the following method from the `siws` handler to unlink a Solana wallet: ```swift theme={"system"} func unlink(address: String) async throws -> PrivyUser ``` ### Returns The updated user object with the wallet removed. ### Usage ```swift theme={"system"} do { // unlink Solana wallet with address let user = try await privy.siws.unlink(address: address) // successfully unlinked Solana wallet } catch { // error unlinking Solana wallet } ``` Use the following method from the `passkey` handler to unlink a user's passkey: ```swift theme={"system"} func unlink(credentialId: String) async throws -> PrivyUser ``` ### Returns The updated user object with the passkey removed. ### Usage ```swift theme={"system"} do { // unlink passkey with credential id let user = try await privy.passkey.unlink(credentialId: credentialId) // successfully unlinked passkey } catch { // error unlinking passkey } ``` Use the following method from the `oAuth` handler to unlink an OAuth account: ```swift theme={"system"} func unlink(with provider: OAuthProvider, subject: String) async throws -> PrivyUser ``` ### Parameters The OAuth provider for the account to unlink (e.g., `.google`, `.discord`, `.twitter`). The provider-specific subject identifier that uniquely identifies the user for the selected OAuth provider. ### Returns The updated user object with the OAuth account removed. ### Usage ```swift theme={"system"} do { // unlink OAuth account let user = try await privy.oAuth.unlink(with: provider, subject: subject) // successfully unlinked OAuth account } catch { // error unlinking OAuth account } ``` Users are only permitted to unlink **an account** so long as they have at least one more linked account. Use the following method from the `email` handler to unlink an email address: ```kotlin theme={"system"} public suspend fun unlink(email: String): Result ``` ### Returns A result type encapsulating the PrivyUser on success. ### Usage ```kotlin theme={"system"} val unlinkResult: Result = privy.email.unlink(email = "user@example.com") unlinkResult.fold( onSuccess = { updatedUser -> // Email successfully unlinked // updatedUser contains the updated user object with the email removed }, onFailure = { println("Error unlinking email: ${it.message}") } ) ``` Use the following method from the `sms` handler to unlink a phone number: ```kotlin theme={"system"} public suspend fun unlink(phoneNumber: String): Result ``` ### Returns A result type encapsulating the PrivyUser on success. ### Usage ```kotlin theme={"system"} val unlinkResult: Result = privy.sms.unlink(phoneNumber = "+1234567890") unlinkResult.fold( onSuccess = { updatedUser -> // Phone number successfully unlinked // updatedUser contains the updated user object with the phone number removed }, onFailure = { println("Error unlinking SMS: ${it.message}") } ) ``` Use the following method from the `siwe` handler to unlink an Ethereum wallet: ```kotlin theme={"system"} public suspend fun unlink(address: String): Result ``` ### Returns A result type encapsulating the PrivyUser on success. ### Usage ```kotlin theme={"system"} val unlinkResult: Result = privy.siwe.unlink(address = "wallet_address") unlinkResult.fold( onSuccess = { updatedUser -> // Wallet successfully unlinked // updatedUser contains the updated user object with the wallet removed }, onFailure = { println("Error unlinking wallet: ${it.message}") } ) ``` Use the following method from the `siws` handler to unlink a Solana wallet: ```kotlin theme={"system"} public suspend fun unlink(address: String): Result ``` ### Returns A result type encapsulating the PrivyUser on success. ### Usage ```kotlin theme={"system"} val unlinkResult: Result = privy.siws.unlink(address = "wallet_address") unlinkResult.fold( onSuccess = { updatedUser -> // Wallet successfully unlinked // updatedUser contains the updated user object with the wallet removed }, onFailure = { println("Error unlinking wallet: ${it.message}") } ) ``` Use the following method from the `passkey` handler to unlink a passkey: ```kotlin theme={"system"} public suspend fun unlink(credentialId: String): Result ``` ### Returns A result type encapsulating the PrivyUser on success. ### Usage ```kotlin theme={"system"} val unlinkResult: Result = privy.passkey.unlink(credentialId = "credential_id") unlinkResult.fold( onSuccess = { updatedUser -> // Passkey successfully unlinked // updatedUser contains the updated user object with the passkey removed }, onFailure = { println("Error unlinking passkey: ${it.message}") } ) ``` Users are only permitted to unlink **an account** so long as they have at least one more linked account. Use the following method from the `email` handler to unlink an email address: ```dart theme={"system"} Future> unlink(String email) ``` ### Parameters The email address to unlink from the user. ### Returns A Result object encapsulating the updated PrivyUser on success, providing immediate access to the user object with the email removed. ### Usage ```dart theme={"system"} final unlinkResult = await privy.email.unlink("user@example.com"); unlinkResult.fold( onSuccess: (updatedUser) { // Email successfully unlinked // updatedUser contains the updated user object with the email removed }, onFailure: (error) { print("Error unlinking email: ${error.message}"); }, ); ``` Use the following method from the `sms` handler to unlink a phone number: ```dart theme={"system"} Future> unlink(String phoneNumber) ``` ### Parameters The phone number to unlink from the user. ### Returns A Result object encapsulating the updated PrivyUser on success, providing immediate access to the user object with the phone number removed. ### Usage ```dart theme={"system"} final unlinkResult = await privy.sms.unlink("+1234567890"); unlinkResult.fold( onSuccess: (updatedUser) { // Phone number successfully unlinked // updatedUser contains the updated user object with the phone number removed }, onFailure: (error) { print("Error unlinking phone: ${error.message}"); }, ); ``` Use the following method from the `siwe` handler to unlink an Ethereum wallet: ```dart theme={"system"} Future> unlink(String address) ``` ### Parameters The Ethereum wallet address to unlink from the user. ### Returns A Result object encapsulating the updated PrivyUser on success, providing immediate access to the user object with the wallet removed. ### Usage ```dart theme={"system"} final unlinkResult = await privy.siwe.unlink("0x1234...5678"); unlinkResult.fold( onSuccess: (updatedUser) { // Wallet successfully unlinked // updatedUser contains the updated user object with the wallet removed }, onFailure: (error) { print("Error unlinking wallet: ${error.message}"); }, ); ``` Use the following method from the `siws` handler to unlink a Solana wallet: ```dart theme={"system"} Future> unlink(String address) ``` ### Parameters The Solana wallet address to unlink from the user. ### Returns A Result object encapsulating the updated PrivyUser on success, providing immediate access to the user object with the wallet removed. ### Usage ```dart theme={"system"} final unlinkResult = await privy.siws.unlink("wallet_address"); unlinkResult.fold( onSuccess: (updatedUser) { // Wallet successfully unlinked // updatedUser contains the updated user object with the wallet removed }, onFailure: (error) { print("Error unlinking wallet: ${error.message}"); }, ); ``` Use the following method from the `passkey` handler to unlink a passkey: ```dart theme={"system"} Future> unlink(String credentialId) ``` ### Parameters The credential ID of the passkey to unlink. ### Returns A Result object encapsulating the updated PrivyUser on success, providing immediate access to the user object with the passkey removed. ### Usage ```dart theme={"system"} final unlinkResult = await privy.passkey.unlink("credential_id"); unlinkResult.fold( onSuccess: (updatedUser) { // Passkey successfully unlinked // updatedUser contains the updated user object with the passkey removed }, onFailure: (error) { print("Error unlinking passkey: ${error.message}"); }, ); ``` Use the following method from the `oAuth` handler to unlink an OAuth account: ```dart theme={"system"} Future> unlink({ required OAuthProvider provider, required String subject, }) ``` ### Parameters The OAuth provider for the account to unlink (e.g., `OAuthProvider.google`, `OAuthProvider.discord`, `OAuthProvider.twitter`, `OAuthProvider.apple`, `OAuthProvider.telegram`). The provider-specific subject identifier that uniquely identifies the user for the selected OAuth provider. ### Returns A Result object encapsulating the updated PrivyUser on success, providing immediate access to the user object with the OAuth account removed. ### Usage ```dart theme={"system"} final unlinkResult = await privy.oAuth.unlink( provider: OAuthProvider.google, subject: subject, ); unlinkResult.fold( onSuccess: (user) { // Successfully unlinked OAuth account }, onFailure: (error) { print("Unlinking failed: ${error.message}"); }, ); ``` Make a `POST` request to: ```bash theme={"system"} https://auth.privy.io/api/v1/apps//users/unlink ``` Replace `` with your Privy app ID and pass in the following parameters: | Parameter | Type | Description | | ---------- | -------- | ------------------------------------------------------------------------------------------- | | `user_id` | `string` | Privy DID of the user | | `type` | `string` | Linked account type (see supported types below) | | `handle` | `string` | The identifier for the account (e.g., email address, wallet address) | | `provider` | `string` | (Only required for cross app unlinking) The cross app provider id, prefixed with `'privy:'` | ### Supported Account Types The following account types can be unlinked via the API: * `email` - Email accounts * `phone` - Phone number accounts * `wallet` - Externally connected wallets (Ethereum or Solana) * `smart_wallet` - Smart contract wallets * `farcaster` - Farcaster accounts * `telegram` - Telegram accounts * `cross_app` - Cross app accounts * OAuth providers: * `google_oauth` * `discord_oauth` * `twitter_oauth` * `github_oauth` * `linkedin_oauth` * `apple_oauth` * `spotify_oauth` * `instagram_oauth` * `tiktok_oauth` The API does not support unlinking `passkey`, `custom_auth`, or `guest` account types. ### Example Requests ```bash Email account theme={"system"} curl --request POST "https://auth.privy.io/api/v1/apps//users/unlink" \ -u ":" \ -H "privy-app-id: " \ -H 'Content-Type: application/json' \ -d '{ "user_id": "", "type": "email", "handle": "test@privy.io" }' ``` ```bash Wallet account theme={"system"} curl --request POST "https://auth.privy.io/api/v1/apps//users/unlink" \ -u ":" \ -H "privy-app-id: " \ -H 'Content-Type: application/json' \ -d '{ "user_id": "", "type": "wallet", "handle": "0x1234...5678" }' ``` ```bash OAuth account theme={"system"} curl --request POST "https://auth.privy.io/api/v1/apps//users/unlink" \ -u ":" \ -H "privy-app-id: " \ -H 'Content-Type: application/json' \ -d '{ "user_id": "", "type": "google_oauth", "handle": "" }' ``` ```bash Cross app account theme={"system"} curl --request POST "https://auth.privy.io/api/v1/apps//users/unlink" \ -u ":" \ -H "privy-app-id: " \ -H 'Content-Type: application/json' \ -d '{ "user_id": "", "type": "cross_app", "handle": "", "provider": "privy:" }' ``` ### Response If the unlinking is successful, the API will return a 200 status code. If there's no account associated with the Privy DID that matches the type and handle, the API will return a 400 status code. To unlink via the dashboard: 1. Navigate to the [Users page](https://dashboard.privy.io/?page=users\&tab=all-users) 2. Select the user 3. Click the button beside the account you want to unlink 4. Click `Unlink account` The unlink option won't appear if unlinking is not available for the account. Unlinking an account in the Privy Dashboard # Updating user accounts Source: https://docs.privy.io/user-management/users/updating-accounts To prompt users to change their email, you can use the `updateEmail` method from the `usePrivy` hook: ```tsx theme={"system"} updateEmail: () => void ``` ### Usage ```tsx theme={"system"} import {usePrivy} from '@privy-io/react-auth'; const {updateEmail} = usePrivy(); ``` When invoked, the method will open the Privy modal and guide the user through updating their existing email to a new one. If a user does not already have an email account and attempts to update it, Privy will throw an error indicating such. ### Example ```tsx theme={"system"} import {usePrivy} from '@privy-io/react-auth'; function Page() { const {ready, authenticated, user, updateEmail} = usePrivy(); return ( ); } ``` In the event that a user encounters an error through the flow, their existing account will be maintained. To prompt users to change their phone number, you can use the `updatePhone` method from the `usePrivy` hook: ```tsx theme={"system"} updatePhone: () => void ``` ### Usage ```tsx theme={"system"} import {usePrivy} from '@privy-io/react-auth'; const {updatePhone} = usePrivy(); ``` When invoked, the method will open the Privy modal and guide the user through updating their existing phone number to a new one. If a user does not already have a phone account and attempts to update it, Privy will throw an error indicating such. ### Example ```tsx theme={"system"} import {usePrivy} from '@privy-io/react-auth'; function Page() { const {ready, authenticated, user, updatePhone} = usePrivy(); return ( ); } ``` In the event that a user encounters an error through the flow, their existing account will be maintained. ### Callbacks To configure callbacks for Privy's `updateEmail` and `updatePhone` methods, use the `useUpdateAccount` hook: ```tsx theme={"system"} useUpdateAccount: ({ onSuccess?: ({user, updateMethod, updatedAccount}) => void, onError?: (error, details) => void }) => {updateEmail: () => void, updatePhone: () => void} ``` ### Usage ```tsx theme={"system"} import {useUpdateAccount} from '@privy-io/react-auth'; const {updateEmail, updatePhone} = useUpdateAccount({ onSuccess: ({user, updateMethod, updatedAccount}) => { console.log(user, updateMethod, updatedAccount); // Any logic you'd like to execute if the user successfully updates an account }, onError: (error, details) => { console.log(error, details); // Any logic you'd like to execute after a user exits the updateAccount flow or there is an error } }); // Then call one of the update methods in your code, which will invoke these callbacks on completion ``` ### Parameters The `useUpdateAccount` hook accepts an options object with the following fields: Optional callback to run after a user successfully updates an account. Optional callback to run if there is an error during the update account flow, or if the user exits the flow prematurely. ### Callback Details #### onSuccess If set, the `onSuccess` callback will execute after a user has successfully updated either their phone or email on their Privy account. Within this callback, you can access: The user object with the user's DID, linked accounts, and more. A string indicating the type of update flow just executed for the authenticated user. Possible values are `'email'` or `'sms'`. An object representing the account that was just updated on the authenticated user.
See an example of using the onSuccess callback for updating an account! As an example, you might configure an `onSuccess` callback to support the following behavior: * If the user updates an email to their account, add the new updated email to your own Users DB. Below is a template for implementing the above with `onSuccess`: ```tsx theme={"system"} const {updateEmail, updatePhone} = useUpdateAccount({ onSuccess: ({user, updatedAccount}) => { if (updatedAccount === 'email') { // show a toast, send analytics event, etc... } else if (updatedAccount === 'sms') { // show a toast, send analytics event, etc... } } }); ```
#### onError If set, the `onError` callback will execute after a user initiates an update account attempt and there is an error, or if the user exits the update account flow prematurely. Within this callback, you can access: The error code with more information about the error. A string indicating the type of update account flow just attempted for the authenticated user.
To update a user's email, use the `useUpdateEmail` hook: ```tsx theme={"system"} const {sendCode, updateEmail} = useUpdateEmail(); ``` ### Send an OTP First, use the `sendCode` method to send an OTP verification code to the user's new email address: ```tsx theme={"system"} sendCode: ({newEmailAddress: string}) => Promise ``` ### Usage ```tsx theme={"system"} import {useUpdateEmail} from '@privy-io/expo'; const {sendCode} = useUpdateEmail(); ``` ### Parameters The new email address to be validated. This will send a one-time passcode to the new email address, which the user will need to enter to verify it and confirm the update. The method returns a `Promise` that resolves if the code was sent successfully, and rejects otherwise. ### Example ```tsx theme={"system"} import {useUpdateEmail} from '@privy-io/expo'; function UpdateEmailForm() { const {sendCode} = useUpdateEmail(); const [newEmailAddress, setNewEmailAddress] = useState(''); return ( ); } ``` ### Verify the OTP Prompt the user for the OTP they received and verify the OTP by passing it to the `updateEmail` method: ```tsx theme={"system"} updateEmail: ({newEmailAddress: string, code: string}) => Promise ``` ### Usage ```tsx theme={"system"} import {useUpdateEmail} from '@privy-io/expo'; const {updateEmail} = useUpdateEmail(); ``` ### Parameters The new email address to set. The one time code received on the new email address. ### Returns A `Promise` that resolves with the updated user object if the update was successful, and rejects otherwise. ### Example ```tsx theme={"system"} import {useUpdateEmail} from '@privy-io/expo'; function ConfirmEmailUpdateForm() { const {updateEmail} = useUpdateEmail(); const [code, setCode] = useState(''); return ( ); } ``` To update a user's phone number, use the `useUpdatePhone` hook: ```tsx theme={"system"} const {sendCode, updatePhone} = useUpdatePhone(); ``` ### Send an OTP First, use the `sendCode` method to send an OTP verification code to the user's new phone number: ```tsx theme={"system"} sendCode: ({newPhoneNumber: string}) => Promise ``` ### Usage ```tsx theme={"system"} import {useUpdatePhone} from '@privy-io/expo'; const {sendCode} = useUpdatePhone(); ``` ### Parameters The new phone number to be validated. This will send a one-time passcode to the new phone number, which the user will need to enter to verify it and confirm the update. The method returns a `Promise` that resolves if the code was sent successfully, and rejects otherwise. ### Example ```tsx theme={"system"} import {useUpdatePhone} from '@privy-io/expo'; function UpdatePhoneForm() { const {sendCode} = useUpdatePhone(); const [newPhoneNumber, setNewPhoneNumber] = useState(''); return ( ); } ``` ### Verify the OTP Prompt the user for the OTP they received and verify the OTP by passing it to the `updatePhone` method: ```tsx theme={"system"} updatePhone: ({newPhoneNumber: string, code: string}) => Promise ``` ### Usage ```tsx theme={"system"} import {useUpdatePhone} from '@privy-io/expo'; const {updatePhone} = useUpdatePhone(); ``` ### Parameters The new phone number to set. The one time code received on the new phone number. ### Returns A `Promise` that resolves with the updated user object if the update was successful, and rejects otherwise. ### Example ```tsx theme={"system"} import {useUpdatePhone} from '@privy-io/expo'; function ConfirmPhoneUpdateForm() { const {updatePhone} = useUpdatePhone(); const [code, setCode] = useState(''); return ( ); } ``` To update a user's email, use the `privy.email.updateWithCode` method: ```swift theme={"system"} updateWithCode(_ code: String, sentTo email: String) async throws ``` ## Send an OTP First, use the `sendCode` method to send an OTP verification code to the user's new email address: ```swift theme={"system"} sendCode(to email: String) async throws ``` ### Usage ```swift theme={"system"} try await privy.email.sendCode(to: "newemail@privy.io") // successfully sent code to user's new email ``` ### Parameters The new email address to be validated. This will send a one-time passcode to the new email address, which the user will need to enter to verify it and confirm the update. ## Verify the OTP Prompt the user for the OTP they received and verify the OTP by passing it to the `updateWithCode` method: ```swift theme={"system"} updateWithCode(_ code: String, sentTo email: String) async throws ``` ### Usage ```swift theme={"system"} try await privy.email.updateWithCode("123456", sentTo: "newemail@privy.io") ``` ### Parameters The one time code received on the new email address. The new email address to set. To update a user's phone number, use the `privy.sms.updateWithCode` method: ```swift theme={"system"} updateWithCode(_ code: String, sentTo phoneNumber: String) async throws ``` ## Send an OTP First, use the `sendCode` method to send an OTP verification code to the user's new phone number: ```swift theme={"system"} sendCode(to phoneNumber: String) async throws ``` ### Usage ```swift theme={"system"} try await privy.sms.sendCode(to: "+1234567890") // successfully sent code to user's new phone number ``` ### Parameters The new phone number to be validated. This will send a one-time passcode to the new phone number, which the user will need to enter to verify it and confirm the update. ## Verify the OTP Prompt the user for the OTP they received and verify the OTP by passing it to the `updateWithCode` method: ```swift theme={"system"} updateWithCode(_ code: String, sentTo phoneNumber: String) async throws ``` ### Usage ```swift theme={"system"} try await privy.sms.updateWithCode("123456", sentTo: "+1234567890") ``` ### Parameters The one time code received on the new phone number. The new phone number to set. To update a user's email, use the `privy.email.updateWithCode` method: ```kotlin theme={"system"} suspend fun updateWithCode(code: String, email: String): Result ``` ## Send an OTP First, use the `sendCode` method to send an OTP verification code to the user's new email address: ```kotlin theme={"system"} suspend fun sendCode(email: String): Result ``` ### Usage ```kotlin theme={"system"} privy.email.sendCode(email = "newemail@privy.io").fold( onSuccess = { // Successfully sent code to user's new email address }, onFailure = { error -> // Handle error } ) ``` ### Parameters The new email address to be validated. This will send a one-time passcode to the new email address, which the user will need to enter to verify it and confirm the update. ## Verify the OTP Prompt the user for the OTP they received and verify the OTP by passing it to the `updateWithCode` method: ```kotlin theme={"system"} suspend fun updateWithCode(code: String, email: String): Result ``` ### Usage ```kotlin theme={"system"} privy.email.updateWithCode(code = "123456", email = "newemail@privy.io").fold( onSuccess = { // Email address successfully updated }, onFailure = { error -> // Handle error } ) ``` ### Parameters The one time code received on the new email address. The new email address to set. To update a user's phone number, use the `privy.sms.updateWithCode` method: ```kotlin theme={"system"} suspend fun updateWithCode(code: String, phoneNumber: String): Result ``` ## Send an OTP First, use the `sendCode` method to send an OTP verification code to the user's new phone number: ```kotlin theme={"system"} suspend fun sendCode(phoneNumber: String): Result ``` ### Usage ```kotlin theme={"system"} privy.sms.sendCode(phoneNumber = "+15551234567").fold( onSuccess = { // Successfully sent code to user's new phone number }, onFailure = { error -> // Handle error } ) ``` ### Parameters The new phone number to be validated. This will send a one-time passcode to the new phone number, which the user will need to enter to verify it and confirm the update. ## Verify the OTP Prompt the user for the OTP they received and verify the OTP by passing it to the `updateWithCode` method: ```kotlin theme={"system"} suspend fun updateWithCode(code: String, phoneNumber: String): Result ``` ### Usage ```kotlin theme={"system"} privy.sms.updateWithCode(code = "123456", phoneNumber = "+15551234567").fold( onSuccess = { // Phone number successfully updated }, onFailure = { error -> // Handle error } ) ``` ### Parameters The one time code received on the new phone number. The new phone number to set. Privy stores a snapshot of a user's Twitter profile, including their username, display name, and profile picture, on their `twitter_oauth` linked account. Privy refreshes this snapshot whenever the user logs in or re-authorizes their Twitter account. If a user changes their Twitter username while they have an active session, their linked account will still show the old username until they next authenticate. To refresh the account before then, make a `POST` request to: ```bash theme={"system"} https://api.privy.io/v1/users/twitter/refresh ``` ### Parameters The Twitter user ID of the account to refresh, taken from the `subject` field of the user's `twitter_oauth` linked account. This is Twitter's stable numeric identifier for the account, not the account's username. ### Example ```sh theme={"system"} curl --request POST \ --url https://api.privy.io/v1/users/twitter/refresh \ -u ":" \ --header 'privy-app-id: ' \ --header 'Content-Type: application/json' \ --data '{ "subject": "1234567890987654321" }' ``` ### Returns The refreshed `twitter_oauth` linked account, not the full user object: ```json theme={"system"} { "type": "twitter_oauth", "subject": "1234567890987654321", "username": "new_handle", "name": "Updated Name", "profile_picture_url": "https://pbs.twimg.com/profile_images/.../avatar.jpg", "verified_at": 1755000000, "first_verified_at": 1755000000, "latest_verified_at": 1755000000 } ``` Privy only overwrites a field when Twitter returns a value for it. If Twitter omits `username`, `name`, or `profile_picture_url`, the existing value is preserved rather than set to `null`. Refreshes are limited to once per day per account. If the linked account was refreshed or otherwise modified within the last 24 hours, Privy returns a `429` with the error `Twitter account refreshes are limited to once per day`. ### Error handling Both `404` and `429` cover two distinct conditions each, so your app should branch on the `error` message rather than the status alone. None of these responses include a `Retry-After` header. | Status | Error | How your app should respond | | ------ | ----------------------------------------------------------------- | --------------------------------------------------------------- | | `404` | `No Twitter account found with the provided subject for this app` | Check that the `subject` came from a linked account in this app | | `404` | `Twitter user no longer exists or has been suspended` | Stop retrying this `subject` | | `429` | `Twitter account refreshes are limited to once per day` | Retry after 24 hours | | `429` | `Twitter API rate limit exceeded, try again later` | Retry shortly; the linked account was not modified | For the full endpoint reference, see [Refresh Twitter account](/api-reference/users/refresh-twitter-account). # Handling events Source: https://docs.privy.io/user-management/users/webhooks/handling-events Respond to webhook events triggered by user actions in your application [Webhooks](/api-reference/webhooks/overview) notify your app in real time when users take actions. Privy sends a signed payload to a configured backend endpoint for each event. ## User events These events fire when users interact with accounts in your app. Fires when a new user is created. Fires when a user is deleted from your app. Fires when a user logs in. Fires when a user links a new login method. Fires when a user unlinks a login method. Fires when a user updates their email or phone number. Fires when a user transfers their account. ## Wallet events These events fire for embedded wallet actions. Fires when an embedded or smart wallet is created. Fires when a user exports their private key. Fires when a user sets up wallet recovery. Fires when a user recovers their embedded wallet. ## MFA events Fires when a user enables multi-factor authentication. Fires when a user disables multi-factor authentication. ## Next steps Set up endpoints, verify payloads, and configure retries. Monitor transactions, deposits, and withdrawals. # Whitelabel Source: https://docs.privy.io/user-management/users/whitelabel Privy enables complete control over user management flows, so you can match every user action to your app's brand and experience. Build your own UI for linking and unlinking accounts, managing user profiles, and more, while Privy handles the backend logic securely. Privy supports whitelabeling user management for linking and unlinking accounts. To whitelabel linking social accounts, use the `useLinkAccount` hook and call `link`. ```tsx theme={"system"} import {useLinkAccount} from '@privy-io/react-auth'; ``` ```tsx theme={"system"} const {linkGoogle, linkTwitter} = useLinkAccount(); linkGoogle(); linkTwitter(); ``` To link [additional OAuth providers](/authentication/user-authentication/login-methods/custom-oauth) that are not natively supported by Privy, use the `linkOAuth` method from the `useLinkAccount` hook. For built-in providers like Google or Twitter, use the dedicated methods (e.g., `linkGoogle`, `linkTwitter`). ```tsx theme={"system"} import {useLinkAccount} from '@privy-io/react-auth'; ``` ```tsx theme={"system"} const {linkOAuth} = useLinkAccount(); linkOAuth({provider: 'custom:twitch'}); ``` ### Parameters The `linkOAuth` method accepts an object with the following fields: The additional OAuth provider to link, in the format `'custom:'` (e.g., `'custom:twitch'`). ### Usage ```tsx theme={"system"} import {useLinkAccount} from '@privy-io/react-auth'; function LinkTwitchButton() { const {linkOAuth} = useLinkAccount({ onSuccess: ({user, linkMethod, linkedAccount}) => { console.log('Linked account:', linkedAccount); }, onError: (error) => { console.error('Failed to link account:', error); } }); return ( ); } ``` To whitelabel linking wallets, use the `useLinkWithSiwe` hook for Ethereum wallets or `useLinkWithSiws` hook for Solana wallets. These hooks allow you to generate messages, request signatures, and link wallets without using Privy's modal UI. To link an Ethereum wallet to a user via [SIWE](https://eips.ethereum.org/EIPS/eip-4361), use the React SDK's `useLinkWithSiwe` hook. ### Generate SIWE message ```tsx theme={"system"} generateSiweMessage({ address: string, chainId: string }) => Promise ``` EIP-55 checksum-encoded wallet address performing the signing. The chain ID to which the session is bound, in [CAIP-2 format](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md), e.g. `'eip155:1'`. ### Sign the SIWE message Request an EIP-191 `personal_sign` signature for the `message` returned by `generateSiweMessage` from the wallet. ```tsx theme={"system"} import {useWallets} from '@privy-io/react-auth'; const {wallets} = useWallets(); const signature = await wallets[0].sign(message); ``` Alternatively, you can request a signature from any external wallet or smart account: ```tsx theme={"system"} const signature = await wallet.signMessage({message}); ``` ### Link with SIWE ```tsx theme={"system"} linkWithSiwe({ signature: string, message: string, chainId: string, walletClientType?: string, connectorType?: string }) => Promise ``` The EIP-191 signature corresponding to the message. The EIP-4361 message returned by `generateSiweMessage`. The same [CAIP-2 formatted](https://github.com/ChainAgnostic/CAIPs/blob/main/CAIPs/caip-2.md) chain ID you passed to `generateSiweMessage`, e.g. `'eip155:1'`. Optional. The wallet client of the external wallet (e.g., `'metamask'`, `'coinbase_wallet'`). Defaults to `null` if not specified. Optional. The method used to connect the wallet to the application (e.g., `'injected'`, `'wallet_connect_v2'`). Defaults to `null` if not specified. ### Usage ```tsx theme={"system"} import {useLinkWithSiwe, useWallets} from '@privy-io/react-auth'; export function LinkWalletButton() { const {generateSiweMessage, linkWithSiwe} = useLinkWithSiwe(); const {wallets} = useWallets(); const handleLink = async () => { if (!wallets?.length) return; const activeWallet = wallets[0]; const message = await generateSiweMessage({ address: activeWallet.address, chainId: 'eip155:1' }); const signature = await activeWallet.sign(message); await linkWithSiwe({ message, chainId: 'eip155:1', signature }); }; return ; } ``` ### Callbacks You can optionally pass callbacks into `useLinkWithSiwe`: ```tsx theme={"system"} const {generateSiweMessage, linkWithSiwe} = useLinkWithSiwe({ onSuccess: ({user, linkMethod, linkedAccount}) => { console.log('Wallet linked successfully', linkedAccount); }, onError: (error) => { console.error('Failed to link wallet', error); } }); ``` To link a Solana wallet to a user via [SIWS](https://github.com/phantom/sign-in-with-solana), use the React SDK's `useLinkWithSiws` hook. ### Generate SIWS message ```tsx theme={"system"} generateSiwsMessage({ address: string }) => Promise ``` The Solana wallet address performing the signing. ### Sign the SIWS message Request a signature for the `message` returned by `generateSiwsMessage` from the Solana wallet. The message needs to be encoded as Uint8Array for signing. ```tsx theme={"system"} import {useWallets} from '@privy-io/react-auth/solana'; const {wallets} = useWallets(); const encodedMessage = new TextEncoder().encode(message); const results = await wallets[0].signMessage({message: encodedMessage}); ``` ### Link with SIWS ```tsx theme={"system"} linkWithSiws({ message: string, signature: string, walletClientType?: string, connectorType?: string }) => Promise<{ user: User; linkedAccount: LinkedAccountWithMetadata | null }> ``` The SIWS message returned from `generateSiwsMessage`. The signature corresponding to the message. Convert the signature bytes from the wallet's `signMessage` method to a base64-encoded string using `Buffer.from(results.signature).toString('base64')`. Optional. A string indicating the wallet client you'd like to associate with the wallet. Defaults to `'privy'`. Optional. A string indicating the connector type you'd like to associate with the wallet. Defaults to `'privy'`. ### Usage ```tsx theme={"system"} import {useLinkWithSiws} from '@privy-io/react-auth'; import {useWallets} from '@privy-io/react-auth/solana'; export function LinkSolanaWalletButton() { const {generateSiwsMessage, linkWithSiws} = useLinkWithSiws(); const {wallets} = useWallets(); const handleLink = async () => { if (!wallets?.length) return; const activeWallet = wallets[0]; const message = await generateSiwsMessage({ address: activeWallet.address }); const encodedMessage = new TextEncoder().encode(message); const results = await activeWallet.signMessage({message: encodedMessage}); // Convert signature bytes to string (base64) const signatureBase64 = Buffer.from(results.signature).toString('base64'); await linkWithSiws({ message, signature: signatureBase64 }); }; return ; } ``` ### Callbacks You can optionally pass callbacks into `useLinkWithSiws`: ```tsx theme={"system"} const {generateSiwsMessage, linkWithSiws} = useLinkWithSiws({ onSuccess: ({user, linkMethod, linkedAccount}) => { console.log('Solana wallet linked successfully', linkedAccount); }, onError: (error) => { console.error('Failed to link Solana wallet', error); } }); ``` To whitelabel updating a user's email address, use the `useUpdateEmail` hook: ```tsx theme={"system"} import {useUpdateEmail} from '@privy-io/react-auth'; const {state, sendCode, verifyCode} = useUpdateEmail(); ``` ### Send an OTP First, use the `sendCode` method to send an OTP verification code to the user's new email address: ```tsx theme={"system"} sendCode: ({newEmailAddress: string}) => Promise; ``` The new email address to send the verification code to. This sends a one-time passcode to the new email address, which the user must enter to verify and confirm the update. ### Verify the OTP Prompt the user for the OTP they received and verify it using the `verifyCode` method: ```tsx theme={"system"} verifyCode: ({code: string}) => Promise<{user: User} | undefined>; ``` The one-time code received on the new email address. The updated user object if the update was successful. ### State The `state` property provides the current state of the OTP flow: | Status | Description | | ----------------------- | --------------------------------------------- | | `'initial'` | The flow has not started | | `'sending-code'` | The code is being sent | | `'awaiting-code-input'` | Waiting for the user to enter the code | | `'submitting-code'` | The code is being verified | | `'done'` | The email was updated successfully | | `'error'` | An error occurred (includes an `error` field) | ### Usage ```tsx theme={"system"} import {useState} from 'react'; import {useUpdateEmail} from '@privy-io/react-auth'; function UpdateEmailForm() { const {state, sendCode, verifyCode} = useUpdateEmail(); const [newEmailAddress, setNewEmailAddress] = useState(''); const [code, setCode] = useState(''); if (state.status === 'initial' || state.status === 'sending-code') { return (
setNewEmailAddress(e.target.value)} placeholder="New email address" />
); } return (
setCode(e.target.value)} placeholder="Enter verification code" />
); } ``` ### Callbacks You can optionally pass callbacks into `useUpdateEmail`: ```tsx theme={"system"} const {state, sendCode, verifyCode} = useUpdateEmail({ onSuccess: ({user, updateMethod, updatedAccount}) => { console.log('Email updated successfully', user); }, onError: (error, details) => { console.error('Failed to update email', error, details); } }); ``` Optional callback that executes after a successful email update. Receives the updated user object, the update method (`'email'`), and the updated account. Optional callback that executes if there is an error during the email update flow.
To link a custom JWT account to an existing user, use the `useLinkJwtAccount` hook. This is useful for integrating with external authentication systems that issue JWTs. ```tsx theme={"system"} import {useLinkJwtAccount} from '@privy-io/react-auth'; ``` ```tsx theme={"system"} const {linkWithCustomJwt, state} = useLinkJwtAccount(); ``` ### Link with custom JWT ```tsx theme={"system"} linkWithCustomJwt(jwt: string) => Promise<{user: User}> ``` The JWT token from your external authentication system to link to the user's account. ### State The `state` property tracks the current state of the JWT linking flow: | Status | Description | | --------------- | ------------------------------------------- | | `'initial'` | The flow has not started | | `'loading'` | The JWT is being verified and linked | | `'not-enabled'` | Custom JWT auth is not enabled for this app | | `'done'` | The account was linked successfully | | `'error'` | An error occurred | ### Callbacks You can optionally pass callbacks into `useLinkJwtAccount`: ```tsx theme={"system"} const {linkWithCustomJwt, state} = useLinkJwtAccount({ onSuccess: ({user, linkMethod, linkedAccount}) => { console.log('JWT account linked successfully', linkedAccount); }, onError: (error) => { console.error('Failed to link JWT account', error); } }); ``` ### Usage ```tsx theme={"system"} import {useLinkJwtAccount} from '@privy-io/react-auth'; function LinkJwtAccountButton() { const {linkWithCustomJwt, state} = useLinkJwtAccount({ onSuccess: ({user, linkedAccount}) => { console.log('Account linked:', linkedAccount); }, onError: (error) => { console.error('Link failed:', error); } }); const handleLink = async () => { const jwt = await getJwtFromExternalAuth(); await linkWithCustomJwt(jwt); }; return ( ); } ``` Custom JWT authentication must be enabled in your Privy Dashboard before using this hook. See the [JWT-based authentication documentation](/authentication/user-authentication/jwt-based-auth/setup) for setup instructions. To whitelabel unlinking an account, use the dedicated unlink hooks from `@privy-io/react-auth`: ```tsx theme={"system"} import {useUnlinkEmail, useUnlinkWallet, useUnlinkOAuth} from '@privy-io/react-auth'; ``` ```tsx theme={"system"} const {unlink: unlinkEmail} = useUnlinkEmail(); const {unlink: unlinkWallet} = useUnlinkWallet(); const {unlink: unlinkOAuth} = useUnlinkOAuth(); // Unlink by passing the relevant identifier unlinkEmail({address: 'user@example.com'}); unlinkOAuth({provider: 'google', subject: 'google-subject-id'}); unlinkWallet({address: '0x...'}); ``` See the [unlinking accounts guide](/user-management/users/unlinking-accounts) for the full list of available hooks and parameters. To unlink any OAuth provider, including built-in providers (e.g., `'google'`, `'twitter'`) and [additional OAuth providers](/authentication/user-authentication/login-methods/custom-oauth) (e.g., `'custom:twitch'`), use the `useUnlinkOAuth` hook. ```tsx theme={"system"} import {useUnlinkOAuth} from '@privy-io/react-auth'; ``` ```tsx theme={"system"} const {unlink: unlinkOAuth} = useUnlinkOAuth(); unlinkOAuth({provider: 'custom:twitch', subject: '12345'}); ``` ### Parameters The `unlink` method from `useUnlinkOAuth` accepts an object with the following fields: The OAuth provider to unlink. Use a built-in provider (e.g., `'google'`, `'twitter'`) or a custom provider in the format `'custom:'` (e.g., `'custom:twitch'`). The provider-specific subject identifier that uniquely identifies the user for the selected OAuth provider. This can be found in the linked account's `subject` field. ### Usage ```tsx theme={"system"} import {usePrivy} from '@privy-io/react-auth'; import {useUnlinkOAuth} from '@privy-io/react-auth'; function UnlinkTwitchButton() { const {user} = usePrivy(); const {unlink: unlinkOAuth} = useUnlinkOAuth(); // Find the custom OAuth account to unlink const twitchAccount = user?.linkedAccounts?.find((account) => account.type === 'custom:twitch'); const handleUnlink = () => { if (twitchAccount) { unlinkOAuth({ provider: 'custom:twitch', subject: twitchAccount.subject }); } }; return ( ); } ```
Privy's React Native SDK is whitelabel by default allowing your app to build your own user management UI and flows using the SDK's functions. Get started with linking a social account [here](/user-management/users/linking-accounts#react-native). Privy's Android SDK is whitelabel by default, enabling apps to implement custom user management UI and flows using the SDK's functions. Get started with linking a social account [here](/user-management/users/linking-accounts#android). Privy's Swift SDK is whitelabel by default, enabling apps to implement custom user management UI and flows using the SDK's functions. Get started with linking a social account [here](/user-management/users/linking-accounts#swift). # Taking actions Source: https://docs.privy.io/wallets/accounts/actions Take actions with an account by acting on wallets within the account To take an action with an account, take an action on the wallet within the account that you'd like to use in your flow. As an example, to spend from an account's non-custodial wallet balance on EVM, use the [`/transfer`](/wallets/actions/transfer/overview) API with the non-custodial EVM wallet within the account. To take an action (such as executing a signature, transfer, or other transaction), there are three approaches. | Integration | Functionality | Recommended for | | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | | [Wallet action APIs](/wallets/actions/overview#available-actions) | Execute common onchain flows such as transfers, swaps, DeFi, and more via a simple, intuitive interface. Privy handles the onchain complexity. | Most use cases | | [Low-level RPC](/wallets/using-wallets/ethereum/sign-a-message) | Compute raw signatures and execute EVM and SVM JSON-RPC requests. Requires developers to construct their own transaction payloads and facilitate multi-transaction flows. | Custom use cases and lower-level flows | | [Intents](/transaction-management/intents/overview) | Supports asynchronous authorization of operations when authorization signatures cannot be collected synchronously. Supports both RPC and wallet action APIs. | Organizations and asynchronous approval workflows | We recommend using the abstracted **wallet action APIs** to perform most common actions. # Get account balance Source: https://docs.privy.io/wallets/accounts/balance Retrieve aggregated account balance across all wallets and supported chains in USD Retrieve the balance of an account, aggregated across all wallets and supported chains. The response includes the total balance in USD and a breakdown of individual asset balances. In particular, this endpoint returns balances for: * USDC on Ethereum, Base, Arbitrum, Polygon, Tempo, Solana * USDT on Ethereum, Base, Arbitrum, Polygon, Tempo, Solana * USDB on Ethereum, Base, Solana * ETH on Ethereum, Base, Arbitrum * SOL on Solana * POL on Polygon Balances are summed across all wallets in the account and are aggregated across chains; for example, the USDC balance represents the USDC balance across all supported chains. Accounts maintain a ledgered balance that may differ from the physical balance of the constituent wallets of an account on the blockchain. Given requirements for custodial wallets, the physical balance of a wallet may not be spendable until it passes KYC/AML screening, meaning the spendable balance of the account may differ from the actual value of assets in its wallets. We strongly recommend you use the Privy API to ledger the balance of your accounts instead of directly querying the blockchain due to custodial balance differences. View the full [API reference](/api-reference/accounts/update) for getting an account's balance. ## Usage To get an account's balance via REST API, make a `GET` request to: ```bash theme={"system"} https://api.privy.io/v1/accounts/{account_id}/balance ``` ### Path parameters The unique ID of the account to retrieve balances for. ### Response The response will include the following fields: The total balance across all assets. Contains: * `value` (string): The monetary value as a string * `currency` (string): The currency code (currently only 'usd' is supported) The individual asset balances, each computed across all supported chains. Each asset contains: * `symbol` (string): The symbol of the asset (e.g., USDC, ETH, SOL) * `amount` (string): The amount of the asset held, denominated in the unit of the asset itself, with 1 decimal of precision * `price` (object): The price of the asset in the provided currency * `value` (string): The monetary value as a string * `currency` (string): The currency code (currently only 'usd' is supported) ### Example #### Request ```bash theme={"system"} curl --request GET https://api.privy.io/v1/accounts//balance \ -u "your-app-id:your-app-secret" \ -H "privy-app-id: your-app-id" ``` #### Response ```json theme={"system"} { "total": { "value": "1477.58", "currency": "usd" }, "assets": [ { "symbol": "USDC", "amount": "500.0", "price": { "value": "1.00", "currency": "usd" } }, { "symbol": "ETH", "amount": "0.3", "price": { "value": "2448.53", "currency": "usd" } }, { "symbol": "SOL", "amount": "2.5", "price": { "value": "97.21", "currency": "usd" } } ] } ``` # Create an account Source: https://docs.privy.io/wallets/accounts/create Create a digital asset account with wallets across multiple chain types and custody configurations Accounts represent a grouping of wallets across multiple chain types and custody configurations. Think of accounts as a single unit of balance; create one account for each end user or customer of your service. When creating an account, specify: * a **display name** for the account * the account's **wallets**, either by providing a `wallets_configuration` to create new wallets, or a list of `wallet_ids` to add existing wallets to the account. For each wallet in a `wallets_configuration`, the wallet's [owner](/controls/authorization-keys/owners/overview) can also be specified. Accounts currently support non-custodial EVM wallets, custodial EVM wallets with Bridge, and non-custodial SVM wallets. View the full [API reference](/api-reference/accounts/create) for creating an account. ## Usage To create an account via REST API, make a `POST` request to: ```bash theme={"system"} https://api.privy.io/v1/accounts ``` ### Body An optional display name for the account. Provide exactly **one** of the following (`wallets_configuration` or `wallets_ids`) to specify the account's wallets: New wallets to create for the account, each specified with a chain type and optional custody configuration. At least one wallet is required. Maximum of five wallets total per account. Mutually exclusive with `wallet_ids`. Each item in the array has the following fields: The chain type of the wallet to create. The custody configuration for the wallet. If omitted, the wallet is non-custodial. The custody provider. Currently, `'bridge'` is the only supported value. The custody provider's unique ID for the KYC'ed entity associated with the wallet. IDs of existing wallets to include in the account. Must contain between one and five wallet IDs. Mutually exclusive with `wallets_configuration`. ### Response Unique ID of the created account. The display name of the account, or `null` if not set. The wallets included in the account. Each wallet contains: * `id` (string): The wallet ID * `chain_type` (`'ethereum' | 'solana'`): The chain type of the wallet * `address` (string): The on-chain address of the wallet * `custody` (object | undefined): The custody configuration, if the wallet is custodial ### Examples #### Using `wallets_configuration` Use `wallets_configuration` to create new wallets as part of the account. **Request** ```bash theme={"system"} curl --request POST https://api.privy.io/v1/accounts \ -u "your-app-id:your-app-secret" \ -H "privy-app-id: your-app-id" \ -H "Content-Type: application/json" \ -d '{ "display_name": "", "wallets_configuration": [ { "chain_type": "ethereum" }, { "chain_type": "ethereum", "custody": { "provider": "bridge", "provider_user_id": "" } }, { "chain_type": "solana" } ] }' ``` **Response** ```json theme={"system"} { "id": "", "display_name": "", "wallets": [ { "id": "", "chain_type": "ethereum", "address": "
" }, { "id": "", "chain_type": "ethereum", "address": "
", "custody": { "provider": "bridge", "provider_user_id": "" } }, { "id": "", "chain_type": "solana", "address": "
" } ] } ``` #### Using `wallet_ids` Use `wallet_ids` to group existing wallets into an account. **Request** ```bash theme={"system"} curl --request POST https://api.privy.io/v1/accounts \ -u "your-app-id:your-app-secret" \ -H "privy-app-id: your-app-id" \ -H "Content-Type: application/json" \ -d '{ "display_name": "", "wallet_ids": [ "", "" ] }' ``` **Response** ```json theme={"system"} { "id": "", "display_name": "", "wallets": [ { "id": "", "chain_type": "ethereum", "address": "
" }, { "id": "", "chain_type": "solana", "address": "
" } ] } ``` # Get an account Source: https://docs.privy.io/wallets/accounts/get Retrieve details of a specific account by its unique account ID Retrieve the details of a specific account by its unique account ID. The response includes the account's display name and all wallets associated with the account across different chain types and custody configurations. View the full API reference for [getting an account](/api-reference/accounts/get) or [listing all accounts](/api-reference/accounts/list). ## Usage To get an account via REST API, make a `GET` request to: ```bash theme={"system"} https://api.privy.io/v1/accounts/{account_id} ``` ### Path parameters The unique ID of the account to retrieve. ### Response The response will include the following fields: The unique ID of the account. An optional display name for the account. The wallets belonging to this account. Each wallet contains: * `id` (string): The wallet ID * `chain_type` ('ethereum' | 'solana'): The chain type of the wallet * `address` (string): The on-chain address of the wallet * `custody` (CustodyConfiguration | undefined): The custody configuration if the wallet is custodial If `custody` is undefined, the wallet is non-custodial. The `CustodyConfiguration` type is defined as `{provider: string; provider_user_id: string}` where: * `provider` is the custody provider. * `provider_user_id` is the custody provider's unique ID for the KYC'ed entity for the wallet. ### Example #### Request ```bash theme={"system"} curl --request GET https://api.privy.io/v1/accounts/ \ -u "your-app-id:your-app-secret" \ -H "privy-app-id: your-app-id" ``` #### Request ```json theme={"system"} { "id": "", "display_name": "", "wallets": [ { "id": "", "chain_type": "ethereum", "address": "0x4f3A1c8B2dE07f59Ca83b1eD6F42c9Ae5d03B7e" }, { "id": "", "chain_type": "ethereum", "address": "0x9bC2E4A0dF31856e7a4D9cB3F108e2Ac6b75d1E", "custody": { "provider": "bridge", "provider_user_id": "" } }, { "id": "", "chain_type": "solana", "address": "7mXkPqR3nWvJhYzT5sLdAeG2cFbN9pUoViQwKtBxC4D" } ] } ``` # Digital asset accounts Source: https://docs.privy.io/wallets/accounts/overview Digital asset accounts representing a single unit of balance across multiple wallets and chain types **Digital asset accounts**, or **accounts**, are a primitive for securely managing digital assets across multiple chains and custody configurations. Accounts are designed for teams building fintech, trading, and consumer applications that need to manage user balances and move assets across chains and custody models. Accounts represent a single unit of balance across these different wallets and configurations. You might create one account for each end user of your application, each customer of your service, or your own internal treasury. images/accounts-splash.png Built on top of **wallets**, accounts are built with the following design principles. 1. **Flexible custody**: accounts contain both custodial and non-custodial wallets, enabling you to satisfy the operational and regulatory requirements of your product while also accessing DeFi, stablecoin orchestration, and more 2. **Multi-chain first**: hold and transact with balances across Base, Solana, Tempo, Arbitrum, Ethereum, and any of Privy's Tier 3 chain types 3. **Simple abstractions for onchain actions**: leverage simple interfaces for transferring stablecoins, trading tokens, earning yield, bridging, and more. Accounts are currently a gated feature. Please reach out to [sales@privy.io](mailto:sales@privy.io) to request access. ## Features Accounts offer the same powerful capabilities as any Privy wallet; you can use the wallets within an account directly, enabling you to access the full functionality of the Privy API. Accounts enhance this functionality with simpler abstractions for: * **Seamless management of balances** across chains, wallets, and custody configurations * **Programmable policies** and cryptographically-enforced authorization with multi-party approvals * **Fiat / stablecoin orchestration** through licensed providers * **Earning yield with DeFi protocols** like Morpho, Aave, and Kamino * **Buy / sell / hold ETH, BTC, SOL** and other assets * **Webhooks for balances and transactions** to reconcile onchain events with your payment ledger ## Use cases Accounts can power common financial workflows including: * **Stablecoin pay-in / pay-out**: enable companies to automate settlements and payouts in real-time, reducing friction and cost. * **Global asset management**: enable entities around the world to hold balances in USD- or EUR-pegged stablecoins, compliant with local regulations and appetite towards custody * **Earn DeFi yield**: allow users and businesses to earn interest on their balances from lending protocols like Morpho, Kamino, or Aave * **Card issuance**: issue physical and digital debit cards that spend from your account's balance with programmable authorization controls * **Buy / sell / hold**: trade ETH, BTC, SOL, wrapped stocks, and more across various marketplaces # Configuring self-custody Source: https://docs.privy.io/wallets/accounts/self-custody Configure non-custodial digital accounts requiring user authentication for wallet actions Digital accounts support both **non-custodial** and **custodial** configurations. To create an account with a non-custodial configuration for a user, follow the steps below. [Create the account](/wallets/accounts/create), specifying the configuration for the wallets you'd like to include in the account (chain types and custody configuration). Make sure to include at least one non-custodial wallet in the account, i.e. with no custody provider set. Next, [update the non-custodial wallets](/wallets/wallets/update-a-wallet) in the account to set its [`owner`](/controls/authorization-keys/owners/types) as the end user. Lastly, when taking actions with non-custodial wallets in the account, [authorize the action](/controls/authorization-keys/using-owners/sign/signing-on-the-server) with a signature from the authenticated user. Privy verifies this signature before executing the action. With this configuration, any non-custodial wallets within the account require an authorization signature from the user to take actions. # Update an account Source: https://docs.privy.io/wallets/accounts/update Update an existing account display name or add new wallets to the account Update an existing account by its unique account ID. Update the account's display name and add new wallets to the account. When updating an account: * Update the **display name** for the account. * Add new **wallets** to the account, either by providing a `wallets_configuration` to create new wallets, or a list of `wallet_ids` to add existing wallets. Wallets cannot be removed from an account. [Update the wallets](/wallets/wallets/update-a-wallet) within an account directly to set [owners, signers,](/controls/authorization-keys/owners/overview) [policies](/controls/policies/overview), and more. View the full [API reference](/api-reference/accounts/update) for updating an account. ## Usage To update an account via REST API, make a `PATCH` request to: ```bash theme={"system"} https://api.privy.io/v1/accounts/{account_id} ``` ### Path parameters The unique ID of the account to update. ### Body An optional display name for the account. Provide exactly **one** of the following (`wallets_configuration` or `wallets_ids`) to add wallets to the account: New wallets to create and add to the account, each specified with a chain type and optional custody configuration. Maximum of five wallets total per account. Mutually exclusive with `wallet_ids`. Each item in the array has the following fields: The chain type of the wallet to create. The custody configuration for the wallet. If omitted, the wallet is non-custodial. The custody provider. Currently, `'bridge'` is the only supported value. The custody provider's unique ID for the KYC'ed entity associated with the wallet. IDs of existing wallets to add to the account. Must contain between one and five wallet IDs. Mutually exclusive with `wallets_configuration`. ### Response The unique ID of the account. The updated display name for the account, or `null` if not set. All wallets in the account, including any newly added wallets. Each wallet contains: * `id` (string): The wallet ID * `chain_type` (`'ethereum' | 'solana'`): The chain type of the wallet * `address` (string): The on-chain address of the wallet * `custody` (object | undefined): The custody configuration, if the wallet is custodial ### Examples #### Using `wallets_configuration` Use `wallets_configuration` to create and add new wallets to the account. **Request** ```bash theme={"system"} curl --request PATCH https://api.privy.io/v1/accounts/ \ -u "your-app-id:your-app-secret" \ -H "privy-app-id: your-app-id" \ -H "Content-Type: application/json" \ -d '{ "display_name": "Updated Account Name", "wallets_configuration": [ { "chain_type": "solana" } ] }' ``` **Response** ```json theme={"system"} { "id": "", "display_name": "Updated Account Name", "wallets": [ { "id": "", "chain_type": "ethereum", "address": "0x4f3A1c8B2dE07f59Ca83b1eD6F42c9Ae5d03B7e" }, { "id": "", "chain_type": "ethereum", "address": "0x9bC2E4A0dF31856e7a4D9cB3F108e2Ac6b75d1E", "custody": { "provider": "bridge", "provider_user_id": "" } }, { "id": "", "chain_type": "solana", "address": "7mXkPqR3nWvJhYzT5sLdAeG2cFbN9pUoViQwKtBxC4D" } ] } ``` #### Using `wallet_ids` Use `wallet_ids` to add existing wallets to the account. **Request** ```bash theme={"system"} curl --request PATCH https://api.privy.io/v1/accounts/ \ -u "your-app-id:your-app-secret" \ -H "privy-app-id: your-app-id" \ -H "Content-Type: application/json" \ -d '{ "wallet_ids": [ "" ] }' ``` **Response** ```json theme={"system"} { "id": "", "display_name": "Updated Account Name", "wallets": [ { "id": "", "chain_type": "ethereum", "address": "0x4f3A1c8B2dE07f59Ca83b1eD6F42c9Ae5d03B7e" }, { "id": "", "chain_type": "solana", "address": "7mXkPqR3nWvJhYzT5sLdAeG2cFbN9pUoViQwKtBxC4D" } ] } ``` # Claim rewards Source: https://docs.privy.io/wallets/actions/earn/claim Collect additional token incentives distributed by yield vaults. Some vaults distribute additional token incentives on top of base yield. Collect these with the [claim](/api-reference/wallets/earn/incentive-claim) endpoint. Claims operate at the chain level — pass a chain name rather than a vault ID. For apps with multiple vaults on the same chain, a single claim collects rewards across all vaults. Claiming rewards is separate from withdrawing yield — it does not affect withdrawals or ongoing earnings. View the full [API reference](/api-reference/wallets/earn/incentive-claim) for the claim endpoint. If your app has gas sponsorship configured, usage of the `/earn/ethereum/incentive/claim` endpoint will be [gas-sponsored by default](/wallets/actions/overview#gas-management). There is no need to specify additional parameters for sponsorship. ## Usage To claim rewards via REST API, make a `POST` request to: ```bash theme={"system"} https://api.privy.io/api/v1/wallets/{wallet_id}/earn/ethereum/incentive/claim ``` ### Parameters The blockchain network on which to perform the incentive claim (e.g. `"base"`, `"ethereum"`, `"arbitrum"`). Claims collect rewards across all vaults on the specified chain. Wallets with `owner_id` present must provide an [authorization signature](/api-reference/authorization-signatures) as a request header for claim operations. ### Returns The wallet action ID. Use this to poll status with [get wallet action](/api-reference/wallets/actions/get). The ID of the wallet claiming rewards. The action type. Always `"earn_incentive_claim"` for this endpoint. The current status of the claim action. The chain name for the claim. List of rewards claimed, each with `token_address`, `token_symbol`, `token_decimals`, and `amount`. Populated after the preparation step fetches claimable rewards. ISO 8601 timestamp of when the action was created. The execution steps. Only returned if `?include=steps` is provided on a GET request. ### Example ```bash theme={"system"} curl -X POST https://api.privy.io/api/v1/wallets/{wallet_id}/earn/ethereum/incentive/claim \ -H "privy-app-id: " \ -H "Authorization: Basic " \ -H "Content-Type: application/json" \ -d '{ "chain": "base" }' ``` ```json Example response theme={"system"} { "id": "", "wallet_id": "", "type": "earn_incentive_claim", "status": "pending", "chain": "base", "rewards": [ { "token_address": "0x1234567890abcdef1234567890abcdef12345678", "token_symbol": "MORPHO", "token_decimals": 18, "amount": "115631364898103632676" } ], "created_at": "2025-04-01T12:00:00.000Z" } ``` Track the claim by polling [get wallet action](/api-reference/wallets/actions/get) or listening for the [`wallet_action.earn_incentive_claim.succeeded`](/api-reference/webhooks/wallet-action/earn-incentive-claim/succeeded) webhook. ## Next steps Query a wallet's holdings and calculate earned yield. # Collect performance fees Source: https://docs.privy.io/wallets/actions/earn/collect-fees Collect an Aave vault's accrued performance fees to its admin wallet. Collect an Aave vault's accrued performance fees with the [collect fees](/api-reference/wallets/earn/fees-collect) endpoint. Fees accrue in the vault contract; this action moves the available fees from the vault to the vault's admin wallet. Fee collection is only supported for Aave vaults. This step is only necessary for Aave vaults. Morpho and Veda vaults accrue and distribute fees differently — Morpho fees accrue as shares in the admin wallet, and Veda distributes fees on a recurring schedule. See [revenue sharing](/wallets/actions/earn/revenue-sharing) for how fee collection works across providers. View the full [API reference](/api-reference/wallets/earn/fees-collect) for the collect fees endpoint. If your app has gas sponsorship configured, usage of the `/earn/ethereum/fees/collect` endpoint will be [gas-sponsored by default](/wallets/actions/overview#gas-management). There is no need to specify additional parameters for sponsorship. ## Usage Make a `POST` request to: ```bash theme={"system"} https://api.privy.io/api/v1/wallets/{wallet_id}/earn/ethereum/fees/collect ``` The `wallet_id` in the path must be the vault's **admin wallet** — find it in the Privy Dashboard or as `admin_wallet_id` in the [get vault details](/wallets/actions/earn/get-vault-details) response. A single call collects all available fees for the vault; partial collection is not supported. ### Parameters The ID of the vault to collect fees from. Must be an Aave vault. This endpoint accepts your app secret or a wallet signer. If the admin wallet was created automatically when the vault was deployed and has not been modified, your app secret alone authorizes the request. Admin wallets with an `owner_id` must provide an [authorization signature](/api-reference/authorization-signatures) as a request header. ### Returns The wallet action ID. Use this to poll status with [get wallet action](/api-reference/wallets/actions/get). The ID of the admin wallet collecting the fees. The action type. Always `"earn_fee_collect"` for this endpoint. The current status of the fee collection action. Chain identifier in CAIP-2 format. The ID of the vault fees were collected from. The onchain address of the vault contract. The underlying asset's token address. The collected amount in the underlying asset, as a human-readable decimal. `null` while the action is pending; populated after onchain confirmation. This is your app's share after the revenue split. The collected amount in the smallest unit of the underlying asset. Populated after onchain confirmation. ISO 8601 timestamp of when the action was created. ### Example ```bash theme={"system"} curl -X POST https://api.privy.io/api/v1/wallets/{wallet_id}/earn/ethereum/fees/collect \ -H "privy-app-id: " \ -H "Authorization: Basic " \ -H "Content-Type: application/json" \ -d '{ "vault_id": "" }' ``` ```json Example response theme={"system"} { "id": "", "wallet_id": "", "type": "earn_fee_collect", "status": "pending", "caip2": "eip155:8453", "vault_id": "", "vault_address": "0x1234567890123456789012345678901234567890", "asset_address": "0x1234567890abcdef1234567890abcdef12345678", "amount": null, "raw_amount": null, "created_at": "2025-04-01T12:00:00.000Z" } ``` Track the collection by polling [get wallet action](/api-reference/wallets/actions/get) or listening for the [`wallet_action.earn_fee_collect.succeeded`](/api-reference/webhooks/wallet-action/earn-fee-collect/succeeded) webhook. ## Next steps Deposit, withdraw, read positions, and collect fees for Aave vaults. # Deposit funds Source: https://docs.privy.io/wallets/actions/earn/deposit Deposit assets from a wallet into a yield vault. Deposit assets from a wallet into a vault using the [deposit](/api-reference/wallets/earn/deposit) endpoint. Privy handles the ERC-20 approval and deposit in a single call. The wallet depositing funds and the admin wallet will receive shares corresponding to the fee share split. View the full [API reference](/api-reference/wallets/earn/deposit) for the deposit endpoint. If your app has gas sponsorship configured, usage of the `/earn/ethereum/deposit` endpoint will be [gas-sponsored by default](/wallets/actions/overview#gas-management). There is no need to specify additional parameters for sponsorship. ### Deposit flow 1. Privy approves the vault to spend the specified amount of the wallet's ERC-20 tokens. 2. The vault converts the deposited assets into **vault shares** at the current share price. 3. Shares are split between the depositing wallet and your app's admin wallet based on the fee percentage configured during [setup](/wallets/actions/earn/setup). 4. The deposit is created as a wallet action with status `pending`. Privy prepares, signs, and broadcasts the transaction asynchronously. The status moves to `succeeded` once confirmed onchain. The `share_amount` in the response is `null` while the action is pending. Once the action succeeds, it reflects the number of shares minted to the depositing wallet. To see the admin wallet's fee shares, query the admin wallet's [position](/wallets/actions/earn/get-vault-position). The wallet must hold enough of the deposit token to cover the full amount. If the balance is insufficient, the deposit fails. Your app should check the wallet's token balance before initiating a deposit. ## Usage Use the `deposit` convenience method on the earn ethereum service to deposit assets into a vault. ```typescript theme={"system"} const response = await privy.wallets().earn().ethereum().deposit('insert-wallet-id', { vault_id: '', amount: '1.5', authorization_context: { authorization_private_keys: [''], }, }); ``` The method returns an `EarnDepositActionResponse` with the pending wallet action. Poll the status with [get wallet action](/api-reference/wallets/actions/get), or listen for the [`wallet_action.earn_deposit.succeeded`](/api-reference/webhooks/wallet-action/earn-deposit/succeeded) webhook. To deposit funds via REST API, make a `POST` request to: ```bash theme={"system"} https://api.privy.io/api/v1/wallets/{wallet_id}/earn/ethereum/deposit ``` ### Parameters The unique identifier for the vault. Copy this from the [Privy Dashboard](/wallets/actions/earn/setup#2-copy-the-vault-id) after deploying a fee wrapper. Human-readable decimal amount to deposit (e.g. `"1.5"` for 1.5 USDC). Exactly one of `amount` or `raw_amount` must be provided. Amount to deposit in the token's smallest unit (e.g. `"1500000"` for 1.5 USDC with 6 decimals). Exactly one of `amount` or `raw_amount` must be provided. Wallets with `owner_id` present must provide an [authorization signature](/api-reference/authorization-signatures) as a request header for deposit operations. ### Returns The wallet action ID. Use this to poll status with [get wallet action](/api-reference/wallets/actions/get). The ID of the wallet that deposited funds. The action type. Always `"earn_deposit"` for this endpoint. The current status of the deposit action. CAIP-2 chain identifier for the deposit (e.g. `"eip155:8453"`). The ID of the vault receiving the deposit. The ERC-4626 vault contract address. The address of the underlying asset token. Amount deposited in the token's smallest unit. Human-readable decimal amount (e.g. `"1.5"`). Only present when the token is known in the asset registry. Asset identifier (e.g. `"usdc"`). Only present when the token is known in the asset registry. Number of decimals for the underlying asset. Only present when the token is known in the asset registry. Vault shares received in base units. `null` until the action succeeds. ISO 8601 timestamp of when the action was created. The execution steps. Only returned if `?include=steps` is provided on a GET request. ### Example ```bash theme={"system"} curl -X POST https://api.privy.io/api/v1/wallets/{wallet_id}/earn/ethereum/deposit \ -H "privy-app-id: " \ -H "Authorization: Basic " \ -H "Content-Type: application/json" \ -d '{ "vault_id": "", "amount": "1.5" }' ``` ```json Example response theme={"system"} { "id": "", "wallet_id": "", "type": "earn_deposit", "status": "pending", "caip2": "eip155:8453", "vault_id": "", "vault_address": "0x5224d0c05698eD4a97C771B62095929F293f1D60", "asset_address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "raw_amount": "1500000", "amount": "1.5", "asset": "usdc", "decimals": 6, "share_amount": null, "created_at": "2025-04-01T12:00:00.000Z" } ``` Poll the status with [get wallet action](/api-reference/wallets/actions/get), or listen for the [`wallet_action.earn_deposit.succeeded`](/api-reference/webhooks/wallet-action/earn-deposit/succeeded) webhook to know when Privy confirms the deposit onchain. A `rejected` status means the action failed before any transaction was signed or broadcast — for example, due to insufficient balance or a policy violation. Your app can safely retry the request. A `failed` status means a transaction was broadcast but reverted onchain. Inspect the action's `steps` for details. ## Next steps Withdraw deposited assets with accrued yield. # Get vault details Source: https://docs.privy.io/wallets/actions/earn/get-vault-details Retrieve vault-level information like current APY, TVL, and available liquidity. Use the [get vault details](/api-reference/wallets/earn/get-vault-details) endpoint to retrieve vault-level information like current APY, TVL, and available liquidity. This is useful for displaying vault metrics to users before they deposit, or for checking liquidity before initiating a large withdrawal. View the full [API reference](/api-reference/wallets/earn/get-vault-details) for the get vault details endpoint. ## Usage Make a `GET` request to: ```bash theme={"system"} https://api.privy.io/api/v1/earn/ethereum/vaults/{vault_id} ``` ### Parameters The unique identifier for the vault. ### Returns The vault's unique identifier. Display name of the vault. The protocol powering the vault. Some response fields vary by provider (see below). The onchain address of the vault contract. The vault's underlying asset. The token contract address. The token symbol (e.g. `"usdc"`). The number of decimals for the token. Chain identifier in CAIP-2 format. Current APY in basis points (e.g. `500` = 5%). See the note below on when this is `null`. The application's share of the APY in basis points. See the note below on when this is `null`. Total value locked in the vault in USD. See the note below on when this is `null`. Liquidity available for withdrawal in USD. See the note below on when this is `null`. The ID of the vault's admin wallet, which receives your app's share of fees. The onchain address of the vault's admin wallet. The following fields are provider-specific: **Morpho vaults only.** Additional token-incentive rewards APR, in basis points. **Aave vaults only.** Performance fees currently available for your app to collect, in the smallest unit of the underlying asset. This is your app's share after the revenue split. Collect it with the [collect fees](/wallets/actions/earn/collect-fees) endpoint. For Morpho and Aave vaults, `user_apy`, `app_apy`, `tvl_usd`, and `available_liquidity_usd` are always populated. For Veda vaults, `available_liquidity_usd` is always `null`, and `user_apy`, `app_apy`, and `tvl_usd` may be `null` for the first 7–10 days after the vault is deployed, after which they are populated. ### Example ```bash theme={"system"} curl https://api.privy.io/api/v1/earn/ethereum/vaults/{vault_id} \ -H "privy-app-id: " \ -H "Authorization: Basic " ``` ```json Morpho vault theme={"system"} { "id": "", "name": "Gauntlet USDC Prime", "provider": "morpho", "vault_address": "0x04422053aDDbc9bB2759b248B574e3FCA76Bc145", "asset": { "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "symbol": "usdc", "decimals": 6 }, "caip2": "eip155:1", "user_apy": 500, "app_apy": 100, "tvl_usd": 1000000, "available_liquidity_usd": 500000, "admin_wallet_id": "", "admin_wallet_address": "0x1234abcd...", "total_rewards_apr": 50 } ``` ```json Aave vault theme={"system"} { "id": "", "name": "Aave USDC", "provider": "aave", "vault_address": "0x1234567890123456789012345678901234567890", "asset": { "address": "0x1234567890abcdef1234567890abcdef12345678", "symbol": "usdc", "decimals": 6 }, "caip2": "eip155:8453", "user_apy": 420, "app_apy": 80, "tvl_usd": 250000, "available_liquidity_usd": 250000, "admin_wallet_id": "", "admin_wallet_address": "0x1234abcd...", "available_fees": "1500000" } ``` Check `available_liquidity_usd` before initiating large withdrawals. If the vault's lending markets are fully utilized, a withdrawal may partially fill or fail. See [liquidity considerations](/wallets/actions/earn/withdraw#liquidity-considerations) for more details. ## Next steps Track deposit, withdrawal, and claim activity in real time. # Get vault position Source: https://docs.privy.io/wallets/actions/earn/get-vault-position Query a wallet's holdings, display balances, and calculate earned yield. Once Privy confirms a deposit, your app can query the wallet's holdings using the [get position](/api-reference/wallets/earn/get-position) endpoint. The `assets_in_vault` field shows the current redeemable value, including accrued yield. View the full [API reference](/api-reference/wallets/earn/get-position) for the get position endpoint. ## Usage To check a wallet's position via REST API, make a `GET` request to: ```bash theme={"system"} https://api.privy.io/api/v1/wallets/{wallet_id}/earn/ethereum/vaults?vault_id={vault_id} ``` ### Parameters The unique identifier for the vault. ### Returns The vault's underlying asset, with `address`, `symbol`, and `decimals` fields. Total amount deposited into the vault, in the token's smallest unit. Total amount withdrawn from the vault, in the token's smallest unit. Current redeemable value of the wallet's vault shares, including accrued yield. Number of vault shares held by the wallet. ### Example ```bash theme={"system"} curl https://api.privy.io/api/v1/wallets/{wallet_id}/earn/ethereum/vaults?vault_id={vault_id} \ -H "privy-app-id: " \ -H "Authorization: Basic " ``` ```json Example response theme={"system"} { "asset": { "address": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48", "symbol": "usdc", "decimals": 6 }, "total_deposited": "1000000", "total_withdrawn": "0", "assets_in_vault": "1050000", "shares_in_vault": "1000000000000000000" } ``` ## Display balances | Field | What it represents | How to use it | | ----------------- | ---------------------------------------- | ----------------------------------------------------- | | `assets_in_vault` | Current redeemable value including yield | Show this as the user's **current balance** | | `total_deposited` | Sum of all deposits | Use as the user's **cost basis** | | `total_withdrawn` | Sum of all withdrawals | Subtract from `total_deposited` for net contributions | | `shares_in_vault` | Raw vault shares held | Rarely shown to users — use `assets_in_vault` instead | ## Calculate earned yield To display how much yield a wallet has earned: ``` earned_yield = assets_in_vault - (total_deposited - total_withdrawn) ``` For example, if a wallet deposited 1,000 USDC, withdrew 200 USDC, and `assets_in_vault` is 850 USDC: ``` earned_yield = 850 - (1000 - 200) = 850 - 800 = 50 USDC ``` All amounts are in the token's smallest unit. For USDC (6 decimals), divide by `10^6` before displaying to users. For example, `1050000` equals 1.05 USDC. ## Next steps Retrieve vault-level information like APY, TVL, and available liquidity. # Overview Source: https://docs.privy.io/wallets/actions/earn/overview Put wallet balances to work with yield from tokenized money market funds and DeFi vaults. Earn enables apps to generate yield on wallet balances. Deposit into vaults, withdraw at any time, and track positions in real time, all with a few API calls. Privy simplifies vault deployment, smart contract interactions, and onchain execution. Earn ## Capabilities * **Deposit and withdraw** assets into yield vaults with a single API call per operation * **Query positions** to display real-time holdings, accrued yield, and vault shares * **Collect fees** from a configurable share of the yield generated through your app * **Sponsor gas** for your users — if your app has [gas sponsorship](/wallets/actions/overview#gas-management) enabled, Privy automatically sponsors gas for earn deposit, withdraw, and incentive claim actions ## Supported yield providers Earn supports multiple providers through a single API. Your app deposits, withdraws, and reads positions through the same endpoints, regardless of provider. Yield is generated via tokenized money market funds, other real world assets, and DeFi lending. A select set of yield sources are available in the Privy Dashboard for self-serve setup. Contact [sales@privy.io](mailto:sales@privy.io) to enable additional Veda, Aave, Morpho, and Kamino vaults from any curator, on any chain. Your app should make clear to end users that yield is generated via a tokenized money market fund, real world asset, or DeFi protocol independent from the wallet provider. Users keep full control of their assets and should explicitly direct the deposit action. ## Tokenized money market funds Tokenized money market funds (TMMFs) invest in short-term, high-quality debt such as US Treasury bills and repurchase agreements. The fund earns interest on these holdings, and that interest is the source of the yield. Rates track prevailing short-term benchmarks rather than onchain borrower demand. Yield reaches token holders in one of two ways, depending on the fund: * **Accruing tokens** rise in value as interest accrues; each redeems for more of the underlying asset over time. * **Distributing tokens** hold a stable value and pay yield as additional tokens on a recurring schedule. Contact [sales@privy.io](mailto:sales@privy.io) to enable tokenized money market fund yield for your app. ## DeFi yield DeFi vaults allocate deposited assets into onchain lending markets where borrowers pay interest to access liquidity. That interest flows back to the vault, increasing the value of deposited shares over time. Vault strategies are managed by curators who determine how capital is allocated across markets to balance risk and return. APY fluctuates based on borrower demand, market utilization, and the curator's allocation strategy. Some vaults also distribute additional token incentives on top of the base lending yield. All lending and borrowing happens onchain through non-custodial smart contracts. ### How DeFi yield accrues ERC-4626 vaults track balances in **shares**. A deposit converts assets into shares at the current share price. As interest accrues, the share price rises — each share redeems for more of the underlying asset. Yield accrues passively with no claiming or compounding required, and withdrawals return the original deposit plus earned yield. Example: a wallet deposits 1,000 USDC at a share price of 1.00 and receives 1,000 shares. When the share price reaches 1.05, those shares are worth 1,050 USDC. No new shares are minted; existing shares appreciate. Vault shares are standard ERC-20 tokens and can be transferred between wallets like any other token. ## Revenue sharing Your app can earn revenue by keeping a configurable share of the yield generated by its users' deposits. An admin wallet your app controls receives the fees. How the fee is capped, split, and collected depends on the vault provider — Morpho captures up to 50% of yield as shares in the admin wallet, Aave applies a performance fee of up to 100% (split 50/50 with Aave Labs), and Veda distributes fees per your agreement. See [revenue sharing](/wallets/actions/earn/revenue-sharing) for how to configure and collect fees across providers. ## Next steps Deploy a fee wrapper and configure your vault in the Privy Dashboard. A working Next.js app with end-to-end deposit and withdraw flows. Privy does not control DeFi vaults or underlying protocols. Vault information is provided for reference only and may change or be inaccurate. Earnings are generated from third-party vaults and are not guaranteed. Using vaults involves risk, including loss of funds. These materials are for general information purposes only and are not investment advice or a recommendation or solicitation to engage in any specific transaction. You are responsible for evaluating vaults at your own discretion. Privy does not provide investment, financial, legal, or tax advice. # Policies Source: https://docs.privy.io/wallets/actions/earn/policies Restrict earn deposits and withdrawals with Privy policies. Apps can use Privy policies to restrict which Earn actions a wallet or signer can take. For Earn, Privy evaluates policies against the original request body sent to the [`deposit`](/api-reference/wallets/earn/deposit) and [`withdraw`](/api-reference/wallets/earn/withdraw) endpoints before it prepares the underlying approval and vault transactions. That means Earn rules should match request-body fields like `vault_id`, `amount`, and `raw_amount`. ## Supported methods Earn policies currently support these rule methods: * `earn_deposit` * `earn_withdraw` If a wallet policy only allows `eth_sendTransaction`, Earn requests will still be denied. Wallets that call the Earn endpoints need explicit `earn_deposit` and/or `earn_withdraw` rules. ## Supported conditions Earn rules support `action_request_body` conditions for the fields below:
Field source Field Supported operators Notes
action\_request\_body vault\_id eq, in, in\_condition\_set Matches the Privy vault ID from the Dashboard.
action\_request\_body amount eq, gt, gte, lt, lte Value must be a positive decimal string, such as "1.5".
action\_request\_body raw\_amount eq, gt, gte, lt, lte Value must be an integer string in base units, such as "1500000".
Earn rules also support shared `system` conditions, such as `current_unix_timestamp`, for time-based controls. Earn policies use `chain_type: "ethereum"`. For Earn methods, the policy engine only accepts `action_request_body` and `system` conditions. ## Choose `amount` or `raw_amount` Use `amount` if your app sends human-readable decimal values like `"1.5"`. Use `raw_amount` if your app sends base-unit values like `"1500000"`. A single rule cannot condition on both `amount` and `raw_amount`. An Earn request includes one or the other, never both. This also affects runtime matching: * A rule using `amount` will not match a request that only sends `raw_amount`. * A rule using `raw_amount` will not match a request that only sends `amount`. Keep your policy format aligned with the request format your application actually sends. ## Example The example below allows: * deposits into one approved vault up to `1000` units of the asset * withdrawals from that same vault After creating the policy, apply it to the wallet with `policy_ids`. For signer-specific Earn permissions, attach the policy as an override policy on a signer instead. ```typescript theme={"system"} const policy = await privy.policies().create({ name: 'Approved earn vault policy', version: '1.0', chain_type: 'ethereum', rules: [ { name: 'Allow deposits up to 1000 into approved vault', method: 'earn_deposit', action: 'ALLOW', conditions: [ { field_source: 'action_request_body', field: 'vault_id', operator: 'eq', value: '', }, { field_source: 'action_request_body', field: 'amount', operator: 'lte', value: '1000.0', }, ], }, { name: 'Allow withdrawals from approved vault', method: 'earn_withdraw', action: 'ALLOW', conditions: [ { field_source: 'action_request_body', field: 'vault_id', operator: 'eq', value: '', }, ], }, ], }); await privy.wallets().update('', { policy_ids: [policy.id], }); ``` If the wallet has an `owner_id`, the wallet update must be authorized by that owner. Create the policy: ```bash theme={"system"} curl -X POST https://api.privy.io/v1/policies \ -H "privy-app-id: " \ -H "Authorization: Basic " \ -H "Content-Type: application/json" \ -d '{ "name": "Approved earn vault policy", "version": "1.0", "chain_type": "ethereum", "rules": [ { "name": "Allow deposits up to 1000 into approved vault", "method": "earn_deposit", "action": "ALLOW", "conditions": [ { "field_source": "action_request_body", "field": "vault_id", "operator": "eq", "value": "" }, { "field_source": "action_request_body", "field": "amount", "operator": "lte", "value": "1000.0" } ] }, { "name": "Allow withdrawals from approved vault", "method": "earn_withdraw", "action": "ALLOW", "conditions": [ { "field_source": "action_request_body", "field": "vault_id", "operator": "eq", "value": "" } ] } ] }' ``` Then apply the returned policy ID to a wallet: ```bash theme={"system"} curl -X PATCH https://api.privy.io/v1/wallets/ \ -H "privy-app-id: " \ -H "Authorization: Basic " \ -H "Content-Type: application/json" \ -d '{ "policy_ids": [""] }' ``` ## Common patterns * Restrict deposits to one vault by matching `vault_id`. * Reuse one rule across many vaults with `vault_id: in` or `vault_id: in_condition_set`. * Enforce maximum deposit or withdrawal size with `amount` or `raw_amount`. * Add time-based controls with `system.current_unix_timestamp`. ## Next steps Learn more about creating and managing policy objects. Apply different Earn permissions to different signers on the same wallet. # Aave Source: https://docs.privy.io/wallets/actions/earn/providers/aave Offer yield on [Aave](https://aave.com/) through [earn](/wallets/actions/earn/overview). Your app deposits, withdraws, and reads positions through the standard earn API, and Privy handles the Aave-specific contract interactions for you. Aave vaults are set up for your app. Contact [sales@privy.io](mailto:sales@privy.io) to get started — you choose the chain, the token to supply, and the performance fee. Privy enables the vault for your account. The vault will appear under **Wallet infrastructure > Earn** in the [Privy Dashboard](https://dashboard.privy.io), where you can view its vault ID, address, admin wallet, and live APY and TVL at any time. ## Resources How Aave's Earn vaults work on Aave v3 markets. Deposit, withdraw, and track positions across yield vaults with a single API. *** ## How Aave works with Privy Aave is served through Privy's native earn API. Once an Aave vault is enabled for your app, you interact with it through the standard earn endpoints — pass a `vault_id` to the deposit, withdraw, position, and vault-details endpoints. A few things are specific to Aave: * **Supplies to an Aave v3 market.** An Aave vault supplies a single token to an [Aave v3](https://aave.com/docs/aave-v3/overview) lending market on a given chain. You choose the chain and the token to supply when the vault is set up. * **Performance fee and revenue sharing.** You set a performance fee between 0% and 100% of the yield the vault earns. The performance fee is shared 50/50 with Aave Labs. For example, with a 20% performance fee, users keep 80% of the vault's yield, your admin wallet receives 10%, and Aave Labs receives 10%. * **Fee collection.** Performance fees accumulate in the vault rather than accruing to the vault's admin wallet automatically. Read the amount available to your app with `available_fees` from [get vault details](/wallets/actions/earn/get-vault-details), and collect it with the [collect performance fees](#collect-performance-fees) endpoint. Because Aave goes through the standard earn API, the [earn product docs](/wallets/actions/earn/overview) — [deposit](/wallets/actions/earn/deposit), [withdraw](/wallets/actions/earn/withdraw), [get vault details](/wallets/actions/earn/get-vault-details), [get vault position](/wallets/actions/earn/get-vault-position), and [webhooks](/wallets/actions/earn/webhooks) — all apply to Aave vaults. This guide highlights what's specific to Aave. ### Supported chains Privy deploys Aave vaults on Ethereum, Base, Optimism, Polygon, and Arbitrum. Each vault supplies its token to an [Aave market](https://app.aave.com/markets) on one of these chains. *** ## Collect performance fees Check how much is collectable with the [get vault details](/wallets/actions/earn/get-vault-details) endpoint. For Aave vaults, the response includes `available_fees` — the performance fees your app can currently collect, in the smallest unit of the underlying asset. This reflects your app's share after the split with Aave Labs. ```bash theme={"system"} curl https://api.privy.io/api/v1/earn/ethereum/vaults/{vault_id} \ -H "privy-app-id: " \ -H "Authorization: Basic " ``` Performance fees accumulate in the vault contract. To collect them, submit a fee collection action with the vault's **admin wallet** — this moves the available fees from the vault to the admin wallet. Call the collect fees endpoint with the admin wallet's ID and the `vault_id`. ```bash theme={"system"} curl -X POST https://api.privy.io/api/v1/wallets/{admin_wallet_id}/earn/ethereum/fees/collect \ -H "privy-app-id: " \ -H "Authorization: Basic " \ -H "Content-Type: application/json" \ -d '{ "vault_id": "" }' ``` * **Use the admin wallet.** The `wallet_id` in the path must be the vault's admin wallet. Find it in the Privy Dashboard or as `admin_wallet_id` in the [get vault details](/wallets/actions/earn/get-vault-details) response. * **Authentication.** If the admin wallet was created automatically when the vault was deployed and has not been modified, you can authenticate this request with just your app secret. * **Collects everything.** A single call collects all available fees for the vault; partial collection is not supported. The call creates a `pending` wallet action that moves to `succeeded` once confirmed onchain. Track it by polling [get wallet action](/api-reference/wallets/actions/get) or by subscribing to the `wallet_action.earn_fee_collect` [webhooks](/wallets/actions/earn/webhooks). See the [collect fees](/wallets/actions/earn/collect-fees) endpoint for the full request and response. The collected amount is your app's share of the performance fee, after the 50/50 split with Aave Labs. It is returned in the underlying asset and sent to the admin wallet. *** ## Key integration tips 1. **Set the performance fee thoughtfully.** It can range from 0% to 100% of yield and is split 50/50 with Aave Labs, so a 20% fee leaves users with 80% of the vault's yield. 2. **Collect accrued fees.** Monitor `available_fees` from get vault details, then collect them by calling the collect fees endpoint with the admin wallet. Until collected, fees remain in the vault rather than your app's wallet. Privy does not control DeFi vaults or underlying protocols. Vault information is provided for reference only and may change or be inaccurate. Earnings are generated from third-party vaults and are not guaranteed. Using vaults involves risk, including loss of funds. These materials are for general information purposes only and are not investment advice or a recommendation or solicitation to engage in any specific transaction. You are responsible for evaluating vaults at your own discretion. Privy does not provide investment, financial, legal, or tax advice. # Morpho Source: https://docs.privy.io/wallets/actions/earn/providers/morpho Offer yield on [Morpho](https://morpho.org/) through [earn](/wallets/actions/earn/overview). Deposit, withdraw, and read positions through the standard earn API—Privy handles the Morpho-specific contract interactions. Morpho vaults are self-serve: deploy a fee wrapper and configure a vault under **Wallet infrastructure > Earn** in the [Privy Dashboard](https://dashboard.privy.io). If the Morpho v2 vault that you would like to integrate is not listed in the Privy Dashboard, reach out to [sales@privy.io](mailto:sales@privy.io) to enable it for your app. See [setup](/wallets/actions/earn/setup) to get started. What's specific to Morpho: * **ERC-4626 vaults** managed by curators like [Gauntlet](https://www.gauntlet.xyz/), [Steakhouse](https://steakhouse.financial/), and [Sentora](https://sentora.com/). * **Fee-wrapper revenue sharing:** capture up to 50% of accrued yield as vault shares in your admin wallet; the rest accrues to depositors. * **Collect by withdrawing:** fees accrue as shares in the admin wallet, so withdraw them any time with the [withdraw](/wallets/actions/earn/withdraw) endpoint. The [collect fees](/wallets/actions/earn/collect-fees) endpoint is Aave-only. Privy does not control DeFi vaults or underlying protocols. Vault information is provided for reference only and may change or be inaccurate. Earnings are generated from third-party vaults and are not guaranteed. Using vaults involves risk, including loss of funds. These materials are for general information purposes only and are not investment advice or a recommendation or solicitation to engage in any specific transaction. You are responsible for evaluating vaults at your own discretion. Privy does not provide investment, financial, legal, or tax advice. # Veda Source: https://docs.privy.io/wallets/actions/earn/providers/veda Offer yield on Veda's [BoringVault](https://docs.veda.tech/) strategies through [earn](/wallets/actions/earn/overview). Your app deposits, withdraws, and reads positions through the standard earn API, and Privy handles the Veda-specific contract interactions (Teller, Accountant, and BoringVault share token) for you. Veda vaults are set up for your app. Veda deploys and operates the vault contracts, and Privy registers them to your app. Contact [sales@privy.io](mailto:sales@privy.io) to enable a Veda vault for your app. Once registered, the vault appears under **Wallet infrastructure > Earn** in the [Privy Dashboard](https://dashboard.privy.io), where you can view its vault ID and details. Before your vault is deployed, create a Privy [wallet](/wallets/overview) in the app that will use the vault, and share its address with Veda. This wallet will receive vault performance fees, and its address is embedded in the vault's parameters at deployment, so the wallet must exist first. ## Resources Official documentation for Veda and the BoringVault architecture. Deposit, withdraw, and track positions across yield vaults with a single API. *** ## How Veda works with Privy Veda is served through Privy's native earn API. Once a Veda vault is registered for your app, you interact with it through the standard earn endpoints — pass a `vault_id` to the deposit, withdraw, position, and vault-details endpoints. Under the hood, Veda differs from an ERC-4626 vault in a few ways: * **BoringVault share token.** A Veda vault is a [BoringVault](https://docs.veda.tech/): the vault contract is also its own ERC-20 share token. It is *not* ERC-4626, so there is no fee-wrapper contract — the vault you deposit into is the underlying vault itself. * **Teller entry point.** Deposits and withdrawals route through a **Teller** contract, and pricing comes from an **Accountant** contract rather than ERC-4626's `convertToAssets`. Privy handles all of this internally. * **Instant deposits and withdrawals.** Both operations settle synchronously in a single transaction. * **Rewards auto-compound.** Veda compounds reward incentives into the vault's share price automatically. There is **no separate incentive claim step** for Veda vaults. * **Share lock period.** After a deposit, shares are locked for a short, vault-configured period. Withdrawals revert until the lock expires. Because Veda goes through the standard earn API, the [earn product docs](/wallets/actions/earn/overview) — [deposit](/wallets/actions/earn/deposit), [withdraw](/wallets/actions/earn/withdraw), [get vault details](/wallets/actions/earn/get-vault-details), [get vault position](/wallets/actions/earn/get-vault-position), and [webhooks](/wallets/actions/earn/webhooks) — all apply to Veda vaults. This guide highlights what's specific to Veda. ### Supported chains Veda vaults are available across major EVM chains, including Ethereum, Base, Tempo, Arbitrum, Optimism, and Linea. Contact [sales@privy.io](mailto:sales@privy.io) for the current list of enabled vaults and chains for your app. *** ## Read vault details and positions Read vault APY and TVL with [get vault details](/wallets/actions/earn/get-vault-details), and a wallet's position with [get vault position](/wallets/actions/earn/get-vault-position). A few fields behave differently for Veda: For Veda vaults, `user_apy`, `app_apy`, and `tvl_usd` are sourced from Veda's analytics API and may be **`null`** — most commonly for a newly enabled vault, whose performance data takes roughly 7–10 days to populate. Your app should handle `null` gracefully rather than rendering `0`. `available_liquidity_usd` is always `null` for Veda vaults. For Veda vaults, `total_deposited` and `total_withdrawn` are computed from the deposits and withdrawals **initiated through Privy**, not reconstructed from all onchain activity. They are accurate as a cost basis for Privy-initiated positions. `assets_in_vault` and `shares_in_vault` are always read live from the vault contract. *** ## Withdraw with accrued yield Withdrawals redeem shares back to assets — plus accrued yield — via the [withdraw](/wallets/actions/earn/withdraw) endpoint. Withdraw up to the wallet's current `assets_in_vault`; for a full exit, read the position first and pass `assets_in_vault` as `raw_amount`. Veda locks a wallet's shares for a short, vault-configured period after each deposit, and a **new deposit resets the lock on the entire position**. A withdrawal requested while shares are still locked is `rejected` before any transaction is broadcast, with an error indicating when the shares unlock. Wait until the lock expires (or surface the unlock time to your user) before retrying. A `rejected` status means the action failed before any transaction was signed — for example, insufficient shares or a still-locked position — and is safe to retry once the underlying condition clears. A `failed` status means a transaction was broadcast but reverted onchain; inspect the action's `steps` for details. *** ## Key integration tips 1. **No incentive claim.** Veda auto-compounds rewards into the share price. Do not call the [incentive claim](/wallets/actions/earn/claim) endpoint for Veda vaults; yield shows up directly in `assets_in_vault`. 2. **Respect the share lock.** Gate your withdraw UI on the lock, especially right after a deposit. Handle the `rejected` status and its unlock timestamp instead of assuming an immediate withdrawal will succeed. 3. **Handle `null` analytics.** Treat `user_apy`, `app_apy`, and `tvl_usd` as optional — render a fallback while a newly enabled vault's data populates. Privy does not control DeFi vaults or underlying protocols. Vault information is provided for reference only and may change or be inaccurate. Earnings are generated from third-party vaults and are not guaranteed. Using vaults involves risk, including loss of funds. These materials are for general information purposes only and are not investment advice or a recommendation or solicitation to engage in any specific transaction. You are responsible for evaluating vaults at your own discretion. Privy does not provide investment, financial, legal, or tax advice. # Revenue sharing Source: https://docs.privy.io/wallets/actions/earn/revenue-sharing Keep a share of the yield generated by your users' Earn deposits. Your app can earn revenue by keeping a share of the yield generated by its users' deposits. This share is a performance fee: users keep the rest of the yield and all principal, and can withdraw at any time. An admin wallet your app controls receives the fees. The fee is set when the vault is configured, and how it is capped, split, and distributed depends on the vault provider. ## Configure the fee The performance fee is set at vault setup, not per deposit. For self-serve Morpho vaults, choose the fee percentage when you deploy the fee wrapper in the [Privy Dashboard](https://dashboard.privy.io). For Aave and Veda vaults, the fee is set when Privy enables the vault for your app. See [setup](/wallets/actions/earn/setup) to get started. Every vault has an **admin wallet**, which is a wallet your app controls that receives the fees. ## Fee limits by provider | Provider | Maximum fee | Revenue split | | ------------------------------------------------ | -------------------------- | -------------------------------------------------------------------------- | | [Morpho](/wallets/actions/earn/providers/morpho) | Up to 50% of yield | Your app keeps the full fee; the remaining yield accrues to depositors. | | [Aave](/wallets/actions/earn/providers/aave) | Up to 100% of yield | Yield not returned to users is split 50/50 between your app and Aave Labs. | | [Veda](/wallets/actions/earn/providers/veda) | Set by agreement with Veda | Defined in your custom agreement with Veda. | For example, an Aave vault with a 20% performance fee leaves users with 80% of the yield; the remaining 20% is split so your app receives 10% and Aave Labs receives 10%. Enabling an Aave or Veda vault, or raising a Morpho fee above the self-serve cap, is handled by Privy. Contact [sales@privy.io](mailto:sales@privy.io) to set these up. ## How fees are distributed Fees reach your admin wallet differently depending on the provider. * **Morpho**: fees accrue as vault shares directly in the admin wallet. Redeem the underlying asset any time with the [withdraw](/wallets/actions/earn/withdraw) endpoint. No separate collection step is required. * **Aave**: fees accumulate in the vault contract. Read the collectable amount from `available_fees` on [get vault details](/wallets/actions/earn/get-vault-details), then move it to the admin wallet with the [collect fees](/wallets/actions/earn/collect-fees) endpoint. * **Veda**: Veda claims accrued fees and withdraws them to your admin wallet on a recurring schedule, per your agreement with Veda. No action is required from your app. For Aave vaults, `available_fees` and the amount returned by collect fees reflect your app's share after the 50/50 split with Aave Labs. ## Next steps Deploy a fee wrapper and configure your vault in the Privy Dashboard. Collect an Aave vault's accrued performance fees to its admin wallet. Privy does not control DeFi vaults or underlying protocols. Vault information is provided for reference only and may change or be inaccurate. Earnings are generated from third-party vaults and are not guaranteed. Using vaults involves risk, including loss of funds. These materials are for general information purposes only and are not investment advice or a recommendation or solicitation to engage in any specific transaction. You are responsible for evaluating vaults at your own discretion. Privy does not provide investment, financial, legal, or tax advice. # Setting up earn Source: https://docs.privy.io/wallets/actions/earn/setup Deploy a fee wrapper and configure your vault in the Privy Dashboard. Your app configures earn settings from the [Privy Dashboard](https://dashboard.privy.io), under **Wallet infrastructure > Earn**. There are two ways to get a vault: deploy one yourself, or have Privy deploy one for you. ## Prerequisites * A Privy app with [embedded wallets](/wallets/overview) configured * API credentials (your app ID and app secret) * A [webhook endpoint](/api-reference/webhooks/overview) registered in the Privy Dashboard (recommended) ## Deploy a vault yourself Open the **Wallet infrastructure > Earn** page in the [Privy Dashboard](https://dashboard.privy.io) and configure a fee wrapper. During setup, your app selects: 1. A Morpho vault to allocate assets into 2. The percentage of generated yield your app receives 3. An admin wallet to claim fees and manage the vault configuration Do your own research when selecting a vault. You can find more information on the vaults supported in the Dashboard, including details on the liquidity and allocation below: * [Sentora PathUSD (PathUSD on Tempo)](https://app.morpho.org/tempo/vault/0x9a044AE05E5e6290DcF56afd69548565e957a626/sentora-pathusd) * [Gauntlet USDC Prime (USDC on Base)](https://app.morpho.org/base/vault/0x050cE30b927Da55177A4914EC73480238BAD56f0/gauntlet-usdc-prime) * [Steakhouse Prime Instant (USDC on Base)](https://app.morpho.org/base/vault/0xbeef0e0834849aCC03f0089F01f4F1Eeb06873C9/steakhouse-prime-instant) The admin wallet must sign onchain transactions to manage the vault and claim fees. Exchange wallets, cold storage, and other non-signing wallets do not work. Privy cannot reassign the admin wallet after creation. Privy assigns all fee wrapper roles to the admin wallet. Developers can update fee wrapper configurations in the future. Learn more about fee wrapper roles [here](https://docs.morpho.org/curate/concepts/roles/). Contact [sales@privy.io](mailto:sales@privy.io) to change the fee wrapper configuration. Privy-generated admin wallets have no owner by default, and your app secret alone authorizes them. [Assign an authorization key](/controls/authorization-keys/using-owners/assign) to require a second factor. See the [security checklist](/security/implementation-guide/security-checklist) for more details. ## Reach out to Privy for other vaults Contact [sales@privy.io](mailto:sales@privy.io) to enable any Aave, Veda, or Morpho vault not shown in the self-serve setup flow in the Privy Dashboard. These vaults will appear in the Dashboard. See the [Aave](/wallets/actions/earn/providers/aave) and [Veda](/wallets/actions/earn/providers/veda) integration guides for more information. ## Copy the vault ID After setup, Privy provides a unique `vault_id` for the vault. Copy this value — all deposit and withdraw API calls require it. To verify the vault is live, query its details with the [get vault details](/api-reference/wallets/earn/get-vault-details) endpoint: ```bash theme={"system"} curl https://api.privy.io/api/v1/earn/ethereum/vaults/{vault_id} \ -H "privy-app-id: " \ -H "Authorization: Basic " ``` The earn API is provider- and chain-agnostic: your app passes a `vault_id` and uses the same endpoints for every vault. Privy namespaces EVM endpoints under `/earn/ethereum/...`, which applies to vaults on any supported EVM chain — not just Ethereum mainnet. Privy routes each request to the vault's configured chain. ## Next steps Deposit assets from a wallet into a yield vault. Privy does not control DeFi vaults or underlying protocols. Vault information is provided for reference only and may change or be inaccurate. Earnings are generated from third-party vaults and are not guaranteed. Using vaults involves risk, including loss of funds. These materials are for general information purposes only and are not investment advice or a recommendation or solicitation to engage in any specific transaction. You are responsible for evaluating vaults at your own discretion. Privy does not provide investment, financial, legal, or tax advice. # Webhooks Source: https://docs.privy.io/wallets/actions/earn/webhooks Track earn deposit, withdrawal, incentive claim, and fee collection activity with real-time webhooks. Privy emits webhooks when earn wallet actions change status, allowing your app to react to deposits, withdrawals, incentive claims, and fee collections in real time without polling. ## Setup Subscribe to earn events from the **Configuration > Webhooks** page in the [Privy Dashboard](https://dashboard.privy.io/apps?page=webhooks). Webhooks can be tested at no cost in development environments. To enable webhooks in production, upgrade to the Enterprise plan in the Privy Dashboard. For general webhook setup instructions, payload verification, and retry behavior, see the [webhooks overview](/api-reference/webhooks/overview). ## Status lifecycle Every earn action (deposit, withdraw, incentive claim, fee collection) moves through a predictable status lifecycle. Privy emits a webhook at each transition. The action is received and queued for processing. At this point, no transaction has been signed or broadcast. The action reaches a terminal state: * **Succeeded** — the transaction confirmed onchain. * **Rejected** — the action failed **before** any transaction was signed or broadcast (e.g. insufficient balance, policy violation). Safe to retry. * **Failed** — a transaction was broadcast but reverted onchain. Inspect the action's `steps` for details. ## Event reference ### Deposit events Fired when a wallet deposits assets into a yield vault. | Event | Fired when | | ------------------------------------------------------------------------------------------------------ | ---------------------------------------- | | [`wallet_action.earn_deposit.created`](/api-reference/webhooks/wallet-action/earn-deposit/created) | A deposit action is created and queued | | [`wallet_action.earn_deposit.succeeded`](/api-reference/webhooks/wallet-action/earn-deposit/succeeded) | The deposit transaction confirms onchain | | [`wallet_action.earn_deposit.rejected`](/api-reference/webhooks/wallet-action/earn-deposit/rejected) | The deposit is rejected before broadcast | | [`wallet_action.earn_deposit.failed`](/api-reference/webhooks/wallet-action/earn-deposit/failed) | The deposit transaction reverts onchain | ### Withdrawal events Fired when a wallet redeems vault shares for the underlying asset plus accrued yield. | Event | Fired when | | -------------------------------------------------------------------------------------------------------- | ------------------------------------------- | | [`wallet_action.earn_withdraw.created`](/api-reference/webhooks/wallet-action/earn-withdraw/created) | A withdrawal action is created and queued | | [`wallet_action.earn_withdraw.succeeded`](/api-reference/webhooks/wallet-action/earn-withdraw/succeeded) | The withdrawal transaction confirms onchain | | [`wallet_action.earn_withdraw.rejected`](/api-reference/webhooks/wallet-action/earn-withdraw/rejected) | The withdrawal is rejected before broadcast | | [`wallet_action.earn_withdraw.failed`](/api-reference/webhooks/wallet-action/earn-withdraw/failed) | The withdrawal transaction reverts onchain | ### Incentive claim events Fired when a wallet claims additional token rewards distributed by the vault. | Event | Fired when | | ---------------------------------------------------------------------------------------------------------------------- | -------------------------------------- | | [`wallet_action.earn_incentive_claim.created`](/api-reference/webhooks/wallet-action/earn-incentive-claim/created) | A claim action is created and queued | | [`wallet_action.earn_incentive_claim.succeeded`](/api-reference/webhooks/wallet-action/earn-incentive-claim/succeeded) | The claim transaction confirms onchain | | [`wallet_action.earn_incentive_claim.rejected`](/api-reference/webhooks/wallet-action/earn-incentive-claim/rejected) | The claim is rejected before broadcast | | [`wallet_action.earn_incentive_claim.failed`](/api-reference/webhooks/wallet-action/earn-incentive-claim/failed) | The claim transaction reverts onchain | ### Fee collection events Fired when your app collects an Aave vault's accrued performance fees. See [collect performance fees](/wallets/actions/earn/providers/aave#collect-performance-fees). | Event | Fired when | | -------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | | [`wallet_action.earn_fee_collect.created`](/api-reference/webhooks/wallet-action/earn-fee-collect/created) | A fee collection action is created and queued | | [`wallet_action.earn_fee_collect.succeeded`](/api-reference/webhooks/wallet-action/earn-fee-collect/succeeded) | The fee collection transaction confirms onchain | | [`wallet_action.earn_fee_collect.rejected`](/api-reference/webhooks/wallet-action/earn-fee-collect/rejected) | The fee collection is rejected before broadcast | | [`wallet_action.earn_fee_collect.failed`](/api-reference/webhooks/wallet-action/earn-fee-collect/failed) | The fee collection transaction reverts onchain | ## Common patterns Listen for `wallet_action.earn_deposit.succeeded` to refresh the user's position. Once the webhook fires, call the [get position](/api-reference/wallets/earn/get-position) endpoint to fetch the updated `assets_in_vault` balance and display it in your UI. Listen for `wallet_action.earn_withdraw.succeeded` to trigger a notification. The webhook payload includes the `wallet_id` and `vault_id`, which your app can use to look up the user and send an in-app or push notification. A `rejected` status means no transaction was broadcast — for example, due to insufficient balance or a policy violation. Your app can safely prompt the user to retry. Check the action's error details to surface a helpful message. A `failed` status means a transaction was broadcast but reverted onchain. Use the [get wallet action](/api-reference/wallets/actions/get) endpoint with `?include=steps` to inspect what went wrong. Common causes include vault liquidity shortfalls or gas estimation issues. # Withdraw assets Source: https://docs.privy.io/wallets/actions/earn/withdraw Withdraw deposited assets with accrued yield from a vault. Redeem vault shares and return assets — plus any accrued yield — to the wallet using the [withdraw](/api-reference/wallets/earn/withdraw) endpoint. The returned `amount` reflects the original deposit plus yield earned. View the full [API reference](/api-reference/wallets/earn/withdraw) for the withdraw endpoint. If your app has gas sponsorship configured, usage of the `/earn/ethereum/withdraw` endpoint will be [gas-sponsored by default](/wallets/actions/overview#gas-management). There is no need to specify additional parameters for sponsorship. ## Withdrawal amount Your app can withdraw any amount up to the wallet's current `assets_in_vault` balance. To withdraw everything, query the wallet's [position](/wallets/actions/earn/get-vault-position) first and pass the full `assets_in_vault` value as the `raw_amount`. Because yield accrues continuously, the redeemable balance may be slightly higher at withdrawal time than when the position was last queried. For a full withdrawal, your app can read the wallet's `assets_in_vault` from the position endpoint and pass that value directly. Any additional yield accrued between the query and the withdrawal will remain in the vault as residual shares. ## Liquidity considerations Withdrawals depend on available liquidity in the underlying vault. Your app should check the vault's `available_liquidity_usd` from the [get vault details](/api-reference/wallets/earn/get-vault-details) endpoint before initiating large withdrawals. ## Usage Use the `withdraw` convenience method on the earn ethereum service to withdraw assets from a vault. ```typescript theme={"system"} const response = await privy.wallets().earn().ethereum().withdraw('insert-wallet-id', { vault_id: '', amount: '1.05', authorization_context: { authorization_private_keys: [''], }, }); ``` The method returns an `EarnWithdrawActionResponse` with the pending wallet action. Poll the status with [get wallet action](/api-reference/wallets/actions/get), or listen for the [`wallet_action.earn_withdraw.succeeded`](/api-reference/webhooks/wallet-action/earn-withdraw/succeeded) webhook. To withdraw funds via REST API, make a `POST` request to: ```bash theme={"system"} https://api.privy.io/api/v1/wallets/{wallet_id}/earn/ethereum/withdraw ``` ### Parameters The unique identifier for the vault. Human-readable decimal amount to withdraw (e.g. `"1.5"` for 1.5 USDC). Exactly one of `amount` or `raw_amount` must be provided. Amount to withdraw in the token's smallest unit (e.g. `"1500000"` for 1.5 USDC with 6 decimals). Exactly one of `amount` or `raw_amount` must be provided. Wallets with `owner_id` present must provide an [authorization signature](/api-reference/authorization-signatures) as a request header for withdraw operations. ### Returns The wallet action ID. Use this to poll status with [get wallet action](/api-reference/wallets/actions/get). The ID of the wallet receiving the withdrawn funds. The action type. Always `"earn_withdraw"` for this endpoint. The current status of the withdrawal action. CAIP-2 chain identifier for the withdrawal (e.g. `"eip155:8453"`). The ID of the vault being withdrawn from. The ERC-4626 vault contract address. The address of the underlying asset token. Amount withdrawn in the token's smallest unit. Human-readable decimal amount (e.g. `"1.5"`). Only present when the token is known in the asset registry. Asset identifier (e.g. `"usdc"`). Only present when the token is known in the asset registry. Number of decimals for the underlying asset. Only present when the token is known in the asset registry. Vault shares redeemed in base units. `null` until the action succeeds. ISO 8601 timestamp of when the action was created. The execution steps. Only returned if `?include=steps` is provided on a GET request. ### Example ```bash theme={"system"} curl -X POST https://api.privy.io/api/v1/wallets/{wallet_id}/earn/ethereum/withdraw \ -H "privy-app-id: " \ -H "Authorization: Basic " \ -H "Content-Type: application/json" \ -d '{ "vault_id": "", "amount": "1.05" }' ``` ```json Example response theme={"system"} { "id": "", "wallet_id": "", "type": "earn_withdraw", "status": "pending", "caip2": "eip155:8453", "vault_id": "", "vault_address": "0x5224d0c05698eD4a97C771B62095929F293f1D60", "asset_address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "raw_amount": "1050000", "amount": "1.05", "asset": "usdc", "decimals": 6, "share_amount": null, "created_at": "2025-04-01T12:00:00.000Z" } ``` Track the withdrawal by polling [get wallet action](/api-reference/wallets/actions/get) or listening for the [`wallet_action.earn_withdraw.succeeded`](/api-reference/webhooks/wallet-action/earn-withdraw/succeeded) webhook. ## Claim reward incentives Some vaults distribute additional token incentives on top of base yield. Collect these with the [claim](/api-reference/wallets/earn/incentive-claim) endpoint. Claims operate at the chain level — pass a chain name rather than a vault ID. For apps with multiple vaults on the same chain, a single claim collects rewards across all vaults. ### Usage Use the `claim` convenience method on the earn ethereum incentive service to claim reward incentives. ```typescript theme={"system"} const response = await privy.wallets().earn().ethereum().incentive().claim('insert-wallet-id', { chain: 'base', authorization_context: { authorization_private_keys: [''], }, }); ``` The method returns an `EarnIncentiveClaimActionResponse` with the pending wallet action. Claims operate at the chain level and collect rewards across all vaults on the specified chain. To claim rewards via REST API, make a `POST` request to: ```bash theme={"system"} https://api.privy.io/api/v1/wallets/{wallet_id}/earn/ethereum/incentive/claim ``` #### Parameters The blockchain network on which to perform the incentive claim (e.g. `"base"`, `"ethereum"`, `"arbitrum"`). Claims collect rewards across all vaults on the specified chain. Wallets with `owner_id` present must provide an [authorization signature](/api-reference/authorization-signatures) as a request header for claim operations. #### Returns The wallet action ID. Use this to poll status with [get wallet action](/api-reference/wallets/actions/get). The ID of the wallet claiming rewards. The action type. Always `"earn_incentive_claim"` for this endpoint. The current status of the claim action. The chain name for the claim. List of rewards claimed, each with `token_address`, `token_symbol`, `token_decimals`, and `amount`. Populated after the preparation step fetches claimable rewards. ISO 8601 timestamp of when the action was created. The execution steps. Only returned if `?include=steps` is provided on a GET request. #### Example ```bash theme={"system"} curl -X POST https://api.privy.io/api/v1/wallets/{wallet_id}/earn/ethereum/incentive/claim \ -H "privy-app-id: " \ -H "Authorization: Basic " \ -H "Content-Type: application/json" \ -d '{ "chain": "base" }' ``` ```json Example response theme={"system"} { "id": "", "wallet_id": "", "type": "earn_incentive_claim", "status": "pending", "chain": "base", "rewards": [ { "token_address": "0x1234567890abcdef1234567890abcdef12345678", "token_symbol": "MORPHO", "token_decimals": 18, "amount": "115631364898103632676" } ], "created_at": "2025-04-01T12:00:00.000Z" } ``` A `rejected` status means the action failed before any transaction was signed or broadcast — for example, due to insufficient balance or a policy violation. Your app can safely retry the request. A `failed` status means a transaction was broadcast but reverted onchain. Inspect the action's `steps` for details. ## Next steps Collect additional token incentives distributed by the vault. # Wallet action lifecycle Source: https://docs.privy.io/wallets/actions/lifecycle Understand how wallet actions progress through statuses, steps, and terminal states When your application executes a wallet action via one of the [available action APIs](/wallets/actions/overview#available-actions), Privy creates a **wallet action** resource that models the action asynchronously. This page covers how a wallet action progresses from creation to completion, the statuses it occupies, and the steps it executes along the way. ## Action statuses Every wallet action has a top-level `status` field that represents its overall progress. Privy derives this status from the outcomes of the action's individual steps. | Status | Description | Terminal | Safe to retry | | ----------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------- | ------------- | | `pending` | Privy created the action and is preparing or executing it. At least one step has not yet reached a terminal state. | No | — | | `succeeded` | All steps completed successfully. The action is fully executed on-chain. | Yes | — | | `rejected` | Privy rejected the action before any on-chain effects occurred. Common causes include policy violations, insufficient balance detected during simulation, or invalid parameters. | Yes | Yes | | `failed` | One or more steps failed during execution. There may be partial on-chain effects (e.g., an approval transaction succeeded but the subsequent swap reverted). | Yes | Inspect steps | Generally, the overall wallet action status should be sufficient. However, your application can use the [get wallet action](/wallets/actions/status) endpoint with `?include=steps` to inspect step-level details when a wallet action fails. ### Status transitions ```mermaid theme={"system"} flowchart LR pending --> succeeded pending --> rejected pending --> failed ``` A wallet action begins in `pending` and transitions to exactly one terminal state: `succeeded`, `rejected`, or `failed`. ## Step types Each wallet action is composed of one or more **steps**. A step represents a discrete operation, typically mapping to a single on-chain state change (e.g., a transaction or user operation). Privy executes steps sequentially — the next step begins only after the previous one confirms. To include step details in the response, your application passes `?include=steps` as a query parameter when [fetching the wallet action status](/wallets/actions/status). | Step type | Description | | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------- | | `evm_transaction` | A standard EVM transaction signed and broadcast by the wallet. | | `evm_user_operation` | An ERC-4337 user operation submitted via a bundler. Privy uses this when the wallet has gas sponsorship enabled or uses a smart contract account. | | `svm_transaction` | A Solana transaction signed and broadcast by the wallet. | | `external_transaction` | A cross-chain or cross-asset fill executed by an external provider (e.g., a bridge relay). The wallet does not sign this step directly. | ## Step statuses Each step has its own `status` field indicating where it is in its execution lifecycle. The available statuses vary by step type, since different blockchain environments have different failure modes. ### EVM step statuses (`evm_transaction` and `evm_user_operation`) | Status | Terminal | Description | | ----------- | -------- | -------------------------------------------------------------------------------------------------------------------- | | `queued` | No | Waiting for a previous step to complete before this step begins. | | `preparing` | No | Building the transaction data (constructing calldata, estimating gas, fetching nonce). | | `pending` | No | Privy broadcasted the transaction to the network and is awaiting inclusion in a block. | | `confirmed` | Yes | The network mined and executed the transaction successfully on-chain. | | `rejected` | Yes | Failed before broadcast — typically due to a policy violation, simulation failure, or signing error. | | `reverted` | Yes | The network mined the transaction but execution reverted on-chain (e.g., a smart contract require statement failed). | | `replaced` | Yes | A different transaction confirmed at the same nonce, invalidating this one. | | `abandoned` | Yes | Privy broadcasted the transaction, but the network never included it on-chain for unknown reasons. | ### SVM step statuses (`svm_transaction`) | Status | Terminal | Description | | ----------- | -------- | ----------------------------------------------------------------------------------------- | | `queued` | No | Waiting for a previous step to complete before this step begins. | | `preparing` | No | Building the Solana transaction (fetching recent blockhash, constructing instructions). | | `pending` | No | Privy broadcasted the transaction and is awaiting confirmation. | | `confirmed` | Yes | The network included the transaction in a confirmed slot and executed it successfully. | | `rejected` | Yes | Failed before broadcast — typically due to a policy violation or simulation failure. | | `reverted` | Yes | The network included the transaction on-chain but an instruction error caused it to fail. | | `failed` | Yes | Transaction timed out (e.g., the blockhash expired before inclusion). | ### External transaction step statuses (`external_transaction`) | Status | Terminal | Description | | ----------- | -------- | ------------------------------------------------------------------------------ | | `queued` | No | Waiting for a previous step to complete. | | `preparing` | No | Setting up the cross-chain fill with the external provider. | | `pending` | No | The external provider is processing the fill. | | `confirmed` | Yes | The fill completed successfully on the destination chain. | | `rejected` | Yes | The provider rejected the fill before execution. | | `failed` | Yes | The fill failed during execution (e.g., provider timeout or liquidity issues). | ## Steps by action type Privy treats the specific steps for each action type as an implementation detail, subject to change. This allows Privy to optimize how transactions land on-chain most effectively. Your application should be robust to varying steps for any wallet action type. ## How Privy derives the top-level status Privy computes the top-level wallet action `status` from the statuses of its steps: * **`succeeded`**: All steps reached `confirmed`. * **`rejected`**: Privy rejected the action during preparation (before creating any steps), or a step reached `rejected` without any post-broadcast failures. * **`failed`**: At least one step reached a post-broadcast failure state (`reverted`, `replaced`, `abandoned`, or `failed`). * **`pending`**: None of the above conditions apply — the action is still in progress. ## Webhooks Your application can subscribe to [wallet action webhooks](/wallets/actions/webhooks) to receive real-time notifications when an action reaches a terminal state, rather than polling for status updates. # Wallet actions Source: https://docs.privy.io/wallets/actions/overview Wallet action APIs for transfers, swaps, and DeFi interactions with asynchronous processing Privy's API provides abstractions for common actions taken by a wallet, including transfers, swaps, interactions with DeFi vaults, and more. These are called **wallet action APIs**. At a high-level, **wallet action APIs** abstract away the complexity of constructing blockchain transactions from scratch, managing blockchain state, and facilitating actions that may require multiple transactions or steps. Your service just integrates a simple API for wallet actions, and Privy handles the onchain complexity. images/wallet-actions-splash.png ### Taking actions with a wallet To take an action (such as executing a signature, transfer, or other transaction), there are three approaches. | Integration | Functionality | Recommended for | | ----------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------- | | [Wallet action APIs](/wallets/actions/overview#available-actions) | Execute common onchain flows such as transfers, swaps, DeFi, and more via a simple, intuitive interface. Privy handles the onchain complexity. | Most use cases | | [Low-level RPC](/wallets/using-wallets/ethereum/sign-a-message) | Compute raw signatures and execute EVM and SVM JSON-RPC requests. Requires developers to construct their own transaction payloads and facilitate multi-transaction flows. | Custom use cases and lower-level flows | | [Intents](/transaction-management/intents/overview) | Supports asynchronous authorization of operations when authorization signatures cannot be collected synchronously. Supports both RPC and wallet action APIs. | Organizations and asynchronous approval workflows | Generally, we recommend using the abstracted **wallet action APIs**, which are listed below. ### Available actions The current set of abstracted wallet actions APIs includes the endpoints below. All endpoints begin with the route `/v1/wallets/{wallet_id}/`, followed by the slug. | Action | Slug | Description | | ---------------------------------------------------------- | ------------- | ---------------------------------------------------------------------------------------------------------------------- | | [Transfer](/wallets/actions/transfer/overview) | `transfer` | Executes a transfer of stablecoins (USDC, USDT, USDB, EURC) or native tokens (ETH, SOL, POL) on EVM chains and Solana. | | [Swap](/wallets/actions/swap/overview) | `swap` | Swaps one asset in the wallet for another via [Uniswap](https://app.uniswap.org/). | | [Earn](/wallets/actions/earn/overview) | `earn` | Deposit into yield-generating vaults, withdraw with accrued yield, and claim reward incentives. | | [Payout](/financial-flows/transfers/fiat-payouts/overview) | `payout/fiat` | Converts crypto in the wallet to fiat and settles it to a registered bank account. | ### Wallet action lifecycle When your application executes a wallet action via one of the endpoints listed above, Privy creates a wallet action resource to model the action and returns it in the API response. Your application can inspect the wallet action resource to get the action's `type`, `status`, and `steps`. For a detailed breakdown of action statuses, step types, and step-specific statuses, see the [wallet action lifecycle](/wallets/actions/lifecycle) page. Your application can [fetch the wallet action resource](/wallets/actions/status) for a given action or subscribe to [webhooks](/wallets/actions/webhooks) on wallet action status updates. ### Authorization signatures For wallets with an [owner and/or signers](/controls/authorization-keys/owners/overview), requests to wallet actions APIs require an [authorization signature](/controls/authorization-keys/using-owners/sign/utility-functions) over the request. This signature is verified by the Privy TEE to authorize request execution. ### Gas management Wallet action APIs support two gas payment modes: **App pays (default):** Privy will **optimistically sponsor gas** for transactions if your application has [gas sponsorship](/wallets/gas-and-asset-management/gas/overview) enabled with sufficient credits. If gas sponsorship is not enabled, the wallet must pay gas from its own native token balance. **User pays:** Gas fees are paid directly from the wallet's USDC or USDT balance, with no ETH required. Once enabled in the [dashboard](/wallets/gas-and-asset-management/gas/setup), any Transfer API transaction using a supported token on a supported chain will automatically use that token to cover gas fees. This mode is currently only available on the [transfer API](/wallets/actions/transfer/overview). ### Policies Each wallet action API has its own [method](/controls/policies/overview) in Privy's policy language. For example, to enforce policies on the `/transfer` API, include a rule in the policy with `method: 'transfer'` to apply a rule to the API. Note that when a wallet action API is invoked, Privy does *not* enforce policy rules for RPC methods that may internally be used implicitly by the wallet action. As an example, when calling the `/transfer` API, policy rules with methods `eth_sendTransaction` and `signAndSendTransaction` are **not** enforced on those those API calls. Only policy rules with `method: 'transfer'` are enforced. See Privy's suggested configuration for [allowlisting wallet action APIs](/controls/policies/overview#allowlisted-rpcs-and-wallet-actions) in policies. # Set a reference ID Source: https://docs.privy.io/wallets/actions/reference-id A `reference_id` is an optional, developer-provided identifier that can be attached to a wallet action for reconciliation with your own internal records. It must be unique per app and can be up to 64 characters. This is useful when your request times out or you never receive the response: because you chose the `reference_id` yourself, you can still find the action afterwards without knowing the ID Privy assigned it. The `reference_id` is included in all wallet action payloads, including [webhook events](/wallets/actions/webhooks), and can be used to [fetch a wallet action by its reference ID](/api-reference/wallets/actions/external-id). A `reference_id` is **not** an idempotency key. Reusing a value returns a `400` rather than replaying the original action, so a second request with different parameters is never silently swallowed. For at-most-once delivery, use the `privy-idempotency-key` header instead. ## Supported actions The `reference_id` parameter is supported on the following wallet actions: * [Transfer](/api-reference/wallets/transfer/index) * [Swap](/api-reference/wallets/swap/quote) * [Earn deposit](/api-reference/wallets/earn/deposit) * [Earn withdraw](/api-reference/wallets/earn/withdraw) * [Earn incentive claim](/api-reference/wallets/earn/incentive-claim) * [Earn fee collect](/api-reference/wallets/earn/fees-collect) Pass the `reference_id` field in the request body when creating the action. ```bash theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/transfer \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "reference_id": "order-abc-123", "amount": "10.5", "source": { "asset": "usdc", "chain": "base" }, "destination": { "address": "" } }' ``` ```bash theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/swap \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "reference_id": "order-abc-123", "base_amount": "1000000000000000000", "amount_type": "exact_input", "source": { "asset_address": "native", "caip2": "eip155:8453" }, "destination": { "asset_address": "", "caip2": "eip155:8453" } }' ``` ```bash theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/earn/ethereum/deposit \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "reference_id": "order-abc-123", "vault_id": "cm7oxq1el000e11o8iwp7d0d0", "amount": "1.5" }' ``` ## Looking up wallet actions by reference ID Once a `reference_id` has been set, your app can look up the associated wallet action using the [get wallet action by external ID](/api-reference/wallets/actions/external-id) endpoint. Unlike the per-wallet endpoints, this searches across every wallet in your app, so you do not need to know which wallet performed the action: ```bash theme={"system"} curl --request GET \ --url 'https://api.privy.io/v1/actions?reference_id=order-abc-123' \ --header 'Authorization: Basic ' \ --header 'privy-app-id: ' ``` Pass `?include=steps` to expand step-level details in the response. If no action matches, the endpoint returns `200` with an empty list rather than a `404`, so a caller polling for an action it may not have created yet does not have to treat `404` as a success case. Your app can also retrieve the action directly by its Privy-assigned ID using the [get wallet action](/api-reference/wallets/actions/get) endpoint. The `reference_id` is included in the response. ## Duplicate reference IDs A `reference_id` must be unique per app. Creating a second wallet action with a value your app has already used returns a `400`: ```json theme={"system"} { "error": "A wallet action with this reference_id already exists for this app" } ``` The action is rejected before anything is signed or broadcast, so a duplicate is always safe to retry with a fresh value. ## Webhooks All [wallet action webhook events](/wallets/actions/webhooks) include the `reference_id` field in their payload when one was provided. This lets your app match incoming webhook notifications to your internal records without an additional API call. # Get wallet action status Source: https://docs.privy.io/wallets/actions/status Check the status of asynchronous wallet actions like transfers and swaps Wallet actions like [transfers](/wallets/actions/transfer/overview) and [swaps](/wallets/actions/swap/overview) are processed asynchronously. When your application initiates an action, the response includes an `id` for the created wallet action. Your application can use this ID to poll for the action's current status. ## Usage To get the status of a wallet action, make a `GET` request to ```bash theme={"system"} https://api.privy.io/v1/wallets/{wallet_id}/actions/{action_id} ``` ### Response The unique identifier for the wallet action. The ID of the wallet that initiated the action. The type of the wallet action. The current status of the action. | Status | Description | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `'created'` | Wallet action has been queued for execution and resource ID has been returned to the caller. | | `'succeeded'` | All steps of the wallet action have successfully executed. This is a terminal state. | | `'rejected'` | The wallet action was rejected prior to executing any steps, e.g. due to a policy violation. This is a terminal state and safe to retry. | | `'failed'` | The wallet action failed during execution of one of its steps. Use the [get wallet action](/api-reference/wallets/actions/get) endpoint with `?include=steps` to inspect what went wrong at the step level. | The steps of the wallet action. Only returned when `?include=steps` is passed as a query parameter. Each step includes its own status, chain identifier, and transaction hash. The response also includes type-specific fields depending on whether the action is a `swap` or `transfer`. See the [API reference](/api-reference/wallets/actions/get) for the full response schema. ### Including step details By default, the response only returns the overall state of the action to provide visibility into whether it succeeded or failed. For more granular information, such as transaction hashes, chain IDs, and per-step statuses, pass `?include=steps` as a query parameter. ```bash theme={"system"} curl https://api.privy.io/v1/wallets/{wallet_id}/actions/{action_id}?include=steps \ -u ":" \ -H "privy-app-id: " ``` ## Listing all wallet actions To retrieve all wallet actions for a given wallet, make a `GET` request to ```bash theme={"system"} https://api.privy.io/v1/wallets/{wallet_id}/actions ``` This endpoint supports cursor-based pagination via `cursor` and `limit` query parameters. ```bash theme={"system"} curl https://api.privy.io/v1/wallets/{wallet_id}/actions?limit=20 \ -u ":" \ -H "privy-app-id: " ``` The response includes a `data` array of wallet actions and a `next_cursor` field for pagination. See the [API reference](/api-reference/wallets/actions/list) for the full request and response schema. ## Polling recommendations Wallet actions typically complete within a few seconds, but can take longer depending on network conditions. Poll infrequently and stop once the `status` reaches a terminal value (`succeeded`, `rejected`, or `failed`). ## API reference See the [API reference](/api-reference/wallets/actions/get) for the full request and response schema, or the [list endpoint](/api-reference/wallets/actions/list) for retrieving all actions for a wallet. # Collect fees Source: https://docs.privy.io/wallets/actions/swap/collect-fees Keep a developer fee on swaps Custom fees let your app keep a developer fee on token swaps, including cross-chain swaps. The fee is captured at the infrastructure layer as part of the swap your app already runs, so there is no separate billing system to build. Users see the full cost, including the fee, in the quote before a swap executes. Privy also supports custom fee structures for cross-chain swaps, including negotiated fee caps and revenue sharing arrangements. Custom fees for swaps are in early access. Contact [sales@privy.io](mailto:sales@privy.io) to enable them for your app and set your fee recipient. ## How it works Once custom fees are enabled, set a total fee cap with the `fee_configuration` parameter on the [quote](/wallets/actions/swap/get-quote) and [execute](/wallets/actions/swap/execute) endpoints. Privy allocates the developer fee within that cap and routes it to your recipient. The fee appears as a `developer` line item in the quote's estimated fees. See [fees](/wallets/actions/swap/overview#fees) for the full breakdown of fees that apply to a swap, and the [get a quote](/wallets/actions/swap/get-quote) page for the `fee_configuration` parameter reference. ## Next steps Keep a share of the revenue generated across Earn, swaps, and transfers. # Errors Source: https://docs.privy.io/wallets/actions/swap/errors Error codes and responses returned by the swap API when requests cannot be fulfilled The swap APIs return the following errors when a request cannot be fulfilled. ## Validation errors | Error | HTTP status | Description | | --------------------------- | ----------- | ------------------------------------------------------------------------------------------------------------------- | | Swaps not enabled | 403 | Swaps are not enabled for your app. [Enable swaps](/wallets/actions/swap/setup) in the Privy Dashboard. | | Unsupported chain | 400 | The `caip2` chain identifier is not a [supported chain](/wallets/actions/swap/overview#supported-chains). | | Same input and output token | 400 | The `input_token` and `output_token` must be different. | | Unsupported wallet type | 400 | The wallet type is not supported for swap operations. Swaps are available for EVM and Solana wallets. | | Gas sponsorship not enabled | 400 | [Gas sponsorship](/wallets/gas-and-asset-management/gas/setup) is required but not enabled for your app. | | Insufficient token balance | 400 | The wallet does not have enough of the input token to cover the swap amount. Returned for `exact_input` swaps only. | ## Routing errors | Error | HTTP status | Description | | ------------------------ | ----------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | No quotes available | 400 | No swap route could be found for the given token pair, amount, or chain. This can occur if the token pair lacks liquidity or the token address is invalid on the specified chain. | | Token or route not found | 404 | The specified token or swap route does not exist. Verify that the token addresses are correct for the target chain. | | Rate limited | 429 | The request was rate limited. Retry after a brief delay. | # Execute a swap Source: https://docs.privy.io/wallets/actions/swap/execute Execute a token swap with automated token approvals and transaction submission Execute a swap using the swap endpoint. Privy automates token approvals and transaction submission. The response is a [wallet action](/wallets/actions/overview) that starts in `pending` status and can be polled for confirmation. View the full [API reference](/api-reference/wallets/swap/tokens) for the swap endpoint. ## Usage To execute a swap, make a `POST` request to: ```bash theme={"system"} https://api.privy.io/v1/wallets/{wallet_id}/swap ``` Wallets with explicit [owners or signers](/controls/authorization-keys/owners/overview) must provide an [authorization signature](/api-reference/authorization-signatures) as a request header. Use `privy.wallets().swaps().execute()` to execute a swap from a wallet. ```typescript {skip-check} theme={"system"} const response = await privy .wallets() .swaps() .execute('insert-wallet-id', { source: { caip2: 'eip155:8453', asset_address: 'native' }, destination: { asset_address: '0x' }, base_amount: '1000000000000000000', amount_type: 'exact_input', authorization_context: { authorization_private_keys: [''] } }); ``` The method returns a `SwapActionResponse` with the pending wallet action. See [wallet action lifecycle](/wallets/actions/overview#wallet-action-lifecycle) to track the status. ### Body The source token and chain. CAIP-2 identifier for the source chain (e.g., `eip155:8453` for Base). See [supported chains](/wallets/actions/swap/overview#supported-chains). Token address to sell, or `"native"` for the chain's native token. The destination token and chain. Token address to receive, or `"native"` for the chain's native token. CAIP-2 identifier for the destination chain. Omit for a same-chain swap. Set to a different chain for a cross-chain swap. See [supported routes](/wallets/actions/swap/overview#cross-chain-swaps). Address to receive the output tokens on the destination chain. Required when swapping between different chain types (e.g. EVM and Solana). Defaults to the source wallet address otherwise. Amount in base units (e.g., wei for ETH, lamports for SOL, or the token's smallest unit). Whether `base_amount` refers to the input or output token. Defaults to `exact_input`. Maximum slippage tolerance in basis points (e.g., `50` for 0.5%). If omitted, auto-slippage is used. Optional developer fee configuration. Only applies to cross-chain swaps. The fee model to apply. Currently only `total_fee_bps` is supported. Total fee cap in basis points (0–10000). Relayer and developer fees must fit within this cap. For example, `80` represents 0.8%. When using auto-slippage (omitting `slippage_bps`), an appropriate tolerance is determined based on the tokens and current market conditions. For large swaps or volatile token pairs, consider setting an explicit `slippage_bps` value and reviewing the `minimum_output_amount` from the [quote response](/wallets/actions/swap/get-quote) before executing. ### Response The ID of the wallet action. The current status of the wallet action. The ID of the wallet involved in the swap. CAIP-2 identifier for the source chain. Token address being sold. Token address being bought. Amount of input token in base units. Populated after confirmation. Amount of output token received in base units. Populated after confirmation. CAIP-2 identifier for the destination chain. Present for cross-chain swaps only. The address receiving output tokens on the destination chain. Present when `destination.destination_address` was specified. Cross-chain swaps only. Estimated fee breakdown at the time of execution. Each item has a `type` (`relayer`, or `developer`) and an `amount` in USD. Items of type `developer` include a `recipient` address. Present for cross-chain swaps only. Estimated gas cost at the time of execution. Contains `base_amount`, `amount`, and `gas_asset` fields. Present for cross-chain swaps only. Actual fees paid. Populated after the swap is confirmed. Same structure as `estimated_fees`. Present for cross-chain swaps only. Actual gas paid. Populated after the swap is confirmed. Same structure as `estimated_gas`. Present for cross-chain swaps only. ### Examples ```bash theme={"system"} curl -X POST https://api.privy.io/v1/wallets/{wallet_id}/swap \ -u ":" \ -H "privy-app-id: " \ -H "privy-authorization-signature: " \ -H "Content-Type: application/json" \ -d '{ "source": { "caip2": "eip155:8453", "asset_address": "native" }, "destination": { "asset_address": "0x" }, "base_amount": "1000000000000000000", "amount_type": "exact_input" }' ``` ```json Example response theme={"system"} { "id": "cm7oxq1el000e11o8iwp7d0d0", "status": "pending", "wallet_id": "fmfdj6yqly31huorjqzq38zc", "caip2": "eip155:8453", "input_token": "native", "output_token": "0x", "input_amount": null, "output_amount": null } ``` ```bash theme={"system"} curl -X POST https://api.privy.io/v1/wallets/{wallet_id}/swap \ -u ":" \ -H "privy-app-id: " \ -H "privy-authorization-signature: " \ -H "Content-Type: application/json" \ -d '{ "source": { "caip2": "eip155:8453", "asset_address": "native" }, "destination": { "caip2": "eip155:42161", "asset_address": "native" }, "base_amount": "1000000000000000000", "amount_type": "exact_input" }' ``` ```json Example response theme={"system"} { "id": "cm7oxq1el000e11o8iwp7d0d0", "status": "pending", "wallet_id": "fmfdj6yqly31huorjqzq38zc", "caip2": "eip155:8453", "input_token": "native", "output_token": "native", "input_amount": null, "output_amount": null, "destination_caip2": "eip155:42161", "estimated_fees": [{ "type": "relayer", "amount": "0.50" }], "estimated_gas": { "base_amount": "210000000000000", "amount": "0.00021", "gas_asset": "ETH" } } ``` # Get a quote Source: https://docs.privy.io/wallets/actions/swap/get-quote Request a price quote to preview estimated swap output amounts before executing Before executing a swap, request a price quote to preview estimated output amounts. The quote reflects Privy and protocol fees in the swap rate. See the [fees section](/wallets/actions/swap/overview#fees) for a breakdown of how fees are applied. View the full [API reference](/api-reference/wallets/swap/quote) for the quote endpoint. ## Usage To get a swap quote, make a `POST` request to: ```bash theme={"system"} https://api.privy.io/v1/wallets/{wallet_id}/swap/quote ``` Use `privy.wallets().swaps().quote()` to get a swap quote for a wallet. ```typescript {skip-check} theme={"system"} const quote = await privy .wallets() .swaps() .quote('insert-wallet-id', { source: { caip2: 'eip155:8453', asset_address: 'native' }, destination: { asset_address: '0x' }, base_amount: '1000000000000000000', amount_type: 'exact_input' }); ``` The method returns a `SwapQuoteResponse` with the estimated output amounts and fees. ### Body The source token and chain. CAIP-2 identifier for the source chain (e.g., `eip155:8453` for Base). See [supported chains](/wallets/actions/swap/overview#supported-chains). Token address to sell, or `"native"` for the chain's native token. The destination token and chain. Token address to receive, or `"native"` for the chain's native token. CAIP-2 identifier for the destination chain. Omit for a same-chain swap. Set to a different chain for a cross-chain swap. See [supported routes](/wallets/actions/swap/overview#cross-chain-swaps). Address to receive the output tokens on the destination chain. Required when swapping between different chain types (e.g. EVM and Solana). Defaults to the source wallet address otherwise. Amount in base units (e.g., wei for ETH, lamports for SOL, or the token's smallest unit). Whether `base_amount` refers to the input or output token. Defaults to `exact_input`. Maximum slippage tolerance in basis points (e.g., `50` for 0.5%). If omitted, auto-slippage is used. Optional developer fee configuration. Only applies to cross-chain swaps. The fee model to apply. Currently only `total_fee_bps` is supported. Total fee cap in basis points (0–10000). Relayer and developer fees must fit within this cap. For example, `80` represents 0.8%. Swap quotes reflect real-time market conditions and can change quickly. Fetch a fresh quote before executing a swap to ensure your app displays accurate pricing. For cross-chain quotes, quoted output amounts and fees are best-effort estimates — small deviations are possible if market conditions shift between quote and execution. ### Response CAIP-2 identifier for the source chain. Token address being sold. Token address being bought. Amount of input token in base units. Estimated amount of output token in base units, after fees. Minimum output amount accounting for slippage, in base units. Use this to verify the swap terms before executing. Estimated gas cost in base units of the native token. For cross-chain swaps, see `estimated_gas` for a structured breakdown. CAIP-2 identifier for the destination chain. Present for cross-chain swaps only. Fee breakdown for the swap. Each item has a `type` (`relayer`, or `developer`) and an `amount` in USD. Items of type `developer` include a `recipient` address. Present for cross-chain swaps only. Structured gas estimate. Present for cross-chain swaps only. Gas cost in the native token's smallest unit. Gas cost as a human-readable decimal string. Symbol of the gas token (e.g., `"ETH"`). Unix timestamp (in seconds) after which the quote is no longer valid. Present for cross-chain swaps only. Fetch a fresh quote if executing after this time. ### Examples ```bash theme={"system"} curl -X POST https://api.privy.io/v1/wallets/{wallet_id}/swap/quote \ -u ":" \ -H "privy-app-id: " \ -H "Content-Type: application/json" \ -d '{ "source": { "caip2": "eip155:8453", "asset_address": "native" }, "destination": { "asset_address": "0x" }, "base_amount": "1000000000000000000", "amount_type": "exact_input" }' ``` ```json Example response theme={"system"} { "caip2": "eip155:8453", "input_token": "native", "output_token": "0x", "input_amount": "1000000000000000000", "est_output_amount": "2000000000", "minimum_output_amount": "1980000000", "gas_estimate": "150000" } ``` ```bash theme={"system"} curl -X POST https://api.privy.io/v1/wallets/{wallet_id}/swap/quote \ -u ":" \ -H "privy-app-id: " \ -H "Content-Type: application/json" \ -d '{ "source": { "caip2": "eip155:8453", "asset_address": "native" }, "destination": { "caip2": "eip155:42161", "asset_address": "native" }, "base_amount": "1000000000000000000", "amount_type": "exact_input" }' ``` ```json Example response theme={"system"} { "caip2": "eip155:8453", "input_token": "native", "output_token": "native", "input_amount": "1000000000000000000", "est_output_amount": "998000000000000000", "minimum_output_amount": "993010000000000000", "destination_caip2": "eip155:42161", "estimated_fees": [{ "type": "relayer", "amount": "0.50" }], "estimated_gas": { "base_amount": "210000000000000", "amount": "0.00021", "gas_asset": "ETH" }, "expires_at": 1715200060 } ``` # Swap tokens Source: https://docs.privy.io/wallets/actions/swap/overview Swap tokens on supported EVM chains and Solana via Privy wallet actions Privy enables your app to support token swaps on supported EVM chains and Solana directly from a wallet. Swaps execute as [wallet actions](/wallets/actions/overview), and Privy automates token approvals and transaction submission. Swap Swaps are executed on third-party decentralized protocols. Privy does not have discretion over how a swap is routed to the protocol or the price at which the swap is ultimately executed by the blockchain network. Swap rates may differ from quoted estimates due to market volatility, liquidity conditions, and slippage. These materials are for general information purposes only and are not investment advice or a recommendation or solicitation to engage in any specific transaction. Privy does not provide investment, financial, legal, or tax advice. ## How it works [Enable swaps](/wallets/actions/swap/setup) in the Privy Dashboard and configure [gas sponsorship](/wallets/gas-and-asset-management/gas/setup) for your app. Call the [quote endpoint](/wallets/actions/swap/get-quote) with the token pair, chain, and amount. The response includes estimated output amounts and a gas estimate. Call the [swap endpoint](/wallets/actions/swap/execute) with the same parameters. The response is a wallet action that can be polled for confirmation. ## Supported chains Swaps are available on the following chains. | Chain | Chain ID | CAIP-2 identifier | Native token | | --------------- | -------- | ----------------- | ------------ | | Ethereum | 1 | `eip155:1` | ETH | | Optimism | 10 | `eip155:10` | ETH | | BNB Smart Chain | 56 | `eip155:56` | BNB | | Unichain | 130 | `eip155:130` | ETH | | Polygon | 137 | `eip155:137` | POL | | Monad | 143 | `eip155:143` | MON | | World Chain | 480 | `eip155:480` | ETH | | Tempo | 4217 | `eip155:4217` | None | | Robinhood Chain | 4663 | `eip155:4663` | ETH | | Base | 8453 | `eip155:8453` | ETH | | Arbitrum | 42161 | `eip155:42161` | ETH | Tempo does not have a native token. Specify token contract addresses for Tempo swaps. | Chain | CAIP-2 identifier | Native token | | -------------- | ----------------------------------------- | ------------ | | Solana mainnet | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | SOL | | Chain | Chain ID | CAIP-2 identifier | Native token | | ----------------- | -------- | ----------------- | ------------ | | Unichain Sepolia | 1301 | `eip155:1301` | ETH | | Monad Testnet | 10143 | `eip155:10143` | MON | | Robinhood Testnet | 46630 | `eip155:46630` | ETH | | Sepolia | 11155111 | `eip155:11155111` | ETH | | Base Sepolia | 84532 | `eip155:84532` | ETH | ## Token addresses Specify token addresses as ERC-20 contract addresses (for EVM chains), TIP-20 contract addresses (for Tempo), or SPL token mint addresses (for Solana). Use `"native"` only on chains with native token support (e.g., ETH on Ethereum or SOL on Solana). The `input_token` and `output_token` must be different. Token addresses are chain-specific. Ensure that the addresses provided for `input_token` and `output_token` correspond to token contracts deployed on the chain specified in the `caip2` field. The same token (e.g., USDC) may have different contract addresses on different chains. Use a resource like [Token Lists](https://tokenlists.org/) to source correct addresses for your target chain. ## Fees The following fees may apply when executing a swap. Fees are subject to change. | Fee | Description | | --------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Privy swap fee** | Privy charges up to 0.25% on each swap, calculated from the input token amount. This fee is included in the swap rate. | | **Protocol fee** | Underlying DEX protocols charge a fee on each swap. For EVM chains, Uniswap liquidity pools typically charge 0.3% for V2 pools and 0.01%–1% for V3/V4 pools depending on the pool's fee tier. This fee is included in the swap rate. | | **Network fee (gas)** | Blockchain transaction processing fees paid to the network. Gas sponsorship is required for swaps, meaning Privy sponsors these fees from your app's gas credits. This includes any token approval transactions required for the swap (e.g., the first time a wallet swaps a given ERC-20 or TIP-20 token). Gas fees fluctuate based on network congestion and transaction complexity. | | **Relayer fee** | (Cross-chain only) Paid to the bridge provider for routing the swap across chains. Varies with network conditions and liquidity. Included in `estimated_fees` on the quote response. | | **Developer fee** | (Cross-chain only) A configurable fee allocated to your app. Requires a [custom fees configuration](/wallets/actions/swap/collect-fees). Set via the `fee_configuration` parameter on the quote and swap endpoints. Included in `estimated_fees` on the quote response. | The estimated output amounts returned by the [quote endpoint](/wallets/actions/swap/get-quote) reflect the swap rate after Privy and protocol fees. The `gas_estimate` field provides a separate estimate of the network fee. ## Slippage Slippage is the difference between the quoted price of a swap and the price at which it executes. Because token prices can change quickly, especially during periods of high volatility or low liquidity, the final execution price may differ from the quoted price. The `slippage_bps` parameter sets the maximum slippage tolerance in basis points (e.g., `50` for 0.5%). This controls the maximum percentage difference your app is willing to accept between the quoted price and the execution price. * If the maximum slippage is set too low, the swap may fail if the price moves beyond the specified tolerance. * If the maximum slippage is set higher, the swap is more likely to succeed, but the wallet may receive a less favorable price if the market moves significantly. * If a swap fails due to slippage, the wallet is still responsible for any network fees incurred. Privy enables the use of auto-slippage by omitting the `slippage_bps` parameter. When omitted, an appropriate slippage tolerance is automatically determined based on the tokens being swapped and current market conditions. Your app can also set a specific `slippage_bps` value and use the `minimum_output_amount` from the [quote response](/wallets/actions/swap/get-quote) to verify the minimum tokens received before executing. ## Cross-chain swaps Privy's swap API supports swapping tokens across different chains. A cross-chain swap routes output tokens to a destination chain — for example, swapping ETH on Base and receiving ETH on Arbitrum, or swapping USDC on Ethereum and receiving SOL on Solana. Cross-chain swaps use a `source` and `destination` object format with separate `caip2` identifiers for each side. See the [get a quote](/wallets/actions/swap/get-quote#cross-chain-swaps) and [execute a swap](/wallets/actions/swap/execute#cross-chain-swaps) pages for usage. Supported cross-chain routes mirror those for [transfers](/wallets/actions/transfer/overview#native-bridging): | Source chain | Supported destination chains | | --------------- | ----------------------------------------------------------- | | Ethereum | Base, Tempo, Robinhood Chain, Arbitrum, Polygon, Solana | | Base | Ethereum, Tempo, Robinhood Chain, Arbitrum, Polygon, Solana | | Tempo | Ethereum, Base, Robinhood Chain, Arbitrum, Polygon, Solana | | Robinhood Chain | Ethereum, Base, Tempo, Arbitrum, Polygon, Solana | | Arbitrum | Ethereum, Base, Tempo, Robinhood Chain, Polygon, Solana | | Polygon | Ethereum, Base, Tempo, Robinhood Chain, Arbitrum, Solana | | Solana | Ethereum, Base, Tempo, Robinhood Chain, Arbitrum, Polygon | Cross-chain swaps are subject to relayer fees, slippage, and current liquidity conditions. Quotes expire — use the `expires_at` field to detect stale quotes and fetch a fresh one before executing. ## Next steps Enable swaps and configure routing in the Privy Dashboard. Fetch a price quote before executing a swap. Execute a token swap from a wallet. Error reference for swap APIs. # Setup Source: https://docs.privy.io/wallets/actions/swap/setup Configure swap settings and routing behavior in the Privy Dashboard Configure swap settings from the [Privy Dashboard](https://dashboard.privy.io). Navigate to **Wallet Infrastructure > Wallets > Advanced** to enable swaps and customize routing behavior. ## 1. Enable swaps Toggle **Enable token swaps** to activate swap functionality for your app. Swaps are disabled by default. ## 2. Configure gas sponsorship [Gas sponsorship](/wallets/gas-and-asset-management/gas/setup) must be enabled for your app before swaps can execute. Configure gas sponsorship from the **Wallet infrastructure > Gas sponsorship** page in the [Privy Dashboard](https://dashboard.privy.io) and ensure your gas credits are funded. Swap requests fail if gas sponsorship is not enabled for your app. See the [gas sponsorship setup guide](/wallets/gas-and-asset-management/gas/setup) for instructions. ## 3. Select protocols Choose which protocols are used to route swaps. ### EVM chains Choose which [Uniswap](https://docs.uniswap.org/) protocols are used to route EVM swaps. Additional protocols and DEX aggregators are planned for future releases. | Option | Description | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **All Uniswap** | Routes through [Uniswap V2](https://docs.uniswap.org/contracts/v2/overview), [V3](https://docs.uniswap.org/contracts/v3/overview), and [V4](https://docs.uniswap.org/contracts/v4/overview) pools. This is the default setting and provides access to the broadest liquidity. | | **Uniswap pools only** | Routes exclusively through Uniswap V2, V3, and V4 on-chain pools. | | **UniswapX only** | Routes through the [UniswapX](https://docs.uniswap.org/contracts/uniswapx/overview) protocol. *Coming soon.* | ### Solana Solana swaps route through a low-latency swap aggregator. No additional protocol configuration is required for Solana. ## 4. Select routing strategy Choose how swaps are optimized when multiple routes are available. | Option | Description | | -------------- | ---------------------------------------------------------------------------------------------------------------------------- | | **Fastest** | Optimizes for execution speed. Selects the route expected to confirm on-chain most quickly. This is the default setting. | | **Best price** | Optimizes for output amount. Selects the route that returns the most tokens to the wallet, which may take longer to execute. | **Fastest** is typically appropriate for most use cases. **Best price** may result in longer confirmation times during periods of network congestion, but can yield better rates for larger swaps. # Bridging Source: https://docs.privy.io/wallets/actions/transfer/bridging The [transfer API](/wallets/actions/transfer/overview) automatically supports bridging assets across chains and converting between stablecoins of the same peg. Cross-chain transfers are routed through third-party bridge providers. Privy does not control bridge routing, fill times, or available liquidity. Transfer rates may differ from quoted estimates due to market conditions. These materials are for general information purposes only and are not investment advice. ## Usage To execute a transfer involving a bridge, specify a `destination.chain` different from `source.chain`. To execute a transfer involving a stablecoin conversion, specify a `destination.asset` different from `source.asset`. You can also combine both a bridge and a stablecoin conversion in the same transfer call. ```typescript theme={"system"} // Bridge USDC from Tempo to Ethereum (exact_input is the default) const response = await privy.wallets().transfer('insert-wallet-id', { amount: '10.0', amount_type: 'exact_input', source: { asset: 'usdc', chain: 'tempo', }, destination: { address: '0xRecipientAddress', chain: 'ethereum', }, }); ``` ```bash theme={"system"} curl -X POST https://api.privy.io/v1/wallets/{wallet_id}/transfer \ -u ":" \ -H "privy-app-id: " \ -H "Content-Type: application/json" \ -d '{ "amount": "10.0", "amount_type": "exact_input", "source": { "asset": "usdc", "chain": "tempo" }, "destination": { "address": "0xRecipientAddress", "chain": "ethereum" } }' ``` ```json Example response theme={"system"} { "id": "action-id", "status": "pending", "wallet_id": "wallet-id", "created_at": "2026-04-14T20:09:11.929Z", "type": "transfer", "source_asset": "usdc", "source_amount": "10.0", "source_chain": "tempo", "destination_address": "0xRecipientAddress", "destination_chain": "ethereum" } ``` ## Amount types The `amount_type` parameter controls whether the specified `amount` refers to what leaves the source wallet or what arrives at the destination. It defaults to `exact_input`. | | `exact_input` (default) | `exact_output` | | ------------------------ | ------------------------------------------------------- | ------------------------------------------------- | | **What `amount` means** | The amount deducted from the source wallet | The guaranteed amount the recipient receives | | **What varies** | Destination amount (depends on fees and bridge pricing) | Source amount (estimated by the bridge provider) | | **When to use** | Sending a known balance or fixed spend | Guaranteeing a recipient receives an exact payout | | **Quote field to check** | `estimated_output_amount` | `estimated_input_amount` | To guarantee an exact destination amount, set `amount_type: 'exact_output'` and specify the desired payout in `amount`. Use the [quote endpoint](/wallets/actions/transfer/quote) to preview the estimated source cost before executing. For `exact_output` transfers, the `source_amount` field on the action response is `null` at creation and populated once the transfer is confirmed on-chain. Exact output bridging is not yet supported when Solana is the source chain. ## Limitations Cross-asset bridges are only supported between assets of the same economic category: * **USD-backed stablecoins** can be bridged to other USD-backed stablecoins (e.g. USDC → USDT, USDC → USDG). * **Native tokens** can only be bridged to the same native token on another chain (e.g. ETH on Base → ETH on Ethereum). * **Cross-category swaps are not supported** — native tokens cannot be exchanged for stablecoins via the bridge path. Use the [swap API](/wallets/actions/swap/overview) for token swaps on a single chain. Cross-chain transfers are only supported for well-known assets (`usdc`, `usdt`, `usdg`, `eth`, etc.). Custom tokens specified via `asset_address` cannot be bridged. The `amount_type: 'exact_output'` parameter is not yet supported when the source chain is Solana. Solana-sourced transfers only support `exact_input` (the default). Testnet bridging is available between `base_sepolia` and `ethereum_sepolia`. Other testnet cross-chain routes may have limited availability depending on the bridge provider's testnet infrastructure. Testnet bridges are best-effort and may have slower fill times than mainnet. Mainnet-to-testnet and testnet-to-mainnet bridges are not supported. The source and destination chains must both be mainnet or both be testnet. ## Tracking a bridge transfer Bridge transfers follow the standard [wallet action lifecycle](/wallets/actions/overview#wallet-action-lifecycle). The action status progresses: 1. `pending` — source chain transaction submitted; bridge fill in progress. 2. `succeeded` — bridge fill confirmed on destination chain. 3. `failed` — the bridge fill was not completed (e.g. insufficient liquidity, expired quote). Bridge fills typically complete within seconds on mainnet. During periods of high network activity, fills may take longer. To receive a notification when the bridge completes, listen for the [`wallet_action.transfer.succeeded`](/api-reference/webhooks/wallet-action/transfer/succeeded) webhook event. ## API reference See the [API reference](/api-reference/wallets/transfer) for the full parameter reference. # Collect fees Source: https://docs.privy.io/wallets/actions/transfer/collect-fees Keep a developer fee on cross-chain transfers Custom fees let your app keep a developer fee on cross-chain transfers. The fee is captured at the infrastructure layer as part of the transfer your app already runs, so there is no separate billing system to build or reconcile. Users see the full cost, including the fee, in the quote before a transfer executes. Privy also supports custom fee structures for cross-chain transfers, including negotiated fee caps and revenue sharing arrangements. Custom fees for transfers are in early access. Contact [sales@privy.io](mailto:sales@privy.io) to enable them for your app and set your fee recipient. ## How it works Once custom fees are enabled, set a total fee cap with the `fee_configuration` parameter on the [quote](/wallets/actions/transfer/quote) and [transfer](/wallets/actions/transfer/usage) endpoints. Privy allocates the developer fee within that cap and routes it to your recipient. The fee applies to cross-chain transfers and appears as a `developer` line item in the quote's estimated fees. See [quote transfer fees](/wallets/actions/transfer/quote) for the `fee_configuration` parameter reference and the fee breakdown returned before a transfer executes. ## Next steps Keep a share of the revenue generated across Earn, swaps, and transfers. # Fixed rates Source: https://docs.privy.io/wallets/actions/transfer/fixed-rates Deliver the exact input amount across chains, with your app covering the transfer and routing costs Fixed rates let a [cross-chain transfer](/wallets/actions/transfer/bridging) deliver its exact input amount, handling any slippage and bridging or swapping fees invisibly. A wallet that sends 100 USDC on Solana delivers 100 USDT on Arbitrum. This suits payouts, remittances, and any flow where a recipient expects an exact figure. Without fixed rates, bridging fees, swap fees, and slippage all come out of the transfer, so the amount that arrives is smaller than the amount sent. ## Setup Fixed rates require an Enterprise account with postpaid billing. Contact [sales@privy.io](mailto:sales@privy.io) to enable them for an app. Privy enables fixed rates per app. Once enabled, they apply automatically to every supported route the app executes — [transfers](/wallets/actions/transfer/bridging), [swaps](/wallets/actions/swap/overview), and [crypto deposit addresses](/wallets/funding/crypto-deposits/overview). There is no request parameter to set and no change to existing integration code. ## How it works Rather than deducting the routing cost from the transfer, Privy meters it and invoices your app at the end of the month. See [usage and billing](/wallets/gas-and-asset-management/usage-billing/overview) for how Privy invoices metered charges, and [usage webhooks](/wallets/gas-and-asset-management/usage-billing/usage-webhooks) for observing each charge as Privy records it. ## Confirming the delivered amount Use the [quote endpoint](/wallets/actions/transfer/quote) and read `estimated_output_amount`. That field is the authoritative statement of what the recipient receives, on fixed-rate and standard routes alike. ```typescript theme={"system"} // On a fixed-rate route, estimated_output_amount matches the amount sent. const quote = await privy.wallets().transferQuote('insert-wallet-id', { amount: '100.0', amount_type: 'exact_input', source: { asset: 'usdc', chain: 'solana', }, destination: { address: '0xRecipientAddress', asset: 'usdt', chain: 'arbitrum', }, }); ``` On a fixed-rate route, `amount_type: 'exact_input'` already delivers an exact figure to the recipient, so `exact_output` is rarely needed. See [amount types](/wallets/actions/transfer/bridging#amount-types) for the difference between the two. ## Limitations Fixed rates cover the cost of routing a transfer between chains or between assets. Same-chain, same-asset transfers have no routing cost to cover. [Gas sponsorship](/wallets/gas-and-asset-management/gas/overview) covers network gas separately. Fixed rates are a property of the app, so an app cannot turn them on for some transfers and not others. Fixed rates change how routing costs are paid, not which routes exist. Available liquidity and supported asset pairs are unchanged. See the [bridging limitations](/wallets/actions/transfer/bridging#limitations) for supported routes. ## Next steps Understand how Privy invoices metered usage at the end of the month. Receive an event each time Privy records a charge. # Transfer Source: https://docs.privy.io/wallets/actions/transfer/overview Transfer tokens from a Privy wallet to a destination address using the transfer action API The transfer [wallet action API](/wallets/actions/overview) sends tokens from a Privy wallet to a destination address. Instead of constructing blockchain transactions from scratch, the transfer action accepts a human-readable asset, amount, chain, and destination address — Privy handles the onchain complexity. The transfer API also natively handles [bridging across chains](/wallets/actions/transfer/overview#native-bridging) and [converting between stablecoins of the same peg](/wallets/actions/transfer/overview#stablecoin-conversions). Wallets can execute transfers with a separate source asset/chain and a separate destination asset/chain. images/transfer-splash.png ## Features | Feature | What it handles | | -------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | | [Native bridging and stablecoin conversions](/wallets/actions/transfer/bridging) | Moves native assets across chains and converts between stablecoins of the same peg. | | [Gas sponsorship](/wallets/gas-and-asset-management/gas/overview) | Covers network gas automatically for configured apps. | | [Fixed rates](/wallets/actions/transfer/fixed-rates) | Delivers the exact input amount by covering routing costs, swap fees, and slippage. | | [Fee collection](/wallets/actions/transfer/collect-fees) | Adds a developer fee to cross-chain transfers and routes the proceeds to the app's fee recipient. | ## Supported assets The transfer API supports transferring all native tokens (ETH, SOL, POL, TRX) and all ERC20/SPL/TRC-20 tokens (including stablecoins). When making a transfer request to the Privy API, you can either specify a well-known asset (e.g. USDC, USDT, USDB, EURC) or you can configure a custom ERC20/SPL (e.g. a custom-issued stablecoin). ### Well-known assets Privy supports the following well-known assets natively on the chains where they are available. | Asset | API value | | ------- | --------- | | USDC | `usdc` | | USDC.e | `usdc_e` | | USDT | `usdt` | | USDT0 | `usdt0` | | USDB | `usdb` | | USDG | `usdg` | | pathUSD | `pathusd` | | EURC | `eurc` | | ETH | `eth` | | SOL | `sol` | | POL | `pol` | | TRX | `trx` | For the most up-to-date list of Privy's well-known assets and the specific chains where they are available, visit the [**Asset watchlist**](https://dashboard.privy.io/apps?page=settings\&tab=token-watchlist) section of the Privy Dashboard. ### Custom assets Privy also supports transfers of custom ERC20/SPL tokens. To configure these assets, visit the [**Asset watchlist**](https://dashboard.privy.io/apps?page=settings\&tab=token-watchlist) tab of the **Wallets** page of the Dashboard to configure your asset. Then, simply pass the asset's address in the `source.asset_address` parameter of your request to Privy to transfer it. ## Supported chains The `/transfer` API supports the following chains: | Chain | CAIP-2 | API value | | ----------------- | ----------------------------------------- | ------------------- | | Ethereum | `eip155:1` | `ethereum` | | Base | `eip155:8453` | `base` | | Arbitrum | `eip155:42161` | `arbitrum` | | Polygon | `eip155:137` | `polygon` | | Tempo | `eip155:4217` | `tempo` | | Robinhood Chain | `eip155:4663` | `robinhood` | | Solana | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | `solana` | | Ethereum Sepolia | `eip155:11155111` | `ethereum_sepolia` | | Base Sepolia | `eip155:84532` | `base_sepolia` | | Arbitrum Sepolia | `eip155:421614` | `arbitrum_sepolia` | | Polygon Amoy | `eip155:80002` | `polygon_amoy` | | Tempo Moderato | `eip155:42431` | `tempo_moderato` | | Robinhood Testnet | `eip155:46630` | `robinhood_testnet` | | Solana Devnet | `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` | `solana_devnet` | | Tron | `tron:mainnet` | `tron` | | Tron Nile | `tron:nile` | `tron_nile` | To transfer on chains not listed above, use the [low-level RPC API](/wallets/using-wallets/ethereum/sign-a-message) to construct and send transactions directly. Tron transfers are same-chain only. Cross-chain bridging and stablecoin conversions are not supported for Tron. Tron destination addresses must be base58check-encoded (starting with `T`). ## Native bridging Privy's `/transfer` API supports natively bridging an asset (e.g. ETH, USDC) across the supported chains listed above. Supported routes include: | Source chain | Supported destination chains | | --------------- | ----------------------------------------------------------- | | Ethereum | Base, Tempo, Robinhood Chain, Arbitrum, Polygon, Solana | | Base | Ethereum, Tempo, Robinhood Chain, Arbitrum, Polygon, Solana | | Tempo | Ethereum, Base, Robinhood Chain, Arbitrum, Polygon, Solana | | Robinhood Chain | Ethereum, Base, Tempo, Arbitrum, Polygon, Solana | | Arbitrum | Ethereum, Base, Tempo, Robinhood Chain, Polygon, Solana | | Polygon | Ethereum, Base, Tempo, Robinhood Chain, Arbitrum, Solana | | Solana | Ethereum, Base, Tempo, Robinhood Chain, Arbitrum, Polygon | Native bridging is subject to fees, slippage, and current liquidity conditions. ## Stablecoin conversions Privy's `/transfer` API also supports converting between stablecoins of the same peg. For instance, the transfer API supports setting different USD stablecoins (USDC, USDT, USDC.e, USDT0) as the source and destination currency. This enables you to hold balance in one stablecoin and payout in the preferred stablecoin or chain of your recipient. Stablecoin conversions is subject to fees, slippage, and current liquidity conditions. Native bridging and stablecoin conversions is only supported for well-known assets where Privy's bridging partners can provide sufficient liquidity. To extend these capabilities to a custom asset, please [reach out](mailto:sales@privy.io). # Policies Source: https://docs.privy.io/wallets/actions/transfer/policies Restrict transfers with Privy policies. Apps can use Privy policies to restrict which transfer actions a wallet or signer can take. For transfers, Privy evaluates policies against the original request body sent to the [`transfer`](/api-reference/wallets/transfer) endpoint before it prepares the underlying transactions. That means transfer rules should match request-body fields like `source.asset`, `source.amount`, `source.chain`, and `destination.address`. ## Supported methods Transfer policies support a single rule method: * `transfer` If a wallet policy only allows `eth_sendTransaction`, transfer requests will still be denied. Wallets that call the transfer endpoint need an explicit `transfer` rule. ## Supported conditions Transfer rules support `action_request_body` conditions for the fields below:
Field source Field Supported operators Notes
action\_request\_body source.asset eq, in, in\_condition\_set Matches a named asset such as "usdc" or "eth".
action\_request\_body source.asset\_address eq, in, in\_condition\_set Matches a custom asset contract address.
action\_request\_body source.amount eq, gt, gte, lt, lte Value must be a positive decimal string, such as "10.5".
action\_request\_body source.chain eq, in, in\_condition\_set Matches the source chain, such as "base" or "solana".
action\_request\_body destination.address eq, in, in\_condition\_set Matches the recipient wallet address.
action\_request\_body destination.asset eq, in, in\_condition\_set Matches the destination asset for cross-asset transfers.
action\_request\_body destination.chain eq, in, in\_condition\_set Matches the destination chain for cross-chain transfers.
Transfer rules also support shared `system` conditions, such as `current_unix_timestamp`, for time-based controls. Transfer policies support both `chain_type: "ethereum"` and `chain_type: "solana"`. For transfer methods, the policy engine only accepts `action_request_body` and `system` conditions. ## Choose `source.asset` or `source.asset_address` Use `source.asset` if your app sends named assets like `"usdc"` or `"eth"`. Use `source.asset_address` if your app sends custom asset contract addresses. A single rule cannot condition on both `source.asset` and `source.asset_address`. A transfer request includes one or the other, never both. This also affects runtime matching: * A rule using `source.asset` will not match a request that sends `source.asset_address`. * A rule using `source.asset_address` will not match a request that sends `source.asset`. Keep your policy format aligned with the request format your application actually sends. ## Example The example below allows: * transfers of USDC up to `1000` units to an approved destination address * transfers on the Tempo chain only After creating the policy, apply it to the wallet with `policy_ids`. For signer-specific transfer permissions, attach the policy as an override policy on a signer instead. ```typescript theme={"system"} const policy = await privy.policies().create({ name: 'Approved transfer policy', version: '1.0', chain_type: 'ethereum', rules: [ { name: 'Allow USDC transfers up to 1000 to approved address on Tempo', method: 'transfer', action: 'ALLOW', conditions: [ { field_source: 'action_request_body', field: 'source.asset', operator: 'eq', value: 'usdc', }, { field_source: 'action_request_body', field: 'source.amount', operator: 'lte', value: '1000.0', }, { field_source: 'action_request_body', field: 'source.chain', operator: 'eq', value: 'tempo', }, { field_source: 'action_request_body', field: 'destination.address', operator: 'eq', value: '', }, ], }, ], }); await privy.wallets().update('', { policy_ids: [policy.id], }); ``` If the wallet has an `owner_id`, the wallet update must be authorized by that owner. Create the policy: ```bash theme={"system"} curl -X POST https://api.privy.io/v1/policies \ -H "privy-app-id: " \ -H "Authorization: Basic " \ -H "Content-Type: application/json" \ -d '{ "name": "Approved transfer policy", "version": "1.0", "chain_type": "ethereum", "rules": [ { "name": "Allow USDC transfers up to 1000 to approved address on Tempo", "method": "transfer", "action": "ALLOW", "conditions": [ { "field_source": "action_request_body", "field": "source.asset", "operator": "eq", "value": "usdc" }, { "field_source": "action_request_body", "field": "source.amount", "operator": "lte", "value": "1000.0" }, { "field_source": "action_request_body", "field": "source.chain", "operator": "eq", "value": "tempo" }, { "field_source": "action_request_body", "field": "destination.address", "operator": "eq", "value": "" } ] } ] }' ``` Then apply the returned policy ID to a wallet: ```bash theme={"system"} curl -X PATCH https://api.privy.io/v1/wallets/ \ -H "privy-app-id: " \ -H "Authorization: Basic " \ -H "Content-Type: application/json" \ -d '{ "policy_ids": [""] }' ``` ## Common patterns * Restrict transfers to an allowlist of destination addresses with `destination.address: in` or `destination.address: in_condition_set`. * Limit which assets can be transferred with `source.asset` or `source.asset_address`. * Enforce maximum transfer size with `source.amount`. * Lock transfers to a specific chain with `source.chain`. * Add time-based controls with `system.current_unix_timestamp`. * Combine chain and destination controls for cross-chain transfer restrictions. ## Next steps Learn more about creating and managing policy objects. Apply different transfer permissions to different signers on the same wallet. # Quote transfer fees Source: https://docs.privy.io/wallets/actions/transfer/quote Get a fee estimate and expected output amount before executing a transfer The transfer quote endpoint returns a fee breakdown and estimated output amount for a cross-chain or cross-asset transfer before executing it. Use quotes to show users what they will receive and what fees apply before they confirm a transfer. The quote endpoint is only supported for cross-chain or cross-asset transfers (i.e. those that specify `destination.chain` or `destination.asset`). Same-chain, same-asset transfers do not require a quote. ## Understanding fees Every cross-chain transfer includes up to three fee components: * **Relayer fee** — paid to the bridge provider for routing the transfer. This varies with network conditions and liquidity. * **Developer fee** — fees to the app developer. Fees are denominated in USD and deducted from the transfer amount. The `estimated_output_amount` already reflects all fees — it is what the recipient will receive. ## Usage To get a quote via REST API, make a `POST` request to : ```bash theme={"system"} https://api.privy.io/v1/wallets/{wallet_id}/transfer/quote ``` ### Body The source asset, amount, and chain for the transfer. The asset to transfer. Must be one of `usdc`, `usdc_e`, `usdt`, `usdt0`, `usdb`, `eth`, `sol`, `pol`, or `eurc`. Custom token addresses (`asset_address`) are not supported for cross-chain transfers. The chain to transfer from. Must be one of `ethereum`, `base`, `arbitrum`, `polygon`, `tempo`, or `solana`. The destination for the transfer. The recipient wallet address. The destination chain. Required for cross-chain transfers. Must differ from `source.chain`. The destination asset. Required for cross-asset transfers. Must be a USD-backed stablecoin if `source.asset` is a USD-backed stablecoin, or the same native token on another chain. Amount as a decimal string in standard units (e.g. `"10.0"` for 10 USDC). For `exact_input`, the amount to send. For `exact_output`, the exact amount to receive. Takes precedence over `source.amount` when both are provided. Whether the amount refers to the input token (`exact_input`) or the output token (`exact_output`). Defaults to `exact_input`. When set to `exact_output`, the quote returns the estimated source amount needed to deliver the specified destination amount. Optional fee configuration to apply to the transfer. The fee configuration type. Currently only `total_fee_bps` is supported. Total fee cap in basis points (0–10000). For example, `100` represents 1%. ### Response The amount type from the request, echoed back for clarity. The source asset, amount, and chain from the request. The destination address, asset, and chain from the request. The estimated amount the sender provides, as a decimal string in source token units. For `exact_input`, this equals the source amount. For `exact_output`, this is the estimated amount needed to deliver the requested destination amount. The estimated amount the recipient will receive, as a decimal string in destination token units (e.g. `"9.97"` for 9.97 USDC). For `exact_output`, this equals the requested amount. An array of fee line items that make up the total transfer cost. Each item has a `type` and an `amount` in USD. | Type | Description | | ----------- | ------------------------------------------ | | `relayer` | Fee charged by the bridge/relayer provider | | `developer` | Fee allocated to the app developer | Unix timestamp (in seconds) after which the quote is no longer accepted. Executing the transfer before this time gives the best chance of matching the quoted output amount and fees. Quoted amounts are estimates — actual results may vary slightly if market conditions shift between quote and execution. ## Examples Quotes are best-effort estimates. Executing a transfer close to the time of the quote improves the likelihood of matching the quoted output and fees, but small deviations are possible if market conditions shift between quote and execution. ```typescript theme={"system"} const quote = await privy.wallets().transferQuote('insert-wallet-id', { amount: '10.0', amount_type: 'exact_input', source: { asset: 'usdc', chain: 'tempo', }, destination: { address: '0xRecipientAddress', chain: 'arbitrum', }, fee_configuration: { type: 'total_fee_bps', value: 80, }, }); ``` ```bash theme={"system"} curl -X POST https://api.privy.io/v1/wallets/{wallet_id}/transfer/quote \ -u ":" \ -H "privy-app-id: " \ -H "Content-Type: application/json" \ -d '{ "amount": "10.0", "amount_type": "exact_input", "source": { "asset": "usdc", "chain": "tempo" }, "destination": { "address": "0xRecipientAddress", "chain": "arbitrum" }, "fee_configuration": { "type": "total_fee_bps", "value": 80 } }' ``` ```json Example response theme={"system"} { "amount": "10.0", "amount_type": "exact_input", "source": { "asset": "usdc", "chain": "tempo" }, "destination": { "address": "0xRecipientAddress", "chain": "arbitrum" }, "estimated_output_amount": "9.94", "estimated_fees": [ { "type": "relayer", "amount": "0.02" }, { "type": "developer", "recipient": "0x1234567890abcdef1234567890abcdef12345678", "amount": "0.04" } ], "expires_at": 1715200000 } ``` To quote a transfer where the recipient receives exactly 100 USDC on Arbitrum, regardless of fees: ```typescript theme={"system"} const quote = await privy.wallets().transferQuote('insert-wallet-id', { amount: '100.0', amount_type: 'exact_output', source: { asset: 'usdc', chain: 'tempo', }, destination: { address: '0xRecipientAddress', chain: 'arbitrum', }, }); // quote.estimated_input_amount — estimated source amount needed (e.g. "100.06") // quote.estimated_output_amount — guaranteed destination amount ("100.0") ``` ```bash theme={"system"} curl -X POST https://api.privy.io/v1/wallets/{wallet_id}/transfer/quote \ -u ":" \ -H "privy-app-id: " \ -H "Content-Type: application/json" \ -d '{ "amount": "100.0", "amount_type": "exact_output", "source": { "asset": "usdc", "chain": "tempo" }, "destination": { "address": "0xRecipientAddress", "chain": "arbitrum" } }' ``` ```json Example response theme={"system"} { "amount": "100.0", "amount_type": "exact_output", "source": { "asset": "usdc", "chain": "tempo" }, "destination": { "address": "0xRecipientAddress", "chain": "arbitrum" }, "estimated_input_amount": "100.06", "estimated_output_amount": "100.0", "estimated_fees": [ { "type": "relayer", "amount": "0.04" } ], "expires_at": 1715200000 } ``` ## Quote expiry Quotes expire quickly — typically within a few minutes. Executing a transfer after `expires_at` means the quoted output and fees no longer apply — the transfer will proceed but at current market rates. Fetch a fresh quote before each transfer execution. ## Limitations The quote endpoint requires either `destination.chain` or `destination.asset` to differ from the source. Same-chain, same-asset transfers have no fees to quote. Transfers using `asset_address` (custom asset contracts) are not supported for cross-chain quotes. Only named assets (`usdc`, `usdt`, `eth`, etc.) can be quoted. ## API reference See the [API reference](/api-reference/wallets/transfer/quote) for the full parameter reference. # Usage Source: https://docs.privy.io/wallets/actions/transfer/usage To execute a transfer, make a `POST` request to `/v1/wallets/{wallet_id}/transfer`. ### Body In the body of the request, pass the following parameters. The source asset, amount, and chain for the transfer. Specify either `asset` (for named assets) or `asset_address` (for custom assets), not both. The named asset to transfer. Must be one of the listed [well-known assets](/wallets/actions/transfer/overview#well-known-assets). Provide either `asset` or `asset_address`. The token contract address (EVM) or mint address (Solana) of the asset to transfer. Use this field for tokens that are not first-class named assets. Provide either `asset` or `asset_address`. The amount of tokens to transfer, as a decimal string in standard units (e.g. `"10.0"` for 10 USDC). The API handles decimal precision based on the asset — most stablecoins like USDC use 6 decimal places, while native tokens like ETH use 18 and SOL uses 9. There is no need to convert to the smallest unit (e.g. wei, lamports, or micro-units) before sending the request. The chain to transfer from. Must be one of the listed [supported chains](/wallets/actions/transfer/overview#supported-chains). The destination for the transfer. The recipient wallet address. Use a hex address for EVM chains and a base58 address for Solana. The destination asset. Must be one of the listed [well-known assets](/wallets/actions/transfer/overview#well-known-assets). Required for cross-asset transfers (e.g. transferring `usdt` on the source and receiving `usdc` on the destination). The destination chain. Must be one of the listed [supported chains](/wallets/actions/transfer/overview#supported-chains). Required for cross-chain transfers (e.g. transferring from `tempo` and receiving on `arbitrum`). Amount as a decimal string in standard units (e.g. `"10.0"` for 10 USDC). For `exact_input`, the amount to send. For `exact_output`, the exact amount to receive. Takes precedence over `source.amount` when both are provided. Whether the amount refers to the input token (`exact_input`) or the output token (`exact_output`). Defaults to `exact_input`. * **`exact_input`** — the specified amount is deducted from the source wallet; the destination amount varies based on fees and bridge pricing. * **`exact_output`** — the recipient receives exactly the specified amount; the source amount is determined by the bridge provider and may be higher due to fees. Exact output is only supported for cross-chain or cross-asset (DADC) transfers on EVM source chains. Solana is not supported as a source chain for exact output. The maximum allowed slippage in basis points (1 bps = 0.01%). Must be between `0` and `10000`. Only applies to cross-chain and cross-asset transfers. If omitted, an appropriate slippage tolerance is automatically determined based on current market conditions. See [slippage](/wallets/actions/swap/overview#slippage) for details. Optional fee configuration for cross-chain transfers. The fee model to apply. Currently only `total_fee_bps` is supported. Total fee cap in basis points (0–10000). Relayer and developer fees must fit within this cap. For example, `80` represents 0.8%. ### Response The endpoint returns a `200` response with a pending [wallet action](/wallets/actions/overview) resource. The transfer action is processed asynchronously. The response contains a pending [wallet action](/wallets/actions/overview) resource with `status: "pending"` that Privy processes in the background. The unique identifier for the wallet action. The current status of the action. The ID of the wallet initiating the transfer. The ISO 8601 timestamp for when the action was created. The type of action. For transfers, this is always `transfer`. The amount type used for this transfer. Omitted when the default `exact_input` was used. The named asset being transferred (e.g. `"usdc"`, `"eth"`). Present when the transfer was initiated with a named `asset`; omitted for custom-token transfers. The token contract address (EVM) or mint address (Solana) of the transferred asset. Present when the transfer was initiated with `asset_address`. The number of decimals for the transferred token. Present when the transfer was initiated with `asset_address` and the decimals were resolved on-chain. The amount sent on the source chain as a decimal string (e.g. `"1.5"`). Omitted for `exact_output` cross-chain transfers until the source amount is determined. The chain the transfer is sent from (e.g. `"tempo"`, `"ethereum"`). The recipient wallet address. The destination asset for cross-asset transfers. Omitted for same-asset transfers. The destination chain for cross-chain transfers. Omitted for same-chain transfers. The amount received on the destination chain. Populated immediately for `exact_output` transfers, or after fill confirmation for `exact_input` cross-chain transfers. Fees paid for the transfer. Each item has a `type` (`relayer` or `developer`) and an `amount` in USD. Items of type `developer` include a `recipient` address. Present on `rejected` or `failed` actions when available. Contains a `message` string with a human-readable description of the failure, and an optional `details` field with additional context. To track the status of a transfer, see [wallet action lifecycle](/wallets/actions/overview#wallet-action-lifecycle). ## Examples Use the `transfer` convenience method on the wallets service to transfer tokens from a wallet. ```typescript {skip-check} theme={"system"} const response = await privy.wallets().transfer('insert-wallet-id', { amount: '10.0', source: { asset: 'usdc', chain: 'tempo' }, destination: { address: '0xRecipientAddress' }, fee_configuration: { type: 'total_fee_bps', value: 80 }, authorization_context: { authorization_private_keys: [''] } }); ``` The method returns a `TransferActionResponse` with the pending wallet action. See [wallet action lifecycle](/wallets/actions/overview#wallet-action-lifecycle) to track the status. ```bash theme={"system"} curl -X POST https://api.privy.io/v1/wallets/{wallet_id}/transfer \ -u ":" \ -H "privy-app-id: " \ -H "Content-Type: application/json" \ -d '{ "source": { "asset": "usdc", "amount": "10.0", "chain": "tempo" }, "destination": { "address": "0xRecipientAddress" }, "fee_configuration": { "type": "total_fee_bps", "value": 80 } }' ``` ```json Example response theme={"system"} { "id": "action-id", "status": "pending", "wallet_id": "wallet-id", "created_at": "2026-04-14T20:09:11.929Z", "type": "transfer", "source_asset": "usdc", "source_amount": "10.0", "source_chain": "tempo", "destination_address": "0xRecipientAddress", "fees": [ { "type": "relayer", "amount": "0.02" }, { "type": "developer", "recipient": "0x1234567890abcdef1234567890abcdef12345678", "amount": "0.04" } ] } ``` To send exactly 100 USDC to a recipient on Arbitrum from a Tempo wallet: ```typescript {skip-check} theme={"system"} const response = await privy.wallets().transfer('insert-wallet-id', { amount: '100.0', amount_type: 'exact_output', source: { asset: 'usdc', chain: 'tempo' }, destination: { address: '0xRecipientAddress', chain: 'arbitrum' }, authorization_context: { authorization_private_keys: [''] } }); ``` ```bash theme={"system"} curl -X POST https://api.privy.io/v1/wallets/{wallet_id}/transfer \ -u ":" \ -H "privy-app-id: " \ -H "Content-Type: application/json" \ -d '{ "amount": "100.0", "amount_type": "exact_output", "source": { "asset": "usdc", "chain": "tempo" }, "destination": { "address": "0xRecipientAddress", "chain": "arbitrum" } }' ``` ```json Example response theme={"system"} { "id": "action-id", "status": "pending", "wallet_id": "wallet-id", "created_at": "2026-04-14T20:09:11.929Z", "type": "transfer", "amount_type": "exact_output", "source_asset": "usdc", "source_amount": null, "source_chain": "tempo", "destination_address": "0xRecipientAddress", "destination_chain": "arbitrum", "destination_amount": "100.0" } ``` For `exact_output` cross-chain transfers, `source_amount` is `null` at creation because the exact source amount is determined by the bridge provider at execution time. It is populated once the transfer is confirmed on-chain. ## API reference See the [API reference](/api-reference/wallets/transfer) for more details. # Webhooks Source: https://docs.privy.io/wallets/actions/webhooks Privy emits webhooks when wallet actions change status, allowing your app to react to swaps, transfers, and earn activity in real time without polling. ## Setup Subscribe to wallet action events from the **Configuration > Webhooks** page in the [Privy Dashboard](https://dashboard.privy.io/apps?page=webhooks). For general webhook setup instructions, payload verification, and retry behavior, see the [webhooks overview](/api-reference/webhooks/overview). Webhooks can be tested at no cost in development environments. To enable webhooks in production, upgrade to the Enterprise plan in the Privy Dashboard. ## Statuses Every wallet action moves through a predictable `status` lifecycle. Privy emits a webhook at each `status` update. | Status | Description | | ------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `'created'` | Wallet action has been queued for execution and resource ID has been returned to the caller. | | `'succeeded'` | All steps of the wallet action have successfully executed. This is a terminal state. | | `'rejected'` | The wallet action was rejected prior to executing any steps, e.g. due to a policy violation. This is a terminal state and safe to retry. | | `'failed'` | The wallet action failed during execution of one of its steps. Use the [get wallet action](/api-reference/wallets/actions/get) endpoint with `?include=steps` to inspect what went wrong at the step level. | ## Action-specific payloads All wallet action webhook payloads include a `type` (event name), `status`, and action `id`. Beyond these fields, payloads will include the fields present in that wallet action's resource. For example, the payloads for `wallet_action.transfer.*` webhooks include the fields when calling fetching the transfer action resource via `GET /v1/actions/{action_id}` where `action_id` corresponds to a transfer. View the payloads for specific wallet actions below. Click the webhook event name to be redirected to its payload schema. ### Transfer | Event | Description | | ---------------------------------------------------------------------------------------------- | ---------------------------------------------- | | [`wallet_action.transfer.created`](/api-reference/webhooks/wallet-action/transfer/created) | A transfer action is created and queued | | [`wallet_action.transfer.succeeded`](/api-reference/webhooks/wallet-action/transfer/succeeded) | The transfer transaction confirms onchain | | [`wallet_action.transfer.rejected`](/api-reference/webhooks/wallet-action/transfer/rejected) | The transfer is rejected before broadcast | | [`wallet_action.transfer.failed`](/api-reference/webhooks/wallet-action/transfer/failed) | The transfer transaction fails after broadcast | ### Swap | Event | Description | | -------------------------------------------------------------------------------------- | ------------------------------------------ | | [`wallet_action.swap.created`](/api-reference/webhooks/wallet-action/swap/created) | A swap action is created and queued | | [`wallet_action.swap.succeeded`](/api-reference/webhooks/wallet-action/swap/succeeded) | The swap transaction confirms onchain | | [`wallet_action.swap.rejected`](/api-reference/webhooks/wallet-action/swap/rejected) | The swap is rejected before broadcast | | [`wallet_action.swap.failed`](/api-reference/webhooks/wallet-action/swap/failed) | The swap transaction fails after broadcast | ### Payout Payouts move crypto out of the wallet on-chain and then settle fiat at the provider, so `succeeded` means the fiat settled, not just that the crypto moved. | Event | Description | | -------------------------------- | ------------------------------------------------------------------------------------------------ | | `wallet_action.payout.created` | A payout action is created and queued | | `wallet_action.payout.succeeded` | The provider settles the fiat to the destination bank account | | `wallet_action.payout.rejected` | Privy rejects the payout before broadcasting the on-chain transfer, leaving the wallet untouched | | `wallet_action.payout.failed` | A step fails after the on-chain transfer is broadcast, either on-chain or during settlement | `failed` spans several outcomes that call for different handling, depending on whether the crypto had already left the wallet. See [payout failure modes](/financial-flows/transfers/fiat-payouts/track-payouts#failure-modes) for how to tell them apart, and for the payload fields. ### Earn View a more [detailed explanation of earn webhooks](/wallets/actions/earn/webhooks). #### Deposit | Event | Description | | ------------------------------------------------------------------------------------------------------ | --------------------------------------------- | | [`wallet_action.earn_deposit.created`](/api-reference/webhooks/wallet-action/earn-deposit/created) | A deposit action is created and queued | | [`wallet_action.earn_deposit.succeeded`](/api-reference/webhooks/wallet-action/earn-deposit/succeeded) | The deposit transaction confirms onchain | | [`wallet_action.earn_deposit.rejected`](/api-reference/webhooks/wallet-action/earn-deposit/rejected) | The deposit is rejected before broadcast | | [`wallet_action.earn_deposit.failed`](/api-reference/webhooks/wallet-action/earn-deposit/failed) | The deposit transaction fails after broadcast | #### Withdraw | Event | Description | | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------ | | [`wallet_action.earn_withdraw.created`](/api-reference/webhooks/wallet-action/earn-withdraw/created) | A withdrawal action is created and queued | | [`wallet_action.earn_withdraw.succeeded`](/api-reference/webhooks/wallet-action/earn-withdraw/succeeded) | The withdrawal transaction confirms onchain | | [`wallet_action.earn_withdraw.rejected`](/api-reference/webhooks/wallet-action/earn-withdraw/rejected) | The withdrawal is rejected before broadcast | | [`wallet_action.earn_withdraw.failed`](/api-reference/webhooks/wallet-action/earn-withdraw/failed) | The withdrawal transaction fails after broadcast | #### Claim incentive | Event | Description | | ---------------------------------------------------------------------------------------------------------------------- | ------------------------------------------- | | [`wallet_action.earn_incentive_claim.created`](/api-reference/webhooks/wallet-action/earn-incentive-claim/created) | A claim action is created and queued | | [`wallet_action.earn_incentive_claim.succeeded`](/api-reference/webhooks/wallet-action/earn-incentive-claim/succeeded) | The claim transaction confirms onchain | | [`wallet_action.earn_incentive_claim.rejected`](/api-reference/webhooks/wallet-action/earn-incentive-claim/rejected) | The claim is rejected before broadcast | | [`wallet_action.earn_incentive_claim.failed`](/api-reference/webhooks/wallet-action/earn-incentive-claim/failed) | The claim transaction fails after broadcast | # Integrating with ethers Source: https://docs.privy.io/wallets/connectors/ethereum/integrations/ethers Get an ethers.js provider and signer from a Privy connected wallet for Ethereum interactions ## Ethers Privy is fully compatible with ethers.js. To get an ethers provider for a user's connected wallet, first [find your desired wallet](/wallets/wallets/get-a-wallet/get-connected-wallet) from the **`wallets`** array and switch it to your desired network, using the wallet's **`switchChain`** method: ### Ethers v5 ```tsx theme={"system"} const privyProvider = await wallet.getEthereumProvider(); const provider = new ethers.providers.Web3Provider(privyProvider); ``` ### Ethers v6 ```tsx theme={"system"} const provider = await wallet.getEthereumProvider(); const ethersProvider = new ethers.BrowserProvider(provider); const signer = ethersProvider.getSigner(); ``` # Integrating with viem Source: https://docs.privy.io/wallets/connectors/ethereum/integrations/viem Create a viem wallet client from a Privy connected wallet for type-safe Ethereum interactions Viem represents connected wallets as a [**wallet client**](https://viem.sh/docs/clients/wallet.html) object, which you can use to get information about the current wallet or the request signatures and transactions. To get a viem wallet client for a user's connected wallet, first import your desired network from the **`viem/chains`** package and import the **`createWalletClient`** method and **`custom`** transport from **`viem`**: ```tsx theme={"system"} import {createWalletClient, custom} from 'viem'; // Replace `sepolia` with your desired network import {sepolia} from 'viem/chains'; ``` Then, find your desired wallet from the **`wallets`** array and switch its network to the chain you imported, using the wallet's **`switchChain`** method: ```tsx theme={"system"} const {wallets} = useWallets(); const wallet = wallets[0]; // Replace this with your desired wallet await wallet.switchChain(sepolia.id); ``` Lastly, get the wallet's EIP1193 provider using the wallet's **`getEthereumProvider`** method and pass it to viem's **`createWalletClient`** method like so: ```tsx theme={"system"} const provider = await wallet.getEthereumProvider(); const walletClient = createWalletClient({ account: wallet.address as Hex, chain: sepolia, transport: custom(provider), }); ``` You can then use the [**wallet client**](https://viem.sh/docs/clients/wallet) to get information about the wallet or request signatures and transactions. # Integrating with wagmi Source: https://docs.privy.io/wallets/connectors/ethereum/integrations/wagmi Integrate Privy with wagmi hooks for React-based Ethereum wallet interactions [Wagmi](https://wagmi.sh/) is a set of React hooks for interfacing with Ethereum wallets, allowing you read wallet state, request signatures or transactions, and take read and write actions on the blockchain. **Privy is fully compatible with [wagmi](https://wagmi.sh/), and you can use [wagmi](https://wagmi.sh/)'s React hooks to interface with external and embedded wallets from Privy.** Just follow the steps below! ## Integration steps This guide assumes you have already integrated Privy into your app. If not, please begin with the Privy [Quickstart](/basics/react/quickstart). ### 1. Install dependencies Install the latest versions of [**`wagmi`**](https://www.npmjs.com/package/wagmi), [**`@tanstack/react-query`**](https://www.npmjs.com/package/@tanstack/react-query), [**`@privy-io/react-auth`**](https://www.npmjs.com/package/@privy-io/react-auth), and [**`@privy-io/wagmi`**](https://www.npmjs.com/package/@privy-io/wagmi): ```sh theme={"system"} npm i wagmi @privy-io/react-auth @privy-io/wagmi @tanstack/react-query ``` ### 2. Setup TanStack Query To start, set up your app with the [TanStack Query's React Provider](https://tanstack.com/query/v5/docs/framework/react/overview). Wagmi uses TanStack Query under the hood to power its data fetching and caching of wallet and blockchain data. To set up your app with TanStack Query, in the component where you render your **`PrivyProvider`**, import the [**`QueryClient`**](https://tanstack.com/query/v4/docs/reference/QueryClient) class and the [**`QueryClientProvider`**](https://tanstack.com/query/latest/docs/framework/react/reference/QueryClientProvider) component from [**`@tanstack/react-query`**](https://www.npmjs.com/package/@tanstack/react-query): ```tsx theme={"system"} import {QueryClient, QueryClientProvider} from '@tanstack/react-query'; ``` Next, create a new instance of the [**`QueryClient`**](https://tanstack.com/query/v4/docs/reference/QueryClient): ```tsx theme={"system"} const queryClient = new QueryClient(); ``` Then, like the **`PrivyProvider`**, wrap your app's components with the [**`QueryClientProvider`**](https://tanstack.com/query/latest/docs/framework/react/reference/QueryClientProvider). This must be rendered *inside* the **`PrivyProvider`** component. ```tsx providers.tsx theme={"system"} {children} ``` For the [**`client`**](https://tanstack.com/query/latest/docs/framework/react/reference/QueryClientProvider) property of the [**`QueryClientProvider`**](https://tanstack.com/query/latest/docs/framework/react/reference/QueryClientProvider), pass the [**`queryClient`**](https://tanstack.com/query/v4/docs/reference/QueryClient) instance you created. ### 3. Setup wagmi Next, setup wagmi. This involves creating your wagmi **`config`** and wrapping your app with the **`WagmiProvider`**. While completing the wagmi setup, make sure to import `createConfig` and `WagmiProvider` from `@privy-io/wagmi`. Do not import these from `wagmi` directly. #### Build your wagmi config To build your [**`wagmi`**](https://wagmi.sh) config, import the `createConfig` method from [**`@privy-io/wagmi`**](https://www.npmjs.com/package/@privy-io/wagmi): ```tsx wagmiConfig.ts theme={"system"} import {createConfig} from '@privy-io/wagmi'; ``` This is a drop-in replacement for [wagmi's native **`createConfig`**](https://wagmi.sh/react/getting-started#create-config), but ensures that the appropriate configuration options are set for the Privy integration. Specifically, it allows Privy to drive wagmi's connectors state, enabling the two libraries to stay in sync. Next, import your app's required chains from [**`viem/chains`**](https://viem.sh/docs/chains/introduction.html) and the [**`http`**](https://wagmi.sh/core/api/transports/http#http) transport from [**`wagmi`**](https://www.npmjs.com/package/wagmi). Your app's required chains should match whatever you configure as [**`supportedChains`**](/basics/react/advanced/configuring-evm-networks#supported-chains) for Privy. ```tsx theme={"system"} import {mainnet, sepolia} from 'viem/chains'; import {http} from 'wagmi'; // Replace this with your app's required chains ``` Lastly, call `createConfig` with your imported chains and the [**`http`**](https://wagmi.sh/core/api/transports/http#http) transport like so: ```tsx wagmiConfig.ts theme={"system"} // Make sure to import `createConfig` from `@privy-io/wagmi`, not `wagmi` import {createConfig} from '@privy-io/wagmi'; ... export const config = createConfig({ chains: [mainnet, sepolia], // Pass your required chains as an array transports: { [mainnet.id]: http(), [sepolia.id]: http(), // For each of your required chains, add an entry to `transports` with // a key of the chain's `id` and a value of `http()` }, }); ``` #### Wrap your app with the `WagmiProvider` Once you've built your wagmi `config`, in the same component where you render your **`PrivyProvider`**, import the `WagmiProvider` component from [**`@privy-io/wagmi`**](https://www.npmjs.com/package/@privy-io/wagmi). ```tsx theme={"system"} import {WagmiProvider} from '@privy-io/wagmi'; ``` This is a drop-in replacement for [wagmi's native **`WagmiProvider`**](https://wagmi.sh/react/api/WagmiProvider#wagmiprovider), but ensures the necessary configuration properties for Privy are set. Specifically, it ensures that the [**`reconnectOnMount`**](https://wagmi.sh/react/api/WagmiProvider#reconnectonmount) prop is set to false, which is required for handling the embedded wallet. Wallets will still be automatically reconnected on mount. Then, like the **`PrivyProvider`**, wrap your app's components with the `WagmiProvider`. This must be rendered *inside* both the **`PrivyProvider`** and [**`QueryClientProvider`**](https://tanstack.com/query/latest/docs/framework/react/reference/QueryClientProvider) components. ```tsx providers.tsx theme={"system"} import {PrivyProvider} from '@privy-io/react-auth'; // Make sure to import `WagmiProvider` from `@privy-io/wagmi`, not `wagmi` import {WagmiProvider} from '@privy-io/wagmi'; import {QueryClientProvider} from '@tanstack/react-query'; ... {children} ``` For the `config` property of the `WagmiProvider`, pass the `config` you created earlier. #### Complete example Altogether, this should look like: ```tsx theme={"system"} import {QueryClient, QueryClientProvider} from '@tanstack/react-query'; import {PrivyProvider} from '@privy-io/react-auth'; // Make sure to import these from `@privy-io/wagmi`, not `wagmi` import {WagmiProvider, createConfig} from '@privy-io/wagmi'; import {privyConfig} from './privyConfig'; import {wagmiConfig} from './wagmiConfig'; const queryClient = new QueryClient(); export default function Providers({children}: {children: React.ReactNode}) { return ( {children} ); } ``` ```tsx theme={"system"} import {mainnet, sepolia} from 'viem/chains'; import {http} from 'wagmi'; import {createConfig} from '@privy-io/wagmi'; // Replace these with your app's chains export const config = createConfig({ chains: [mainnet, sepolia], transports: { [mainnet.id]: http(), [sepolia.id]: http() } }); ``` ```tsx theme={"system"} import type {PrivyClientConfig} from '@privy-io/react-auth'; // Replace this with your Privy config export const privyConfig: PrivyClientConfig = { embeddedWallets: { createOnLogin: 'users-without-wallets', requireUserPasswordOnCreate: true, showWalletUIs: true }, loginMethods: ['wallet', 'email', 'sms'], appearance: { showWalletLoginFirst: true } }; ``` **That's it! You've successfully integrated Privy alongside [`wagmi`](https://wagmi.sh) in your app! 🎉** ### 4. Use `wagmi` throughout your app Once you've completed the setup above, you can use [**`wagmi`**](https://wagmi.sh)'s React hooks throughout your app to interface with wallets and take read and write actions on the blockchain. #### Using `wagmi` hooks To use [**`wagmi`**](https://wagmi.sh) hooks, like [**`useAccount`**](https://wagmi.sh/react/api/hooks/useAccount#useaccount), in your components, import the hook directly from [**`wagmi`**](https://wagmi.sh) and call it as usual: ```tsx theme={"system"} import {useAccount} from 'wagmi'; export default const WalletAddress = () => { const {address} = useAccount(); return

Wallet address: {address}

; } ``` Injected wallets, like the MetaMask browser extension, cannot be programmatically disconnected from your site; they can only be manually disconnected. In kind, Privy does not currently support programmatically disconnecting a wallet via wagmi's [`useDisconnect`](https://wagmi.sh/react/api/hooks/useDisconnect) hook. This hook "shims" a disconnection, which can create discrepancies between what wallets are connected to an app vs. wagmi. Instead of disconnecting a given wallet, you can always prompt a user to connect a different wallet via the [`connectWallet`](/wallets/connectors/usage/connecting-external-wallets) method. #### When to use Privy vs. `wagmi` Both Privy's out-of-the-box interfaces and wagmi's React hooks enable you to interface with wallets and to request signatures and transactions. If your app integrates Privy alongside wagmi, you should: * use Privy to connect external wallets and create embedded wallets * use [**`wagmi`**](https://wagmi.sh) to take read or write actions from a connected wallet #### Updating the active wallet With Privy, users may have multiple wallets connected to your app, but [**`wagmi`**](https://wagmi.sh)'s React hooks return information for only *one* connected wallet at a time. This is referred to as the **active wallet**. To update [**`wagmi`**](https://wagmi.sh) to return information for a *different* connected wallet, first import the **`useWallets`** hook from [**`@privy-io/react-auth`**](https://www.npmjs.com/package/@privy-io/react-auth) and the `useSetActiveWallet` hook from [**`@privy-io/wagmi`**](https://www.npmjs.com/package/@privy-io/wagmi): ```tsx theme={"system"} import {useWallets} from '@privy-io/react-auth'; import {useSetActiveWallet} from '@privy-io/wagmi'; ``` Then, find your desired active wallet from the **`wallets`** array returned by **`useWallets`** ```tsx theme={"system"} const {wallets} = useWallets(); // Replace this logic to find your desired wallet const newActiveWallet = wallets.find((wallet) => wallet.address === 'insert-your-desired-address'); ``` Lastly, pass your desired active wallet to the `setActiveWallet` method returned by the `useSetActiveWallet` hook: ```tsx theme={"system"} await setActiveWallet(newActiveWallet); ``` ## Demo app Check out our [wagmi demo app](https://wagmi-app.vercel.app) to see the hooks listed above in action. Feel free to take a look at the [app's source code](https://github.com/privy-io/examples/tree/main/examples/privy-next-wagmi) to see an end-to-end implementation of Privy with wagmi. # Overview Source: https://docs.privy.io/wallets/connectors/overview Connect external wallets (MetaMask, Phantom, Coinbase) to your app using Privy wallet connectors. Privy can be integrated with all popular wallet connectors so your application can easily interface with your users wallets. Privy is built to connect with all external wallets, including those on browser and mobile devices, so that users can bring their existing wallets and assets into your app. You can integrate Wagmi, Viem, Ethers, @solana/web3.js, and web3swift to manage embedded or external wallets on your app. This compatibility allows your application to interface with all of your user's wallets in your existing web3 stack. Connectors3 # Configure external connector chains Source: https://docs.privy.io/wallets/connectors/setup/configuring-external-connector-chains Configure which chain types (EVM, Solana) external wallet connectors support in your app Privy supports connecting wallets on both EVM networks and Solana to your application. To configure your app for the wallet types you need, follow the steps below. ## Configuring EVM/Solana external connectors If you are connecting to Solana wallets, you must also initialize Solana connectors using Privy's `toSolanaWalletConnectors` method and pass them to the `config.externalWallets.solana.connectors` field. In your `PrivyProvider`, set the `config.appearance.walletChainType` to `'ethereum-and-solana'`. ```tsx theme={"system"} import {PrivyProvider} from '@privy-io/react-auth'; import {toSolanaWalletConnectors} from "@privy-io/react-auth/solana"; {children} ``` `toSolanaWalletConnectors` accepts an optional `shouldAutoConnect` parameter (defaults to `true`) that silently reconnects previously-authorized Solana wallets on page load. Set it to `false` if connecting a wallet on page load triggers an unwanted connection prompt. In your `PrivyProvider`, set the `config.appearance.walletChainType` to `'ethereum-only'`. ```tsx theme={"system"} import {PrivyProvider} from '@privy-io/react-auth'; {children} ``` If you are connecting to Solana wallets, you must also initialize Solana connectors using Privy's `toSolanaWalletConnectors` method and pass them to the `config.externalWallets.solana.connectors` field. In your `PrivyProvider`, set the `config.appearance.walletChainType` to `'solana-only'`. ```tsx theme={"system"} import {PrivyProvider} from '@privy-io/react-auth'; import {toSolanaWalletConnectors} from "@privy-io/react-auth/solana"; {children} ``` `toSolanaWalletConnectors` accepts an optional `shouldAutoConnect` parameter (defaults to `true`) that silently reconnects previously-authorized Solana wallets on page load. Set it to `false` if connecting a wallet on page load triggers an unwanted connection prompt. # Configure wallet options Source: https://docs.privy.io/wallets/connectors/setup/configuring-external-connector-wallets Customize the list of external wallets shown to users with WalletListEntry configuration **Looking for examples?** Check out our [Wallet List Configuration Recipes](/recipes/react/wallet-list-configurations) for common configurations with code examples. To customize the external wallet options for your app, pass in a **`WalletListEntry`** array to the **`config.appearance.walletList`** property. When users login with, connect, or link an external wallet in your app, the possible options (e.g. MetaMask, Rainbow, WalletConnect) will be presented to users in the order you configure them in this array. ```tsx theme={"system"} {children} ``` When your React web app is accessed through the in-app browser of a mobile wallet (e.g., Rainbow, Phantom, etc.) and that wallet is selected as a login option, the Privy SDK will automatically detect the wallet object and prompt the user to connect in app. However, if your app is accessed via a standard browser (e.g., Chrome, Safari, etc.), Privy will default to using WalletConnect for mobile wallet connection. You can also configure which wallet options to show at runtime, by passing in `walletList` to the `connectWallet` method: ```tsx theme={"system"} import {usePrivy} from '@privy-io/react-auth'; const {connectWallet} = usePrivy(); ; ``` *** ## Available Wallet List Entries ### Special Entries | Entry | Chain Support | Platform | Description | | --------------------------- | ------------- | -------- | ------------------------------------------------------------------------------------------------ | | `detected_ethereum_wallets` | Ethereum | Desktop | All detected Ethereum browser extensions not explicitly listed elsewhere | | `detected_solana_wallets` | Solana | Desktop | All detected Solana browser extensions not explicitly listed elsewhere | | `wallet_connect` | Both\* | Both | Shows ALL WalletConnect registry wallets (100+ options) as individual buttons (\*see limitation) | | `wallet_connect_qr` | Ethereum | Desktop | Shows single "WalletConnect" button with QR code for Ethereum | | `wallet_connect_qr_solana` | Solana | Desktop | Shows single "WalletConnect" button with QR code for Solana | **About `detected_*_wallets`**: These entries only include wallets that are **not** explicitly listed elsewhere in your `walletList`. For example, if you include both `'metamask'` and `'detected_ethereum_wallets'`, MetaMask will appear at the position of `'metamask'`, not under `detected_ethereum_wallets`. **Current Limitation with `wallet_connect` in Multi-Chain Apps** If your app supports both Ethereum and Solana (`walletChainType: 'ethereum-and-solana'`), be aware that `wallet_connect` has a session limitation: * Once a user connects to an **Ethereum chain** via WalletConnect, they **cannot connect to Solana chains** in the same session * Once a user connects to a **Solana chain** via WalletConnect, they **cannot connect to Ethereum chains** in the same session ### Ethereum Wallets * `metamask` - MetaMask browser extension and mobile wallet * `coinbase_wallet` - Coinbase Wallet * `rainbow` - Rainbow wallet * `zerion` - Zerion wallet * `safe` - Safe (formerly Gnosis Safe) * `uniswap` - Uniswap Wallet * `kraken_wallet` - Kraken Wallet * `binance` - Binance Wallet * `okx_wallet` - OKX Wallet * `bybit_wallet` - Bybit Wallet * `bitget_wallet` - Bitget Wallet (formerly BitKeep) * `cryptocom` - Crypto.com DeFi Wallet * `universal_profile` - Universal Profile * `ronin_wallet` - Ronin Wallet * `base_account` - Base Account (formerly Coinbase Smart Wallet) ### Solana Wallets **Solana wallets require additional configuration**. You must: 1. Set `walletChainType` to `'solana-only'` or `'ethereum-and-solana'` 2. Configure `externalWallets.solana.connectors` with `toSolanaWalletConnectors()` See [Configuring external connectors](/recipes/react/configuring-external-connectors) for setup instructions. * `phantom` - Phantom wallet (supports both Ethereum and Solana) * `solflare` - Solflare wallet * `backpack` - Backpack wallet * `jupiter` - Jupiter wallet * `haha_wallet` - Haha wallet ### Deprecated Entries * `detected_wallets` - **Deprecated**. Use `detected_ethereum_wallets` or `detected_solana_wallets` instead * `rabby_wallet` - **Deprecated**. Rabby Wallet is no longer supported *** ## Understanding WalletConnect Options ### `wallet_connect` vs `wallet_connect_qr` vs `wallet_connect_qr_solana` **Shows**: A searchable list of 100+ individual WalletConnect-supported wallets, each with its own connection button. **Best for:** * Desktop users who want to browse available wallets * Supporting the long-tail of wallets not explicitly in your list * When you want users to see all WalletConnect options ```tsx theme={"system"} walletList: ['metamask', 'rainbow', 'wallet_connect'] ``` **Shows**: A single "WalletConnect" button that displays a universal QR code for Ethereum wallets when clicked. **Platform**: Desktop only (does not work on mobile browsers) **Best for:** - Desktop users connecting their mobile wallets via QR code scan `tsx walletList: ['metamask', 'rainbow', 'wallet_connect_qr'] ` **Shows**: A single "WalletConnect" button that displays a universal QR code for Solana wallets when clicked. **Platform**: Desktop only (does not work on mobile browsers) ```tsx theme={"system"} walletList: ['phantom', 'solflare', 'wallet_connect_qr_solana'] ``` *** ## WalletConnect Configuration FAQ ### Why do I see wallets I didn't configure? If you include `wallet_connect` in your `walletList`, Privy will show **ALL wallets** from the WalletConnect registry that support your configured chains. To show only specific wallets, remove `wallet_connect` and list individual wallets explicitly. ```tsx theme={"system"} // ❌ Shows 100+ wallets from registry walletList: ['wallet_connect']; // ✅ Shows only the wallets you specify walletList: ['metamask', 'rainbow', 'coinbase_wallet', 'wallet_connect_qr']; ``` ### Why isn't my wallet showing up? **Common causes and solutions:** 1. **Chain mismatch** - Solana wallets require `walletChainType: 'solana-only'` or `'ethereum-and-solana'` ```tsx theme={"system"} // ❌ WRONG - Solflare won't show (missing Solana configuration) config: { appearance: { walletList: ['solflare'] } } // ✅ CORRECT config: { appearance: { walletList: ['solflare'], walletChainType: 'solana-only' }, externalWallets: { solana: { // if not specified, solana wallets will show but connector won't work and defaults to opening the wallet installation page connectors: toSolanaWalletConnectors() } } } ``` 2. **Not in walletList** - The wallet must be explicitly included or covered by `detected_*_wallets` 3. **Mobile browser limitations** - Wallets aren't injected in mobile web browser environments, with the exception of the in-app browser for a few wallets (see [In-App Browsers](#in-app-browsers)). Thus, `detected_*_wallets` will show empty in mobile environments ```tsx theme={"system"} // ❌ WRONG - Nothing shows on mobile Safari/Chrome walletList: ['detected_ethereum_wallets']; // ✅ CORRECT - Works on mobile walletList: [ 'detected_ethereum_wallets', // Works on desktop 'metamask' // Works on both ]; ``` 4. **Extension not installed** - Browser extension wallets only appear if installed (unless using `wallet_connect`) ### What does `detected_ethereum_wallets` / `detected_solana_wallets` do? These entries show ALL wallets that Privy detects (via EIP-6963, window\.ethereum, or mobile in-app browsers) that **aren't explicitly listed elsewhere** in your `walletList`. **Important ordering rule:** If you have both a specific wallet name (e.g., `'metamask'`) and `'detected_ethereum_wallets'` in your list, the wallet will appear at the position of the specific name, NOT at the position of `detected_ethereum_wallets`. ```tsx theme={"system"} // ❌ POTENTIAL ISSUE - Detected wallets show first walletList: ['detected_solana_wallets', 'phantom']; // Result: If Phantom is detected, it appears below detected_solana_wallets // This can cause confusing duplicate-looking entries // ✅ CORRECT - Named wallets first, then detected ones walletList: ['phantom', 'solflare', 'detected_solana_wallets']; // Result: Phantom and Solflare always show first, then any other detected wallets ``` ### Why am I seeing "No wallets found" on mobile? This typically happens when you only have `detected_*_wallets` or `wallet_connect_qr` entries in your walletList on mobile browsers where they aren't supported. **Fix:** ```tsx theme={"system"} // ❌ WRONG - Nothing shows on mobile browsers walletList: ['detected_ethereum_wallets', 'wallet_connect_qr']; // ✅ CORRECT - Add mobile-friendly options walletList: [ 'detected_ethereum_wallets', 'metamask', // Shows "Open in MetaMask" option on mobile 'phantom', 'rainbow' ]; ``` ### Can I use WalletConnect's modal directly? No. Privy handles wallet connections through its own UI for a consistent experience. Attempting to use WalletConnect's modal directly will result in an error: ``` "WalletConnect modal not available - Privy handles wallet connections through its own UI" ``` Instead, use `wallet_connect`, `wallet_connect_qr`, or `wallet_connect_qr_solana` in your `walletList`. ### How does wallet ordering work? Wallets appear in the **exact order** you specify in `walletList`. Wallets matching `detected_*_wallets` entries appear at that position in the list, sorted alphabetically within their group. ```tsx theme={"system"} walletList: [ 'metamask', // Position 1 (if detected, appears here) 'rainbow', // Position 2 'detected_ethereum_wallets', // Position 3+ (other detected wallets, alphabetically) 'wallet_connect_qr' // Last position ]; ``` ### How do I support a wallet that's not in the detected list? 1. **If the wallet supports WalletConnect**: Add `wallet_connect` to your `walletList` as a fallback 2. **If it's a popular wallet**: Check if it has a dedicated entry in the list above 3. **If it's a new/niche wallet**: Use `wallet_connect` to provide access through the WalletConnect registry *** ## Platform Considerations ### Mobile Browser Limitations Wallets aren't injected in mobile web browser environments, with the exception of the in-app browser for a few wallets (see [In-App Browsers](#in-app-browsers) section below). Thus, adding `detected_ethereum_wallets` or `detected_solana_wallets` will show empty in mobile environments. On mobile: * ✅ **Works**: Specific wallet names (like `'metamask'`, `'phantom'`), `wallet_connect` (full registry) * ❌ **Doesn't work**: `wallet_connect_qr`, `wallet_connect_qr_solana`, `detected_ethereum_wallets`, `detected_solana_wallets` **Recommended mobile configuration:** ```tsx theme={"system"} walletList: [ 'metamask', // Shows "Open in MetaMask" button 'phantom', 'rainbow', 'coinbase_wallet' ]; ``` ### In-App Browsers On mobile, some wallets will connect via the in-app browser of that wallet's mobile app. These wallets include: * **Phantom** (Ethereum and Solana) * **Backpack** (Ethereum and Solana) * **OKX Wallet** (Ethereum and Solana) * **Solflare** (Solana only) * **Jupiter Wallet** (Solana only) When users access your app through one of these wallet's built-in browsers: * The wallet is automatically detected * No additional configuration needed * The detected wallet is prioritized regardless of `walletList` order ### Desktop Optimization For desktop-focused apps, emphasize detected wallets and browser extensions: ```tsx theme={"system"} walletList: [ 'metamask', 'rainbow', 'coinbase_wallet', 'detected_ethereum_wallets', // Shows all installed browser extensions 'wallet_connect' // Fallback for wallets not installed ]; ``` ### Dynamic Platform Configuration You can show different wallets based on the user's platform: ```tsx theme={"system"} const {connectWallet} = usePrivy(); const isMobile = /iPhone|iPad|iPod|Android/i.test(navigator.userAgent); const openWalletModal = () => { connectWallet({ walletList: isMobile ? ['metamask', 'phantom', 'rainbow', 'coinbase_wallet'] : ['metamask', 'rainbow', 'wallet_connect_qr', 'detected_ethereum_wallets'] }); }; ``` *** ## Quick Reference | Entry | Shows | Platform | Chain Support | | --------------------------- | -------------------------------------- | ----------------------------------------------------------- | ------------------------------- | | `detected_ethereum_wallets` | All detected EVM browser extensions | Desktop (browser extensions), Mobile (in-app browsers only) | Ethereum | | `detected_solana_wallets` | All detected Solana browser extensions | Desktop (browser extensions), Mobile (in-app browsers only) | Solana | | `wallet_connect` | List of 100+ WalletConnect wallets | Desktop, Mobile | Both | | `wallet_connect_qr` | Universal QR code button | Desktop only | Ethereum | | `wallet_connect_qr_solana` | Universal QR code button | Desktop only | Solana | | `metamask` | MetaMask option | Desktop (extension), Mobile (deep link/in-app) | Ethereum | | `phantom` | Phantom option | Desktop (extension), Mobile (deep link/in-app) | Solana (+ Ethereum with config) | | `coinbase_wallet` | Coinbase Wallet option | Desktop, Mobile | Ethereum | | `rainbow` | Rainbow option | Desktop, Mobile | Ethereum | **Note**: `wallet_connect_qr` and `wallet_connect_qr_solana` are desktop-only features and will not display on mobile browsers. **Solana wallets require**: `externalWallets.solana.connectors` configuration and appropriate `walletChainType` setting. *** Coinbase Smart Wallet is now [Base Account](https://www.base.org/build/base-account). If you support Coinbase Smart Wallet, you should add the `base_account` option to your walletList while keeping `coinbase_wallet` if you'd like to maintain support for existing Coinbase Smart Wallet users. For more details, see the [Base Account migration guide](https://docs.base.org/base-account/guides/migration-guide). *** ## Related Resources * [Wallet List Configuration Recipes](/recipes/react/wallet-list-configurations) - Practical examples for common use cases * [Configuring External Connectors](/recipes/react/configuring-external-connectors) - Setting up Solana and EVM connectors * [Connecting External Wallets](/wallets/connectors/usage/connecting-external-wallets) - Using the `connectWallet` method # Integrating with @solana/kit Source: https://docs.privy.io/wallets/connectors/solana/kit-integrations Privy's **ConnectedStandardSolanaWallet** object is fully compatible with popular web3 libraries for interfacing wallets and signing transactions and messages, such as [`@solana/kit`](https://www.solanakit.com/). Read below to learn how to best integrate Privy alongside `@solana/kit`. First find your desired wallet from the **`wallets`** array: ```tsx theme={"system"} import {useWallets} from '@privy-io/react-auth/solana'; const {wallets} = useWallets(); const wallet = wallets[0]; // Replace this with your desired wallet ``` ## Signing Transactions Transactions generated by `@solana/kit` can be signed using the `signTransaction` method from the `useStandardSignTransaction` hook. ```tsx theme={"system"} import { pipe, createTransactionMessage, setTransactionMessageFeePayer, setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstructions, compileTransaction, createNoopSigner, createSolanaRpc, getTransactionEncoder } from '@solana/kit'; import {getTransferSolInstruction} from '@solana-program/system'; import {useStandardSignTransaction} from '@privy-io/react-auth/solana'; const {signTransaction} = useStandardSignTransaction(); const LAMPORTS_PER_SOL = 1_000_000_000; const transferInstruction = getTransferSolInstruction({ amount: LAMPORTS_PER_SOL * 1, destination: address(to), source: createNoopSigner(address(wallet.address)) }); const {getLatestBlockhash} = createSolanaRpc('YOUR_SOLANA_RPC_URL'); const {value: latestBlockhash} = await getLatestBlockhash().send(); // Create transaction const transaction = pipe( createTransactionMessage({version: 0}), (tx) => setTransactionMessageFeePayer(address(wallet.address), tx), (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), (tx) => appendTransactionMessageInstructions([transferInstruction], tx), (tx) => compileTransaction(tx) ); const encodedTransaction = getTransactionEncoder().encode(transaction); // Sign the transaction const signedTransaction = await signTransaction({ transaction: new Uint8Array(encodedTransaction), wallet: wallet }); ``` ## Sending Transactions Transactions signed using the `signTransaction` method can be sent using the `signAndSendTransaction` method from the `useStandardSignAndSendTransaction` hook. Some external wallets connected via `wallet_connect_qr_solana` (such as Fireblocks) only support signing and do not support sign-and-send. For these wallets, `signTransaction` returns a raw 64-byte signature instead of a fully signed transaction. See the [sign a transaction](/wallets/using-wallets/solana/sign-a-transaction) guide for instructions on how to attach the signature to the compiled transaction and send it manually. ```tsx theme={"system"} import {useStandardSignAndSendTransaction} from '@privy-io/react-auth/solana'; const {signAndSendTransaction} = useStandardSignAndSendTransaction(); const signature = await signAndSendTransaction({ transaction: new Uint8Array(transaction), // The transaction to send, from the previous example wallet: wallet }).signature; ``` # Integrating with @solana/web3.js Source: https://docs.privy.io/wallets/connectors/solana/web3-integrations Integrate Privy Solana wallets with @solana/web3.js for program interactions and transactions Privy's **`ConnectedStandardSolanaWallet`** object is fully compatible with popular web3 libraries for interfacing wallets, such as [`@solana/web3js`](https://solana-foundation.github.io/solana-web3.js/). Read below to learn how to best integrate Privy alongside @solana/web3.js. First find your desired wallet from the **`wallets`** array: ```tsx theme={"system"} import {PublicKey, Transaction, Connection, SystemProgram} from '@solana/web3.js'; const {wallets} = useWallets(); const wallet = wallets[0]; // Replace this with your desired wallet ``` Then, use this wallet to then send Transactions using the @solana/web3.js Transaction and Connection classes: ```tsx theme={"system"} // Build out the transaction object for your desired program // https://solana-foundation.github.io/solana-web3.js/classes/Transaction.html let transaction = new Transaction(); // Send transaction console.log( await wallet.signAndSendTransaction!({ chain: 'solana:devnet', transaction: new Uint8Array( transaction.serialize({ requireAllSignatures: false, verifySignatures: false }) ) }) ); ``` # Authenticate a connected wallet Source: https://docs.privy.io/wallets/connectors/usage/authenticate Authenticate a connected external wallet by prompting login or linking after connection Once a user has connected their wallet to your app, and the wallet is available in either of the **`useWallets`** arrays, you can also prompt them to **login** with that wallet or **link** that wallet to their existing account, instead of prompting the entire **`login`** or **`linkWallet`** flow. To do so, find the **`ConnectedWallet`** or **`ConnectedStandardSolanaWallet`** object from Privy, and call the object's **`loginOrLink`** method for EVM wallets and use the **`useLoginWithSiws`** or **`useLinkWithSiws`** hooks for the Solana wallets: ```tsx theme={"system"} import {useWallets} from '@privy-io/react-auth'; ... const {wallets} = useWallets(); ... wallets[0].loginOrLink(); ``` ```tsx theme={"system"} import {base64} from '@scure/base'; import {useLoginWithSiws} from '@privy-io/react-auth'; import {useWallets} from '@privy-io/react-auth/solana'; const {wallets} = useWallets(); const {generateSiwsMessage, loginWithSiws} = useLoginWithSiws(); const message = await generateSiwsMessage({address: wallets[0].address}); const encodedMessage = new TextEncoder().encode(message); const {signature} = await wallets[0].signMessage({message: encodedMessage}); await loginWithSiws({message, signature: base64.encode(signature)}); ``` When called, **`loginOrLink`** will directly request a [SIWE](https://docs.login.xyz/general-information/siwe-overview/eip-4361) signature from the user's connected wallet to authenticate the wallet. If the user was not **`authenticated`** when the method was called, the user will become **`authenticated`** after signing the message. If the user was already **`authenticated`** when the method was called, the user will remain **`authenticated`** after signing the message, and the connected wallet will become one of the user's **`linkedAccounts`** in their **`user`** object. You might use the methods above to "split up" the connect and sign steps of external wallet login, like so: ```tsx theme={"system"} import {useConnectWallet, useWallets} from '@privy-io/react-auth'; export default function WalletButton() { const {connectWallet} = useConnectWallet(); const {wallets} = useWallets(); // Prompt user to connect a wallet with Privy modal return ( {/* Button to connect wallet */} {/* Button to login with or link the most recently connected wallet */} ); } ``` ```tsx theme={"system"} import {base64} from '@scure/base'; import {useConnectWallet, useLoginWithSiws} from '@privy-io/react-auth'; import {useWallets} from '@privy-io/react-auth/solana'; export default function WalletButton() { const {connectWallet} = useConnectWallet(); const {wallets} = useWallets(); const {generateSiwsMessage, loginWithSiws} = useLoginWithSiws() // Prompt user to connect a wallet with Privy modal return ( {/* Button to connect wallet */} {/* Button to login with the most recently connected wallet */} ); } ``` ### Sign in with Ledger #### EVM For EVM chains, Ledger is supported automatically when connecting through another wallet like MetaMask or Phantom. No additional configuration is required. #### Solana Ledger Solana hardware wallets only support transaction signatures, not the message signatures required for Sign-In With Solana (SIWS) authentication. In order to authenticate with a Solana Ledger wallet, you must mount the `useSolanaLedgerPlugin` hook **inside** your `PrivyProvider`. **Critical:** The `useSolanaLedgerPlugin` hook **must be placed inside** a component that is wrapped by `PrivyProvider`. If the hook is placed alongside or outside the `PrivyProvider`, it will not function correctly. ```tsx theme={"system"} import {PrivyProvider} from '@privy-io/react-auth'; import {useSolanaLedgerPlugin} from '@privy-io/react-auth/solana'; function SolanaLedgerSetup() { // This hook MUST be called inside a component wrapped by PrivyProvider useSolanaLedgerPlugin(); return null; } export default function App() { return ( {/* Your app components */} ); } ``` Then, when you attempt to login with a Phantom Solana wallet, you will be prompted to indicate whether you are signing with a Ledger wallet, which will initiate a separate SIWS flow wherein which a no-op transaction will be signed and used for verification. #### Headless Solana Ledger (useLoginWithSiws) When using `useLoginWithSiws` directly, use `generateSiwsOffchainMessage` to wrap the SIWS message in the [Solana off-chain message format](https://solana.com/developers/guides/advanced/off-chain-message-signing) that Ledger requires. Then pass `messageType: 'offchain-message'` to `loginWithSiws`. ```tsx theme={"system"} import {useLoginWithSiws} from '@privy-io/react-auth'; import {useWallets} from '@privy-io/react-auth/solana'; export function LoginWithLedgerButton() { const {generateSiwsMessage, generateSiwsOffchainMessage, loginWithSiws} = useLoginWithSiws(); const {wallets} = useWallets(); const handleLogin = async () => { if (!wallets?.length) return; const wallet = wallets[0]; // 1. Generate the plaintext SIWS message const message = await generateSiwsMessage({address: wallet.address}); // 2. Wrap in the Solana off-chain format that Ledger requires const offchainBytes = generateSiwsOffchainMessage({message, address: wallet.address}); // 3. Sign the off-chain bytes with the Ledger-connected wallet const {signature} = await wallet.signMessage({message: offchainBytes}); // 4. Submit the original plaintext message with messageType: 'offchain-message' await loginWithSiws({message, signature, messageType: 'offchain-message'}); }; return ; } ``` Pass the original **plaintext `message`** from step 1 to `loginWithSiws`, not the off-chain bytes. Privy's backend reconstructs the off-chain bytes from the plaintext message for signature verification. # Connect or create a wallet Source: https://docs.privy.io/wallets/connectors/usage/connect-or-create Connect an existing external wallet or create an embedded wallet if the user does not have one You can also use Privy to connect a user's external wallet if they have one, or to create an embedded wallet for them if they do not. This ensures users always have a connected wallet they can use with your application, and allows them to choose to use their external wallet if preferred. To do so, use the **`connectOrCreateWallet`** method of the **`usePrivy`** hook: ```tsx theme={"system"} const {connectOrCreateWallet} = usePrivy(); ``` This method will prompt the user to connect an external wallet, or log in with email, SMS, or socials, depending on your configured `loginMethods`, to create an embedded wallet. Privy's `connectOrCreate` interface currently only supports external and embedded wallets on EVM networks. For example, you might have a "Connect" button in your app that prompts users to connect their wallet, like so: ```tsx theme={"system"} import {useConnectOrCreateWallet} from '@privy-io/react-auth'; export default function ConnectWalletButton() { const {connectOrCreateWallet} = useConnectOrCreateWallet(); // Prompt user to connect a wallet with Privy modal return ; } ``` This method functions exactly the same as Privy's `login` method, except when users connect their external wallet, they will not automatically be prompted to authenticate that wallet by signing a message ### Callbacks You can optionally pass callbacks to the `useConnectOrCreateWallet` hook to run custom logic after connecting a wallet or to handle errors. #### `onSuccess` ```tsx theme={"system"} onSuccess: (args: {wallet: ConnectedWallet}) => Promise; ``` ##### Parameters The most recently connected wallet. #### `onError` ```tsx theme={"system"} onError: (error: Error) => Promise; ``` ##### Parameters The error that occurred during the this flow. # Connect an external wallet Source: https://docs.privy.io/wallets/connectors/usage/connecting-external-wallets Connect a user external wallet to your app for on-chain interactions To determine if Privy has fully processed all external and embedded wallet connections, use the **`ready`** boolean returned by the **`useWallets`** hooks. To prompt a user to connect an external wallet (on EVM networks or Solana) to your app, use the `connectWallet` method from the `useConnectWallet` hook. ```tsx theme={"system"} connectWallet: async ({ description?: string, walletList?: WalletListEntry[], walletChainType?: 'ethereum' | 'solana' }) => void ``` ### Usage To connect external wallets on Solana, your application must first explicitly configure Solana connectors for Privy. [Learn more](/recipes/react/configuring-external-connectors#connecting-external-wallets-on-solana) ```tsx theme={"system"} import {useConnectWallet} from '@privy-io/react-auth'; const {connectWallet} = useConnectWallet(); connectWallet(); ``` ### Parameters A description for the wallet connection prompt, which will be displayed in Privy's UI. A list of \[wallet option] that you would like Privy to display in the connection prompt. Filter the login wallet options to only show wallets that support the specified chain type. ### Callbacks You can optionally register an onSuccess or onError callback on the useConnectWallet hook. ```tsx theme={"system"} const {connectWallet} = useConnectWallet({ onSuccess: ({wallet}) => { console.log(wallet); }, onError: (error) => { console.log(error); }, }); ``` An optional callback function that is called when a user successfully connects their wallet. An optional callback function that is called when a user exits the connection flow or there is an error. # Authorization and controls Source: https://docs.privy.io/wallets/custodial-wallets/advanced/authorization-controls Custodial wallets support authorization controls in the same way as non-custodial wallets. You can add owners, signers, and [policies](/wallets/custodial-wallets/advanced/authorization-controls#policy-enforcement) to existing or new custodial wallets to control who can initiate transactions and modify wallet configuration. ## Meaning of `owner` for custodial wallets For custodial wallets, the `owner` field has a different meaning than for non-custodial wallets. Unlike non-custodial wallets, the owner for custodial wallets cannot export the wallet's private key or unilaterally execute transactions without the custodian's approval. The `owner` field represents the authorized controller who can configure wallet policies and additional signers, as well as initiate wallet operations. The owner does **not** have the ability to export the wallet's private key. All transactions are still mediated through the custody provider's infrastructure. ### Configuration guidance You may require an additional authorization key to sign over each transaction request by adding an owner and/or signer to the custodial wallet. This ensures integrity of the transaction request and adds an additional layer of security beyond API key authentication. You can configure just an owner, or an owner with additional signers. We recommend the latter if you plan to rotate keys in the future. Additional signers can also be added after the wallet is created. For detailed information on using public keys as authorization keys, see the [authorization keys documentation](/controls/authorization-keys/overview). ## Setting authorization controls on a custodial wallet To create a custodial wallet with an owner, provide the `owner` argument with a public key as part of [wallet creation](/wallets/custodial-wallets/create-custodial-wallet). You can update an existing custodial wallet's owner, signers, or policies using the `PATCH /wallets/{id}` endpoint. See the [wallets API reference](/wallets/wallets/update-a-wallet) for details. When providing the `public_key` input, make sure to include `\n` to indicate newlines in the public key string. ## Additional signers You may also set [additional signers](/wallets/using-wallets/signers/overview) on a custodial wallet, which are authorized keys that can initiate transaction requests for the wallet according to set signer-specific policies. ## Signing transaction requests Once a custodial wallet has an owner or signer, all requests to Privy's `/wallets/{id}/rpc` endpoint require an [authorization signature](/controls/authorization-keys/using-owners/sign/overview) in the `privy-authorization-signature` header. ## Policy enforcement Custodial wallets support the same robust policy engine available for all Privy wallets. View the complete [Policies documentation](/controls/policies/overview) to learn about all available policy options and configuration. ## Next steps Monitor wallet events and transaction status Execute transactions from custodial wallets # Funding Source: https://docs.privy.io/wallets/custodial-wallets/advanced/funding Just like for non-custodial wallets, you can fund custodial wallets through a number of [different methods](/financial-flows/payments). Most commonly, you would use Bridge to onramp and offramp funds from your Bridge-custodied wallets. Below is our recommended setup for using Privy custodial wallets with Bridge orchestration: ## Types of conversions ### Fiat to stablecoin To onramp funds from a bank account to your Privy custodial wallet, you can use either of the following methods: * [Virtual account](https://apidocs.bridge.xyz/platform/orchestration/virtual_accounts/virtual-account): Permanent, reusable fiat deposit addresses that convert incoming fiat into stablecoins, directly deposited into the Privy custodial wallet. * [Transfers](https://apidocs.bridge.xyz/platform/orchestration/transfers/transfer): Generate deposit instructions for the sender. Once the sender completes the deposit, the stablecoins are deposited into the Privy custodial wallet. ### Stablecoin to stablecoin When funding from another wallet via a different chain or asset, you can use [liquidation addresses](https://apidocs.bridge.xyz/platform/orchestration/liquidation_address/liquidation_address) to convert the funds into the desired stablecoin and deposit them into the Privy custodial wallet. ### Stablecoin to fiat To offramp funds from your Privy custodial wallet to a bank account, use the Bridge [transfers API](https://apidocs.bridge.xyz/platform/orchestration/transfers/transfer) to generate onchain deposit instructions for the Privy custodial wallet. Once funds have been sent from the wallet, the funds will land in the bank account provided. ## Payout flow When sending funds out of the Privy custodial wallet using Bridge orchestration to either an onchain destination or bank account, the flow uses both the Bridge API and Privy API: 1. Call the Bridge [transfers API](https://apidocs.bridge.xyz/platform/orchestration/transfers/transfer) to generate the `source_deposit_instructions` for the Privy custodial wallet. 2. Use Privy to [send the funds](/wallets/custodial-wallets/sending-funds) from the Privy custodial wallet to `source_deposit_instructions.address`. 3. Wait until funds have been converted to the desired destination asset and location (e.g. onchain or bank account). If an offramp is unsuccessful, the funds will be returned onchain. You can listen to deposit webhooks to know whether a deposit is because of an offramp refund by checking the `bridge_metadata` field. See the [webhooks documentation](/wallets/custodial-wallets/advanced/webhooks) for more details. ## Next steps Monitor wallet events and transaction status Execute transactions from custodial wallets # Webhooks Source: https://docs.privy.io/wallets/custodial-wallets/advanced/webhooks Privy emits webhooks for custodial wallet events, enabling your app to stay synchronized with wallet state and transfer lifecycle. Custodial wallets support both balance and transfer webhooks. Webhooks can be tested at no cost in development environments. To enable webhooks in production, upgrade to the Enterprise plan in the Privy Dashboard. ### Transfer events When you initiate a transfer from a custodial wallet via `/transfer`, Privy emits wallet action webhooks as the transfer progresses through its [lifecycle](/wallets/custodial-wallets/transaction-lifecycle): | Event | Description | | ---------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------- | | [`wallet_action.transfer.created`](/api-reference/webhooks/wallet-action/transfer/created) | The transfer action has been created and submitted to the custodian for review. | | [`wallet_action.transfer.succeeded`](/api-reference/webhooks/wallet-action/transfer/succeeded) | The transfer has been confirmed on-chain. | | [`wallet_action.transfer.rejected`](/api-reference/webhooks/wallet-action/transfer/rejected) | The transfer was rejected by the custodian during compliance review. | | [`wallet_action.transfer.failed`](/api-reference/webhooks/wallet-action/transfer/failed) | The transfer failed after being initiated on-chain. | The webhook payload contains the full wallet action response, including the transfer details (source asset, amount, chain, destination address) and the current status. See the [wallet action webhooks documentation](/wallets/actions/webhooks) for payload structure and setup instructions. ### Balance events You can subscribe to balance change events when funds are deposited or withdrawn via webhooks. For complete details on balance webhook events, payloads, and setup, see the [balance event webhooks documentation](/wallets/gas-and-asset-management/assets/balance-event-webhooks). If the deposit is an outcome of orchestration via a known provider (e.g. onramp, a refund from a failed offramp attempt, or indirect deposit from another crypto wallet), the `wallet.funds_deposited` [webhook payload](/wallets/gas-and-asset-management/assets/balance-event-webhooks#payload) would include an additional `bridge_metadata` field with orchestration details: Method used to orchestrate the deposit. Type of the orchestration. `fiat_deposit` for onramp from fiat, `crypto_deposit` for deposit from crypto, and `refund` for a refund from a failed offramp attempt. ID of the activity that caused the deposit. Only applicable when `method` is `virtual_account`. ID of the transfer that caused the deposit. Only applicable when `method` is `transfer`. ID of the liquidation address that caused the deposit. Only applicable when `method` is `liquidation_address`. ID of the drain event of the deposit. Only applicable when `method` is `liquidation_address`. Address of the source wallet that initiated the deposit. Only applicable when `method` is `transfer` or `liquidation_address`. Hash of the original transaction that caused the offramp attempt. Only applicable when `type` is `refund`. ID of the virtual account that caused the deposit. Only applicable when `method` is `virtual_account`. ```json theme={"system"} { "event": "wallet.funds_deposited", "data": { "amount": "2340000", "asset": { "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", "type": "erc20" }, "bridge_metadata": { "activity_id": "dbd8b3ce-b7cc-4bc9-8e99-adf8defbb25d", "method": "virtual_account", "type": "fiat_deposit", "virtual_account_id": "bf187db1-1bd5-4716-81d3-e51b01e3637d" } } ... } ``` ## Next steps Complete guide to wallet action lifecycle webhooks Complete guide to using Bridge to onramp and offramp funds from your Privy custodial wallets Complete guide to balance change webhooks # Create a custodial wallet Source: https://docs.privy.io/wallets/custodial-wallets/create-custodial-wallet Privy enables you to create custodial wallets that are managed through licensed custody partners. Custodial wallets provide institutional-grade custody controls while maintaining the same programmability and policy enforcement as standard embedded wallets. ## Usage To create a custodial wallet via REST API, make a `POST` request to: ```bash theme={"system"} https://api.privy.io/v1/custodial_wallets ``` ### Body The blockchain type for the custodial wallet. An `ethereum` type wallet creates a custodial wallet on Base, `solana` on Solana mainnet. The custodian of the wallet. The user ID of the beneficiary of the custodial wallet, provided by the licensing provider after KYC. The entity that can authorize transactions from the custodial wallet and configure additional signers. Additional signers for the custodial wallet. Each signer can have a list of policy IDs that override the base policy IDs set on the wallet. List of policy IDs to enforce on the wallet. ### Example ```bash theme={"system"} curl --request POST https://api.privy.io/v1/custodial_wallets \ -u ":" \ -H "privy-app-id: " \ -H 'Content-Type: application/json' \ -d '{ "chain_type": "ethereum", "provider": "bridge", "provider_user_id": "user_xxxxx", "owner": { "public_key": "-----BEGIN PUBLIC KEY-----\nMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEbL2Fl6Um/Ne6hXJl1i7oCU4tqmnR\nFWd4b9xVc6cGYFaqGgvEb/4+GCFsJu+bX+uvQvGvbXQCijIWpmIHwpRpkg==\n-----END PUBLIC KEY-----" } }' ``` ### Response The response will include the following fields: Unique ID of the created wallet. This will be the primary identifier when using the wallet in the future. Address of the created wallet. Chain type of the created wallet. The custodian of the wallet. The key quorum ID of the owner of the wallet. If an `owner` was passed in, this field will be populated. The key quorum IDs of the additional signers for the wallet. List of policy IDs for policies that are enforced on the wallet. The creation date of the wallet, in milliseconds since midnight, January 1, 1970 UTC. ```json theme={"system"} { "id": "p12aj1whizzmklph0b36xk6n", "address": "0x9f284C7Eaf97b0f9B5542d83Af7F785D12E803a", "chain_type": "ethereum", "provider": "bridge", "owner_id": "owner_xxxxx", "additional_signers": [], "policy_ids": [], "created_at": 1733923425155 } ``` ## Next steps Now that you have created a custodial wallet, you can [send funds from a custodial wallet](/wallets/custodial-wallets/sending-funds). Execute transactions and send funds from custodial wallets Learn about the transaction lifecycle for custodial wallets Configure policies and additional security measures for custodial wallets # Overview Source: https://docs.privy.io/wallets/custodial-wallets/overview Overview of custodial wallets in Privy for applications that manage wallet custody on behalf of users. Privy enables developers to provision custodial wallets operated with a licensed custodian of their choice. Custodial wallets are built on Privy’s [secure key management](/wallets/overview) and [authorization framework](/controls/authorization-keys/overview). Only appropriate parties can take actions with a given wallet, which are governed by cryptographically enforced authorization keys and configurable [policies](/controls/policies/overview), with support for [multi-party key quorums](/wallets/using-wallets/signers/overview) and fine-grained controls. Developers can define who must initiate wallet actions — whether that’s a single user key, a service key, or a combination of signers in an m-of-n configuration. Custodial wallets offer the same core powerful features as any Privy wallet but are backed by a licensed custodian. Privy's architecture can work with any custodian; today, we work with [Bridge (a Stripe company)](https://bridge.xyz). You can see a full list of supported regions [here](https://apidocs.bridge.xyz/platform/customers/compliance/supported-countries-list). If you’d like to enable custodial wallets with another custodian, please reach out at [support@privy.io](mailto:support@privy.io). We’d love to hear from you. ## Features Privy custodial wallets support the following features: * Simple wallet creation for custodied accounts * Transaction screening and deposit addresses * Stablecoin transfers * Fiat / stablecoin orchestration through providers Beyond this, custodial wallets support the same range of features as other embedded wallets such as: * **Transaction fee sponsorship**: Automatically sponsor gas fees for user transactions. * **Policies and authorization controls**: Enforce granular policies on wallet actions and require multi-party approvals. * **Balance webhooks**: Receive real-time notifications about wallet balance changes. * **Transaction webhooks**: Monitor transaction status and lifecycle events, including custodial provider-specific events. ### Differences between custodial wallets and other embedded wallets There are some differences in features and behaviors between custodial wallets and other embedded wallets, including: * [Transaction lifecycle](/wallets/custodial-wallets/transaction-lifecycle) * [Meaning of a wallet owner](/wallets/custodial-wallets/advanced/authorization-controls#meaning-of-owner-for-custodial-wallets) * Custodial wallets can only send [certain assets on certain chains](/wallets/custodial-wallets/sending-funds) ## Get started Get started with flexible custody Learn how to create and provision custodial wallets Execute transactions and send funds from custodial wallets