# 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. > [!TIP] > > Authorization signatures are an important security measure and we strongly recommend registering > authorization keys for all production resources. > [!NOTE] > > 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: **1. Get your private keys** 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). **2. Construct your request** 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. **3. Sign the request** Format and sign the request with your private key(s). **4. Include the signature as a request header** 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. > **[Using user owners](/controls/authorization-keys/keys/create/user/overview)** > > Request keys for user owners. > **[Get user keys](/controls/authorization-keys/keys/create/user/overview)** > > 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: > **[Sign requests](/controls/authorization-keys/using-owners/sign)** > > 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. **privy-authorization-signature** `string` The authorization signature. If multiple signatures are required, include them as a comma-delimited string. **privy-request-expiry** `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. > [!TIP] > > 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. # Get condition sets Source: https://docs.privy.io/api-reference/condition-sets/get-all get /v1/condition_sets List condition sets in an app. # 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 reusable crypto deposit addresses that convert incoming funds into a target asset. See [supported assets](/wallets/funding/crypto-deposits/overview#supported-assets) and [supported chains](/wallets/funding/crypto-deposits/overview#supported-chains) before creating a deposit address. Use the [deposit quote API](/api-reference/crypto-deposits/quote) to preview current route pricing. Crypto deposit addresses require [app-pays gas sponsorship](/wallets/funding/crypto-deposits/setup) on each source chain. [Swap provider fee sponsorship](/wallets/actions/transfer/fixed-rates) is optional and covers provider fees separately. ## Deposit address strategy **deposit\_address\_strategy** `'dedicated' | 'prefer_destination' | 'require_destination'` Optional top-level field for both `inline_route` and `deposit_config` requests. Defaults to `dedicated`, including when a route already exists. * `dedicated`: Always use a separate source wallet. Privy reuses or creates one for the depositing user. * `prefer_destination`: Use the destination wallet's own address for its chain family. If the destination wallet was exported, use a separate source wallet. * `require_destination`: Use the destination wallet's own address for its chain family. If the destination wallet was exported, the request fails. Other chain families always use separate source wallets. See [where deposit addresses live](/wallets/funding/crypto-deposits/overview#where-deposit-addresses-live). For example, attach an existing deposit configuration and use the destination wallet's own address: ```json theme={"system"} { "type": "deposit_config", "deposit_config_id": "", "deposit_address_strategy": "prefer_destination" } ``` Sign the request body as sent, including `deposit_address_strategy` when provided. Do not add a default value after signing. For source wallet reuse, exported wallets, attached automations, and address changes, see [Deposit address behavior](/wallets/funding/crypto-deposits/create-deposit-account#deposit-address-behavior). # Get next crypto deposit account order Source: https://docs.privy.io/api-reference/crypto-deposits/get-next-order get /v1/wallets/{wallet_id}/deposit_accounts/crypto/next_order Fetch the earliest crypto deposit order into a destination wallet after a timestamp. Pass the destination wallet ID as `wallet_id`. Use this endpoint to poll for an order when your app has no order ID. `after` must be an ISO 8601 timestamp in UTC, ending in `Z`. # Get crypto deposit account order Source: https://docs.privy.io/api-reference/crypto-deposits/get-order get /v1/wallets/{wallet_id}/deposit_accounts/crypto/orders/{order_id} Fetch the status of a crypto deposit order. The order ID is the wallet action ID of the deposit conversion. Pass the destination wallet ID as `wallet_id`. Pass the wallet action ID of the deposit's conversion as `order_id`, such as the `action_id` from the [`wallet_automation.submitted`](/wallets/automations/webhooks) webhook. See [get the order ID](/wallets/funding/crypto-deposits/orders#get-the-order-id). # Quote crypto deposit account Source: https://docs.privy.io/api-reference/crypto-deposits/quote post /v1/deposit_accounts/crypto/quote Return an indicative quote for a crypto deposit route without creating a wallet. Omit `input_amount` to quote approximately \$50 of the source asset. Source and destination must both be mainnet or both be testnet. Quotes do not require a wallet ID or an authorization signature. # 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. # Submit KYB data Source: https://docs.privy.io/api-reference/fiat/kyb/submit post /v1/organizations/{organization_id}/kyb/submit Submits KYB verification data for the organization. Safe to call more than once: the first call creates the provider customer and later calls update it, so a partial submission can be completed incrementally. # 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. # Submit KYC data Source: https://docs.privy.io/api-reference/fiat/kyc-server/submit post /v1/users/{user_id}/kyc/submit Submits KYC data, creating a provider customer on the first call and updating it on later calls. Stripe Link can supply identity details, but an email is still required to create a customer. # 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: **privy-idempotency-key** `string` *required* 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 **1. First request** 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. **2. Subsequent requests** 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 **3. Key expiration** After 24 hours, idempotency keys expire. Privy processes requests with an expired key as new requests. > [!WARNING] > > 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 | > [!WARNING] > > 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. > [!NOTE] > > **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' } } ); ``` ```python Python theme={"system"} from uuid import uuid4 response = client.wallets.ethereum.send_transaction( "insert-wallet-id", idempotency_key=str(uuid4()), caip2="eip155:8453", params={ "transaction": { "to": "insert-recipient-address", "value": "0x2386F26FC10000", "chain_id": 8453, } }, ) ``` > [!TIP] > > 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: **Authorization** `string` *required* Basic Auth header with your app ID as the username and your app secret as the password. [Read-only app secrets](/basics/get-started/dashboard/app-secrets) work only on endpoints that read data. **privy-app-id** `string` *required* Your Privy app ID as a string. Requests missing either of these headers will be rejected by Privy's middleware. > [!NOTE] > > 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** ```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)); ``` **cURL** ```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. > [!TIP] > > 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. # Get key quorums Source: https://docs.privy.io/api-reference/key-quorums/get-all get /v1/key_quorums List key quorums in an app. # 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. > [!WARNING] > > 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. > [!NOTE] > > 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). # Get policies Source: https://docs.privy.io/api-reference/policies/get-all get /v1/policies List policies in an app, excluding rules. # 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: **privy-request-expiry** `string` 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 **1. Set the expiry** When making a request, include the `privy-request-expiry` header with a Unix timestamp in milliseconds representing the deadline for the request. **2. Include in signature payload** 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. **3. Server-side validation** 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. > [!WARNING] > > 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 > [!NOTE] > > 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: **Node SDK** ```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. > [!TIP] > > 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). > [!NOTE] > > 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). # Freeze user Source: https://docs.privy.io/api-reference/users/freeze post /v1/users/{user_id}/freeze Freezes a user by user ID, blocking new logins and revoking active sessions. # 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). > [!WARNING] > > Lookups by user ID are subject to rate limits. When accessing an authenticated user, use [identity > tokens](/user-management/users/identity-tokens). # 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. # Start passkey MFA enrollment Source: https://docs.privy.io/api-reference/users/mfa/passkey/enrollment/init post /v1/users/{user_id}/mfa/passkey/enrollment/init Returns WebAuthn registration options for a new MFA passkey. # Complete passkey MFA enrollment Source: https://docs.privy.io/api-reference/users/mfa/passkey/enrollment/verify post /v1/users/{user_id}/mfa/passkey/enrollment/verify Enrolls a passkey in MFA using the WebAuthn registration response. # Start passkey MFA verification Source: https://docs.privy.io/api-reference/users/mfa/passkey/verification/init post /v1/users/{user_id}/mfa/passkey/verification/init Returns WebAuthn authentication options for the enrolled MFA passkeys. # Complete passkey MFA verification Source: https://docs.privy.io/api-reference/users/mfa/passkey/verification/verify post /v1/users/{user_id}/mfa/passkey/verification/verify Verifies the WebAuthn authentication response from an MFA passkey. # Start SMS MFA enrollment Source: https://docs.privy.io/api-reference/users/mfa/sms/enrollment/init post /v1/users/{user_id}/mfa/sms/enrollment/init Sends an SMS MFA enrollment code to a phone number. # Complete SMS MFA enrollment Source: https://docs.privy.io/api-reference/users/mfa/sms/enrollment/verify post /v1/users/{user_id}/mfa/sms/enrollment/verify Enrolls the phone number from init using the code sent to it. # Start SMS MFA verification Source: https://docs.privy.io/api-reference/users/mfa/sms/verification/init post /v1/users/{user_id}/mfa/sms/verification/init Sends an SMS MFA verification code to the enrolled phone number. # Complete SMS MFA verification Source: https://docs.privy.io/api-reference/users/mfa/sms/verification/verify post /v1/users/{user_id}/mfa/sms/verification/verify Verifies the code sent to the enrolled phone number. # Start TOTP MFA enrollment Source: https://docs.privy.io/api-reference/users/mfa/totp/enrollment/init post /v1/users/{user_id}/mfa/totp/enrollment/init Creates a TOTP secret for the user to add to an authenticator app. # Complete TOTP MFA enrollment Source: https://docs.privy.io/api-reference/users/mfa/totp/enrollment/verify post /v1/users/{user_id}/mfa/totp/enrollment/verify Enrolls the authenticator from init using a code it generated. # Complete TOTP MFA verification Source: https://docs.privy.io/api-reference/users/mfa/totp/verification/verify post /v1/users/{user_id}/mfa/totp/verification/verify Verifies a code from the enrolled authenticator. # 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 **subject** `string` *required* 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. **type** `string` *required* Available options: `twitter_oauth` **subject** `string` *required* The Twitter user ID of the account. This value is stable and does not change when the user changes their username. **username** `string | null` *required* The user's Twitter username. **name** `string | null` *required* The user's display name on Twitter. **profile\_picture\_url** `string | null` *required* A URL for the user's Twitter profile picture. **verified\_at** `number` *required* Unix timestamp, in seconds, of when the user linked their Twitter account to their Privy account. **first\_verified\_at** `number | null` *required* Unix timestamp, in seconds, of when the user first linked their Twitter account. **latest\_verified\_at** `number | null` *required* 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). # Unfreeze user Source: https://docs.privy.io/api-reference/users/unfreeze delete /v1/users/{user_id}/freeze Unfreezes a user by user ID, restoring their ability to log in. # Attach wallet automations Source: https://docs.privy.io/api-reference/wallet-automations/attach post /v1/wallets/{wallet_id}/automations/attach Attach one or more automations to a wallet. > [!WARNING] > > The wallet must be generated by Privy and never exported. Users cannot attach automations to > imported or previously exported wallets. Attaching an automation that is already attached updates its parameters and preserves its matching priority. # Create wallet automation Source: https://docs.privy.io/api-reference/wallet-automations/create post /v1/wallet_automations Create a reusable wallet automation definition. # Delete wallet automation Source: https://docs.privy.io/api-reference/wallet-automations/delete delete /v1/wallet_automations/{automation_id} Delete a wallet automation and its wallet attachments. # Detach wallet automations Source: https://docs.privy.io/api-reference/wallet-automations/detach post /v1/wallets/{wallet_id}/automations/detach Detach one or more automations from a wallet. # Get wallet automation Source: https://docs.privy.io/api-reference/wallet-automations/get get /v1/wallet_automations/{automation_id} Get a wallet automation by ID. # List wallet automations Source: https://docs.privy.io/api-reference/wallet-automations/list get /v1/wallet_automations List wallet automations, optionally filtered by an attached wallet. # List wallet automation executions Source: https://docs.privy.io/api-reference/wallet-automations/list-executions get /v1/wallet_automations/executions List wallet automation executions, optionally filtered by wallet. # Reindex wallet automations Source: https://docs.privy.io/api-reference/wallet-automations/reindex post /v1/wallet_automations/reindex Recheck a wallet balance and recover a missed or failed automation execution. Reindexing checks the asset's current wallet balance. It does not replay the original deposit amount. See [recover a missed or failed deposit](/wallets/automations/lifecycle#recover-a-missed-or-failed-deposit) for recovery behavior and result statuses. # Update wallet automation Source: https://docs.privy.io/api-reference/wallet-automations/update patch /v1/wallet_automations/{automation_id} Update, enable, or disable a wallet automation. # 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. > [!TIP] > > 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. > [!NOTE] > > 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:
  • KEM (Key Encapsulation Mechanism): DHKEM\_P256\_HKDF\_SHA256
  • KDF (Key Derivation Function): HKDF\_SHA256
  • AEAD (Authenticated Encryption with Associated Data): CHACHA20\_POLY1305
  • Mode: BASE
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. > [!TIP] > > 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). > [!WARNING] > > **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 custodial wallet on Tempo, Base, or Solana. # 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. > [!TIP] > > 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. > [!NOTE] > > **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. > [!TIP] > > 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. > [!TIP] > > 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. > [!NOTE] > > **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. > [!TIP] > > 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. > [!NOTE] > > **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. > [!WARNING] > > 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). *** > [!WARNING] > > **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" } } ``` > [!WARNING] > > 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 **privy-app-id** `string` *required* ID of your Privy app. **privy-authorization-signature** `string` Request authorization signature. If multiple signatures are required, they should be comma separated. **privy-request-expiry** `string` Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters **wallet\_id** `string` *required* ID of the wallet to get. ### Body **method** `string` *required* Available options: `eth_sendTransaction` **caip2** `string` *required* **params** `object` *required* **child attributes** **transaction** `object` *required* **child attributes** **from** `string` **chain\_id** `string | number` **nonce** `string | number` **type** `number` Available options: `0`, `1`, `2`, `4`. **gas\_limit** `string | number` **max\_fee\_per\_gas** `string | number` **max\_priority\_fee\_per\_gas** `string | number` **to** `string` **data** `string` **value** `string | number` **gas\_price** `string | number` **authorization\_list** `object[]` Applies to EIP-7702 type-4 transactions. **child attributes** **contract** `string` The delegated contract address. **chain\_id** `string | number` **nonce** `string | number` **r** `string` **s** `string` **y\_parity** `0 | 1` **sponsor** `boolean` Optional parameter to enable gas sponsorship for this transaction. [Learn more.](/wallets/gas-and-asset-management/gas/overview) **sponsor\_options** `object` 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. **child attributes** **asset** `string` *required* 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). **reference\_id** `string` 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). **address** `string` **chain\_type** `string` Available options: `ethereum` ### Returns **method** `enum\` *required* Available options: `eth_sendTransaction` **data** `object` *required* **child attributes** **hash** `string` *required* The transaction hash. For paymaster-sponsored transactions, returns an empty string until the transaction confirms on-chain. **user\_operation\_hash** `string` The user operation hash. Only present for standard EVM paymaster-sponsored transactions. **transaction\_request** `object` 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. **caip2** `string` *required* **transaction\_id** `string` **reference\_id** `string | null` 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). *** > [!WARNING] > > **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 **privy-app-id** `string` *required* ID of your Privy app. **privy-authorization-signature** `string` Request authorization signature. If multiple signatures are required, they should be comma separated. **privy-request-expiry** `string` Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters **wallet\_id** `string` *required* ID of the wallet to get. ### Body **method** `string` *required* The RPC method to execute. Must be `eth_sign7702Authorization`. **params** `object` *required* The parameters for signing the EIP-7702 authorization. **child properties** **contract** `string` *required* The address of the smart contract that the EOA will delegate to. Must be a valid Ethereum address in hex format. **chain\_id** `number` *required* The chain ID where this authorization will be valid. **nonce** `number` The nonce for the authorization. If not provided, we will fetch the current nonce for the wallet. ### Response **method** `string` *required* The RPC method that was executed. Will be `eth_sign7702Authorization`. **data** `object` *required* The response data containing the signed authorization. **child properties** **authorization** `object` *required* The signed EIP-7702 authorization object. **authorization properties** **contract** `string` *required* The address of the smart contract that the EOA delegates to. **chain\_id** `number` *required* The chain ID where this authorization is valid. **nonce** `number` *required* The nonce for the authorization. **r** `string` *required* The r component of the ECDSA signature. **s** `string` *required* The s component of the ECDSA signature. **y\_parity** `number` *required* 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). *** > [!WARNING] > > **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 **privy-app-id** `string` *required* ID of your Privy app. **privy-authorization-signature** `string` Request authorization signature. If multiple signatures are required, they should be comma separated. **privy-request-expiry** `string` Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters **wallet\_id** `string` *required* ID of the wallet to get. ### Body **method** `string` *required* Available options: `eth_signTransaction` **params** `object` *required* **child attributes** **transaction** `object` *required* **child attributes** **from** `string` **chain\_id** `string | number` **nonce** `string | number` **type** `number` Available options: `0`, `1`, `2`, `4`. **gas\_limit** `string | number` **max\_fee\_per\_gas** `string | number` **max\_priority\_fee\_per\_gas** `string | number` **to** `string` **data** `string` **value** `string | number` **gas\_price** `string | number` **authorization\_list** `object[]` Applies to EIP-7702 type-4 transactions. **child attributes** **contract** `string` The delegated contract address. **chain\_id** `string | number` **nonce** `string | number` **r** `string` **s** `string` **y\_parity** `0 | 1` ### Response **method** `enum\` *required* Available options: `eth_signTransaction` **data** `object` *required* **child properties** **signed\_transaction** `string` *required* **encoding** `enum\` 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. > [!NOTE] > > This method is currently only supported for these smart contract addresses: > `0x69007702764179f14F51cdce752f4f775d74E139`, `0x00000000000002377B26b1EdA7b0BC371C60DD4f`, > `0xd6CEDDe84be40893d153Be9d467CD6aD37875b28`, `0x63c0c19a282a1B52b07dD5a65b58948A07DAE32B`, and > `0x77021100bD87b7008E5E1989d0eB38555d0d0000`. *** > [!WARNING] > > **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 **privy-app-id** `string` *required* ID of your Privy app. **privy-authorization-signature** `string` Request authorization signature. If multiple signatures are required, they should be comma separated. **privy-request-expiry** `string` Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters **wallet\_id** `string` *required* ID of the wallet to get. ### Body **method** `string` *required* Available options: `eth_signUserOperation` **params** `object` *required* **child attributes** **contract** `string` *required* The smart contract address for the user operation. Currently supports `0x69007702764179f14F51cdce752f4f775d74E139`, `0x00000000000002377B26b1EdA7b0BC371C60DD4f`, `0xd6CEDDe84be40893d153Be9d467CD6aD37875b28`, `0x63c0c19a282a1B52b07dD5a65b58948A07DAE32B`, and `0x77021100bD87b7008E5E1989d0eB38555d0d0000`. **user\_operation** `object` *required* **child attributes** **sender** `string` *required* The account making the operation. **nonce** `string` *required* Anti-replay parameter; also used as the salt for first-time account creation. **factory** `string` The account factory address, if deploying a new account. **factory\_data** `string` Additional data for the account factory. **call\_data** `string` *required* The data to pass to the sender during the main execution call. **call\_gas\_limit** `string` *required* The amount of gas to allocate the main execution call. **verification\_gas\_limit** `string` *required* The amount of gas to allocate for the verification step. **pre\_verification\_gas** `string` *required* Extra gas to pay the bundler. **max\_fee\_per\_gas** `string` *required* Maximum fee per gas (similar to EIP-1559 max\_fee\_per\_gas). **max\_priority\_fee\_per\_gas** `string` *required* Maximum priority fee per gas (similar to EIP-1559 max\_priority\_fee\_per\_gas). **paymaster** `string` Address of paymaster sponsoring the transaction, zero for self-sponsored. **paymaster\_data** `string` Extra data to send to the paymaster. **paymaster\_verification\_gas\_limit** `string` The amount of gas to allocate for the paymaster validation code. **paymaster\_post\_op\_gas\_limit** `string` The amount of gas to allocate for the paymaster post-operation code. **chain\_id** `string` *required* The chain ID for the user operation. ### Response **method** `enum\` *required* Available options: `eth_signUserOperation` **data** `object` *required* **child properties** **signature** `string` *required* The hex-encoded signature of the user operation. **encoding** `enum\` 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). *** > [!WARNING] > > **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 **privy-app-id** `string` *required* ID of your Privy app. **privy-authorization-signature** `string` Request authorization signature. If multiple signatures are required, they should be comma separated. **privy-request-expiry** `string` Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters **wallet\_id** `string` *required* ID of the wallet to get. ### Body **method** `string` *required* Available options: `eth_signTypedData_v4` **params** `object` *required* **child attributes** **typed\_data** `object` *required* **child attributes** **domain** `object` *required* **types** `object` *required* **message** `object` *required* **primary\_type** `string` *required* **caip2** `string` A CAIP-2 chain ID specifying which chain to sign on (e.g. `eip155:1`). Required when using `signature_options`. **signature\_options** `object` 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). **child properties** **type** `enum\` *required* 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 **method** `enum\` *required* Available options: `eth_signTypedData_v4` **data** `object` *required* **child attributes** **signature** `string` *required* **encoding** `enum\` 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). *** > [!WARNING] > > **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 **privy-app-id** `string` *required* ID of your Privy app. **privy-authorization-signature** `string` Request authorization signature. If multiple signatures are required, they should be comma separated. **privy-request-expiry** `string` Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters **wallet\_id** `string` *required* ID of the wallet to get. ### Body **method** `string` *required* Available options: `personal_sign` **params** `object` *required* **child properties** **message** `string` **encoding** `string` Available options: `utf-8`, `hex` **caip2** `string` A CAIP-2 chain ID specifying which chain to sign on (e.g. `eip155:1`). Required when using `signature_options`. **signature\_options** `object` 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). **child properties** **type** `enum\` *required* 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 **method** `enum\` *required* Available options: `personal_sign` **data** `object` *required* **child properties** **signature** `string` *required* **encoding** `enum\` 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). *** > [!WARNING] > > **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 **privy-app-id** `string` *required* ID of your Privy app. **privy-authorization-signature** `string` Request authorization signature. If multiple signatures are required, they should be comma separated. **privy-request-expiry** `string` Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters **wallet\_id** `string` *required* ID of the wallet to get. ### Body **method** `string` *required* Available options: `secp256k1_sign` **params** `object` *required* **child properties** **hash** `string` ### Response **method** `enum\` *required* Available options: `secp256k1_sign` **data** `object` *required* **child properties** **signature** `string` *required* **encoding** `enum\` 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). *** > [!WARNING] > > **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" } } ``` > [!WARNING] > > 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 **privy-app-id** `string` *required* ID of your Privy app. **privy-authorization-signature** `string` Request authorization signature. If multiple signatures are required, they should be comma separated. **privy-request-expiry** `string` Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters **wallet\_id** `string` *required* ID of the wallet to get. ### Body **method** `string` *required* Available options: `wallet_sendCalls` **caip2** `string` *required* **params** `object` *required* **child attributes** **calls** `object[]` *required* An array of call objects to execute in a batch. **child attributes** **to** `string` *required* The recipient address for the call. **value** `string` The value to send in the call in wei as a hexadecimal string. **data** `string` The encoded calldata for the call. **sponsor** `boolean` Optional parameter to enable gas sponsorship for this transaction. [Learn more.](/wallets/gas-and-asset-management/gas/overview) **sponsor\_options** `object` 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. **child attributes** **asset** `string` *required* 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). **chain\_type** `string` Available options: `ethereum` ### Returns **method** `enum\` *required* Available options: `wallet_sendCalls` **data** `object` *required* **child attributes** **transaction\_id** `string` *required* A unique identifier for the batch transaction. **caip2** `string` *required* # 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). > [!WARNING] > > **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). > [!WARNING] > > **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). > [!NOTE] > > 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). *** > [!WARNING] > > 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). > [!WARNING] > > **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 **privy-app-id** `string` *required* ID of your Privy app. **privy-authorization-signature** `string` Request authorization signature. If multiple signatures are required, they should be comma separated. **privy-request-expiry** `string` Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters **wallet\_id** `string` *required* ID of the wallet to get. ### Body **method** `string` *required* Available options: `signAndSendTransaction` **caip2** `string` *required* Available options: `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` (Solana Mainnet), `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` (Solana Devnet), `solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z` (Solana Testnet) **params** `object` *required* **child attributes** **transaction** `string` Base64 encoded serialized transaction to sign. **encoding** `string` Available options: `base64` **sponsor** `boolean` Optional parameter to enable gas sponsorship for this transaction. [Learn more.](/wallets/gas-and-asset-management/gas/overview) **reference\_id** `string` 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). **optimistic\_broadcast** `boolean` 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. > [!TIP] > > **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 **method** `enum\` *required* Available options: `signAndSendTransaction` **data** `object` *required* **child attributes** **hash** `string` *required* Transaction hash of the signed and sent transaction. **caip2** `string` *required* CAIP-2 chain ID of the network where the transaction was sent. **transaction\_id** `string` Optional Privy-assigned transaction ID. **reference\_id** `string | null` The developer-provided reference ID, if one was provided in the request. **signed\_transaction** `string` 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). > [!WARNING] > > **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 **privy-app-id** `string` *required* ID of your Privy app. **privy-authorization-signature** `string` Request authorization signature. If multiple signatures are required, they should be comma separated. **privy-request-expiry** `string` Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters **wallet\_id** `string` *required* ID of the wallet to get. ### Body **method** `string` *required* Available options: `signMessage` **params** `object` *required* **child properties** **message** `string` Base64 encoded message to sign. **encoding** `string` Available options: `base64` ### Response **method** `enum\` *required* Available options: `signMessage` **data** `object` *required* **child properties** **signature** `string` *required* **encoding** `enum\` 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). > [!WARNING] > > **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 **privy-app-id** `string` *required* ID of your Privy app. **privy-authorization-signature** `string` Request authorization signature. If multiple signatures are required, they should be comma separated. **privy-request-expiry** `string` Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters **wallet\_id** `string` *required* ID of the wallet to get. ### Body **method** `string` *required* Available options: `signTransaction` **params** `object` *required* **child attributes** **transaction** `string` Base64 encoded serialized transaction to sign. **encoding** `string` Available options: `base64` ### Response **method** `enum\` *required* Available options: `signTransaction` **data** `object` *required* **child attributes** **signed\_transaction** `string` *required* **encoding** `enum\` 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. > [!WARNING] > > **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 **privy-app-id** `string` *required* ID of your Privy app. **privy-authorization-signature** `string` Request authorization signature. If multiple signatures are required, they should be comma separated. **privy-request-expiry** `string` Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters **wallet\_id** `string` *required* ID of the wallet to get. > [!NOTE] > > 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 **method** `string` *required* Available options: `claimStaticDeposit` **network** `string` *required* Available options: `MAINNET`, `REGTEST` **params** `object` *required* Required parameters to claim the deposit. **child attributes** **credit\_amount\_sats** `number` *required* Amount of native tokens (in satoshis) being claimed from the UTXO. **signature** `string` *required* A signature authorizing the claim, signed by the address that received the deposit. **transaction\_id** `string` *required* The transaction ID (hash) of the deposit transaction containing the UTXO. **output\_index** `number` *required* The index of the UTXO output in the transaction. ### Returns **method** `enum\` *required* Always `"claimStaticDeposit"` **data** `object` *required* Information about the claimed deposit. **child attributes** **transfer\_id** `string` *required* 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. > [!WARNING] > > **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 **privy-app-id** `string` *required* ID of your Privy app. **privy-authorization-signature** `string` Request authorization signature. If multiple signatures are required, they should be comma separated. **privy-request-expiry** `string` Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters **wallet\_id** `string` *required* ID of the wallet to get. > [!NOTE] > > 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 **method** `string` *required* Available options: `createLightningInvoice` **network** `string` *required* Available options: `MAINNET`, `REGTEST` **params** `object` *required* Parameters for the invoice to be created. **child attributes** **amount\_sats** `number` *required* The amount of sats to be received. ### Returns **method** `enum\` *required* Always `"createLightningInvoice"` **data** `object` *required* Details about the created Lightning invoice. **child attributes** **id** `string` *required* Unique ID of the Lightning receive request. **created\_at** `string` *required* Timestamp of invoice creation. **updated\_at** `string` *required* Timestamp of last update. **network** `string` *required* The Bitcoin network this invoice is valid for. **invoice** `object` *required* Encoded invoice details. **child attributes** **encodedInvoice** `string` *required* Bolt11-encoded Lightning invoice string. **bitcoinNetwork** `string` *required* The Bitcoin network. **paymentHash** `string` *required* The hash of the preimage for this payment request. **amount** `object` *required* Amount details. **child attributes** **originalValue** `number` *required* **originalUnit** `string` *required* **preferredCurrencyUnit** `string` *required* **preferredCurrencyValueRounded** `number` *required* **preferredCurrencyValueApprox** `number` *required* **createdAt** `string` *required* Timestamp of invoice creation. **expiresAt** `string` *required* Timestamp of invoice expiration. **memo** `string` Optional memo included with the invoice. **status** `string` *required* Current status of the invoice. Possible value: `"INVOICE_CREATED"`. **typename** `string` *required* **payment\_preimage** `string` **receiver\_identity\_public\_key** `string` 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 **privy-app-id** `string` *required* ID of your Privy app. **privy-authorization-signature** `string` Request authorization signature. If multiple signatures are required, they should be comma separated. **privy-request-expiry** `string` Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters **wallet\_id** `string` *required* ID of the wallet to get. > [!NOTE] > > 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 **method** `string` *required* Available options: `getBalance` **network** `string` *required* Available options: `MAINNET`, `REGTEST` ### Returns **method** `enum\` *required* Available options: `getBalance` **data** `object` *required* The balance and token holdings of the wallet. **child attributes** **balance** `string` *required* Native Spark balance in satoshis. **token\_balances** `object` *required* A mapping of token Spark addresses to token data. **child attributes** **\[token\_spark\_address]** `object` *required* **child attributes** **balance** `string` *required* **token\_metadata** `object` *required* **child attributes** **raw\_token\_identifier** `string` *required* **token\_public\_key** `string` *required* **token\_name** `string` *required* **token\_ticker** `string` *required* **decimals** `number` *required* **max\_supply** `string` *required* **encoding** `string` *required* ``` ``` # 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 **privy-app-id** `string` *required* ID of your Privy app. **privy-authorization-signature** `string` Request authorization signature. If multiple signatures are required, they should be comma separated. **privy-request-expiry** `string` Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters **wallet\_id** `string` *required* ID of the wallet to get. > [!NOTE] > > 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 **method** `string` *required* Available options: `getStaticDepositAddress` **network** `string` *required* Available options: `MAINNET`, `REGTEST` ### Returns **method** `enum\` *required* Always `"getStaticDepositAddress"` **data** `object` *required* The static deposit address for the wallet. **child attributes** **address** `string` *required* 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 **privy-app-id** `string` *required* ID of your Privy app. **privy-authorization-signature** `string` Request authorization signature. If multiple signatures are required, they should be comma separated. **privy-request-expiry** `string` Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters **wallet\_id** `string` *required* ID of the wallet to get. > [!NOTE] > > 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 **method** `string` *required* Must be set to getClaimStaticDepositQuote. **network** `string` *required* Blockchain network to use. Options: `MAINNET`, `REGTEST`. **params** `object` *required* **child attributes** **transaction\_id** `string` *required* The transaction hash of the BTC sent to the static deposit address. ### Returns **method** `enum\` *required* **data** `object` *required* Contains the BTC claim quote. **child attributes** **credit\_amount\_sats** `number` *required* The amount of sats credited to the wallet after claiming. **signature** `string` *required* **transaction\_id** `string` *required* The hash of the Bitcoin transaction containing the deposit. **output\_index** `number` *required* The index of the output within the transaction that is being claimed. **network** `string` *required* 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 **privy-app-id** `string` *required* ID of your Privy app. **privy-authorization-signature** `string` Request authorization signature. If multiple signatures are required, they should be comma separated. **privy-request-expiry** `string` Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters **wallet\_id** `string` *required* ID of the wallet to get. > [!NOTE] > > 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 **method** `string` *required* Must be set to `getWithdrawalFeeQuote`. **network** `string` *required* Blockchain network to use. Options: `MAINNET`, `REGTEST`. **params** `object` *required* **child attributes** **amount\_sats** `number` *required* The amount of satoshis to withdraw. **onchain\_address** `string` *required* The Bitcoin L1 address to withdraw to. ### Returns **method** `enum\` *required* Available options: `getWithdrawalFeeQuote` **data** `object` *required* A `CoopExitFeeQuote` object with the following fields: **child attributes** **id** `string` *required* The unique identifier of this fee quote. **created\_at** `string` *required* The date and time when the quote was first created. **updated\_at** `string` *required* The date and time when the quote was last updated. **network** `string` *required* The network the coop exit fee quote is on. **total\_amount** `object` *required* The total amount of all nodes swapped for the coop exit quote. **child attributes** **original\_value** `number` *required* **original\_unit** `string` *required* **user\_fee\_fast** `object` *required* The user fee (excluding L1 broadcast fee) for fast exit speed. **child attributes** **original\_value** `number` *required* **original\_unit** `string` *required* **user\_fee\_medium** `object` *required* The user fee (excluding L1 broadcast fee) for medium exit speed. **child attributes** **original\_value** `number` *required* **original\_unit** `string` *required* **user\_fee\_slow** `object` *required* The user fee (excluding L1 broadcast fee) for slow exit speed. **child attributes** **original\_value** `number` *required* **original\_unit** `string` *required* **l1\_broadcast\_fee\_fast** `object` *required* The L1 broadcast fee for fast exit speed. **child attributes** **original\_value** `number` *required* **original\_unit** `string` *required* **l1\_broadcast\_fee\_medium** `object` *required* The L1 broadcast fee for medium exit speed. **child attributes** **original\_value** `number` *required* **original\_unit** `string` *required* **l1\_broadcast\_fee\_slow** `object` *required* The L1 broadcast fee for slow exit speed. **child attributes** **original\_value** `number` *required* **original\_unit** `string` *required* **expires\_at** `string` *required* 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. > [!WARNING] > > **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 **privy-app-id** `string` *required* ID of your Privy app. **privy-authorization-signature** `string` Request authorization signature. If multiple signatures are required, they should be comma separated. **privy-request-expiry** `string` Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters **wallet\_id** `string` *required* ID of the wallet to get. > [!NOTE] > > 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 **method** `string` *required* Available options: `payLightningInvoice` **network** `string` *required* Available options: `MAINNET`, `REGTEST` **params** `object` *required* **child attributes** **invoice** `string` *required* The BOLT11 Lightning invoice to pay. **max\_fee\_sats** `number` *required* Maximum fee (in sats) the payer is willing to pay. **prefer\_spark** `boolean` Whether to prefer paying on Spark. If true, will return a Transfer object. Defaults to false. **amount\_sats\_to\_send** `number` Amount to pay in sats. Required only for zero-amount invoices. ### Returns **method** `enum\` *required* Available options: `payLightningInvoice` **data** `object` *required* If `prefer_spark` is false, a `LightningSendRequest` object with the following fields: **child attributes** **id** `string` *required* **created\_at** `string` *required* **updated\_at** `string` *required* **network** `string` *required* **encoded\_invoice** `string` *required* **fee** `object` *required* **child attributes** **original\_value** `number` **original\_unit** `string` **status** `string` *required* **typename** `string` *required* If `prefer_spark` is true, the response will be a `Transfer` object with the following fields: **child attributes** **id** `string` *required* **sender\_identity\_public\_key** `string` *required* **receiver\_identity\_public\_key** `string` *required* **status** `string` *required* **total\_value** `number` *required* **expiry\_time** `string` **leaves** `array\` *required* Each item contains transfer metadata and encrypted leaf information. **child attributes** **leaf** `object` *required* **child attributes** **id** `string` *required* **tree\_id** `string` *required* **value** `number` *required* **parent\_node\_id** `string` *required* **node\_tx** `string` *required* **refund\_tx** `string` *required* **vout** `number` *required* **verifying\_public\_key** `string` *required* **owner\_identity\_public\_key** `string` *required* **signing\_keyshare** `object` *required* **child attributes** **owner\_identifiers** `array\` *required* **threshold** `number` *required* **public\_key** `string` *required* **public\_shares** `object` *required* A mapping from signer identifier to public share **updated\_time** `string` *required* **status** `string` *required* **network** `string` *required* **secret\_cipher** `string` *required* **signature** `string` *required* **intermediate\_refund\_tx** `string` *required* **created\_time** `string` **updated\_time** `string` **type** `string` **transfer\_direction** `string` **encoding** `string` # 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. > [!WARNING] > > **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 **privy-app-id** `string` *required* ID of your Privy app. **privy-authorization-signature** `string` Request authorization signature. If multiple signatures are required, they should be comma separated. **privy-request-expiry** `string` Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters **wallet\_id** `string` *required* ID of the wallet to get. > [!NOTE] > > 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 **method** `string` *required* Available options: `signMessageWithIdentityKey` **network** `string` *required* Available options: `MAINNET`, `REGTEST` **params** `object` *required* **child attributes** **message** `string` *required* The message to sign with the identity key. **compact** `boolean` Whether to use compact signature format. Defaults to false. ### Returns **method** `enum\` *required* Available options: `signMessageWithIdentityKey` **data** `object` *required* The signature response object **child attributes** **signature** `string` *required* 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. > [!WARNING] > > **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 **privy-app-id** `string` *required* ID of your Privy app. **privy-authorization-signature** `string` Request authorization signature. If multiple signatures are required, they should be comma separated. **privy-request-expiry** `string` Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters **wallet\_id** `string` *required* ID of the wallet to get. > [!NOTE] > > 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 **method** `string` *required* Available options: `transfer` **network** `string` *required* Available options: `MAINNET`, `REGTEST` **params** `object` *required* **child attributes** **receiver\_spark\_address** `string` *required* The Spark address of the recipient. **amount\_sats** `number` *required* The amount to send in satoshis. ### Returns **method** `enum\` *required* Available options: `transfer` **data** `object` *required* The returned Transfer object **child attributes** **id** `string` *required* **sender\_identity\_public\_key** `string` *required* **receiver\_identity\_public\_key** `string` *required* **status** `string` *required* **total\_value** `number` *required* **expiry\_time** `string` **leaves** `array\` *required* Each item contains transfer metadata and encrypted leaf information. **child attributes** **leaf** `object` *required* **child attributes** **id** `string` *required* **tree\_id** `string` *required* **value** `number` *required* **parent\_node\_id** `string` *required* **node\_tx** `string` *required* **refund\_tx** `string` *required* **vout** `number` *required* **verifying\_public\_key** `string` *required* **owner\_identity\_public\_key** `string` *required* **signing\_keyshare** `object` *required* **child attributes** **owner\_identifiers** `array\` *required* **threshold** `number` *required* **public\_key** `string` *required* **public\_shares** `object` *required* A mapping from signer identifier to public share **updated\_time** `string` *required* **status** `string` *required* **network** `string` *required* **secret\_cipher** `string` *required* **signature** `string` *required* **intermediate\_refund\_tx** `string` *required* **created\_time** `string` **updated\_time** `string` **type** `string` **transfer\_direction** `string` **encoding** `string` # 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. > [!WARNING] > > **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 **privy-app-id** `string` *required* ID of your Privy app. **privy-authorization-signature** `string` Request authorization signature. If multiple signatures are required, they should be comma separated. **privy-request-expiry** `string` Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters **wallet\_id** `string` *required* ID of the wallet to get. > [!NOTE] > > 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 **method** `string` *required* Available options: `transferTokens` **network** `string` *required* Available options: `MAINNET`, `REGTEST` **params** `object` *required* Parameters for the token transfer. **child attributes** **token\_identifier** `string` *required* Spark token address (starts with `btkn`). **token\_amount** `number` *required* Number of tokens to send **receiver\_spark\_address** `string` *required* Spark address to send the tokens to. ### Returns **method** `enum\` *required* Always `"transferTokens"` **data** `object` *required* The result of the token transfer. **child attributes** **id** `string` *required* 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). > [!WARNING] > > **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 **privy-app-id** `string` *required* ID of your Privy app. **privy-authorization-signature** `string` Request authorization signature. If multiple signatures are required, they should be comma separated. **privy-request-expiry** `string` Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters **wallet\_id** `string` *required* ID of the wallet to get. > [!NOTE] > > 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 **method** `string` *required* Must be set to `withdraw`. **network** `string` *required* Blockchain network to use. Options: `MAINNET`, `REGTEST`. **params** `object` *required* **child attributes** **onchain\_address** `string` *required* The Bitcoin L1 address to withdraw to. **exit\_speed** `enum\` *required* The exit speed for the withdrawal. Options: `FAST`, `MEDIUM`, `SLOW`. **amount\_sats** `number` The amount of satoshis to withdraw. Withdraws the full balance if not specified. **fee\_amount\_sats** `number` The fee amount in satoshis, from a prior `getWithdrawalFeeQuote` call. **fee\_quote\_id** `string` The fee quote ID from a prior `getWithdrawalFeeQuote` call. **deduct\_fee\_from\_withdrawal\_amount** `boolean` Whether to deduct the fee from the withdrawal amount. Defaults to false. ### Returns **method** `enum\` *required* Available options: `withdraw` **data** `object` *required* A `CoopExitRequest` object with the following fields: **child attributes** **id** `string` *required* The unique identifier of this coop exit request. **created\_at** `string` *required* The date and time when the request was first created. **updated\_at** `string` *required* The date and time when the request was last updated. **network** `string` *required* The network the coop exit request is on. **fee** `object` *required* The fee the user pays for the coop exit, not including the L1 broadcast fee. **child attributes** **original\_value** `number` *required* **original\_unit** `string` *required* **l1\_broadcast\_fee** `object` *required* The L1 broadcast fee the user pays for the coop exit. **child attributes** **original\_value** `number` *required* **original\_unit** `string` *required* **status** `string` *required* The status of this coop exit request. **expires\_at** `string` *required* The time when the coop exit request expires and the UTXOs are released. **coop\_exit\_txid** `string` *required* The transaction id of the coop exit transaction. **fee\_quote\_id** `string` The fee quote ID used for this coop exit. **exit\_speed** `enum\` 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. > [!TIP] > > 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. ### Optional setup Configure [gas sponsorship](/wallets/gas-and-asset-management/gas/setup) to cover network fees. Without sponsorship, the wallet pays network fees in the chain's native token. *** > [!TIP] > > Privy automates token approvals and transaction submission. The response is a wallet action that > starts in `pending` status and can be polled for confirmation. *** > [!NOTE] > > **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). *** > [!WARNING] > > **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" } } ``` > [!WARNING] > > 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 **privy-app-id** `string` *required* ID of your Privy app. **privy-authorization-signature** `string` Request authorization signature. If multiple signatures are required, they should be comma separated. **privy-request-expiry** `string` Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters **wallet\_id** `string` *required* ID of the wallet to get. ### Body **method** `string` *required* Available options: `eth_sendTransaction` **caip2** `string` *required* The CAIP-2 chain identifier for the Tempo network. **params** `object` *required* **child attributes** **transaction** `object` *required* **child attributes** **type** `number` *required* Must be `118` for Tempo transactions. **from** `string` **chain\_id** `string | number` **nonce** `string | number` **gas\_limit** `string | number` **max\_fee\_per\_gas** `string | number` **max\_priority\_fee\_per\_gas** `string | number` **calls** `object[]` *required* The calls to execute atomically. **child attributes** **to** `string` *required* The target address for the call. **data** `string` The calldata for the call. **value** `string | number` The value to send with the call in wei. **fee\_token** `string` 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. **nonce\_key** `string | number` The Tempo 2D nonce key. **valid\_before** `string | number` Unix timestamp in seconds after which Tempo will reject the transaction. **valid\_after** `string | number` Unix timestamp in seconds before which Tempo will reject the transaction. **fee\_payer\_signature** `object` A fee payer signature for sponsored Tempo transactions. To have Privy populate this field, pass `sponsor: true` at the request root. **child attributes** **r** `string` *required* **s** `string` *required* **y\_parity** `0 | 1` *required* **access\_list** `object[]` **child attributes** **address** `string` *required* **storage\_keys** `string[]` *required* **aa\_authorization\_list** `object[]` Account abstraction authorizations for Tempo transactions. **child attributes** **contract** `string` *required* **chain\_id** `string | number` *required* **nonce** `string | number` *required* **signature** `string` *required* **sponsor** `boolean` Enable gas sponsorship for this transaction. [Learn more.](/wallets/gas-and-asset-management/gas/overview) **reference\_id** `string` 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). **address** `string` **chain\_type** `string` Available options: `ethereum` ### Returns **method** `enum\` *required* Available options: `eth_sendTransaction` **data** `object` *required* **child attributes** **hash** `string` *required* The broadcast transaction hash. **transaction\_request** `object` The full transaction object that was signed and broadcast. **caip2** `string` *required* **transaction\_id** `string` **reference\_id** `string | null` 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). > [!WARNING] > > **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 **privy-app-id** `string` *required* ID of your Privy app. **privy-authorization-signature** `string` Request authorization signature. If multiple signatures are required, they should be comma separated. **privy-request-expiry** `string` Request expiry. Value is a Unix timestamp in milliseconds representing the deadline by which the request must be processed. ### Path Parameters **wallet\_id** `string` *required* ID of the wallet to get. ### Body **method** `string` *required* Available options: `eth_signTransaction` **params** `object` *required* **child attributes** **transaction** `object` *required* **child attributes** **type** `number` *required* Must be `118` for Tempo transactions. **from** `string` **chain\_id** `string | number` **nonce** `string | number` **gas\_limit** `string | number` **max\_fee\_per\_gas** `string | number` **max\_priority\_fee\_per\_gas** `string | number` **calls** `object[]` *required* The calls to include in the transaction. **child attributes** **to** `string` *required* The target address for the call. **data** `string` The calldata for the call. **value** `string | number` The value to send with the call in wei. **fee\_token** `string` 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. **nonce\_key** `string | number` The Tempo 2D nonce key. **valid\_before** `string | number` Unix timestamp in seconds after which Tempo will reject the transaction. **valid\_after** `string | number` Unix timestamp in seconds before which Tempo will reject the transaction. **fee\_payer\_signature** `object` A fee payer signature for sponsored Tempo transactions. **child attributes** **r** `string` *required* **s** `string` *required* **y\_parity** `0 | 1` *required* **access\_list** `object[]` **child attributes** **address** `string` *required* **storage\_keys** `string[]` *required* **aa\_authorization\_list** `object[]` Account abstraction authorizations for Tempo transactions. **child attributes** **contract** `string` *required* **chain\_id** `string | number` *required* **nonce** `string | number` *required* **signature** `string` *required* ### Response **method** `enum\` *required* Available options: `eth_signTransaction` **data** `object` *required* **child properties** **signed\_transaction** `string` *required* **encoding** `enum\` 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. See [supported assets and chains](/wallets/actions/transfer/overview#supported-assets) and [gas and fee sponsorship](/wallets/actions/transfer/overview#gas-and-fee-sponsorship) for transfer coverage and payment options. > [!TIP] > > 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. > [!NOTE] > > 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. > [!NOTE] > > **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 } ``` # tron_sendTransaction Source: https://docs.privy.io/api-reference/wallets/tron/tron-send-transaction Sign and broadcast a Tron transaction in one call Sign a Tron transaction and broadcast it to the network. Privy automatically fetches block reference fields (`ref_block_bytes`, `ref_block_hash`, `expiration`) if you omit them. > [!NOTE] > > To sign without broadcasting — for example when using a custom RPC or gas sponsor — use > [`tron_signTransaction`](/api-reference/wallets/tron/tron-sign-transaction) instead. > [!WARNING] > > 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). ### SDK methods Learn more about sending Tron transactions using our SDKs [here](/wallets/using-wallets/tron/send-a-transaction). *** > [!WARNING] > > **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 TRX transfer theme={"system"} 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": "tron_sendTransaction", "caip2": "tron:mainnet", "params": { "raw_data": { "contract": [ { "type": "TransferContract", "owner_address": "41a614f803b6fd780986a42c78ec9c7f77e6ded13c", "to_address": "41e552f6487585c2b58bc2c9bb4492bc1f17132cd0", "amount": 1000000 } ] } } }' ``` ```sh TRC-20 transfer theme={"system"} 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": "tron_sendTransaction", "caip2": "tron:mainnet", "params": { "raw_data": { "contract": [ { "type": "TriggerSmartContract", "owner_address": "41a614f803b6fd780986a42c78ec9c7f77e6ded13c", "contract_address": "41a614f803b6fd780986a42c78ec9c7f77e6ded13c", "data": "a9059cbb000000000000000041e552f6487585c2b58bc2c9bb4492bc1f17132cd000000000000000000000000000000000000000000000000000000000000f4240", "call_value": 0 } ], "fee_limit": 100000000 }, "reference_id": "my-idempotency-key-123" } }' ``` ```json 200 theme={"system"} theme={"system"} { "method": "tron_sendTransaction", "data": { "hash": "8a3f2b1c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1", "caip2": "tron:mainnet", "transaction_id": "8a3f2b1c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1" } } ``` ### Headers **privy-app-id** `string` *required* ID of your Privy app. **privy-authorization-signature** `string` Request authorization signature. If multiple signatures are required, they should be comma separated. ### Path Parameters **wallet\_id** `string` *required* ID of the Tron wallet to sign and send with. ### Body **method** `string` *required* Must be `"tron_sendTransaction"`. **caip2** `string` Tron CAIP-2 chain. Defaults to `tron:mainnet`. Use `tron:nile` for the Nile testnet. **params** `object` *required* **properties** **params.raw\_data** `object` *required* **properties** **params.raw\_data.contract** `array` *required* Array of exactly one contract object. Use `TransferContract` for native TRX transfers; use `TriggerSmartContract` for TRC-20 transfers and other smart contract calls. All addresses must be 41-prefixed hex (use `TronWeb.address.toHex()` to convert from base58check). **TransferContract:** * `type`: `"TransferContract"` * `owner_address`: sender, 41-prefixed hex * `to_address`: recipient, 41-prefixed hex * `amount`: sun (1 TRX = 1,000,000 sun) **TriggerSmartContract:** * `type`: `"TriggerSmartContract"` * `owner_address`: caller, 41-prefixed hex * `contract_address`: contract, 41-prefixed hex * `data` (optional): ABI-encoded calldata, hex string * `call_value` (optional): TRX value in sun sent with the call * `call_token_value` (optional): TRC-10 token value * `token_id` (optional): TRC-10 token ID **params.raw\_data.fee\_limit** `number` Maximum energy fee in sun. Required for `TriggerSmartContract`. `100_000_000` (100 TRX) is a safe default for standard TRC-20 transfers. **params.raw\_data.ref\_block\_bytes** `string` Block reference bytes — 4 hex characters. Optional — Privy fetches fresh values if omitted. **params.raw\_data.ref\_block\_hash** `string` Block reference hash — 16 hex characters. Optional — Privy fetches fresh values if omitted. **params.raw\_data.expiration** `number` Expiration in Unix milliseconds. Optional — Privy sets `now + 60s` if omitted. **params.raw\_data.timestamp** `number` Creation timestamp in Unix milliseconds. Defaults to `Date.now()` if omitted. **params.raw\_data.data** `string` Transaction memo, hex-encoded. Distinct from `TriggerSmartContract.data` (calldata). **params.reference\_id** `string` Optional idempotency key (max 64 characters). Re-submitting the same `reference_id` returns the cached response. ### Returns **method** `string` Always `"tron_sendTransaction"`. **data.hash** `string` Tron transaction ID — SHA-256 of the raw transaction bytes, lowercase hex, no `0x` prefix. **data.caip2** `string` The CAIP-2 chain the transaction was broadcast on. **data.transaction\_id** `string` Privy internal transaction ID. **data.reference\_id** `string` Echoed back if provided in the request. # tron_signTransaction Source: https://docs.privy.io/api-reference/wallets/tron/tron-sign-transaction Sign a Tron transaction without broadcasting it Sign a Tron transaction using the wallet's private key. Returns the full signed transaction hex — you are responsible for broadcasting it. > [!NOTE] > > For a combined sign-and-broadcast call, use > [`tron_sendTransaction`](/api-reference/wallets/tron/tron-send-transaction) instead. *** > [!WARNING] > > **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 TRX transfer theme={"system"} 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": "tron_signTransaction", "params": { "raw_data": { "contract": [ { "type": "TransferContract", "owner_address": "41a614f803b6fd780986a42c78ec9c7f77e6ded13c", "to_address": "41e552f6487585c2b58bc2c9bb4492bc1f17132cd0", "amount": 1000000 } ], "ref_block_bytes": "a2b4", "ref_block_hash": "1234567890abcdef", "expiration": 1735689600000, "timestamp": 1735689540000 } } }' ``` ```sh TRC-20 transfer theme={"system"} 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": "tron_signTransaction", "params": { "raw_data": { "contract": [ { "type": "TriggerSmartContract", "owner_address": "41a614f803b6fd780986a42c78ec9c7f77e6ded13c", "contract_address": "41a614f803b6fd780986a42c78ec9c7f77e6ded13c", "data": "a9059cbb000000000000000041e552f6487585c2b58bc2c9bb4492bc1f17132cd000000000000000000000000000000000000000000000000000000000000f4240", "call_value": 0 } ], "ref_block_bytes": "a2b4", "ref_block_hash": "1234567890abcdef", "expiration": 1735689600000, "timestamp": 1735689540000, "fee_limit": 100000000 } } }' ``` ```json 200 theme={"system"} theme={"system"} { "method": "tron_signTransaction", "data": { "signed_transaction": "0a02a2b42208...", "encoding": "hex" } } ``` ### Headers **privy-app-id** `string` *required* ID of your Privy app. **privy-authorization-signature** `string` Request authorization signature. If multiple signatures are required, they should be comma separated. ### Path Parameters **wallet\_id** `string` *required* ID of the Tron wallet to sign with. ### Body **method** `string` *required* Must be `"tron_signTransaction"`. **params** `object` *required* **properties** **params.raw\_data** `object` *required* **properties** **params.raw\_data.contract** `array` *required* Array of exactly one contract object. Use `TransferContract` for native TRX transfers; use `TriggerSmartContract` for TRC-20 transfers and other smart contract calls. All addresses must be 41-prefixed hex (use `TronWeb.address.toHex()` to convert from base58check). **TransferContract:** * `type`: `"TransferContract"` * `owner_address`: sender, 41-prefixed hex * `to_address`: recipient, 41-prefixed hex * `amount`: sun (1 TRX = 1,000,000 sun) **TriggerSmartContract:** * `type`: `"TriggerSmartContract"` * `owner_address`: caller, 41-prefixed hex * `contract_address`: contract, 41-prefixed hex * `data` (optional): ABI-encoded calldata, hex string * `call_value` (optional): TRX value in sun sent with the call * `call_token_value` (optional): TRC-10 token value * `token_id` (optional): TRC-10 token ID **params.raw\_data.ref\_block\_bytes** `string` *required* Block reference bytes — 4 hex characters. Must be fetched fresh from chain before signing (expires in \~60s). Use `TronWeb.trx.getCurrentRefBlockParams()`. **params.raw\_data.ref\_block\_hash** `string` *required* Block reference hash — 16 hex characters. Fetched alongside `ref_block_bytes`. **params.raw\_data.expiration** `number` *required* Transaction expiration in Unix milliseconds. **params.raw\_data.timestamp** `number` Transaction creation timestamp in Unix milliseconds. Defaults to `Date.now()` if omitted. **params.raw\_data.fee\_limit** `number` Maximum energy fee in sun. Required for `TriggerSmartContract`. `100_000_000` (100 TRX) is a safe default for standard TRC-20 transfers. **params.raw\_data.data** `string` Transaction memo, hex-encoded. Distinct from `TriggerSmartContract.data` (calldata). ### Returns **method** `string` Always `"tron_signTransaction"`. **data.signed\_transaction** `string` The signed transaction hex: `rawDataHex + signatureHex`. The signature occupies the last 130 hex characters (65 bytes). Pass directly to `POST /wallet/broadcasthex` on a Tron full node, or use `TronWeb.trx.sendRawTransaction()`. **data.encoding** `string` Always `"hex"`. # 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. > [!WARNING] > > The following functionality exists for [wallets reconstituted > server-side](/wallets/wallets/create/create-a-wallet). More on [Privy architecture > here](/security/wallet-infrastructure/architecture) > [!NOTE] > > 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](https://mintcdn.com/privy-c2af3412/YvGXGsI-T4KAqoan/images/Webhooks.png?fit=max\&auto=format\&n=YvGXGsI-T4KAqoan\&q=85\&s=991c4fac21a2532704a32d492dfaeb50) ## Registering an endpoint 1. In your backend, create a new endpoint that will accept **POST** requests from Privy > [!TIP] > > 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 Privy publishes usage charge events for sponsoring gas, bridging, and swap provider fees. View [usage webhooks](/wallets/gas-and-asset-management/usage-billing/usage-webhooks) for event availability, payloads, and reconciliation guidance. | Event Name | Type | Action | | - | - | - | | Gas sponsorship charge recorded | usage.gas\_sponsorship.recorded | Privy recorded a charge for sponsored network gas. | | Swap provider fee charge recorded | usage.swap\_provider\_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 > **Quick testing guide** > > 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 > > > [!TIP] > > > > 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. > [!NOTE] > > 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. > **Verifying a webhook payload** > > 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. > > > [!TIP] > > > > 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 > > **NodeJS** > > 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`. > > **Go** > > 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`. > > **Ruby** > > 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 > ``` > > **Python** > > Use `client.webhooks.unsafe_unwrap` to parse a verified raw webhook payload into a typed event. > Verify the Svix signature at your HTTP layer before calling this method. > > ```python theme={"system"} > event = client.webhooks.unsafe_unwrap(raw_payload) > > if event.type == "transaction.confirmed": > print(event.transaction_hash) > ``` > > ### 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 > **[User events](/api-reference/webhooks/user/created)** > > 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. > **[Wallet events](/api-reference/webhooks/wallet/funds_deposited)** > > 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. > **[Transaction events](/api-reference/webhooks/transaction/confirmed)** > > Events for transaction lifecycle. Includes > [`transaction.confirmed`](/api-reference/webhooks/transaction/confirmed), > [`transaction.failed`](/api-reference/webhooks/transaction/failed), and more. > **[Wallet action events](/api-reference/webhooks/wallet-action/swap/created)** > > Events for swaps, transfers, and earn actions. Includes `wallet_action.swap.*`, > `wallet_action.transfer.*`, and more. > **[MFA events](/api-reference/webhooks/mfa/enabled)** > > Events for multi-factor authentication. Includes > [`mfa.enabled`](/api-reference/webhooks/mfa/enabled) and > [`mfa.disabled`](/api-reference/webhooks/mfa/disabled). > **[Intent events](/api-reference/webhooks/intents/created)** > > 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). > **[User operation events](/api-reference/webhooks/user-operation/completed)** > > 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; fetch the transaction directly for up-to-date gas data, since `gas_credits_charged` may still be `null` right after this webhook. 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; fetch the transaction directly for up-to-date gas data, since `gas_credits_charged` may still be `null` right after this webhook. 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. # 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. # Swap provider fee charge recorded Source: https://docs.privy.io/api-reference/webhooks/usage/swap_provider_fee_recorded webhook usage.swap_provider_fee.recorded Fired when Privy records a swap provider fee on a cross-chain transfer or swap 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 automation submitted Source: https://docs.privy.io/api-reference/webhooks/wallet-automation/submitted webhook wallet_automation.submitted Fired when a deposit automation has fired and its wallet action has been submitted for processing. > [!NOTE] > > The `trigger_asset_address` field contains `native` when the trigger asset is the chain's native > asset. # 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](https://mintcdn.com/privy-c2af3412/TvBDVAWEhg0sbEJZ/images/auth-splash.png?fit=max\&auto=format\&n=TvBDVAWEhg0sbEJZ\&q=85\&s=ecebe674810255c6542035f2c60ef015) *** ## 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 > [!WARNING] > > 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 > **[Login users with their email](/authentication/user-authentication/login-methods/email)** > > Authenticate users using just their email address and a one-time passcode. > **[Login with metamask](/authentication/user-authentication/login-methods/wallet)** > > Authenticate users with their externally owned Ethereum or Solana wallets. > **[Set up MFA](/authentication/user-authentication/mfa/overview)** > > Require passkey or authenticator app MFA to protect delegated logins. > **[Seamless Farcaster Mini App login](/recipes/farcaster/mini-apps)** > > 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. > [!NOTE] > > 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: **properties** **sid** `string` The user's current session ID **sub** `string` The user's Privy DID **iss** `string` The token issuer, which should always be [privy.io](https://privy.io) **aud** `string` Your Privy app ID **iat** `number` The timestamp of when the JWT was issued **exp** `number` The timestamp of when the JWT will expire and is no longer valid. This is generally 1 hour after the JWT was issued. > [!NOTE] > > 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. **React** 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(); ``` > [!WARNING] > > When using direct imports, you must ensure **`PrivyProvider`** has rendered before invoking the method. > Whenever possible, you should retrieve **`getAccessToken`** from the **`usePrivy`** hook. **React Native** 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(); ``` **Swift** 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 } ``` **Android** 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 }, ) } ``` **Flutter** 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 }, ); } ``` **Unity** 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); } ``` > [!NOTE] > > 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: **fetch** ```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) }); ``` **axios** ```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 }); ``` **ofetch** ```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. **local storage setup** 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: **NodeJS** ```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** ```go theme={"system"} // Example for Go accessToken := r.Header.Get("Authorization") accessToken = strings.Replace(accessToken, "Bearer ", "", 1) ``` **cookie setup** 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: **NodeJS** ```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** ```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. **NodeJS** ### 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). > [!TIP] > > 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: **jose** 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. **jsonwebtoken** 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. **Go** 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. **Rust** 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. **React** You can use the boolean `authenticated` from the `usePrivy` hook to determine if your user is authenticated or not. ```tsx theme={"system"} authenticated: boolean; ``` > [!TIP] > > 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.

; } } ``` **React Native** 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; ``` > [!TIP] > > Before determining a user's auth status from Privy, verify that Privy has fully initialized and > **`isReady`** is true. ### 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. ); } } ``` **Swift** 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 } ``` **Android** 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 } } ``` **Unity** 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; } ``` **Flutter** 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 a persisted session that cannot be verified without network connectivity. class AuthenticatedUnverified extends AuthState { const AuthenticatedUnverified(); } /// 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 { // Waits for initialization and retrieves the latest authentication state. Future getAuthState(); // Returns the latest cached state immediately and may return NotReady. AuthState get currentAuthState; // A stream for auth state updates. Stream get authStateStream; } ``` ### 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 `Unauthenticated`. If there is, Privy can't verify the validity of the prior session without network connectivity. Thus, the auth state will be set to `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. ### 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 = await privy.getAuthState(); 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 AuthenticatedUnverified(): // A prior session exists, but can't be verified without network connectivity. 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. > [!TIP] > > Enable CAPTCHA in the [Privy > Dashboard](https://dashboard.privy.io/apps?page=settings\&setting=advanced) before implementing > this feature. > [!NOTE] > > 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.)* **React** ```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 > [!NOTE] > > 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](https://mintcdn.com/privy-c2af3412/zlmLhiIqRR7ViKN0/images/jwt-based-auth-splash.png?fit=max\&auto=format\&n=zlmLhiIqRR7ViKN0\&q=85\&s=ac522c8b8c19e1983d064c042db39936) # 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](https://mintcdn.com/privy-c2af3412/zSwkF5-adOBDSGL-/images/jwt.png?fit=max\&auto=format\&n=zSwkF5-adOBDSGL-\&q=85\&s=41c879569e14361b74ab0f68ad32f777) You'll need to provide the following information: **Authentication environment** Choose where JWT-authenticated requests can originate from: client-side (end user devices), server-side (your backend), or both. > [!WARNING] > > **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. **JWT verification details** *required* 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: > **JWKS Endpoint** > > 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" > // ... > } > ] > } > ``` > **Public verification key** > > 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----- > ``` **JWT ID Claim** (default: sub) 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. **React** ### 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 > [!WARNING] > > 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}

); } ``` > [!NOTE] > > 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. **React Native** ### 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} ); } ``` > [!NOTE] > > 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. **Swift** ### 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 > [!NOTE] > > 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. **Android** ### 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 null only if the user is signed out of your auth provider if (!yourAuthManager.isSignedIn()) return null // Your custom logic to retrieve the JWT token // This might be from shared preferences, secure storage, or an API call // Let exceptions propagate when the token is temporarily unavailable (e.g. no network) return yourAuthManager.getAccessToken() } ``` The `tokenProvider` callback should: * Return the current user's access token as a `String` when authenticated * Return `null` only when the user is signed out of your auth provider. Privy logs the user out when restoring or refreshing a session. * Throw when the token is temporarily unavailable, such as when the device is offline. Privy keeps the session and retries on the next call. * Be implemented as a suspending function that can perform asynchronous operations > [!NOTE] > > Throwing from `tokenProvider` to keep the session requires Android SDK `0.18.0` or later. #### 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 > [!NOTE] > > 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. **Flutter** ### 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 `PrivyAuthException`. #### 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 > [!NOTE] > > 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). > [!WARNING] > > 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. > [!WARNING] > > 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}

))}
); } ``` > [!TIP] > > 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. > [!TIP] > > Enable email authentication in the [Privy > Dashboard](https://dashboard.privy.io/apps?page=login-methods) before implementing this feature. > [!NOTE] > > 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). **React** To authenticate your users with a one-time passcode (OTP) sent to their email address, use the `useLoginWithEmail` hook. > [!NOTE] > > 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. > [!NOTE] > > 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 **email** `string` *required* The email address of the user to log in. **disableSignup** `boolean` Whether to disable the ability to sign up with the email address. ### Returns **void** `Promise\` A promise that resolves when the code is sent. ## Login with Code ```tsx theme={"system"} loginWithCode: ({ code: string }) => Promise; ``` ### Parameters **code** `string` *required* The one-time passcode sent to the user's email address. ### Returns **void** `Promise\` 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'}; ``` **status** `'initial' | 'error' | 'sending-code' | 'awaiting-code-input' | 'submitting-code' | 'done'` The current state of the OTP flow. **error** `Error | null` 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 **user** `User` The user object corresponding to the authenticated user. **isNewUser** `boolean` Whether the user is a new user or an existing user. **wasAlreadyAuthenticated** `boolean` Whether the user entered the application already authenticated. **loginMethod** `LoginMethod | null` The method used by the user to login. **loginAccount** `LinkedAccountWithMetadata | null` The account corresponding to the loginMethod used. ### `onError` ```tsx theme={"system"} onError: (error: Error) => void ``` #### Parameters **error** `Error` The error that occurred during the login flow. ## Resources > **[React starter repo](https://github.com/privy-io/examples/tree/main/privy-react-starter)** > > Get started with React and Privy. > **[Next.js starter repo](https://github.com/privy-io/examples/tree/main/privy-next-starter)** > > Get started with Next.js and Privy. > **[Whitelabel starter repo](https://github.com/privy-io/examples/tree/main/privy-react-whitelabel-starter)** > > Get started with a whitelabel Privy integration. **React Native** To authenticate your users with a one-time passcode (OTP) sent to their email address, use the `useLoginWithEmail` hook. > [!NOTE] > > 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 **email** `string` *required* The email address of the user to log in. ### Returns **success** `boolean` 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 **code** `string` *required* The one-time passcode sent to the user's email address. **email** `string` The user's email address. Though this parameter is optional, it is highly recommended that you pass the user's email address explicitly. **disableSignup** `boolean` Whether to disable the ability to sign up with the email address. ### Returns **user** `PrivyUser` 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'}; ``` **status** `'initial' | 'error' | 'sending-code' | 'awaiting-code-input' | 'submitting-code' | 'done'` The current state of the email login flow. **error** `Error | null` 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 **email** `string` The email the code was sent to. ### `onLoginSuccess` ```tsx theme={"system"} onLoginSuccess?: ((user: User, isNewUser: boolean) => void) | undefined ``` #### Parameters **user** `User` The PrivyUser returned by loginWithCode. **isNewUser** `boolean` Whether the user is a new user or an existing user. ### `onError` ```tsx theme={"system"} onError?: (error: Error) => void ``` #### Parameters **error** `Error` The error that occurred during the login flow. ## Resources > **[Expo starter repo](https://github.com/privy-io/examples/tree/main/privy-expo-starter)** > > Get started with Expo and Privy. > **[Expo bare starter repo](https://github.com/privy-io/examples/tree/main/privy-expo-bare-starter)** > > Get started with Expo bare and Privy. **Swift** 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 **to** `String` *required* 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 **code** `String` *required* The one-time passcode sent to the user's email address. **sentTo** `String` *required* The user's email address. ### Returns **PrivyUser** `PrivyUser` 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)") } ``` **Android** 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 **email** `String` *required* The email address of the user to log in. ### Returns **Result\** `Result\` 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 **code** `String` *required* The one-time passcode sent to the user's email address. **email** `String` (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 **Result\** `Result\` 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}") } ) ``` **Unity** 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 **email** `string` *required* The email address of the user to log in. ### Returns **Task\** `Task\` 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 **email** `string` *required* The user's email address. **code** `string` *required* The one-time passcode sent to the user's email address. ### Returns **Task\** `Task\` 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."); } ``` **Flutter** 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 **email** `String` *required* The email address of the user to log in. ### Returns **Result\** `Future\\>` 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 **code** `String` *required* The one-time passcode sent to the user's email address. **email** `String` (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 **Result\** `Future\\>` 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 > **[Flutter starter repo](https://github.com/privy-io/examples/tree/main/privy-flutter-starter)** > > 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. > [!TIP] > > 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. > [!TIP] > > Interested in building a Farcaster Mini App? Check out our [Farcaster Mini App > recipe](/recipes/farcaster/mini-apps)! **React** > [!NOTE] > > 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 > **[React starter repo](https://github.com/privy-io/examples/tree/main/privy-react-starter)** > > Get started with React and Privy. > **[Next.js starter repo](https://github.com/privy-io/examples/tree/main/privy-next-starter)** > > Get started with Next.js and Privy. > **[Whitelabel starter repo](https://github.com/privy-io/examples/tree/main/privy-react-whitelabel-starter)** > > Get started with a whitelabel Privy integration. **React Native** To authenticate a user via Farcaster ([SIWF](https://github.com/farcasterxyz/protocol/discussions/110)), use the `loginWithFarcaster` method from the `useLoginWithFarcaster` hook. > [!NOTE] > > 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: **input.relyingParty** `string` *required* Your app's website. Described in SIWF spec as "Origin domain of app frontend." **input.redirectUrl** `string` 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. **input.disableSignup** `boolean` If true, the flow will only allow existing users to log in, preventing new account creation. **options.pollIntervalMs** `number` The interval in milliseconds which your app will poll a status endpoint to check if the user has successfully signed in using Farcaster. **options.pollAttempts** `number` The number of polling attempts that will be made to check for successful login. > [!WARNING] > > 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'}; ``` **status** `'initial' | 'error' | 'generating-uri' | 'awaiting-uri' | 'polling-status' | 'submitting-token' | 'done'` The current state of the Farcaster flow. **error** `Error | null` 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 ``` **user** `User` The user object returned after successful login. **isNewUser** `boolean` Whether the user is a new user or an existing user. **wasAlreadyAuthenticated** `boolean` Whether the user was already authenticated before the OAuth flow. **loginMethod** `string` The login method used ('google', 'apple', etc.). **linkedAccount** `LinkedAccount` The linked account if the user was already authenticated. ## Security and tokens > [!WARNING] > > 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 > **[React starter repo](https://github.com/privy-io/examples/tree/main/privy-react-starter)** > > Get started with React and Privy. > **[Next.js starter repo](https://github.com/privy-io/examples/tree/main/privy-next-starter)** > > Get started with Next.js and Privy. > **[Whitelabel starter repo](https://github.com/privy-io/examples/tree/main/privy-react-whitelabel-starter)** > > Get started with a whitelabel Privy integration. **React Native** 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). > [!WARNING] > > Login with OAuth might **not** work if you have not completed this step. > [!NOTE] > > 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 ``` **provider** `OAuthProviderType` *required* The OAuth provider to use for authentication. Valid values are: `'google'`, `'apple'`, `'twitter'`, `'github'`, `'discord'`, `'linkedin'`, `'spotify'`, `'tiktok'`, `'instagram'`, `'telegram'`. **disableSignup** `boolean` 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'}; ``` **status** `'initial' | 'error' | 'generating-challenge' | 'awaiting-passkey' | 'submitting-response' | 'done'` The current state of the passkey flow. **error** `Error | null` 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 **User** `PrivyUser` The [user object](/user-management/users/the-user-object) returned after successful login or signup." **isNewUser** `boolean` Whether the user is a new user or an existing user. **wasAlreadyAuthenticated** `boolean` Whether the user was already authenticated before the passkey flow. **loginMethod** `'passkey'` The login method used to authenticate the user. **linkedAccount** `LinkedAccount` The linked account if the user was already authenticated. ### `onError` ```tsx theme={"system"} onError: (error: Error) => void ``` #### Parameters **error** `Error` The error that occurred during the passkey flow. ## Resources > **[React starter repo](https://github.com/privy-io/examples/tree/main/privy-react-starter)** > > Get started with React and Privy. > **[Next.js starter repo](https://github.com/privy-io/examples/tree/main/privy-next-starter)** > > Get started with Next.js and Privy. > **[Whitelabel starter repo](https://github.com/privy-io/examples/tree/main/privy-react-whitelabel-starter)** > > Get started with a whitelabel Privy integration. **React Native** > [!TIP] > > Follow the [passkeys setup guide](/basics/react-native/advanced/setup-passkeys) to enable passkey authentication in your React Native app. > [!NOTE] > > 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 **relyingParty** `string` *required* The URL origin where your Apple App Site Association or Digital Asset Links are available (e.g. `https://example.com`). ### Response **user** `PrivyUser` The user object returned after successful login. **child attributes** **id** `string` The Privy DID which you may use to identify your user on your backend **created\_at** `number` UNIX timestamp of when the user was created **linked\_accounts** `Array` An array of the user's linked accounts **Account Types** **AppleAccount** `Object` **child attributes** **type** `string` 'apple\_oauth' **email** `string` Email address associated with the user's Apple account **subject** `number` ID of user from Apple's user API **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **CustomJwtAccount** `Object` **child attributes** **type** `string` 'custom\_auth' **custom\_user\_id** `string` ID of user from custom auth provider **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **DiscordAccount** `Object` **child attributes** **type** `string` 'discord\_oauth' **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, including the 4-digit discriminator prefixed by '#' **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **EmailAccount** `Object` **child attributes** **type** `string` 'email' **address** `string` Email address of user account **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **FarcasterAccount** `Object` **child attributes** **type** `string` 'farcaster' **fid** `number` FID of the user from Farcaster user API response **owner\_address** `string` Wallet address of the user from Farcaster user API response (Farcaster wallet address, not Privy embedded wallet address) **username** `string` Username of user from Farcaster user API response (does not include the '@') **display\_name** `string` Display name of user from Farcaster user API response **bio** `string` Bio of user from Farcaster user API response **profile\_picture\_url** `string` Profile picture URL of the user from Farcaster user API response **homepage\_url** `string` Profile URL of the user from Farcaster user API response **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **GithubAccount** `Object` **child attributes** **type** `string` 'github\_oauth' **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 **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **GoogleAccount** `Object` **child attributes** **type** `string` 'google\_oauth' **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 **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **InstagramAccount** `Object` **child attributes** **type** `string` 'instagram\_oauth' **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 **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **LinkedinAccount** `Object` **child attributes** **type** `string` 'linkedin\_oauth' **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 (does not include the '@') **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **PhoneAccount** `Object` **child attributes** **type** `string` 'phone' **number** `string` Phone number of user account (non-international numbers default to US) **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **SpotifyAccount** `Object` **child attributes** **type** `string` 'spotify\_oauth' **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 **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **TelegramAccount** `Object` **child attributes** **type** `string` 'telegram' **telegram\_user\_id** `string` ID of a user's telegram account **first\_name** `string` The first name displayed on a user's telegram account **last\_name** `string` The last name displayed on a user's telegram account **username** `string` The username displayed on a user's telegram account **photo\_url** `string` The url of a user's telegram account profile picture **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **TwitterAccount** `Object` **child attributes** **type** `string` 'twitter\_oauth' **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 (does not include the '@') **profile\_picture\_url** `string` Profile picture URL of the user from Twitter user API response **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **WalletAccount** `Object` **child attributes** **type** `string` 'wallet' **chain\_type** `string` Type of chain for the wallet: 'ethereum' or 'solana' **address** `string` Checksummed wallet address **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified ### 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 **relyingParty** `string` *required* The URL origin where your Apple App Site Association or Digital Asset Links are available (e.g. `https://example.com`). ### Response **user** `PrivyUser` The user object returned after successful signup. **child attributes** **id** `string` The Privy DID which you may use to identify your user on your backend **created\_at** `number` UNIX timestamp of when the user was created **linked\_accounts** `Array` An array of the user's linked accounts **Account Types** **AppleAccount** `Object` **child attributes** **type** `string` 'apple\_oauth' **email** `string` Email address associated with the user's Apple account **subject** `number` ID of user from Apple's user API **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **CustomJwtAccount** `Object` **child attributes** **type** `string` 'custom\_auth' **custom\_user\_id** `string` ID of user from custom auth provider **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **DiscordAccount** `Object` **child attributes** **type** `string` 'discord\_oauth' **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, including the 4-digit discriminator prefixed by '#' **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **EmailAccount** `Object` **child attributes** **type** `string` 'email' **address** `string` Email address of user account **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **FarcasterAccount** `Object` **child attributes** **type** `string` 'farcaster' **fid** `number` FID of the user from Farcaster user API response **owner\_address** `string` Wallet address of the user from Farcaster user API response (Farcaster wallet address, not Privy embedded wallet address) **username** `string` Username of user from Farcaster user API response (does not include the '@') **display\_name** `string` Display name of user from Farcaster user API response **bio** `string` Bio of user from Farcaster user API response **profile\_picture\_url** `string` Profile picture URL of the user from Farcaster user API response **homepage\_url** `string` Profile URL of the user from Farcaster user API response **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **GithubAccount** `Object` **child attributes** **type** `string` 'github\_oauth' **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 **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **GoogleAccount** `Object` **child attributes** **type** `string` 'google\_oauth' **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 **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **InstagramAccount** `Object` **child attributes** **type** `string` 'instagram\_oauth' **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 **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **LinkedinAccount** `Object` **child attributes** **type** `string` 'linkedin\_oauth' **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 (does not include the '@') **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **PhoneAccount** `Object` **child attributes** **type** `string` 'phone' **number** `string` Phone number of user account (non-international numbers default to US) **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **SpotifyAccount** `Object` **child attributes** **type** `string` 'spotify\_oauth' **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 **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **TelegramAccount** `Object` **child attributes** **type** `string` 'telegram' **telegram\_user\_id** `string` ID of a user's telegram account **first\_name** `string` The first name displayed on a user's telegram account **last\_name** `string` The last name displayed on a user's telegram account **username** `string` The username displayed on a user's telegram account **photo\_url** `string` The url of a user's telegram account profile picture **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **TwitterAccount** `Object` **child attributes** **type** `string` 'twitter\_oauth' **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 (does not include the '@') **profile\_picture\_url** `string` Profile picture URL of the user from Twitter user API response **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **WalletAccount** `Object` **child attributes** **type** `string` 'wallet' **chain\_type** `string` Type of chain for the wallet: 'ethereum' or 'solana' **address** `string` Checksummed wallet address **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified ### 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'}; ``` **status** `'initial' | 'error' | 'generating-challenege' | 'awaiting-passkey' | 'submitting-response' | 'done'` The current state of the passkey flow. **error** `Error | null` 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 **user** `PrivyUser` The user object returned after successful login or signup. **child attributes** **id** `string` The Privy DID which you may use to identify your user on your backend **created\_at** `number` UNIX timestamp of when the user was created **linked\_accounts** `Array` An array of the user's linked accounts **Account Types** **AppleAccount** `Object` **child attributes** **type** `string` 'apple\_oauth' **email** `string` Email address associated with the user's Apple account **subject** `number` ID of user from Apple's user API **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **CustomJwtAccount** `Object` **child attributes** **type** `string` 'custom\_auth' **custom\_user\_id** `string` ID of user from custom auth provider **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **DiscordAccount** `Object` **child attributes** **type** `string` 'discord\_oauth' **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, including the 4-digit discriminator prefixed by '#' **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **EmailAccount** `Object` **child attributes** **type** `string` 'email' **address** `string` Email address of user account **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **FarcasterAccount** `Object` **child attributes** **type** `string` 'farcaster' **fid** `number` FID of the user from Farcaster user API response **owner\_address** `string` Wallet address of the user from Farcaster user API response (Farcaster wallet address, not Privy embedded wallet address) **username** `string` Username of user from Farcaster user API response (does not include the '@') **display\_name** `string` Display name of user from Farcaster user API response **bio** `string` Bio of user from Farcaster user API response **profile\_picture\_url** `string` Profile picture URL of the user from Farcaster user API response **homepage\_url** `string` Profile URL of the user from Farcaster user API response **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **GithubAccount** `Object` **child attributes** **type** `string` 'github\_oauth' **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 **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **GoogleAccount** `Object` **child attributes** **type** `string` 'google\_oauth' **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 **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **InstagramAccount** `Object` **child attributes** **type** `string` 'instagram\_oauth' **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 **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **LinkedinAccount** `Object` **child attributes** **type** `string` 'linkedin\_oauth' **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 (does not include the '@') **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **PhoneAccount** `Object` **child attributes** **type** `string` 'phone' **number** `string` Phone number of user account (non-international numbers default to US) **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **SpotifyAccount** `Object` **child attributes** **type** `string` 'spotify\_oauth' **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 **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **TelegramAccount** `Object` **child attributes** **type** `string` 'telegram' **telegram\_user\_id** `string` ID of a user's telegram account **first\_name** `string` The first name displayed on a user's telegram account **last\_name** `string` The last name displayed on a user's telegram account **username** `string` The username displayed on a user's telegram account **photo\_url** `string` The url of a user's telegram account profile picture **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **TwitterAccount** `Object` **child attributes** **type** `string` 'twitter\_oauth' **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 (does not include the '@') **profile\_picture\_url** `string` Profile picture URL of the user from Twitter user API response **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **WalletAccount** `Object` **child attributes** **type** `string` 'wallet' **chain\_type** `string` Type of chain for the wallet: 'ethereum' or 'solana' **address** `string` Checksummed wallet address **first\_verified\_at** `number` UNIX timestamp for when the account was first verified and linked to the user **latest\_verified\_at** `number` UNIX timestamp for when the account was last verified **isNewUser** `boolean` 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 **error** `Error` 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 > **[Expo starter repo](https://github.com/privy-io/examples/tree/main/privy-expo-starter)** > > Get started with Expo and Privy. > **[Expo bare starter repo](https://github.com/privy-io/examples/tree/main/privy-expo-bare-starter)** > > Get started with Expo bare and Privy. **Swift** > [!TIP] > > 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 **relyingParty** `String` *required* The URL origin where your Digital Asset Links are available (e.g., `https://example.com`). ### Returns **PrivyUser** `PrivyUser` 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 **relyingParty** `String` *required* The URL origin where your Digital Asset Links are available (e.g., `https://example.com`). **displayName** `String` 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 **PrivyUser** `PrivyUser` 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)") } ``` **Android** > [!TIP] > > 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, passkeyContext: PasskeyContext? = null): Result ``` ### Parameters **relyingParty** `String` *required* The URL origin where your Digital Asset Links are available (e.g., `https://example.com`). **passkeyContext** `PasskeyContext?` The Android `Activity` used to launch the Credential Manager UI. Pass the current activity for reliable passkey prompts; if omitted, the SDK falls back to the application context. ### Response Returns a `Result` containing the user object after successful login. **PrivyUser** **id** `String` *required* The unique identifier for this user **linkedAccounts** `List\` *required* 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 **Properties** **CustomAuth** **customUserId** `String` *required* ID of user from custom auth provider **firstVerifiedAt** `Int?` UNIX timestamp for when the account was first verified and linked to the user **latestVerifiedAt** `Int?` UNIX timestamp for when the account was last verified **EmbeddedEthereumWalletAccount** **id** `String?` The unique identifier for this embedded wallet **address** `String` *required* The wallet address **chainId** `String?` The chain ID for this wallet **recoveryMethod** `String?` The recovery method configured for this wallet **hdWalletIndex** `Int` *required* The HD wallet index (0 is the primary wallet) **userCanSign** `Boolean` *required* Whether the user can sign with this wallet on their own. `false` when the user is only the wallet's [entity](/kyc-kyb/entities#check-whether-the-user-can-sign) **EmbeddedSolanaWalletAccount** **id** `String?` The unique identifier for this embedded wallet **address** `String` *required* The wallet address **chainId** `String?` The chain ID for this wallet **recoveryMethod** `String?` The recovery method configured for this wallet **hdWalletIndex** `Int` *required* The HD wallet index (0 is the primary wallet) **userCanSign** `Boolean` *required* Whether the user can sign with this wallet on their own. `false` when the user is only the wallet's [entity](/kyc-kyb/entities#check-whether-the-user-can-sign) **ExternalWalletAccount** **address** `String` *required* The wallet address **chainType** `ChainType` *required* The type of blockchain: ethereum or solana **chainId** `String?` The chain ID for this wallet **walletClientType** `String?` The wallet client type **connectorType** `String?` The connector type used **firstVerifiedAt** `Int?` UNIX timestamp for when the account was first verified and linked to the user **latestVerifiedAt** `Int?` UNIX timestamp for when the account was last verified **PhoneAccount** **phoneNumber** `String` *required* Phone number of user account **EmailAccount** **emailAddress** `String` *required* Email address of user account **GoogleOAuthAccount** **subject** `String` *required* ID of user from Google user API response **email** `String` *required* Email of user from Google user API response **name** `String?` Name of user from Google user API response **firstVerifiedAt** `Int?` UNIX timestamp for when the account was first verified and linked to the user **latestVerifiedAt** `Int?` UNIX timestamp for when the account was last verified **TwitterOAuthAccount** **subject** `String` *required* ID of user from Twitter user API response **username** `String` *required* Username of user from Twitter user API response (does not include the '@') **name** `String?` Name of user from Twitter user API response **email** `String?` Email of user from Twitter user API response **profilePictureUrl** `String?` Profile picture URL of the user from Twitter user API response **firstVerifiedAt** `Int?` UNIX timestamp for when the account was first verified and linked to the user **latestVerifiedAt** `Int?` UNIX timestamp for when the account was last verified **DiscordOAuthAccount** **subject** `String` *required* ID of user from Discord user API response **username** `String` *required* Username of user from Discord user API response **email** `String?` Email of user from Discord user API response **firstVerifiedAt** `Int?` UNIX timestamp for when the account was first verified and linked to the user **latestVerifiedAt** `Int?` UNIX timestamp for when the account was last verified **PasskeyAccount** **credentialId** `String` *required* The credential ID for this passkey **authenticatorName** `String?` Name of the authenticator device **createdWithBrowser** `String?` Browser used to create the passkey **createdWithOs** `String?` Operating system used to create the passkey **createdWithDevice** `String?` Device used to create the passkey **publicKey** `String?` The public key for this passkey **enrolledInMfa** `Boolean` *required* Whether this passkey is enrolled in multi-factor authentication **verifiedAt** `Long` *required* UNIX timestamp for when the account was verified **firstVerifiedAt** `Long?` UNIX timestamp for when the account was first verified and linked to the user **latestVerifiedAt** `Long?` UNIX timestamp for when the account was last verified **identityToken** `String?` 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 **embeddedEthereumWallets** `List\` *required* A list of the user's embedded Ethereum wallets. These wallets expose a "provider" instance which can be used to take wallet actions **embeddedSolanaWallets** `List\` *required* 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://", passkeyContext = activity // The current Android Activity ) 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, passkeyContext: PasskeyContext? = null): Result ``` ### Parameters **relyingParty** `String` *required* The URL origin where your Digital Asset Links are available (e.g., `https://example.com`). **displayName** `String` 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. **passkeyContext** `PasskeyContext?` The Android `Activity` used to launch the Credential Manager UI. Pass the current activity for reliable passkey prompts; if omitted, the SDK falls back to the application context. The `link` method also accepts this parameter. ### Response Returns a `Result` containing the user object after successful signup. **PrivyUser** **id** `String` *required* The unique identifier for this user **linkedAccounts** `List\` *required* 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 **Properties** **CustomAuth** **customUserId** `String` *required* ID of user from custom auth provider **firstVerifiedAt** `Int?` UNIX timestamp for when the account was first verified and linked to the user **latestVerifiedAt** `Int?` UNIX timestamp for when the account was last verified **EmbeddedEthereumWalletAccount** **id** `String?` The unique identifier for this embedded wallet **address** `String` *required* The wallet address **chainId** `String?` The chain ID for this wallet **recoveryMethod** `String?` The recovery method configured for this wallet **hdWalletIndex** `Int` *required* The HD wallet index (0 is the primary wallet) **userCanSign** `Boolean` *required* Whether the user can sign with this wallet on their own. `false` when the user is only the wallet's [entity](/kyc-kyb/entities#check-whether-the-user-can-sign) **EmbeddedSolanaWalletAccount** **id** `String?` The unique identifier for this embedded wallet **address** `String` *required* The wallet address **chainId** `String?` The chain ID for this wallet **recoveryMethod** `String?` The recovery method configured for this wallet **hdWalletIndex** `Int` *required* The HD wallet index (0 is the primary wallet) **userCanSign** `Boolean` *required* Whether the user can sign with this wallet on their own. `false` when the user is only the wallet's [entity](/kyc-kyb/entities#check-whether-the-user-can-sign) **ExternalWalletAccount** **address** `String` *required* The wallet address **chainType** `ChainType` *required* The type of blockchain: ethereum or solana **chainId** `String?` The chain ID for this wallet **walletClientType** `String?` The wallet client type **connectorType** `String?` The connector type used **firstVerifiedAt** `Int?` UNIX timestamp for when the account was first verified and linked to the user **latestVerifiedAt** `Int?` UNIX timestamp for when the account was last verified **PhoneAccount** **phoneNumber** `String` *required* Phone number of user account **EmailAccount** **emailAddress** `String` *required* Email address of user account **GoogleOAuthAccount** **subject** `String` *required* ID of user from Google user API response **email** `String` *required* Email of user from Google user API response **name** `String?` Name of user from Google user API response **firstVerifiedAt** `Int?` UNIX timestamp for when the account was first verified and linked to the user **latestVerifiedAt** `Int?` UNIX timestamp for when the account was last verified **TwitterOAuthAccount** **subject** `String` *required* ID of user from Twitter user API response **username** `String` *required* Username of user from Twitter user API response (does not include the '@') **name** `String?` Name of user from Twitter user API response **email** `String?` Email of user from Twitter user API response **profilePictureUrl** `String?` Profile picture URL of the user from Twitter user API response **firstVerifiedAt** `Int?` UNIX timestamp for when the account was first verified and linked to the user **latestVerifiedAt** `Int?` UNIX timestamp for when the account was last verified **DiscordOAuthAccount** **subject** `String` *required* ID of user from Discord user API response **username** `String` *required* Username of user from Discord user API response **email** `String?` Email of user from Discord user API response **firstVerifiedAt** `Int?` UNIX timestamp for when the account was first verified and linked to the user **latestVerifiedAt** `Int?` UNIX timestamp for when the account was last verified **PasskeyAccount** **credentialId** `String` *required* The credential ID for this passkey **authenticatorName** `String?` Name of the authenticator device **createdWithBrowser** `String?` Browser used to create the passkey **createdWithOs** `String?` Operating system used to create the passkey **createdWithDevice** `String?` Device used to create the passkey **publicKey** `String?` The public key for this passkey **enrolledInMfa** `Boolean` *required* Whether this passkey is enrolled in multi-factor authentication **verifiedAt** `Long` *required* UNIX timestamp for when the account was verified **firstVerifiedAt** `Long?` UNIX timestamp for when the account was first verified and linked to the user **latestVerifiedAt** `Long?` UNIX timestamp for when the account was last verified **identityToken** `String?` 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 **embeddedEthereumWallets** `List\` *required* A list of the user's embedded Ethereum wallets. These wallets expose a "provider" instance which can be used to take wallet actions **embeddedSolanaWallets** `List\` *required* 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://", passkeyContext = activity // The current Android Activity ) result.fold( onSuccess = { user -> // Handle successful signup }, onFailure = { error -> // Handle signup error } ) ``` **Flutter** > [!TIP] > > 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 **relyingParty** `String` *required* 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 **relyingParty** `String` *required* The URL origin where your Digital Asset Links are available (e.g., `https://example.com`). **displayName** `String` 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). > [!NOTE] > > 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. **React** ## 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. > [!NOTE] > > 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 **phoneNumber** `string` *required* The phone number of the user to log in. Must follow specific formatting conventions (see below). **disableSignup** `boolean` Whether to disable the ability to sign up with the phone number. ### Returns **void** `Promise\` 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 **code** `string` *required* The one-time passcode sent to the user's phone number. ### Returns **void** `Promise\` 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'}; ``` **status** `'initial' | 'error' | 'sending-code' | 'awaiting-code-input' | 'submitting-code' | 'done'` The current state of the OTP flow. **error** `Error | null` 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 **user** `User` The user object corresponding to the authenticated user. **isNewUser** `boolean` Whether the user is a new user or an existing user. **wasAlreadyAuthenticated** `boolean` Whether the user entered the application already authenticated. **loginMethod** `LoginMethod | null` The method used by the user to login. **loginAccount** `LinkedAccountWithMetadata | null` The account corresponding to the loginMethod used. ### `onError` ```tsx theme={"system"} onError: (error: Error) => void ``` #### Parameters **error** `Error` The error that occurred during the login flow. ## Resources > **[React starter repo](https://github.com/privy-io/examples/tree/main/privy-react-starter)** > > Get started with React and Privy. > **[Next.js starter repo](https://github.com/privy-io/examples/tree/main/privy-next-starter)** > > Get started with Next.js and Privy. > **[Whitelabel starter repo](https://github.com/privy-io/examples/tree/main/privy-react-whitelabel-starter)** > > Get started with a whitelabel Privy integration. **React Native** To authenticate your users with a one-time passcode (OTP) sent to their phone number, use the `useLoginWithSMS` hook. > [!NOTE] > > 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 **phone** `string` *required* The phone number of the user to log in. ### Returns **success** `boolean` 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 **code** `string` *required* The one-time passcode sent to the user's phone number. **phone** `string` The user's phone number. Though this parameter is optional, it is highly recommended that you pass the user's phone number explicitly. **disableSignup** `boolean` Whether to disable the ability to sign up with the phone number. ### Returns **user** `PrivyUser` 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'}; ``` **status** `'initial' | 'error' | 'sending-code' | 'awaiting-code-input' | 'submitting-code' | 'done'` The current state of the SMS login flow. **error** `Error | null` 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 **phone** `string` The phone number the code was sent to. ### `onLoginSuccess` ```tsx theme={"system"} onLoginSuccess?: ((user: User, isNewUser: boolean) => void) | undefined ``` #### Parameters **user** `User` The PrivyUser returned by loginWithCode. **isNewUser** `boolean` Whether the user is a new user or an existing user. ### `onError` ```tsx theme={"system"} onError?: (error: Error) => void ``` #### Parameters **error** `Error` The error that occurred during the login flow. ## Resources > **[Expo starter repo](https://github.com/privy-io/examples/tree/main/privy-expo-starter)** > > Get started with Expo and Privy. > **[Expo bare starter repo](https://github.com/privy-io/examples/tree/main/privy-expo-bare-starter)** > > Get started with Expo bare and Privy. **Swift** 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 **to** `String` *required* 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 **code** `String` *required* The one-time passcode sent to the user's phone number. **sentTo** `String` *required* The user's phone number. ### Returns **PrivyUser** `PrivyUser` 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)") } ``` **Android** 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 **phoneNumber** `String` *required* The phone number of the user to log in. Must be in E.164 format (e.g., "+14155552671"). ### Returns **Result\** `Result\` 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 **code** `String` *required* The one-time passcode sent to the user's phone number. **phoneNumber** `String` (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 **Result\** `Result\` 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}") } ) ``` **Flutter** 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 **phoneNumber** `String` *required* The phone number of the user to log in. Must be in E.164 format (e.g., "+14155552671"). ### Returns **Result\** `Future\\>` 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 **code** `String` *required* The one-time passcode sent to the user's phone number. **phoneNumber** `String` (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 **Result\** `Future\\>` 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 > **[Flutter starter repo](https://github.com/privy-io/examples/tree/main/privy-flutter-starter)** > > 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. > [!TIP] > > Enable wallet authentication in the [Privy > Dashboard](https://dashboard.privy.io/apps?page=login-methods) before implementing this feature. **React** **Ethereum (SIWE)** 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. > [!NOTE] > > In order to use Privy's login with wallet flow, users must actively have a Ethereum 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 **address** `string` *required* EIP-55 checksum-encoded wallet address performing the signing. **chainId** `eip155:$\{number\}` *required* EIP-155 Chain ID to which the session is bound (in CAIP-2 format), e.g. `eip155:1`. **disableSignup** `boolean` Whether to disable signup for this login flow. ### Returns **message** `string` 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 **signature** `string` *required* The EIP-191 signature corresponding to the message. **message** `string` *required* The EIP-4361 message returned by `generateSiweMessage`. **disableSignup** `boolean` Whether to disable signup for the login flow. ### Returns **User** `User` 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 **user** `User` The user object corresponding to the authenticated user. **isNewUser** `boolean` Whether the user is a new user or an existing user. **wasAlreadyAuthenticated** `boolean` Whether the user was already authenticated when the flow ran. **loginMethod** `LoginMethod | null` The method used by the user to login (if applicable). **loginAccount** `LinkedAccountWithMetadata | null` The account corresponding to the login method. ### `onError` ```tsx theme={"system"} onError?: (error: PrivyErrorCode) => void ``` #### Parameters **error** `PrivyErrorCode` 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`. > [!WARNING] > > **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 ; } ``` > [!NOTE] > > 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. **Solana (SIWS)** 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. > [!NOTE] > > In order to use Privy's login with wallet flow, users must actively have a Solana wallet > connected to your app from which you can request signatures. ## Generate SIWS message ```tsx theme={"system"} generateSiwsMessage({ address: string }) => Promise ``` ### Parameters **address** `string` *required* The base58-encoded Solana wallet address performing the signing. ### Returns **message** `string` 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 **signature** `string` *required* The signature corresponding to the SIWS message. **message** `string` *required* The SIWS message returned by `generateSiwsMessage`. **disableSignup** `boolean` Whether to disable signup for the login flow. ### Returns **User** `User` 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 > **[React starter repo](https://github.com/privy-io/examples/tree/main/privy-react-starter)** > > Get started with React and Privy. > **[Next.js starter repo](https://github.com/privy-io/examples/tree/main/privy-next-starter)** > > Get started with Next.js and Privy. > **[Whitelabel starter repo](https://github.com/privy-io/examples/tree/main/privy-react-whitelabel-starter)** > > Get started with a whitelabel Privy integration. **React Native** **Ethereum (SIWE)** To authenticate a user via an Ethereum wallet *([SIWE](https://eips.ethereum.org/EIPS/eip-4361))*, use the React Native SDK's `useLoginWithSiwe` hook. > [!NOTE] > > In order to use Privy's login with wallet flow, users must actively have a Ethereum 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` Wallet object containing EIP-55 compliant wallet address and chainId in CAIP-2 format. **chainId** `string` *required* The chain ID of the wallet. **address** `string` *required* The address of the wallet. **from** `Object` Origin object containing domain and uri. **domain** `string` *required* The domain of the origin. Must be allowlisted in the Privy dashboard. **uri** `string` *required* The uri of the origin. ### Returns **message** `string` 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. > [!TIP] > > 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 **signature** `string` *required* The signature of the SIWE message, signed by the user's wallet. **messageOverride** `string` An optional override for the message that is signed. **disableSignup** `boolean` If true, the user will not be automatically created if they do not exist in the Privy database. ### Returns **PrivyUser** `PrivyUser` 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 **message** `string` The SIWE message that was generated. ### `onSuccess` ```tsx theme={"system"} onSuccess?: ((user: PrivyUser, isNewUser: boolean) => void) | undefined ``` #### Parameters **user** `PrivyUser` The user object corresponding to the authenticated user. **isNewUser** `boolean` Whether the user is a new user or an existing user. ### `onError` ```tsx theme={"system"} onError?: (error: Error) => void ``` #### Parameters **error** `Error` 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" }; ``` **Solana (SIWS)** 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. > [!NOTE] > > In order to use Privy's login with wallet flow, users must actively have a Solana 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` Wallet object containing Solana wallet address. **address** `string` *required* The address of the wallet. **from** `Object` Origin object containing domain and uri. **domain** `string` *required* The domain of the origin. **uri** `string` *required* The uri of the origin. ### Returns **message** `string` 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 **signature** `string` *required* The signature of the SIWS message, signed by the user's wallet. **message** `string` *required* The original message that was signed. **wallet** `Object` *required* **walletClientType** `string` *required* The client of the connected wallet (e.g. 'phantom'). **connectorType** `string` *required* The type of the connector (e.g. 'wallet\_connect' or 'mobile\_wallet\_protocol'). **disableSignup** `boolean` If true, the user will not be automatically created if they do not exist in the Privy database. ### Returns **PrivyUser** `PrivyUser` 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 > **[Expo starter repo](https://github.com/privy-io/examples/tree/main/privy-expo-starter)** > > Get started with Expo and Privy. > **[Expo bare starter repo](https://github.com/privy-io/examples/tree/main/privy-expo-bare-starter)** > > Get started with Expo bare and Privy. **Swift** **Ethereum (SIWE)** 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 **params** `SiweMessageParams` Set of parameters required to generate the message. **appDomain** `String` *required* Your app's domain. e.g. "my-domain.com" **appUri** `String` *required* Your app's URI. e.g. "[https://my-domain.com](https://my-domain.com)" **chainId** `String` *required* EVM Chain ID, e.g. "1" for Ethereum Mainnet **walletAddress** `String` *required* The user's [ERC-55](https://eips.ethereum.org/EIPS/eip-55) compliant wallet address. ### Returns **String** `String` 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 **message** `String` *required* The message returned from "generateMessage". **signature** `String` *required* The signature of the SIWE message, signed by the user's wallet. **params** `SiweMessageParams` *required* The same SiweMessageParams passed into "generateMessage". **metadata** `WalletLoginMetadata` (Optional) you can pass additional metadata that will be stored with the linked wallet. **walletClientType** `WalletClientType` An enum specifying the type of wallet used to login. e.g. WalletClientType.metamask **connectorType** `String` A string identifying how wallet was connected. e.g. "wallet\_connect" ### Returns **PrivyUser** `PrivyUser` 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 } ``` **Solana (SIWS)** 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 **params** `SiwsMessageParams` The parameters For building a Sign-In with Solana (SIWS) message. **domain** `String` *required* The domain that is requesting the signing. Its value MUST be an *[RFC 3986](https://datatracker.ietf.org/doc/html/rfc3986) authority*. **uri** `String` *required* 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). **address** `String` *required* Solana address performing the sign-in. The address is case-sensitive. ### Returns **message** `String` 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 **message** `String` *required* The previously generated SIWS message. **signature** `String` *required* The SIWS signature generated by the wallet. **metadata** `WalletLoginMetadata` (Optional) Additional metadata specifying wallet client and connector type. **walletClientType** `WalletClientType` An enum specifying the type of wallet used to login. e.g. WalletClientType.metamask **connectorType** `String` A string identifying how wallet was connected. e.g. "wallet\_connect" ### Returns **user** `PrivyUser` 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 ``` **Android** **Ethereum (SIWE)** 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 **params** `SiweMessageParams` Set of parameters required to generate the message. **appDomain** `String` *required* Your app's domain. e.g. "my-domain.com" **appUri** `String` *required* Your app's URI. e.g. "[https://my-domain.com](https://my-domain.com)" **chainId** `String` *required* EVM Chain ID, e.g. "1" for Ethereum Mainnet **walletAddress** `String` *required* The user's [ERC-55](https://eips.ethereum.org/EIPS/eip-55) compliant wallet address. ### Returns **String** `Result\` 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 **message** `String` *required* The message returned from "generateMessage". **signature** `String` *required* The signature of the SIWE message, signed by the user's wallet. **params** `SiweMessageParams` *required* The same SiweMessageParams passed into "generateMessage". **metadata** `WalletLoginMetadata` (Optional) you can pass additional metadata that will be stored with the linked wallet. **walletClientType** `WalletClientType` An enum specifying the type of wallet used to login. e.g. WalletClientType.Metamask **connectorType** `String` A string identifying how wallet was connected. e.g. "wallet\_connect" ### Returns **PrivyUser** `Result\` 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 } ) ``` > [!NOTE] > > Want to link a wallet to an existing user? Check out the [linking accounts documentation](/user-management/users/linking-accounts#external-wallets). **Solana (SIWS)** 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 **params** `SiwsMessageParams` Set of parameters required to generate the message. **appDomain** `String` *required* Your app's domain. e.g. "my-domain.com" **appUri** `String` *required* Your app's URI. e.g. "[https://my-domain.com](https://my-domain.com)" **walletAddress** `String` *required* The user's Solana wallet address. ### Returns **String** `Result\` 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 **message** `String` *required* The message returned from "generateMessage". **signature** `String` *required* The signature of the SIWS message, signed by the user's wallet. **params** `SiwsMessageParams` *required* The same SiwsMessageParams passed into "generateMessage". **metadata** `WalletLoginMetadata` (Optional) you can pass additional metadata that will be stored with the linked wallet. **walletClientType** `String` The client of the connected wallet (e.g. "phantom"). **connectorType** `String` A string identifying how wallet was connected e.g. "wallet\_connect" ### Returns **PrivyUser** `Result\` 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 } ) ``` > [!NOTE] > > Want to link a wallet to an existing user? Check out the [linking accounts documentation](/user-management/users/linking-accounts#external-wallets). **Flutter** **Ethereum (SIWE)** 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 **params** `SiweMessageParams` Set of parameters required to generate the message. **appDomain** `String` *required* Your app's domain. e.g. "my-domain.com" **appUri** `String` *required* Your app's URI. e.g. "[https://my-domain.com](https://my-domain.com)" **chainId** `String` *required* EVM Chain ID, e.g. "1" for Ethereum Mainnet **walletAddress** `String` *required* The user's [ERC-55](https://eips.ethereum.org/EIPS/eip-55) compliant wallet address. ### Returns **String** `Result\` 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 **message** `String` *required* The message returned from "generateMessage". **signature** `String` *required* The signature of the SIWE message, signed by the user's wallet. **params** `SiweMessageParams` *required* The same SiweMessageParams passed into "generateMessage". **metadata** `WalletLoginMetadata` (Optional) you can pass additional metadata that will be stored with the linked wallet. **walletClientType** `WalletClientType` An enum specifying the type of wallet used to login. e.g. WalletClientType.metamask **connectorType** `String` A string identifying how wallet was connected. e.g. "wallet\_connect" ### Returns **PrivyUser** `Result\` 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 }, ); ``` **Solana (SIWS)** 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 **params** `SiwsMessageParams` Set of parameters required to generate the message. **appDomain** `String` *required* Your app's domain. e.g. "my-domain.com" **appUri** `String` *required* Your app's URI. e.g. "[https://my-domain.com](https://my-domain.com)" **walletAddress** `String` *required* The user's base58-encoded Solana wallet address. ### Returns **String** `Result\` 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 **message** `String` *required* The message returned from "generateMessage". **signature** `String` *required* The signature of the SIWS message, signed by the user's wallet. **params** `SiwsMessageParams` *required* The same SiwsMessageParams passed into "generateMessage". **metadata** `WalletLoginMetadata` (Optional) you can pass additional metadata that will be stored with the linked wallet. **walletClientType** `WalletClientType` An enum specifying the type of wallet used to login. e.g. WalletClientType.phantom **connectorType** `String` A string identifying how wallet was connected. e.g. "solana\_mobile\_wallet\_adapter" ### Returns **PrivyUser** `Result\` 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. **React** ```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 ; } ``` **React Native** ```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** ```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. **Android** ```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. **Flutter** ```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. > [!NOTE] > > 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: > **[Enrollment](/authentication/user-authentication/mfa/enrollment/overview)** > > Allow users to enroll in MFA with SMS, TOTP, or passkeys > **[Verification](/authentication/user-authentication/mfa/verify/overview)** > > Guide users through completing MFA when required > **[MFA required listener](/authentication/user-authentication/mfa/listener)** > > Know when MFA is required and trigger your custom UI ## Additional resources > **[Unenrollment](/authentication/user-authentication/mfa/unenroll)** > > Allow users to remove enrolled MFA methods > **[Error handling](/authentication/user-authentication/mfa/errors)** > > Handle errors during MFA flows > **[Default UI](/authentication/user-authentication/mfa/default-ui)** > > 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 > [!TIP] > > Enable MFA methods in the [Privy > Dashboard](https://dashboard.privy.io/apps?page=login-methods\&logins=mfa) before implementing this > feature. > [!NOTE] > > 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. **React** ### 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](https://mintcdn.com/privy-c2af3412/YvGXGsI-T4KAqoan/images/MFA.png?fit=max\&auto=format\&n=YvGXGsI-T4KAqoan\&q=85\&s=2ad6a50823b29bacbabd08067f025e0b) 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. > [!NOTE] > > 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"} > appId="your-privy-app-id" > config={{ > ...theRestOfYourConfig, > passkeys: { > // Set this to `false` if you wish to keep the passkey as a login method after unenrolling from MFA. > shouldUnlinkOnUnenrollMfa: false, > }, > }} > > > {/* 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](https://mintcdn.com/privy-c2af3412/zlmLhiIqRR7ViKN0/images/mfa-usage.png?fit=max\&auto=format\&n=zlmLhiIqRR7ViKN0\&q=85\&s=180dd7aec11922e9e59692313f20ebca) 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. **React Native** ### 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. > [!NOTE] > > 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: **mfaMethods** `MfaMethod[]` 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. **relyingParty** `string` 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 ; } ``` > [!WARNING] > > `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). > [!TIP] > > 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. > [!TIP] > > 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. > [!WARNING] > > 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 > > ); > } > ``` **React Native** ## 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}); ``` > **See an end-to-end example of enrolling users in MFA with passkeys** > > 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 } > > > ); > } > ``` **Swift** ## 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) ``` **Android** ## 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 } ``` **Flutter** ## 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 > [!NOTE] > > 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. > [!NOTE] > > 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. **React** ## 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, }); ``` > **See an end-to-end example of enrolling users in MFA with SMS** > > 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)}/> > > ; > } > ``` **React Native** ## 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, }); ``` > **See an end-to-end example of enrolling users in MFA with SMS** > > 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 ( > > onChangeText={setPhoneNumber} > flex={1} > keyboardType="number-pad" > /> > > > ) > } > > // Input field for the user to enter the MFA code sent to their phone number > return > <> > > > ; > } > ``` **Swift** ## 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) ``` **Android** ## 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 } ``` **Flutter** ## 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 > [!NOTE] > > 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. **React** ## 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 > [!TIP] > > 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}); ``` > **See an end-to-end example of enrolling users in MFA with TOTP** > > 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)}/> > >
> ); > } > ``` **React Native** ## 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. > [!TIP] > > 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}); ``` > **See an end-to-end example of enrolling users in MFA with TOTP** > > 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} > > value={mfaCode} > onChangeText={setMfaCode} > flex={1} > keyboardType="number-pad" > /> > {// Once the TOTP code is received in the authenticator app and input above, submit for enrollment} > > > } > > ); > } > ``` **Swift** ## 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) ``` **Android** ## 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 } ``` **Flutter** ## 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. **React** ```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`. **React Native** ```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`. **Swift** 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 | **Android** 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. **Flutter** 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 **React** ```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` } } ``` **React Native** ```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** ```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)" } } ``` **Android** ```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}" } } } ``` **Flutter** ```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 **React** 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}
); }; ``` > [!NOTE] > > 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. **React Native** 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} ); }; ``` > [!NOTE] > > 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. **Swift** 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() ``` > [!WARNING] > > 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) ``` **Android** 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 } } ``` > [!WARNING] > > 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")) ``` **Flutter** 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 }, ); ``` > [!WARNING] > > 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 > **See a recommended abstraction for guiding users to complete MFA** > > 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. > > **React** > > ```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}

} >
> ); > }; > ``` > > **React Native** > > ```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) => ( > ); } ``` **React Native** 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'}); ``` > [!NOTE] > > 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 ( ); } ``` **Swift** 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() ``` > [!NOTE] > > 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) > ``` **Android** 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 } ``` > [!NOTE] > > 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 > } > ``` **Flutter** 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 }, ); ``` > [!NOTE] > > 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 > [!TIP] > > 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 **React** 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 **React Native** 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 **Swift** 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 > [!WARNING] > > 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. **Android** 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 > [!WARNING] > > 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. **Flutter** 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 > [!WARNING] > > 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 **React** 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(); ``` **React Native** 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(); ``` **Swift** > [!WARNING] > > 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. **Android** > [!WARNING] > > 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. **Flutter** > [!WARNING] > > 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 > **[SMS verification](/authentication/user-authentication/mfa/verify/sms)** > > Verify users with SMS codes > **[TOTP verification](/authentication/user-authentication/mfa/verify/totp)** > > Verify users with authenticator apps > **[Passkey verification](/authentication/user-authentication/mfa/verify/passkeys)** > > Verify users with passkeys > **[MFA required listener](/authentication/user-authentication/mfa/listener)** > > Register an MFA event listener > **[Error handling](/authentication/user-authentication/mfa/errors)** > > 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. **React** ## 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. **React Native** ## 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. **Swift** ## 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. > [!WARNING] > > 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. **Android** ## 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. > [!WARNING] > > 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. **Flutter** ## 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. > [!WARNING] > > 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. **React** ## 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. **React Native** ## 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. **Swift** ## 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. > [!WARNING] > > 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. **Android** ## 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. > [!WARNING] > > 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. **Flutter** ## 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. > [!WARNING] > > 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. **React** ## 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. **React Native** ## 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. **Swift** ## 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. > [!WARNING] > > 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. **Android** ## 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. > [!WARNING] > > 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. **Flutter** ## 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. > [!WARNING] > > 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. > [!WARNING] > > 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. > **[Access tokens](#access-tokens)** > > Verify user authentication > **[Refresh tokens](#refresh-tokens)** > > Maintain user sessions > **[Identity tokens](#identity-tokens)** > > 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 > [!NOTE] > > Access token lifetime can be configured in the [Privy Dashboard](https://dashboard.privy.io) under > **User management > Authentication > Advanced**. > **[Access tokens guide](/authentication/user-authentication/access-tokens)** > > 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:** **1. User authenticates** Privy issues both an access token and a refresh token **2. Access token expires** After 1 hour, the access token becomes invalid **3. Automatic refresh** The SDK uses the refresh token to request a new access token **4. Session continues** The user remains authenticated without re-logging in **5. Refresh token expires** After 30 days (default), the user must re-authenticate > [!NOTE] > > 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 > [!NOTE] > > 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**. > **[Identity tokens guide](/user-management/users/identity-tokens)** > > 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) > [!NOTE] > > Session duration can be configured under **User management > Authentication > Advanced**. *** ## Security best practices > **Token transmission** > > **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 > **Token verification** > > **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 > > > [!TIP] > > > > Use Privy SDKs to ensure tokens are verified correctly on the server. > **Token exposure** > > **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 token protection** > > **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 > [!WARNING] > > 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 | > **[Complete access token guide](/authentication/user-authentication/access-tokens#verifying-the-access-token)** > > 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. > [!WARNING] > > The `verifyAuthToken` method only works with access tokens. Always use the appropriate identity > token verification method when working with identity tokens. > **[Complete identity token guide](/user-management/users/identity-tokens#verifying-the-identity-token)** > > 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 prebuilt 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](https://mintcdn.com/privy-c2af3412/YvGXGsI-T4KAqoan/images/Onboard.png?fit=max\&auto=format\&n=YvGXGsI-T4KAqoan\&q=85\&s=73a585da38b0eae634f6446982028b74) > [!NOTE] > > [Configure your login methods](/basics/get-started/dashboard/configure-login-methods) in the Privy > Dashboard before using the UI components. **React** > [!TIP] > > 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 **loginMethods** `PrivyClientConfig['loginMethods']` Optionally specify which login methods to display in the modal. **Login Methods** The following login methods are supported:
    **wallet** `string` **email** `string` **sms** `string` **google** `string` **twitter** `string` **discord** `string` **github** `string` **linkedin** `string` **spotify** `string` **instagram** `string` **tiktok** `string` **apple** `string` **farcaster** `string` **telegram** `string` **line** `string` **passkey** `string`
**prefill** `\{ type: 'email' | 'phone', value: string \}` Optionally pre-fill the login modal with the user's email or phone number. **disableSignup** `boolean` 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. **walletChainType** `'ethereum-only' | 'solana-only' | 'ethereum-or-solana'` 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 ; } ``` **onComplete** `(\{ user, isNewUser, wasAlreadyAuthenticated, loginMethod, linkedAccount \}) =\> void` 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. **Callback Parameters** **user** `PrivyUser` The user object with DID, linked accounts, and more. **isNewUser** `boolean` Whether this is the user's first login or a returning user. **wasAlreadyAuthenticated** `boolean` Whether the user was already authenticated when the component mounted. **loginMethod** `string | null` The authentication method used ('email', 'sms', 'siwe', 'apple', 'discord', 'github', 'google', 'linkedin', 'spotify', 'tiktok', 'twitter', 'farcaster', 'passkey', 'telegram', 'line') or null if already authenticated. **loginAccount** `object` 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'). **onError** `(error) =\> void` Callback that executes when there's an error during login or when the user exits the login flow. **React Native** > [!TIP] > > Make sure you have [properly configured PrivyElements](/basics/react-native/advanced/setup-privyelements) before using UI components for authentication. > [!WARNING] > > **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. > [!NOTE] > > 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 > [!TIP] > > 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 > [!TIP] > > 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 > [!NOTE] > > 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 **iOS** 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 ``` **Android** 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 **Using expo/router** 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, }); // ... } ``` **Without expo/router** 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. > [!WARNING] > > 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 @privy-io/expo expo-application expo-crypto expo-linking expo-secure-store expo-web-browser react-native-webview expo-apple-authentication react-native-passkeys @privy-io/expo-native-extensions ``` > [!NOTE] > > For fine-grained control over your project's dependencies, React Native 0.81+ (or Expo SDK 54+) > allows you to omit the following optional packages if you are not utilizing their corresponding > features: > > * `expo-apple-authentication` for native Sign in with Apple > * `react-native-passkeys` for passkey authentication > * `@privy-io/expo-native-extensions` for iCloud recovery > > Only omit these packages if you are certain your application will never invoke these specific > authentication methods. Calling these features without the underlying dependencies installed will > result in runtime crashes. ### Required Polyfills Install the necessary polyfills: ```bash theme={"system"} npm i fast-text-encoding react-native-get-random-values @ethersproject/shims ``` > [!TIP] > > 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 **Using expo/router** 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" } ``` **Without expo/router** 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() { ... } ``` > [!NOTE] > > 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; > ``` > **Metro build configuration** > > 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 > > > [!NOTE] > > > > 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 > [!TIP] > > 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. > [!NOTE] > > 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. **Ethereum** ```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`** **Example signature** ```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]] }); ``` **Example transaction** ```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' } ] }); ``` **Solana** ```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. **Example signature** ```tsx theme={"system"} // Sign message const message = 'Hello world'; const {signature} = await provider.request({ method: 'signMessage', params: {message} }); ``` **Example transaction** ```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 } }); ``` > [!TIP] > > [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. > [!WARNING] > > 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. **Using expo/router** 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`** > > ### 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 ( > loading={} > 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/). **Without expo/router** 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: **appId** `string` *required* Your Privy App ID. You can find this in the Privy Dashboard. **clientId** `string` *required* Your Privy Client ID. You can find this in the Privy Dashboard. ## Waiting for Privy to be ready When `PrivyProvider` first renders, Privy restores the prior session and initializes the embedded wallet. Initialization can refresh auth tokens and fetch up-to-date user data. Use `isReady` from `usePrivy` to await initialization completion. Your app can use it to gate `user` reads and wallet operations. The rest of the app can render immediately. Reading `user` before `isReady` can show stale auth state. Wallet calls before `isReady` can throw. `isReady` already includes embedded wallet initialization. Expo apps should not wait on a separate `useWallets` `ready` flag. ```tsx theme={"system"} import {usePrivy, useEmbeddedEthereumWallet} from '@privy-io/expo'; function HomeScreen() { const {isReady, user} = usePrivy(); const {wallets} = useEmbeddedEthereumWallet(); const onSend = async () => { if (!isReady || !wallets[0]) return; const provider = await wallets[0].getProvider(); await provider.request({ method: 'eth_sendTransaction', params: [ /* ... */ ] }); }; return ( Welcome {isReady && user ? {user.id} : null} ))} ); } ``` #### 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 ``` > **Solana dependencies** > > 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. > [!NOTE] > > 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 > [!TIP] > > 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. > [!NOTE] > > Privy can provision wallets for your users on both **Ethereum** and **Solana**. ## 3. Send a transaction with the embedded wallet **EVM** 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 ; } ``` > [!TIP] > > [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). **Solana** 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 ; } ``` > [!TIP] > > [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. > [!TIP] > > 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. > [!TIP] > > 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)! **Ethereum** ```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( ); ``` **Solana** ```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( ); ``` > [!NOTE] > > > > 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: **appId** `string` *required* Your Privy App ID. You can find this in the Privy Dashboard. **clientId** `string` (Optional) A client ID to be used for this app client. Learn more about app clients [here](/basics/get-started/dashboard/app-clients). **config** `Object` Configuration options for the Privy SDK. > [!NOTE] > > 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!
; } ``` > [!NOTE] > > **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. > **[Quickstart Guide](/basics/react/quickstart)** > > Learn how to log users in and transact with embedded wallets > **[NextJS starter repo](https://github.com/privy-io/examples/tree/main/privy-next-starter)** > > Check out the NextJS app starter repo for a complete example integration > **[React starter repo](https://github.com/privy-io/examples/tree/main/privy-react-starter)** > > Check out the React app starter repo for a complete example integration > **[Whitelabel starter repo](https://github.com/privy-io/examples/tree/main/privy-react-whitelabel-starter)** > > 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" }' ``` > [!NOTE] > > [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"} ] }' ``` > [!NOTE] > > 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). > [!NOTE] > > 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 } } }' ``` > [!WARNING] > > 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: **Authorization** `string` *required* Basic Auth header with your app ID as the username and your app secret as the password. [Read-only app secrets](/basics/get-started/dashboard/app-secrets) work only on endpoints that read data. **privy-app-id** `string` *required* Your Privy app ID as a string. Requests missing either of these headers will be rejected by Privy's middleware. > [!NOTE] > > 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** ```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)); ``` **cURL** ```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 ``` > [!NOTE] > > The Privy Ruby SDK requires Ruby 3.2.0 or later. ## Next steps > **[Setup](/basics/ruby/setup)** > > Configure the Privy client with app credentials and authorization context. > **[Quickstart](/basics/ruby/quickstart)** > > 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. **Ethereum** ```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 ``` **Solana** ```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 ``` > [!TIP] > > [Learn more](/wallets/wallets/create/create-a-wallet) about creating wallets. > [!NOTE] > > 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. **Ethereum** ```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 ``` **Solana** ```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 ``` > [!NOTE] > > 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. **Ethereum** ```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 ``` **Solana** ```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 ``` > [!TIP] > > [Learn more](/wallets/using-wallets/ethereum/sign-a-message) about signing messages. ## 3. Sending transactions > [!NOTE] > > 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. **Ethereum** ```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 ``` **Solana** ```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 ``` > [!TIP] > > [Learn more](/wallets/using-wallets/ethereum/send-a-transaction) about sending transactions. > [!TIP] > > 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 ``` > [!TIP] > > [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 > **[Authorization keys](/controls/authorization-keys/overview)** > > Add an extra layer of security by signing requests with authorization keys. > **[Policies](/controls/policies/overview)** > > Restrict what wallets can do with configurable policies. > **[Idempotency keys](/api-reference/idempotency-keys)** > > Prevent duplicate transactions with idempotency key support. > **[Quorum approvals](/controls/quorum-approvals/overview)** > > 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. > [!TIP] > > 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. > [!TIP] > > 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. > [!TIP] > > Learn more about optimizing request patterns and handling rate limits in the > [optimizing](/recipes/dashboard/optimizing) guide. ## Next steps > **[Quickstart](/basics/ruby/quickstart)** > > Create wallets, sign messages, and send transactions with the Ruby SDK. > **[Authorization keys](/controls/authorization-keys/overview)** > > 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 ``` > [!NOTE] > > 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. **Ethereum** ```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; ``` **Solana** ```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; ``` > [!TIP] > > [Learn more](/wallets/wallets/create/create-a-wallet) about creating wallets. > [!NOTE] > > 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. **Ethereum** ```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; ``` **Solana** ```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; ``` > [!TIP] > > [Learn more](/wallets/using-wallets/ethereum/sign-a-message) about signing messages. ## 3. Sending transactions > [!NOTE] > > 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. **Ethereum** ```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; ``` **Solana** ```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; ``` > [!TIP] > > [Learn more](/wallets/using-wallets/ethereum/send-a-transaction) about sending transactions. > [!TIP] > > 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; ``` > [!TIP] > > [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: **anyhow** ```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) } ``` **thiserror** ```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) } ``` > [!TIP] > > 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(()) } ``` > [!TIP] > > 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. > [!TIP] > > 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. > [!TIP] > > 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. > [!NOTE] > > 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 > [!TIP] > > 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. > [!NOTE] > > 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. **Ethereum** ### 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)") } } ``` **Solana** ### 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. > [!WARNING] > > 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: **appId** `String` *required* Your Privy application ID, which can be obtained from the [**Privy Developer Dashboard**](https://dashboard.privy.io), under App Settings > Basics **appClientId** `String` *required* Your app client ID, which can be obtained from the [**Privy Developer Dashboard**](https://dashboard.privy.io), under App Settings > Clients **loggingConfig** `PrivyLoggingConfig` (Optional) Your preferred log level and logging method. If no log level is specified, it will default to `PrivyLogLevel.NONE`. **customAuthConfig** `LoginWithCustomAuthConfig` (Optional) Only use this if you plan to use custom authentication. Find more information [here](/authentication/user-authentication/jwt-based-auth/overview). > [!TIP] > > 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. > [!NOTE] > > 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. > [!TIP] > > 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 | | [`insufficient_app_secret_permissions`](#insufficient-app-secret-permissions) | The app secret does not have permission to make this request | ## 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 credit balance on the [Fee sponsorship](https://dashboard.privy.io/billing?tab=fee-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. ## App secret errors ### `insufficient_app_secret_permissions` **Description:** The app secret does not have permission to make this request This error occurs when a request is authenticated with a [read-only app secret](/basics/get-started/dashboard/app-secrets) but the endpoint requires a full access app secret. The API returns a `403 Forbidden` status. **Common causes:** * A read-only app secret is used to create, update, or delete a resource, such as creating a wallet or sending a transaction * A read-only app secret is used on an [unsupported `GET` endpoint](/basics/get-started/dashboard/app-secrets#supported-endpoints), such as listing intents **Troubleshooting:** * **Check the endpoint:** Confirm the endpoint is listed under [supported endpoints](/basics/get-started/dashboard/app-secrets#supported-endpoints) for read-only app secrets * **Check the secret's access level:** In [Configuration > App settings > Basics](https://dashboard.privy.io/apps?page=settings\&tab=basics), confirm which secrets have read-only access * **Use a full access secret:** If the service needs to make changes, generate a full access app secret for it. The access level of an existing secret cannot be changed # 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. > [!TIP] > > 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 finish initialization before using wallets * The app is not waiting for wallets to finish loading before interacting with them **Troubleshooting:** * Confirm that the origin is allowlisted in the dashboard [here](https://dashboard.privy.io/apps?setting=domains\&page=settings). * **React:** Wait for `ready` from `usePrivy` [here](/basics/react/setup#waiting-for-privy-to-be-ready) and `ready` from `useWallets` [here](/wallets/wallets/get-a-wallet/get-connected-wallet#waiting-for-wallets-to-be-ready). * **React Native:** Wait for `isReady` from `usePrivy` before wallet operations [here](/basics/react-native/setup#waiting-for-privy-to-be-ready). `isReady` includes embedded wallet initialization. Expo does not expose `useWallets().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: **Gatsby** 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`**. > [!NOTE] > > Still have questions? Reach out to our [support team](https://privy.io/slack) – we're here to help! **NextJS** 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). > [!NOTE] > > Still have questions? Reach out to our [support team](https://privy.io/slack) – we're here to help! **Create React App** 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!** > [!NOTE] > > Still have questions? Reach out to our [support team](https://privy.io/slack) – we're here to help! **Vite** 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!** > [!NOTE] > > 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 ``` > [!NOTE] > > 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. > [!TIP] > > 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](https://mintcdn.com/privy-c2af3412/Ih_Fo3QYM486gzWq/images/unity-setup/webview-template.png?fit=max\&auto=format\&n=Ih_Fo3QYM486gzWq\&q=85\&s=0b6d2f19f9457b42eca83ff790b483bb) * In **Other Settings/Optimization**, **managed stripping level** to **minimal** ![webview-stripping-settings](https://mintcdn.com/privy-c2af3412/zlmLhiIqRR7ViKN0/images/unity-setup/webview-stripping-settings.png?fit=max\&auto=format\&n=zlmLhiIqRR7ViKN0\&q=85\&s=96b832009a26bc0408c7b526f793561a) > [!NOTE] > > 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 > [!TIP] > > 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 **Ethereum** ### 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}"); } ``` **Solana** ### 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. **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"} 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 } ``` **2. Encode the message for signature** 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); ``` **3. Execute the signature request** 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. > [!WARNING] > > 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 > [!TIP] > > 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 New Privy features shipped every week across wallets, stablecoin payments, funding, policies, and agent payments. Every Friday, Privy ships new features for developers building on wallets and stablecoin rails. Weekly releases are listed first, newest first, followed by monthly rollups. ## Weekly shipped features **October 16, 2026** Earn deposit automations ## Sweep deposits into yield vaults automatically Set an automation and matching deposits sweep into a yield-bearing vault as soon as they land, with no extra steps from users. Pair it with deposit addresses so assets sent from other chains are converted into the vault's required asset and start earning automatically. Create one automation per vault and attach it to as many wallets as you need through the API or the Node SDK. Webhooks confirm each deposit as it reaches the vault. [Learn more](https://docs.privy.io/wallets/automations/earn-deposits) **October 9, 2026** Monthly postpaid billing ## Monthly postpaid billing for Enterprise Enterprise customers can now receive one monthly bill instead of prepaying for usage credits, forecasting usage, or topping up their account when sponsoring users' gas or swap fees. Custom monthly usage limits are coming soon. Enable postpaid billing through your account manager. [Learn more](https://docs.privy.io/wallets/gas-and-asset-management/usage-billing/overview#choosing-your-plan) **October 2, 2026** Swap fee sponsorship ## Sponsor swap fees for your users Your app can now sponsor swap and routing fees so users move between stablecoins without losing value. Turn it on in your dashboard and it applies automatically to transfers, swaps, and deposit addresses, with no code changes. * 1:1 conversions on supported routes * ETH, SOL, and BTC to stablecoins at market rates with fixed swap and routing fees * Swap and routing fees on all other routes * Prepaid credits or postpaid billing (Enterprise only) [Learn more](https://docs.privy.io/wallets/actions/transfer/fixed-rates) **September 25, 2026** Wallet addresses as deposit addresses ## Fund a wallet by sending to its address Your users' primary wallet addresses can now double as deposit addresses. Configure wallets to automatically bridge and swap incoming deposits so your app receives the asset it requires, and optionally sponsor gas and swap fees to preserve the value of each deposit. If an unsupported asset or chain is used, you can recover the funds yourself. [Learn more](https://docs.privy.io/wallets/funding/crypto-deposits/overview) **September 18, 2026** KYB and fiat payouts for organizations ## KYB and fiat payouts for organizations Verify a business, link its company bank account, and pay out from its organization wallet, natively in the Privy APIs. KYB runs against the organization itself, so the compliance record names the company rather than the employee who completed verification. * Hosted KYB links for organizations * Beneficial owner verification in one flow * Company bank account destinations * Payouts from organization wallets * Webhooks on KYB status and outstanding documents [Learn more](https://docs.privy.io/kyc-kyb/kyb) **August 14, 2026** x402 for gas-sponsored wallets ## x402 payments on gas-sponsored wallets Agents can now pay for APIs and content with x402 from gas-sponsored and delegated wallets, without holding native tokens for gas. Privy handles the ERC-1271 signatures these wallets require with a single parameter in your signing call. The same setup works across DeFi protocols, Sign-in with Ethereum, and more. Available across the API, React SDK, and Node SDK. [Learn more](https://docs.privy.io/recipes/agent-integrations/x402#with-gas-sponsored-wallets) **August 7, 2026** Revenue dashboard ## Track Earn revenue in your dashboard The new Revenue section in your dashboard tracks fee revenue generated from Earn vaults over time, so you can see how the revenue stream you configure grows. [Learn more](https://docs.privy.io/wallets/actions/earn/overview#capabilities) **July 31, 2026** Robinhood Chain support ## Deposit, swap, and transfer on Robinhood Chain The same Privy APIs now work on Robinhood Chain with no special configuration. Developers can optionally add their own fees on swaps. * Deposit addresses with USDG and ETH * Swaps across all assets * Transfers within Robinhood Chain or cross-chain [Learn more](https://docs.privy.io/wallets/funding/crypto-deposit-addresses#supported-chains) **July 24, 2026** Unified funding ## Fiat and crypto deposits in one flow Users can now fund their wallets with fiat or crypto through a single hook and one in-app modal. You choose where funds go, and Privy handles payment method selection, provider routing, and deposit completion. * Cards, Apple Pay, Google Pay, and ACH via Stripe * Crypto deposits from any wallet or exchange * Automatic provider routing by region * No redirects or third-party sign-in [Learn more](https://docs.privy.io/wallets/funding/add-funds) **July 17, 2026** Dashboard activity overview ## A live view of your onchain business Your Privy dashboard now tracks total assets, transaction volume, and user activity for the past 30 days in one place, without building custom dashboards or querying the chain. [Open the dashboard](https://dashboard.privy.io) **July 10, 2026** AWS Bedrock AgentCore payments ## Agent payments on AWS Bedrock AgentCore Agents on AWS Bedrock AgentCore can now pay for APIs, MCP tools, and web content autonomously using Privy wallets. AgentCore enforces spend limits, signs through Privy, and settles in USDC on Ethereum, Solana, or Base. The agent never handles keys directly, and users retain wallet ownership and can revoke access at any time. [Learn more](https://docs.privy.io/recipes/agent-integrations/agentcore-payments) **June 26, 2026** Real-time balances in the dashboard ## Real-time wallet balances in the dashboard Wallet balances in your dashboard now update in real time, and you can add any token for the same visibility. Supported across Ethereum, Solana, Tempo, Base, Polygon, and Arbitrum. [Open the dashboard](https://dashboard.privy.io) **June 19, 2026** Pay gas with stablecoins ## Let users pay gas with stablecoins Users can now pay gas fees with stablecoins instead of holding native tokens. Enable User pays mode in your dashboard, and stablecoin transfers automatically use that token to cover gas. Supported tokens include USDC, USDT, EURC, USDG, and USDC.e across supported EVM chains. [Open the dashboard](https://dashboard.privy.io) **June 12, 2026** Headless deposit addresses ## Headless React support for universal deposit addresses Build fully custom deposit UIs while Privy handles bridging, swapping, and settlement behind the scenes. Users deposit crypto from supported chains through a persistent address. Available for all React developers. [Learn more](https://docs.privy.io/wallets/funding/headless-deposit-addresses) **June 5, 2026** Transfer policies ## Enforce transfer rules at the wallet layer Add guardrails around where funds can move before a transaction is signed, configured through Privy's policy engine. Restrict transfers by asset, amount, chain, or destination address. Useful for AI agents that should operate within defined limits, treasury wallets that should only send to approved destinations, and consumer apps that need spending and withdrawal controls. [Learn more](https://docs.privy.io/wallets/actions/transfer/policies) **May 29, 2026** Open-source server SDKs ## Open-source server-side SDKs The Node SDK is now open source, alongside the Go, Ruby, and Rust SDKs, with Java and Python coming soon. Inspect, customize, and contribute to the code you use, or open an issue on GitHub. [View on GitHub](https://github.com/privy-io) **May 22, 2026** Dashboard command palette ## Command palette in the dashboard Press Cmd+K on Mac or Ctrl+K on Windows to search and navigate anywhere in your dashboard. [Open the dashboard](https://dashboard.privy.io) **May 15, 2026** Cross-chain transfers ## Cross-chain stablecoin transfers in one API call Privy now bridges and swaps stablecoins across Ethereum, Base, Arbitrum, Polygon, Solana, and Tempo in a single call, handling execution, gas sponsorship, status polling, and webhooks behind the scenes. * Move USDC, USDT, or other stablecoins across chains in one call * Swap between USD-backed stablecoins across networks * Abstract away gas and bridging entirely [Learn more](https://docs.privy.io/wallets/actions/transfer/bridging) **May 8, 2026** Gasless USDT on Tron ## Send USDT on Tron without holding TRX A new recipe with TransaTron lets your app fully sponsor gas on Tron, so users can send USDT without a TRX balance. TransaTron delegates energy and bandwidth on your users' behalf. [Learn more](https://docs.privy.io/recipes/tron/transatron) ## Monthly rollups ## September 2026 * **[Treasury Management workspace](https://docs.privy.io/treasury-management/overview) is live.** Finance and Ops teams can hold, move, and govern onchain capital directly from the Privy dashboard, with maker-checker flows, multi-party approvals, and granular transfer limits and allowlists. * **[Prebuilt card components](https://docs.privy.io/financial-flows/cards/pre-built-components/overview) are now available.** Go live with a global stablecoin card program in as little as one month on Stripe Issuing, which already powers 400M+ cards. Components ship in the Privy React SDK and cover onboarding, disclosures, KYC, balances, statements, and card management, with Managed Support from Stripe. * **[Organizations API](https://docs.privy.io/organizations/setup/organizations) for team-owned wallets.** Create organizations such as businesses and customers, add users, and provision shared wallets programmatically. Built for business banking, treasury, payroll, and B2B payments. * **[End-of-month gas billing](https://docs.privy.io/wallets/gas-and-asset-management/usage-billing/overview#choosing-your-plan) for Enterprise.** Privy meters gas sponsorship charges and invoices them at the end of the month instead of requiring prepaid credits, for quicker setup and easier forecasting. * **[OUSD](https://docs.privy.io/recipes/send-ousd) is live.** OUSD is a stablecoin designed for money movement, with no mint or burn fees, reliable rewards for Network Partners, and strong pricing across the Stripe stack. ## August 2026 * **[Organization Wallets](https://docs.privy.io/organizations/overview) are live.** B2B products can provision wallets that teams share, so they can hold funds, manage access, and operate financial workflows together. Built for business banking, treasury, payroll, and B2B payments on top of users, key quorums, intents, and policies. * **Improved server-side fiat orchestration APIs.** Server-side [KYC/KYB](https://docs.privy.io/kyc-kyb/overview), onramp, and offramp with Bridge as the underlying provider, covering ACH, wire, FedNow, Faster Payments, Pix, and SEPA. Contact [sales@privy.io](mailto:sales@privy.io) to get started. * **[Tempo](https://tempo.xyz/) is offering a boost of up to 7% APY on balances held via [Earn](https://docs.privy.io/wallets/actions/earn/overview).** Live from the first week of September for a limited time, available to Privy customers only. Contact [sales@privy.io](mailto:sales@privy.io) to take advantage. * **Guaranteed 1:1 rates on stablecoin swaps and cross-chain transfers.** Rolling out now so end users move between stablecoins without rate slippage. Contact [sales@privy.io](mailto:sales@privy.io) about specific stablecoins or routes. * **Global fiat card onramps with [Meld](https://docs.privy.io/wallets/funding/fiat-onramp).** Configure Meld in the dashboard under Onramps: enable fiat onramps, select Configure Meld, then request production access with a short KYB form. * **Proactive status updates.** Go to [status.privy.io](https://status.privy.io), select Get Updates, and choose your preferred communication method to receive updates on Privy's operational systems. * **Revenue is a brand new tab in the dashboard.** Monitor how much revenue you are generating from Privy features, starting with fees collected from [Earn](https://docs.privy.io/wallets/actions/earn/overview). Swap and transfer fees are coming soon. ## July 2026 * **[Cards linked to stablecoin balances](https://docs.privy.io/financial-flows/cards/overview#choose-your-integration).** Issue cards that spend directly from stablecoin balances, even when those balances are connected to Earn DeFi vaults. You and your users can earn and spend on idle funds at the same time, within the same flow. * **[Morpho Earn on Tempo](https://docs.privy.io/wallets/actions/earn/overview), plus [Veda](https://docs.privy.io/wallets/actions/earn/providers/veda#how-veda-works-with-privy).** Earning on idle balances through Morpho is now available on Tempo and can be enabled directly from the Privy dashboard. Veda is also live, letting you allocate across multiple lending protocols in one integration for a more diversified yield strategy. * **[Unified funding hook](https://docs.privy.io/wallets/funding/add-funds).** One onboarding flow lets users fund their wallets with fiat, crypto, or both, instead of branching between separate funding paths. * **Monetize [cross-chain transfers](https://docs.privy.io/wallets/actions/transfer/bridging) and [swaps](https://docs.privy.io/wallets/actions/swap/execute#cross-chain).** Charge fees on these wallet actions directly through Privy, keeping fee flows inside the platform. Contact [sales@privy.io](mailto:sales@privy.io) to get set up. * **[x402 payments out of the box](https://docs.privy.io/recipes/agent-integrations/x402#with-gas-sponsored-wallets) with gas-sponsored wallets.** Accept x402 payments from gas-sponsored wallets across the API, React, and Node SDKs, with no workarounds required. Privy also generates [ERC-1271 signatures](https://docs.privy.io/recipes/evm/erc-1271-signatures) for other smart wallet verification flows. * **[Archive wallets](https://docs.privy.io/wallets/wallets/archive-a-wallet).** Keep wallet management clean by archiving unused or test wallets via API, and restore them from the dashboard at any time. Archiving also keeps excessive wallet counts from blocking auth token issuance. * **Granular wallet analytics for Enterprise.** New data sharing capabilities go beyond what's [available in the dashboard](https://docs.privy.io/wallets/overview) for more granular analytics on your Privy wallets. Available to Enterprise customers, so contact [sales@privy.io](mailto:sales@privy.io) if you are interested. ## 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**. **Dashboard** 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. > [!NOTE] > > 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. **NodeJS** 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] } }); ``` **Python** Generate a P-256 authorization keypair with `generate_p256_key_pair`. ```python theme={"system"} from privy import AuthorizationContext, PrivyRequestOptions, generate_p256_key_pair keypair = generate_p256_key_pair() wallet = client.wallets.create( chain_type="ethereum", owner={"public_key": keypair.public_key}, ) response = client.wallets.ethereum.sign_message( wallet.id, "Hello, world!", request_options=PrivyRequestOptions( authorization_context=AuthorizationContext( authorization_private_keys=[keypair.private_key] ) ), ) ``` The returned public and private keys are base64-encoded DER values without PEM headers or footers. **REST API** Authorization keys are [P-256](https://neuromancer.sk/std/nist/P-256) public-private keypairs. > [!NOTE] > > 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. > [!TIP] > > 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** 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. > [!TIP] > > 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 > [!TIP] > > 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. > [!TIP] > > 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: **1. Configure authentication settings** 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. > [!TIP] > > If you are using Privy as your authentication provider, you can skip this step. **2. Generate a keypair to encrypt user keys** 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. **3. Request a user key from the Privy API** 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. **1. Request a user key for a user** 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. **2. Sign the request with the user key** 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. **3. Pass the signature in request headers** 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. **NodeJS** ### 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); } ``` **Java** ### 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().orElse(e.getMessage()); System.err.println(errorBody); } catch (Exception e) { System.err.println(e.getMessage()); } ``` **Go** ### 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 ``` **Ruby** ### 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 ``` **REST API** > [!NOTE] > > 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. > [!TIP] > > 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 **With encryption** 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. > **Show code examples of creating ECH P-256 keypairs** > > **TypeScript** > > ```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}; > } > ``` **Without encryption** If you are requesting user authorization keys without encryption, you can skip this step. #### 2. Request a user's authorization key > [!TIP] > > 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. > **Show request body** > > **With encryption** > > **user\_jwt** `string` *required* > > 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. > > **encryption\_type** `'HPKE` *required* > > The encryption type for the authentication response. Currently only supports HPKE. Omit this field > if you are requesting the authorization key unencrypted. > > **recipient\_public\_key** `string` *required* > > 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. > > **Without encryption** > > **user\_jwt** `string` *required* > > 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. > **Show response body** > > **With encryption** > > **encrypted\_authorization\_key** `object` > > 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`. > > **encryption\_type** `'HPKE'` > > The encryption type used. Currently only supports HPKE. > > **encapsulated\_key** `string` > > Base64-encoded ephemeral public key used in the HPKE encryption process. Required for decryption. > > **ciphertext** `string` > > The encrypted authorization key corresponding to the user's current authentication session. > > **expires\_at** `number` > > The expiration time of the authorization key in seconds since the epoch. > > **wallets** `object[]` > > The wallets that the signer has access to. > > **Without encryption** > > **authorization\_key** `string` > > 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`. > > **expires\_at** `number` > > The expiration time of the authorization key in seconds since the epoch. > > **wallets** `object[]` > > The wallets that the signer has access to. See an example request and successful response below. > **Show example request and response** > > **With encryption** > > 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": "" > } > ] > } > ``` > > **Without encryption** > > 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 **With encryption** 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. > **Show code examples for decrypting the authorization key** > > **TypeScript** > > ```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'))); > ``` **Without encryption** 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. **Python** Add the user's JWT to an authorization context. The SDK exchanges it for a short-lived authorization key and signs the wallet request. ```python theme={"system"} from privy import AuthorizationContext, PrivyRequestOptions request_options = PrivyRequestOptions( authorization_context=AuthorizationContext(user_jwts=[user_jwt]) ) response = client.wallets.ethereum.sign_message( wallet_id, "Hello, Privy!", request_options=request_options, ) ``` # 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. > **[Self-custodial user wallets](/controls/authorization-keys/owners/configuration/user/overview)** > > Create non-custodial user wallets and enable offline actions, server-side transactions, and > more. > **[Programmable controls](/controls/authorization-keys/owners/configuration/programmable)** > > 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: **1. Create a wallet owned by an m-of-k key quorum** 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). **2. Have users and server(s) both sign transaction requests** 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. **3. Execute your transaction request with the user and server signatures** 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: **1. Create a wallet with a user owner** 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. **2. Add an authorization key as an additional signer** 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) **3. Execute scoped transactions with your additional signer** 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. > **[Offline actions](/controls/authorization-keys/owners/configuration/user/offline)** > > Take actions with wallets while users are offline, such as limit orders, agentic trading, and > portfolio rebalancing. > **[Multi-party approvals](/controls/authorization-keys/owners/configuration/user/dual-approval)** > > Require that both users and servers sign transaction requests. > **[Send transactions from your server](/controls/authorization-keys/owners/configuration/user/server-transactions)** > > Send transactions from your server for increased control over transaction flows. > **[Update wallets from your server](/controls/authorization-keys/owners/configuration/user/server-updates)** > > Update the policies and signers assigned to wallets from your server, even when users are > offline. > **[Export wallets from your server](/controls/authorization-keys/owners/configuration/user/server-export)** > > 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: **1. Create a wallet with a 1-of-k key quorum** 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). **2. Export the wallet from your server** 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: **Using Privy as your authentication provider** **1. Create wallets with a user owner** 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. **2. Request a signature over your transaction request** [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. **3. Execute the transaction request from your server** 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. **Using your own authentication provider** **1. Create wallets with a user owner** 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. **2. Request a user key** 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. **3. Sign the request with your user key** 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 **4. Execute the transaction request from your server** 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: **1. Create a wallet with a 1-of-k key quorum** 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). **2. Update the wallet with your authorization key** 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). > [!TIP] > > 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 > [!TIP] > > 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. **Users** 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) **Authorization key** 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) **Key quorums** 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. > [!WARNING] > > 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: **1. Build signature payload** 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. | | | | **2. Canonicalize signature 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. **3. Sign signature payload** 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. **4. Include the signature in request headers** 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. > [!TIP] > > 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. **Vanilla TypeScript** ```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** ```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** ```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** ```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**. > [!TIP] > > 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. > [!NOTE] > > 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. > [!NOTE] > > 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 > [!NOTE] > > 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. **1. Build 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. **2. Pass the authorization context to SDK methods** 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. > **Signing with authorization keys** > > 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"] > ) > ``` > > ```python Python highlight={4} theme={"system"} > from privy import AuthorizationContext > > authorization_context = AuthorizationContext( > authorization_private_keys=["authorization-key"] > ) > ``` > **Signing with users** > > 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"] > ) > ``` > > ```python Python highlight={4} theme={"system"} > from privy import AuthorizationContext > > authorization_context = AuthorizationContext(user_jwts=["user-jwt"]) > ``` > **Signing with a custom sign function** > > 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]) > ``` > > ```python Python theme={"system"} > from privy import AuthorizationContext > > def sign_with_kms(payload: bytes) -> str: > return kms.sign(payload) # Return a base64-encoded P-256 signature. > > authorization_context = AuthorizationContext(signers=[sign_with_kms]) > ``` > > > [!NOTE] > > > > 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. > **Signing with key quorums** > > 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"] > ) > ``` > > ```python Python theme={"system"} > from privy import AuthorizationContext > > authorization_context = AuthorizationContext( > user_jwts=["user-jwt"], > authorization_private_keys=["authorization-key"], > ) > ``` > **Adding signatures directly** > > 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"] > ) > ``` > > ```python Python theme={"system"} > from privy import AuthorizationContext > > authorization_context = AuthorizationContext( > 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 ) ``` ```python Python theme={"system"} from privy import AuthorizationContext, PrivyRequestOptions request_options = PrivyRequestOptions( authorization_context=AuthorizationContext( authorization_private_keys=["authorization-key"] ) ) response = client.wallets.ethereum.sign_message( "insert-wallet-id", "Hello, Privy!", request_options=request_options, ) ``` 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 **1. Format the payload on your server** 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'); ``` **2. Send the binary payload to your client** Send the base64-encoded bytes from your server to the client (e.g., as a JSON response field). **3. Sign the binary payload on the client** Decode the base64 payload and pass the raw bytes to `generateAuthorizationSignature`. **React** ```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); ``` **React Native** ```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** ```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) ``` **Android** ```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 } ``` **Unity** ```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); ``` **4. Send the signature to your backend** Return the base64-encoded signature string to your server. **5. Send the request to the Privy API** 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. **Node SDK** ```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]} }); ``` **REST API** ```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: **TypeScript** ```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** ```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 **version** `Int` *required* The payload version. Currently, only version `1` is supported. **url** `String` *required* The full URL of the API endpoint, including protocol and domain. Should not include a trailing slash. **method** `String` *required* The HTTP method for the request (e.g., `"POST"`, `"PUT"`, `"PATCH"`, `"DELETE"`). **headers** `[String: String]` *required* 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. **body** `T: Encodable` *required* The request body to be serialized and included in the signature. Must conform to `Encodable`. **Android** > [!WARNING] > > 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 **version** `Int` *required* The payload version. Currently, only version `1` is supported. **url** `String` *required* The full URL of the API endpoint, including protocol and domain. Should not include a trailing slash. **method** `String` *required* The HTTP method for the request (e.g., `"POST"`, `"PUT"`, `"PATCH"`, `"DELETE"`). **headers** `Map\` *required* 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. **body** `T: @Serializable` *required* The request body to be serialized and included in the signature. Must be annotated with `@Serializable`. **Flutter** 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 **version** `int` *required* The payload version. Currently, only version `1` is supported. **url** `String` *required* The full URL of the API endpoint, including protocol and domain. Should not include a trailing slash. **method** `String` *required* The HTTP method for the request (e.g., `"POST"`, `"PUT"`, `"PATCH"`, `"DELETE"`). **headers** `Map\` *required* 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. **body** `Map\` *required* The request body as a JSON-serializable map. Serialized automatically before signing. **Unity** 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 **Version** `int` *required* The payload version. Currently, only version `1` is supported. **Url** `string` *required* The full URL of the API endpoint, including protocol and domain. Should not include a trailing slash. **Method** `string` *required* The HTTP method for the request (e.g., `"POST"`, `"PUT"`, `"PATCH"`, `"DELETE"`). **Headers** `Dictionary\` *required* 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. **Body** `object` *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. **React** ```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); ``` **React Native** ```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); ``` **Swift** 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 **signature** `String` The cryptographic signature as a base64-encoded `String` that can be included in Privy API requests as the `privy-authorization-signature` header. **Android** 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 **signature** `String` The cryptographic signature as a base64-encoded `String` that can be included in Privy API requests as the `privy-authorization-signature` header. **Flutter** 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 **signature** `String (inside Result\)` The cryptographic signature as a base64-encoded `String` that can be included in Privy API requests as the `privy-authorization-signature` header. **Unity** 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 **signature** `string` 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?; ``` ```python Python theme={"system"} from privy import ( WalletAPIRequestSignatureInput, format_request_for_authorization_signature, generate_authorization_signature, ) request = WalletAPIRequestSignatureInput( method="POST", url="https://api.privy.io/v1/wallets//rpc", body={ "method": "personal_sign", "params": {"message": "Hello from Privy!", "encoding": "utf-8"}, }, headers={"privy-app-id": "insert-your-app-id"}, ) payload = format_request_for_authorization_signature(request) signature = generate_authorization_signature( "insert-your-base64-der-private-key", payload, ) ``` 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](https://mintcdn.com/privy-c2af3412/zlmLhiIqRR7ViKN0/images/owner-setups/delegating-permissions.png?fit=max\&auto=format\&n=zlmLhiIqRR7ViKN0\&q=85\&s=0988be2d2e851395f0acd27e6d4c2d7b) 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. > **[Owners & signers](/controls/authorization-keys/owners/overview)** > > 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](https://mintcdn.com/privy-c2af3412/zlmLhiIqRR7ViKN0/images/owner-setups/quorum.png?fit=max\&auto=format\&n=zlmLhiIqRR7ViKN0\&q=85\&s=a65b82a9df67965ebc6219a84979ad37) 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. > **[Key quorums](/controls/key-quorum/overview)** > > 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](https://mintcdn.com/privy-c2af3412/zlmLhiIqRR7ViKN0/images/owner-setups/single-party.png?fit=max\&auto=format\&n=zlmLhiIqRR7ViKN0\&q=85\&s=133864e53664a3e7bb126c69ee8dc72c) 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. > **[Configure user-owned wallets](/controls/authorization-keys/owners/configuration/user/overview)** > > 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](https://mintcdn.com/privy-c2af3412/dohNl2t3r3C-HNyO/images/manual-approvals-splash.png?fit=max\&auto=format\&n=dohNl2t3r3C-HNyO\&q=85\&s=30f501a5841d281608b294dd803f9d5e) ### 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. > [!WARNING] > > Review an intent carefully before approving. Approvals cannot be revoked after submission. > [!NOTE] > > 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. > [!TIP] > > 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. > **[Intent lifecycle](/transaction-management/intents/lifecycle)** > > 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. **1. Invite team members to the Privy account** 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. **2. Enroll team members in MFA** 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](https://mintcdn.com/privy-c2af3412/YvGXGsI-T4KAqoan/images/dashboard-mfa-1.png?fit=max\&auto=format\&n=YvGXGsI-T4KAqoan\&q=85\&s=6f5db8f2d38b584fc31c282ded958f6b) **3. Create a key quorum of team members** 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](https://mintcdn.com/privy-c2af3412/_NYQJC6EQbjpQkbE/images/create-key-quorum.png?fit=max\&auto=format\&n=_NYQJC6EQbjpQkbE\&q=85\&s=3617222dff3cb800129efe7784b1ba4c) **4. Assign the quorum as the owner of resources** 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](https://mintcdn.com/privy-c2af3412/_NYQJC6EQbjpQkbE/images/create-wallet-with-owner.png?fit=max\&auto=format\&n=_NYQJC6EQbjpQkbE\&q=85\&s=f6cd08b2de31bc7a676667449620389d) 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](/transaction-management/intents/create/execute-rpc)** > > Propose intents to authorize a transaction or update a wallet or policy. > **[Review intents](/controls/dashboard/approvals)** > > Approve or reject intents in the Privy Dashboard. > **[Intent lifecycle](/transaction-management/intents/lifecycle)** > > 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. > [!NOTE] > > Manual approvals is an Enterprise feature. Reach out to [sales@privy.io](mailto:sales@privy.io) to > request access for your app. > [!TIP] > > 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: **1. Create a group of reviewers** 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. **2. Assign the review group as the owner of a resource** 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. **3. Propose an intent for review** 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. **4. Review and approve or reject the intent** 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](https://mintcdn.com/privy-c2af3412/n0WBs_xVfDqajUiO/images/manual-approvals-overview-diagram.png?fit=max\&auto=format\&n=n0WBs_xVfDqajUiO\&q=85\&s=46b0c335c2ac33fd7334f85b2df6b148) ## Get started Get started with manual approvals using the guides below. > **[Set up reviewers](/controls/dashboard/key-quorum)** > > Create a key quorum of team members who review intents. > **[Propose intents](/transaction-management/intents/create/execute-rpc)** > > Propose intents to authorize a transaction or update a wallet or policy. > **[Review intents](/controls/dashboard/approvals)** > > Approve or reject intents in the Privy Dashboard. > **[Intent lifecycle](/transaction-management/intents/lifecycle)** > > Understand intent statuses from pending to executed. > **[Fetch intent](/transaction-management/intents/fetch-intent)** > > Retrieve intent status and execution results via the API. > **[Intent webhooks](/transaction-management/intents/intent-webhooks)** > > 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. **Dashboard** 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. > [!NOTE] > > Key quorums containing both user IDs and authorization keys must be created via the REST API. ![Dashboard](https://mintcdn.com/privy-c2af3412/YvGXGsI-T4KAqoan/images/authkeys.png?fit=max\&auto=format\&n=YvGXGsI-T4KAqoan\&q=85\&s=e9705db989c192fe5f754f9171182807) **NodeJS** You can create a key quorum using the Node SDK by using the `keyQuorums().create()` method. ### Usage > [!TIP] > > 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. **Java** You can create a key quorum using the Java SDK by using the `keyQuorums().create()` method. ### Usage > [!TIP] > > 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().orElse(e.getMessage()); 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: **publicKeys** `List\` A list of base64-encoded, DER-formatted P-256 public keys to register. **userIds** `List\` A list of user IDs to include in the key quorum. **keyQuorumIds** `List\` 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. **authorizationThreshold** `Double` The minimum number of signatures required to authorize an action. If left unset, the default is all keys. **displayName** `String` 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. **keyQuorum()** `Optional\` The created `KeyQuorum` object. **KeyQuorum** **id** `String` Unique ID for the key quorum, used to assign the `owner_id` to a resource. **authorizationKeys** `List\` The list of authorization keys included in the key quorum. **AuthorizationKey** **publicKey** `String` The public key of the authorization key. **displayName** `String` The display name of the authorization key. **userIds** `List\` The list of user IDs included in the key quorum. **keyQuorumIds** `List\` The list of key quorum IDs nested as members of this key quorum. **authorizationThreshold** `Double` The minimum number of signatures required to authorize an action. If left unset, the default is all keys. **displayName** `String` Human readable display name to attach to the key. **Rust** You can create a key quorum using the Rust SDK by using the `key_quorums().create()` method. ### Usage > [!TIP] > > 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). **Go** To create a key quorum with the Go SDK, use the `New` method on the `KeyQuorums` service. ### Usage > [!TIP] > > 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. **Ruby** To create a key quorum with the Ruby SDK, use the `create` method on the `key_quorums` service. ### Usage > [!TIP] > > 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. **REST API** 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. > **Show request body parameters** > > **public\_keys** `string[]` > > A list of base64-encoded, DER-formatted P-256 public keys to register. > > **user\_ids** `string[]` > > A list of user IDs to include in the key quorum. > > **key\_quorum\_ids** `string[]` > > 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. > > **authorization\_threshold** `number` > > The minimum number of signatures required to authorize an action. If left unset, the default is all keys. > > **display\_name** `string` > > Human readable display name to attach to the key. If the request is successful, Privy will return the following fields in the response. > **Show response body fields** > > **id** `string` > > Unique ID for the key quorum, used to assign the `owner_id` to a resource. > > **authorization\_keys** `\{public_key: string, display_name: string | null\}[]` > > The list of public keys and their display names. > > **user\_ids** `string[]` > > The list of user IDs included in the key quorum. > > **key\_quorum\_ids** `string[]` > > 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. > > **display\_name** `string` > > Human readable display name to attach to the key. > [!TIP] > > 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. > **Show example request to create a key quorum** > > 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 > } > ``` **Python** Use the `create` method on the key quorums service. ```python theme={"system"} from privy import generate_p256_key_pair first_key = generate_p256_key_pair() second_key = generate_p256_key_pair() # Store the private keys securely. You will need them to authorize requests. key_quorum = client.key_quorums.create( authorization_threshold=2, display_name="Treasury quorum", public_keys=[first_key.public_key, second_key.public_key], ) key_quorum_id = key_quorum.id ``` > [!WARNING] > > Privy does not store your private authorization keys and cannot recover them for you. Store them > securely before using their public keys in a key quorum. # Overview Source: https://docs.privy.io/controls/key-quorum/overview Overview of key quorums for multi-party authorization on Privy wallet actions. > [!TIP] > > 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](/controls/key-quorum/create)** > > Create key quorums from authorization keys. > **[Sign requests](/controls/key-quorum/sign)** > > 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. > [!NOTE] > > 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 **REST API** ```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" } } }' ``` **NodeJS** 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); } ``` **Java** 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().orElse(e.getMessage()); System.err.println(errorBody); } catch (Exception e) { System.err.println(e.getMessage()); } ``` **Rust** 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). **Go** 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) ``` **Ruby** 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) ``` **Python** Pass every required quorum credential in an authorization context. ```python theme={"system"} from privy import AuthorizationContext, PrivyRequestOptions request_options = PrivyRequestOptions( authorization_context=AuthorizationContext( authorization_private_keys=[first_private_key, second_private_key] ) ) response = client.wallets.ethereum.sign_message( wallet_id, "Hello, Privy!", request_options=request_options, ) ``` 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](https://mintcdn.com/privy-c2af3412/xvj7sxQ8SLxi5bGo/images/controls-splash.png?fit=max\&auto=format\&n=xvj7sxQ8SLxi5bGo\&q=85\&s=3b55840f66ec24235dc69f6eda861bac) ## 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 for wallet actions. These actions include signature requests, transactions, and key export. The enclave evaluates all policy rules in a tamper-proof environment before signing or exporting key material. Depending on the chain, method, and app configuration, separate compliance screening or transaction simulation may run outside the enclave. These checks are not policy enforcement. * **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](https://mintcdn.com/privy-c2af3412/YvGXGsI-T4KAqoan/images/Policies.png?fit=max\&auto=format\&n=YvGXGsI-T4KAqoan\&q=85\&s=1b18efd9275a116faa8528c2f71c8053) ## 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. > [!TIP] > > * 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. > [!TIP] > > 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. > [!WARNING] > > 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. > [!WARNING] > > 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). > [!TIP] > > 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. **NodeJS** 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: 'Allowlist OUSD', method: 'eth_sendTransaction', action: 'ALLOW', conditions: [ { field_source: 'ethereum_transaction', field: 'to', operator: 'eq', value: '0x20c0000000000000000000006a37da5c996874be' } ] } ], owner_id: 'fmfdj6yqly31huorjqzq38zc' }); ``` **Python** Use the `create` method on the policies service. ```python theme={"system"} policy = client.policies.create( chain_type="ethereum", name="Allow transfers to a known address", version="1.0", rules=[ { "name": "Allow recipient", "method": "eth_sendTransaction", "action": "ALLOW", "conditions": [ { "field_source": "ethereum_transaction", "field": "to", "operator": "eq", "value": "insert-allowed-address", } ], } ], owner_id="insert-key-quorum-id", ) ``` **Java** 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 OUSD transfers Rule allowOusd = Rule.builder() .name("Allowlist OUSD") .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("0x20c0000000000000000000006a37da5c996874be")) .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( allowOusd )) .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().orElse(e.getMessage()); 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: **parameters** **version** `Version` Version of the policy. **name** `String` Name to assign to policy. **chainType** `WalletChainType` Chain type for wallets that the policy will be applied to. **rules** `List\` 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). **owner** `OwnerInput` The owner of the policy. You should specify either an `owner` or an `ownerId`, but not both. **ownerId** `String` 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. **policy()** `Optional\` The created policy. **Policy** **version** `Version` Version of the policy. **name** `String` Name of the policy. **chainType** `WalletChainType` Chain type of the wallets that the policy will be applied to. **id** `String` Unique ID of the policy. **ownerId** `String` The key quorum ID of the owner of the policy. **createdAt** `Double` The Unix time of when the policy was created. **rules** `List\` 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). **Rust** 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 ousd_condition = PolicyRuleCondition { field_source: "ethereum_transaction".to_string(), field: "to".to_string(), operator: "eq".to_string(), value: serde_json::Value::String("0x20c0000000000000000000006a37da5c996874be".to_string()), }; let allow_ousd_rule = PolicyRule { name: "Allowlist OUSD".to_string(), method: "eth_sendTransaction".to_string(), action: PolicyRuleAction::Allow, conditions: vec![ousd_condition], }; let request = CreatePolicyBody { name: "Allowlist certain smart contracts".to_string(), version: "1.0".to_string(), chain_type: "ethereum".to_string(), rules: vec![allow_ousd_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). **Go** 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: "Allowlist OUSD", Method: "eth_sendTransaction", Action: "ALLOW", Conditions: []privy.PolicyNewParamsRuleConditionUnion{ { OfEthereumTransaction: &privy.PolicyNewParamsRuleConditionEthereumTransaction{ Field: "to", Operator: "eq", Value: privy.PolicyNewParamsRuleConditionEthereumTransactionValueUnion{ OfString: privy.String("0x20c0000000000000000000006a37da5c996874be"), }, }, }, }, }, }, }) 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. **Ruby** 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: "Allowlist OUSD", method: "eth_sendTransaction", action: "ALLOW", conditions: [ { field_source: "ethereum_transaction", field: "to", operator: "eq", value: "0x20c0000000000000000000006a37da5c996874be" } ] } ] } ) puts(policy.id) ``` ### Parameters and Returns See the [API reference](/api-reference/policies/create) for more details. **REST API** To create a new policy, make a `POST` request to: ```sh theme={"system"} https://api.privy.io/v1/policies ``` > [!TIP] > > 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: **body attributes** **version** `'1.0'` Version of the policy. Currently, 1.0 is the only version. **name** `string` Name to assign to policy. **chain\_type** `'ethereum'` Chain type for wallets that the policy will be applied to. **rules** `Rule` 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). **owner** `\{public_key: string\} | null` 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. **owner\_id** `string | null` 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. **response** **id** `string` Unique ID for the policy. **version** `'1.0'` Version of the policy. Currently, 1.0 is the only version. **name** `string` Name to assign to policy. **chain\_type** `'ethereum'` Chain type for wallets that the policy will be applied to. **rules** `Rule[]` 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). **owner\_id** `string | null` 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 OUSD", "method": "eth_sendTransaction", "conditions": [ { "field_source": "ethereum_transaction", "field": "to", "operator": "eq", "value": "0x20c0000000000000000000006a37da5c996874be" } ], "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 OUSD", "method": "eth_sendTransaction", "conditions": [ { "field_source": "ethereum_transaction", "field": "to", "operator": "eq", "value": "0x20c0000000000000000000006a37da5c996874be" } ], "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 OUSD address', method: 'eth_sendTransaction', action: 'ALLOW', conditions: [ { field_source: 'ethereum_transaction', field: 'to', operator: 'eq', value: '0x20c0000000000000000000006a37da5c996874be' }, ] }, { name: 'Allowlist for Tempo specifically', method: 'eth_signTypedData_v4', action: 'ALLOW', conditions: [ { field_source: 'ethereum_typed_data_domain', field: 'chainId', operator: 'eq', value: '4217' } ] } ], } ``` ## 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. **Single chain** ```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' }] } ``` **Multiple chains** ```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 OUSD transfers to be less than or equal to some value', method: 'eth_sendTransaction', conditions: [ { field_source: 'ethereum_transaction', field: 'to', operator: 'eq', value: '0x20c0000000000000000000006a37da5c996874be' // OUSD address on Tempo }, { 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 OUSD contract', method: 'eth_sendTransaction', conditions: [ { field_source: 'ethereum_transaction', field: 'to', operator: 'eq', value: '0x20c0000000000000000000006a37da5c996874be' }, ], 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 OUSD 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 OUSD contract', method: 'eth_sendTransaction', conditions: [ { field_source: 'ethereum_transaction', field: 'to', operator: 'eq', value: '0x20c0000000000000000000006a37da5c996874be' }, ], 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. **Allow messages with a specific prefix** ```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' }] } ``` **Restrict message byte length** ```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: '4217' }, { field_source: 'ethereum_typed_data_domain', field: 'verifyingContract', operator: 'eq', value: '0x20c0000000000000000000006a37da5c996874be' } ], action: 'ALLOW' }], } ``` ## Restrict parameters of a typed data message > [!WARNING] > > 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. **Allow messages with a specific prefix** ```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' }] } ``` **Restrict message byte length** ```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' }] } ``` **Deny messages containing specific text** ```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) > [!WARNING] > > 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 * `solana_instruction_data` conditions on `accounts` fields follow the same rule. For example, CCTP transactions commonly load `burn_token_mint` from an ALT, so a condition on `deposit_for_burn.accounts.burn_token_mint` rejects them. Conditions on decoded arguments, such as `deposit_for_burn.args.params.amount`, work 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. # Solana IDL examples Source: https://docs.privy.io/controls/policies/example-policies/solana-idls The `solana_instruction_data` field source decodes instructions for any [Anchor](https://www.anchor-lang.com/) program. Each condition includes the program's [IDL](https://www.anchor-lang.com/docs/basics/idl), the JSON interface that describes its instructions. Policies can then allow or deny instructions based on their decoded arguments and named accounts. > [!WARNING] > > An IDL condition matches any instruction whose data begins with a discriminator from the IDL, > regardless of which program it calls. Anchor derives discriminators from instruction names, so > unrelated programs with a `deposit` instruction share the same discriminator. Always pair IDL > conditions with a `solana_program_instruction` `programId` condition in the same rule. Privy > ignores the IDL's `address` field. ## IDL requirements * Privy only accepts IDLs in the modern Anchor format, introduced in Anchor 0.30. [Learn how to convert a legacy IDL](https://www.anchor-lang.com/docs/updates/release-notes/0-30-1#convert-legacy-idls) * Each IDL can be up to 32 KB, and all IDLs in a policy up to 128 KB combined. Each condition carries its own copy of its IDL. Published IDLs are often larger, so trim them to the instructions the policy uses. Keep each of those instructions complete, with its accounts in their original order, along with every type it uses. * Template variables are not yet supported in `solana_instruction_data` conditions. ## Fields | Field | Description | | - | - | | `instruction_name` | The name of the IDL instruction whose discriminator begins the instruction data. Supports `eq` and `in`. | | `discriminator` | The matched instruction's discriminator as `0x`-prefixed hex. Supports `eq` and `in`. | | `.args.` | A decoded instruction argument, such as `deposit.args.lamports`. | | `.args..` | A field inside a struct argument, such as `deposit_for_burn.args.params.amount`. Paths can continue through nested structs, `Option` values, and type aliases. An empty `Option` doesn't match. | | `.accounts.` | The address at the named account's position in the instruction. Accounts inside an IDL account group use a dotted path, such as `deposit.accounts.group.vault`. | Fields must resolve to one of the types below. Other types, such as enums, vectors, and floats, can appear in an instruction but cannot be compared. | IDL type | Value format | Operators | | - | - | - | | `u8` to `u256`, `i8` to `i256` | Decimal string, such as `'1000000000'`. Compared without rounding. | `eq`, `in`, `gt`, `gte`, `lt`, `lte` | | `bool` | `'true'` or `'false'` | `eq`, `in` | | `string` | Any string. Compared case-sensitively. | `eq`, `in` | | `pubkey` and named accounts | Base58 address | `eq`, `in` | | `bytes` and `[u8; N]` | `0x`-prefixed hex. Compared case-insensitively. `[u8; N]` values must be exactly `N` bytes. | `eq`, `in` | ## Allow Marinade deposits up to a maximum value This policy allows a wallet to stake up to 1 SOL with [Marinade](https://marinade.finance/). It also requires the minted mSOL to go to the wallet's own mSOL token account. The `programId` condition limits the rule to the Marinade program. ```ts {skip-check} theme={"system"} // A trimmed Marinade IDL with only the `deposit` instruction. const marinadeIdl = { instructions: [ { name: 'deposit', discriminator: [242, 35, 198, 137, 82, 225, 242, 182], accounts: [ {name: 'state'}, {name: 'msol_mint'}, {name: 'liq_pool_sol_leg_pda'}, {name: 'liq_pool_msol_leg'}, {name: 'liq_pool_msol_leg_authority'}, {name: 'reserve_pda'}, {name: 'transfer_from'}, {name: 'mint_to'}, {name: 'msol_mint_authority'}, {name: 'system_program'}, {name: 'token_program'} ], args: [{name: 'lamports', type: 'u64'}] } ] }; const policy = { version: '1.0', name: 'Marinade deposits', chain_type: 'solana', rules: [ { name: 'Allow Marinade deposits of up to 1 SOL', method: 'signAndSendTransaction', conditions: [ { field_source: 'solana_program_instruction', field: 'programId', operator: 'eq', value: 'MarBmsSgKXdrN1egZf5sqe1TMai9K1rChYNDJgjq7aD' // Marinade program }, { field_source: 'solana_instruction_data', idl: marinadeIdl, field: 'deposit.args.lamports', operator: 'lte', value: '1000000000' // 1 SOL }, { field_source: 'solana_instruction_data', idl: marinadeIdl, field: 'deposit.accounts.mint_to', operator: 'eq', value: '' // The wallet's mSOL token account } ], action: 'ALLOW' } ] }; ``` ## Restrict USDC bridging to a specific recipient and chain IDL fields can reference fields inside struct arguments. This policy allows Circle [CCTP](https://developers.circle.com/cctp) burns of up to 1,000 USDC, only to a specific recipient on Base. ```ts {skip-check} theme={"system"} // A trimmed CCTP V1 TokenMessengerMinter IDL with only the `deposit_for_burn` instruction. const cctpIdl = { instructions: [ { name: 'deposit_for_burn', discriminator: [215, 60, 61, 46, 114, 55, 128, 176], accounts: [ {name: 'owner'}, {name: 'event_rent_payer'}, {name: 'sender_authority_pda'}, {name: 'burn_token_account'}, {name: 'message_transmitter'}, {name: 'token_messenger'}, {name: 'remote_token_messenger'}, {name: 'token_minter'}, {name: 'local_token'}, {name: 'burn_token_mint'}, {name: 'message_sent_event_data'}, {name: 'message_transmitter_program'}, {name: 'token_messenger_minter_program'}, {name: 'token_program'}, {name: 'system_program'}, {name: 'event_authority'}, {name: 'program'} ], args: [{name: 'params', type: {defined: {name: 'DepositForBurnParams'}}}] } ], types: [ { name: 'DepositForBurnParams', type: { kind: 'struct', fields: [ {name: 'amount', type: 'u64'}, {name: 'destination_domain', type: 'u32'}, {name: 'mint_recipient', type: 'pubkey'} ] } } ] }; const policy = { version: '1.0', name: 'CCTP bridging', chain_type: 'solana', rules: [ { name: 'Allow USDC burns to one recipient on Base', method: 'signAndSendTransaction', conditions: [ { field_source: 'solana_program_instruction', field: 'programId', operator: 'eq', value: 'CCTPiPYPc6AsJuwueEnWgSgucamXDZwBd53dQ11YiKX3' // CCTP TokenMessengerMinter program }, { field_source: 'solana_instruction_data', idl: cctpIdl, field: 'deposit_for_burn.args.params.amount', operator: 'lte', value: '1000000000' // 1,000 USDC (6 decimals) }, { field_source: 'solana_instruction_data', idl: cctpIdl, field: 'deposit_for_burn.args.params.destination_domain', operator: 'eq', value: '6' // Base }, { field_source: 'solana_instruction_data', idl: cctpIdl, field: 'deposit_for_burn.args.params.mint_recipient', operator: 'eq', value: '' } ], action: 'ALLOW' } ] }; ``` # Sui examples Source: https://docs.privy.io/controls/policies/example-policies/sui > [!NOTE] > > 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. **Allow messages with a specific prefix** ```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" } ] } ``` **Restrict message byte length** ```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" } ] } ``` **Allow only specific messages** ```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 > [!NOTE] > > 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. > [!NOTE] > > 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. > [!NOTE] > > 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 **RPC methods** 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" } ] } ``` **Raw signing** ```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) **RPC methods** ```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" } ] } ``` **Raw signing** ```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) **RPC methods** ```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" } ] } ``` **Raw signing** ```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) **RPC methods** ```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" } ] } ``` **Raw signing** ```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) **RPC methods** ```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" } ] } ``` **Raw signing** ```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 **RPC methods** ```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" } ] } ``` **Raw signing** ```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 **RPC methods** ```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" } ] } ``` **Raw signing** ```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 > [!NOTE] > > This is an example of mixing TriggerSmartContract and ABI configurations. **RPC methods** ```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" } ] } ``` **Raw signing** ```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 **RPC methods** ```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" } ] } ``` **Raw signing** ```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 **RPC methods** ```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" } ] } ``` **Raw signing** ```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 > [!NOTE] > > This is an example of mixing TransferContract and System configurations. **RPC methods** ```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" } ] } ``` **Raw signing** ```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 > [!NOTE] > > This is an example of mixing TriggerSmartContract with ABI and System configurations. **RPC methods** ```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" } ] } ``` **Raw signing** ```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" } ] } ``` # XRPL examples Source: https://docs.privy.io/controls/policies/example-policies/xrpl Example XRPL policies for restricting payment destinations, capping amounts, and limiting transaction types XRPL policies evaluate decoded transaction fields signed via `xrpl_signTransaction`. * Your app uses `xrpl_signTransaction` for RPC transaction signing. Rules for this method can use the `xrpl_transaction` field source below. * Your app uses `raw_sign` for raw hash signing. Rules that apply to `raw_sign` requests can only use `system` conditions, since there's no decoded transaction to evaluate. * XRPL policies also apply to the `exportPrivateKey` and `exportSeedPhrase` methods. No other method is supported — `*` is not a valid method for the `xrpl` chain type. The policy engine inspects fields on these transaction types: `Payment`, `OfferCreate`, `OfferCancel`, and `TrustSet`. All conditions use the `xrpl_transaction` field source. Other transaction types can still be signed — field conditions are not available for them, so constrain them with a `TransactionType` condition and rely on the engine's default deny. For wallets that should sign only a known set of transaction types, write `ALLOW` rules scoped to those types and let everything else fall through to the default deny, rather than adding an unconditional `ALLOW`. ## Restrict to Payment transactions only ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "Allow only Payments", "chain_type": "xrpl", "rules": [ { "name": "Restrict to Payment type", "method": "xrpl_signTransaction", "conditions": [ { "field_source": "xrpl_transaction", "field": "TransactionType", "operator": "eq", "value": "Payment" } ], "action": "ALLOW" } ] } ``` ## Allowlist Payment destinations ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "Allowlist destinations", "chain_type": "xrpl", "rules": [ { "name": "Allow payments to specific addresses", "method": "xrpl_signTransaction", "conditions": [ { "field_source": "xrpl_transaction", "field": "TransactionType", "operator": "eq", "value": "Payment" }, { "field_source": "xrpl_transaction", "field": "Payment.Destination", "operator": "in", "value": [ "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe" ] } ], "action": "ALLOW" } ] } ``` ## Cap XRP payment amount XRP amounts are expressed in drops (1 XRP = 1,000,000 drops). Cap native XRP transfers with the `Payment.Amount.drops` field. ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "Cap XRP payments", "chain_type": "xrpl", "rules": [ { "name": "Allow XRP payments under 1000 XRP", "method": "xrpl_signTransaction", "conditions": [ { "field_source": "xrpl_transaction", "field": "TransactionType", "operator": "eq", "value": "Payment" }, { "field_source": "xrpl_transaction", "field": "Payment.Amount.drops", "operator": "lt", "value": "1000000000" } ], "action": "ALLOW" } ] } ``` ## Restrict IOU payments by currency and issuer For issued currency (IOU) payments, constrain the token, issuer, and amount. Use `Payment.Amount.value`, `Payment.Amount.currency`, and `Payment.Amount.issuer`. > [!NOTE] > > `Payment.Amount.drops` and `Payment.Amount.value` are mutually exclusive — a condition using > `.drops` matches only native XRP payments, while `.value` matches only IOU payments. ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "Allow specific IOU payments", "chain_type": "xrpl", "rules": [ { "name": "Allow USD payments from trusted issuer under 10000", "method": "xrpl_signTransaction", "conditions": [ { "field_source": "xrpl_transaction", "field": "TransactionType", "operator": "eq", "value": "Payment" }, { "field_source": "xrpl_transaction", "field": "Payment.Amount.currency", "operator": "eq", "value": "USD" }, { "field_source": "xrpl_transaction", "field": "Payment.Amount.issuer", "operator": "eq", "value": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" }, { "field_source": "xrpl_transaction", "field": "Payment.Amount.value", "operator": "lt", "value": "10000" } ], "action": "ALLOW" } ] } ``` ## Cap OfferCreate amounts Your app can constrain DEX offers with the `OfferCreate.TakerPays` and `OfferCreate.TakerGets` fields. ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "Cap DEX offers", "chain_type": "xrpl", "rules": [ { "name": "Allow offers paying under 500 XRP", "method": "xrpl_signTransaction", "conditions": [ { "field_source": "xrpl_transaction", "field": "TransactionType", "operator": "eq", "value": "OfferCreate" }, { "field_source": "xrpl_transaction", "field": "OfferCreate.TakerPays.drops", "operator": "lt", "value": "500000000" } ], "action": "ALLOW" } ] } ``` ## Restrict TrustSet limits Your app can control which trust lines a wallet establishes and set a maximum amount. ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "Restrict trust lines", "chain_type": "xrpl", "rules": [ { "name": "Allow trust lines to specific issuer with limited amount", "method": "xrpl_signTransaction", "conditions": [ { "field_source": "xrpl_transaction", "field": "TransactionType", "operator": "eq", "value": "TrustSet" }, { "field_source": "xrpl_transaction", "field": "TrustSet.LimitAmount.issuer", "operator": "eq", "value": "rvYAfWj5gh67oV6fW32ZzP3Aw4Eubs59B" }, { "field_source": "xrpl_transaction", "field": "TrustSet.LimitAmount.value", "operator": "lte", "value": "1000000" } ], "action": "ALLOW" } ] } ``` ## Denylist destinations with condition sets Reference a dynamic address list with `in_condition_set`. Manage the list via the [condition sets API](/controls/policies/condition-sets). > [!NOTE] > > The policy engine denies a request by default when no rule matches, so a `DENY` rule needs a > companion `ALLOW` rule or every request is denied. Scope that `ALLOW` to the transaction type the > wallet should sign, as the `Payment` condition below does. `DENY` takes precedence for sanctioned > addresses, legitimate payments fall through to the `ALLOW`, and every other transaction type is > denied by default. > [!WARNING] > > A `Payment.Destination` denylist only screens `Payment` transactions. It does not stop value > reaching the same address through a `CheckCreate`, `EscrowCreate`, or `PaymentChannelCreate`, and > it does not cover account-control transactions such as `SetRegularKey`, `SignerListSet`, > `AccountDelete`, or `DelegateSet`. The engine evaluates only the top-level transaction, so a > `Batch` that wraps a `Payment` is not inspected either. Never pair a denylist with an > unconditional `ALLOW`, which would permit all of these. For agent-controlled wallets, prefer a > default-deny allowlist that permits only the specific transaction types the wallet needs. > [!WARNING] > > Store classic `r`-addresses in condition sets, not X-addresses — an X-address item, tagged or > untagged, never matches, so a sanctions-list `DENY` rule silently fails to catch that address. > Condition sets also can't match on destination tags; use a separate `Payment.DestinationTag` > condition for that. ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "Denylist Payment destinations", "chain_type": "xrpl", "rules": [ { "name": "Deny payments to sanctioned addresses", "method": "xrpl_signTransaction", "conditions": [ { "field_source": "xrpl_transaction", "field": "Payment.Destination", "operator": "in_condition_set", "value": "your-condition-set-id" } ], "action": "DENY" }, { "name": "Allow other Payments", "method": "xrpl_signTransaction", "conditions": [ { "field_source": "xrpl_transaction", "field": "TransactionType", "operator": "eq", "value": "Payment" } ], "action": "ALLOW" } ] } ``` To let the wallet sign other transaction types as well, add a separate scoped `ALLOW` rule for each type the wallet legitimately needs, rather than widening this rule to an unconditional `ALLOW`. ## Only allow signing after a certain date Your app can gate signing on the current timestamp with system conditions. ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "Time-gated XRPL signing", "chain_type": "xrpl", "rules": [ { "name": "Allow signing after launch date", "method": "xrpl_signTransaction", "conditions": [ { "field_source": "system", "field": "current_unix_timestamp", "operator": "gt", "value": "1757304000" } ], "action": "ALLOW" } ] } ``` ## Combine transaction and system conditions ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "Time-gated payments to allowlist", "chain_type": "xrpl", "rules": [ { "name": "Allow payments to approved addresses after launch", "method": "xrpl_signTransaction", "conditions": [ { "field_source": "xrpl_transaction", "field": "TransactionType", "operator": "eq", "value": "Payment" }, { "field_source": "xrpl_transaction", "field": "Payment.Destination", "operator": "in", "value": [ "ra5nK24KXen9AHvsdFTKHSANinZseWnPcX", "rPT1Sjq2YGrBMTttX4GZHjKu9dyfzbpAYe" ] }, { "field_source": "system", "field": "current_unix_timestamp", "operator": "gt", "value": "1757304000" } ], "action": "ALLOW" } ] } ``` ## Deny all signing requests `*` is not a supported method for the `xrpl` chain type. To deny every `xrpl_signTransaction` request, use an empty-conditions `DENY` rule for that method instead. ```ts {skip-check} theme={"system"} { "version": "1.0", "name": "Deny all XRPL signing", "chain_type": "xrpl", "rules": [ { "name": "Deny all signing requests", "method": "xrpl_signTransaction", "conditions": [], "action": "DENY" } ] } ``` ## Supported policy fields > [!NOTE] > > `address` type fields accept both classic `r`-addresses and untagged X-addresses — Privy > normalizes X-addresses to classic format before comparing. Tagged X-addresses are rejected when > the policy is created. To restrict by destination tag, add a separate `Payment.DestinationTag` > condition alongside `Payment.Destination`. | Field | Type | Operators | | :- | :- | :- | | `TransactionType` | string | `eq`, `in`, `in_condition_set` | | `Payment.Destination` | address | `eq`, `in`, `in_condition_set` | | `Payment.DestinationTag` | UInt32 | `eq`, `lt`, `lte`, `gt`, `gte` | | `Payment.Amount.drops` | decimal | `eq`, `lt`, `lte`, `gt`, `gte` | | `Payment.Amount.value` | decimal | `eq`, `lt`, `lte`, `gt`, `gte` | | `Payment.Amount.currency` | string | `eq`, `in`, `in_condition_set` | | `Payment.Amount.issuer` | address | `eq`, `in`, `in_condition_set` | | `Payment.SendMax.drops` | decimal | `eq`, `lt`, `lte`, `gt`, `gte` | | `Payment.SendMax.value` | decimal | `eq`, `lt`, `lte`, `gt`, `gte` | | `Payment.SendMax.currency` | string | `eq`, `in`, `in_condition_set` | | `Payment.SendMax.issuer` | address | `eq`, `in`, `in_condition_set` | | `Payment.DeliverMin.drops` | decimal | `eq`, `lt`, `lte`, `gt`, `gte` | | `Payment.DeliverMin.value` | decimal | `eq`, `lt`, `lte`, `gt`, `gte` | | `Payment.DeliverMin.currency` | string | `eq`, `in`, `in_condition_set` | | `Payment.DeliverMin.issuer` | address | `eq`, `in`, `in_condition_set` | | `OfferCreate.TakerPays.drops` | decimal | `eq`, `lt`, `lte`, `gt`, `gte` | | `OfferCreate.TakerPays.value` | decimal | `eq`, `lt`, `lte`, `gt`, `gte` | | `OfferCreate.TakerPays.currency` | string | `eq`, `in`, `in_condition_set` | | `OfferCreate.TakerPays.issuer` | address | `eq`, `in`, `in_condition_set` | | `OfferCreate.TakerGets.drops` | decimal | `eq`, `lt`, `lte`, `gt`, `gte` | | `OfferCreate.TakerGets.value` | decimal | `eq`, `lt`, `lte`, `gt`, `gte` | | `OfferCreate.TakerGets.currency` | string | `eq`, `in`, `in_condition_set` | | `OfferCreate.TakerGets.issuer` | address | `eq`, `in`, `in_condition_set` | | `OfferCreate.Expiration` | UInt32 | `eq`, `lt`, `lte`, `gt`, `gte` | | `TrustSet.LimitAmount.value` | decimal | `eq`, `lt`, `lte`, `gt`, `gte` | | `TrustSet.LimitAmount.currency` | string | `eq`, `in`, `in_condition_set` | | `TrustSet.LimitAmount.issuer` | address | `eq`, `in`, `in_condition_set` | | `TrustSet.QualityIn` | UInt32 | `eq`, `lt`, `lte`, `gt`, `gte` | | `TrustSet.QualityOut` | UInt32 | `eq`, `lt`, `lte`, `gt`, `gte` | # Get a policy Source: https://docs.privy.io/controls/policies/get-a-policy **NodeJS** 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'); ``` **Python** Use the `get` method on the policies service. ```python theme={"system"} policy = client.policies.get("insert-policy-id") ``` **Java** 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().orElse(e.getMessage()); System.err.println(errorBody); } catch (Exception e) { System.err.println(e.getMessage()); } ``` ### Parameters **id** `String` 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. **policy()** `Optional\` The retrieved policy. **Policy** **version** `Version` Version of the policy. **name** `String` Name of the policy. **chainType** `WalletChainType` Chain type of the wallets that the policy will be applied to. **id** `String` Unique ID of the policy. **ownerId** `String` The key quorum ID of the owner of the policy. **createdAt** `Double` The Unix time of when the policy was created. **rules** `List\` 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). **Rust** 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). **Go** 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. **Ruby** 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. **REST API** 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: **id** `string` Unique ID for the policy. **version** `'1.0'` Version of the policy. Currently, 1.0 is the only version. **name** `string` Name to assign to policy. **chain\_type** `'ethereum'` Chain type for wallets that the policy will be applied to. **rules** `Rule[]` 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). **owner\_id** `string | null` 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 OUSD", "method": "eth_sendTransaction", "conditions": [ { "field_source": "ethereum_transaction", "field": "to", "operator": "eq", "value": "0x20c0000000000000000000006a37da5c996874be" } ], "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](https://mintcdn.com/privy-c2af3412/IQmti1WCL7AzN0e1/images/policy-splash.png?fit=max\&auto=format\&n=IQmti1WCL7AzN0e1\&q=85\&s=c9541babbe140c284d1b52318038ded5) # 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: **child attributes** **version** `'1.0'` Version of the policy. Currently, 1.0 is the only version. **name** `string` Name to assign to policy. **chain\_type** `'ethereum' | 'solana' | 'tron' | 'sui' | 'xrpl'` Chain type for wallets that the policy will be applied to. **rules** `Rule[]` 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 for wallet actions. These actions include signature requests, transactions, and key export. The enclave evaluates all policy rules in a tamper-proof environment before signing or exporting key material. Depending on the chain, method, and app configuration, separate compliance screening or transaction simulation may run outside the enclave. These checks are not policy enforcement. > [!NOTE] > > 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. > [!NOTE] > > 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: **child attributes** **name** `string` Name to assign to the rule. **method** `'eth_sendTransaction' | 'eth_signTransaction' | 'eth_signUserOperation' | 'eth_signTypedData_v4' | 'personal_sign' | 'eth_sign7702Authorization' | 'wallet_sendCalls' | 'signTransaction' | 'signAndSendTransaction' | 'signMessage' | 'exportPrivateKey' | 'exportSeedPhrase' | 'signRawMessageBytes' | 'signTransactionBytes' | 'tron_signTransaction' | 'tron_sendTransaction' | 'xrpl_signTransaction' | 'earn_deposit' | 'earn_withdraw' | 'transfer' | '*'` 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. **conditions** `Condition[]` A set of boolean conditions that define the action the rule allows or denies. For `exportPrivateKey`, leave `conditions` empty. **action** `'ALLOW' | 'DENY'` 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. **child attributes** **field\_source** `'ethereum_transaction' | 'ethereum_calldata' | 'ethereum_typed_data_domain' | 'ethereum_typed_data_message' | 'ethereum_7702_authorization' | 'solana_program_instruction' | 'solana_instruction_data' | 'solana_system_program_instruction' | 'solana_token_program_instruction' | 'tron_transaction' | 'tron_trigger_smart_contract_data' | 'sui_transaction_command' | 'sui_transfer_objects_command' | 'xrpl_transaction' | 'tempo_transaction' | 'message' | 'action_request_body' | 'system' | 'reference'` Data source from which to derive the `field` for the condition. **field** `string` The attribute to evaluate for a wallet request. As an example, the field for the recipient of an EVM transaction is `'to'`. **abi** `JSON` 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. **idl** `JSON` Anchor IDL to decode Solana instruction data against. Should only be set for `'solana_instruction_data'` policies. Must be a modern Anchor IDL (Anchor 0.30 or later) with explicit instruction discriminators, up to 32 KB. Privy ignores the IDL's `address`, so pair the condition with a `programId` condition. See [Solana IDL examples](/controls/policies/example-policies/solana-idls). **operator** `'eq' | 'neq' | 'lt' | 'lte' | 'gt' | 'gte' | 'in' | 'in_condition_set' | 'contains' | 'starts_with' | 'ends_with'` Boolean operator used to compare a `field` with a `value` **value** `string | number | string[]` 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, `solana_instruction_data` conditions require an `idl` parameter used to decode the instruction, 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. > [!TIP] > > 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 OUSD in its > smallest unit. | 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_instruction_data'` | Arguments and named accounts of a custom Solana program instruction, decoded with an [Anchor IDL](https://www.anchor-lang.com/docs/basics/idl). Applies to `signTransaction` and `signAndSendTransaction` requests. Note: `'solana_instruction_data'` conditions must include an `idl` parameter in the modern Anchor format, and should be paired with a `'solana_program_instruction'` `programId` condition. See [Solana IDL examples](/controls/policies/example-policies/solana-idls). | `instruction_name`, `discriminator`, `deposit.args.lamports`, `deposit_for_burn.args.params.amount`, `deposit.accounts.mint_to` | | `'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. > [!TIP] > > 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. > [!TIP] > > Aggregations are associated with the app that creates them. Each aggregation tracks data for RPC > requests made through that app's wallets. > [!NOTE] > > 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. > [!NOTE] > > 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. > [!WARNING] > > 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. > [!TIP] > > 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. > [!WARNING] > > 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: **metric attributes** **field** `string` *required* The field to extract from the request. For example, `'value'` for transaction value. **field\_source** `'ethereum_transaction' | 'ethereum_calldata'` *required* The data source from which to derive the field. Must correspond to the aggregation's `method`. **function** `'sum'` *required* The aggregation function to apply. Currently, only `'sum'` is supported. **abi** `JSON` 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: **window attributes** **type** `'rolling'` *required* The bucketing type. Currently, only `'rolling'` windows are supported. **seconds** `number` *required* 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. **condition attributes** **field\_source** `'ethereum_transaction' | 'ethereum_calldata'` *required* The data source from which to derive the field for the condition. **field** `string` *required* The field to evaluate. For example, `'to'` for the transaction recipient or `'chain_id'` for the chain ID. **operator** `'eq' | 'neq' | 'lt' | 'lte' | 'gt' | 'gte' | 'in' | 'in_condition_set'` *required* The comparison operator to use. The `in_condition_set` operator is supported for aggregation conditions. **value** `string | number | string[]` *required* The value to compare against. Use an array for the `'in'` operator. **abi** `JSON` Contract ABI to decode calldata against. Required when `field_source` is `'ethereum_calldata'`. > [!TIP] > > 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: **group\_by attributes** **field** `string` *required* The field to group by. For example, `'chain_id'`. **field\_source** `string` *required* 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 OUSD transfers per recipient on Tempo This example demonstrates a rolling spending cap that tracks OUSD transfer amounts on a per-recipient basis. It uses conditions to scope the aggregation to only OUSD transfers on Tempo, 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 OUSD `transfer` amounts, filtered to the OUSD contract on Tempo, 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": "0x20c0000000000000000000006a37da5c996874be" }, { "field_source": "ethereum_transaction", "field": "chain_id", "operator": "eq", "value": "4217" } ], "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 OUSD (1000e6 = `0x3B9ACA00` in hex) within the rolling 24-hour window. ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Per-recipient OUSD spending cap on Tempo', chain_type: 'ethereum', rules: [ { name: 'Allow OUSD transfers under 1000 OUSD per recipient per 24h', method: 'eth_signTransaction', conditions: [ { field_source: 'ethereum_transaction', field: 'to', operator: 'eq', value: '0x20c0000000000000000000006a37da5c996874be' // OUSD on Tempo }, { field_source: 'ethereum_transaction', field: 'chain_id', operator: 'eq', value: '4217' // Tempo }, { field_source: 'reference', field: 'aggregation.gml7d33d3i10bf94m5m2o8tc', // replace with your aggregation ID operator: 'lte', value: '0x3B9ACA00' // 1000 OUSD (6 decimals) } ], action: 'ALLOW' } ] } ``` With this configuration, each unique recipient address has its own rolling 24-hour budget of 1000 OUSD. A wallet can send 500 OUSD to address A and 800 OUSD to address B in the same window, since the limits are tracked independently per recipient. ## Recipes > **[Enforce a 24-hour OUSD transfer cap](/recipes/using-stateful-policies)** > > Step-by-step implementation of a rolling OUSD 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' } ] } ``` # Template variables Source: https://docs.privy.io/controls/policies/template-variables Compare a policy condition against the address of the wallet signing the request. # Overview Template variables let a policy condition compare against a value that Privy resolves per request. The policy does not fix that value when it is written. A condition holds a placeholder such as `{{wallet.address}}`, and Privy substitutes the value each time it evaluates the condition. Together, template variables let one set of rules adapt to each request, instead of a separate rule per case. This approach offers several benefits: * **Reusability**: Encode a rule once and reuse the same policy wherever it applies * **Maintainability**: Keep rules stable as the set of resources a policy governs changes * **Scalability**: Cover any number of cases with one rule instead of one rule per case * **Safety**: Privy resolves the value from the request context, so a caller cannot supply its own ## Concepts A **template variable** is a placeholder, written as `{{namespace.field}}`, that a condition value holds in place of a literal. Before evaluating the condition, the policy engine replaces it with a secure value from the request context. The value a rule compares against can therefore differ from one request to the next. Privy supports one template variable today: * **`{{wallet.address}}`** resolves to the address of the wallet signing the request. Privy resolves it per request, so one policy can hold a rule about each wallet's own address. ## Where it can be used `{{wallet.address}}` can be used anywhere a condition compares an address, and inside the content of a message. These operators accept it: | Condition compares | Operators | Notes | | - | - | - | | An address | `eq`, `in` | The variable must be the entire value. With `in`, it can share the array with literal addresses. | | Message content | `eq`, `contains`, `starts_with`, `ends_with`, `in` | The variable can sit inside surrounding text. | The `in_condition_set` operator does not accept a template variable, because its value names a condition set rather than an address. > [!NOTE] > > Aggregation conditions do not accept a template variable yet. See [stateful > policies](/controls/policies/stateful-policies) for what aggregations support. ## Examples ### Restrict an ERC-20 transfer recipient to the signing wallet The recipient argument must decode to the signing wallet's own address, which keeps funds from leaving the wallet that holds them. ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Only transfer to self', chain_type: 'ethereum', rules: [ { name: 'Allow ERC20 transfers back to the signing wallet', method: 'eth_sendTransaction', action: 'ALLOW', conditions: [ { field_source: 'ethereum_calldata', field: 'transfer.recipient', abi: [{ "name": "transfer", "type": "function", "inputs": [ {"name": "recipient", "type": "address"}, {"name": "amount", "type": "uint256"} ] }], operator: 'eq', value: '{{wallet.address}}' } ] } ] } ``` ### Allow the signing wallet alongside fixed addresses The `in` operator accepts template variables and literal addresses in the same array. This rule allows a transaction to the signing wallet or to a treasury address. ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Transfer to self or treasury', chain_type: 'ethereum', rules: [ { name: 'Allow the signing wallet or the treasury', method: 'eth_sendTransaction', action: 'ALLOW', conditions: [ { field_source: 'ethereum_transaction', field: 'to', operator: 'in', value: [ '{{wallet.address}}', '' ] } ] } ] } ``` ### Restrict a transfer action recipient to the signing wallet The [transfer](/wallets/actions/transfer/policies) wallet action evaluates against the request body, so the same constraint applies to `destination.address`. ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Transfer action to self or treasury', chain_type: 'ethereum', rules: [ { name: 'Allow transfers to the wallet or the treasury', method: 'transfer', action: 'ALLOW', conditions: [ { field_source: 'action_request_body', field: 'destination.address', operator: 'in', value: [ '{{wallet.address}}', '' ] }, { field_source: 'action_request_body', field: 'source.chain', operator: 'eq', value: 'tempo' } ] } ] } ``` ### Only sign typed data that names the signing wallet A condition can compare an `address` leaf of an EIP-712 message against the signing wallet. This rule stops a wallet from signing an order on another account's behalf. ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Only sign orders for the signing wallet', chain_type: 'ethereum', rules: [ { name: 'Require the order maker to be the signing wallet', method: 'eth_signTypedData_v4', action: 'ALLOW', conditions: [ { 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'}, ], Order: [ {name: 'maker', type: 'address'}, {name: 'amount', type: 'uint256'}, ], }, primary_type: 'Order', }, field: 'maker', operator: 'eq', value: '{{wallet.address}}' } ] } ] } ``` > [!WARNING] > > An `ethereum_typed_data_message` condition only evaluates when the `types` map in the policy > matches the `types` map in the signing request exactly. See [Restrict parameters of a typed data > message](/controls/policies/example-policies/ethereum#restrict-parameters-of-a-typed-data-message) > for the full requirement. ### Only sign login messages that name the signing wallet The `message` field source compares free-form text, so a template variable can sit inside a longer value. This rule allows a [Sign-In with Ethereum](https://eips.ethereum.org/EIPS/eip-4361) message only when it names the signing wallet. ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Sign-in messages only', chain_type: 'ethereum', rules: [ { name: 'Allow login messages that name the wallet', method: 'personal_sign', action: 'ALLOW', conditions: [ { field_source: 'message', field: 'content', operator: 'starts_with', value: 'example.com wants you to sign in with your Ethereum account:\n{{wallet.address}}' } ] } ] } ``` > [!WARNING] > > String comparisons are case-sensitive. For Ethereum wallets, `{{wallet.address}}` resolves to the EIP-55 > checksummed address that the [wallets API](/api-reference/wallets/get) returns, so a message holding > a lowercase address will not match. Address fields compare EVM addresses without regard to case, so they are > unaffected. ### Only debit the signing wallet on Solana A Solana transaction can carry instructions that move funds from other signers. A condition on `Transfer.from` limits a transaction to debiting the signing wallet. ```ts {skip-check} theme={"system"} { version: '1.0', name: 'Debit the signing wallet only', chain_type: 'solana', rules: [ { name: 'Allow SOL transfers that debit the wallet', method: 'signAndSendTransaction', action: 'ALLOW', conditions: [ { field_source: 'solana_system_program_instruction', field: 'Transfer.from', operator: 'eq', value: '{{wallet.address}}' } ] } ] } ``` ## Next steps > **[Create a policy](/controls/policies/create-a-policy)** > > Create and attach a policy with the Node SDK or the REST API. > **[Conditional policies per signer](/recipes/wallets/conditional-signer-policies)** > > Apply different permissions to different signers on the same wallet. # 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 **NodeJS** 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' }); ``` **Python** Use the `create_rule` method on the policies service to add a rule to a policy. ```python theme={"system"} rule = client.policies.create_rule( "insert-policy-id", policy_create_rule_params={ "name": "Allow list USDT", "method": "eth_sendTransaction", "conditions": [ { "field_source": "ethereum_transaction", "field": "to", "operator": "eq", "value": "insert-token-contract-address", } ], "action": "ALLOW", }, request_options=request_options, ) ``` **Rust** 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). **REST API** 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: **body attributes** **name** `string` Name to assign to the rule. **method** `'personal_sign' | 'eth_signTypedData_v4' | 'eth_signTransaction' | 'eth_sendTransaction' | 'signTransaction' | 'signAndSendTransaction' | '*'` RPC method to apply the `conditions` to. Must correspond to the `chain_type` of the parent policy. **conditions** `Condition[]` A set of boolean conditions that define the action the rule allows or denies. **action** `'ALLOW' | 'DENY'` 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 **NodeJS** 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' }); ``` **Python** Use the `update_rule` method on the policies service to edit a rule in a policy. ```python theme={"system"} rule = client.policies.update_rule( "insert-rule-id", policy_update_rule_params={ "policy_id": "insert-policy-id", "name": "Allow list USDT", "method": "eth_sendTransaction", "conditions": [ { "field_source": "ethereum_transaction", "field": "to", "operator": "eq", "value": "insert-token-contract-address", } ], "action": "ALLOW", }, request_options=request_options, ) ``` **Rust** 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). **REST API** 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: **body attributes** **name** `string` Name to assign to the rule. **method** `'personal_sign' | 'eth_signTypedData_v4' | 'eth_signTransaction' | 'eth_sendTransaction' | 'signTransaction' | 'signAndSendTransaction' | '*'` RPC method to apply the `conditions` to. Must correspond to the `chain_type` of the parent policy. **conditions** `Condition[]` A set of boolean conditions that define the action the rule allows or denies. **action** `'ALLOW' | 'DENY'` 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 **NodeJS** 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' }); ``` **Python** Use the `delete_rule` method on the policies service to delete a rule from a policy. ```python theme={"system"} client.policies.delete_rule( "insert-rule-id", policy_id="insert-policy-id", request_options=request_options, ) ``` **Rust** 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). **REST API** 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 **NodeJS** 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' } ] } ] }); ``` **Python** Use the `update` method on the policies service. Include authorization for an owned policy. ```python theme={"system"} policy = client.policies.update( "insert-policy-id", policy_update_params={"name": "Updated policy"}, request_options=request_options, ) ``` **Java** You can update a policy using the Java SDK by using the `policies().update()` method. > [!TIP] > > 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().orElse(e.getMessage()); 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** `String` Name to assign to policy. **chainType** `WalletChainType` Chain type for wallets that the policy will be applied to. **rules** `List\` 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). **owner** `OwnerInput` The owner of the policy. **ownerId** `String` 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. **policy()** `Optional\` The updated policy. **Policy** **version** `Version` Version of the policy. **name** `String` Name of the policy. **chainType** `WalletChainType` Chain type of the wallets that the policy will be applied to. **id** `String` Unique ID of the policy. **ownerId** `String` The key quorum ID of the owner of the policy. **createdAt** `Double` The Unix time of when the policy was created. **rules** `List\` 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). **Rust** 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). **Go** 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. **Ruby** 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. **REST API** 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. > [!TIP] > > 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: **name** `string` (Optional) New name to assign to policy. **rules** `Rule` (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). **owner** `\{public_key: string\} | null` 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. **owner\_id** `string | null` 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. **id** `string` Unique ID for the policy. **version** `'1.0'` Version of the policy. Currently, 1.0 is the only version. **name** `string` Updated name of the policy. **chain\_type** `'ethereum'` Chain type for wallets that the policy will be applied to. **rules** `Rule` 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). **owner\_id** `string | null` 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](https://mintcdn.com/privy-c2af3412/7cXr-D-UUALqEUgn/images/card-overview.png?fit=max\&auto=format\&n=7cXr-D-UUALqEUgn\&q=85\&s=3a5c20064f075e3d27e226e4839d76f4) 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. > [!NOTE] > > [Prebuilt components](/financial-flows/cards/pre-built-components/overview) work for any > authenticated Privy user, including users authenticated with [custom JWT > authentication](/authentication/user-authentication/jwt-based-auth/overview). ## Spend from vault balances Approved card programs can spend from [Earn](/wallets/actions/earn/overview) vault balances. Bridge redeems vault shares just-in-time to fund card purchases. Follow [Spend from vault balances](/financial-flows/cards/pre-built-components/spend-from-vault) for program prerequisites, share approval, and balance display. ## Choose your integration Privy supports two paths for launching a card program. Prebuilt components offer a managed path designed for faster time to market, while direct API integrations give your team more control over the customer experience, operations, and program economics. The right approach depends on how much of the program your team wants to own today and as you scale. | | Prebuilt components | API integration | | - | - | - | | **Best for** | Teams prioritizing speed to market with a managed consumer card program | Teams that want greater control over their card program and customer experience | | **How you build** | Prebuilt UI components in the Privy React and React Native SDKs | Stripe Issuing and Bridge APIs | | **Cardholder experience** | Pre-approved flows and UI provided by Privy | Custom UX and branding built by your app | | **Customer relationship** | Card servicing and support managed by Stripe | Your team can own the end-customer relationship and support experience | | **Compliance** | Powered by Stripe | Powered by Stripe or your in-house compliance team | | **Economics** | Designed to make launching a program economical from day one | Variable economics designed to support programs as they scale | | **Operations** | Core operational workflows managed for you | Greater ownership and flexibility across program operations | | **As you grow** | Designed with a path to bring more functions in-house over time | Full control over the program from the outset | > [!TIP] > > **Not sure which path to choose?** Prebuilt components are designed for teams that want to get to > market quickly without building the operational stack themselves. If owning your customer > experience, support model, program economics, or other aspects of the card program is important > from day one, a direct API integration may be a better fit. Teams can also start with prebuilt > components and bring more of the experience in-house as they grow. > **[Prebuilt components](/financial-flows/cards/pre-built-components/overview)** > > Embed bank-approved card UI with the React or React Native SDK. > **[API integration](/financial-flows/cards/integration-guide)** > > Issue cards server-side with Stripe Issuing and Bridge. ## Get started Prebuilt components are self-serve. Integrate them with the React or React Native SDK and test in sandbox, then contact [sales@privy.io](mailto:sales@privy.io) for production access. Server-side API integrations begin with 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 > **[Complete KYC for a user](/kyc-kyb/kyc)** > > Set up Bridge and verify users before issuing cards. > **[Bridge + Stripe Issuing documentation](https://apidocs.bridge.xyz/platform/cards/overview/stripe-issuing)** > > 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. > [!WARNING] > > 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. **Solana** > [!WARNING] > > 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" ``` **EVM** ```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" ``` Set `crypto_wallet[currency]` to the stablecoin the card spends. To issue an OUSD card, set it to `ousd`. OUSD cards are available on chains where OUSD is deployed, including Tempo, Base, and Solana. On Solana, Bridge submits an onchain transaction to register the program delegate address for the wallet after the card is created. > [!NOTE] > > 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). **Solana** ## 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. > [!NOTE] > > OUSD on Solana (`ousd2mJsPEckLHcSCDxyKD7NDGARZcfLbDZkKiatYHB`) is a Token-2022 mint. For an > OUSD card, set `MINT_PUBKEY` to the OUSD mint and replace `TOKEN_PROGRAM_ID` with > `TOKEN_2022_PROGRAM_ID` in every call below, including the approval instruction. ```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. **React** 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 ; } ``` **NodeJS** 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); ``` **Python** Use the [Wallet API](/wallets/using-wallets/solana/send-a-transaction) to sign and send the transaction server-side: ```python theme={"system"} response = client.wallets.solana.sign_and_send_transaction( "insert-wallet-id", transaction.serialize(require_all_signatures=False), caip2="solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", sponsor=True, ) print("Approval tx:", response.hash) ``` The result is a delegate approval resembling [this transaction](https://solscan.io/tx/5o1aAuCFpmB9sEt1djX9PDENGmkNEitUYoqg93a9JLdvQ1pZbFX5ChkVFULq38aArBCg9sQTREQt9fTFr1puPJ1H). **EVM** Submit a standard ERC-20 `approve` call to allow the Bridge issuer contract to spend from the wallet. > [!WARNING] > > 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 **React** 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 ; } ``` **NodeJS** 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); ``` **Python** Use the [Wallet API](/wallets/using-wallets/ethereum/send-a-transaction) to send the transaction server-side: ```python theme={"system"} response = client.wallets.ethereum.send_transaction( "insert-wallet-id", caip2="eip155:480", # World Chain params={ "transaction": { "to": token_address, "data": approve_data, } }, ) print("Approval tx:", response.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. > [!NOTE] > > 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: **Solana** ```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" } ] } } ``` **EVM** ```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: **Solana** ```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" } ] } } ``` **EVM** ```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" } ] } } ``` > [!NOTE] > > 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: > **Block withdrawals with a policy** > > 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. > **Track balances with onchain bookkeeping** > > 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. > **Validate requests with cookies** > > [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](https://mintcdn.com/privy-c2af3412/7cXr-D-UUALqEUgn/images/card-overview.png?fit=max\&auto=format\&n=7cXr-D-UUALqEUgn\&q=85\&s=3a5c20064f075e3d27e226e4839d76f4) 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. > [!NOTE] > > [Prebuilt components](/financial-flows/cards/pre-built-components/overview) work for any > authenticated Privy user, including users authenticated with [custom JWT > authentication](/authentication/user-authentication/jwt-based-auth/overview). ## Spend from vault balances Approved card programs can spend from [Earn](/wallets/actions/earn/overview) vault balances. Bridge redeems vault shares just-in-time to fund card purchases. Follow [Spend from vault balances](/financial-flows/cards/pre-built-components/spend-from-vault) for program prerequisites, share approval, and balance display. ## Choose your integration Privy supports two paths for launching a card program. Prebuilt components offer a managed path designed for faster time to market, while direct API integrations give your team more control over the customer experience, operations, and program economics. The right approach depends on how much of the program your team wants to own today and as you scale. | | Prebuilt components | API integration | | - | - | - | | **Best for** | Teams prioritizing speed to market with a managed consumer card program | Teams that want greater control over their card program and customer experience | | **How you build** | Prebuilt UI components in the Privy React and React Native SDKs | Stripe Issuing and Bridge APIs | | **Cardholder experience** | Pre-approved flows and UI provided by Privy | Custom UX and branding built by your app | | **Customer relationship** | Card servicing and support managed by Stripe | Your team can own the end-customer relationship and support experience | | **Compliance** | Powered by Stripe | Powered by Stripe or your in-house compliance team | | **Economics** | Designed to make launching a program economical from day one | Variable economics designed to support programs as they scale | | **Operations** | Core operational workflows managed for you | Greater ownership and flexibility across program operations | | **As you grow** | Designed with a path to bring more functions in-house over time | Full control over the program from the outset | > [!TIP] > > **Not sure which path to choose?** Prebuilt components are designed for teams that want to get to > market quickly without building the operational stack themselves. If owning your customer > experience, support model, program economics, or other aspects of the card program is important > from day one, a direct API integration may be a better fit. Teams can also start with prebuilt > components and bring more of the experience in-house as they grow. > **[Prebuilt components](/financial-flows/cards/pre-built-components/overview)** > > Embed bank-approved card UI with the React or React Native SDK. > **[API integration](/financial-flows/cards/integration-guide)** > > Issue cards server-side with Stripe Issuing and Bridge. ## Get started Prebuilt components are self-serve. Integrate them with the React or React Native SDK and test in sandbox, then contact [sales@privy.io](mailto:sales@privy.io) for production access. Server-side API integrations begin with a guided Privy and Bridge onboarding. Reach out to [sales@privy.io](mailto:sales@privy.io) to get started. # Open cards in a mobile WebView Source: https://docs.privy.io/financial-flows/cards/pre-built-components/mobile-webview Open the cards React experience in a WebView on iOS and Android Card prebuilt components ship in the Privy React SDK. iOS and Android apps open a hosted copy of that experience in a WebView. Host the page from the [React integration](/financial-flows/cards/pre-built-components/react-integration) on a domain in the app's [allowed origins](https://dashboard.privy.io/apps?setting=domains\&page=settings). Use the same Privy app ID as the mobile app. The WebView has its own Privy session so the user signs in on the hosted page. ## How it works 1. The user opens cards in the native app. 2. The app loads the hosted React page in a WebView. 3. The user signs in on that page when the WebView has no Privy session. 4. The page runs card signup and displays the card summary. ## Render the card views For the best experience, render the card views full screen in the WebView. > [!NOTE] > > Host the page on HTTPS. Add that origin in the [Privy > Dashboard](https://dashboard.privy.io/apps?setting=domains\&page=settings) before the WebView loads > it. ## Handling window popups Card signup calls `window.open` for provider terms and identity verification. Return that window to the page, and release it on close so the page can read `closed`. **iOS** ```swift theme={"system"} func webView( _ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures ) -> WKWebView? { guard navigationAction.targetFrame == nil else { return nil } let popup = WKWebView(frame: view.bounds, configuration: configuration) popup.uiDelegate = self view.addSubview(popup) return popup } func webViewDidClose(_ webView: WKWebView) { webView.removeFromSuperview() } ``` **Android** ```kotlin theme={"system"} settings.javaScriptCanOpenWindowsAutomatically = true settings.setSupportMultipleWindows(true) override fun onCreateWindow( view: WebView, isDialog: Boolean, isUserGesture: Boolean, resultMsg: Message ): Boolean { val popup = WebView(view.context) val transport = resultMsg.obj as WebView.WebViewTransport transport.webView = popup resultMsg.sendToTarget() return true } override fun onCloseWindow(window: WebView) { window.destroy() } ``` ## Open the hosted page The app loads the hosted React page in the WebView. On Android, enable JavaScript, DOM storage, and third-party cookies, and match the parent height so the cards layout keeps its size. **iOS** ```swift theme={"system"} let config = WKWebViewConfiguration() config.allowsInlineMediaPlayback = true let webView = WKWebView(frame: view.bounds, configuration: config) webView.uiDelegate = self view.addSubview(webView) if let url = URL(string: "https://your-domain.com/cards") { webView.load(URLRequest(url: url)) } ``` **Android** ```kotlin theme={"system"} val webView = WebView(this).apply { layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) settings.javaScriptEnabled = true settings.domStorageEnabled = true CookieManager.getInstance().setAcceptThirdPartyCookies(this, true) } webView.loadUrl("https://your-domain.com/cards") ``` ## Handling webview permissions Identity verification uses the camera and microphone. Declare that access, then grant the capture request to the WebView. **iOS** Add these keys to `Info.plist`. ```xml theme={"system"} NSCameraUsageDescription This app uses the camera to verify the cardholder's identity. NSMicrophoneUsageDescription This app uses the microphone to verify the cardholder's identity. ``` ```swift theme={"system"} func webView( _ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin, initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType, decisionHandler: @escaping (WKPermissionDecision) -> Void ) { decisionHandler(.grant) } ``` **Android** Add these permissions to `AndroidManifest.xml`. ```xml theme={"system"} ``` ```kotlin theme={"system"} registerForActivityResult(ActivityResultContracts.RequestMultiplePermissions()) { granted -> if (granted.values.all { it }) webView.loadUrl(cardPageUrl) }.launch(arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO)) override fun onPermissionRequest(request: PermissionRequest) { request.grant(request.resources) } ``` ## Complete example **iOS** ```swift theme={"system"} import UIKit import WebKit class CardViewController: UIViewController, WKUIDelegate { private var webView: WKWebView! private var popupWebView: WKWebView? private let cardPageUrl = "https://your-domain.com/cards" private let popupCloseButton: UIButton = { let button = UIButton(type: .system) button.setTitle("Close", for: .normal) button.isHidden = true return button }() override func viewDidLoad() { super.viewDidLoad() let config = WKWebViewConfiguration() config.allowsInlineMediaPlayback = true webView = WKWebView(frame: view.bounds, configuration: config) webView.uiDelegate = self webView.autoresizingMask = [.flexibleWidth, .flexibleHeight] view.addSubview(webView) popupCloseButton.addTarget(self, action: #selector(closePopup), for: .touchUpInside) popupCloseButton.frame = CGRect(x: 16, y: 16, width: 72, height: 32) view.addSubview(popupCloseButton) if let url = URL(string: cardPageUrl) { webView.load(URLRequest(url: url)) } } func webView( _ webView: WKWebView, createWebViewWith configuration: WKWebViewConfiguration, for navigationAction: WKNavigationAction, windowFeatures: WKWindowFeatures ) -> WKWebView? { guard navigationAction.targetFrame == nil else { return nil } popupWebView?.removeFromSuperview() // Use this configuration so JavaScript receives the new window. let popup = WKWebView(frame: view.bounds, configuration: configuration) popup.uiDelegate = self popup.autoresizingMask = [.flexibleWidth, .flexibleHeight] view.addSubview(popup) popupWebView = popup popupCloseButton.isHidden = false view.bringSubviewToFront(popupCloseButton) return popup } func webViewDidClose(_ webView: WKWebView) { guard webView == popupWebView else { return } webView.removeFromSuperview() popupWebView = nil popupCloseButton.isHidden = true } @objc private func closePopup() { popupWebView?.removeFromSuperview() popupWebView = nil popupCloseButton.isHidden = true } func webView( _ webView: WKWebView, requestMediaCapturePermissionFor origin: WKSecurityOrigin, initiatedByFrame frame: WKFrameInfo, type: WKMediaCaptureType, decisionHandler: @escaping (WKPermissionDecision) -> Void ) { decisionHandler(.grant) } } ``` **Android** ```kotlin theme={"system"} import android.Manifest import android.annotation.SuppressLint import android.content.pm.PackageManager import android.os.Bundle import android.os.Message import android.view.ViewGroup.LayoutParams.MATCH_PARENT import android.webkit.CookieManager import android.webkit.PermissionRequest import android.webkit.WebChromeClient import android.webkit.WebView import android.webkit.WebViewClient import android.widget.FrameLayout import androidx.activity.OnBackPressedCallback import androidx.activity.result.contract.ActivityResultContracts import androidx.appcompat.app.AppCompatActivity import androidx.core.content.ContextCompat class CardActivity : AppCompatActivity() { private val cardPageUrl = "https://your-domain.com/cards" private lateinit var root: FrameLayout private var webView: WebView? = null private var popupWebView: WebView? = null private var popupLayer: FrameLayout? = null private val requestCapture = registerForActivityResult( ActivityResultContracts.RequestMultiplePermissions() ) { webView?.loadUrl(cardPageUrl) } private val backCallback = object : OnBackPressedCallback(true) { override fun handleOnBackPressed() { when { popupWebView != null -> dismissPopup(popupWebView) webView?.canGoBack() == true -> webView?.goBack() else -> { isEnabled = false onBackPressedDispatcher.onBackPressed() } } } } @SuppressLint("SetJavaScriptEnabled") override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) onBackPressedDispatcher.addCallback(this, backCallback) root = FrameLayout(this) setContentView(root) val wv = newWebView(onCreatePopup = { resultMsg -> val popup = newWebView(onCreatePopup = null, onClosePopup = { dismissPopup(popupWebView) }) showPopup(popup) val transport = resultMsg.obj as WebView.WebViewTransport transport.webView = popup resultMsg.sendToTarget() }) webView = wv root.addView(wv, FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)) val needed = arrayOf(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO) val missing = needed.any { ContextCompat.checkSelfPermission(this, it) != PackageManager.PERMISSION_GRANTED } if (missing) { requestCapture.launch(needed) } else { wv.loadUrl(cardPageUrl) } } override fun onResume() { super.onResume() webView?.onResume() popupWebView?.onResume() } override fun onPause() { webView?.onPause() popupWebView?.onPause() super.onPause() } @SuppressLint("SetJavaScriptEnabled") private fun newWebView( onCreatePopup: ((Message) -> Unit)?, onClosePopup: (() -> Unit)? = null ): WebView { return WebView(this).apply { layoutParams = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT) settings.javaScriptEnabled = true settings.domStorageEnabled = true settings.javaScriptCanOpenWindowsAutomatically = true settings.setSupportMultipleWindows(true) settings.mediaPlaybackRequiresUserGesture = false CookieManager.getInstance().setAcceptThirdPartyCookies(this, true) webViewClient = WebViewClient() webChromeClient = object : WebChromeClient() { override fun onPermissionRequest(request: PermissionRequest) { request.grant(request.resources) } override fun onCreateWindow( view: WebView, isDialog: Boolean, isUserGesture: Boolean, resultMsg: Message ): Boolean { val create = onCreatePopup ?: return false create(resultMsg) return true } override fun onCloseWindow(window: WebView) { onClosePopup?.invoke() } } } } private fun showPopup(popup: WebView) { dismissPopup(popupWebView) popup.onResume() val layer = FrameLayout(this) layer.addView(popup, FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)) root.addView(layer, FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)) popupLayer = layer popupWebView = popup } private fun dismissPopup(popup: WebView?) { val layer = popupLayer ?: return root.removeView(layer) popupLayer = null popupWebView = null popup?.destroy() } } ``` # Cards prebuilt 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 and React Native SDKs Prebuilt components are embeddable UI components that let your app offer stablecoin-funded consumer cards. The components are delivered through the Privy React and React Native SDKs. They cover onboarding and disclosures, identity verification, balances, transaction history, card management, and customer support. Prebuilt components let your team: * **Launch in as little as 1 month** with prebuilt, customizable UI components and card art. * **Stay compliant.** Onboarding, disclosures, statements, and support are handled for you. ## What's included Prebuilt 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. **1. Onboarding and disclosures** The cardholder accepts the required terms and long-form bank disclosures before a card is issued. **2. Identity verification** The cardholder completes KYC, including identity document and selfie verification, inside your app. **3. Card issuance** The cardholder approves the onchain spend contract for their wallet, and a virtual card is issued. **4. Balances and transactions** The cardholder views their available balance, browses transaction history, and opens individual transactions for merchant, amount, and fee details. **5. Management and support** The cardholder freezes, replaces, or closes the card, downloads statements, and reaches managed support to file disputes. ## Get started > **[Setup](/financial-flows/cards/pre-built-components/setup)** > > Connect your Stripe and Bridge accounts. > **[React integration](/financial-flows/cards/pre-built-components/react-integration)** > > Integrate cards prebuilt components with the Privy React SDK. > **[React Native integration](/financial-flows/cards/pre-built-components/react-native-integration)** > > Integrate card signup and management with the Privy React Native 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 prebuilt 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. For an approved Earn-backed card program, follow [Spend from vault balances](/financial-flows/cards/pre-built-components/spend-from-vault). The same hooks and components handle vault-share approval and available balances. > [!NOTE] > > `useSignUpForCard` works for any authenticated Privy user, including users authenticated with > [custom JWT authentication](/authentication/user-authentication/jwt-based-auth/overview). ## Install and import the SDK > [!NOTE] > > Card prebuilt 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. ## Choose the card asset Each card spends one stablecoin from its funding wallet. Pass the stablecoin's asset ID as `asset` when calling `signUp`. Every supported chain has a default asset, and some chains also accept a second one. | Network | Chain ID | Default asset | Also accepts | | - | - | - | - | | Tempo | `eip155:4217` | `'ousd'` | `'pathusd'` | | Ethereum | `eip155:1` | `'ousd'` | `'usdc'` | | Base | `eip155:8453` | `'ousd'` | `'usdc'` | | Solana | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | `'ousd'` | `'usdc'` | | Optimism | `eip155:10` | `'usdc'` | None | | Polygon | `eip155:137` | `'usdc'` | None | | Arbitrum | `eip155:42161` | `'usdc'` | None | | Avalanche C-Chain | `eip155:43114` | `'usdc'` | None | | Tempo Moderato (sandbox) | `eip155:42431` | `'ousd'` | `'pathusd'` | The other sandbox networks, such as Base Sepolia and Solana devnet, accept only `'usdc'`. To test OUSD cards in sandbox, use Tempo Moderato. > [!NOTE] > > OUSD cards require `@privy-io/react-auth` version 3.49.1 or later. A wallet can hold one open card per chain and asset. For example, the same Tempo wallet can fund both a PathUSD card and an OUSD card. `signUp` rejects when `asset` is not available on `chainId`. ## 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 OUSD. To issue a PathUSD card instead, set `CARD_ASSET` to `'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 CARD_CHAIN_ID = 'eip155:42431'; // Tempo Moderato const CARD_ASSET = 'ousd'; // or 'pathusd' 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.chain_id === CARD_CHAIN_ID && x.asset === CARD_ASSET && (x.status === 'active' || x.status === 'inactive') ); if (existing) { setCardId(existing.id); return; } const {id} = await signUp({ environment: 'sandbox', walletId, chainId: CARD_CHAIN_ID, asset: CARD_ASSET }); 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.chain_id === chainId && x.asset === asset && (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 that matches the funding wallet, chain, and asset. > [!WARNING] > > 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. On Tempo Moderato, the built-in target approves OUSD when `asset` is `'ousd'` and PathUSD otherwise. 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. For an OUSD card, set `stablecoinAddress` to the OUSD contract on the card's chain, or to the OUSD mint on Solana: | Network | OUSD address | | - | - | | Tempo | `0x20c0000000000000000000006a37DA5C996874BE` | | Ethereum | `0x9f6F3991D525015a6F8CaF062C83b62fD3AC4436` | | Base | `0xB2000000000000000000002fEb517dFeC7415344` | | Solana | `ousd2mJsPEckLHcSCDxyKD7NDGARZcfLbDZkKiatYHB` | The Solana OUSD mint uses the Token-2022 program. The SDK derives the token account and builds the approval under Token-2022 automatically, so no extra configuration is needed. **EVM** 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: 'pathusd', // or 'ousd' spendApproval: { stablecoinAddress: process.env.NEXT_PUBLIC_CARD_STABLECOIN_ADDRESS!, spenderAddress: process.env.NEXT_PUBLIC_CARD_SPENDER_ADDRESS!, }, }); setCardId(id); }; ``` **Solana** 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', // or 'ousd' 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` | `'usdc' \| 'pathusd' \| 'ousd'` | Stablecoin the card spends. Must be available on `chainId`. See [Choose the card asset](#choose-the-card-asset). | | `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. | > [!WARNING] > > 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 React Native integration Source: https://docs.privy.io/financial-flows/cards/pre-built-components/react-native-integration Integrate stablecoin card signup and management with the Privy React Native SDK Use `useSignUpForCard` to open card onboarding, `useGetCardsForUser` to find existing cards, and `CardSummaryView` to display and manage a card. Import these APIs from `@privy-io/expo/cards`. Card signup runs in the `PrivyElements` modal. `CardSummaryView` renders inside the app's own screen. For an approved Earn-backed card program, follow [Spend from vault balances](/financial-flows/cards/pre-built-components/spend-from-vault). The same hooks and components handle vault-share approval and available balances. > [!NOTE] > > `useSignUpForCard` works for any authenticated Privy user, including users authenticated with > [custom JWT authentication](/authentication/user-authentication/jwt-based-auth/overview). ## Set up the SDK Complete the [cards setup](/financial-flows/cards/pre-built-components/setup), [React Native SDK installation](/basics/react-native/installation), and [Privy UI setup](/basics/react-native/advanced/setup-privyelements), including the UI dependencies and fonts. Use `@privy-io/expo` version **0.76.4 or later** for signup, card management, and card-detail reveal: ```bash theme={"system"} pnpm exec expo install @privy-io/expo@latest ``` Mount `PrivyElements` once inside `PrivyProvider`, alongside the app's navigation. Use the app ID and mobile client ID from the [Privy dashboard](/basics/get-started/dashboard/app-clients). ```tsx theme={"system"} import {PrivyProvider} from '@privy-io/expo'; import {PrivyElements} from '@privy-io/expo/ui'; import {Slot} from 'expo-router'; export default function RootLayout() { return ( ); } ``` Authenticate the user and [create an embedded Ethereum wallet](/wallets/wallets/create/create-a-wallet) before starting signup. The wallet must be connected to approve spending. Enable [gas sponsorship](/wallets/gas-and-asset-management/gas/setup) for the funding chain, or fund the wallet with the chain's gas token. ## Choose the card asset Each card spends one stablecoin from its funding wallet. Pass the stablecoin's asset ID as `asset` when calling `signUp`. Every supported chain has a default asset, and some chains also accept a second one. | Network | Chain ID | Default asset | Also accepts | | - | - | - | - | | Tempo | `eip155:4217` | `'ousd'` | `'pathusd'` | | Ethereum | `eip155:1` | `'ousd'` | `'usdc'` | | Base | `eip155:8453` | `'ousd'` | `'usdc'` | | Optimism | `eip155:10` | `'usdc'` | None | | Polygon | `eip155:137` | `'usdc'` | None | | Arbitrum | `eip155:42161` | `'usdc'` | None | | Avalanche C-Chain | `eip155:43114` | `'usdc'` | None | | Tempo Moderato (sandbox) | `eip155:42431` | `'ousd'` | `'pathusd'` | Solana-funded cards are available in [React](/financial-flows/cards/pre-built-components/react-integration#choose-the-card-asset). To test OUSD cards in sandbox, use Tempo Moderato. A wallet can hold one open card per chain and asset. For example, the same Tempo wallet can fund both a PathUSD card and an OUSD card. `signUp` rejects when `asset` is not available on `chainId`. ## Add the card flow The example below checks every page for an existing open card before starting signup. It uses Tempo Moderato and PathUSD in sandbox. ```tsx theme={"system"} import {getAllUserEmbeddedEthereumWallets, usePrivy} from '@privy-io/expo'; import {CardSummaryView, useGetCardsForUser, useSignUpForCard} from '@privy-io/expo/cards'; import {useState} from 'react'; import {Button, Text, View} from 'react-native'; export default function CardsScreen() { const {isReady, user} = usePrivy(); const {signUp, isOpen} = useSignUpForCard(); const {getCardsForUser} = useGetCardsForUser(); const [cardId, setCardId] = useState(null); const [isLoading, setIsLoading] = useState(false); const [error, setError] = useState(null); const walletId = getAllUserEmbeddedEthereumWallets(user)[0]?.id; const openCard = async (resumeSetup = false) => { if (!isReady || !walletId || isLoading || isOpen) return; setIsLoading(true); setError(null); try { let cursor: string | undefined; do { const page = await getCardsForUser({environment: 'sandbox', limit: 20, cursor}); const existing = page.data.find( (card) => card.wallet_id === walletId && card.chain_id === 'eip155:42431' && card.asset === 'pathusd' && (card.status === 'active' || card.status === 'inactive') ); if (existing && !resumeSetup) { setCardId(existing.id); return; } cursor = page.next_cursor ?? undefined; } while (cursor); const {id} = await signUp({ environment: 'sandbox', walletId, chainId: 'eip155:42431', asset: 'pathusd' }); setCardId(id); } catch (error) { setError(error instanceof Error ? error.message : 'Could not open card'); } finally { setIsLoading(false); } }; if (cardId) { return ( {error ? {error} : null} ); } ``` **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](https://mintcdn.com/privy-c2af3412/zeEEdrxSbmZQCA-7/images/wallets/smart-wallets.png?fit=max\&auto=format\&n=zeEEdrxSbmZQCA-7\&q=85\&s=af4dd0c0e048cb8fe71daf516ee1e85e) ## 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. **iOS (Swift)** ```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)") } ``` **Android (Kotlin)** ```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}") } ) } ``` > [!NOTE] > > 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; } ``` > [!TIP] > > 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`. **iOS (Swift)** ```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)") } } ``` **Android (Kotlin)** ```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. **iOS (Swift)** ```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 } ``` **Android (Kotlin)** ```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 ) } ``` **Server (Node.js)** ```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 > **[Gas sponsorship](/wallets/gas-and-asset-management/gas/overview)** > > Configure paymasters to sponsor gas fees for your users. > **[Smart wallet overview](/wallets/using-wallets/evm-smart-wallets/overview)** > > Learn more about smart wallet features and supported providers. > **[Batch transactions](/recipes/batch-transactions)** > > Send multiple transactions atomically from a smart wallet. > **[Dashboard configuration](/wallets/using-wallets/evm-smart-wallets/setup/configuring-dashboard)** > > 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 > **[Wagmi starter template](https://github.com/privy-io/examples/tree/main/examples/privy-next-wagmi)** > > 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. | > [!NOTE] > > **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 🎉.** > [!NOTE] > > 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. > **[Stripe embedded onramp](/recipes/stripe-embedded-onramp)** > > Embed card-based funding directly in app flows. > **[Crypto deposits](/wallets/funding/crypto-deposits/overview)** > > Create deposit addresses and route inbound funds automatically. > **[Due on and off ramp](/recipes/due-on-off-ramp)** > > Add localized fiat conversion through Due. > **[Bitso MXNB onramp](/recipes/bitso-mxnb-onramp)** > > 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). **1. Agent requests a device code** 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… ``` **2. User approves in browser** 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. **3. Agent receives tokens** 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. > [!WARNING] > > 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. **4. Agent transacts** 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. > [!TIP] > > 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. **React** ```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 ? ( ) : ( <> )}
); } ``` **Vanilla JS** ```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(); } ``` > [!WARNING] > > 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. > [!WARNING] > > 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 > **[Agent CLI](/recipes/agent-integrations/agent-cli)** > > Give any agent a wallet with a CLI command. No integration code needed. > **[Agentic wallets](/recipes/agent-integrations/agentic-wallets)** > > Create developer-controlled agent wallets with policy guardrails. > **[Policies](/controls/policies/overview)** > > Constrain agent behavior with transfer limits, allowlists, and time-based controls. > **[x402 payments](/recipes/agent-integrations/x402)** > > 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 **For humans** **1. Visit the agent sandbox** Go to [agents.privy.io](https://agents.privy.io) to create an account, view wallets, and manage agent activity. **2. Install the CLI** 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 ``` **3. Log in** 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. **4. Fund wallets** 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. **5. View wallets** 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) ``` **6. Send transactions** 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 ``` **For agents** ``` 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 > **[Agent CLI](https://agents.privy.io)** > > Track agent activity, view balances, and manage wallets. > **[Agentic wallets](/recipes/agent-integrations/agentic-wallets)** > > Build developer-controlled agent wallets with policy guardrails. > **[x402 payments](/recipes/agent-integrations/x402)** > > Enable HTTP-native payments for APIs and digital content. > **[MPP integration](/recipes/agent-integrations/mpp)** > > Machine-to-machine payments over HTTP with Privy wallets. > **[Gas sponsorship](/wallets/gas-and-asset-management/gas/overview)** > > Sponsor gas fees for agent transactions. > **[Agent authorization](/recipes/agent-integrations/agent-authorization)** > > 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 a full access **App Secret** from the app settings. AgentCore signs wallet operations, so a [read-only app secret](/basics/get-started/dashboard/app-secrets) does not work. * **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. > [!WARNING] > > 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"] ``` > [!WARNING] > > 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. > [!NOTE] > > 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. > [!WARNING] > > 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. > [!TIP] > > 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. **1. Choose your control model** 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). **2. Create authorization keys** 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](/controls/authorization-keys/keys/create/key)** > > Create authorization keys in your Privy Dashboard. > **[Key quorums quickstart](/controls/key-quorum/create#nodejs)** > > Set up a key quorum for enhanced security. **3. Define agent policies** 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. > **[Policies overview](/controls/policies/overview)** > > Learn how to construct policies with Privy's policy language. > **[Create a policy](/controls/policies/create-a-policy#nodejs)** > > Create policies for your agents. **4. Create the agent wallet** 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. > **[Wallets quickstart](/wallets/wallets/create/create-a-wallet#nodejs)** > > Create a wallet. **5. Execute transactions** You can now send transactions, sign transactions, or sign messages with Privy's API. Follow the guide below to send a transaction. > **[Send EVM transaction](/wallets/using-wallets/ethereum/send-a-transaction#nodejs)** > > Execute transactions on Ethereum and EVM chains. > **[Send Solana transaction](/wallets/using-wallets/solana/send-a-transaction#nodejs)** > > Execute transactions on Solana. > **[Raw signing](/wallets/using-wallets/other-chains#nodejs)** > > Execute transactions on other supported chains. **6. Monitor and observe agent behavior** 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. > **[Transaction webhooks](/wallets/gas-and-asset-management/assets/transaction-event-webhooks)** > > Monitor transaction status and completion. > **[Balance webhooks](/wallets/gas-and-asset-management/assets/balance-event-webhooks)** > > Track deposits and withdrawals. ## Learn more > **[OpenClaw integration](/recipes/agent-integrations/openclaw-agentic-wallets)** > > Use Privy wallets with OpenClaw agents. > **[x402 payments quickstart](/recipes/agent-integrations/x402)** > > Set up your agent to pay for APIs and content. > **[Hyperliquid quickstart](/recipes/hyperliquid-guide)** > > Set up your agent to trade on Hyperliquid. > **[Gas sponsorship](/wallets/gas-and-asset-management/gas/overview)** > > 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 > **[Clanker API docs](https://clanker.gitbook.io/clanker-documentation/developers/api)** > > Learn how to deploy tokens on Base using the Clanker API. > **[Privy Wallets](/wallets/overview)** > > Privy wallets for secure EVM wallet management. > **[Twitter API](https://developer.twitter.com/en/docs/twitter-api)** > > 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). **Set up Twitter bot (pseudocode)** **1. Register your Twitter app** 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. **2. Set up Node.js Twitter bot server** 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: **Example Twitter API mention payload** ```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. **1. Create a wallet for a user** 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; } ``` **2. Get a user 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; } ``` **3. Get or create a wallet** 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. **Pseudocode for LLM integration** **1. Sample LLM prompt and response** **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" } } ``` **2. Pseudocode for LLM integration** ```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. Request API access** 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. **2. Next: Deploy a token** 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. **Pseudocode for deploying a token** **1. Sample LLM prompt and response** **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" } } ``` **2. Parse Twitter message for command** ```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(); ``` **3. Use LLM to extract intent and parameters** ```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; ``` **4. Look up or create user's wallet** ```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 ``` **5. Deploy token using Clanker API** ```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; ``` **6. Notify user of deployment** ```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. **Pseudocode for sending tokens** **1. Sample LLM prompt and response** **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" } } ``` **2. Look up sender and recipient wallets** ```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); ``` **3. Send transaction using Privy wallet (NodeJS)** ```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' }); ``` > [!NOTE] > > 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. > [!WARNING] > > **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. > [!NOTE] > > **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: > [!WARNING] > > **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 ``` > [!NOTE] > > 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" } } } ``` > [!WARNING] > > **Security Notice**: A full access Privy App Secret can create wallets and sign transactions. > OpenClaw requires a full access secret, because a [read-only app > secret](/basics/get-started/dashboard/app-secrets) cannot create wallets or sign. 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 > [!WARNING] > > **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. > **[Agent CLI](/recipes/agent-integrations/agent-cli)** > > Give any agent a wallet with a CLI command. No integration code needed. > **[Agentic wallets](/recipes/agent-integrations/agentic-wallets)** > > Create wallets for autonomous agents with policy guardrails. > **[x402](/recipes/agent-integrations/x402)** > > Enable HTTP-native payments for APIs and digital content. > **[Machine Payments Protocol](/recipes/agent-integrations/mpp)** > > Coordinate automated wallet-to-wallet payments for agent systems. > **[Bankr Twitter bot guide](/recipes/agent-integrations/bankr-bot-guide)** > > Build a Twitter bot with Privy wallets and Clanker. > **[OpenClaw agentic wallets](/recipes/agent-integrations/openclaw-agentic-wallets)** > > Connect OpenClaw agents to Privy server wallets. > **[Agent authorization](/recipes/agent-integrations/agent-authorization)** > > 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 **1. Create the Privy app and a simple frontend** 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 ``. **2. Enable agent authorization and build the verification page** 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. **3. Spin up a Hermes agent on Telegram** 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 ``` > [!TIP] > > 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 ``` > [!WARNING] > > 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 } } } ``` > [!NOTE] > > 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: **1. Connect the wallet (one time)** **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. **2. Review the portfolio** **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. **3. Decide and execute a trade** **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 > **[Agent authorization](/recipes/agent-integrations/agent-authorization)** > > The full device-flow API reference this skill is built on. > **[Agent CLI](/recipes/agent-integrations/agent-cli)** > > Give any agent a wallet with a CLI command. No integration code needed. > **[Policies](/controls/policies/overview)** > > Constrain agent behavior with transfer limits, allowlists, and time-based controls. > **[Agentic wallets](/recipes/agent-integrations/agentic-wallets)** > > Create developer-controlled agent wallets with policy guardrails. > **[x402 payments](/recipes/agent-integrations/x402)** > > 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. > [!NOTE] > > 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 **1. Install and authenticate** ```bash theme={"system"} npm install -g acp-cli acp configure ``` OAuth tokens are stored in the OS keychain and refreshed automatically. **2. Create the agent and attach a signer** ```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. **3. Verify, check balance, and top up** ```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. **4. Provision agent identity** 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 > **[EconomyOS docs](https://os.virtuals.io/)** > > Full documentation, advanced flows, and SDK usage from Virtuals. > **[Agent email](https://os.virtuals.io/agent-identity/email/overview)** > > Dedicated inbox for agent logins, OTPs, and notifications. > **[Agent card](https://os.virtuals.io/agent-identity/card/overview)** > > Virtual Visa cards for real-world agent checkout. > **[Agent Commerce Protocol](https://os.virtuals.io/acp/overview)** > > How agents buy and sell services on-chain. > **[Privy server wallets](/wallets/overview)** > > Learn how Privy server wallets work under the hood. > **[Agent compute](https://os.virtuals.io/agent-identity/compute/overview)** > > 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 **React** ```bash theme={"system"} npm install @privy-io/react-auth ``` The `useX402Fetch` hook is built into `@privy-io/react-auth` (v3.7.0+). **Node.js** ```bash theme={"system"} npm install @privy-io/node @x402/fetch ``` The `createX402Client` function is available in `@privy-io/node`. ## Usage ### Basic example **React** ```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 ; } ``` **Node.js** ```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 **React** ```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 **React** ```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 **React** 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 ; } ``` **Node.js** 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/) * For testing, get free devEURAU from [AllUnity's faucet](https://www.allunity.ai/faucet) ## 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) * AllUnity: [facilitator](https://allunity.com/agentic-facilitator), [docs](https://allunity.com/product-docs/agentic-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. > [!NOTE] > > 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": "" } ]' ``` > [!TIP] > > 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. > [!WARNING] > > 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. > [!WARNING] > > 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' } ] })) ] }); ``` > [!WARNING] > > 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'} ] } ]; ``` > [!WARNING] > > 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). > [!TIP] > > 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..." }]' ``` > [!WARNING] > > 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. > [!WARNING] > > 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. > [!NOTE] > > 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. > [!NOTE] > > 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. > **[Guest accounts](/recipes/react/guest-accounts)** > > Launch with instant sign-in and progressive account upgrades. > **[Seamless Telegram login](/recipes/react/seamless-telegram)** > > Add one-tap Telegram sign-in with wallet provisioning. > **[Login with Farcaster](/recipes/farcaster/login)** > > Authenticate users from Farcaster mini apps. > **[SMS and WhatsApp login](/recipes/dashboard/login-methods/sms-whatsapp)** > > Configure phone-based authentication in the dashboard. > **[Custom auth with Supabase](/recipes/authentication/using-supabase-for-custom-auth)** > > Connect Supabase sessions to Privy user authentication. > **[Migrate JWT auth from Alchemy](/recipes/migrating-jwt-authentication-from-alchemy)** > > 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. > [!NOTE] > > 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. **Server** ```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}` } : {}, }, } ) } ``` **Client** ```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 **NodeJS** ```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 } ] } }); ``` **Python** ```python theme={"system"} response = client.wallets.ethereum.send_calls( "insert-wallet-id", caip2="eip155:8453", # Base params={ "calls": [ {"to": "0xRecipientAddress1", "value": "0x2386F26FC10000"}, {"to": "0xRecipientAddress2", "value": "0x2386F26FC10000"}, ] }, ) ``` **cURL** ```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. This example approves OUSD on Tempo: ```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 OUSD (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:4217', params: { calls: [ { to: '0x20c0000000000000000000006a37da5c996874be', 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** ```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. > [!NOTE] > > 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. > [!NOTE] > > 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. > [!WARNING] > > 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. > [!TIP] > > 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. > [!TIP] > > Pass an `X-Idempotency-Key` header (UUID v1) to prevent duplicate withdrawals if the request is > retried. > **[Juno docs](https://docs.bitso.com/juno/docs/overview-of-basic-operations)** > > Learn more about MXNB issuances, redemptions, and withdrawals > **[Create a Privy wallet](/api-reference/wallets/create)** > > 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! > [!TIP] > > 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](https://mintcdn.com/privy-c2af3412/iLCCWBdcq9hjhX4f/images/allowed-origins-for-capacitor.png?fit=max\&auto=format\&n=iLCCWBdcq9hjhX4f\&q=85\&s=12d6a02c3cdf291466c1c779d2d709fd) * **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](https://mintcdn.com/privy-c2af3412/iLCCWBdcq9hjhX4f/images/allowed-redirect-urls.png?fit=max\&auto=format\&n=iLCCWBdcq9hjhX4f\&q=85\&s=fbfcfc1bf244f15f5be0789cfdaf1f43) * 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). > [!WARNING] > > 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. > [!WARNING] > > 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`: > [!NOTE] > > 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 **iOS** 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 ``` **Android** Configure your Android app to handle OAuth redirects by adding intent filters to your `AndroidManifest.xml`: ```xml theme={"system"} ``` > [!WARNING] > > 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. > [!TIP] > > 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 > **[Dolomite](https://dolomite.io)** > > Explore Markets powered by Dolomite. > **[Get started with Privy](/basics/react/quickstart)** > > Set up Privy and create embedded wallets for your app. ## Deposit USD1 into a Dolomite market **1. 1. Configure addresses** 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 ``` **2. 2. Approve the Dolomite router to spend USD1** 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 }); ``` **3. 3. Deposit USD1 into the market** 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). > [!TIP] > > 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 > [!WARNING] > > 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: > [!TIP] > > 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 **Wallet 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 **Swap Issues** * **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 > [!WARNING] > > 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 > **[Privy TON Wallets](/wallets/using-wallets/other-chains)** > > Learn how to use Privy embedded wallets with TON for signing messages and transactions. > **[React quickstart](/basics/react/installation)** > > Get started with Privy's React SDK. ## Creating your Privy app > [!NOTE] > > 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. > [!TIP] > > 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 > [!TIP] > > 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. > [!NOTE] > > `@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() }); ``` > [!WARNING] > > Only instantiate a single Privy client for your application. Creating multiple instances will > cause unexpected behavior. **Configuration options** **appId** `string` *required* Your Privy app ID from the Dashboard. **clientId** `string` Your Privy client ID from the Dashboard under **Settings → Clients**. **storage** `Storage` *required* A storage adapter for persisting session state. Use `LocalStorage` for browsers, or implement a custom adapter for other environments. **logger** `Logger` A custom logger object to replace the default console-based logger. Useful for routing Privy logs to your observability stack. > **Custom storage adapters** > > 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); } ``` > [!NOTE] > > 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); }); ``` > [!TIP] > > 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}` } }); ``` > [!NOTE] > > `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. > [!NOTE] > > 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 > [!NOTE] > > 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'} }); ``` > [!TIP] > > [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 > [!WARNING] > > On iOS and Android, enabling this setting is **not enough on its own**. Your app must also catch > the transfer conflict and complete it with `transferWithCode`/`transferAccount` — the SDK won't > transfer anything automatically. See [Handle account transfer in your mobile > app](/recipes/mobile-account-transfer) for the client-side implementation. 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](https://mintcdn.com/privy-c2af3412/YvGXGsI-T4KAqoan/images/account-transfer.png?fit=max\&auto=format\&n=YvGXGsI-T4KAqoan\&q=85\&s=615ecae9c06c7cd9017fd71cc705c1be) **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. > [!NOTE] > > 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. > [!NOTE] > > 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. > [!TIP] > > 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. > [!NOTE] > > 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 > [!NOTE] > > 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. > [!NOTE] > > 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. > **US and Canada only SMS support** > > > [!NOTE] > > > > Included on all plans. > > This enables SMS log in for only US and Canada. > **International SMS support** > > > [!NOTE] > > > > 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 > > > [!NOTE] > > > > 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. > > > [!WARNING] > > > > 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. > **WhatsApp support** > > > [!NOTE] > > > > 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. > [!NOTE] > > 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. > [!NOTE] > > 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 > **Help! My legitimate users are failing CAPTCHA** > > 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
  • >
> **How do I delete bots?** > > 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). > **What if I manage my own Twilio account and my app is experiencing SMS fraud?** > > 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. # Recovering assets from a dashboard wallet Source: https://docs.privy.io/recipes/dashboard/recovering-assets Move ERC-20 and SPL tokens out of a wallet when the Privy Dashboard does not support sending the asset, chain, or wallet type. The Privy Dashboard supports manually initiating transfers from wallets for [supported assets](/wallets/actions/transfer/overview#supported-assets) on [supported chains](/wallets/actions/transfer/overview#supported-chains). A wallet can therefore hold a token that the dashboard displays but does not automatically support sending: an unlisted ERC-20, an SPL token on a network the dashboard does not offer, or a balance in a wallet the dashboard cannot send from. Every one of those balances is still recoverable. The wallet's keys are unchanged, and Privy's wallet API supports a far wider range of assets, chains, and wallet types than the dashboard UI exposes. This recipe covers the three recovery paths, in order of increasing effort, and how to choose between them. > [!NOTE] > > Recovery needs no support ticket and no new wallet. Refer to the [chains > overview](/wallets/overview/chains) for the support tier of the chain in question, since the tier > determines whether Privy broadcasts the transaction or the transaction must be broadcast > independently. ## Why the dashboard cannot send an asset The dashboard displays balances for a broader set of assets than it can transfer, so a balance appearing in the UI is not a guarantee that the UI can move it. Identify the cause first, because it determines the cheapest recovery path. | Cause | Recovery path | | - | - | | The token is an ERC-20 or SPL token that is not one of Privy's well-known assets | [Option 1: Token watchlist](#option-1-token-watchlist) | | The wallet is an end-user or embedded wallet rather than a server wallet | [Option 2: Transfer action API](#option-2-transfer-action-api) | | The wallet is owned by a key quorum of team members | [Propose a transfer intent](/controls/dashboard/intents#transfer-funds) for manual approval | | The network is outside the transfer API's supported chains | [Option 3: Low-level RPC](#option-3-low-level-rpc) | | The chain type is neither EVM nor Solana, such as Tron or Sui | [Tier 2 chains recipe](/recipes/use-tier-2) | ## Choosing a recovery path > **[Token watchlist](#option-1-token-watchlist)** > > No code. Registers the token so the dashboard transfer flow can send it. Covers ERC-20, SPL, and > Token-2022 on supported networks. > **[Transfer action API](#option-2-transfer-action-api)** > > Recommended. One request moves any ERC-20 or SPL token on a supported chain, from any wallet, > with no transaction construction. > **[Low-level RPC](#option-3-low-level-rpc)** > > For chains the transfer API does not cover. Build the transaction, have Privy sign it, and > broadcast it if Privy does not. > **[Transfer intent](/controls/dashboard/intents#transfer-funds)** > > For a wallet owned by a key quorum of team members. Propose the transfer, then approve it from > the dashboard. Every option keeps the wallet's private key inside Privy's secure enclave. Recovery never requires exporting the key. ## Prerequisites * The wallet's ID, from the [Wallets](https://dashboard.privy.io/apps?page=wallets) page of the dashboard * The app's ID and app secret, from the [Basics](https://dashboard.privy.io/apps?page=settings\&tab=basics) tab of the app's settings * The token's contract address (EVM) or mint address (Solana) * A destination address that can hold the asset * Enough native currency in the wallet to pay gas, unless the app has [gas sponsorship](/wallets/gas-and-asset-management/gas/overview) configured > [!WARNING] > > A token transfer is a contract call, and the wallet pays its own fees. Fund the wallet with a > small amount of ETH, SOL, or the chain's native gas token before attempting any transfer path. > Transfers made through the [transfer action API](#option-2-transfer-action-api) are gas-sponsored > by default when the app has sponsorship configured. ### Check whether the wallet has an owner Whether the wallet has an owner determines how each request is authorized, so establish it before starting. Fetch the wallet and check `owner_id`, which is `null` for an ownerless wallet: ```bash theme={"system"} curl https://api.privy.io/v1/wallets/{wallet_id} \ --header 'Authorization: Basic ' \ --header 'privy-app-id: ' ``` | Wallet | How to recover | | - | - | | No owner, the default for dashboard-created wallets | Call the API directly, as shown below. The app secret alone authorizes each request | | Owned by a key quorum of team members | [Propose an intent](/controls/dashboard/intents), then approve it from the dashboard | > [!NOTE] > > A wallet owned by a key quorum cannot be recovered with a direct API call, because the request > carries no authorization signatures from the quorum. Propose the transfer as an > [intent](/controls/dashboard/intents#transfer-funds) instead, then have the quorum's reviewers > approve it in the dashboard. Intents expire 72 hours after creation. ## Option 1: Token watchlist Privy's transfer flow sends [well-known assets](/wallets/actions/transfer/overview#well-known-assets) such as USDC, USDT, EURC, ETH, and SOL out of the box. Any other ERC-20 or SPL token becomes sendable once it is registered as a custom asset for the app. This is the only recovery path that requires no code, so it is worth attempting first. **1. Register the token** Open the [**Token watchlist**](https://dashboard.privy.io/apps?page=settings\&tab=token-watchlist) tab of the **Wallets** page, then add the token's contract or mint address on the network holding the balance. **2. Confirm the balance appears** Return to the wallet's page. The token now appears as a selectable asset in the transfer flow rather than a display-only balance. **3. Send the balance** Start a transfer from the wallet, select the newly registered token, and enter the destination address. If the token still cannot be selected, the blocker is the wallet or the network rather than the asset. Continue to option 2. ## Option 2: Transfer action API The [transfer action API](/wallets/actions/transfer/overview) accepts a human-readable asset, amount, chain, and destination, and handles the onchain construction. It works from any wallet the caller is authorized to use, including end-user and embedded wallets that the dashboard cannot send from, and it resolves token decimals so the amount is a plain decimal string. Register the token in the [token watchlist](#option-1-token-watchlist) first, then pass its address as `source.asset_address`: ```bash theme={"system"} curl --request POST https://api.privy.io/v1/wallets/{wallet_id}/transfer \ --header 'Authorization: Basic ' \ --header 'privy-app-id: ' \ --header 'Content-Type: application/json' \ --data '{ "source": { "asset_address": "0x...", "amount": "1234.56", "chain": "base" }, "destination": { "address": "0x..." } }' ``` For a well-known asset, pass `source.asset` instead of `source.asset_address`, using the asset's API value such as `usdc`. Provide exactly one of the two fields; sending both is rejected. Refer to [transfer usage](/wallets/actions/transfer/usage) for the full parameter list and response shape. The response is an action rather than a transaction, returning an `id` and a `status` of `pending`. Poll the action until it reports a terminal status to confirm the recovery landed: ```bash theme={"system"} curl https://api.privy.io/v1/wallets/{wallet_id}/actions/{action_id} \ --header 'Authorization: Basic ' \ --header 'privy-app-id: ' ``` To recover the entire position, read the wallet's balance for the asset first and pass it as `source.amount`. Recovering the wallet's native balance is the same request with `asset` set to the native token, run last so the earlier transfers still have gas available. > [!TIP] > > Confirm that no [policy](/controls/policies/overview) on the wallet denies the transfer before > debugging further. A policy that allowlists specific contracts will reject a recovery transaction > against an unlisted token contract. ## Option 3: Low-level RPC The transfer action API covers a [fixed set of chains](/wallets/actions/transfer/overview#supported-chains). Outside that set, build the transaction and use Privy's signing RPCs directly. Which RPC applies depends on the wallet's `chain_type`, not on the token. **ERC-20 on any EVM chain** For an EVM chain that Privy broadcasts to, encode a `transfer` call and send it as a normal transaction. The `to` field is the token contract; the destination is an argument to `transfer`. ```typescript {skip-check} theme={"system"} import {PrivyClient} from '@privy-io/node'; import {createPublicClient, http, encodeFunctionData, erc20Abi} from 'viem'; import {base} from 'viem/chains'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID!, appSecret: process.env.PRIVY_APP_SECRET! }); const walletId = 'insert-wallet-id'; const walletAddress = '0x...'; // the wallet's address const tokenAddress = '0x...'; // the ERC-20 contract to recover const destination = '0x...'; // where the balance should end up // Read the full balance so the entire position is recovered const publicClient = createPublicClient({chain: base, transport: http()}); const balance = await publicClient.readContract({ address: tokenAddress, abi: erc20Abi, functionName: 'balanceOf', args: [walletAddress] }); if (balance === 0n) throw new Error('Nothing to recover'); const data = encodeFunctionData({ abi: erc20Abi, functionName: 'transfer', args: [destination, balance] }); const {hash} = await privy .wallets() .ethereum() .sendTransaction(walletId, { caip2: 'eip155:8453', params: {transaction: {to: tokenAddress, data, chain_id: 8453}} }); console.log('Recovery transaction:', hash); ``` If Privy does not broadcast to the chain, sign the transaction and submit it independently. Use [`eth_signTransaction`](/api-reference/wallets/ethereum/eth-sign-transaction), which returns an RLP-encoded signed transaction ready for `eth_sendRawTransaction`. It takes an explicit `chain_id` and no `caip2`, so it works on EVM chains Privy does not broadcast to: **Typescript** ```typescript {skip-check} theme={"system"} const {signed_transaction} = await privy .wallets() .ethereum() .signTransaction(walletId, { params: { transaction: { to: tokenAddress, data, chain_id: 8453, nonce: 0, gas_limit: '0x...', max_fee_per_gas: '0x...', max_priority_fee_per_gas: '0x...', type: 2 } } }); // Broadcast through any RPC provider for the chain const hash = await publicClient.request({ method: 'eth_sendRawTransaction', params: [signed_transaction as `0x${string}`] }); ``` Signing this way means populating `nonce`, `gas_limit`, and the fee fields, since Privy is not estimating them. **SPL on Solana** Build the transfer instruction between the sender's and recipient's associated token accounts, then hand the serialized transaction to Privy. Privy fills in the recent blockhash when the transaction carries the dummy value `11111111111111111111111111111111`. ```typescript {skip-check} theme={"system"} import {PrivyClient} from '@privy-io/node'; import {Connection, PublicKey, Transaction} from '@solana/web3.js'; import { getAssociatedTokenAddress, getAccount, createTransferInstruction, createAssociatedTokenAccountInstruction } from '@solana/spl-token'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID!, appSecret: process.env.PRIVY_APP_SECRET! }); const walletId = 'insert-wallet-id'; const connection = new Connection('insert-your-rpc-url', 'confirmed'); const from = new PublicKey('...'); // the wallet's address const to = new PublicKey('...'); // the destination address const mint = new PublicKey('...'); // the SPL mint to recover const fromTokenAccount = await getAssociatedTokenAddress(mint, from); const toTokenAccount = await getAssociatedTokenAddress(mint, to); // Read the full balance so the entire position is recovered const {amount} = await getAccount(connection, fromTokenAccount); if (amount === 0n) throw new Error('Nothing to recover'); const transaction = new Transaction(); // The destination needs a token account for this mint before it can receive the transfer if (!(await connection.getAccountInfo(toTokenAccount))) { transaction.add(createAssociatedTokenAccountInstruction(from, toTokenAccount, to, mint)); } transaction.add(createTransferInstruction(fromTokenAccount, toTokenAccount, from, amount)); transaction.feePayer = from; // Privy populates the real blockhash when given this dummy value transaction.recentBlockhash = '11111111111111111111111111111111'; const {hash} = await privy .wallets() .solana() .signAndSendTransaction(walletId, { caip2: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', transaction: transaction .serialize({requireAllSignatures: false, verifySignatures: false}) .toString('base64') }); console.log('Recovery transaction:', hash); ``` Creating the destination's associated token account costs rent, paid by the wallet being recovered, so the wallet needs a small SOL balance beyond the transaction fee. For Token-2022 mints, pass the Token-2022 program ID to each of the `@solana/spl-token` helpers. > [!NOTE] > > Recovering a token on a chain type other than EVM or Solana, such as a TRC-20 balance on Tron, > uses that chain's own signing RPC rather than the methods above. Refer to the [Tier 2 chains > recipe](/recipes/use-tier-2) for per-chain examples, and adapt the transaction being built from a > native transfer to the chain's token transfer. ## Verify the recovery Confirm the transaction onchain, then re-check the wallet's balances in the dashboard. A recovered wallet shows a zero balance for the asset, plus whatever native currency was left behind for gas. Repeat per asset and per network, since each transfer moves one asset on one chain. ## Related resources > **[Transfer action API](/wallets/actions/transfer/overview)** > > Supported assets and chains, custom assets, and native bridging. > **[Propose intents](/controls/dashboard/intents)** > > Recovering from a wallet owned by a key quorum of team members. > **[Chain support](/wallets/overview/chains)** > > Which chains Privy signs for, and which it also broadcasts for. > **[Sending ERC-20s](/recipes/send-usdc)** > > Encoding an ERC-20 transfer across Privy's SDKs. > **[Sending SPL tokens](/recipes/solana/send-spl-tokens)** > > Building SPL transfers and handling associated token accounts. > **[Using Tier 2 chains](/recipes/use-tier-2)** > > Signing and broadcasting examples for chain types beyond EVM and Solana. # 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"} }' ``` > **Example quote** > > ```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" > } > ``` > [!NOTE] > > 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. **1. Enable Abstract integration** 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. **2. Login users with their Abstract Global Wallet** **React** 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 ( ); } ``` **React Native** 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 ( ); } ``` **3. Sign a message with the user's Abstract Global Wallet** **React** 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. **React Native** 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 configured with `@privy-io/react-auth` v3.22.0+ or `@privy-io/expo` v0.73.0+ * 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: **React** ```bash theme={"system"} npm install ox ``` **React Native** ```bash theme={"system"} npx expo install ox ``` ## Integration **React** 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 the Builder Code const ERC_8021_ATTRIBUTION_SUFFIX = Attribution.toDataSuffix({ codes: ['YOUR-BUILDER-CODE'] // Replace with the code from base.dev > Settings > Builder Codes }); function App() { return ( {/* App content */} ); } ``` **React Native** Create one Privy client with the `dataSuffix` plugin, then pass the client to `PrivyProvider`: ```tsx theme={"system"} import {createPrivyClient, dataSuffix, PrivyProvider} from '@privy-io/expo'; import {Attribution} from 'ox/erc8021'; // Generate the ERC-8021 attribution suffix with the Builder Code const ERC_8021_ATTRIBUTION_SUFFIX = Attribution.toDataSuffix({ codes: ['YOUR-BUILDER-CODE'] // Replace with the code from base.dev > Settings > Builder Codes }); // Create one client for the app const client = createPrivyClient({ appId: 'your-privy-app-id', clientId: 'your-privy-client-id', plugins: [dataSuffix(ERC_8021_ATTRIBUTION_SUFFIX)] }); function App() { return {/* App 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. > [!NOTE] > > Base Builder Codes rewards are specific to transactions on Base mainnet and Base Sepolia. However, > the `dataSuffix` plugin appends the suffix to every transaction on every supported chain, with the > sole exception of Tempo (type 118) transactions. If your app needs chain-specific suffix behavior, > [contact the Privy team](https://privy.io/slack). > [!WARNING] > > 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 > **[ERC-8021 Specification](https://www.erc8021.com/)** > > Learn more about the ERC-8021 attribution standard. > **[Base Builder Codes](https://base.dev)** > > Register for a Builder Code and track your rewards. > **[ox Library](https://oxlib.sh)** > > 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. **React** ```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; } ``` **React Native** ```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; } ``` **Node.js** ```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; } ``` **Python** Encode and submit the token approval with `web3.py` and Privy: ```python theme={"system"} import os from eth_abi import encode from eth_utils import keccak bebop_settlement_address = os.environ["BEBOP_SETTLEMENT_ADDRESS"] weth_address = os.environ["WETH_ADDRESS"] selector = keccak(text="approve(address,uint256)")[:4] approve_data = "0x" + ( selector + encode( ["address", "uint256"], [bebop_settlement_address, 2**256 - 1], ) ).hex() response = client.wallets.ethereum.send_transaction( "insert-wallet-id", caip2="eip155:8453", params={ "transaction": { "to": weth_address, "data": approve_data, "chain_id": 8453, } }, ) ``` > [!NOTE] > > 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** ```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. **React** ```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; } } ``` **React Native** ```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; } } ``` **Node.js** ```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; } } ``` **Python** Submit the transaction returned by the Bebop quote API with Privy: ```python theme={"system"} wallet = client.wallets.get("insert-wallet-id") transaction = get_swap_quote(wallet.address) response = client.wallets.ethereum.send_transaction( wallet.id, caip2=transaction.get("caip2", "eip155:8453"), params={ "transaction": { "to": transaction["to"], "data": transaction["data"], "value": transaction["value"], "chain_id": 8453, } }, ) transaction_hash = response.hash ``` ## 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. > [!TIP] > > 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. > [!NOTE] > > 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. **React, Expo** 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 */} ``` **NodeJS** 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}); ``` **Python** Sign the transaction with Privy, then broadcast it through the custom Flashblocks RPC with `web3.py`: ```python theme={"system"} from web3 import Web3 response = client.wallets.ethereum.sign_transaction( "insert-wallet-id", params={ "transaction": { "to": "insert-recipient-address", "value": "0x2386F26FC10000", "chain_id": 8453, } }, ) web3 = Web3(Web3.HTTPProvider("insert-custom-flashblocks-rpc-url")) transaction_hash = web3.eth.send_raw_transaction(response.signed_transaction) receipt = web3.eth.wait_for_transaction_receipt(transaction_hash) ``` **REST API** 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: > [!NOTE] > > 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 USDC](/recipes/send-usdc)** > > Send ERC-20 transfers from Privy-managed wallets. > **[Batch transactions](/recipes/batch-transactions)** > > Combine multiple onchain actions into one execution flow. > **[Speed up transactions](/recipes/speeding-up-transactions)** > > Replace stuck transactions with higher-fee submissions. > **[Flashblocks](/recipes/evm/flashblocks)** > > Integrate low-latency block execution patterns. > **[EIP-7702](/recipes/react/eip-7702)** > > 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! > [!TIP] > > 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! > [!TIP] > > 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`. > **[Mini Apps starter](https://github.com/privy-io/examples/tree/main/examples/privy-next-farcaster-mini-app)** > > A starter repository for building a Farcaster Mini App with Privy and the Mini Apps SDK. **How does seamless Farcaster login work in a Mini App?** 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 > [!NOTE] > > 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. > [!TIP] > > 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 > [!NOTE] > > 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 ``` > [!TIP] > > 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`. > [!WARNING] > > 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]); ``` > [!WARNING] > > **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. > [!TIP] > > 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 > [!WARNING] > > 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'); ``` > [!TIP] > > 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: **Mainnet** ```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' ); ``` **Sepolia** ```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/' ); ``` **Holesky** ```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`: **Mainnet** ```tsx theme={"system"} {/* your app's content */} ``` **Sepolia** ```tsx theme={"system"} {/* your app's content */} ``` **Holesky** ```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. > [!NOTE] > > 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 > **[Gas sponsorship overview](/wallets/gas-and-asset-management/gas/overview)** > > Learn about Privy's gas sponsorship engine > **[Send a transaction](/wallets/using-wallets/ethereum/send-a-transaction)** > > 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).
**React** ## Creating multiple HD wallets To create multiple HD wallets for a user, use the `createWallet` method: **Ethereum** ```tsx theme={"system"} import {useCreateWallet} from '@privy-io/react-auth'; const {createWallet} = useCreateWallet(); ``` **Solana** ```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`: **createAdditional** `boolean` 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. **walletIndex** `number` The specified HD wallet index. Must be a positive number, and must be `0` for the user's first wallet. > [!NOTE] > > 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: **Ethereum** ```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 ); ``` **Solana** ```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: **Ethereum** #### `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 }); ``` **Solana** #### `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: **Ethereum** ```tsx theme={"system"} const {exportWallet} = usePrivy(); await exportWallet({address: 'insert-your-desired-address'}); ``` **Solana** ```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. > [!NOTE] > > 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" } ] } ``` **React Native** **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. **createAdditional** `boolean` 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); ``` **Solana** ## 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: **createAdditional** `boolean` 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: **address** `string` The address (base58-encoded public key) for the wallet. **publicKey** `string` The address (base58-encoded public key) for the wallet. **walletIndex** `number` The HD index for the wallet. **getProvider** `() =\> Promise\` 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); ``` > [!NOTE] > > 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); ``` > [!NOTE] > > 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); ``` > [!WARNING] > > 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"}' ``` > [!NOTE] > > 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): > [!TIP] > > 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: > **[Agent wallets](/recipes/hyperliquid/agents-and-subaccounts)** > > Learn how to set up API wallets for secure, programmatic trading operations. > **[Executing trades](/recipes/hyperliquid/trading-patterns)** > > Discover common trading patterns and best practices for placing orders, managing positions, and > more. > **[Client-side SDKs](/recipes/hyperliquid/client-side-usage)** > > Build React apps with external wallets like MetaMask and agent wallets. > **[Policies](/recipes/hyperliquid/policies-and-offline-actions)** > > Implement secure trading policies and execute offline actions for advanced risk management. > **[HyperEVM](/recipes/hyperliquid/hyperevm)** > > Develop smart contracts on Hyperliquid's EVM-compatible blockchain. ## Resources > **[Hyperliquid Docs](https://hyperliquid.gitbook.io/hyperliquid-docs/)** > > Official documentation explaining Hyperliquid's architecture, trading features, and API > endpoints. > **[HyperEVM Docs](https://hyperliquid.gitbook.io/hyperliquid-docs/hyperevm)** > > Overview of Hyperliquid's EVM chain, including architecture and features. > **[EVM Transactions with Privy](/wallets/using-wallets/ethereum/send-a-transaction)** > > 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 > [!TIP] > > 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. > [!NOTE] > > 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 > **Organize by strategy** > > 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. > **Set appropriate expirations** > > For automated trading systems, set reasonable expiration times on agent wallets to limit > exposure if a key is compromised. > **Monitor agent permissions** > > Regularly audit which agent wallets are registered and revoke access for agents that are no > longer needed. ## Next Steps > **[Subaccounts](/recipes/hyperliquid/subaccounts)** > > Learn how to use subaccounts for risk isolation > **[Executing trades](/recipes/hyperliquid/trading-patterns)** > > 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. > [!NOTE] > > 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) }); ``` > [!NOTE] > > 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 > **Monitor builder code performance** > > Track the fee revenue from your builder code to understand user engagement and optimize your > application. > **Set competitive builder fees** > > Balance between earning revenue and providing value to users. Lower fees may attract more users. > **Test on testnet first** > > Always test your builder code integration on testnet before going live. ## Resources > **[Builder Codes Documentation](https://hyperliquid.gitbook.io/hyperliquid-docs/trading/builder-codes)** > > Official guide to order-level revenue sharing ## Next Steps > **[Executing trades](/recipes/hyperliquid/trading-patterns)** > > 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 }; } ``` > [!NOTE] > > **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 > **Initialize agent on first use** > > Create and approve the agent wallet the first time a user wants to trade. Store the agent wallet > reference for future sessions. > **Handle wallet disconnections** > > Implement proper error handling for when users disconnect their external wallet. The agent > wallet can continue operating, but User Signed Actions will fail. > **Clear user feedback** > > Communicate to users which wallet is being used for each action. For example, "Placing order > with trading wallet" vs "Approve with MetaMask". ## Next Steps > **[Agent wallets](/recipes/hyperliquid/agents-and-subaccounts)** > > Learn more about agent wallets > **[Executing trades](/recipes/hyperliquid/trading-patterns)** > > 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. > [!NOTE] > > 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. > [!NOTE] > > 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](https://mintcdn.com/privy-c2af3412/p9eun2yyY6PeENXT/images/hyperevm-custom-chain.png?fit=max\&auto=format\&n=p9eun2yyY6PeENXT\&q=85\&s=2fcb2aa00949781113aed4418e06f90b) * **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`) > [!WARNING] > > 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 > **[HyperEVM Documentation](https://hyperliquid.gitbook.io/hyperliquid-docs/hyperevm)** > > Official HyperEVM smart contract documentation > **[Smart Wallets Documentation](https://docs.privy.io/wallets/using-wallets/evm-smart-wallets/overview)** > > Learn more about using smart wallets, including how to send transactions, batch operations, and > configure advanced features. ## Next Steps > **[Building on Hyperliquid with Privy](/recipes/hyperliquid-guide)** > > Return to the getting started guide > **[Executing trades](/recipes/hyperliquid/trading-patterns)** > > 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: > **[Signers](/wallets/using-wallets/signers/overview)** > > Learn how to create additional signers for your wallets > **[Policies](/controls/policies/overview)** > > 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 > [!NOTE] > > 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, }); ``` > [!NOTE] > > 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 > **Use separate keys for different functions** > > Create dedicated authorization keys for trading operations vs. administrative functions. > **Start with restrictive policies** > > Begin with strict policies and gradually relax them as needed, rather than starting permissive. > **Test policies in testnet** > > Always validate your policy configuration on Hyperliquid testnet before deploying to production. > **Regular policy reviews** > > Periodically review and update policies to match current risk management needs. ## Next Steps > **[Agent wallets](/recipes/hyperliquid/agents-and-subaccounts)** > > Learn about managing multiple accounts > **[Builder Codes](/recipes/hyperliquid/builder-codes)** > > Earn fees with builder codes on Hyperliquid orders > **[HyperEVM](/recipes/hyperliquid/hyperevm)** > > 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. > [!NOTE] > > 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 > [!WARNING] > > 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: "" }); ``` > [!NOTE] > > 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' }); ``` > [!TIP] > > 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 > **Use subaccounts for risk isolation** > > 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. > **Monitor subaccount balances** > > Regularly check subaccount balances and positions to ensure proper risk allocation across your > strategies. > **Consolidate for fee benefits** > > Remember that all subaccount trading volume rolls up to the master account, so you maintain your > fee tier benefits across all strategies. ## Next Steps > **[Agent wallets](/recipes/hyperliquid/agents-and-subaccounts)** > > Learn about agent wallets for programmatic trading > **[Executing trades](/recipes/hyperliquid/trading-patterns)** > > 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. > [!NOTE] > > 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); ``` > [!NOTE] > > **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' }); ``` > [!WARNING] > > 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: > **Gtc (Good-'Til-Canceled)** > > 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" } } > ``` > **Ioc (Immediate-Or-Cancel)** > > 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" } } > ``` > **Alo (Add-Liquidity-Only / Post-Only)** > > 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. > [!NOTE] > > 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 > [!NOTE] > > 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); ``` > [!WARNING] > > 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 > **Always validate prices** > > Before placing orders, verify that prices are within expected ranges to avoid fat-finger errors. > **Use reduce-only orders for exits** > > When closing positions, use reduce-only orders to prevent accidentally opening new positions. > **Monitor funding rates** > > Keep track of funding rates, especially for large positions, as they can significantly impact > profitability. > **Implement circuit breakers** > > Add safeguards to stop trading if certain thresholds are hit (e.g., max daily loss, max position > size). ## Next Steps > **[Agent wallets](/recipes/hyperliquid/agents-and-subaccounts)** > > Learn about agent wallets for programmatic trading > **[Policies](/recipes/hyperliquid/policies-and-offline-actions)** > > 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 > **[Lens Docs](https://lens.xyz/docs/protocol/getting-started/react)** > > Official documentation for Lens protocol and SDK. > **[Privy Wagmi integration](/wallets/connectors/ethereum/integrations/wagmi)** > > Configure Privy with wagmi for EVM integrations. *** ## Integrate Lens login **1. 1. Install dependencies** ```bash theme={"system"} pnpm add @privy-io/react-auth @lens-protocol/react@canary @privy-io/wagmi @tanstack/react-query wagmi viem ``` > [!TIP] > > We use `@lens-protocol/react@canary` to access the latest Lens features. **2. 2. Set up providers** 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. **providers.tsx** ```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 ++] */} ); ``` **privy-config.ts** ```ts {skip-check} theme={"system"} import {lens, lensTestnet} from 'viem/chains'; export const privyConfig = { supportedChains: [lens, lensTestnet] }; ``` **wagmi-config.ts** ```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. **3. 3. Connect a wallet with Privy** 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. > [!TIP] > > You can customize whether to automatically create an embedded wallet for the user. See the > [automatic wallet creation guide](/basics/react/advanced/automatic-wallet-creation). ```tsx theme={"system"} import {usePrivy} from '@privy-io/react-auth'; const {login} = usePrivy(); ; ``` **4. 4. Fetch accounts for the connected wallet** 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 }); ``` > **Switching wallets (if the user has more than one wallet)** > > 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]); > ``` **5. 5. Implement login with Lens** 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) }); }; } ``` **6. 6. Render accounts and trigger login** 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) => (
))}
); } ``` **7. 7. Check authentication status** 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). # Trade on Lighter Source: https://docs.privy.io/recipes/lighter-guide Use Privy wallets to create a Lighter account, fund it, register a trading API key, and place perpetual futures orders. [Lighter](https://lighter.xyz/) is a high-performance, zk-based orderbook exchange for perpetual futures. It settles on Ethereum with a validity-proof rollup, offering low fees and verifiable execution. This guide shows how to use Privy wallets with Lighter: owning a user's Lighter account, registering a trading API key on-chain, funding the account, and placing orders. ## How Lighter differs from a typical EVM integration Unlike most exchanges, Lighter separates authority into two layers, and it's important to understand which key does what before you start: * **L1 account owner (the Privy wallet).** A standard Ethereum address. This wallet owns the Lighter account and signs every on-chain action: registering a trading API key (`changePubKey`), deposits, and withdrawals. All L1 interactions can be sponsored with native [gas sponsorship](/wallets/gas-and-asset-management/gas/overview). * **L2 trading API key (a Lighter keypair).** Use Lighter API keys to sign and submit orders directly to the order book. Lighter signs orders with a separate API key using its own signature scheme. This key is generated by the Lighter SDK and managed by your application. > [!NOTE] > > A Privy embedded or server wallet cannot produce Lighter's L2 order signatures — that scheme is > not ECDSA. Use the Privy embedded wallet to authorize the trading API key on-chain. Your > application holds the Lighter API key that signs orders, and Privy never has to expose a raw > private key to do so. This guide shows how to: * Create a Lighter account * Add funds to the Lighter account * Register a Lighter trading API key * Sign and submit trades to the Lighter order book ## Prerequisites Before you begin, make sure you have: * A [Privy app](https://dashboard.privy.io) with [gas sponsorship enabled](/wallets/gas-and-asset-management/gas/setup). Lighter's contract is on **Ethereum mainnet**, so sponsor there (or fund the wallet with ETH for gas). * Python 3.10+. * The Lighter contract address on Ethereum mainnet: `0x3B4D794a66304F130a4Db8F2551B0070dfCf5ca7`. ## Installation ```bash theme={"system"} pip install privy-client git+https://github.com/elliottech/lighter-python.git web3 requests ``` This guide is Python throughout: Privy's on-chain steps use the [Privy Python SDK](https://github.com/privy-io/python-sdk), and Lighter ships its order signer for Python and Go but not JavaScript. ## 1. Create the account owner wallet Create the Ethereum wallet that will own the Lighter account. ```python theme={"system"} from privy import PrivyAPI privy = PrivyAPI(app_id='insert-your-app-id', app_secret='insert-your-app-secret') wallet = privy.wallets.create(chain_type='ethereum') owner_address = wallet.address ``` ## 2. Fund the account To activate a Lighter account, deposit USDC directly into the Privy wallet. The minimum direct deposit is **1 USDC**. USDC on Ethereum mainnet is `0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48`. A deposit is two ordinary transactions signed by the Privy wallet: an ERC-20 `approve` to the Lighter contract, then `deposit(to, assetIndex, routeType, amount)` with `routeType = 0` for the perps account. ```python theme={"system"} from web3 import Web3 LIGHTER = Web3.to_checksum_address('0x3B4D794a66304F130a4Db8F2551B0070dfCf5ca7') USDC = Web3.to_checksum_address('0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48') ETHEREUM = 'eip155:1' w3 = Web3(Web3.HTTPProvider('https://your-ethereum-rpc-url')) amount = 5 * 10**6 # 5 USDC (6 decimals) erc20 = w3.eth.contract(abi=[{ 'name': 'approve', 'type': 'function', 'stateMutability': 'nonpayable', 'inputs': [{'name': 'spender', 'type': 'address'}, {'name': 'amount', 'type': 'uint256'}], 'outputs': [{'name': '', 'type': 'bool'}], }]) lighter = w3.eth.contract(address=LIGHTER, abi=[ { 'name': 'deposit', 'type': 'function', 'stateMutability': 'payable', 'inputs': [ {'name': '_to', 'type': 'address'}, {'name': '_assetIndex', 'type': 'uint16'}, {'name': '_routeType', 'type': 'uint8'}, {'name': '_amount', 'type': 'uint256'}, ], 'outputs': [], }, { 'name': 'tokenToAssetIndex', 'type': 'function', 'stateMutability': 'view', 'inputs': [{'name': '', 'type': 'address'}], 'outputs': [{'name': '', 'type': 'uint16'}], }, ]) # Read USDC's asset index from the Lighter contract usdc_asset_index = lighter.functions.tokenToAssetIndex(USDC).call() def send_tx(to, data): # Sign and broadcast an EVM transaction from the Privy wallet, with gas sponsored. return privy.intents.rpc( wallet.id, method='eth_sendTransaction', caip2=ETHEREUM, sponsor=True, params={'transaction': {'to': to, 'data': data}}, ) # 1. Approve, then 2. Deposit into the perps account (routeType 0) send_tx(USDC, erc20.encode_abi('approve', args=[LIGHTER, amount])) send_tx(LIGHTER, lighter.encode_abi('deposit', args=[owner_address, usdc_asset_index, 0, amount])) ``` > [!NOTE] > > Ethereum mainnet gas can be significant. For cheaper funding from Arbitrum, Base, and other > chains, Lighter also supports deposits through the Fun.xyz Universal Deposit Address — see the > [deposits guide](https://apidocs.lighter.xyz/docs/deposits-transfers-and-withdrawals). API-key > registration in the next step is always a one-time mainnet transaction. ## 3. Register a trading API key Generate a Lighter API keypair with the SDK, then register its public key on-chain with `changePubKey`, signed by the Privy wallet. The signature scheme means the key is registered through the contract directly rather than handing a raw private key to the SDK — the recommended path when the account owner is a smart-contract or multi-sig wallet. First, find the account index (assigned when the account was funded) and generate the keypair: ```python theme={"system"} import lighter, requests # Look up the account index for the L1 address via Lighter's API resp = requests.get( 'https://mainnet.zklighter.elliot.ai/api/v1/accountsByL1Address', params={'l1_address': owner_address}, ).json() account_index = resp['sub_accounts'][0]['index'] # Generate a Lighter API keypair (this does NOT need the Ethereum key) api_private_key, api_public_key, err = lighter.create_api_key() if err is not None: raise RuntimeError(err) # Store api_private_key securely — your application signs orders with it. ``` Then register the public key on-chain from the Privy wallet. `changePubKey(accountIndex, apiKeyIndex, pubKey)` must be sent by the L1 address that owns the account (the Privy wallet). API key indices `2`–`254` are available (`0`–`1` are reserved). ```python theme={"system"} API_KEY_INDEX = 2 change_pub_key = w3.eth.contract(abi=[{ 'name': 'changePubKey', 'type': 'function', 'stateMutability': 'nonpayable', 'inputs': [ {'name': '_accountIndex', 'type': 'uint48'}, {'name': '_apiKeyIndex', 'type': 'uint8'}, {'name': '_pubKey', 'type': 'bytes'}, ], 'outputs': [], }]) pub_key_bytes = bytes.fromhex(api_public_key.removeprefix('0x')) send_tx(LIGHTER, change_pub_key.encode_abi( 'changePubKey', args=[account_index, API_KEY_INDEX, pub_key_bytes] )) ``` > [!WARNING] > > Keep `api_private_key` server-side and out of source control. It can place and cancel orders on > the account, but it cannot move funds — deposits and withdrawals still require the Privy-owned L1 > wallet. ## 4. Place an order With the API key registered, sign and submit orders using the Lighter `SignerClient`. This uses the API key, not the Privy wallet. ```python theme={"system"} import asyncio import lighter BASE_URL = 'https://mainnet.zklighter.elliot.ai' async def main(): client = lighter.SignerClient( url=BASE_URL, account_index=account_index, api_private_keys={API_KEY_INDEX: api_private_key}, ) # Place a limit buy order created_order, api_response, err = await client.create_order( market_index=0, # e.g. 0 = BTC perp client_order_index=0, # your own client-side id base_amount=100, # size, in the market's base units price=95000, # limit price, in the market's price units is_ask=False, # False = buy, True = sell order_type=lighter.SignerClient.ORDER_TYPE_LIMIT, time_in_force=lighter.SignerClient.ORDER_TIME_IN_FORCE_GOOD_TILL_TIME, ) if err is not None: raise RuntimeError(err) asyncio.run(main()) ``` > [!NOTE] > > `base_amount` and `price` are integers scaled by each market's size and price decimals. Fetch > market metadata (decimals, indices) from Lighter's `orderBookDetails` endpoint before placing > orders. See the [trading guide](https://apidocs.lighter.xyz/docs/trading) for order types and > time-in-force options. ## Withdrawing Withdrawals return funds to the account owner and are authorized by the Privy L1 wallet, either through the contract's `withdraw` function or Lighter's fast-withdraw flow (min 4 USDC). See the [withdrawals guide](https://apidocs.lighter.xyz/docs/deposits-transfers-and-withdrawals). ## Resources > **[Lighter API Docs](https://apidocs.lighter.xyz/docs/partner-integration)** > > Lighter's partner integration, trading, and deposit documentation. > **[Lighter Python SDK](https://github.com/elliottech/lighter-python)** > > The signer client, API-key generation, and order examples. > **[EVM Transactions with Privy](/wallets/using-wallets/ethereum/send-a-transaction)** > > Learn how to send EVM transactions using Privy wallets. ## Why use Privy with Lighter? * **Security**: The account owner's private key never leaves Privy's secure infrastructure. * **Multi-sig ready**: On-chain registration via `changePubKey` works for smart-contract and multi-sig owners, no raw key required. * **Simplicity**: No key storage or rotation to manage for the account owner. * **Gas sponsorship**: Sponsor account setup, deposits, and withdrawals so users never hold ETH. > [!TIP] > > You're ready to build trading applications on Lighter with Privy. # 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. > [!WARNING] > > 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`. > [!WARNING] > > 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 prebuilt 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. > [!WARNING] > > 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. > [!NOTE] > > 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. > [!NOTE] > > 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}!
; } ``` > [!WARNING] > > 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) > [!WARNING] > > 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. # Handle account transfer in your mobile app Source: https://docs.privy.io/recipes/mobile-account-transfer Catch account transfer conflicts on iOS and Android, and let users move a login method from an orphaned account to their currently authenticated account **When a user tries to link a login method that's already linked to a different Privy user, the SDK surfaces a transfer conflict instead of failing outright.** Your app can catch this conflict, confirm the transfer with the user, and re-submit the login method to move it onto the currently authenticated account. > [!NOTE] > > Programmatic account transfer is currently available on the Privy iOS and Android native SDKs > only. > [!WARNING] > > **Login method transfer must be enabled in your [dashboard](https://dashboard.privy.io)** under > **User management > Authentication > Login method transfer** before the SDK will surface transfer > conflicts. See [Configure account transfer](/recipes/dashboard/account-transfer) for background on > the underlying flow, including the current limitation that the orphaned account must have only one > login method. ## How it works 1. Your app calls a `link` (or `linkWithCode`) method for email, SMS, OAuth, SIWE, or SIWS. 2. If that login method is already linked to a **different** Privy user who has no other login methods, the call fails with a transfer conflict instead of a generic link error. The conflict carries a one-time `nonce` and `ConflictingUserMetadata` describing the other account. 3. Your app shows a confirmation prompt (e.g. "Transfer this email from *display name*?") using the metadata. 4. If the user confirms, your app re-proves ownership of the login method (resubmit the OTP code, redo the OAuth flow, or re-sign the SIWE/SIWS message) and calls the corresponding `transferWithCode`/`transferAccount` method with the `nonce`. 5. On success, the login method moves to the current user, and the orphaned account is deleted. > [!WARNING] > > Transferring an account only moves the login method itself. It does **not** transfer any embedded > wallet from the orphaned account — that wallet, if any, remains with the deleted account's assets > orphaned along with it. Don't let users trigger a transfer without warning them about this if the > conflicting account might hold funds. ## Catching the transfer conflict **iOS** On iOS, a transfer conflict is thrown as a `PrivyError` with `errorCode` `.authenticationFailure(.accountTransferRequired(nonce:conflictingUser:))`: ```swift theme={"system"} do { let user = try await privy.email.linkWithCode(code, sentTo: email) } catch let privyError as PrivyError { guard case .authenticationFailure(.accountTransferRequired(let nonce, let conflictingUser)) = privyError.errorCode else { // Some other link failure — handle normally return } // Show a confirmation prompt using `conflictingUser`, then call // privy.email.transferWithCode(nonce:code:sentTo:) if the user confirms. } ``` **Android** On Android, methods return `Result` rather than throwing. Inspect the failure for an `AccountTransferRequiredException`: ```kotlin theme={"system"} val result = privy.email.linkWithCode(code, email) result.onFailure { error -> if (error is AccountTransferRequiredException) { // Show a confirmation prompt using error.conflictingUser, then call // privy.email.transferWithCode(error.nonce, code, email) if the user confirms. } else { // Some other link failure — handle normally } } ``` ### ConflictingUserMetadata Use these fields only to render the confirmation prompt. They identify a different Privy user, so avoid logging, persisting, or sending them to analytics. | Field | Description | | - | - | | `displayName` | A display name for the conflicting account (email address or OAuth name) | | `embeddedWalletAddress` | The conflicting user's embedded wallet address, if any | | `farcasterEmbeddedAddress` | The conflicting user's Farcaster-linked embedded wallet address, if any | | `oAuthUserInfo` | OAuth user info fields for the conflicting account, if any | ## Transferring by login method Each method requires you to re-prove ownership of the login method before the transfer nonce is accepted — a stale OTP code or signature won't work, so re-collect it after the user confirms. ### Email The nonce is tied to a fresh OTP, so send a new code to the same email address before calling `transferWithCode` — the code from the original `linkWithCode` attempt won't work. **iOS** ```swift theme={"system"} try await privy.email.sendCode(to: email) // Once the user has entered the new code: let user = try await privy.email.transferWithCode( nonce: nonce, code: code, // the newly-sent OTP, not the original one sentTo: email ) ``` **Android** ```kotlin theme={"system"} privy.email.sendCode(email) // Once the user has entered the new code: val result = privy.email.transferWithCode(nonce, code, email) // the newly-sent OTP, not the original one ``` ### SMS The nonce is tied to a fresh OTP, so send a new code to the same phone number before calling `transferWithCode` — the code from the original `linkWithCode` attempt won't work. **iOS** ```swift theme={"system"} try await privy.sms.sendCode(to: phoneNumber) // Once the user has entered the new code: let user = try await privy.sms.transferWithCode( nonce: nonce, code: code, // the newly-sent OTP, not the original one sentTo: phoneNumber ) ``` **Android** ```kotlin theme={"system"} privy.sms.sendCode(phoneNumber) // Once the user has entered the new code: val result = privy.sms.transferWithCode(nonce, code, phoneNumber) // the newly-sent OTP, not the original one ``` ### OAuth A fresh OAuth browser flow must run to produce new credentials before the transfer can complete. **iOS** ```swift theme={"system"} let user = try await privy.oAuth.transferAccount( nonce: nonce, with: provider, appUrlScheme: appUrlScheme ) ``` **Android** ```kotlin theme={"system"} val result = privy.oAuth.transferAccount(nonce, oAuthProvider, appUrlScheme) ``` ### SIWE (Sign-In with Ethereum) Generate a fresh SIWE message and signature — don't reuse the one from the original `link` call. **iOS** ```swift theme={"system"} let message = try await privy.siwe.generateMessage(params: params) let signature = try await wallet.signMessage(message) // your wallet's signing call let user = try await privy.siwe.transferAccount( nonce: nonce, message: message, signature: signature, params: params ) ``` **Android** ```kotlin theme={"system"} val message = privy.siwe.generateMessage(params).getOrThrow() val signature = wallet.signMessage(message) // your wallet's signing call val result = privy.siwe.transferAccount(nonce, message, signature, params) ``` ### SIWS (Sign-In with Solana) Generate a fresh SIWS message and signature — don't reuse the one from the original `link` call. **iOS** ```swift theme={"system"} let message = try await privy.siws.generateMessage(params: params) let signature = try await wallet.signMessage(message) // your wallet's signing call let user = try await privy.siws.transferAccount( nonce: nonce, message: message, signature: signature, params: params ) ``` **Android** ```kotlin theme={"system"} val message = privy.siws.generateMessage(params).getOrThrow() val signature = wallet.signMessage(message) // your wallet's signing call val result = privy.siws.transferAccount(nonce, message, signature, params) ``` > [!TIP] > > If the transfer call itself fails (e.g. the nonce expired), surface a normal error and let the > user restart the link flow from the beginning — nonces are one-time use and tied to the > credentials submitted alongside them. # Mobile SDK breadcrumbs Source: https://docs.privy.io/recipes/mobile-breadcrumbs Register a breadcrumb handler to receive lightweight diagnostic events from the Privy iOS and Android SDKs and route them to your own analytics or observability tooling **Breadcrumbs are lightweight diagnostic events emitted as the Privy SDK progresses through key flows**, such as session restoration and wallet creation. Register a breadcrumb handler to receive these events and forward them to your own analytics, logging, or observability tooling (e.g. Sentry, Datadog, Firebase). > [!NOTE] > > Breadcrumbs are currently available on the Privy iOS and Android native SDKs only. > [!TIP] > > Breadcrumbs are for lightweight operational visibility, not full error reporting. Payloads never > contain PII, and handlers must not throw or block, since they're called synchronously on the > emitting thread. ### 1. Register a breadcrumb handler Implement the platform's breadcrumb handler interface and pass it to your `PrivyConfig` when initializing the SDK. **iOS** ```swift theme={"system"} import PrivySDK struct LoggingBreadcrumbHandler: PrivyBreadcrumbHandler { func receive(_ breadcrumb: PrivyBreadcrumb) { print("PrivyBreadcrumb: \(breadcrumb.event) timestamp=\(breadcrumb.timestamp) data=\(breadcrumb.data)") // Forward to your analytics or observability tool, e.g.: // Analytics.track(breadcrumb.event, properties: breadcrumb.data) } } let config = PrivyConfig( appId: privyAppId, appClientId: privyAppClientId, breadcrumbHandler: LoggingBreadcrumbHandler() ) ``` **Android** ```kotlin theme={"system"} import io.privy.breadcrumbs.PrivyBreadcrumbHandler import io.privy.sdk.PrivyConfig val config = PrivyConfig( appId = privyAppId, appClientId = privyAppClientId, breadcrumbHandler = PrivyBreadcrumbHandler { breadcrumb -> Log.d("PrivyBreadcrumb", "${breadcrumb.event} timestamp=${breadcrumb.timestamp} data=${breadcrumb.data}") // Forward to your analytics or observability tool, e.g.: // analytics.track(breadcrumb.event, breadcrumb.data) }, ) ``` `PrivyBreadcrumbHandler` is a functional interface, so a lambda works directly wherever a `PrivyConfig` is constructed. `breadcrumbHandler` is optional. If it's omitted, the SDK emits no breadcrumbs and there's no performance cost. ### 2. Breadcrumb payload shape Every breadcrumb exposes the same three fields, regardless of event type: | Field | Type | Description | | - | - | - | | `event` | `String` | Dotted event name, e.g. `auth.session_restore.started` | | `timestamp` | `Date` (iOS) / `Long` epoch millis (Android) | The time the event occurred | | `data` | `[String: String]` (iOS) / `Map` (Android) | Additional context for the event. Values are strings only | `receive(_:)` is called synchronously on the thread that triggered the event. Keep handler implementations fast and non-blocking, and dispatch any expensive work (network calls, disk writes) to a background queue. ### 3. Available breadcrumb events **Session restore events** Emitted when the SDK attempts to restore a cached session on startup. | Event | When it fires | | - | - | | `auth.session_restore.started` | A prior session was found and restoration has begun | | `auth.session_restore.completed` | The session was successfully refreshed and restored | | `auth.session_restore.failed` | Restoration failed; `data.error` describes the failure | **Wallet events** Emitted for embedded wallet creation, migration, and bridge operations. | Event | When it fires | | - | - | | `wallet.create.started` | Embedded wallet creation began; `data.chain-type` is `ethereum` or `solana` | | `wallet.create.completed` | Embedded wallet creation succeeded | | `wallet.create.failed` | Embedded wallet creation failed; `data.error` describes the failure | | `wallet.migration.started` | Migration of legacy embedded wallets to the TEE stack began | | `wallet.migration.completed` | Migration completed successfully | | `wallet.migration.failed` | Migration failed; `data.error` describes the failure | | `wallet.bridge.failed` | The webview/iframe bridge failed to complete an operation (timeout or transport error), as opposed to the operation being rejected by the server | > [!WARNING] > > On iOS, `data.error` is a stable, non-sensitive error code (the `PrivyError` code hierarchy, or > just the Swift type name for non-`PrivyError` types). On Android, `data.error` is currently the > underlying exception's message. Avoid logging Android `data.error` values to systems where raw > exception messages shouldn't be stored, and treat both as best-effort diagnostic strings rather > than a stable enum. ### 4. Example: forwarding to Sentry **iOS** ```swift theme={"system"} import Sentry struct SentryBreadcrumbHandler: PrivyBreadcrumbHandler { func receive(_ breadcrumb: PrivyBreadcrumb) { let crumb = Breadcrumb(level: .info, category: "privy") crumb.message = breadcrumb.event crumb.data = breadcrumb.data SentrySDK.addBreadcrumb(crumb) } } ``` **Android** ```kotlin theme={"system"} import io.sentry.Breadcrumb import io.sentry.Sentry import io.privy.breadcrumbs.PrivyBreadcrumbHandler val sentryBreadcrumbHandler = PrivyBreadcrumbHandler { breadcrumb -> val crumb = Breadcrumb().apply { message = breadcrumb.event category = "privy" breadcrumb.data.forEach { (key, value) -> setData(key, value) } } Sentry.addBreadcrumb(crumb) } ``` > [!TIP] > > Because breadcrumbs carry no PII and are cheap to emit, it's safe to register a handler in every > build configuration, including production. Gate any verbose console logging (like the examples in > step 1) behind a debug flag. # 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( ); ``` > [!TIP] > > Host this page on a domain listed in the app's allowed origins. Configure allowed origins in the > [Privy Dashboard](https://dashboard.privy.io). > [!WARNING] > > 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. **React Native** ```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). **3. Create a policy to bypass MFA** Create a policy that defines which transactions can be executed without MFA. For example, allow OUSD transfers under 1000 OUSD 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 OUSD_TEMPO_ADDRESS = "0x20c0000000000000000000006a37da5c996874be"; const privy = new PrivyClient({ appId: PRIVY_APP_ID, appSecret: PRIVY_APP_SECRET, }); const policy = await privy.policies().create({ name: "OUSD Transfer Policy", version: "1.0", chain_type: "ethereum", rules: [ { name: "OUSD Transfer Policy", method: "eth_sendTransaction", action: "ALLOW", conditions: [ { field_source: "ethereum_transaction", field: "to", operator: "eq", value: OUSD_TEMPO_ADDRESS, }, { field_source: "ethereum_calldata", field: "transfer.amount", abi: erc20Abi, operator: "lte", value: parseUnits("1000", 6).toString(), // 1000 OUSD } ] }, ] }); ``` This policy allows transactions to the OUSD contract with transfer amounts up to 1000 OUSD. 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). **4. Add a signer to the wallet** 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). **5. Route transactions through the appropriate endpoint** 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:4217', 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 {tempo} from 'viem/chains'; const OUSD_TEMPO_ADDRESS = "0x20c0000000000000000000006a37da5c996874be"; const { user } = usePrivy(); const { sendTransaction } = useSendTransaction(); const handleSignTransaction = async () => { if (!user || !user.wallet) { return; } const transaction = { to: OUSD_TEMPO_ADDRESS, data: encodeFunctionData({ abi: erc20Abi, functionName: "transfer", args: [ "0xE3070d3e4309afA3bC9a6b057685743CF42da77C", parseUnits(amount, 6), ], }), chain_id: tempo.id, }; // If transaction satisfies policy (OUSD transfer under 1000), sign with additional signer that does not require MFA if ( transaction.to === OUSD_TEMPO_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 (OUSD 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. > **[Polymarket](https://polymarket.com/)** > > The world's largest prediction market platform. > **[Builder Program](https://docs.polymarket.com/developers/builders/builder-intro)** > > Official builder program documentation. > **[Polymarket builder code example repo](https://github.com/Polymarket/privy-safe-builder-example)** > > Official Next.js example with Privy authentication and Polymarket trading. ## Getting started **1. Create a Privy app** Sign up at [dashboard.privy.io](https://dashboard.privy.io) and create an app. **2. Get builder credentials** Obtain your builder API key, secret, and passphrase from [Polymarket](https://polymarket.com/settings?tab=builder). **3. Get a Polygon RPC URL** 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 }); } ``` > [!WARNING] > > 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 > **Privy authentication failed** > > Verify `NEXT_PUBLIC_PRIVY_APP_ID` is set correctly. Check that your domain is allowed in the > Privy dashboard. > **Failed to initialize relay client** > > Check builder credentials in `.env.local`. Verify the `/api/polymarket/sign` endpoint is > accessible. > **Safe deployment failed** > > Ensure the Polygon RPC URL is valid. The user must approve the signature via Privy. > **Token approval failed** > > The Safe must be deployed first. Verify the builder relay service is operational. > **Orders not appearing** > > Verify the trading session is complete and the Safe has a USDC.e balance. Wait 2-3 seconds for > CLOB sync. ## Resources > **[CLOB API](https://docs.polymarket.com/developers/CLOB/introduction)** > > Order book API for placing and managing orders. > **[Relayer client](https://docs.polymarket.com/developers/builders/relayer-client)** > > 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. **NodeJS** 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. **Python** Use `client.users.create` to create a user and pregenerate wallets in one operation. ```python theme={"system"} user = client.users.create( linked_accounts=[ {"type": "email", "address": "user@example.com"}, ], wallets=[ {"chain_type": "ethereum"}, {"chain_type": "solana"}, ], ) ``` **Java** To pregenerate embedded wallets for a new user, use the `createUser` method. > [!TIP] > > 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().orElse(e.getMessage()); System.err.println(errorBody); } catch (Exception e) { System.err.println(e.getMessage()); } ``` **REST API** 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 **linked\_accounts** `LinkedAccountType[]` *required* An array of linked accounts to associate with the user. **custom\_metadata** `CustomMetadataType` Custom metadata to associate with the user. **wallets** `WalletCreateRequestType[]` An array of wallets to create for the user. **chain\_type** `'ethereum' | 'solana' | 'stellar' | 'cosmos' | 'sui' | 'tron' | 'bitcoin-segwit' | 'near' | 'ton' | 'starknet' | 'movement'` *required* The chain type of the wallet to create. **wallet\_index** `number` The HD wallet index to use for wallet generation. Defaults to `0`. **policy\_ids** `string[]` List of policy IDs for policies that should be enforced on all transactions on the wallet, regardless of signer. **additional\_signers** `object[]` **signer\_id** `string` The ID of the signer. **override\_policy\_ids** `string[]` List of policy IDs for policies specific to this signer. Currently, only one policy is supported per wallet. **create\_smart\_account** `boolean` 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`. **create\_direct\_signer** `boolean` 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. **NodeJS** 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. **Go** 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. **Ruby** 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. **Python** Use the `pregenerate_wallets` method on the users service. ```python theme={"system"} user = client.users.pregenerate_wallets( "did:privy:xxxxx", wallets=[{"chain_type": "ethereum"}], ) ``` **REST API** 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 **user\_id** `string` *required* The Privy user ID to create wallets for. **wallets** `WalletCreateRequestType[]` An array of wallets to create for the user. **chain\_type** `'ethereum' | 'solana' | 'stellar' | 'cosmos' | 'sui' | 'tron' | 'bitcoin-segwit' | 'near' | 'ton' | 'starknet' | 'movement'` *required* The chain type of the wallet to create. **wallet\_index** `number` The HD wallet index to use for wallet generation. Defaults to `0`. **policy\_ids** `string[]` List of policy IDs for policies that should be enforced on all transactions on the wallet, regardless of signer. **additional\_signers** `object[]` **signer\_id** `string` The ID of the signer. **override\_policy\_ids** `string[]` List of policy IDs for policies specific to this signer. Currently, only one policy is supported per wallet. **create\_smart\_account** `boolean` 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`. **create\_direct\_signer** `boolean` 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. **REST API** 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 **users** `UserImportRequest[]` *required* An array of user objects to import. Each object has the following fields: **linked\_accounts** `LinkedAccountType[]` *required* An array of linked accounts to associate with the user. **custom\_metadata** `CustomMetadataType` Custom metadata to associate with the user. **wallets** `WalletCreateRequestType[]` An array of wallets to create for the user. **chain\_type** `'ethereum' | 'solana' | 'stellar' | 'cosmos' | 'sui' | 'tron' | 'bitcoin-segwit' | 'near' | 'ton' | 'starknet' | 'movement'` *required* The chain type of the wallet to create. **wallet\_index** `number` The HD wallet index to use for wallet generation. Defaults to `0`. **policy\_ids** `string[]` List of policy IDs for policies that should be enforced on all transactions on the wallet, regardless of signer. **additional\_signers** `object[]` **signer\_id** `string` The ID of the signer. **override\_policy\_ids** `string[]` List of policy IDs for policies specific to this signer. Currently, only one policy is supported per wallet. **create\_smart\_account** `boolean` 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`. **create\_direct\_signer** `boolean` 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 **Phantom** 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 | **Backpack** 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 | **Other wallet providers** 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 | > [!NOTE] > > 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); } }; ``` > [!NOTE] > > 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 🎉** > [!NOTE] > > 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; } ``` > [!NOTE] > > `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 > **[Chrome Extension Starter](https://github.com/privy-io/examples/tree/main/examples/privy-react-chrome-extension)** > > 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';" } } ``` > [!WARNING] > > 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). > **Security best practices** > > #### 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';" > } > } > ``` > > > [!WARNING] > > > > 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/*"] > } > ``` > > > [!TIP] > > > > 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 > } > } > ``` > > > [!WARNING] > > > > 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/*"] > } > ] > } > ``` > > > [!TIP] > > > > 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); > } > }); > ``` > > > [!NOTE] > > > > Content scripts can be compromised by malicious websites, so treat all messages from content > > scripts as potentially malicious. *** ## Configure your Privy dashboard > [!NOTE] > > 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** > [!TIP] > > 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 **1. User initiates social login** ### 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 }); }; ``` **2. Open authentication context (options page or popup window)** Both approaches use the same authentication logic: > [!TIP] > > 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 = () => ( ); ``` **3. Redirect back to the extension** 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: **Webpack** ```js theme={"system"} // webpack.config.js module.exports = { resolve: { alias: { '@marsidev/react-turnstile': false, '@hcaptcha/react-hcaptcha': false, }, }, }; ``` **Vite** 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. > [!WARNING] > > 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 > [!TIP] > > 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 **EVM** 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. **Solana** 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` > [!NOTE] > > 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. | **EVM** ```tsx theme={"system"} connectWallet({ description: 'Connect your wallet to access the app', walletList: ['metamask', 'safe'] }); ``` **Solana** ```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. > [!NOTE] > > 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 ; } ``` > [!TIP] > > 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: **EVM** ```tsx theme={"system"} import {useWallets} from '@privy-io/react-auth'; const {wallets} = useWallets(); wallets[0].loginOrLink(); ``` **Solana** ```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: **EVM** ```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 */} ); } ``` **Solana** ```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. > [!NOTE] > > 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.** > [!NOTE] > > Once your domain is verified, the corresponding App ID can only be used on that **exact** > production domain. > [!NOTE] > > 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 By default an app serves HttpOnly cookies on a single **cookie domain**. With **app clients**, one app can serve cookies on **multiple parent domains** — one per client — while keeping a single shared set of users and wallets. This is useful when the same Privy app powers products on different domains (for example `example.com` and `example.io`). Each app client either: * **Inherits the app's cookie domain** — the client uses whatever domain and cookie settings are configured at the app level. * **Owns a client-specific cookie domain** — the client has its own verified domain, independent of the app's. Verifying or changing one client's domain never disrupts another's. > [!NOTE] > > Cookies are only ever set once a base domain is verified. If a client inherits but no domain is > set on the app or a client owns a domain that is not yet verified no cookies are set for that > client. #### Configuring cookies per client In the [**Privy Dashboard**](https://dashboard.privy.io/), open **Configuration > App settings > Domains and clients**. The **Configure by client** card lists your app clients; use **Create client** to add one. For each client, choose how it resolves its cookie domain: * **Use the app's cookie domain** — the client inherits the app-level domain. No additional DNS setup is needed. * **Set up a different domain** — configure a client-specific domain. You will be taken to the client's page to enter the domain and complete DNS verification. Verifying a client-specific domain follows the same flow as the app-level domain (see steps 2–3 above): set the domain, add the **CNAME** record shown, click **Refresh**, then add the **TXT** record Privy issues and **Refresh** again. > [!NOTE] > > A client-specific cookie domain is locked once verified: a verified domain cannot be edited in > place or switched back to inheriting. #### Using an app client in your app Pass the client's **client ID** to `PrivyProvider` alongside your shared app ID. `clientId` is an existing prop. No other code changes are needed. ```tsx theme={"system"} {children} ``` > [!WARNING] > > Allowed origins are configured per client and are **not** inherited from the app. When cookies are > enabled for a client, that client's domain must be listed in its allowed origins. Ensure that > client id is set in client setup so that the correct allowed origins are enforced for that client. ### 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(); } ``` > [!NOTE] > > 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). > [!WARNING] > > 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`. **Viem** ```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}`); ``` **Full helper function (robust & typed)** ```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}`; } ``` **Ethers** ```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}`); ``` **Full helper function (robust)** ```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}`; } ``` > [!NOTE] > > 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 **Alchemy** 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). **Biconomy** In this guide we'll see a quick example of using the [Biconomy's](https://biconomy.io/) Modular Execution Environment (MEE) stack with Privy! > [!NOTE] > > 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. **Pimlico** 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. > [!NOTE] > > 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. > [!NOTE] > > Want to see a full end to end example? Check out our starter repo > [here](https://github.com/pimlicolabs/permissionless-privy-7702)! **Porto** 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. **ZeroDev** 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. > [!NOTE] > > 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 for steps 3 & 4 together to create or get a Sub Account.** 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. **Sign a message** ```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 }); ``` **Send a transaction** ```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' }] }); ``` **Send a batch of transactions** ```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' }] }] }); ``` > [!TIP] > > 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 > [!NOTE] > > 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 > [!WARNING] > > 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 > [!TIP] > > 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. > [!NOTE] > > 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 > [!TIP] > > 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 > [!TIP] > > 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 > [!NOTE] > > 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. **React** **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. > [!TIP] > > 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. **React Native** **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 ); } }); ``` > [!TIP] > > 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. > [!NOTE] > > 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. > [!NOTE] > > 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. > [!NOTE] > > 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} ; ``` > [!WARNING] > > **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 > [!NOTE] > > 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} ``` > [!WARNING] > > **`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 > [!TIP] > > 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. > [!TIP] > > 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 > **[Authentication flows](/recipes/react/whitelabel#authentication)** > > Whitelabel login and MFA with your own UI and branding. > **[Embedded wallets](/recipes/react/whitelabel#wallets)** > > Create seamless wallet interactions with your own UI components and styling. > **[User management](/recipes/react/whitelabel#user-management)** > > 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. > **Email** > > 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). > **SMS** > > 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). > **Social logins** > > 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). > **Passkeys** > > 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). > **Telegram** > > 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). > **MFA** > > 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. > **Provider config (globally)** > > In your `PrivyProvider` config you can control the default wallet UI for all flows in your app. > > ```tsx {5} theme={"system"} > appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID || ''} > config={{ > embeddedWallets: { > showWalletUIs: false, > priceDisplay: {primary: 'native-token', secondary: null} > } > }} > > > > > ``` > > For more granular control, you can also control wallet UIs for specific flows in the sections below. > **Create a wallet** > > Privy supports whitelabeling wallet creation for Ethereum, Solana, and other chains. > > **Ethereum** > > ```tsx theme={"system"} > import {useCreateWallet} from '@privy-io/react-auth'; > ``` > > ```tsx theme={"system"} > const {createWallet} = useCreateWallet(); > createWallet(); > ``` > > **Solana** > > ```tsx theme={"system"} > import {useWallets} from '@privy-io/react-auth/solana'; > ``` > > ```tsx theme={"system"} > const {createWallet} = useWallets(); > createWallet(); > ``` > > **Other chains** > > ```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. > ``` > **Signing a message** > > To whitelabel Privy's message signing functionality, use the `useSignMessage` hook and call `signMessage` with your desired message. > > **Ethereum** > > ```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}} > ); > ``` > > **Solana** > > ```tsx theme={"system"} > import {useSignMessage} from '@privy-io/react-auth/solana'; > ``` > > ```tsx theme={"system"} > const {signMessage} = useSignMessage(); > signMessage({ > message: 'messageinUint8Array', > options: {uiOptions: {showWalletUIs: false}} > }); > ``` > > **Other chains** > > ```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' > }); > ``` > **Sending a transaction** > > To whitelabel Privy's transaction sending functionality, use the `useSendTransaction` hook and call `sendTransaction` with your desired transaction. > > **Ethereum** > > ```tsx theme={"system"} > import {useSendTransaction} from '@privy-io/react-auth'; > ``` > > ```tsx theme={"system"} > const {sendTransaction} = useSendTransaction(); > sendTransaction( > { > to: '0xE3070d3e4309afA3bC9a6b057685743CF42da77C', > value: 100000 > }, > { > uiOptions: {showWalletUIs: false} > } > ); > ``` > > **Solana** > > ```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. > **Linking a social account** > > 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(); > ``` > **Linking an additional OAuth account** > > 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: > > **provider** `string` *required* > > 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 ( > > ); > } > ``` > **Linking a wallet** > > 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. > > **Ethereum (SIWE)** > > 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 > ``` > > **Parameters** > > **address** `string` *required* > > EIP-55 checksum-encoded wallet address performing the signing. > > **chainId** `string` *required* > > 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 > ``` > > **Parameters** > > **signature** `string` *required* > > The EIP-191 signature corresponding to the message. > > **message** `string` *required* > > The EIP-4361 message returned by `generateSiweMessage`. > > **chainId** `string` *required* > > 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'`. > > **walletClientType** `string` > > Optional. The wallet client of the external wallet (e.g., `'metamask'`, `'coinbase_wallet'`). > Defaults to `null` if not specified. > > **connectorType** `string` > > 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); > } > }); > ``` > > **Solana (SIWS)** > > 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 > ``` > > **Parameters** > > **address** `string` *required* > > 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 }> > ``` > > **Parameters** > > **message** `string` *required* > > The SIWS message returned from `generateSiwsMessage`. > > **signature** `string` *required* > > 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')`. > > **walletClientType** `string` > > Optional. A string indicating the wallet client you'd like to associate with the wallet. Defaults > to `'privy'`. > > **connectorType** `string` > > 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); > } > }); > ``` > **Updating an email** > > 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; > ``` > > **Parameters** > > **newEmailAddress** `string` *required* > > 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>; > ``` > > **Parameters** > > **code** `string` *required* > > The one-time code received on the new email address. > > **Returns** > > **user** `User` > > 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 ( >
> type="email" > value={newEmailAddress} > onChange={(e) => setNewEmailAddress(e.target.value)} > placeholder="New email address" > /> > >
> ); > } > > return ( >
> value={code} > onChange={(e) => 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); > } > }); > ``` > > **Callback parameters** > > **onSuccess** `(\{user: User, updateMethod: string, updatedAccount: LinkedAccountType\}) =\> void` > > Optional callback that executes after a successful email update. Receives the updated user object, the update method (`'email'`), and the updated account. > > **onError** `(error: PrivyErrorCode, details: \{linkMethod: string\}) =\> void` > > Optional callback that executes if there is an error during the email update flow. > **Linking a custom JWT account** > > 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}> > ``` > > **jwt** `string` *required* > > 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 ( > > ); > } > ``` > > > [!NOTE] > > > > 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. > **Unlinking an account** > > 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. > **Unlinking an OAuth account** > > 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: > > **provider** `string` *required* > > 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'`). > > **subject** `string` *required* > > 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 > **[Whitelabel starter](https://github.com/privy-io/examples/tree/main/privy-react-whitelabel-starter)** > > Fork our starter repository to begin building your custom Privy integration. > **[Whitelabel demo](https://whitelabel.privy.io)** > > 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 > **[Worldcoin Mini Apps Docs](https://docs.world.org/mini-apps)** > > Official documentation for building Worldcoin Mini Apps. > **[Worldcoin Developer Portal](https://developer.worldcoin.org/)** > > Register your mini app and manage API keys. > **[Privy React Setup](/basics/react/setup)** > > 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). **Create a Worldcoin developer account and mini app** **1. Create a Worldcoin developer account and mini app** 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. **2. Get your appId and API key** 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 > [!NOTE] > > 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 **1. Create your mini app using the quickstart** 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. **2. Explore the starter repo structure** 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: **1. Get a nonce from Privy** Generate a unique nonce using `generateSiweNonce()` to ensure the SIWE message is secure. **2. Request wallet signature** Pass the nonce to World MiniKit's `walletAuth()` command, which prompts the user to sign a SIWE message in their World App wallet. **3. Complete authentication** 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); ``` > [!TIP] > > Your Worldcoin Mini App now supports SIWE authentication with Privy and World App! Enjoy seamless, > secure onboarding for your users 🚀 # Sending OUSD (or other ERC-20s) Source: https://docs.privy.io/recipes/send-ousd Sending OUSD, 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 OUSD as an example. ## 1. Get the OUSD contract address OUSD is an ERC-20-compatible token on Tempo. Its contract address on Tempo mainnet is `0x20c0000000000000000000006a37da5c996874be`. OUSD uses the same contract address on Tempo Moderato (testnet, chain ID `42431`). The examples below use Tempo mainnet (chain ID `4217`). For network setup, refer to [Using Tempo with Privy](/recipes/tempo/send-transactions). ## 2. Format the transaction send input data OUSD, 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 OUSD, the `decimals` value is 6, but for most other ERC-20 tokens, it's 18. **Typescript** 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 OUSD const decimals = 6; // OUSD 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. **React** ```typescript theme={"system"} import {useSendTransaction} from '@privy-io/react-auth'; const {sendTransaction} = useSendTransaction(); const {hash} = await sendTransaction({ to: '$OUSD_CONTRACT_ADDRESS', data: '0x', // from the previous step chainId: 4217 // Tempo's chainId }); ``` **React Native** ```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: '$OUSD_CONTRACT_ADDRESS', chainId: '0x1079', // Tempo's chainId (4217) in hex data: '0x' // from the previous step } ] }); ``` **NodeJS** ```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 ousdContractAddress = '0x20c0000000000000000000006a37da5c996874be'; // on Tempo const {hash} = await privy .wallets() .ethereum() .sendTransaction('insert-wallet-id', { caip2: 'eip155:4217', // Tempo's caip2 params: { transaction: { to: ousdContractAddress, data: encodedData, // from the previous step chain_id: 4217 // Tempo's chainId } } }); ``` **Python** ```python theme={"system"} ousd_contract_address = "insert-ousd-contract-address" response = client.wallets.ethereum.send_transaction( "insert-wallet-id", caip2="eip155:4217", params={ "transaction": { "to": ousd_contract_address, "data": encoded_data, # From the previous step "chain_id": 4217, } }, ) transaction_hash = response.hash ``` You've successfully sent OUSD! # 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. **Typescript** 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. **React** ```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 }); ``` **React Native** ```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 } ] }); ``` **NodeJS** ```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 } } }); ``` **Python** ```python theme={"system"} usdc_contract_address = "insert-usdc-contract-address" response = client.wallets.ethereum.send_transaction( "insert-wallet-id", caip2="eip155:8453", params={ "transaction": { "to": usdc_contract_address, "data": encoded_data, # From the previous step "chain_id": 8453, } }, ) transaction_hash = response.hash ``` 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. **React** ## Resources > **[Privy Solana setup](/basics/react/setup#solana)** > > 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() }); ``` **React Native** > [!NOTE] > > Solana Mobile Wallet Adapter is only supported on Android devices. ## Resources > **[Mobile Wallet Adapter documentation](https://docs.solanamobile.com/react-native/using_mobile_wallet_adapter#add-dependencies)** > > 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. > **[Get started with Privy and Solana](/recipes/solana/getting-started-with-privy-and-solana)** > > Set up core Solana wallet and transaction primitives. > **[Send SOL](/recipes/solana/send-sol)** > > Transfer native SOL with managed wallet infrastructure. > **[Send SPL tokens](/recipes/solana/send-spl-tokens)** > > 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. > [!NOTE] > > 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** ```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: **React** ```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] }); ``` **React Native** ```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 } }); ``` **NodeJS** ```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') }); ``` **Python** ```python theme={"system"} # `serialized_transaction` is the serialized transaction bytes from the previous step. response = client.wallets.solana.sign_and_send_transaction( "insert-wallet-id", serialized_transaction, caip2="solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", # Devnet ) signature = response.hash ``` 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. > [!NOTE] > > 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** ```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: **React** ```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], }); ``` **React Native** ```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 } }); ``` **NodeJS** ```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') }); ``` **Python** ```python theme={"system"} # `serialized_transaction` is the serialized transaction bytes from the previous step. response = client.wallets.solana.sign_and_send_transaction( "insert-wallet-id", serialized_transaction, caip2="solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", # Mainnet ) signature = response.hash ``` You've successfully sent SPL tokens! ## Token account considerations > [!WARNING] > > 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) **React** ## 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). **React** ```tsx theme={"system"} import {useCreateWallet} from '@privy-io/react-auth/extended-chains'; const {createWallet} = useCreateWallet(); const {user, wallet} = await createWallet({chainType: 'spark'}); ``` **React Native** ```tsx theme={"system"} import {useCreateWallet} from '@privy-io/expo/extended-chains'; const {createWallet} = useCreateWallet(); const {user, wallet} = await createWallet({chainType: 'spark'}); ``` **REST API** ```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...`). > [!NOTE] > > 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. > [!NOTE] > > 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). **React** ```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(); ``` **React Native** ```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. **React** ```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`); ``` **React Native** ```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). > [!NOTE] > > 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" } ``` > [!TIP] > > 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 > **[Transfer API](/wallets/actions/transfer/usage)** > > Review transfer parameters, supported assets, and error handling. > **[Wallet action status](/wallets/actions/status)** > > Track a transfer from pending to a terminal state. # Stripe embedded onramp Source: https://docs.privy.io/recipes/stripe-embedded-onramp Build a fully embedded, one-tap Apple Pay and card crypto onramp with Privy embedded wallets and Stripe Embedded Components Privy's embedded wallets integrate with Stripe's Embedded Components for the Crypto Onramp SDK, so your app can offer a fully embedded, one-tap checkout with no redirect and no hosted Stripe page. Users buy crypto with Apple Pay, Google Pay, or card in a single tap, and it lands directly in their Privy embedded wallet. Stripe handles payment processing, KYC, and crypto delivery while your app owns the entire UI. > [!TIP] > > This integration is only available in the React Native SDK. For React support, use the [deposit > modal](/wallets/funding/use-deposit-funds). ## Prerequisites Before integrating, ensure your app meets the following requirements: 1. **Stripe account with fiat-to-crypto onramp**: Create your Stripe account and submit your [onramp application](https://docs.stripe.com/crypto/onramp#submit-your-application) * Add your mobile app details in your Stripe onramp application * After Stripe approves your application, choose the Embedded Components integration and start development using a sandbox 2. **Privy App**: A configured Privy app with embedded wallets enabled ## Architecture overview The Embedded Components onramp flow involves three components: 1. **Your Frontend (Expo/React Native)**: Collects user input, displays UI, and calls Stripe Embedded Components SDK methods 2. **Your Backend**: Calls Stripe APIs to manage crypto customers, sessions, and transactions 3. **Stripe Embedded Components SDK**: Handles Link authentication, KYC collection, payment method selection, and identity verification ## Setup ### 1. Install the Stripe React Native SDK with Embedded Components Add the Stripe React Native SDK with Embedded Components onramp support to your Expo project: ```bash theme={"system"} npm install @stripe/stripe-react-native@0.52.0-crypto-onramp-2-private-beta ``` ### 2. Configure Expo Update your `app.json` to include the onramp plugin: ```json theme={"system"} { "expo": { "plugins": [ [ "@stripe/stripe-react-native", { "merchantIdentifier": "merchant.com.your-app", "enableGooglePay": false, "includeOnramp": true } ] ] } } ``` ### 3. Configure StripeProvider Wrap your app with both `PrivyProvider` and `StripeProvider`: ```tsx theme={"system"} import {PrivyProvider} from '@privy-io/expo'; import {StripeProvider} from '@stripe/stripe-react-native'; export default function App() { return ( ); } ``` ## Integration flow The Embedded Components onramp follows this sequence: 1. User signs up or logs in to Link (returns a `CryptoCustomer` ID) 2. Your backend fetches the customer's status (KYC, wallets, payment methods) 3. If setup is needed, the Embedded Components SDK helps the user complete it 4. Your backend creates and completes an onramp session ### Step 1: Initialize the Onramp Coordinator Configure the Stripe Embedded Components SDK when your component mounts: ```tsx theme={"system"} import {useOnramp} from '@stripe/stripe-react-native'; import {usePrivy} from '@privy-io/expo'; function OnrampScreen() { const {configure, hasLinkAccount, registerLinkUser, authenticateUser} = useOnramp(); const {user} = usePrivy(); const [cryptoCustomerId, setCryptoCustomerId] = useState(null); useEffect(() => { // Initialize the onramp coordinator configure({ appearance: { // Customize the appearance of Stripe UI components colors: { primary: '#0066FF' } } }); }, []); } ``` ### Step 2: Handle Link authentication Check if the user has a Link account and authenticate or register them: ```tsx theme={"system"} async function handleLinkAuth(email: string) { // Check if user already has a Link account const result = await hasLinkAccount(email); if (result.hasLinkAccount) { // Authenticate existing user - presents Stripe UI for OTP verification const authResult = await authenticateUser(); if (authResult.customerId) { setCryptoCustomerId(authResult.customerId); return authResult.customerId; } } else { // Register new Link user const regResult = await registerLinkUser({ email, phone: '+1234567890', // Collect from user in E.164 format country: 'US', // Two-letter country code fullName: 'John Doe' // Recommended for non-US users }); if (regResult.customerId) { setCryptoCustomerId(regResult.customerId); return regResult.customerId; } } return null; } ``` ### Step 3: Check customer status (Backend) After authentication, fetch the customer's status from your backend: ```typescript Backend (Node.js) theme={"system"} import Stripe from 'stripe'; const stripe = new Stripe(process.env.STRIPE_SECRET_KEY); // GET /api/crypto/customer/:id async function getCryptoCustomer(req, res) { const {id} = req.params; const oauthToken = req.headers['stripe-oauth-token']; const response = await fetch(`https://api.stripe.com/v1/crypto/customers/${id}`, { headers: { Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`, 'Stripe-OAuth-Token': oauthToken } }); const customer = await response.json(); // Check verification status const kycVerified = customer.verifications.find((v) => v.name === 'kyc_verified'); const idDocVerified = customer.verifications.find((v) => v.name === 'id_document_verified'); res.json({ customerId: customer.id, providedFields: customer.provided_fields, kycStatus: kycVerified?.status || 'not_started', idDocStatus: idDocVerified?.status || 'not_started' }); } ``` ```tsx Frontend theme={"system"} async function checkCustomerStatus(customerId: string) { const response = await fetch(`/api/crypto/customer/${customerId}`, { headers: { Authorization: `Bearer ${accessToken}` } }); return response.json(); } ``` ### Step 4: Collect KYC data if needed If the customer needs KYC verification, collect and submit the data: ```tsx theme={"system"} import {useOnramp} from '@stripe/stripe-react-native'; function KYCForm({customerId}: {customerId: string}) { const {attachKycInfo} = useOnramp(); async function submitKYC(formData: KYCFormData) { const result = await attachKycInfo({ firstName: formData.firstName, lastName: formData.lastName, dateOfBirth: { day: formData.dobDay, month: formData.dobMonth, year: formData.dobYear }, address: { line1: formData.addressLine1, line2: formData.addressLine2, city: formData.city, state: formData.state, postalCode: formData.postalCode, country: formData.country }, // Optional fields depending on jurisdiction idType: 'social_security_number', idNumber: formData.ssn }); if (!result.error) { // KYC submitted successfully return true; } return false; } } ``` ### Step 5: Handle identity verification if required For transactions requiring document verification, use the identity verification API: ```tsx theme={"system"} async function handleIdentityVerification() { const {verifyIdentity} = useOnramp(); // This launches Stripe's Identity SDK for document upload const result = await verifyIdentity(); if (!result.error) { // User completed document upload return true; } // User canceled or error occurred - allow retry return false; } ``` ### Step 6: Register wallet address Register the user's Privy wallet as their crypto destination: ```tsx theme={"system"} import {useEmbeddedEthereumWallet} from '@privy-io/expo'; import {useOnramp} from '@stripe/stripe-react-native'; function WalletRegistration() { const {wallets} = useEmbeddedEthereumWallet(); const {registerWalletAddress} = useOnramp(); async function registerWallet() { const wallet = wallets[0]; if (!wallet) return; // Register wallet with network type const result = await registerWalletAddress(wallet.address, 'base'); return !result.error; } } ``` ### Step 7: Collect payment method Allow users to select or add a payment method: ```tsx theme={"system"} async function handlePaymentMethodCollection(paymentType: 'Card' | 'BankAccount' | 'PlatformPay') { const {collectPaymentMethod} = useOnramp(); // Collect a specific payment method type // For PlatformPay (Apple Pay/Google Pay), pass additional params const result = await collectPaymentMethod( paymentType, paymentType === 'PlatformPay' ? { // Optional PlatformPay params for Apple Pay / Google Pay } : undefined ); if (result.paymentDisplayData) { // Returns PaymentDisplayData with icon, label, sublabel return result.paymentDisplayData; } return null; } ``` ### Step 8: Create and complete the onramp session (Backend) Create an onramp session and execute the transaction: ```typescript Backend (Node.js) theme={"system"} // POST /api/crypto/onramp async function createOnrampSession(req, res) { const { cryptoCustomerId, paymentToken, walletId, sourceCurrency, destinationCurrency, sourceAmount, customerIpAddress } = req.body; // Create the onramp session const session = await fetch('https://api.stripe.com/v1/crypto/onramp_sessions', { method: 'POST', headers: { Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}`, 'Content-Type': 'application/x-www-form-urlencoded' }, body: new URLSearchParams({ ui_mode: 'headless', crypto_customer_id: cryptoCustomerId, payment_token: paymentToken, wallet_id: walletId, source_currency: sourceCurrency, // e.g., 'usd.fiat' destination_currency: destinationCurrency, // e.g., 'eth.ethereum' source_amount: sourceAmount.toString(), customer_ip_address: customerIpAddress, off_session: 'false' }) }); const sessionData = await session.json(); res.json(sessionData); } // POST /api/crypto/onramp/:sessionId/checkout async function completeCheckout(req, res) { const {sessionId} = req.params; const result = await fetch( `https://api.stripe.com/v1/crypto/onramp_sessions/${sessionId}/checkout`, { method: 'POST', headers: { Authorization: `Bearer ${process.env.STRIPE_SECRET_KEY}` } } ); const data = await result.json(); // Check for errors if (data.error_reason) { return res.status(400).json({ error: data.error_reason // Possible values: location_not_supported, transaction_limit_reached, // charged_with_expired_quote, action_required, transaction_failed, // missing_kyc, missing_document_verification, missing_consumer_wallet }); } res.json({ status: data.status, transactionDetails: data.transaction_details }); } ``` ```tsx Frontend theme={"system"} async function executeOnramp(params: {amount: string; destinationCurrency: string}) { // Create session const session = await fetch('/api/crypto/onramp', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ cryptoCustomerId, paymentToken: selectedPaymentMethod.token, walletId: registeredWalletId, sourceCurrency: 'usd.fiat', destinationCurrency: params.destinationCurrency, sourceAmount: params.amount, customerIpAddress: await getClientIp() }) }); const sessionData = await session.json(); // Complete checkout const result = await fetch(`/api/crypto/onramp/${sessionData.id}/checkout`, { method: 'POST' }); return result.json(); } ``` ### Step 9: Handle 3DS and additional authentication Some transactions require additional authentication (like 3DS for cards). The `performCheckout` SDK method handles this automatically: ```tsx theme={"system"} import {useOnramp} from '@stripe/stripe-react-native'; function CheckoutButton({sessionId}: {sessionId: string}) { const {performCheckout} = useOnramp(); async function handleCheckout() { // performCheckout handles 3DS and other auth flows automatically // The callback may be invoked twice: initially and after any required auth const result = await performCheckout(sessionId, async () => { const response = await fetch(`/api/crypto/onramp/${sessionId}/checkout`, { method: 'POST' }); const data = await response.json(); return data.client_secret; }); if (!result.error) { // Transaction successful // Poll for blockchain confirmation using transaction_details.blockchain_tx_id } } } ``` ## Complete example Here's a complete component that ties together the entire flow (you can find the repo example [here](https://github.com/privy-io/expo-starter/tree/feature/stripe-headless-onramp)): ```tsx theme={"system"} import {useState, useEffect} from 'react'; import {View, Text, Button, TextInput} from 'react-native'; import {usePrivy, useEmbeddedEthereumWallet} from '@privy-io/expo'; import {useOnramp} from '@stripe/stripe-react-native'; type OnrampStep = 'auth' | 'kyc' | 'wallet' | 'payment' | 'checkout' | 'complete'; export function HeadlessOnramp() { const {user} = usePrivy(); const {wallets} = useEmbeddedEthereumWallet(); const { configure, hasLinkAccount, registerLinkUser, authenticateUser, attachKycInfo, registerWalletAddress, collectPaymentMethod, createCryptoPaymentToken, performCheckout } = useOnramp(); const [step, setStep] = useState('auth'); const [cryptoCustomerId, setCryptoCustomerId] = useState(null); const [amount, setAmount] = useState('100'); useEffect(() => { configure({ appearance: {colors: {primary: '#6366F1'}} }); }, []); // Step 1: Link Authentication async function handleAuth() { const email = user?.email?.address; if (!email) return; const lookupResult = await hasLinkAccount(email); if (lookupResult.hasLinkAccount) { const authResult = await authenticateUser(); if (authResult.customerId) { setCryptoCustomerId(authResult.customerId); await checkAndAdvance(authResult.customerId); } } else { // Show registration form... } } // Check customer status and advance to appropriate step async function checkAndAdvance(customerId: string) { const status = await fetch(`/api/crypto/customer/${customerId}`).then((r) => r.json()); if (status.kycStatus !== 'verified') { setStep('kyc'); } else if (!status.hasWallet) { setStep('wallet'); } else if (!status.hasPaymentMethod) { setStep('payment'); } else { setStep('checkout'); } } // Step 2: Register Privy wallet async function handleWalletRegistration() { const wallet = wallets[0]; if (!wallet) return; const result = await registerWalletAddress(wallet.address, 'base'); if (!result.error) { setStep('payment'); } } // Step 3: Collect payment method async function handlePaymentMethod() { const result = await collectPaymentMethod('Card'); if (result.paymentDisplayData) { setStep('checkout'); } } // Step 4: Execute checkout async function handleCheckout() { // Create a payment token for the selected method const tokenResult = await createCryptoPaymentToken(); if (!tokenResult.cryptoPaymentToken) return; const session = await fetch('/api/crypto/onramp', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({ cryptoCustomerId, paymentToken: tokenResult.cryptoPaymentToken, sourceAmount: amount, destinationCurrency: 'usdc.base' }) }).then((r) => r.json()); const result = await performCheckout(session.id, async () => { const checkout = await fetch(`/api/crypto/onramp/${session.id}/checkout`, { method: 'POST' }).then((r) => r.json()); return checkout.client_secret; }); if (!result.error) { setStep('complete'); } } return ( {step === 'auth' && ); } ``` 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) **create\_ethereum\_wallet** `boolean` (Optional) Whether to create an Ethereum wallet for the user. **create\_solana\_wallet** `boolean` (Optional) Whether to create a Solana wallet for the user. **create\_ethereum\_smart\_wallet** `boolean` (Optional) Whether to create an Ethereum smart wallet for the user. **number\_of\_ethereum\_wallets\_to\_create** `number` (Optional) The number of Ethereum wallets to pregenerate for the user. Defaults to `1`. #### Wallet creation (updated) **wallets** `object[]` *required* The wallets to create for the user. **chain\_type** `ethereum | solana | cosmos | stellar | sui | bitcoin-segwit | near | ton | starknet | aptos` *required* Chain type of the wallet. "ethereum" supports any EVM-compatible network. **policy\_ids** `string[]` List of policy IDs for policies that should be enforced on the wallet. Currently, only one policy is supported per wallet. **additional\_signers** `object[]` **signer\_id** `string` The key ID of the signer. **override\_policy\_ids** `string[]` List of policy IDs for policies that should be enforced on the wallet. Currently, only one policy is supported per wallet. **create\_smart\_wallet** `boolean` *required* 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 > **[Telegram trading bot starter](https://github.com/privy-io/examples/tree/main/examples/privy-node-telegram-trading-bot)** > > 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. **how to create a new bot** **1. Create a new bot** 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` **2. Set up Node.js Telegram bot server** 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. **how to set up a command** 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. **how to get the wallet ID for the Telegram user** 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. **1. Create a wallet associated with your user** 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: [] }], }); }); ``` **2. Send transactions with the wallet** 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. > [!TIP] > > 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} }); }); ``` **3. Allow the user to claim their wallet and use it from your app** 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. **1. Instrument your app with Privy** 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). **2. Create wallets for your users** 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. **3. Add a signer to the user's wallet** 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. > **[Signer quickstart](/wallets/using-wallets/signers/quickstart)** > > Request access to user wallets with signers. **4. Execute actions with your signer** 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. > [!TIP] > > 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} }); }); ``` ```python Python theme={"system"} user = client.users.get_by_telegram_user_id( telegram_user_id=str(message.from_user.id) ) wallet = next( account for account in user.linked_accounts if account.type == "wallet" and account.wallet_client_type == "privy" ) client.wallets.ethereum.send_transaction( wallet.id, caip2="eip155:1", params={"transaction": transaction}, ) ``` > **[Sign requests](/controls/authorization-keys/using-owners/sign)** > > Authorize requests to the Privy API with your signer. > **[EVM](/wallets/using-wallets/ethereum/send-a-transaction)** > > Take actions on EVM chains with Privy's NodeJS SDK or REST API. > **[Solana](/wallets/using-wallets/solana/send-a-transaction)** > > 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. > **[Using Tempo with Privy](/recipes/tempo/send-transactions)** > > Create embedded wallets and send transactions on Tempo. > **[Policy examples](/controls/policies/example-policies/tempo)** > > Configure policies to control fee tokens, sponsorship, and time windows on Tempo. > **[Private transactions](/financial-flows/private-transactions)** > > Keep balances, payments, and yield private with Tempo Zones. # 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. **NodeJS** ```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; } ``` **Python** ```python theme={"system"} wallet = client.wallets.create(chain_type="ethereum") print("Created wallet:", wallet.address) ``` **React** ```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 such as OUSD on Tempo. Privy handles Tempo transaction construction, token decimal precision, and gas sponsorship automatically. **NodeJS** ```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: 'ousd', chain: 'tempo' }, destination: { address: '0xRecipientAddress' } }); console.log('Transfer action:', response.id, response.status); ``` **REST API** ```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": "ousd", "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": "ousd", "source_amount": "10.0", "source_chain": "tempo", "destination_address": "0xRecipientAddress" } ``` > [!TIP] > > 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. > [!NOTE] > > To send transactions on Tempo Moderato (testnet), specify the CAIP-2 identifier `eip155:42431`. > [!NOTE] > > 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. > [!NOTE] > > 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. > **Using Privy SDK or REST API** > > 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. > > **NodeJS** > > ```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); > ``` > > **Python** > > ```python theme={"system"} > # `data` is the hex-encoded ERC-20 transfer calldata. > response = client.wallets.ethereum.send_transaction( > "insert-wallet-id", > caip2="eip155:4217", # Use eip155:42431 for Tempo Moderato (testnet) > params={ > "transaction": { > "type": 118, > "fee_token": "insert-fee-token-address", > "calls": [ > { > "to": "insert-usdc-e-contract-address", > "data": data, > } > ], > } > }, > ) > > print("Transaction hash:", response.hash) > ``` > > **React** > > ```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 ; > } > ``` > > **REST API** > > ```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": "" > } > ] > } > } > }' > ``` > **Using viem tempoActions** > > 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. > > **NodeJS** > > ```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); > ``` > > **React** > > ```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 ; > } > ``` > > **REST API** > > ```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": "" > } > ] > } > } > }' > ``` > **Using gas sponsorship** > > 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. > > **NodeJS** > > ```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 OUSD_ADDRESS = '0x20c0000000000000000000006a37da5c996874be'; > > const data = encodeFunctionData({ > abi: parseAbi(['function transfer(address to, uint256 amount) public returns (bool)']), > functionName: 'transfer', > args: ['0xRecipientAddress', 1_000_000n] // 1 OUSD (6 decimals) > }); > > const {hash} = await privy > .wallets() > .ethereum() > .sendTransaction('insert-wallet-id', { > caip2: 'eip155:4217', // Tempo mainnet > params: { > transaction: { > type: 118, > calls: [ > { > to: OUSD_ADDRESS, > data > } > ] > } > }, > sponsor: true > }); > > console.log('Transaction hash:', hash); > ``` > > **Python** > > ```python theme={"system"} > # `data` is the hex-encoded ERC-20 transfer calldata. > response = client.wallets.ethereum.send_transaction( > "insert-wallet-id", > caip2="eip155:4217", # Tempo mainnet > params={ > "transaction": { > "type": 118, > "calls": [ > { > "to": "0x20c0000000000000000000006a37da5c996874be", > "data": data, > } > ], > } > }, > sponsor=True, > ) > > print("Transaction hash:", response.hash) > ``` > > **React** > > ```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 ousd = '0x20c0000000000000000000006a37da5c996874be'; > > function SendSponsoredTempoTransaction() { > const {wallets} = useWallets(); > const {sendTransaction} = useSendTransaction(); > > const handleSend = async () => { > const {hash} = await sendTransaction( > { > transaction: { > type: 118, > chainId: 4217, // Tempo mainnet > calls: [ > { > to: ousd, > data: encodeFunctionData({ > abi: parseAbi([ > 'function transfer(address to, uint256 amount) public returns (bool)' > ]), > functionName: 'transfer', > args: ['0xRecipientAddress', 1_000_000n] // 1 OUSD (6 decimals) > }) > } > ] > }, > wallet: wallets[0] > }, > {sponsor: true} > ); > console.log('Transaction hash:', hash); > }; > > return ; > } > ``` > > **REST API** > > ```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": "0x20c0000000000000000000006a37da5c996874be", > "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 > **[Send a transaction](/wallets/using-wallets/ethereum/send-a-transaction)** > > Complete guide to sending transactions with Privy > **[Gas sponsorship overview](/wallets/gas-and-asset-management/gas/overview)** > > Learn about Privy's gas sponsorship engine > **[Balance webhooks](/wallets/gas-and-asset-management/assets/balance-event-webhooks)** > > > > Subscribe to deposit and withdrawal events. > **[Create a wallet](/wallets/wallets/create/create-a-wallet)** > > Learn more about creating embedded wallets > **[Tempo policy examples](/controls/policies/example-policies/tempo)** > > 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. > [!NOTE] > > 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). **Code example** ```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). > [!NOTE] > > 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. **Code example** ```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: **Code example** ```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. **code example of creating and signing a transfer** ```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. **code example of deploying your Starknet account and sending a transfer** ```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. > [!NOTE] > > 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. **1. Create a wallet** ```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); ``` **2. Build the transaction** ```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); ``` **3. Sign with Privy** ```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); ``` **4. Broadcast the transaction** ```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): **Code example** ```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). > [!NOTE] > > 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. ### Stellar Stellar implements the EdDSA signing algorithm using the Ed25519 curve. The following example demonstrates hash signing for Stellar transactions: **Code example** ```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). **code example of signing and sending a transaction** ```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.) **code example of creating, signing, and sending a transaction** ```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). **code example of signing and sending a transaction** ```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. > **[Telegram trading bot guide](/recipes/telegram-bot)** > > Guide to building a Solana trading bot for Telegram. > **[Flashbots integration](/recipes/flashbots-protect)** > > Learn how to integrate with Flashbots to avoid bot attacks. > **[Hyperliquid Guide](/recipes/hyperliquid-guide)** > > Integrate with the Hyperliquid SDKs to make trades on Hyperliquid. > **[Morpho Guide](/recipes/yield/morpho-guide)** > > Learn how to integrate with Morpho lending protocol using Privy wallets. > **[Request server-side access to user wallets](/recipes/wallets/session-signer-use-cases/server-side-access)** > > Configure custom control models where both users and your server can execute transactions with > specific permissions. > **[Execute limit orders](/recipes/wallets/session-signer-use-cases/limit-orders)** > > Request server-side access to user wallets to execute limit orders when users are offline. > **[Onramp with Stripe](/recipes/stripe-embedded-onramp)** > > Enable users to fund wallets with Stripe's crypto onramp. > **[Configure confirmation modals](/recipes/react/manage-wallet-UIs)** > > Learn how to disable transaction confirmations for faster trading. > **[Implement robust policies](/controls/overview)** > > 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. > **[Trading apps](/recipes/trading-apps-homepage)** > > Explore architecture and implementation patterns for trading apps. > **[Polymarket guide](/recipes/polymarket-guide)** > > Build prediction market experiences with wallet-backed users. > **[Telegram trading bot](/recipes/telegram-bot)** > > Launch automated trading interactions through Telegram. > **[Server-side wallet access](/recipes/wallets/session-signer-use-cases/server-side-access)** > > Execute trading actions with controlled server-side signers. > **[Limit orders with session signers](/recipes/wallets/session-signer-use-cases/limit-orders)** > > Implement delegated limit-order execution workflows. > **[Hyperliquid quickstart](/recipes/hyperliquid-guide)** > > 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](/wallets/using-wallets/tron/sign-a-transaction) with Transatron's gas sponsorship to broadcast TRX-less transfers from a user's embedded wallet. ## Resources > **[Transatron docs](https://docs.transatron.io)** > > Official Transatron integration documentation. > **[Tron transaction signing](/wallets/using-wallets/tron/sign-a-transaction)** > > How Privy signs Tron transactions. *** ## 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. > [!WARNING] > > 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 3 level. Create a wallet with `chain_type: 'tron'`. The wallet address can receive TRX, USDT, and other TRC-20 assets. **React** 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 ; } ``` **NodeJS** ```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); ``` **Python** ```python theme={"system"} wallet = client.wallets.create(chain_type="tron") print("Wallet ID:", wallet.id) print("Tron address:", wallet.address) ``` **REST API** ```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" }' ``` > [!NOTE] > > 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, utils} 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 For server-side signing, use Privy's structured `tron_signTransaction` method. It returns a complete signed transaction with the required 65-byte signature that you can submit directly to Transatron. For client-side signing, use `useSignRawHash`; the server must attach the recovery byte before broadcasting. Building and broadcasting stay on the server because both require the Transatron API key. **React** 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; } ``` **NodeJS** Use the typed `privy.wallets().tron().signTransaction()` method for backend bots, schedulers, and other server-side flows. It handles the recovery byte and returns the full signed transaction hex. See the [Tron signing guide](/wallets/using-wallets/tron/sign-a-transaction#nodejs) for the method signature and a complete example. **Python** When signing happens on the server, use `raw_sign` to sign the transaction's `txID`. Tron requires a recovery byte after Privy's 64-byte compact signature, so try `1b` and then `1c` if address recovery does not match the wallet. ```python theme={"system"} tx_id = transaction["txID"] tx_id_hex = tx_id if tx_id.startswith("0x") else f"0x{tx_id}" response = client.wallets.raw_sign( wallet_id, wallet_raw_sign_params={"params": {"hash": tx_id_hex}}, ) base_signature = response.data.signature.removeprefix("0x") transaction["signature"] = [f"{base_signature}1b"] # If your Tron library recovers an address other than `wallet_address`, use 0x1c. if recover_tron_address(transaction) != wallet_address: transaction["signature"] = [f"{base_signature}1c"] ``` ## 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 using `tron_signTransaction`. If signing happens in a React client (per step 3), build the transaction with TronWeb, return its `txID` to the client, receive the 64-byte signature, finalize it with `attachTronSignature(...)`, and broadcast via `/wallet/broadcasttransaction` instead. **1. Compute the Tron fee limit** 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 } ``` **2. Build, sign, and broadcast the transfer** Encode the TRC-20 transfer, sign it with Privy's structured Tron method, and broadcast the signed transaction 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 recipientHex = tronWeb.address.toHex(recipientAddress).replace(/^41/, '0x'); const calldata = utils.abi.encodeParamsV2ByABI( { name: 'transfer', type: 'function', inputs: [ {name: 'to', type: 'address'}, {name: 'value', type: 'uint256'} ], outputs: [{name: '', type: 'bool'}], stateMutability: 'nonpayable' }, [recipientHex, BigInt(amountBaseUnits)] ); const data = 'a9059cbb' + calldata.slice(2); const refBlock = await tronWeb.trx.getCurrentRefBlockParams(); const {signed_transaction: signedTransaction} = await privy .wallets() .tron() .signTransaction(walletId, { params: { raw_data: { contract: [ { type: 'TriggerSmartContract' as const, owner_address: ownerHex, contract_address: contractHex, data, call_value: 0 } ], ref_block_bytes: refBlock.ref_block_bytes, ref_block_hash: refBlock.ref_block_hash, expiration: refBlock.expiration, timestamp: Date.now(), fee_limit: feeLimit } } }); return tronWeb.fullNode.request( 'wallet/broadcasthex', {transaction: signedTransaction}, '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. **3. Orchestrate the full flow** 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. > **Check account balance** > > 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); > ``` > **View spending history** > > 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}` > ); > } > ``` > **Check current prices** > > 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'); > ``` > [!NOTE] > > 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 > [!TIP] > > 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.** > [!NOTE] > > 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 OUSD 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 OUSD `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 > [!WARNING] > > 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. **1. Create a spend-tracking aggregation** Create an aggregation that sums the `amount` field from ERC-20 `transfer` calldata over a rolling 24-hour window. Scope it to the OUSD contract so only OUSD 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!; // OUSD on Tempo mainnet (chain ID 4217). const OUSD_ADDRESS = '0x20c0000000000000000000006a37da5c996874be'; 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: 'OUSD 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: OUSD_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. > [!NOTE] > > 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. **2. Create a policy with an aggregation cap** Create a policy with an `eth_signTransaction` rule that allows OUSD 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 OUSD cap: 100 × 10^6 base units = 100,000,000 = 0x5F5E100 const SPENDING_CAP_HEX = '0x5F5E100'; const policy = await privy.policies.create({ name: 'OUSD 100/24h transfer cap', version: '1.0', chain_type: 'ethereum', rules: [ { name: 'Allow OUSD transfers within 24h rolling cap', method: 'eth_signTransaction', action: 'ALLOW', conditions: [ { field_source: 'ethereum_transaction', field: 'to', operator: 'eq', value: OUSD_ADDRESS }, { field_source: 'reference', field: `aggregation.${aggregationId}`, operator: 'lte', value: SPENDING_CAP_HEX } ] } ] }); ``` > [!TIP] > > OUSD uses 6 decimal places. Convert a human-readable amount to base units before setting it as a > hex cap value: `100 OUSD = 100 × 10^6 = 100,000,000 = 0x5F5E100`. > [!WARNING] > > 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. **3. Create a wallet with the policy** 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. **4. Sign and broadcast an OUSD transfer** 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 {tempo} from 'viem/chains'; const recipientAddress = '0x...'; const amount = '25'; // OUSD 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: OUSD_ADDRESS, data, chain_id: tempo.id // 4217 } } }); // 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: tempo, 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 OUSD is blocked from a 15 OUSD transfer, even though the cap is 100 OUSD. **5. Handle policy violations** 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 sendOusdWithBudgetGuard( walletId: string, walletAddress: string, recipient: string, amountOusd: string ) { const data = encodeFunctionData({ abi: erc20Abi, functionName: 'transfer', args: [recipient as `0x${string}`, parseUnits(amountOusd, 6)] }); try { const signResponse = await privy.wallets._rpc(walletId, { method: 'eth_signTransaction', chain_type: 'ethereum', params: { transaction: { from: walletAddress, to: OUSD_ADDRESS, data, chain_id: tempo.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 OUSD transfers to one contract but the policy rule covers all OUSD 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 OUSD 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. > [!WARNING] > > 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. > [!NOTE] > > 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. > [!NOTE] > > 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. > [!NOTE] > > 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. > [!TIP] > > 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. ## Getting test credentials programmatically The Python SDK can retrieve the test accounts and credentials enabled for an app. ```python theme={"system"} test_accounts = client.apps.get_test_credentials("insert-app-id") for account in test_accounts.data: print(account.email, account.phone_number, account.otp_code) ``` ## 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. > [!WARNING] > > `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 **NodeJS** 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. > **[Pregenerating wallets](/recipes/pregenerate-wallets)** > > Create wallets in advance for low-latency onboarding. > **[Server-side user wallets](/recipes/wallets/server-side-user-wallets)** > > Manage user wallets with server-side authorization. > **[HD wallets](/recipes/hd-wallets)** > > Derive scalable wallet trees from managed roots. > **[Stateful policies](/recipes/using-stateful-policies)** > > Apply dynamic policy checks before transaction execution. > **[Agentic wallets](/recipes/agent-integrations/agentic-wallets)** > > Provision programmable wallets for autonomous agents. > **[Custom account abstraction](/recipes/account-abstraction/custom-implementation)** > > 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 > [!NOTE] > > 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'; ; ``` > [!TIP] > > 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`) > [!WARNING] > > 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}`; } ``` > [!NOTE] > > 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}`); } }) ); ``` > [!NOTE] > > 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 } ``` > **TypeScript types reference** > > 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` | > [!TIP] > > WalletConnect Pay matches available payment options to the chains your app lists. Add more chains > to `SUPPORTED_CHAINS` to offer users additional payment methods. > [!NOTE] > > For testnet development, use the [WalletConnect Dashboard POS > tool](https://dashboard.walletconnect.com) to create test payments on supported testnets. *** # Testing **1. Create a test payment** Go to the [WalletConnect Dashboard](https://dashboard.walletconnect.com) and use the POS tool to create a test payment. Copy the payment link. **2. Log in with Privy** Log in to your app with Privy so an embedded wallet is provisioned. **3. Complete the payment flow** Paste the link into your app and complete the payment flow. *** ## Learn more > **[WalletConnect Pay documentation](https://docs.walletconnect.com/payments/wallets/overview)** > > Official WalletConnect Pay integration guide for wallets > **[WalletConnect Dashboard](https://dashboard.walletconnect.com)** > > 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: **1. Create policies** Define a policy for each access level (e.g., a restrictive policy and a permissive policy). **2. Create authorization keys** Create an authorization key (or key quorum) for each signer that needs access to the wallet. **3. Add signers with override policies** Add each signer to the wallet with its corresponding policy. **4. Route transactions through the appropriate signer** 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 OUSD transfers, while the full-access signer can send transactions to any address. **NodeJS** ```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 OUSD_ADDRESS = '0x20c0000000000000000000006a37da5c996874be'; // OUSD on Tempo // Restrictive policy: only OUSD transfers under 1,000 const restrictivePolicy = await privy.policies().create({ name: 'Small OUSD transfers only', version: '1.0', chain_type: 'ethereum', rules: [ { name: 'Allow small OUSD transfers', method: 'eth_sendTransaction', action: 'ALLOW', conditions: [ { field_source: 'ethereum_transaction', field: 'to', operator: 'eq', value: OUSD_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: [] } ] }); ``` **Python** ```python theme={"system"} erc20_transfer_abi = [ { "type": "function", "name": "transfer", "state_mutability": "nonpayable", "inputs": [ {"name": "to", "type": "address"}, {"name": "amount", "type": "uint256"}, ], "outputs": [{"name": "", "type": "bool"}], } ] ousd_address = "0x20c0000000000000000000006a37da5c996874be" # OUSD on Tempo restrictive_policy = client.policies.create( name="Small OUSD transfers only", version="1.0", chain_type="ethereum", rules=[ { "name": "Allow small OUSD transfers", "method": "eth_sendTransaction", "action": "ALLOW", "conditions": [ { "field_source": "ethereum_transaction", "field": "to", "operator": "eq", "value": ousd_address, }, { "field_source": "ethereum_calldata", "field": "transfer.amount", "abi": erc20_transfer_abi, "operator": "lte", "value": "1000000000", }, ], } ], ) permissive_policy = client.policies.create( name="Allow all transactions", version="1.0", chain_type="ethereum", rules=[ { "name": "Allow all sends", "method": "eth_sendTransaction", "action": "ALLOW", "conditions": [], } ], ) ``` **REST API** Make `POST` requests to: ```sh theme={"system"} https://api.privy.io/v1/policies ``` **Restrictive policy** (small OUSD transfers only): ```json theme={"system"} { "name": "Small OUSD transfers only", "version": "1.0", "chain_type": "ethereum", "rules": [ { "name": "Allow small OUSD transfers", "method": "eth_sendTransaction", "action": "ALLOW", "conditions": [ { "field_source": "ethereum_transaction", "field": "to", "operator": "eq", "value": "0x20c0000000000000000000006a37da5c996874be" }, { "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. > **[Create a policy](/controls/policies/create-a-policy)** > > 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. > **[Create authorization keys](/controls/authorization-keys/keys/create/key)** > > 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. **React** ```tsx theme={"system"} import {useSigners} from '@privy-io/react-auth'; const {addSigners} = useSigners(); await addSigners({ address: walletAddress, signers: [ { signerId: '', policyIds: [''] }, { signerId: '', policyIds: [''] } ] }); ``` **NodeJS** 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: [''] } ] }); ``` **Python** Update the wallet with the additional signers and their override policies. The wallet owner must authorize this request. ```python theme={"system"} wallet = client.wallets.update( wallet_id, wallet_update_params={ "additional_signers": [ { "signer_id": "insert-restricted-signer-authorization-key-id", "override_policy_ids": [restrictive_policy.id], }, { "signer_id": "insert-full-access-signer-authorization-key-id", "override_policy_ids": [permissive_policy.id], }, ] }, request_options=owner_request_options, ) ``` **REST API** 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. **NodeJS** ```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:4217', 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); ``` **Python** ```python theme={"system"} from privy import AuthorizationContext, PrivyRequestOptions def send_transaction(wallet_id, transaction, signer_private_key): return client.wallets.ethereum.send_transaction( wallet_id, caip2="eip155:4217", params={"transaction": transaction}, request_options=PrivyRequestOptions( authorization_context=AuthorizationContext( authorization_private_keys=[signer_private_key] ) ), ) # The automated and admin paths select different signers and therefore different policies. send_transaction(wallet_id, bot_transaction, restricted_signer_private_key) send_transaction(wallet_id, admin_transaction, full_access_signer_private_key) ``` **REST API** 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:4217", "method": "eth_sendTransaction", "chain_type": "ethereum", "params": { "transaction": { "to": "0x20c0000000000000000000006a37da5c996874be", "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 > **[Additional signers](/wallets/using-wallets/signers/overview)** > > Add signers to wallets and manage their permissions. > **[Policies](/controls/policies/overview)** > > Define rules that constrain which transactions are allowed. > **[Authorization keys](/controls/authorization-keys/keys/create/key)** > > 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. > [!NOTE] > > Execution wallets work alongside existing external wallet setups. ## Architecture ![Execution wallets architecture diagram](https://mintcdn.com/privy-c2af3412/JqVAwdW8cFxEN_JW/images/execution-wallets-diagram.png?fit=max\&auto=format\&n=JqVAwdW8cFxEN_JW\&q=85\&s=39bfb11b4f7c11f292331bc31d6a16fe) 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 **1. Create authorization keys** 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](/controls/authorization-keys/keys/create/key)** > > Create authorization keys in your Privy Dashboard. > **[Key quorums quickstart](/controls/key-quorum/create)** > > Set up a key quorum for multi-party governance. **2. Define execution policies** 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. > **[Policies overview](/controls/policies/overview)** > > Learn how to construct policies with Privy's policy language. > **[Create a policy](/controls/policies/create-a-policy)** > > Create a policy for your execution wallets. **3. Provision an execution wallet fleet** 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 > **[Create a wallet](/wallets/wallets/create/create-a-wallet)** > > Provision wallets for your execution fleet. > [!TIP] > > 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. **4. Upgrade execution wallets with EIP-7702** 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. > **[EIP-7702 overview](/recipes/react/eip-7702)** > > Learn how to upgrade EOAs with EIP-7702. > **[Sign EIP-7702 authorization](/wallets/using-wallets/ethereum/sign-7702-authorization)** > > Sign an EIP-7702 authorization with a Privy wallet. **5. Execute batched transactions** 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. > **[Send a Solana transaction](/wallets/using-wallets/solana/send-a-transaction)** > > Execute transactions on Solana. > **[Send batch transactions](https://docs.privy.io/recipes/batch-transactions#sending-batch-transactions)** > > Follow the batch transactions guide for the transaction submission pattern. **6. Monitor execution activity** 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. > **[Transaction webhooks](/wallets/gas-and-asset-management/assets/transaction-event-webhooks)** > > Monitor transaction status and completion events. > **[Balance webhooks](/wallets/gas-and-asset-management/assets/balance-event-webhooks)** > > Track deposits and withdrawals across the fleet. ## Learn more > **[Organization wallets](/recipes/wallets/organization-wallets)** > > Set up an organization wallet with key quorums and policies. > **[EIP-7702](/recipes/react/eip-7702)** > > Learn how EIP-7702 enables batch transactions and gas sponsorship. > **[Policies overview](/controls/policies/overview)** > > Configure fine-grained execution controls. > **[Manual approvals](/controls/dashboard/overview)** > > 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. > [!WARNING] > > 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: **1. Configure authentication settings** 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. **2. Create your user** [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. **3. Create a wallet owned by your user** 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. **4. Request a user key with a user's access token** 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. **5. Execute transactions from your server** 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. **NodeJS** 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. **Python** Use the `create` method on the users service. ```python theme={"system"} user = client.users.create( linked_accounts=[ { "type": "custom_auth", "custom_user_id": "insert-user-id-from-authentication-provider", } ] ) user_id = user.id ``` Save `user_id` for the next step. **REST API** 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. > [!TIP] > > 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. **NodeJS** 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. **Python** ```python theme={"system"} wallet = client.wallets.create( chain_type="ethereum", owner={"user_id": user_id}, policy_ids=["insert-any-policy-id-to-associate-with-wallet"], ) wallet_id = wallet.id wallet_address = wallet.address ``` Save `wallet_id` to transact with the wallet. **REST API** 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. **NodeJS** 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'] }; ``` **Python** The Python SDK requests and caches the ephemeral user key automatically when a user JWT is present in the authorization context. ```python theme={"system"} from privy import AuthorizationContext, PrivyRequestOptions request_options = PrivyRequestOptions( authorization_context=AuthorizationContext( user_jwts=["insert-user-jwt"], ) ) ``` **REST API** 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. > [!TIP] > > 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. > [!TIP] > > You can also enable [key export of a user's wallet](/wallets/wallets/export) with a valid access > token from the user. **NodeJS** 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. **Python** Pass the request options from step 4 to a chain-specific wallet method. The SDK automatically adds the user's authorization signature. ```python theme={"system"} response = client.wallets.ethereum.send_transaction( wallet_id, caip2="eip155:8453", params={ "transaction": { "to": "0xRecipientAddress", "value": "0x1", } }, request_options=request_options, ) ``` **REST API** 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. **1. Add a signer to the user's wallet** 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. > **[Adding signers quickstart](/wallets/using-wallets/signers/quickstart)** > > Request access to user wallets with signers. **2. Execute the limit order from your server** 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](/controls/authorization-keys/using-owners/sign)** > > Sign requests to the Privy API with your signer. > **[EVM](/wallets/using-wallets/ethereum/send-a-transaction)** > > Take actions on EVM chains with Privy's NodeJS SDK or REST API. > **[Solana](/wallets/using-wallets/solana/send-a-transaction)** > > 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. **1. Add a signer to the user's wallet** 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. > **[Adding a signer quickstart](/wallets/using-wallets/signers/quickstart)** > > Request access to user wallets with signers. **2. Execute actions with your signer** 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](/controls/authorization-keys/using-owners/sign)** > > Sign requests to the Privy API with your signer. > **[EVM](/wallets/using-wallets/ethereum/send-a-transaction)** > > Take actions on EVM chains with Privy's NodeJS SDK or REST API. > **[Solana](/wallets/using-wallets/solana/send-a-transaction)** > > 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 > **[Treasury recipe](/recipes/wallets/wallet-infrastructure)** > > 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. > **[Organization wallets](/recipes/wallets/organization-wallets)** > > 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. > **[Execution wallets recipe](/recipes/wallets/execution-wallets)** > > Provision a wallet fleet. ## Features > **Multi-party authorization** > > Configurable key quorums. > **Declarative policies** > > Enforce transfer limits, contract allowlists, and more. > **Dual approval paths** > > Routine operations run automatically while sensitive actions require human sign-off, all on a > single wallet. > **Parallel execution** > > Wallet fleets with independent nonces eliminate sequential bottlenecks. > **Batched transactions** > > EIP-7702 smart account upgrades bundle multiple contract calls into a single atomic operation. > **Gas sponsorship** > > Execution wallets operate without maintaining working balances. > **Programmatic on/off-ramping** > > 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: **1. Create a server authorization key** 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. **2. Create a 2-of-2 key quorum** Register a key quorum that contains the user ID and the server authorization key, with an `authorization_threshold` of 2. **3. Create a wallet owned by the quorum** Create a wallet whose owner is the key quorum. All subsequent actions on this wallet require both signatures. **4. Execute transactions with 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. > **[Create authorization keys](/controls/authorization-keys/keys/create/key)** > > 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. > [!TIP] > > 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. **NodeJS** ```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. **Python** ```python theme={"system"} user_id = "insert-privy-user-id" key_quorum = client.key_quorums.create( display_name=f"2-of-2 quorum for user {user_id}", public_keys=["insert-server-authorization-public-key"], user_ids=[user_id], authorization_threshold=2, ) key_quorum_id = key_quorum.id ``` Save `key_quorum_id`; it becomes the wallet's `owner_id` in the next step. **REST API** 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. **NodeJS** ```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 }); ``` **Python** ```python theme={"system"} wallet = client.wallets.create( chain_type="ethereum", owner_id=key_quorum_id, ) wallet_id = wallet.id wallet_address = wallet.address ``` **REST API** 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. > [!TIP] > > 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 **1. Build the request payload on the client** 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. **React** ```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; ``` **React Native** ```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; ``` **2. Sign the payload with the user's key on the client** 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. **React** ```tsx theme={"system"} import {useAuthorizationSignature} from '@privy-io/react-auth'; const {generateAuthorizationSignature} = useAuthorizationSignature(); const {signature: userSignature} = await generateAuthorizationSignature(requestPayload); ``` **React Native** ```tsx theme={"system"} import {useAuthorizationSignature} from '@privy-io/expo'; const {generateAuthorizationSignature} = useAuthorizationSignature(); const {signature: userSignature} = await generateAuthorizationSignature(requestPayload); ``` **3. Send the payload and user signature to your server** 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 }) }); ``` **4. Sign with the server key and send to Privy** 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. **NodeJS** ```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) }); ``` **Python** ```python theme={"system"} import httpx from privy import ( WalletAPIRequestSignatureInput, format_request_for_authorization_signature, generate_authorization_signature, ) # `request_payload` and `user_signature` are received from the client. signature_input = WalletAPIRequestSignatureInput( version=1, method=request_payload["method"], url=request_payload["url"], headers=request_payload["headers"], body=request_payload["body"], ) payload = format_request_for_authorization_signature(signature_input) server_signature = generate_authorization_signature( "insert-server-authorization-private-key", payload, ) response = httpx.request( request_payload["method"], request_payload["url"], auth=("insert-app-id", "insert-app-secret"), headers={ **request_payload["headers"], "Content-Type": "application/json", "privy-authorization-signature": f"{user_signature},{server_signature}", }, json=request_payload["body"], ) response.raise_for_status() ``` **REST API** 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 > **[Key quorums](/controls/key-quorum/overview)** > > Learn how key quorums define ownership and authorization thresholds. > **[Authorization keys](/controls/authorization-keys/keys/create/key)** > > Create and manage server-controlled authorization keys. > **[Policies](/controls/policies/overview)** > > 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. > [!TIP] > > 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. > [!TIP] > > 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. > [!TIP] > > 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. **React (web apps and Telegram mini apps)** 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} ``` **React Native (mobile apps)** 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. > [!TIP] > > 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. **React (web apps and Telegram mini apps)** 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'] }] }) ``` > **Add a signer to a user's wallet immediately after they login.** > > 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 > ``` > **Add a signer for limit orders.** > > If your app offers limit orders to users, we recommend the following flow for using signers to execute limit orders. > > **1. Create your limit order policy** > > Create a policy that allows your signer to execute the limit order from the user's wallet. > > **2. Add your signer** > > Add your signer to the user's wallet via the `addSessionSigner` method, using the policy from step 1. > > **3. Execute the limit order** > > 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. **React Native (mobile apps)** 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: [] }] }) ``` > **Add a signer for limit orders.** > > 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 > **[React](/wallets/using-wallets/ethereum/send-a-transaction)** > > Send Ethereum transactions from a web app or a Telegram mini-app using Privy's React SDK. > **[React Native](/wallets/using-wallets/ethereum/send-a-transaction#react-native)** > > Send Ethereum transactions from a mobile app or a Telegram mini-app using Privy's React Native > SDK. #### Solana > **[React](/wallets/using-wallets/solana/send-a-transaction)** > > Send Solana transactions from a web app or a Telegram mini-app using Privy's React SDK. > **[React Native](/wallets/using-wallets/solana/send-a-transaction#react-native)** > > 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. > [!NOTE] > > 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). **REST API** 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. > [!TIP] > > **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 > [!NOTE] > > 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). > [!NOTE] > > 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 **1. Create an admin key quorum** 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. > **[Key quorums quickstart](/controls/key-quorum/create)** > > Create a key quorum in the Privy Dashboard. > **[Manual approvals](/controls/dashboard/overview)** > > Learn how to manually approve transactions. **2. Create authorization keys** 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. > **[Create authorization keys](/controls/authorization-keys/keys/create/key)** > > Generate authorization keys in the Privy Dashboard. **3. Create policies** 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. > [!TIP] > > A typical policy configuration applies stricter limits to server-approved transactions (for example, capping transfers at 1,000 OUSD). Any transaction above that threshold instead requires human approval through the admin flow. > **[Policies overview](/controls/policies/overview)** > > Learn how to construct policies with Privy's policy language. > **[Policies quickstart](/controls/policies/create-a-policy)** > > Create a policy. **4. Create wallets** 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. > **[Wallets quickstart](/wallets/wallets/create/create-a-wallet)** > > Create a wallet. > [!NOTE] > > 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. **5. Execute transactions** 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. > [!TIP] > > 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. > [!NOTE] > > 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. > **[Send an EVM transaction](/wallets/using-wallets/ethereum/send-a-transaction)** > > Learn how to send an EVM transaction. > **[Send a Solana transaction](/wallets/using-wallets/solana/send-a-transaction)** > > Learn how to send a Solana transaction. > **[Authorization signatures](/controls/authorization-keys/using-owners/sign/overview)** > > 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. > **[World Mini Apps Docs](https://docs.world.org/mini-apps)** > > Official documentation for building World Mini Apps. > **[World Developer Portal](https://developer.worldcoin.org/)** > > Register your mini app and manage API keys. > **[XMTP Documentation](https://docs.xmtp.org/)** > > Learn about XMTP, the protocol powering World Chat. > [!TIP] > > **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. **1. Configure your World developer account** 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. **2. Create your World 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. **3. Set up Privy** 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. > **Link World wallet to existing accounts** > > 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, > }); > }; > ``` > **Link other accounts** > > 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 }); ``` > [!TIP] > > 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 > **[Aave Docs](https://docs.aave.com/)** > > Official documentation for Aave protocol and smart contracts. > **[Privy Wallets](/wallets/overview)** > > 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 **React** ### 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. **1. 1. Execute the supply transaction** 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. **1. 1. Get reserve information for vault deployment** ```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 }); ``` **2. 2. Deploy a new vault** ```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}`); }; ``` **3. 3. Deposit into a vault** ```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. # Reading Earn vault totals onchain Source: https://docs.privy.io/recipes/yield/earn-onchain-totals Compute deposit and withdrawal totals for a Privy wallet directly from Morpho and Aave vault events. The [get vault position](/wallets/actions/earn/get-vault-position) endpoint returns `total_deposited` and `total_withdrawn` for a wallet. For all vault providers, Privy computes these totals from the deposits and withdrawals made through Privy Earn. Deposits and withdrawals made directly onchain are not counted. This recipe shows how your app can read the same values directly from the vault contract for Morpho and Aave vaults. The onchain totals include every deposit and withdrawal for the wallet, no matter how it was sent. > [!NOTE] > > Your app only needs this recipe if wallets interact with a vault outside the Privy Earn > [deposit](/wallets/actions/earn/deposit) and [withdraw](/wallets/actions/earn/withdraw) endpoints. > For example, your app sends a vault `deposit` call with `sendTransaction`, or users deposit from > another app. If all activity goes through Privy Earn, your app can use the endpoint totals. ## Resources > **[Get vault position](/wallets/actions/earn/get-vault-position)** > > The Privy endpoint that returns a wallet's vault position. > **[ERC-4626 standard](https://eips.ethereum.org/EIPS/eip-4626)** > > The tokenized vault standard that Morpho and Aave Earn vaults implement. *** ## Field sources The table maps each field of the get vault position response to its source in the endpoint and onchain. | Response field | Endpoint source | Onchain source | | - | - | - | | `shares_in_vault` | Live read from the vault contract | `balanceOf(wallet)` on the vault contract | | `assets_in_vault` | Live read from the vault contract | `convertToAssets(shares_in_vault)` on the vault contract | | `total_deposited` | Privy Earn deposits | Sum of `assets` in the vault's `Deposit` events where `receiver` is the wallet | | `total_withdrawn` | Privy Earn withdrawals | Sum of `assets` in the vault's `Withdraw` events where `owner` is the wallet | The endpoint values for `shares_in_vault` and `assets_in_vault` match the onchain reads. This recipe also shows how to read them onchain, so that your app reads all four values at the same block. > [!NOTE] > > This recipe covers Morpho and Aave vaults. ## Prerequisites * A Privy app with an Earn vault. See [Earn setup](/wallets/actions/earn/setup). * Your app ID and app secret, for the Privy API. * An RPC URL for the vault's chain. The chain ID is the number after `eip155:` in the vault's `caip2`. * The block number at which the vault contract was deployed. Find the contract creation transaction for the vault address on a block explorer for the vault's chain. Install the Privy Node SDK and `viem`: ```bash theme={"system"} npm install @privy-io/node viem ``` ## 1. Get the vault and wallet addresses The onchain reads need two addresses: the vault contract address and the wallet address. Get the vault address and chain with the `vaultDetails` method, which calls the [get vault details](/wallets/actions/earn/get-vault-details) endpoint. Get the wallet address with the [get wallet](/wallets/wallets/get-a-wallet/get-wallet-by-id) method. ```typescript theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: 'insert-your-app-id', appSecret: 'insert-your-app-secret' }); export async function getVaultAndWallet({vaultId, walletId}: {vaultId: string; walletId: string}) { const [vault, wallet] = await Promise.all([ privy.wallets().earn().ethereum().vaultDetails(vaultId), privy.wallets().get(walletId) ]); return { vaultAddress: vault.vault_address as `0x${string}`, caip2: vault.caip2, assetDecimals: vault.asset.decimals, walletAddress: wallet.address as `0x${string}` }; } ``` Store the vault address, `caip2`, and asset decimals with the vault ID. These values do not change, so your app only needs to fetch them once per vault. ## 2. Read the position The wallet's position is its vault share balance. `convertToAssets` returns the value of these shares in the underlying asset, including accrued yield. These two reads return the same values as `shares_in_vault` and `assets_in_vault`. ```typescript {skip-check} theme={"system"} import {type Address, createPublicClient, erc4626Abi, http} from 'viem'; // Use an RPC URL for the vault's chain. const publicClient = createPublicClient({transport: http('insert-your-rpc-url')}); export async function getPosition({ vaultAddress, walletAddress, blockNumber }: { vaultAddress: Address; walletAddress: Address; blockNumber?: bigint; }) { const sharesInVault = await publicClient.readContract({ address: vaultAddress, abi: erc4626Abi, functionName: 'balanceOf', args: [walletAddress], blockNumber }); const assetsInVault = sharesInVault > 0n ? await publicClient.readContract({ address: vaultAddress, abi: erc4626Abi, functionName: 'convertToAssets', args: [sharesInVault], blockNumber }) : 0n; return {sharesInVault, assetsInVault}; } ``` The optional `blockNumber` reads the position at a past block. Step 4 uses it to keep the position and the totals at the same block. ## 3. Compute the totals from vault events Every ERC-4626 vault emits a `Deposit` event for each deposit and a `Withdraw` event for each withdrawal. Both events include `assets`, the amount of the underlying asset in the token's smallest unit. * `Deposit` indexes `receiver`, the address that receives the vault shares. * `Withdraw` indexes `owner`, the address whose vault shares are burned. Filter `Deposit` events by `receiver` and `Withdraw` events by `owner`, then sum `assets`. Most RPC providers limit the block range of one `eth_getLogs` request, so the function below scans the range in chunks. ```typescript theme={"system"} import {type Address, createPublicClient, erc4626Abi, http} from 'viem'; // Use an RPC URL for the vault's chain. const publicClient = createPublicClient({transport: http('insert-your-rpc-url')}); // Set this to the `eth_getLogs` block range limit of your RPC provider. const MAX_BLOCK_RANGE = 10_000n; export async function getEarnTotals({ vaultAddress, walletAddress, fromBlock, toBlock }: { vaultAddress: Address; walletAddress: Address; fromBlock: bigint; toBlock: bigint; }) { let totalDeposited = 0n; let totalWithdrawn = 0n; for (let start = fromBlock; start <= toBlock; start += MAX_BLOCK_RANGE) { const end = start + MAX_BLOCK_RANGE - 1n < toBlock ? start + MAX_BLOCK_RANGE - 1n : toBlock; const [deposits, withdrawals] = await Promise.all([ publicClient.getContractEvents({ address: vaultAddress, abi: erc4626Abi, eventName: 'Deposit', args: {receiver: walletAddress}, fromBlock: start, toBlock: end }), publicClient.getContractEvents({ address: vaultAddress, abi: erc4626Abi, eventName: 'Withdraw', args: {owner: walletAddress}, fromBlock: start, toBlock: end }) ]); for (const log of deposits) { totalDeposited += log.args.assets ?? 0n; } for (const log of withdrawals) { totalWithdrawn += log.args.assets ?? 0n; } } return {totalDeposited, totalWithdrawn}; } ``` To get the lifetime totals, scan from the vault's deployment block to the latest block. A full scan is the simplest option, but its cost grows with the age of the vault. For each new read, step 4 scans only the blocks since the last read. ## 4. Store a checkpoint and scan new blocks Store the totals and the last scanned block for each wallet and vault. On each read, scan only the blocks after the checkpoint, and add the new amounts to the stored totals. ```typescript {skip-check} theme={"system"} type EarnTotalsCheckpoint = { totalDeposited: bigint; totalWithdrawn: bigint; lastScannedBlock: bigint; }; // Number of block confirmations to wait before counting events. The most recent blocks stay // unscanned, so that a chain reorganization is unlikely to change counted events. // This value is an approximation. Some chains, such as Base, have no fixed number of blocks // after which a block is final. Increase this value for a stronger guarantee. const CONFIRMATIONS = 12n; export async function syncEarnTotals({ vaultAddress, walletAddress, checkpoint }: { vaultAddress: Address; walletAddress: Address; checkpoint: EarnTotalsCheckpoint; }): Promise { const toBlock = (await publicClient.getBlockNumber()) - CONFIRMATIONS; if (toBlock <= checkpoint.lastScannedBlock) { return checkpoint; } const newTotals = await getEarnTotals({ vaultAddress, walletAddress, fromBlock: checkpoint.lastScannedBlock + 1n, toBlock }); return { totalDeposited: checkpoint.totalDeposited + newTotals.totalDeposited, totalWithdrawn: checkpoint.totalWithdrawn + newTotals.totalWithdrawn, lastScannedBlock: toBlock }; } ``` For the first read of a wallet, start from an empty checkpoint at the block before the vault's deployment: ```typescript {skip-check} theme={"system"} const initialCheckpoint: EarnTotalsCheckpoint = { totalDeposited: 0n, totalWithdrawn: 0n, lastScannedBlock: vaultDeploymentBlock - 1n }; ``` Save the returned checkpoint to your database after each sync. Store the amounts as strings or as a numeric type with enough precision, because they can exceed the safe range of a JavaScript `number`. ## 5. Calculate earned yield Read the position at the checkpoint's `lastScannedBlock`. This keeps the position and the totals at the same block. If the position is read at the latest block, a deposit in the unscanned blocks increases `assetsInVault` before it increases `totalDeposited`, and the earned yield is too high. ```typescript {skip-check} theme={"system"} const checkpoint = await syncEarnTotals({ vaultAddress, walletAddress, checkpoint: storedCheckpoint }); const {assetsInVault} = await getPosition({ vaultAddress, walletAddress, blockNumber: checkpoint.lastScannedBlock }); const earnedYield = assetsInVault - (checkpoint.totalDeposited - checkpoint.totalWithdrawn); ``` The formula is the same one that the [get vault position](/wallets/actions/earn/get-vault-position#calculate-earned-yield) guide uses with the endpoint fields. All amounts are in the asset's smallest unit. Divide by `10^decimals` before your app shows them to users. ## Key integration tips 1. **Read events from the vault address.** Use the `vault_address` from get vault details. For Morpho, this is the Privy fee wrapper vault, not the underlying Morpho vault. 2. **Share transfers are not deposits.** If a wallet receives vault shares through an ERC-20 `Transfer`, no `Deposit` event names the wallet. The shares count in `assets_in_vault`, but not in `total_deposited`. The same applies to shares that the wallet sends away. 3. **Use archive access for past blocks.** Reads at a past `blockNumber` need an RPC node that keeps historical state. Most hosted RPC providers support this. 4. **Check the block range limit.** If `eth_getLogs` fails with a range error, lower `MAX_BLOCK_RANGE` to your provider's limit. ## Next steps > **[Earn webhooks](/wallets/actions/earn/webhooks)** > > Track Privy Earn deposits and withdrawals in real time. > **[Get vault details](/wallets/actions/earn/get-vault-details)** > > Retrieve vault-level information like APY, TVL, and available liquidity. > [!WARNING] > > 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. # 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 > **[Ethena Staking Docs](https://docs.ethena.fi/solution-design/staking-usde)** > > Official documentation for staking USDe and the sUSDe contract. > **[Get started with Privy](/basics/react/quickstart)** > > 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 **1. 1. Configure addresses** 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 ``` **2. 2. Approve the sUSDe vault to spend USDe** 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 }); ``` **3. 3. Deposit USDe into the vault** 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. > [!WARNING] > > Unlike instant withdrawal vaults, Ethena requires a 7-day cooldown period after requesting an > unstake before USDe can be withdrawn. **Request unstake (start cooldown)** 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 }); ``` **Withdraw after cooldown** 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). > [!TIP] > > Your app is now ready to interact with Ethena using Privy embedded wallets! # Integrating Ground gtokens with Privy Source: https://docs.privy.io/recipes/yield/ground-guide Ground is the cash management layer for stablecoins and lets your app offer RWA yield through gtokens held in user-owned Privy wallets. Users authorize your developer signer once; your Node.js backend then discovers sources, allowlists their wallets, and executes deposits and withdrawals on their behalf. ## Resources > **[Ground gtoken docs](https://docs.groundtech.co/docs/gtokens/deposit-and-redeem)** > > Official documentation for gtokens, deposits, and redemptions. > **[Privy developer signer quickstart](/wallets/using-wallets/signers/quickstart)** > > Generate an authorization key and let users add it to their wallet. The example below uses **gUSTB** and **USDC** on **Ethereum Sepolia**, Ground's sandbox network for this walkthrough. gUSTB is the receipt token of a Ground vault that holds Superstate's USTB, a tokenized fund providing exposure to short-duration U.S. Treasury bills. ## Before you start **1. Create a Ground account and API key** Create an account in the [Ground Developer Portal](https://docs.groundtech.co). Once your account is set up, select the sandbox environment and open **API Keys** to generate a key. Copy it when shown and store it securely — it is displayed only once. **2. Install dependencies and configure environment variables** Install the server-side dependencies: ```bash theme={"system"} npm install @privy-io/node viem npm install -D typescript tsx @types/node ``` Configure these environment variables. Keep all secrets on your backend: ```bash theme={"system"} export GROUND_BASE_URL="https://sandbox.groundtech.co" export GROUND_API_KEY="" export PRIVY_APP_ID="" export PRIVY_APP_SECRET="" export PRIVY_AUTHORIZATION_PRIVATE_KEY="" export SEPOLIA_RPC_URL="" ``` **3. Set up a Privy app with a user-owned wallet and your developer signer** Create a Privy app and enable user-owned embedded Ethereum wallets. Following the [signer quickstart](/wallets/using-wallets/signers/quickstart), generate a P-256 authorization key and register its public key in a 1-of-1 key quorum. Keep the authorization private key on your backend as base64-encoded PKCS8 without PEM headers, and attach policies permitting Ground's `Admission` signature, USDC approval, and vault calls on Sepolia. Have the logged-in user add the signer to their wallet: ```tsx theme={"system"} addSigners({ address: userWalletAddress, signers: [{signerId: quorumId, policyIds: [groundPolicyId]}] }); ``` **4. Fund the user's wallet** Fund the user's wallet with 10 sandbox USDC and some Sepolia ETH for gas. Get test ETH from a [Sepolia faucet](https://cloud.google.com/application/web3/faucet/ethereum/sepolia). ## Discover sources and allowlist the wallet **1. 1. Discover available yield sources** Fetch the sources available to your organization. Each catalogue row identifies a source, network, vault address, deposit asset, and contract interface. Fetch each environment's catalogue separately, and discover addresses from the catalogue instead of hardcoding the vault. ```ts theme={"system"} import {PrivyClient, isEmbeddedWalletLinkedAccount} from '@privy-io/node'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID!, appSecret: process.env.PRIVY_APP_SECRET! }); // Obtain these from the authenticated request, not a global wallet setting. const idToken = ''; const selectedWalletId = ''; // Verify the identity token before accepting a wallet ID from the request. const user = await privy.users().get({id_token: idToken}); // Reject wallets that are not linked to this authenticated user. const linkedWallet = user.linked_accounts .filter(isEmbeddedWalletLinkedAccount) .find((account) => account.id === selectedWalletId && account.chain_type === 'ethereum'); if (!linkedWallet) throw new Error('Selected wallet does not belong to the authenticated user'); const wallet = await privy.wallets().get(selectedWalletId); // Authorize Privy requests with the developer signer the user added to their wallet. const authorizationContext = { authorization_private_keys: [process.env.PRIVY_AUTHORIZATION_PRIVATE_KEY!] }; const baseUrl = process.env.GROUND_BASE_URL!; const headers = {Authorization: `Bearer ${process.env.GROUND_API_KEY!}`}; const response = await fetch(`${baseUrl}/v2/gvaults/yield-sources`, {headers}); if (!response.ok) throw new Error(`Discovery failed: ${response.status}`); const {data: sources} = await response.json(); // Discover addresses from the catalogue instead of hardcoding the vault. const source = sources.find( (row: {interface: string; chain: string}) => row.interface === 'ground_ustb_v1' && row.chain === 'ethereum_sepolia' ); if (!source) throw new Error('gUSTB is unavailable'); ``` > [!NOTE] > > Always verify the identity token and confirm the selected wallet belongs to the authenticated user > before taking any action on it. Fund the selected user-owned wallet before continuing to deposits. **2. 2. Allowlist the wallet** Fetch Ground's authorization challenge and use the developer signer to sign it for the user-owned Privy wallet that will hold gUSTB. Submit the signed proof to Ground: ```ts {skip-check} theme={"system"} const accessUrl = `${baseUrl}/v2/gvaults/${encodeURIComponent(source.id)}/allowlist/${encodeURIComponent(wallet.address)}`; // Ground supplies the exact typed data and expiry for this wallet and source. const challengeResponse = await fetch(`${accessUrl}/authorization`, {headers}); if (!challengeResponse.ok) throw new Error(`Challenge failed: ${challengeResponse.status}`); const challenge = await challengeResponse.json(); // Sign as the user wallet through its authorized developer signer; no onchain transaction yet. const {signature} = await privy .wallets() .ethereum() .signTypedData(wallet.id, { authorization_context: authorizationContext, params: { typed_data: { domain: challenge.typedData.domain, types: challenge.typedData.types, primary_type: 'Admission', message: challenge.typedData.value } } }); const ownershipProof = {issuedAt: challenge.issuedAt, expiresAt: challenge.expiresAt, signature}; const submitted = await fetch(accessUrl, { method: 'PUT', headers: {...headers, 'Content-Type': 'application/json'}, body: JSON.stringify({ownershipProof}) }); if (!submitted.ok) throw new Error(`Allowlisting failed: ${submitted.status}`); // Submission can be pending. Continue only once allowed is true; otherwise poll this GET. const checked = await fetch(accessUrl, {headers}); if (!checked.ok) throw new Error(`Access check failed: ${checked.status}`); const {allowed, status} = await checked.json(); ``` > [!WARNING] > > Enable deposits only when the `GET` returns `allowed: true`. HTTP 202 means the request is pending > — poll `GET` from your backend. Stop and surface `failed` or `reconciliation_required` rather than > polling forever. If the signature expires before submission, fetch and sign a new challenge. ## Transact with the developer signer Use Node.js 22 or later with TypeScript. The snippets share the user-owned wallet, authorization context, and catalogue source above; put imports at module scope and execute steps in order inside an async function. Download the selected vault's verified implementation ABI from the Ground contracts resource into `ground-ustb-abi.json`. **1. 3. Approve USDC** ```ts {skip-check} theme={"system"} import { createPublicClient, http, encodeFunctionData, erc20Abi, parseUnits, type Abi, type Address, type Hex } from 'viem'; import {sepolia} from 'viem/chains'; import deployedAbi from './ground-ustb-abi.json'; const publicClient = createPublicClient({ chain: sepolia, transport: http(process.env.SEPOLIA_RPC_URL!) }); const owner = wallet.address as Address; const vault = source.contractAddress as Address; const asset = source.asset.contractAddress as Address; const vaultAbi = deployedAbi as Abi; // Convert 10 USDC to integer token units using the catalogue decimals. const assets = parseUnits('10', source.asset.decimals); // Confirm the vault accepts the asset we are about to approve. const onchainAsset = (await publicClient.readContract({ address: vault, abi: vaultAbi, functionName: 'asset' })) as Address; if (onchainAsset.toLowerCase() !== asset.toLowerCase()) throw new Error('Deposit asset does not match catalogue'); // Reuse this helper for approval, deposit and redemption from the same user wallet. async function send(to: Address, data: Hex) { const {hash} = await privy .wallets() .ethereum() .sendTransaction(wallet.id, { authorization_context: authorizationContext, caip2: 'eip155:11155111', params: {transaction: {to, data, value: '0x0'}} }); // Persist hash before waiting; reconcile it before retrying. const receipt = await publicClient.waitForTransactionReceipt({hash: hash as Hex}); if (receipt.status !== 'success') throw new Error('Transaction reverted'); return receipt; } // Allow the vault to spend only the deposit amount, rather than an unlimited allowance. await send( asset, encodeFunctionData({abi: erc20Abi, functionName: 'approve', args: [vault, assets]}) ); ``` **2. 4. Deposit and receive gUSTB** After access and approval are confirmed, deposit USDC with the wallet as receiver. The gUSTB shares stay in the user's wallet: ```ts {skip-check} theme={"system"} // Simulate first to catch current contract reverts before submitting. await publicClient.simulateContract({ account: owner, address: vault, abi: vaultAbi, functionName: 'deposit', args: [assets, owner] }); // The second argument is the receiver: gUSTB shares stay in the user wallet. const depositReceipt = await send( vault, encodeFunctionData({ abi: vaultAbi, functionName: 'deposit', args: [assets, owner] }) ); ``` > [!NOTE] > > Use `convertToShares(assets)` for an estimate and confirmed deposit events for actual minted > shares. The `deposit` method does not enforce a minimum-share output. **3. 5. Check balances** Show wallet-held investments, unsettled withdrawals, and claimable USDC separately. Under the USTB escrow implementation, requested shares leave the wallet, so `balanceOf` alone excludes that withdrawal value. ```ts {skip-check} theme={"system"} // Read every balance at the same block for a consistent snapshot. const blockNumber = await publicClient.getBlockNumber(); const read = (functionName: string, args: readonly unknown[] = [owner]) => publicClient.readContract({address: vault, abi: vaultAbi, functionName, args, blockNumber}); const walletShares = (await read('balanceOf')) as bigint; const requestedShares = (await read('pendingRedeemShares')) as bigint; const settledShares = (await read('processedRedeemSharesByOwner')) as bigint; const claimableUsdc = (await read('processedRedeemAssetsByOwner')) as bigint; const awaitingSettlementShares = requestedShares - settledShares; // The underlying calculator rejects zero; zero shares have zero value. const investedUsdcEstimate = walletShares === 0n ? 0n : await read('convertToAssets', [walletShares]); const pendingUsdcEstimate = awaitingSettlementShares === 0n ? 0n : await read('convertToAssets', [awaitingSettlementShares]); ``` > [!NOTE] > > These reads require the deployed escrow-enabled USTB ABI. `pendingRedeemShares` includes settled, > unclaimed shares, so subtract `settledShares` before estimating unsettled value. Claimable USDC is > the actual settled amount; do not also count settled shares in the estimates. Format asset amounts > using the deposit token's decimals and share amounts using the vault's `decimals()`. **4. 6. Withdraw to the wallet** For an immediate exit, check current capacity and redeem. USDC returns to the same user wallet: ```ts {skip-check} theme={"system"} const shares = walletShares; // maxRedeem is immediate exit capacity; larger exits must use the queued flow. const max = (await publicClient.readContract({ address: vault, abi: vaultAbi, functionName: 'maxRedeem', args: [owner] })) as bigint; if (shares <= 0n || shares > max) throw new Error('Choose an amount within immediate capacity, or queue an exit'); await publicClient.simulateContract({ account: owner, address: vault, abi: vaultAbi, functionName: 'redeem', args: [shares, owner, owner] }); // redeem takes shares, receiver and owner; USDC returns to the same user wallet. await send( vault, encodeFunctionData({abi: vaultAbi, functionName: 'redeem', args: [shares, owner, owner]}) ); ``` For a queued exit, call `requestRedeem(owner, shares)` through the same server `send` helper and save the request ID decoded from the confirmed event. After processing, call `claimRedeem(owner, requestId)` using the verified ABI. Check that request's state or simulate its claim before submitting. Refresh balances after confirmed requests and claims; withdrawal completes when proceeds reach the wallet. See the [Ground deposit and redeem docs](https://docs.groundtech.co/docs/gtokens/deposit-and-redeem). ## Conclusion Privy makes it straightforward to offer Ground RWA yield from user-owned wallets your users control. For more detail, refer to the [Ground gtoken docs](https://docs.groundtech.co/docs/gtokens/deposit-and-redeem), or reach out in [Slack](https://privy.io/slack). > [!TIP] > > Your app is now ready to deposit into and redeem Ground gtokens with Privy user-owned 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 **1. 1. Import dependencies, configure Privy, and initialize clients** 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); ``` **2. 2. Create a Privy user and Solana wallet** 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 }); ``` **3. 3. Build Kamino Earn deposit instructions and transaction message** 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) ); ``` **4. 4. Sign the transaction with Privy** 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] } }); ``` **5. 5. Send the transaction** 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); ``` > [!TIP] > > Your deposit is complete! The user’s funds are now deposited in the Kamino Earn vault using their > Privy embedded wallet. ## Additional resources > **[Kamino Developer Docs](https://docs.kamino.finance/)** > > Official documentation for Kamino protocol. > **[Privy Solana wallets](/wallets/wallets/create/create-a-wallet#solana)** > > 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). > [!NOTE] > > 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); ``` > [!NOTE] > > 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. > [!WARNING] > > 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); ``` > [!TIP] > > 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 > **[Morpho SDK](https://docs.morpho.org/tools/offchain/sdks/morpho-sdk/)** > > TypeScript SDK reference for Morpho protocol interactions > **[Sending transactions on Tempo](/recipes/tempo/send-transactions)** > > 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. > [!TIP] > > 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 > **[Morpho](/wallets/actions/earn/overview)** > > Deposit into Morpho vaults, collect fees, and track positions with Privy's earn feature. > **[Aave](/recipes/yield/aave-guide)** > > Build lending and vault flows on Aave. > **[Veda](/wallets/actions/earn/providers/veda)** > > Offer yield on Veda BoringVault strategies. > **[Ethena](/recipes/yield/ethena-guide)** > > Earn yield by staking USDe in sUSDe vaults. > **[Sky Savings](/recipes/yield/sky-savings-guide)** > > Earn the Sky Savings Rate by depositing USDS into sUSDS vaults. > **[Kamino](/recipes/yield/kamino-guide)** > > Build a Solana-based yield flow with Kamino Earn. > **[Pods](/recipes/yield/pods-guide)** > > Access diversified yield strategies across multiple DeFi protocols. > **[Jupiter Earn](https://dev.jup.ag/docs/lend/wallets/privy-earn)** > > Deposit and withdraw from Jupiter Earn vaults using Privy embedded wallets. > **[Yield.xyz AgentKit](/recipes/yield/yield-agentkit-guide)** > > Use Privy embedded wallets with Yield.xyz AgentKit. > **[Dolomite](/recipes/community/dolomite-guide)** > > Deposit USD1 into markets powered by Dolomite to earn yield. > **[Ground](/recipes/yield/ground-guide)** > > Offer RWA yield through Ground gtokens held in user-owned wallets. > **[Earn totals onchain](/recipes/yield/earn-onchain-totals)** > > Compute Earn deposit and withdrawal totals directly from vault events. ## Borrow > **[Morpho](/recipes/yield/morpho-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 > **[Pods Docs](https://docs.pods.finance)** > > Official documentation for Pods strategies and API. > **[Starter repo](https://github.com/pods-finance/pods-privy-integration)** > > 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 **1. 1. Get the user's wallet address** ```tsx theme={"system"} import {useWallets} from '@privy-io/react-auth'; const {wallets} = useWallets(); const walletAddress = wallets[0]?.address; ``` **2. 2. Generate transaction bytecodes from Pods** 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; }[]; }; ``` **3. 3. Execute the transactions with Privy** 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 prebuilt `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 ``` > [!WARNING] > > 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"} ``` > [!NOTE] > > 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 > **[Sky Developer Docs](https://developers.skyeco.com)** > > Official documentation for the Sky protocol and smart contracts. > **[Privy Wallets](/wallets/overview)** > > 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 **1. 1. Configure addresses** 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 ``` **2. 2. Approve the sUSDS vault to spend USDS** 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 }); ``` **3. 3. Deposit USDS into the vault** 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. **Withdraw a specific USDS amount** ```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 }); ``` **Redeem all shares (full exit)** ```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). > [!TIP] > > 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 > **[Yield.xyz AgentKit](https://github.com/stakekit/agentkit)** > > GitHub repository for the Yield.xyz AgentKit MCP server. > **[Yield.xyz AgentKit docs](https://docs.yield.xyz/)** > > Official documentation for the Yield.xyz AgentKit. > **[Privy agentic wallets](/recipes/agent-integrations/agentic-wallets)** > > Set up Privy server wallets for autonomous agents. > **[Privy manual approvals](/transaction-management/overview)** > > 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 | > [!NOTE] > > 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. > [!NOTE] > > **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. > [!TIP] > > 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. **1. Verify credentials** The skill checks that `PRIVY_APP_ID`, `PRIVY_APP_SECRET`, and optionally `PRIVY_WALLET_ID` are present. **2. Register the MCP server** The skill registers the Yield.xyz AgentKit MCP server: ```bash theme={"system"} claude mcp add --transport http yield-agentkit https://mcp.yield.xyz/mcp ``` **3. Configure a policy (optional)** 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. **4. Create the wallet** The skill creates a wallet and attaches the policy if one was configured. **5. Fund the wallet** Send assets to the displayed wallet address. **6. Issue DeFi instructions** 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). **1. Complete autonomous setup steps 1–3** Follow the credential verification, MCP registration, and policy configuration steps from the autonomous workflow above. **2. Confirm Enterprise prerequisites** Verify the Enterprise plan is active and the approver has been invited and completed MFA. **3. Create a key quorum** 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}`. **4. Create the wallet** The skill creates the wallet with the key quorum as owner: ```bash theme={"system"} POST /v1/wallets chain_type: "ethereum" owner_id: policy_ids: [] ``` **5. Set up webhooks (optional)** 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. > [!TIP] > > 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). > [!NOTE] > > 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 > **[Yield.xyz AgentKit](https://github.com/stakekit/agentkit)** > > Skill repository and MCP server source. > **[Yield.xyz AgentKit docs](https://docs.yield.xyz/docs/yieldxyz-agentkit)** > > AgentKit reference documentation. > **[Yield.xyz AgentKit MCP](https://docs.yield.xyz/docs/mcp-server)** > > MCP server endpoint and setup. > **[Yield.xyz RWA yields](https://docs.yield.xyz/docs/rwa)** > > Tokenized real-world asset reference. > **[Privy agentic wallets](/recipes/agent-integrations/agentic-wallets)** > > Wallet setup guide for autonomous agents. > **[Privy manual approvals](/transaction-management/overview)** > > Intents API reference. > **[Privy webhooks](/api-reference/webhooks/overview)** > > Webhooks overview for transaction events. > **[Privy dashboard](https://dashboard.privy.io/)** > > 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. > [!TIP] > > 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 > [!NOTE] > > 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](https://mintcdn.com/privy-c2af3412/zlmLhiIqRR7ViKN0/images/security/api-signer.png?fit=max\&auto=format\&n=zlmLhiIqRR7ViKN0\&q=85\&s=401aafbbeab800590d4e4585500384ac) 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. > [!WARNING] > > 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. > [!TIP] > > 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, wallet, and user activity in the Privy Dashboard The Privy Dashboard shows activity logs for the organization and for individual wallets and users. Organization logs record Dashboard actions across all apps. Use these logs to review changes to settings and apps. Each entry shows what changed and when, along with actor details when available. ## View organization activity logs 1. Sign in to the [Privy Dashboard](https://dashboard.privy.io/). 2. Open **Account**. 3. Select **Activity logs**. Or open [Activity logs](https://dashboard.privy.io/organization?organization=activity) directly. ## What organization activity logs record 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. > [!NOTE] > > Access to activity logs depends on the team member's Dashboard permissions. ## View wallet activity logs To review activity for a specific wallet: 1. Sign in to the [Privy Dashboard](https://dashboard.privy.io/) and select your app. 2. Open the [Wallets page](https://dashboard.privy.io/apps?page=wallets). 3. Click a wallet to view its activity logs and wallet details. ## View user activity logs To review activity for a specific user: 1. Sign in to the [Privy Dashboard](https://dashboard.privy.io/) and select your app. 2. Open the [Users page](https://dashboard.privy.io/?page=users\&tab=all-users). 3. Click a user to view their activity logs and user details. ## Activity logs and Webhooks history If the organization enables webhooks, **Webhooks history** also shows activity for apps with a webhook endpoint. The view shows events the endpoint subscribes to. 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. Open a message to view its payload. Filter messages by event type or search by message ID. Security-relevant webhook event types include `user.authenticated`, `mfa.enabled`, `mfa.disabled`, `wallet.private_key_export`, `wallet.seed_phrase_export`, and `wallet.recovered`. > [!TIP] > > Use organization logs to review Dashboard changes. Open a wallet or user to review its activity > and details. If the organization enables webhooks, **Webhooks history** also shows activity for > your app. # Guidance for content security policies (CSPs) Source: https://docs.privy.io/security/implementation-guide/content-security-policy > [!NOTE] > > 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 > [!TIP] > > Remember to add your own domain to the relevant directives (e.g., add your domain to > `connect-src`, `script-src`, etc.) ### Base CSP configuration **Raw CSP** ``` 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' ``` **Next.js** ```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' `, }, ], }, ]; }, }; ``` **Express** ```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 on Rails** ```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: > [!NOTE] > > 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 > [!TIP] > > 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. > [!NOTE] > > 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 | > [!TIP] > > 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 > [!WARNING] > > 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. The IP allowlist applies to all app secrets, including [read-only app secrets](/basics/get-started/dashboard/app-secrets). Privy checks the allowlist before it checks the secret's access level. ## 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 > [!NOTE] > > 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) > [!NOTE] > > 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 > [!NOTE] > > 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 * Use [read-only app secrets](/basics/get-started/dashboard/app-secrets) for services that only read data, such as analytics, support tools, and internal dashboards * 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. > [!TIP] > > 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. > [!NOTE] > > 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. > [!TIP] > > 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 > [!NOTE] > > 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. > [!TIP] > > 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. > [!TIP] > > 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. > [!TIP] > > 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 > [!NOTE] > > 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](https://mintcdn.com/privy-c2af3412/zlmLhiIqRR7ViKN0/images/security/on-device-shares.png?fit=max\&auto=format\&n=zlmLhiIqRR7ViKN0\&q=85\&s=889175c4a676e2af190b733ea6156fda) ### 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. > [!WARNING] > > 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](https://mintcdn.com/privy-c2af3412/zlmLhiIqRR7ViKN0/images/security/on-device-signing.png?fit=max\&auto=format\&n=zlmLhiIqRR7ViKN0\&q=85\&s=aedbe71a00750ec3c627149f2f98a1f9) ### 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](https://mintcdn.com/privy-c2af3412/zlmLhiIqRR7ViKN0/images/security/recovery.png?fit=max\&auto=format\&n=zlmLhiIqRR7ViKN0\&q=85\&s=643b0669dfe159c6781a2cb80758dcfe) ## 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. > [!NOTE] > > 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](https://mintcdn.com/privy-c2af3412/zlmLhiIqRR7ViKN0/images/security/tee-shares.png?fit=max\&auto=format\&n=zlmLhiIqRR7ViKN0\&q=85\&s=43055667284e4eec7a3bd04d45507280) 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. > [!TIP] > > 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. > [!NOTE] > > 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](https://mintcdn.com/privy-c2af3412/zlmLhiIqRR7ViKN0/images/security/enclave-flow.png?fit=max\&auto=format\&n=zlmLhiIqRR7ViKN0\&q=85\&s=219506b83f251a70b0e8a0e5dd1d5409) ## 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. > [!TIP] > > 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 for wallet actions. These actions include signature requests, transactions, and key export. The enclave evaluates all policy rules in a tamper-proof environment before signing or exporting key material. Depending on the chain, method, and app configuration, separate compliance screening or transaction simulation may run outside the enclave. These checks are not policy enforcement. 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](https://mintcdn.com/privy-c2af3412/IQmti1WCL7AzN0e1/images/policy-splash.png?fit=max\&auto=format\&n=IQmti1WCL7AzN0e1\&q=85\&s=c9541babbe140c284d1b52318038ded5) # 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. > [!NOTE] > > Intents expire 72 hours after creation by default. Signers must > [authorize](/transaction-management/intents/sign-intents) them within this window. **REST API** ```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" } } }' ``` **Node SDK** ```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 SDK** ```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 SDK** ```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 SDK** ```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); ``` **Python SDK** ```python theme={"system"} intent = client.intents.rpc( wallet_id, intent_rpc_request_body={ "method": "eth_sendTransaction", "caip2": "eip155:1", "params": { "transaction": { "to": "insert-recipient-address", "value": "0x1", } }, }, ) ``` From the response, note the returned `intent_id`. View the [API reference](/api-reference/intents/rpc) for submitting an RPC intent. ## Next steps > **[Sign intents](/transaction-management/intents/sign-intents)** > > Add authorization signatures to execute a proposed intent. > **[Intent status](/transaction-management/intents/lifecycle)** > > 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. > [!NOTE] > > Intents expire 72 hours after creation by default. Signers must > [authorize](/transaction-management/intents/sign-intents) them within this window. The examples below transfer OUSD on Tempo (chain ID `4217`) using the `ousd` asset alias. **REST API** ```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": "ousd", "amount": "10.0", "chain": "tempo" }, "destination": { "address": "0xB00F0759DbeeF5E543Cc3E3B07A6442F5f3928a2" } }' ``` **Node SDK** ```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: 'ousd', 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 SDK** ```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: "ousd", amount: "10.0", chain: "tempo" }, destination: { address: "0xB00F0759DbeeF5E543Cc3E3B07A6442F5f3928a2" } ) puts(intent.intent_id, intent.status, intent.authorization_details) ``` **Java SDK** ```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("ousd") .amount("10.0") .chain("tempo") .build())) .destination(TokenTransferDestination.builder() .address("0xB00F0759DbeeF5E543Cc3E3B07A6442F5f3928a2") .build()) .build(), null); ``` **Python SDK** ```python theme={"system"} intent = client.intents.transfer( wallet_id, intent_transfer_params={ "amount": "10.0", "source": { "asset": "ousd", "chain": "tempo", }, "destination": {"address": "insert-recipient-address"}, }, ) ``` 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 > **[Sign intents](/transaction-management/intents/sign-intents)** > > Add authorization signatures to execute a proposed intent. > **[Intent status](/transaction-management/intents/lifecycle)** > > 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. > [!NOTE] > > Intents expire 72 hours after creation by default. Signers must > [authorize](/transaction-management/intents/sign-intents) them within this window. **REST API** ```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 }' ``` **Node SDK** ```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 SDK** ```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 SDK** ```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 SDK** ```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); ``` **Python SDK** ```python theme={"system"} intent = client.intents.update_key_quorum( key_quorum_id, intent_update_key_quorum_params={"authorization_threshold": 2}, ) ``` View the [API reference](/api-reference/intents/update-key-quorum) for submitting a key quorum intent. ## Next steps > **[Sign intents](/transaction-management/intents/sign-intents)** > > Add authorization signatures to execute a proposed intent. > **[Intent status](/transaction-management/intents/lifecycle)** > > 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. > [!NOTE] > > Intents expire 72 hours after creation by default. Signers must > [authorize](/transaction-management/intents/sign-intents) them within this window. **REST API** ```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" }' ``` **Node SDK** ```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 SDK** ```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 SDK** ```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 SDK** ```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); ``` **Python SDK** ```python theme={"system"} intent = client.intents.update_policy( policy_id, intent_update_policy_params={"name": "Updated policy"}, ) ``` View the [API reference](/api-reference/intents/update-policy) for submitting a policy intent. ## Next steps > **[Sign intents](/transaction-management/intents/sign-intents)** > > Add authorization signatures to execute a proposed intent. > **[Intent status](/transaction-management/intents/lifecycle)** > > 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. > [!NOTE] > > Intents expire 72 hours after creation by default. Signers must > [authorize](/transaction-management/intents/sign-intents) them within this window. **REST API** ```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" } ] }' ``` **Node SDK** ```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 SDK** ```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 SDK** ```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 SDK** ```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); ``` **Python SDK** ```python theme={"system"} intent = client.intents.create_policy_rule( "insert-policy-id", intent_create_policy_rule_params={ "name": "Restrict destination address", "action": "ALLOW", "method": "eth_sendTransaction", "conditions": [ { "field": "to", "field_source": "ethereum_transaction", "operator": "eq", "value": "insert-token-contract-address", } ], }, ) ``` 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 > **[Sign intents](/transaction-management/intents/sign-intents)** > > Add authorization signatures to execute a proposed intent. > **[Intent status](/transaction-management/intents/lifecycle)** > > 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. > [!NOTE] > > Intents expire 72 hours after creation by default. Signers must > [authorize](/transaction-management/intents/sign-intents) them within this window. **REST API** ```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"] }' ``` **Node SDK** ```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 SDK** ```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 SDK** ```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 SDK** ```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); ``` **Python SDK** ```python theme={"system"} intent = client.intents.update_wallet( wallet_id, intent_update_wallet_params={"display_name": "Updated wallet"}, ) ``` View the [API reference](/api-reference/intents/update-wallet) for submitting a wallet intent. ## Next steps > **[Sign intents](/transaction-management/intents/sign-intents)** > > Add authorization signatures to execute a proposed intent. > **[Intent status](/transaction-management/intents/lifecycle)** > > 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}`. **REST API** ```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", "custom_expiry": false, "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 } } } } } ``` **Node SDK** ```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 SDK** ```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 SDK** ```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 SDK** ```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"); ``` **Python SDK** ```python theme={"system"} intent = client.intents.get("insert-intent-id") ``` ## Response fields **intent\_id** `string` Unique identifier for the intent. **intent\_type** `'RPC' | 'TRANSFER' | 'WALLET' | 'POLICY' | 'RULE' | 'KEY_QUORUM'` The type of action the intent proposes. **created\_by\_display\_name** `string | null` Email address of the team member who proposed the intent. May be `null` for intents proposed via the API. **created\_by\_id** `string` The Privy DID of the team member who proposed the intent. **created\_at** `number` UNIX timestamp (ms) for when the intent was created. **resource\_id** `string` ID of the resource the intent targets (e.g., a wallet ID or policy ID). **authorization\_details** `array` List of key quorums assigned to the intent. **authorization\_details properties** **authorization\_details\[].display\_name** `string` Name of the key quorum. **authorization\_details\[].threshold** `number` Number of approvals required from this quorum. **authorization\_details\[].members** `array` Members of the quorum. **member properties** **members\[].type** `string` The member type (e.g., `'user'`). **members\[].user\_id** `string` The Privy DID of the member. **members\[].display\_name** `string` Email address of the member. **members\[].signed\_at** `number | null` Timestamp of when this member signed. Null if they have yet to sign. **status** `'pending' | 'granted' | 'processing' | 'executed' | 'rejected' | 'expired' | 'dismissed'` Current status of the intent. See [intent lifecycle](/transaction-management/intents/lifecycle) for details on each status. **expires\_at** `number` UNIX timestamp (ms) for when the intent expires. **custom\_expiry** `boolean` Whether the intent was proposed with a custom expiry. When `true`, the [intent signature payload](/transaction-management/intents/sign-intents#signature-payload) must include `expires_at` as a `privy-request-expiry` header. **request\_details** `object` The full request for the proposed action, including HTTP method, URL, and body. Authorization signatures are computed over these values, not over the authorize request. See [intent signature payload](/transaction-management/intents/sign-intents#signature-payload). **action\_result** `object | null` Response body from execution, if the intent has completed. For transaction intents, this contains the transaction hash. > [!TIP] > > For transaction intents, your app can read the transaction hash from `action_result` to track the > transaction on-chain. ## API reference > **[Get intent](/api-reference/intents/get)** > > 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. > [!NOTE] > > 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. **authorization\_details** `array` List of key quorums assigned to the intent. **authorization\_details properties** **authorization\_details\[].display\_name** `string` Name of the key quorum. **authorization\_details\[].threshold** `number` Number of approvals required from this quorum. **authorization\_details\[].members** `array` Members of the quorum. **member properties** **members\[].type** `string` The member type (e.g., `'user'`). **members\[].user\_id** `string` The Privy DID of the member. **members\[].display\_name** `string` Email address of the member. **members\[].signed\_at** `number | null` Timestamp of when this member signed. Null if they have yet to sign. **expires\_at** `number` UNIX timestamp after which the intent expires and can no longer be executed. > [!TIP] > > Intents expire 72 hours after creation by default. Build alerting around the `expires_at` field to > remind reviewers before the window closes. > **[Event reference](/api-reference/webhooks/intents/created)** > > 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. **member** `object` The team member who authorized the intent. **member properties** **member.type** `string` The member type (e.g., `'user'`). **member.user\_id** `string` The Privy DID of the member. **member.display\_name** `string` Email address of the member. **member.signed\_at** `number | null` Timestamp of when this member signed. **authorized\_at** `number` UNIX timestamp when the team member signed the intent. > **[Event reference](/api-reference/webhooks/intents/authorized)** > > 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. **rejected\_at** `number` UNIX timestamp when the intent was rejected. > [!WARNING] > > A rejected intent is in a terminal state. To retry the action, propose a new intent with the same > parameters. > **[Event reference](/api-reference/webhooks/intents/rejected)** > > 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. **action\_result** `object` Result of the failed intent execution. **action\_result properties** **action\_result.status\_code** `number` HTTP status code from the action execution. **action\_result.executed\_at** `number` UNIX timestamp when the action was executed. **action\_result.authorized\_by\_display\_name** `string` Display name of the key quorum that authorized execution. **action\_result.authorized\_by\_id** `string` ID of the key quorum that authorized execution. **action\_result.response\_body** `object` Response from the execution. For transaction intents, this contains the transaction hash. > **[Event reference](/api-reference/webhooks/intents/executed)** > > 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. > [!WARNING] > > A failed intent is in a terminal state. To retry the action, propose a new intent with the same > parameters. **action\_result** `object` Result of the failed intent execution. **action\_result properties** **action\_result.status\_code** `number` HTTP status code from the action execution. **action\_result.executed\_at** `number` UNIX timestamp when the action was executed. **action\_result.authorized\_by\_display\_name** `string` Display name of the key quorum that authorized execution. **action\_result.authorized\_by\_id** `string` ID of the key quorum that authorized execution. **action\_result.response\_body** `object` Response from the execution, if available. > **[Event reference](/api-reference/webhooks/intents/failed)** > > 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. > [!TIP] > > 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 > **[Sign intents](/transaction-management/intents/sign-intents)** > > Add authorization signatures to execute a proposed intent. > **[Fetch intent](/transaction-management/intents/fetch-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.](https://mintcdn.com/privy-c2af3412/IQmti1WCL7AzN0e1/images/intents-sync-async.png?fit=max\&auto=format\&n=IQmti1WCL7AzN0e1\&q=85\&s=0dd7cbb978a60e5ac1de4a23de9b5829) > [!NOTE] > > 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 intents](/transaction-management/intents/create/execute-transfer)** > > Propose an intent to transfer funds, run a transaction, or update a resource. > **[Sign intents](/transaction-management/intents/sign-intents)** > > Add authorization signatures to execute a proposed intent. > **[Intent status](/transaction-management/intents/lifecycle)** > > Track an intent from proposal to execution. > **[API reference](/api-reference/intents/get)** > > 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. > [!TIP] > > Only a pending intent can be rejected. An intent that is already in a terminal or `Processing` > state cannot be rejected. ## API reference > **[Reject intent](/api-reference/intents/reject)** > > View the full API reference for rejecting an intent. ## Next steps > **[Authorize intent](/transaction-management/intents/sign-intents)** > > Authorize a pending intent so Privy can execute it. > **[Intent status](/transaction-management/intents/lifecycle)** > > 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: **signature** `string` *required* An [authorization signature](/api-reference/authorization-signatures) over the intent's action. **timestamp** `number` *required* Unix timestamp, in milliseconds, when the signature was created. Privy uses it to verify the signing key was valid at signing time. Must match the `timestamp` in the [signature payload](#signature-payload). **REST API** ```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 SDK** ```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, generated with the resource owner's authorization key. Intents use their own payload shape, described in [signature payload](#signature-payload) below. > [!TIP] > > 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. ## Signature payload An intent authorization signature covers the intent's **underlying action**, not the authorize request. The payload is the [standard signature payload](/controls/authorization-keys/using-owners/sign/overview#signature-payload) plus two fields specific to intents, `timestamp` and `intent_id`. Omitting either one fails verification with `Invalid signature`. > [!TIP] > > Privy's SDKs build and sign this payload automatically. The fields below are only needed when > [implementing signing > directly](/controls/authorization-keys/using-owners/sign/direct-implementation). Sign a JSON object with the following fields: **version** `1` *required* Authorization signature version. Currently, `1` is the only version. **method** `'POST' | 'PATCH' | 'DELETE'` *required* HTTP method of the intent's underlying action, copied from `request_details.method`. This is not the method of the authorize request. **url** `string` *required* Full URL of the intent's underlying action, copied from `request_details.url` (e.g. `https://api.privy.io/v1/wallets/{wallet_id}/rpc`). This is not the URL of the authorize endpoint. **body** `JSON` *required* JSON body of the intent's underlying action, copied from `request_details.body`. **timestamp** `number` *required* Unix timestamp, in milliseconds, when the signature was created. Must match the `timestamp` sent in the authorize request body, and must be within 5 minutes of Privy's server time. **intent\_id** `string` *required* ID of the intent being authorized. Binds the signature to a single intent so it cannot be replayed against another. **headers** `object` *required* JSON object containing only the headers below. Exclude all others, including `privy-idempotency-key`, which Privy does not include when verifying intent signatures. **headers properties** **headers.privy-app-id** `string` *required* Privy app ID. **headers.privy-request-expiry** `string` The intent's `expires_at` value as a string. Include this only when the intent's `custom_expiry` is `true`. An example payload for an RPC intent: ```json theme={"system"} { "version": 1, "method": "POST", "url": "https://api.privy.io/v1/wallets/xs76o3pi0v5syd62ui1wmijw/rpc", "body": { "method": "eth_sendTransaction", "caip2": "eip155:4217", "chain_type": "ethereum", "params": { "transaction": { "to": "0x0000000000000000000000000000000000000000", "value": 1 } } }, "timestamp": 1741834854578, "intent_id": "clpq1234567890abcdefghij", "headers": { "privy-app-id": "insert-your-app-id" } } ``` Canonicalize the payload per [RFC 8785](https://www.rfc-editor.org/rfc/rfc8785), sign it with ECDSA P-256, and base64-encode the signature, exactly as described in [implementing signing directly](/controls/authorization-keys/using-owners/sign/direct-implementation). Unlike other signed requests, the signature is submitted in the authorize request **body** rather than in a `privy-authorization-signature` header. ### Worked example The example below fetches an intent, builds the payload from its `request_details`, signs it, and submits the authorization. Because Privy rejects a `timestamp` more than 5 minutes from its server time, build and sign the payload immediately before submitting it. ```typescript theme={"system"} import canonicalize from 'canonicalize'; import crypto from 'crypto'; const PRIVY_APP_ID = 'insert-your-app-id'; const PRIVY_APP_SECRET = 'insert-your-app-secret'; const PRIVY_AUTHORIZATION_KEY = 'wallet-auth:insert-your-private-key-here'; const authHeaders = { Authorization: `Basic ${Buffer.from(`${PRIVY_APP_ID}:${PRIVY_APP_SECRET}`).toString('base64')}`, 'privy-app-id': PRIVY_APP_ID, 'Content-Type': 'application/json' }; async function authorizeIntent(intentId: string) { // 1. Fetch the intent to read the underlying action and expiry details. const intent = await fetch(`https://api.privy.io/v1/intents/${intentId}`, { headers: authHeaders }).then((res) => res.json()); // 2. Build the payload. `timestamp` must be within 5 minutes of Privy's server time. const timestamp = Date.now(); const payload = { version: 1, method: intent.request_details.method, url: intent.request_details.url, body: intent.request_details.body, timestamp, intent_id: intentId, headers: { 'privy-app-id': PRIVY_APP_ID, // Only required for intents proposed with a custom expiry. ...(intent.custom_expiry ? {'privy-request-expiry': String(intent.expires_at)} : {}) } }; // 3. Canonicalize per RFC 8785 and sign with ECDSA P-256. const serializedPayload = Buffer.from(canonicalize(payload) as string); const privateKey = crypto.createPrivateKey({ key: `-----BEGIN PRIVATE KEY-----\n${PRIVY_AUTHORIZATION_KEY.replace('wallet-auth:', '')}\n-----END PRIVATE KEY-----`, format: 'pem' }); const signature = crypto.sign('sha256', serializedPayload, privateKey).toString('base64'); // 4. Submit the signature with the same timestamp used in the payload. return fetch(`https://api.privy.io/v1/intents/${intentId}/authorize`, { method: 'POST', headers: authHeaders, body: JSON.stringify({signature, timestamp}) }).then((res) => res.json()); } ``` ## 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, does not match the [signature payload](#signature-payload), or the `timestamp` falls outside the window in which the signing key was valid. | | Timestamp out of range | The `timestamp` is more than 5 minutes from Privy's server time. | | Ineligible signer | The signer is not an owner or signer eligible to authorize this intent. | ## API reference > **[Authorize intent](/api-reference/intents/authorize)** > > View the full API reference for authorizing an intent. ## Next steps > **[Propose intents](/transaction-management/intents/create/execute-transfer)** > > Propose an intent to transfer funds, run a transaction, or update a resource. > **[Intent status](/transaction-management/intents/lifecycle)** > > Track an intent from proposal to execution. > **[Reject intent](/transaction-management/intents/reject-intents)** > > Cancel a pending intent before it is authorized and executed. # Set a reference ID Source: https://docs.privy.io/transaction-management/transactions/reference-id > [!WARNING] > > 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. **EVM** ```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" } } }' ``` **Solana** ```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. # Collect approvals Source: https://docs.privy.io/treasury-management/approvals > [!NOTE] > > Manual Approvals is an Enterprise feature. Reach out to [sales@privy.io](mailto:sales@privy.io) to > request access. To require approval from the assigned quorum of reviewers before a treasury operation (transfers, configuration changes, and signatures) executes, you will need to enable Manual Approvals. All approvals are secured by biometric and/or TOTP MFA. No transaction can execute without the required number of authenticated approvals. > **[Manual approvals documentation](/controls/dashboard/overview)** > > Full guide to setting up reviewer groups, proposing intents, and understanding intent lifecycle > and statuses. ## Approval workflow At a high level, Treasury Management follows a **propose → review → approve** flow: **1. Propose an intent** A team member [initiates a transfer](/treasury-management/transfers), signature, or configuration change via the Treasury Management workspace or API. This creates a pending intent. **2. Review the intent** Team members in the assigned quorum [review the pending intent](/controls/dashboard/approvals) on the Approvals page. Each reviewer inspects the details — recipient, amount, chain, and any relevant context. **3. Approve or reject** Reviewers authorize or reject the intent using MFA-secured approval. Once the quorum threshold is met (e.g., 2-of-3 approval), the transaction executes automatically. ## Related resources > **[Key quorum setup](/treasury-management/setup#creating-a-key-quorum)** > > Create and manage approval groups > **[Policies](/treasury-management/setup#5-add-policies-optional)** > > Configure enforceable transaction guardrails # FAQ Source: https://docs.privy.io/treasury-management/faq Frequently asked questions about Treasury Management. ## How do I navigate to the Treasury Management workspace? The [Treasury Management workspace](https://dashboard.privy.io/treasury-management) lives at the **account level** in the Privy dashboard, not within a specific app. To access it: 1. Log in to your [Privy dashboard](https://dashboard.privy.io) 2. Navigate to the account-level view 3. Click **Treasury Management** in the sidebar ![](https://mintcdn.com/privy-c2af3412/M19afhOpULYpAAbw/images/treasury-nav.png?fit=max\&auto=format\&n=M19afhOpULYpAAbw\&q=85\&s=61bf7839eaeb33e229d059f7db29255f) ## I sent funds to my wallet, but I can't see the balance in the Treasury Management workspace The Treasury Management workspace only displays balances for **monitored assets**. If you funded your wallet but don't see the balance: 1. Navigate to the [Assets](https://dashboard.privy.io/treasury-management?page=settings%2Fassets) page under **Treasury Management > Settings > Assets** 2. Confirm the token you deposited is listed as a monitored asset 3. If it is a custom ERC20 or SPL token, first add it to your [Asset watchlist](https://dashboard.privy.io/apps?page=settings\&tab=token-watchlist) as a monitored asset Assets not configured in the dashboard are still held by the wallet and accessible via Privy APIs — they just won't appear in the dashboard UI. For more details, see [Configure monitored assets](/treasury-management/setup#6-configure-monitored-assets). ## How do I verify the balance of my wallet? The Treasury Management workspace only displays balances for supported and monitored assets. If you have sent unsupported assets to your wallet and would like to confirm the onchain balance of a treasury wallet independently of the dashboard: 1. Click on the wallet name in the Treasury Management workspace 2. Select **Details** and copy the wallet **Address** 3. Paste the address into a block explorer for the relevant chain (e.g., [Etherscan](https://etherscan.io) for Ethereum, [Solscan](https://solscan.io) for Solana) The explorer displays all token balances and transaction history for that address, regardless of which assets are monitored in the dashboard. ## I see an asset in my Treasury Management workspace, but I'm not able to send it out If a transfer is being blocked despite the asset appearing in your dashboard balance, first check the following: 1. **Policy restrictions**: A [policy](/treasury-management/setup#3-add-policies) on the wallet may restrict which tokens, chains, or recipients are allowed. Navigate to **Treasury > Policies** to review the rules applied to your wallet. 2. **Quorum approvals**: The transaction may require [key quorum approval](/treasury-management/approvals) before it executes. Check **Treasury > Pending** for transactions awaiting signatures. 3. **Gas sponsorship**: If the wallet does not hold native gas tokens and [gas sponsorship](/treasury-management/setup#5-set-up-gas-sponsorship) is not configured, the transaction cannot be submitted onchain. 4. **Restricted assets and chains**: The workspace only supports a subset of [supported chains and assets](/treasury-management/supported-chains-and-assets). > [!WARNING] > > If none of the above apply, you will need to [recover unsupported assets via > API](/recipes/dashboard/recovering-assets). ## Can I both automate actions and collect manual approvals? Yes. Treasury wallets support both automated transfers via API and manual approval flows. Your app can programmatically [initiate transfers via the Transfers API](/wallets/actions/transfer/overview) while still enforcing [key quorum approvals](/treasury-management/approvals) and [policies](/treasury-management/setup#3-add-policies) on each transaction. For example, an automated system can propose a transfer, but the transaction will not execute until the required number of quorum members approve it — unless the policy and quorum configuration allow for immediate execution. See [Automations](/treasury-management/transfers#automations) for more on programmatic transfers. ## How do I convert from stablecoins to fiat? To offramp from stablecoins back to fiat currency, [initiate a transfer](/treasury-management/transfers) from your treasury wallet to a liquidation address or virtual account address provided by your offramp partner. Once the stablecoin transfer confirms on-chain, the offramp provider converts the funds to fiat and deposits them into your bank account. Common payout approaches include: * Sending USDC/USDT onchain to an exchange account or OTC desk * Transferring to a virtual account address tied to a fiat settlement rail ## How do I request support for another chain? To request support for an additional chain on the Treasury Management workspace, please reach out to [sales@privy.io](mailto:sales@privy.io). > [!NOTE] > > Privy is actively adding more chains. [Contact sales](mailto:sales@privy.io) with your chain > requirements and timeline. ## I need more help understanding the Treasury Management workspace. Please reach out to [sales@privy.io](mailto:sales@privy.io)! We'd be happy to assist. # Treasury management Source: https://docs.privy.io/treasury-management/overview The [Treasury Management workspace](https://dashboard.privy.io/treasury-management) is an out-of-the-box tool that enables your Finance and Operations team to hold, move, and govern your onchain capital. Secure your operations with maker-checker flows, configurable authorization thresholds, easy automations, and granular policies -- all through an accessible interface. ![](https://mintcdn.com/privy-c2af3412/M19afhOpULYpAAbw/images/treasury.png?fit=max\&auto=format\&n=M19afhOpULYpAAbw\&q=85\&s=debeacc05d1056747d161fac0f5e21ee) With the Treasury Management workspace, your Finance and Ops teams can easily manage company capital, such as to: * Initiate stablecoin transfers via a simple and accessible UI * Rebalance liquidity across international entities instantly * Manage and top up funds to your partners' accounts * Orchestrate advanced disbursement flows to customers' accounts * Store and monitor revenue from your application's onchain earnings * Earn yield 24/7 on idle capital, with no lockups ## Features The workspace is a separate, unified surface designed specifically for Operations and Finance teams. Your team can access Privy's full capabilities through a simple, user-friendly interface: * Create wallets on [supported chains](/treasury-management/supported-chains-and-assets) with configurable controls * Define quorums of team members that require m-of-n approvals for sensitive actions * Apply policies conditionally to different team members, enabling different levels of access over wallets for different users * Enforce transfer limits, recipient allowlists, and chain restrictions at the per-wallet level * Review and approve transactions with MFA-secured manual approvals * Monitor balances and activity across your entire treasury in real time * Implement automations to reduce manual effort > [!NOTE] > > The Treasury Management workspace lives at the account level in your Privy dashboard, across all > applications. It is not app-specific. ## Supported chains and assets The Treasury Management workspace has full support for certain EVM chains, Solana, and Tron. See the full list of [supported chains and assets](/treasury-management/supported-chains-and-assets). > [!WARNING] > > Assets sent to wallets on unsupported chains are not recoverable via the dashboard, and must be > [recovered via API](/recipes/dashboard/recovering-assets). ## Get started > **[Set up Treasury Management](/treasury-management/setup)** > > Configure wallets, team member quorums, and policies in the Treasury Management workspace > **[Transfers](/treasury-management/transfers)** > > Execute transfers from your Treasury Management wallets # Setup Source: https://docs.privy.io/treasury-management/setup Configure the Treasury Management workspace for your team. All setup can be performed directly in the Treasury Management workspace, with no code required. This guide assumes you have already created your [Privy dashboard](https://dashboard.privy.io) account. ## 1. Access the Treasury Management workspace [Access the Treasury Management workspace](https://dashboard.privy.io/treasury-management). You may also navigate to the workspace via the account level drop-down. Click **Treasury**, as demonstrated in the image below. ![](https://mintcdn.com/privy-c2af3412/M19afhOpULYpAAbw/images/treasury-nav.png?fit=max\&auto=format\&n=M19afhOpULYpAAbw\&q=85\&s=61bf7839eaeb33e229d059f7db29255f) Upon entering the workspace, you will be prompted to create your first wallet. Enter a name and a default chain for the wallet. ## 2. Set up gas sponsorship [Configure gas sponsorship](https://dashboard.privy.io/treasury?page=settings%2Fgas-sponsorship) to cover network fees for transactions initiated from the Treasury Management workspace. [Gas sponsorship](/wallets/gas-and-asset-management/gas/overview) ensures that treasury wallets can transact without needing to hold native gas tokens on every chain. > [!NOTE] > > To enable non-EVM gas sponsorship for your Treasury Management workspace, please reach out to > [sales@privy.io](mailto:sales@privy.io). ## 3. Configure monitored assets By default, the Treasury Management workspace supports all native tokens (ETH, SOL, POL) and all ERC20/SPL tokens, including stablecoins like USDC and USDT. [Manage which default assets](https://dashboard.privy.io/treasury?page=settings%2Fassets) you want to monitor in the dashboard under **Treasury > Settings > Assets**. Unmonitored assets are still supported by Privy APIs but will not be visible in your dashboard. > [!NOTE] > > To monitor custom assets such as custom ERC20/SPL tokens, first add them to your Asset watchlist > in the Treasury Management workspace, under **Treasury > Settings > Assets > Add custom asset**. Learn more about supported assets [here](/treasury-management/supported-chains-and-assets). ## 4. Fund your wallet There are two ways to deposit funds into a Treasury Management wallet: * Fund from a card-based onramp. From the [Treasury Wallets](https://dashboard.privy.io/treasury-management) view, click **Fund** next to the wallet you'd like to deposit into. * Fund from an external funding source. You can retrieve the wallet's address by clicking on the wallet name, selecting **Details**, then copying the **Address**. Share this address with your funding source (exchange, another wallet, or fiat onramp) to complete the deposit. Once the transaction confirms onchain, the balance will appear in the Treasury Management workspace for any [monitored assets](#3-configure-monitored-assets). > [!WARNING] > > When funding treasury wallets, only deposit assets on chains that are [explicitly > supported](/treasury-management/supported-chains-and-assets) via Privy's Treasury Management > workspace. Any other assets cannot be recovered from the Dashboard, and must be [recovered by a > direct API call](/recipes/dashboard/recovering-assets) with manual approval. ## 5. Add policies (optional) You can optionally [configure policies](https://dashboard.privy.io/treasury?page=settings%2Fpolicies) to enforce additional guardrails on your treasury wallets: * Transfer limits (per-transaction or time-windowed) * Recipient allowlists or denylists * Chain and token restrictions * Smart contract interaction rules > [!WARNING] > > Policies are enforced in Privy's secure execution environment before any transaction is signed: > even if a quorum approves, policy violations are blocked. #### Setting up policies Policies are granular and expressive. The example below shows a simple policy to deny transactions worth over 2000 USDC. ![Creating a policy in the Privy Dashboard to deny transactions over 2000 USDC](https://mintcdn.com/privy-c2af3412/x5eDwvbar1J3ZpOb/images/treasury-sample-policy.png?fit=max\&auto=format\&n=x5eDwvbar1J3ZpOb\&q=85\&s=03b8425c6d20140e0602c6e02b1da0fe) Learn more about policies [here](/controls/policies/overview). #### Applying policies Once you create a policy, you will need to apply it to a specific wallet. Navigate to the **Treasury > Wallets** page, and click on the desired wallet that will be assigned the policy. Select the edit (pencil) icon, add your desired policies, then **Save changes**. ## 6. Set up Manual Approvals (optional) > [!NOTE] > > Manual Approvals is an Enterprise feature. Reach out to [sales@privy.io](mailto:sales@privy.io) to > request access. #### What are Manual Approvals? Manual Approvals let you require designated team members to review and approve certain actions before they can be executed. You can optionally use Manual Approvals to add an additional layer of review for proposed intents such as wallet or policy updates, and transactions in the Privy Treasury Management workspace. To determine who must approve an action, Manual Approvals uses **key quorums**. #### What are key quorums? A key quorum is a group of team members with a defined approval threshold. When a quorum is assigned to a resource, such as a wallet or policy, the designated members must satisfy the configured threshold of approvals before an action can execute. A key quorum specifies: * **Members**: which team members are part of the approval group * **Threshold**: how many members must approve before the action executes (e.g., 2-of-3) Set up different quorums for different risk levels. For example: * A 1-of-2 quorum for routine, lower-value transfers * A 2-of-3 quorum for high-value or one-off transactions requiring greater team review #### Creating a key quorum To require manual approvals from a designated group of reviewers, [create a quorum for manual review](/controls/dashboard/key-quorum). At a high level, you will need to: 1. Invite the relevant team members to your Privy account 2. Enroll those team members in MFA 3. [Register the quorum](https://dashboard.privy.io/treasury-management?page=settings%2Fkeys) in the Treasury Management workspace 4. Apply the quorum as the owner of the relevant resource, such as a wallet or policy #### Applying quorums Once you create a quorum, you will need to apply it to a specific resource, such as a wallet or policy. Navigate to the **Treasury > Wallets** page, and open any wallet you'd like to assign a quorum to. Select the edit (pencil) icon, input your desired key quorum in the **Owner** field, then **Save changes**. > [!NOTE] > > Once you apply ownership and controls, all further updates to the wallet will require the assigned > quorum's approval. ## Next steps > **[Transfers](/treasury-management/transfers)** > > Learn how and when to propose transactions day-to-day # Supported chains and assets Source: https://docs.privy.io/treasury-management/supported-chains-and-assets The Treasury Management workspace supports transfers, balance monitoring, and wallet creation on the chains and assets listed below. > [!WARNING] > > Assets sent to wallets on unsupported chains are not recoverable via the dashboard, and must be > [recovered via API](/recipes/dashboard/recovering-assets). ## Supported 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_testnet` | | Robinhood Testnet | `eip155:46630` | `robinhood_testnet` | | Solana Devnet | `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` | `solana_devnet` | | Tron | `tron:mainnet` | `tron` | | Tron Nile | `tron:nile` | `tron_nile` | To transfer funds on chains not listed above, [recover assets via API](/recipes/dashboard/recovering-assets) to construct and send transactions directly. ## Supported assets By default, the Treasury Management workspace supports all native tokens (ETH, SOL, POL) and all ERC20/SPL tokens, including stablecoins like USDC and USDT. [Manage which default assets](https://dashboard.privy.io/treasury?page=settings%2Fassets) you want to monitor in the dashboard under **Treasury > Settings > Assets**. Unmonitored assets are still supported by Privy APIs but will not be visible in your dashboard until added as Monitored assets. > [!NOTE] > > To monitor custom assets such as custom ERC20 or SPL tokens, first add them to your Asset > watchlist in the Treasury Management workspace, under **Treasury > Settings > Assets > Add custom > asset**. Privy supports the following well-known assets natively on the chains where they are available. | Asset | API value | | - | - | | OUSD | `ousd` | | 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` | > [!TIP] > > 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. # Initiate transfers Source: https://docs.privy.io/treasury-management/transfers Initiate transfers from your treasury wallets via the Treasury Management workspace or API. Transfers can be used to: * Rebalance liquidity across your treasury wallets * Facilitate cross-border payments between your international entities * Top up your partner accounts * Offramp from stablecoin back to fiat, by sending funds to a liquidation address or virtual account address > [!NOTE] > > Dashboard transfers are only supported on [chains supported by the Treasury Management > workspace](/treasury-management/supported-chains-and-assets). **1. Identify the source wallet** From the [Treasury Management workspace](https://dashboard.privy.io/treasury-management), select **New transaction** and choose your desired source wallet from which to initiate the transfer. Alternatively, select **Send** directly from the source wallet. **2. Enter amount to transfer** Enter the amount of funds to transfer out of the Treasury wallet. Specify the desired asset type and network. **3. Select destination wallet** Select a destination wallet to transfer funds to. You may either search for an existing Treasury wallet created within the dashboard, or manually input an external wallet address. **4. Initiate the transfer** Review the final details, and **Propose transaction**. Once submitted, the transfer creates a pending intent that requires approval from the wallet's assigned key quorum before it executes. If you have not configured additional manual approvals for the given transfer, you will be able to immediately execute the transaction instead. ## Supported assets Dashboard-based transfers only support tokens configured on the **Assets** tab. To configure which assets appear in your dashboard, see [Setup](/treasury-management/setup#3-configure-monitored-assets). ## Supported chains See the [supported chains](/treasury-management/supported-chains-and-assets) for the Treasury Management workspace. > [!WARNING] > > To recover assets on unsupported chains, use the [asset recovery > recipe](/recipes/dashboard/recovering-assets). ## Programmatic transfers Transfers from Treasury wallets can also be automated to eliminate manual workflows. Programmatically [initiate transfers via API](/wallets/actions/transfer/overview). ## Next steps > **[Collect approvals](/treasury-management/approvals)** > > Learn how proposed intents are reviewed and approved by your team # 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. **REST API** 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 **linked\_accounts** `LinkedAccount[]` *required* 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. > **LinkedAccount types** > > > **AppleAccount** > > > > | 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. | > > > **CustomJwtAccount** > > > > | Field | Type | Description | > > | - | - | - | > > | `type` | `'custom_auth'` | N/A | > > | API: `custom_user_id` SDK: `customUserId` | `string` | ID of user from custom auth provider. | > > > **DiscordAccount** > > > > | 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)) > > > **EmailAccount** > > > > | Field | Type | Description | > > | - | - | - | > > | `type` | `'email'` | N/A | > > | `address` | `string` | Email address of user account. | > > > **FarcasterAccount** > > > > | 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.) > > > **GithubAccount** > > > > | 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)) > > > **GoogleAccount** > > > > | 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. | > > > **InstagramAccount** > > > > | 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/)) > > > **LinkedinAccount** > > > > | 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)) > > > **PhoneAccount** > > > > | 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. > > > **SpotifyAccount** > > > > | 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)) > > > **TelegramAccount** > > > > | 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)) > > > **TwitterAccount** > > > > | 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)) > > > **SmartWalletAccount** > > > > | 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'` | > > > **WalletAccount** > > > > | 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. | **wallets** `WalletCreateRequestType[]` (Optional) An array of wallets to create for the user. **chain\_type** `'ethereum' | 'solana' | 'stellar' | 'cosmos' | 'sui' | 'tron' | 'bitcoin-segwit' | 'near' | 'ton' | 'starknet' | 'movement' | 'aptos'` *required* The chain type of the wallet to create. **additional\_signers** `object[]` **signer\_id** `string` The ID of the signer. **policy\_ids** `string[]` List of policy IDs for policies that should be enforced on the wallet. Currently, only one policy is supported per wallet. **create\_smart\_wallet** `boolean` 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: **action** `string` The action taken ("create"). **index** `number` The index of the user in the request array. **success** `boolean` Whether the user creation succeeded. **id** `string` The Privy DID of the user (if successful). **code** `number` Error code (if unsuccessful). **error** `string` Error message (if unsuccessful). **cause** `string` The conflicting DID (if there was an account conflict). > [!NOTE] > > 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. **NodeJS** 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. **Java** 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().orElse(e.getMessage()); System.err.println(errorBody); } catch (Exception e) { System.err.println(e.getMessage()); } ``` ### Parameters You can specify the following values on the `UserCreateRequestBody` builder: **linkedAccounts** `List\` *required* A list of linked accounts to create for the user. **customMetadata** `Map\` 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. **wallets** `List\` 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. **user()** `Optional\` The created `User` object. See the [user object](/user-management/users/the-user-object) for more details. **Rust** 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). **Go** 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. **Ruby** 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. **REST API** Make a `POST` request to: ```sh theme={"system"} https://api.privy.io/v1/users ``` Below is a **sample cURL command** for creating a new user: ```bash theme={"system"} $ curl --request POST https://api.privy.io/v1/users \ -u ":" \ -H "privy-app-id: " \ -H 'Content-Type: application/json' \ -d '{ "linked_accounts": [ { "address": "batman@privy.io", "type": "email" } ] }' ``` ### Parameters **linked\_accounts** `LinkedAccount[]` *required* 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. **custom\_metadata** `object` 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. **wallets** `WalletCreateRequestType[]` (Optional) An array of wallets to create for the user. **chain\_type** `'ethereum' | 'solana' | 'stellar' | 'cosmos' | 'sui' | 'tron' | 'bitcoin-segwit' | 'near' | 'ton' | 'starknet' | 'aptos'` *required* The chain type of the wallet to create. **additional\_signers** `object[]` **signer\_id** `string` The ID of the signer. **override\_policy\_ids** `string[]` 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. **policy\_ids** `string[]` List of policy IDs for policies that should be enforced on the wallet. Currently, only one policy is supported per wallet. **create\_smart\_wallet** `boolean` 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`. 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 } ] } ``` **Python** Use the `create` method on the users service. ```python theme={"system"} user = client.users.create( linked_accounts=[ {"type": "email", "address": "user@example.com"}, ], wallets=[{"chain_type": "ethereum"}], ) ``` > [!NOTE] > > 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. **Step 1** If entry does not have a `PrivyDID`, add the Privy DID from your request to the `PrivyDID` column in your database. **Step 2** 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. **Step 3** 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. > [!TIP] > > 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 > **Migrating from a custodial system** > > 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. > **Migrating from a non-custodial system** > > 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 > **Migrating from a custodial system (where you have access to user keys)** > > 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. > **Migrating from a non-custodial system** > > We recommend you prompt your user to export their keys so they can use them with an external > wallet (like MetaMask). > [!TIP] > > 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. > [!NOTE] > > Partial updates are currently only supported via the REST API. SDK methods replace the complete > custom metadata object. **NodeJS** 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. **Java** 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().orElse(e.getMessage()); 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: **customMetadata** `Map\` 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. **user()** `Optional\` The updated user object. See the [user object](/user-management/users/the-user-object) for more details. **Rust** 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 **Go** 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) ``` **Ruby** 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) ``` **REST API** ### 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://api.privy.io/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://api.privy.io/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 } ] } ``` > [!WARNING] > > 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://api.privy.io/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://api.privy.io/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 } ] } ``` > [!NOTE] > > 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. **Python** Use the `set_custom_metadata` method on the users service. ```python theme={"system"} user = client.users.set_custom_metadata( "insert-user-id", custom_metadata={ "plan": "premium", "is_active": True, }, ) ``` # 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. > [!TIP] > > 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: **claims** **linked\_accounts** `string` A stringified array containing a lightweight version of the current user's `linkedAccounts`. It omits [`user_can_sign`](/kyc-kyb/entities#check-whether-the-user-can-sign), so your app should read the user from the SDK or API when it needs that field. **custom\_metadata** `string` A stringified version of the current user's `customMetadata` **sub** `string` The user's Privy DID **iss** `string` The token issuer, which should always be `privy.io` **aud** `string` Your Privy app ID **iat** `number` The timestamp of when the JWT was issued **exp** `number` The timestamp of when the JWT will expire (generally 1 hour after issuance) ## Retrieving identity tokens **React** 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 ( ); } ``` > [!TIP] > > We strongly recommend setting a base domain for your application, so that Privy can set the identity token as a more secure **HttpOnly** cookie. **React Native** 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 ( ; } ``` **React Native** 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) } }) ``` **onSuccess** `(\{user: User, linkMethod: string, linkedAccount: linkedAccount\}) =\> void` Optional callback to run after a user successfully links an account. **onError** `(error: string) =\> void` Optional callback to run after there is an error during account linkage. > [!NOTE] > > **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: **provider** `string` *required* 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 **name** `string` 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 **jwt** `string` *required* 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); } }); ``` > [!NOTE] > > 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. **React Native** **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` | > [!NOTE] > > 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. **Email** 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}> ``` **email** `string` *required* The email address to send the verification code to. ### Link with code ```tsx theme={"system"} linkWithCode({code: string, email?: string}) => Promise ``` **code** `string` *required* The one-time passcode received at the email address. **email** `string` 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: **provider** `string` *required* 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'`). **subject** `string` *required* 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. **React Native** 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` | > [!NOTE] > > Users are only permitted to unlink **an account** so long as they have at least one more linked account. **Email** ### Usage ```tsx theme={"system"} import {useUnlinkEmail} from '@privy-io/expo'; const {unlinkEmail} = useUnlinkEmail(); await unlinkEmail({ email: 'user@example.com' }); ``` ### Parameters **email** `string` *required* 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), }); ``` **onSuccess** `(user: User) =\> void` Optional callback to run after a user successfully unlinks an email address. **onError** `(err: string) =\> void` Optional callback to run after there is an error during email unlinking. **Wallet** ### Usage ```tsx theme={"system"} import {useUnlinkWallet} from '@privy-io/expo'; const {unlinkWallet} = useUnlinkWallet(); await unlinkWallet({ address: 'wallet_address' }); ``` ### Parameters **address** `string` *required* 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), }); ``` **onSuccess** `(user: User, isNewUser: boolean) =\> void` Optional callback to run after a user successfully unlinks a wallet. **onError** `(err: string) =\> void` Optional callback to run after there is an error during wallet unlinking. **OAuth** ### Usage ```tsx theme={"system"} import {useUnlinkOAuth} from '@privy-io/expo'; const {unlinkOAuth} = useUnlinkOAuth(); await unlinkOAuth({ provider: 'google', subject: 'subject_identifier' }); ``` ### Parameters **provider** `'google' | 'apple' | 'twitter' | 'discord' | 'github' | 'linkedin' | 'tiktok' | 'spotify' | 'instagram'` *required* The OAuth provider for the account to unlink. **subject** `string` *required* The provider-specific subject ("sub" claim) that uniquely identifies the user for the selected OAuth provider. **Farcaster** ### Usage ```tsx theme={"system"} import {useUnlinkFarcaster} from '@privy-io/expo'; const {unlinkFarcaster} = useUnlinkFarcaster(); await unlinkFarcaster({ fid: 123 }); ``` ### Parameters **fid** `number` *required* 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), }); ``` **onSuccess** `(user: User) =\> void` Optional callback to run after a user successfully unlinks a Farcaster account. **onError** `(err: string) =\> void` Optional callback to run after there is an error during Farcaster unlinking. **Swift** > [!NOTE] > > Users are only permitted to unlink **an account** so long as they have at least one more linked account. **Email** Use the following method from the `email` handler to unlink an email address: ```swift theme={"system"} func unlink(email: String) async throws -> PrivyUser ``` ### Returns **PrivyUser** `PrivyUser` 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 } ``` **Phone** Use the following method from the `sms` handler to unlink a phone number: ```swift theme={"system"} func unlink(phoneNumber: String) async throws -> PrivyUser ``` ### Returns **PrivyUser** `PrivyUser` 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 } ``` **Ethereum Wallet** Use the following method from the `siwe` handler to unlink an Ethereum wallet: ```swift theme={"system"} func unlink(address: String) async throws -> PrivyUser ``` ### Returns **PrivyUser** `PrivyUser` 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 } ``` **Solana 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 **PrivyUser** `PrivyUser` 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 } ``` **Passkey** 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 **PrivyUser** `PrivyUser` 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 } ``` **OAuth** 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 **provider** `OAuthProvider` *required* The OAuth provider for the account to unlink (e.g., `.google`, `.discord`, `.twitter`). **subject** `String` *required* The provider-specific subject identifier that uniquely identifies the user for the selected OAuth provider. ### Returns **PrivyUser** `PrivyUser` 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 } ``` **Android** > [!NOTE] > > Users are only permitted to unlink **an account** so long as they have at least one more linked account. **Email** Use the following method from the `email` handler to unlink an email address: ```kotlin theme={"system"} public suspend fun unlink(email: String): Result ``` ### Returns **Result\** `Result\` 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}") } ) ``` **Phone** Use the following method from the `sms` handler to unlink a phone number: ```kotlin theme={"system"} public suspend fun unlink(phoneNumber: String): Result ``` ### Returns **Result\** `Result\` 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}") } ) ``` **Ethereum Wallet** Use the following method from the `siwe` handler to unlink an Ethereum wallet: ```kotlin theme={"system"} public suspend fun unlink(address: String): Result ``` ### Returns **Result\** `Result\` 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}") } ) ``` **Solana Wallet** Use the following method from the `siws` handler to unlink a Solana wallet: ```kotlin theme={"system"} public suspend fun unlink(address: String): Result ``` ### Returns **Result\** `Result\` 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}") } ) ``` **Passkey** Use the following method from the `passkey` handler to unlink a passkey: ```kotlin theme={"system"} public suspend fun unlink(credentialId: String): Result ``` ### Returns **Result\** `Result\` 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}") } ) ``` **Flutter** > [!NOTE] > > Users are only permitted to unlink **an account** so long as they have at least one more linked account. **Email** Use the following method from the `email` handler to unlink an email address: ```dart theme={"system"} Future> unlink(String email) ``` ### Parameters **email** `String` *required* The email address to unlink from the user. ### Returns **Result\** `Result\` 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}"); }, ); ``` **Phone** Use the following method from the `sms` handler to unlink a phone number: ```dart theme={"system"} Future> unlink(String phoneNumber) ``` ### Parameters **phoneNumber** `String` *required* The phone number to unlink from the user. ### Returns **Result\** `Result\` 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}"); }, ); ``` **Ethereum Wallet** Use the following method from the `siwe` handler to unlink an Ethereum wallet: ```dart theme={"system"} Future> unlink(String address) ``` ### Parameters **address** `String` *required* The Ethereum wallet address to unlink from the user. ### Returns **Result\** `Result\` 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}"); }, ); ``` **Solana Wallet** Use the following method from the `siws` handler to unlink a Solana wallet: ```dart theme={"system"} Future> unlink(String address) ``` ### Parameters **address** `String` *required* The Solana wallet address to unlink from the user. ### Returns **Result\** `Result\` 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}"); }, ); ``` **Passkey** Use the following method from the `passkey` handler to unlink a passkey: ```dart theme={"system"} Future> unlink(String credentialId) ``` ### Parameters **credentialId** `String` *required* The credential ID of the passkey to unlink. ### Returns **Result\** `Result\` 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}"); }, ); ``` **OAuth** 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 **provider** `OAuthProvider` *required* The OAuth provider for the account to unlink (e.g., `OAuthProvider.google`, `OAuthProvider.discord`, `OAuthProvider.twitter`, `OAuthProvider.apple`, `OAuthProvider.telegram`). **subject** `String` *required* The provider-specific subject identifier that uniquely identifies the user for the selected OAuth provider. ### Returns **Result\** `Result\` 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}"); }, ); ``` **REST API** 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 * `custom_auth` - Custom JWT 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` > [!NOTE] > > The API does not support unlinking `passkey` 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 Custom JWT 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": "custom_auth", "handle": "" }' ``` ```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. **Dashboard** 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 unlink button beside the account 4. Click `Yes, unlink` to confirm > [!NOTE] > > The unlink option won't appear if unlinking is not available for the account. ![Unlinking an account in the Privy Dashboard](https://mintcdn.com/privy-c2af3412/Ih_Fo3QYM486gzWq/images/users-unlink.png?fit=max\&auto=format\&n=Ih_Fo3QYM486gzWq\&q=85\&s=3504f9666c84c559bb47b134172aa8d3) # Updating user accounts Source: https://docs.privy.io/user-management/users/updating-accounts **React** **Email** 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 ( ); } ``` > [!NOTE] > > In the event that a user encounters an error through the flow, their existing account will be maintained. **Phone Number** 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 ( ); } ``` > [!NOTE] > > 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: **onSuccess** `(\{user: User, updateMethod: string, updatedAccount: LinkedAccountType\}) =\> void` Optional callback to run after a user successfully updates an account. **onError** `(error: string, details: \{updateMethod: string\}) =\> void` 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: **user** `User` The user object with the user's DID, linked accounts, and more. **updateMethod** `string` A string indicating the type of update flow just executed for the authenticated user. Possible values are `'email'` or `'sms'`. **updatedAccount** `LinkedAccountType` 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: **error** `string` The error code with more information about the error. **details.updateMethod** `string` A string indicating the type of update account flow just attempted for the authenticated user. **React Native** **Email** 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 **newEmailAddress** `string` 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 **newEmailAddress** `string` The new email address to set. **code** `string` The one time code received on the new email address. ### Returns **user** `Promise\` 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 ( ); } ``` **Phone Number** 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 **newPhoneNumber** `string` 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 **newPhoneNumber** `string` The new phone number to set. **code** `string` The one time code received on the new phone number. ### Returns **user** `Promise\` 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 ( ); } ``` **Swift** **Email** 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 **to email** `string` 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 **\_ code** `string` The one time code received on the new email address. **sentTo email** `string` The new email address to set. **Phone Number** 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 **to phoneNumber** `string` 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 **\_ code** `string` The one time code received on the new phone number. **sentTo phoneNumber** `string` The new phone number to set. **Android** **Email** 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 **email** `String` 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 **code** `String` The one time code received on the new email address. **email** `String` The new email address to set. **Phone Number** 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 **phoneNumber** `String` 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 **code** `String` The one time code received on the new phone number. **phoneNumber** `String` The new phone number to set. **REST API** **Twitter** 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 **subject** `string` *required* 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`. > [!NOTE] > > 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. > **[User created](/api-reference/webhooks/user/created)** > > Fires when a new user is created. > **[User deleted](/api-reference/webhooks/user/deleted)** > > Fires when a user is deleted from your app. > **[User authenticated](/api-reference/webhooks/user/authenticated)** > > Fires when a user logs in. > **[Account linked](/api-reference/webhooks/user/linked_account)** > > Fires when a user links a new login method. > **[Account unlinked](/api-reference/webhooks/user/unlinked_account)** > > Fires when a user unlinks a login method. > **[Account updated](/api-reference/webhooks/user/updated_account)** > > Fires when a user updates their email or phone number. > **[Account transferred](/api-reference/webhooks/user/transferred_account)** > > Fires when a user transfers their account. ## Wallet events These events fire for embedded wallet actions. > **[Wallet created](/api-reference/webhooks/user/wallet_created)** > > Fires when an embedded or smart wallet is created. > **[Private key exported](/api-reference/webhooks/wallet/private_key_export)** > > Fires when a user exports their private key. > **[Recovery set up](/api-reference/webhooks/wallet/recovery_setup)** > > Fires when a user sets up wallet recovery. > **[Wallet recovered](/api-reference/webhooks/wallet/recovered)** > > Fires when a user recovers their embedded wallet. ## MFA events > **[MFA enabled](/api-reference/webhooks/mfa/enabled)** > > Fires when a user enables multi-factor authentication. > **[MFA disabled](/api-reference/webhooks/mfa/disabled)** > > Fires when a user disables multi-factor authentication. ## Next steps > **[Webhooks overview](/api-reference/webhooks/overview)** > > Set up endpoints, verify payloads, and configure retries. > **[Wallet webhooks](/wallets/gas-and-asset-management/assets/overview)** > > 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. **React** Privy supports whitelabeling user management for linking and unlinking accounts. > **Linking a social account** > > 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(); > ``` > **Linking an additional OAuth account** > > 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: > > **provider** `string` *required* > > 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 ( > > ); > } > ``` > **Linking a wallet** > > 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. > > **Ethereum (SIWE)** > > 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 > ``` > > **Parameters** > > **address** `string` *required* > > EIP-55 checksum-encoded wallet address performing the signing. > > **chainId** `string` *required* > > 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 > ``` > > **Parameters** > > **signature** `string` *required* > > The EIP-191 signature corresponding to the message. > > **message** `string` *required* > > The EIP-4361 message returned by `generateSiweMessage`. > > **chainId** `string` *required* > > 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'`. > > **walletClientType** `string` > > Optional. The wallet client of the external wallet (e.g., `'metamask'`, `'coinbase_wallet'`). > Defaults to `null` if not specified. > > **connectorType** `string` > > 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); > } > }); > ``` > > **Solana (SIWS)** > > 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 > ``` > > **Parameters** > > **address** `string` *required* > > 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 }> > ``` > > **Parameters** > > **message** `string` *required* > > The SIWS message returned from `generateSiwsMessage`. > > **signature** `string` *required* > > 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')`. > > **walletClientType** `string` > > Optional. A string indicating the wallet client you'd like to associate with the wallet. Defaults > to `'privy'`. > > **connectorType** `string` > > 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); > } > }); > ``` > **Updating an email** > > 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; > ``` > > **Parameters** > > **newEmailAddress** `string` *required* > > 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>; > ``` > > **Parameters** > > **code** `string` *required* > > The one-time code received on the new email address. > > **Returns** > > **user** `User` > > 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 ( >
> type="email" > value={newEmailAddress} > onChange={(e) => setNewEmailAddress(e.target.value)} > placeholder="New email address" > /> > >
> ); > } > > return ( >
> value={code} > onChange={(e) => 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); > } > }); > ``` > > **Callback parameters** > > **onSuccess** `(\{user: User, updateMethod: string, updatedAccount: LinkedAccountType\}) =\> void` > > Optional callback that executes after a successful email update. Receives the updated user object, the update method (`'email'`), and the updated account. > > **onError** `(error: PrivyErrorCode, details: \{linkMethod: string\}) =\> void` > > Optional callback that executes if there is an error during the email update flow. > **Linking a custom JWT account** > > 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}> > ``` > > **jwt** `string` *required* > > 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 ( > > ); > } > ``` > > > [!NOTE] > > > > 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. > **Unlinking an account** > > 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. > **Unlinking an OAuth account** > > 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: > > **provider** `string` *required* > > 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'`). > > **subject** `string` *required* > > 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 ( > > ); > } > ``` **React Native** 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). **Android** 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). **Swift** 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 four 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 | | [Wallet automations](/wallets/automations/overview) | Execute wallet actions automatically when configured events occur. Privy monitors events and submits matching actions outside the app's lifecycle. | Event-driven wallet workflows | | [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 started with wallet actions](/wallets/actions/overview)** > **[Get started with RPCs](/wallets/using-wallets/rpc)** # 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 * USDG on Ethereum and Solana * OUSD on Ethereum, Base, Tempo, Solana, and Tempo Moderato (testnet) * 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. > [!TIP] > > 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. > [!NOTE] > > View the full [API reference](/api-reference/accounts/update) for getting an account's balance. ## Usage **REST API** 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 **account\_id** `string` *required* The unique ID of the account to retrieve balances for. ### Response The response will include the following fields: **total** `object` 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) **assets** `array` 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. > [!NOTE] > > View the full [API reference](/api-reference/accounts/create) for creating an account. ## Usage **REST API** To create an account via REST API, make a `POST` request to: ```bash theme={"system"} https://api.privy.io/v1/accounts ``` ### Body **display\_name** `string` An optional display name for the account. Provide exactly **one** of the following (`wallets_configuration` or `wallets_ids`) to specify the account's wallets: **wallets\_configuration** `object[]` *required* 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: **wallets\_configuration item fields** **chain\_type** `'ethereum' | 'solana'` *required* The chain type of the wallet to create. **custody** `object` The custody configuration for the wallet. If omitted, the wallet is non-custodial. **custody fields** **provider** `string` *required* The custody provider. Currently, `'bridge'` is the only supported value. **provider\_user\_id** `string` *required* The custody provider's unique ID for the KYC'ed entity associated with the wallet. **wallet\_ids** `string[]` *required* IDs of existing wallets to include in the account. Must contain between one and five wallet IDs. Mutually exclusive with `wallets_configuration`. ### Response **id** `string` Unique ID of the created account. **display\_name** `string | null` The display name of the account, or `null` if not set. **wallets** `object[]` 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. > [!NOTE] > > View the full API reference for [getting an account](/api-reference/accounts/get) or [listing all > accounts](/api-reference/accounts/list). ## Usage **REST API** 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 **account\_id** `string` *required* The unique ID of the account to retrieve. ### Response The response will include the following fields: **id** `string` The unique ID of the account. **display\_name** `string | null` An optional display name for the account. **wallets** `array` 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](https://mintcdn.com/privy-c2af3412/IQmti1WCL7AzN0e1/images/accounts-splash.png?fit=max\&auto=format\&n=IQmti1WCL7AzN0e1\&q=85\&s=d82a21705ed8d58425c452b1dc5de6e6) 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. > [!TIP] > > 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. **1. Create the account** [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. **2. Set the user as the owner of the account's wallets** 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. **3. Authorize actions from the account's wallets with the user's signature** 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. > [!TIP] > > [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. > [!NOTE] > > View the full [API reference](/api-reference/accounts/update) for updating an account. ## Usage **REST API** 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 **account\_id** `string` *required* The unique ID of the account to update. ### Body **display\_name** `string` An optional display name for the account. Provide exactly **one** of the following (`wallets_configuration` or `wallets_ids`) to add wallets to the account: **wallets\_configuration** `object[]` 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: **wallets\_configuration item fields** **chain\_type** `'ethereum' | 'solana'` *required* The chain type of the wallet to create. **custody** `object` The custody configuration for the wallet. If omitted, the wallet is non-custodial. **custody fields** **provider** `string` *required* The custody provider. Currently, `'bridge'` is the only supported value. **provider\_user\_id** `string` *required* The custody provider's unique ID for the KYC'ed entity associated with the wallet. **wallet\_ids** `string[]` *required* IDs of existing wallets to add to the account. Must contain between one and five wallet IDs. Mutually exclusive with `wallets_configuration`. ### Response **id** `string` The unique ID of the account. **display\_name** `string | null` The updated display name for the account, or `null` if not set. **wallets** `object[]` 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. > [!NOTE] > > View the full [API reference](/api-reference/wallets/earn/incentive-claim) for the claim endpoint. > [!TIP] > > 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 **REST API** 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 **chain** `string` *required* 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. > [!NOTE] > > Wallets with `owner_id` present must provide an [authorization > signature](/api-reference/authorization-signatures) as a request header for claim operations. ### Returns **id** `string` The wallet action ID. Use this to poll status with [get wallet action](/api-reference/wallets/actions/get). **wallet\_id** `string` The ID of the wallet claiming rewards. **type** `string` The action type. Always `"earn_incentive_claim"` for this endpoint. **status** `'pending' | 'succeeded' | 'rejected' | 'failed'` The current status of the claim action. **chain** `string` The chain name for the claim. **rewards** `array | null` List of rewards claimed, each with `token_address`, `token_symbol`, `token_decimals`, and `amount`. Populated after the preparation step fetches claimable rewards. **created\_at** `string` ISO 8601 timestamp of when the action was created. **steps** `array | undefined` 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" } ``` > [!TIP] > > 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 > **[Get vault position](/wallets/actions/earn/get-vault-position)** > > 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. > [!NOTE] > > 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. > [!NOTE] > > View the full [API reference](/api-reference/wallets/earn/fees-collect) for the collect fees > endpoint. > [!TIP] > > 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 **REST API** 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 **vault\_id** `string` *required* The ID of the vault to collect fees from. Must be an Aave vault. > [!NOTE] > > 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 **id** `string` The wallet action ID. Use this to poll status with [get wallet action](/api-reference/wallets/actions/get). **wallet\_id** `string` The ID of the admin wallet collecting the fees. **type** `string` The action type. Always `"earn_fee_collect"` for this endpoint. **status** `'pending' | 'succeeded' | 'rejected' | 'failed'` The current status of the fee collection action. **caip2** `string` Chain identifier in CAIP-2 format. **vault\_id** `string` The ID of the vault fees were collected from. **vault\_address** `string` The onchain address of the vault contract. **asset\_address** `string` The underlying asset's token address. **amount** `string | null` 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. **raw\_amount** `string | null` The collected amount in the smallest unit of the underlying asset. Populated after onchain confirmation. **created\_at** `string` 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" } ``` > [!TIP] > > 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 > **[Aave integration guide](/wallets/actions/earn/providers/aave)** > > 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. > [!NOTE] > > View the full [API reference](/api-reference/wallets/earn/deposit) for the deposit endpoint. > [!TIP] > > 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). > [!WARNING] > > 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 **Node SDK** 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. **REST API** 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 **vault\_id** `string` *required* 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. **amount** `string` Human-readable decimal amount to deposit (e.g. `"1.5"` for 1.5 OUSD). Exactly one of `amount` or `raw_amount` must be provided. **raw\_amount** `string` Amount to deposit in the token's smallest unit (e.g. `"1500000"` for 1.5 OUSD on Tempo with 6 decimals). Exactly one of `amount` or `raw_amount` must be provided. > [!NOTE] > > Wallets with `owner_id` present must provide an [authorization > signature](/api-reference/authorization-signatures) as a request header for deposit operations. ### Returns **id** `string` The wallet action ID. Use this to poll status with [get wallet action](/api-reference/wallets/actions/get). **wallet\_id** `string` The ID of the wallet that deposited funds. **type** `string` The action type. Always `"earn_deposit"` for this endpoint. **status** `'pending' | 'succeeded' | 'rejected' | 'failed'` The current status of the deposit action. **caip2** `string` CAIP-2 chain identifier for the deposit (e.g. `"eip155:8453"`). **vault\_id** `string` The ID of the vault receiving the deposit. **vault\_address** `string` The ERC-4626 vault contract address. **asset\_address** `string` The address of the underlying asset token. **raw\_amount** `string` Amount deposited in the token's smallest unit. **amount** `string | undefined` Human-readable decimal amount (e.g. `"1.5"`). Only present when the token is known in the asset registry. **asset** `string | undefined` Asset identifier (e.g. `"usdc"`). Only present when the token is known in the asset registry. **decimals** `number | undefined` Number of decimals for the underlying asset. Only present when the token is known in the asset registry. **share\_amount** `string | null` Vault shares received in base units. `null` until the action succeeds. **created\_at** `string` ISO 8601 timestamp of when the action was created. **steps** `array | undefined` 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" } ``` > [!TIP] > > 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. > [!NOTE] > > 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 assets](/wallets/actions/earn/withdraw)** > > 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. > [!NOTE] > > View the full [API reference](/api-reference/wallets/earn/get-vault-details) for the get vault > details endpoint. ## Usage **REST API** Make a `GET` request to: ```bash theme={"system"} https://api.privy.io/api/v1/earn/ethereum/vaults/{vault_id} ``` ### Parameters **vault\_id** `string` *required* The unique identifier for the vault. ### Returns **id** `string` The vault's unique identifier. **name** `string` Display name of the vault. **provider** `"morpho" | "aave" | "veda"` The protocol powering the vault. Some response fields vary by provider (see below). **vault\_address** `string` The onchain address of the vault contract. **asset** `object` The vault's underlying asset. **Asset fields** **asset.address** `string` The token contract address. **asset.symbol** `string` The token symbol (e.g. `"ousd"` for OUSD on Tempo). **asset.decimals** `number` The number of decimals for the token. **caip2** `string` Chain identifier in CAIP-2 format. **user\_apy** `number | null` Current APY in basis points (e.g. `500` = 5%). See the note below on when this is `null`. **app\_apy** `number | null` The application's share of the APY in basis points. See the note below on when this is `null`. **tvl\_usd** `number | null` Total value locked in the vault in USD. See the note below on when this is `null`. **available\_liquidity\_usd** `number | null` Liquidity available for withdrawal in USD. See the note below on when this is `null`. **admin\_wallet\_id** `string` The ID of the vault's admin wallet, which receives your app's share of fees. **admin\_wallet\_address** `string` The onchain address of the vault's admin wallet. The following fields are provider-specific: **total\_rewards\_apr** `number` **Morpho vaults only.** Additional token-incentive rewards APR, in basis points. **available\_fees** `string` **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. > [!NOTE] > > 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" } ``` > [!TIP] > > 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 > **[Webhooks](/wallets/actions/earn/webhooks)** > > 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. > [!NOTE] > > View the full [API reference](/api-reference/wallets/earn/get-position) for the get position > endpoint. ## Usage **REST API** 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 **vault\_id** `string` *required* The unique identifier for the vault. ### Returns **asset** `object` The vault's underlying asset, with `address`, `symbol`, and `decimals` fields. **total\_deposited** `string` Total amount deposited into the vault through Privy Earn, in the token's smallest unit. **total\_withdrawn** `string` Total amount withdrawn from the vault through Privy Earn, in the token's smallest unit. **assets\_in\_vault** `string` Current redeemable value of the wallet's vault shares, including accrued yield. **shares\_in\_vault** `string` 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 Privy Earn deposits | Use as the user's **cost basis** | | `total_withdrawn` | Sum of Privy Earn withdrawals | Subtract from `total_deposited` for net contributions | | `shares_in_vault` | Raw vault shares held | Rarely shown to users — use `assets_in_vault` instead | > [!NOTE] > > Privy computes `total_deposited` and `total_withdrawn` from the deposits and withdrawals made > through the Privy Earn [deposit](/wallets/actions/earn/deposit) and > [withdraw](/wallets/actions/earn/withdraw) endpoints. Deposits and withdrawals made directly > onchain are not counted. `assets_in_vault` and `shares_in_vault` are read live from the vault > contract, so they include all activity. If wallets interact with a vault outside Privy Earn, see > [Reading Earn vault totals onchain](/recipes/yield/earn-onchain-totals) to compute totals that > include all activity. ## 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 ``` > [!NOTE] > > 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 > **[Get vault details](/wallets/actions/earn/get-vault-details)** > > 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](https://mintcdn.com/privy-c2af3412/7cXr-D-UUALqEUgn/images/earn-tempo.png?fit=max\&auto=format\&n=7cXr-D-UUALqEUgn\&q=85\&s=e832d803c823d717a7e5dad26d67bbd3) ## 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. > [!NOTE] > > 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%, 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 > **[Setup](/wallets/actions/earn/setup)** > > Deploy a fee wrapper and configure your vault in the Privy Dashboard. > **[Starter template](https://github.com/privy-io/examples/tree/main/examples/privy-next-yield-demo)** > > A working Next.js app with end-to-end deposit and withdraw flows. > [!WARNING] > > 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` > [!WARNING] > > 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. > [!NOTE] > > 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"`. > [!WARNING] > > 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. **Node SDK** ```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. **REST API** 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 > **[Create a policy](/controls/policies/create-a-policy)** > > Learn more about creating and managing policy objects. > **[Conditional policies per signer](/recipes/wallets/conditional-signer-policies)** > > 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. > [!NOTE] > > Create a **USDC vault on Base** under **Wallet infrastructure > Earn** in the [Privy > Dashboard](https://dashboard.privy.io/apps?page=earn). Select **Aave USDC on Base**, set the > performance fee, and select **Create vault**. Privy creates an admin wallet by default; use > **Advanced settings** to provide an existing wallet. After creation, copy the vault ID to use with > the earn API. Contact [sales@privy.io](mailto:sales@privy.io) for other tokens or chains. ## Resources > **[Aave Earn vaults](https://aave.com/docs/vaults/simple-earn/overview)** > > How Aave's Earn vaults work on Aave v3 markets. > **[Earn](/wallets/actions/earn/overview)** > > 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. For example, with a 20% performance fee, users keep 80% of the vault's yield and your admin wallet receives 20%. A 0% fee leaves all yield with users. Existing vaults retain their configured fee split. * **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. > [!TIP] > > 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. ```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. > [!NOTE] > > The collected amount 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%. For example, 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. > [!WARNING] > > 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. > [!NOTE] > > 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. > [!WARNING] > > 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. > [!NOTE] > > 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 > **[Veda Docs](https://docs.veda.tech/)** > > Official documentation for Veda and the BoringVault architecture. > **[Earn](/wallets/actions/earn/overview)** > > 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. > [!TIP] > > 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: > [!WARNING] > > 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. > [!NOTE] > > 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`. > [!WARNING] > > 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. > [!NOTE] > > 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. > [!WARNING] > > 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 and Aave vaults, choose the fee percentage when you create the vault in the [Privy Dashboard](https://dashboard.privy.io). For other 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 | Your app keeps the full fee; the remaining yield accrues to depositors. | | [Veda](/wallets/actions/earn/providers/veda) | Set by agreement with Veda | Defined in your custom agreement with Veda. | > [!NOTE] > > Enabling other Aave or Veda vaults, 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. ## Next steps > **[Setup](/wallets/actions/earn/setup)** > > Deploy a fee wrapper and configure your vault in the Privy Dashboard. > **[Collect performance fees](/wallets/actions/earn/collect-fees)** > > Collect an Aave vault's accrued performance fees to its admin wallet. > [!WARNING] > > 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 create a Morpho or Aave vault. During setup, your app selects: 1. A Morpho vault or **Aave USDC on Base** to allocate assets into 2. The percentage of generated yield your app receives: 0–50% for Morpho or 0–100% for Aave 3. An admin wallet to claim fees and manage the vault configuration See the [Aave guide](/wallets/actions/earn/providers/aave) for self-serve setup and fee details. 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 OUSD Main (OUSD on Tempo)](https://app.morpho.org/tempo/vault/0x0D136e26B4D9e4836fc5cb202baeb49Cb6f5df48/sentora-ousd-main) * [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) > **Using an external wallet as an admin wallet (advanced)** > > 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. > **Updating the Morpho fee wrapper configuration (advanced)** > > 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. > [!WARNING] > > 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 " ``` > [!NOTE] > > 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 funds](/wallets/actions/earn/deposit)** > > Deposit assets from a wallet into a yield vault. > **[Automate deposits](/wallets/automations/earn-deposits)** > > Deposit a wallet's balance into a vault when matching funds arrive. > [!WARNING] > > 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). > [!NOTE] > > 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. **1. Created** The action is received and queued for processing. At this point, no transaction has been signed or broadcast. **2. Succeeded, rejected, or failed** 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 > **Update user balances after a deposit** > > 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. > **Notify users when a withdrawal completes** > > 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. > **Handle rejected actions gracefully** > > 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. > **Detect failed transactions** > > 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. > [!NOTE] > > View the full [API reference](/api-reference/wallets/earn/withdraw) for the withdraw endpoint. > [!TIP] > > 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. > [!TIP] > > 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 **Node SDK** 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. **REST API** 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 **vault\_id** `string` *required* The unique identifier for the vault. **amount** `string` Human-readable decimal amount to withdraw (e.g. `"1.5"` for 1.5 USDC). Exactly one of `amount` or `raw_amount` must be provided. **raw\_amount** `string` 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. > [!NOTE] > > Wallets with `owner_id` present must provide an [authorization > signature](/api-reference/authorization-signatures) as a request header for withdraw operations. ### Returns **id** `string` The wallet action ID. Use this to poll status with [get wallet action](/api-reference/wallets/actions/get). **wallet\_id** `string` The ID of the wallet receiving the withdrawn funds. **type** `string` The action type. Always `"earn_withdraw"` for this endpoint. **status** `'pending' | 'succeeded' | 'rejected' | 'failed'` The current status of the withdrawal action. **caip2** `string` CAIP-2 chain identifier for the withdrawal (e.g. `"eip155:8453"`). **vault\_id** `string` The ID of the vault being withdrawn from. **vault\_address** `string` The ERC-4626 vault contract address. **asset\_address** `string` The address of the underlying asset token. **raw\_amount** `string` Amount withdrawn in the token's smallest unit. **amount** `string | undefined` Human-readable decimal amount (e.g. `"1.5"`). Only present when the token is known in the asset registry. **asset** `string | undefined` Asset identifier (e.g. `"usdc"`). Only present when the token is known in the asset registry. **decimals** `number | undefined` Number of decimals for the underlying asset. Only present when the token is known in the asset registry. **share\_amount** `string | null` Vault shares redeemed in base units. `null` until the action succeeds. **created\_at** `string` ISO 8601 timestamp of when the action was created. **steps** `array | undefined` 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" } ``` > [!TIP] > > 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 **Node SDK** 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. **REST API** 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 **chain** `string` *required* 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. > [!NOTE] > > Wallets with `owner_id` present must provide an [authorization > signature](/api-reference/authorization-signatures) as a request header for claim operations. #### Returns **id** `string` The wallet action ID. Use this to poll status with [get wallet action](/api-reference/wallets/actions/get). **wallet\_id** `string` The ID of the wallet claiming rewards. **type** `string` The action type. Always `"earn_incentive_claim"` for this endpoint. **status** `'pending' | 'succeeded' | 'rejected' | 'failed'` The current status of the claim action. **chain** `string` The chain name for the claim. **rewards** `array | null` List of rewards claimed, each with `token_address`, `token_symbol`, `token_decimals`, and `amount`. Populated after the preparation step fetches claimable rewards. **created\_at** `string` ISO 8601 timestamp of when the action was created. **steps** `array | undefined` 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" } ``` > [!NOTE] > > 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 > **[Claim rewards](/wallets/actions/earn/claim)** > > 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 | > [!NOTE] > > 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. | | `tempo_zone_settlement` | Settlement of a Tempo Zone withdrawal on its parent chain, including any callback execution. | ## 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). | ### Tempo Zone settlement step statuses (`tempo_zone_settlement`) Tempo Zone settlement steps use the same status values as [external transaction steps](#external-transaction-step-statuses-external_transaction). The step stays `pending` while awaiting settlement and the originating zone's import of the parent block. It becomes `confirmed` when settlement succeeds, or `failed` if settlement fails or polling times out. These steps include two additional fields: * `caip2`: The Tempo parent chain's CAIP-2 identifier as a string, or `null` when unavailable. * `transaction_hash`: The parent-chain settlement hash, or `null` when unavailable. A failed settlement can also have a transaction hash. ## 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. Tempo Zone withdrawals, including zone Earn operations, typically have an `evm_transaction` step followed by a `tempo_zone_settlement` step. Confirming the zone transaction alone does not complete the action; settlement must also succeed. ## 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](https://mintcdn.com/privy-c2af3412/IQmti1WCL7AzN0e1/images/wallet-actions-splash.png?fit=max\&auto=format\&n=IQmti1WCL7AzN0e1\&q=85\&s=5e7411a83f0ea2951c35826859756825) ### Taking actions with a wallet To take an action (such as executing a signature, transfer, or other transaction), there are four 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 | | [Wallet automations](/wallets/automations/overview) | Execute wallet actions automatically when configured events occur. Privy monitors events and submits matching actions outside the app's lifecycle. | Event-driven wallet workflows | | [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 (OUSD on Tempo, 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:** Privy automatically sponsors gas when the app has [gas sponsorship](/wallets/gas-and-asset-management/gas/overview) enabled for the source chain, with funded prepaid credits or postpaid billing. **User pays:** For transfers using [supported tokens and chains](/wallets/gas-and-asset-management/gas/overview#supported-networks), gas can be paid from the wallet's stablecoin balance. Enable this mode in the [Dashboard](/wallets/gas-and-asset-management/gas/setup). This mode is currently only available for the [transfer API](/wallets/actions/transfer/overview). Without a configured gas payment mode, the wallet pays network fees using the chain's gas token. Paying gas through Privy's user-pays mode requires configuring that mode. ### Gas and fee sponsorship Gas sponsorship and swap provider fee sponsorship cover different costs. Configure them separately on the app's [**Fee sponsorship**](https://dashboard.privy.io/apps?page=fee-sponsorship) page in the Privy Dashboard. | Setting | What the app pays for | Guide | | - | - | - | | **Sponsor gas fees** | Network transaction fees on the selected chains. Gas sponsorship alone does not cover swap provider fees. | [Gas sponsorship setup](/wallets/gas-and-asset-management/gas/setup) | | **Sponsor swap provider fees** | Provider fees for supported transfers, swaps, and crypto deposits, rather than deducting those fees from the delivered amount. | [Fee sponsorship and fixed rates](/wallets/actions/transfer/fixed-rates) | Without swap provider fee sponsorship, provider fees are paid from the transferred or swapped amount. Provider fee sponsorship is configured per app and applies to all supported routes, rather than individual users or executions. Sponsorship does not expand supported assets, chains, or routes. Sponsored costs use the account's [shared prepaid credit balance or postpaid billing](/wallets/gas-and-asset-management/usage-billing/overview). Postpaid billing is available on the Enterprise plan. [Crypto deposit addresses](/wallets/funding/crypto-deposits/setup) require app-pays gas sponsorship on every source chain. [Transfers](/wallets/actions/transfer/overview#gas-and-fee-sponsorship) can use other gas payment modes. Swap provider fee sponsorship is optional for both. ### Fees and pricing Same-chain, same-asset transfers have no exchange fees, but network gas still applies. Cross-chain transfers and swaps can incur exchange fees. For current pricing and expected output, use the [crypto deposit quote API](/api-reference/crypto-deposits/quote), [transfer quote API](/api-reference/wallets/transfer/quote), or [swap quote API](/api-reference/wallets/swap/quote) for the requested route and amount. ### 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. > [!TIP] > > 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). > [!NOTE] > > 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. **Transfer** Transfer OUSD on Tempo with a reference ID: ```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": "ousd", "chain": "tempo" }, "destination": { "address": "" } }' ``` **Swap** ```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" } }' ``` **Earn deposit** ```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 **React** Use the `getAction` method from the `useWalletActions` hook to fetch the current status of a wallet action. ```tsx theme={"system"} import {useWalletActions} from '@privy-io/react-auth'; const {getAction} = useWalletActions(); const action = await getAction('insert-wallet-id', 'insert-action-id'); ``` **React Native** Use the `getAction` method from the `useWalletActions` hook to fetch the current status of a wallet action. ```tsx theme={"system"} import {useWalletActions} from '@privy-io/expo'; const {getAction} = useWalletActions(); const action = await getAction('insert-wallet-id', 'insert-action-id'); ``` **REST API** 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 **id** `string` The unique identifier for the wallet action. **wallet\_id** `string` The ID of the wallet that initiated the action. **type** `'swap' | 'transfer'` The type of the wallet action. **status** `'pending' | 'succeeded' | 'rejected' | 'failed'` 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. | **steps** `array` 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: " ``` > [!NOTE] > > The `getAction` method on the React and React Native `useWalletActions` hook does not yet support > the `include=steps` query parameter. Use the REST API directly if you need step-level detail from > a client-side app. ## Listing all wallet actions **React** Use the `listActions` method from the `useWalletActions` hook to list wallet actions for a wallet. ```tsx theme={"system"} import {useWalletActions} from '@privy-io/react-auth'; const {listActions} = useWalletActions(); const {data, next_cursor} = await listActions('insert-wallet-id', {limit: 20}); ``` **React Native** Use the `listActions` method from the `useWalletActions` hook to list wallet actions for a wallet. ```tsx theme={"system"} import {useWalletActions} from '@privy-io/expo'; const {listActions} = useWalletActions(); const {data, next_cursor} = await listActions('insert-wallet-id', {limit: 20}); ``` **REST API** 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. > [!NOTE] > > Custom fees for swaps are in early access and only available for Enterprise customers. 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. > [!NOTE] > > 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 > **[Monetization](/financial-flows/monetization)** > > 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 | | - | - | - | | 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. | | 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. | ## Execution errors A swap that passes validation is accepted as a [wallet action](/wallets/actions/overview) and executed asynchronously. The following conditions are not returned on the request — they surface as a `rejected` wallet action, before anything is submitted on-chain. Poll the wallet action or subscribe to the [`swap.rejected` webhook](/api-reference/webhooks/wallet-action/swap/rejected) to detect them. | Error | Description | | - | - | | Gas sponsorship not enabled | The swap was prepared as sponsored, but [gas sponsorship](/wallets/actions/swap/setup#1-configure-fee-sponsorship) was no longer enabled for the app when the swap reached execution. | | Insufficient gas credits | Gas sponsorship is enabled but the app's gas credit balance cannot cover the network fee. Fund the credit balance or add a payment method in the Dashboard. | # 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. > [!NOTE] > > 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. **NodeJS** 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. **Python** Use `client.wallets.swap.execute` to execute a swap. ```python theme={"system"} response = client.wallets.swap.execute( "insert-wallet-id", source={ "caip2": "eip155:8453", "asset_address": "native", }, destination={"asset_address": "0x"}, base_amount="1000000000000000000", amount_type="exact_input", privy_authorization_signature="", ) ``` The method returns a `SwapActionResponse` containing the pending wallet action. **REST API** ### Body **source** `object` *required* The source token and chain. **properties** **source.caip2** `string` *required* CAIP-2 identifier for the source chain (e.g., `eip155:8453` for Base). See [supported chains](/wallets/actions/swap/overview#supported-chains). **source.asset\_address** `string` *required* Token address to sell, or `"native"` for the chain's native token. **destination** `object` *required* The destination token and chain. **properties** **destination.asset\_address** `string` *required* Token address to receive, or `"native"` for the chain's native token. **destination.caip2** `string` 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). **destination.destination\_address** `string` 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. **base\_amount** `string` *required* Amount in base units (e.g., wei for ETH, lamports for SOL, or the token's smallest unit). **amount\_type** `'exact_input' | 'exact_output'` Whether `base_amount` refers to the input or output token. Defaults to `exact_input`. **slippage\_bps** `number` Maximum slippage tolerance in basis points (e.g., `50` for 0.5%). If omitted, auto-slippage is used. **fee\_configuration** `object` Optional developer fee configuration. Only applies to cross-chain swaps. **properties** **fee\_configuration.type** `'total_fee_bps'` *required* The fee model to apply. Currently only `total_fee_bps` is supported. **fee\_configuration.value** `number` *required* Total fee cap in basis points (0–10000). Relayer and developer fees must fit within this cap. For example, `80` represents 0.8%. > [!WARNING] > > 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 **id** `string` The ID of the wallet action. **status** `'pending' | 'succeeded' | 'rejected' | 'failed'` The current status of the wallet action. **wallet\_id** `string` The ID of the wallet involved in the swap. **caip2** `string` CAIP-2 identifier for the source chain. **input\_token** `string` Token address being sold. **output\_token** `string` Token address being bought. **input\_amount** `string` Amount of input token in base units. Populated after confirmation. **output\_amount** `string` Amount of output token received in base units. Populated after confirmation. **destination\_caip2** `string` CAIP-2 identifier for the destination chain. Present for cross-chain swaps only. **destination\_address** `string` The address receiving output tokens on the destination chain. Present when `destination.destination_address` was specified. Cross-chain swaps only. **estimated\_fees** `FeeLineItem[]` 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** `object` Estimated gas cost at the time of execution. Contains `base_amount`, `amount`, and `gas_asset` fields. Present for cross-chain swaps only. **fees** `FeeLineItem[]` Actual fees paid. Populated after the swap is confirmed. Same structure as `estimated_fees`. Present for cross-chain swaps only. **gas** `object` Actual gas paid. Populated after the swap is confirmed. Same structure as `estimated_gas`. Present for cross-chain swaps only. ### Examples **Same-chain** ```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 } ``` **Cross-chain** ```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. > [!NOTE] > > 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 ``` **NodeJS** 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. **Python** Use `client.wallets.swap.quote` to preview a swap. ```python theme={"system"} quote = client.wallets.swap.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 estimated output amounts and fees. **REST API** ### Body **source** `object` *required* The source token and chain. **properties** **source.caip2** `string` *required* CAIP-2 identifier for the source chain (e.g., `eip155:8453` for Base). See [supported chains](/wallets/actions/swap/overview#supported-chains). **source.asset\_address** `string` *required* Token address to sell, or `"native"` for the chain's native token. **destination** `object` *required* The destination token and chain. **properties** **destination.asset\_address** `string` *required* Token address to receive, or `"native"` for the chain's native token. **destination.caip2** `string` 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). **destination.destination\_address** `string` 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. **base\_amount** `string` *required* Amount in base units (e.g., wei for ETH, lamports for SOL, or the token's smallest unit). **amount\_type** `'exact_input' | 'exact_output'` Whether `base_amount` refers to the input or output token. Defaults to `exact_input`. **slippage\_bps** `number` Maximum slippage tolerance in basis points (e.g., `50` for 0.5%). If omitted, auto-slippage is used. **fee\_configuration** `object` Optional developer fee configuration. Only applies to cross-chain swaps. **properties** **fee\_configuration.type** `'total_fee_bps'` *required* The fee model to apply. Currently only `total_fee_bps` is supported. **fee\_configuration.value** `number` *required* Total fee cap in basis points (0–10000). Relayer and developer fees must fit within this cap. For example, `80` represents 0.8%. > [!TIP] > > 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 **caip2** `string` CAIP-2 identifier for the source chain. **input\_token** `string` Token address being sold. **output\_token** `string` Token address being bought. **input\_amount** `string` Amount of input token in base units. **est\_output\_amount** `string` Estimated amount of output token in base units, after fees. **minimum\_output\_amount** `string` Minimum output amount accounting for slippage, in base units. Use this to verify the swap terms before executing. **gas\_estimate** `string` Estimated gas cost in base units of the native token. For cross-chain swaps, see `estimated_gas` for a structured breakdown. **destination\_caip2** `string` CAIP-2 identifier for the destination chain. Present for cross-chain swaps only. **estimated\_fees** `FeeLineItem[]` 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. **estimated\_gas** `object` Structured gas estimate. Present for cross-chain swaps only. **properties** **base\_amount** `string` Gas cost in the native token's smallest unit. **amount** `string` Gas cost as a human-readable decimal string. **gas\_asset** `string` Symbol of the gas token (e.g., `"ETH"`). **expires\_at** `number` 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 **Same-chain** ```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" } ``` **Cross-chain** ```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](https://mintcdn.com/privy-c2af3412/IQmti1WCL7AzN0e1/images/swaps.png?fit=max\&auto=format\&n=IQmti1WCL7AzN0e1\&q=85\&s=c5b5388a96d03034999554b3e9f6da96) > [!NOTE] > > 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 **1. Setup** Swaps are available by default. Optionally configure [swap routing](/wallets/actions/swap/setup) and [gas sponsorship](/wallets/gas-and-asset-management/gas/setup). **2. Get a quote** 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. **3. Execute the swap** Call the [swap endpoint](/wallets/actions/swap/execute) with the same parameters. The response is a wallet action that can be polled for confirmation. > [!TIP] > > To convert incoming funds automatically, use [crypto > deposits](/wallets/funding/crypto-deposits/overview). Privy swaps or bridges each deposit to a > target asset without an API call per deposit. ## Supported chains Swaps are available on the following chains. > **EVM mainnet 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 | > [!NOTE] > > Tempo does not have a native token. Specify token contract addresses for Tempo swaps. > **Solana** > > | Chain | CAIP-2 identifier | Native token | > | - | - | - | > | Solana mainnet | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | SOL | > **EVM testnet chains** > > | 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. > [!WARNING] > > 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. If [gas sponsorship](/wallets/actions/swap/setup#1-configure-fee-sponsorship) is enabled for the swap's source chain, these fees are charged to your app's gas credits. Otherwise the wallet pays them from its native token balance. Either way, 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. | > [!NOTE] > > 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 is submitted on-chain and then reverts due to slippage, the network fee for that attempt is still owed: it is charged to your app's gas credits if the swap was sponsored, or to the wallet's native token balance if it was not. Swaps rejected before submission, and transactions that are dropped (EVM) or expire before landing (Solana), incur no network fee. 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 > **[Setup](/wallets/actions/swap/setup)** > > Configure swap routing and fee sponsorship in the Privy Dashboard. > **[Get a quote](/wallets/actions/swap/get-quote)** > > Fetch a price quote before executing a swap. > **[Execute a swap](/wallets/actions/swap/execute)** > > Execute a token swap from a wallet. > **[Errors](/wallets/actions/swap/errors)** > > 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 Swaps are available by default. To customize routing behavior, open [**Funding**](https://dashboard.privy.io/apps?page=funding) in the Privy Dashboard and select **Settings**. ## 1. Configure fee sponsorship Open [**Fee sponsorship**](https://dashboard.privy.io/apps?page=fee-sponsorship) in the Dashboard and configure the following options: * **Gas sponsorship (optional):** Enable **Sponsor gas fees** to cover network fees for swaps from your app's [gas credits](/wallets/gas-and-asset-management/gas/setup). Sponsorship applies per chain, so select every chain your app swaps on under **Supported chains**. Without sponsorship, the swapping wallet pays its own network fees and must hold a balance of the chain's native token. * **Swap provider fee sponsorship (optional):** Enable **Sponsor swap provider fees** to cover swap provider fees for cross-chain swaps on all supported routes. This includes fees charged by decentralized exchanges (DEXes). > [!NOTE] > > Paying network fees in ERC-20 tokens ([user pays > mode](/wallets/gas-and-asset-management/gas/setup)) is not supported for swaps. Apps configured > for user pays mode can still execute swaps, but the wallet pays network fees in the chain's native > token. > [!NOTE] > > Swaps on [supported fixed-rate routes](/wallets/actions/transfer/supported-sponsored-routes) > execute at a fixed rate without slippage. Enabling both gas sponsorship and swap provider fee > sponsorship allows 1:1 swaps between supported stablecoins. Swap provider fee sponsorship does not > cover slippage on other routes. Fee sponsorship supports prepaid and postpaid billing. For prepaid accounts, ensure the credit balance is funded. Follow the [fee sponsorship setup](/wallets/actions/transfer/fixed-rates#setup) for details. > [!NOTE] > > On EVM chains other than Tempo, an unsponsored swap that also needs a token approval is submitted > as a [batch transaction](/recipes/batch-transactions). If the wallet has not been upgraded on that > chain yet, Privy upgrades it via EIP-7702 as part of the swap. ## 2. 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. ## 3. 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. | > [!NOTE] > > **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. > [!NOTE] > > 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. **NodeJS** ```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', }, }); ``` **REST API** ```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. > [!WARNING] > > Exact output bridging is not yet supported when Solana is the source chain. ## Limitations > **Supported bridging and conversion routes** > > 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. > **Custom tokens not supported for bridging or stablecoin conversions** > > Cross-chain transfers are only supported for well-known assets (`usdc`, `usdt`, `usdg`, `eth`, > etc.). Custom tokens specified via `asset_address` cannot be bridged. > **Solana not yet supported as source for exact\_output** > > 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 support** > > 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. > **Source and destination must be on the same network** > > 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. > [!NOTE] > > 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. > [!NOTE] > > 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 > **[Monetization](/financial-flows/monetization)** > > 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 provider fees 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. ## How it works The app pays the provider fees so they do not reduce the amount delivered to the recipient. Prepaid accounts pay from their credit balance. Postpaid accounts pay through their monthly invoice. The prepaid balance must cover the sponsored fees before the transfer can proceed. Add credits if the balance is insufficient. View [usage and billing](/wallets/gas-and-asset-management/usage-billing/overview) for credit management and [usage webhooks](/wallets/gas-and-asset-management/usage-billing/usage-webhooks) for charge events. ## Setup Fee sponsorship supports prepaid and postpaid billing. **1. Set up billing** **Prepaid** Open **Fee sponsorship** in the [Privy Dashboard](https://dashboard.privy.io/apps?page=fee-sponsorship) for the app. Select **Add credits** and complete the purchase. Credits are a balance purchased in advance. Privy deducts [prepaid credits](/wallets/gas-and-asset-management/usage-billing/overview#purchasing-credits-prepaid) whenever it executes a sponsored action for the app, such as sponsoring gas or swap provider fees. **Postpaid** Postpaid billing is available for Enterprise customers. Contact the Privy account manager or [sales@privy.io](mailto:sales@privy.io) to enable [postpaid billing](/wallets/gas-and-asset-management/usage-billing/overview#choosing-your-plan) for the account. Sponsored fees appear on the monthly invoice, so no prepaid credits are required. **2. Enable gas sponsorship** Open the app's [**Fee sponsorship**](https://dashboard.privy.io/apps?page=fee-sponsorship) page and turn on **Sponsor gas fees** to cover network gas. Under **Supported chains**, select the networks where the app will sponsor gas. See the [gas sponsorship setup guide](/wallets/gas-and-asset-management/gas/setup) for details. **3. Enable swap provider fee sponsorship** Open the app's [**Fee sponsorship**](https://dashboard.privy.io/apps?page=fee-sponsorship) page and turn on **Sponsor swap provider fees**. Sponsorship is supported for all routes. The setting applies automatically to all supported routes for [transfers](/wallets/actions/transfer/bridging), [swaps](/wallets/actions/swap/overview), and [crypto deposit addresses](/wallets/funding/crypto-deposits/overview). No request parameter or integration change is required. ## Confirming the delivered amount Use the [quote endpoint](/wallets/actions/transfer/quote) and read `estimated_output_amount` to review the expected output. On non-fixed rate routes, the final amount received can differ due to slippage. **NodeJS** ```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', }, }); ``` > [!TIP] > > 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 > **Applies to routed transfers only** > > Fixed rates cover provider fees for transfers between chains or between assets. Same-chain, > same-asset transfers have no provider fee to cover. [Gas > sponsorship](/wallets/gas-and-asset-management/gas/overview) covers network gas separately. > **Configured per app, not per transfer** > > Fixed rates are a property of the app, so an app cannot turn them on for some transfers and not > others. > **Route availability still applies** > > Fixed rates change how provider fees 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 > **[Usage and billing](/wallets/gas-and-asset-management/usage-billing/overview)** > > Manage prepaid credits or review postpaid billing. > **[Usage webhooks](/wallets/gas-and-asset-management/usage-billing/usage-webhooks)** > > 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](https://mintcdn.com/privy-c2af3412/IQmti1WCL7AzN0e1/images/transfer-splash.png?fit=max\&auto=format\&n=IQmti1WCL7AzN0e1\&q=85\&s=6fadf7e1aaeb6262de36716c61425530) ## 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. | | [Fee sponsorship and fixed rates](/wallets/actions/transfer/fixed-rates) | Covers provider fees for supported transfers; fixed-rate stablecoin routes deliver the exact input amount. | | [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 For same-chain, same-asset transfers, the transfer API supports native tokens (ETH, SOL, POL, TRX) and ERC-20, SPL, and TRC-20 tokens on the supported chains below. Cross-chain bridging and stablecoin conversions require supported well-known assets and available provider liquidity. A token's same-chain transfer support does not guarantee a cross-chain route. Use the [transfer quote API](/api-reference/wallets/transfer/quote) to check the requested asset and chain pair. When making a transfer request to the Privy API, specify a well-known asset (e.g. OUSD, USDC, USDT, USDB, EURC) or a token contract address. ### Well-known assets Privy supports the following well-known assets natively on the chains where they are available. | Asset | API value | | - | - | | OUSD | `ousd` | | 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` | > [!TIP] > > 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 same-chain transfers of custom ERC-20 and 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_testnet` | | Robinhood Testnet | `eip155:46630` | `robinhood_testnet` | | Solana Devnet | `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` | `solana_devnet` | | Tron | `tron:mainnet` | `tron` | | Tron Nile | `tron:nile` | `tron_nile` | > [!NOTE] > > 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. > [!NOTE] > > 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, OUSD) 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 are subject to fees, slippage, and current liquidity conditions. > [!NOTE] > > 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). ## Pricing and quotes Call [`POST /v1/wallets/{wallet_id}/transfer/quote`](/api-reference/wallets/transfer/quote) before a cross-chain or cross-asset transfer. The response includes `estimated_input_amount`, `estimated_output_amount`, `estimated_fees`, and `expires_at` for the requested route and amount. See [quote transfer fees](/wallets/actions/transfer/quote) for examples. While quotes provide current pricing estimates, fees and liquidity can change before execution. Fetch a fresh quote before each transfer. Same-chain, same-asset transfers have no exchange fees and do not use this quote endpoint, but network gas still applies. ## Gas and fee sponsorship Gas sponsorship and swap provider fee sponsorship cover different costs. Configure them separately on the app's [**Fee sponsorship**](https://dashboard.privy.io/apps?page=fee-sponsorship) page in the Privy Dashboard. | Setting | What the app pays for | Guide | | - | - | - | | **Sponsor gas fees** | Network transaction fees on the selected chains. Gas sponsorship alone does not cover swap provider fees. | [Gas sponsorship setup](/wallets/gas-and-asset-management/gas/setup) | | **Sponsor swap provider fees** | Provider fees for supported transfers, swaps, and crypto deposits, rather than deducting those fees from the delivered amount. | [Fee sponsorship and fixed rates](/wallets/actions/transfer/fixed-rates) | Without swap provider fee sponsorship, provider fees are paid from the transferred or swapped amount. Provider fee sponsorship is configured per app and applies to all supported routes, rather than individual users or executions. Sponsorship does not expand supported assets, chains, or routes. Sponsored costs use the account's [shared prepaid credit balance or postpaid billing](/wallets/gas-and-asset-management/usage-billing/overview). Postpaid billing is available on the Enterprise plan. Transfers use the app's [gas payment configuration](/wallets/actions/overview#gas-management). App-pays gas sponsorship is optional for transfers. Without it, the wallet must fund network gas, either with its native token balance or through [user-pays mode](/wallets/gas-and-asset-management/gas/overview) on supported token and chain combinations. Transfers between different input and output assets can optionally use [swap provider fee sponsorship](/wallets/actions/transfer/fixed-rates). When enabled, the app covers the fees associated with converting between the assets. Use the [transfer quote API](/wallets/actions/transfer/quote) to estimate the amount the recipient will receive. # 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` > [!WARNING] > > 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. > [!NOTE] > > 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 `"ousd"` or `"eth"`. Use `source.asset_address` if your app sends custom asset contract addresses. > [!WARNING] > > 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 OUSD 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. **Node SDK** ```typescript theme={"system"} const policy = await privy.policies().create({ name: 'Approved transfer policy', version: '1.0', chain_type: 'ethereum', rules: [ { name: 'Allow OUSD 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: 'ousd', }, { 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. **REST API** 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 OUSD 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": "ousd" }, { "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 > **[Create a policy](/controls/policies/create-a-policy)** > > Learn more about creating and managing policy objects. > **[Conditional policies per signer](/recipes/wallets/conditional-signer-policies)** > > 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. > [!NOTE] > > 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 Cross-chain transfers can include the following fee components: * **Relayer fee** — paid to the bridge provider for processing the transfer. This varies with network conditions and liquidity. * **Developer fee** — fees to the app developer. Provider fees are denominated in USD. Unsponsored provider fees are deducted from the transfer amount. The `estimated_output_amount` reflects the route's fees and the app's [swap provider fee sponsorship](/wallets/actions/transfer/fixed-rates) configuration. [Gas sponsorship](/wallets/gas-and-asset-management/gas/overview) covers network transaction fees separately. Enabling it alone does not cover swap provider fees. See [gas and fee sponsorship](/wallets/actions/transfer/overview#gas-and-fee-sponsorship) for payment options. ## 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 **source** `object` *required* The source asset, amount, and chain for the transfer. **properties** **source.asset** `string` *required* The asset to transfer. Must be one of `ousd`, `usdc`, `usdc_e`, `usdt`, `usdt0`, `usdb`, `eth`, `sol`, `pol`, or `eurc`. Custom token addresses (`asset_address`) are not supported for cross-chain transfers. **source.chain** `string` *required* The chain to transfer from. Must be one of `ethereum`, `base`, `arbitrum`, `polygon`, `tempo`, or `solana`. **destination** `object` *required* The destination for the transfer. **properties** **destination.address** `string` *required* The recipient wallet address. **destination.chain** `string` The destination chain. Required for cross-chain transfers. Must differ from `source.chain`. **destination.asset** `string` 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** `string` 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. **amount\_type** `'exact_input' | 'exact_output'` 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. **fee\_configuration** `object` Optional fee configuration to apply to the transfer. **properties** **fee\_configuration.type** `'total_fee_bps'` *required* The fee configuration type. Currently only `total_fee_bps` is supported. **fee\_configuration.value** `number` *required* Total fee cap in basis points (0–10000). For example, `100` represents 1%. ### Response **amount\_type** `'exact_input' | 'exact_output'` The amount type from the request, echoed back for clarity. **source** `object` The source asset, amount, and chain from the request. **destination** `object` The destination address, asset, and chain from the request. **estimated\_input\_amount** `string` 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. **estimated\_output\_amount** `string` 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. **estimated\_fees** `FeeLineItem[]` An array of fee line items that make up the total transfer cost. Each item has a `type` and an `amount` in USD. **Fee line item types** | Type | Description | | - | - | | `relayer` | Fee charged by the bridge/relayer provider | | `developer` | Fee allocated to the app developer | **expires\_at** `number` 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 > [!TIP] > > 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. > **Example (exact\_input)** > > **Node SDK** > > ```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, > }, > }); > ``` > > **REST API** > > ```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 > } > ``` > **Example (exact\_output)** > > To quote a transfer where the recipient receives exactly 100 USDC on Arbitrum, regardless of fees: > > **Node SDK** > > ```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") > ``` > > **REST API** > > ```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 > **Same-chain transfers not supported** > > 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. > **Custom tokens not supported** > > 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 **React** Use the `transfer` method from the `useTransfer` hook to transfer tokens from a wallet. ```tsx theme={"system"} import {useTransfer} from '@privy-io/react-auth'; const {transfer} = useTransfer(); ``` **React Native** Use the `transfer` method from the `useTransfer` hook to transfer tokens from a wallet. ```tsx theme={"system"} import {useTransfer} from '@privy-io/expo'; const {transfer} = useTransfer(); ``` **NodeJS** 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', { // ... }); ``` **REST API** 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. **source** `object` *required* The source asset, amount, and chain for the transfer. Specify either `asset` (for named assets) or `asset_address` (for custom assets), not both. **properties** **source.asset** `string` 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`. **source.asset\_address** `string` 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`. **source.amount** `string` *required* The amount of tokens to transfer, as a decimal string in standard units (e.g. `"10.0"` for 10 OUSD). The API handles decimal precision based on the asset — most stablecoins like OUSD 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. **source.chain** `string` *required* The chain to transfer from. Must be one of the listed [supported chains](/wallets/actions/transfer/overview#supported-chains). **destination** `object` *required* The destination for the transfer. **properties** **destination.address** `string` *required* The recipient wallet address. Use a hex address for EVM chains and a base58 address for Solana. **destination.asset** `string` 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). **destination.chain** `string` 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** `string` Amount as a decimal string in standard units (e.g. `"10.0"` for 10 OUSD). For `exact_input`, the amount to send. For `exact_output`, the exact amount to receive. Takes precedence over `source.amount` when both are provided. **amount\_type** `'exact_input' | 'exact_output'` 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. **slippage\_bps** `integer` 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. **fee\_configuration** `object` Optional fee configuration for cross-chain transfers. **properties** **fee\_configuration.type** `'total_fee_bps'` *required* The fee model to apply. Currently only `total_fee_bps` is supported. **fee\_configuration.value** `number` *required* 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. > [!NOTE] > > 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. **id** `string` The unique identifier for the wallet action. **status** `'pending' | 'succeeded' | 'rejected' | 'failed'` The current status of the action. **wallet\_id** `string` The ID of the wallet initiating the transfer. **created\_at** `string` The ISO 8601 timestamp for when the action was created. **type** `'transfer'` The type of action. For transfers, this is always `transfer`. **amount\_type** `'exact_input' | 'exact_output'` The amount type used for this transfer. Omitted when the default `exact_input` was used. **source\_asset** `string` The named asset being transferred (e.g. `"usdc"`, `"eth"`). Present when the transfer was initiated with a named `asset`; omitted for custom-token transfers. **source\_asset\_address** `string` The token contract address (EVM) or mint address (Solana) of the transferred asset. Present when the transfer was initiated with `asset_address`. **source\_asset\_decimals** `integer` The number of decimals for the transferred token. Present when the transfer was initiated with `asset_address` and the decimals were resolved on-chain. **source\_amount** `string` 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. **source\_chain** `string` The chain the transfer is sent from (e.g. `"tempo"`, `"ethereum"`). **destination\_address** `string` The recipient wallet address. **destination\_asset** `string` The destination asset for cross-asset transfers. Omitted for same-asset transfers. **destination\_chain** `string` The destination chain for cross-chain transfers. Omitted for same-chain transfers. **destination\_amount** `string` The amount received on the destination chain. Populated immediately for `exact_output` transfers, or after fill confirmation for `exact_input` cross-chain transfers. **fees** `FeeLineItem[]` 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. **failure\_reason** `object` 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 > **Exact input example** > > Transfer OUSD on Tempo using the `ousd` asset alias: > > **React** > > Use the `transfer` method from the `useTransfer` hook to transfer tokens from a wallet. > > ```tsx theme={"system"} > import {useTransfer} from '@privy-io/react-auth'; > > const {transfer} = useTransfer(); > > const response = await transfer('insert-wallet-id', { > amount: '10.0', > source: { > asset: 'ousd', > chain: 'tempo' > }, > destination: { > address: '0xRecipientAddress' > }, > fee_configuration: { > type: 'total_fee_bps', > value: 80 > } > }); > ``` > > The hook signs the transfer request with the user's embedded wallet and returns a `TransferActionResponse` with the pending wallet action. See [wallet action lifecycle](/wallets/actions/overview#wallet-action-lifecycle) to track the status. > > **React Native** > > Use the `transfer` method from the `useTransfer` hook to transfer tokens from a wallet. > > ```tsx theme={"system"} > import {useTransfer} from '@privy-io/expo'; > > const {transfer} = useTransfer(); > > const response = await transfer('insert-wallet-id', { > amount: '10.0', > source: { > asset: 'ousd', > chain: 'tempo' > }, > destination: { > address: '0xRecipientAddress' > }, > fee_configuration: { > type: 'total_fee_bps', > value: 80 > } > }); > ``` > > The hook signs the transfer request with the user's embedded wallet and returns a `TransferActionResponse` with the pending wallet action. See [wallet action lifecycle](/wallets/actions/overview#wallet-action-lifecycle) to track the status. > > **NodeJS** > > 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: 'ousd', > 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. > > **REST API** > > ```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": "ousd", > "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": "ousd", > "source_amount": "10.0", > "source_chain": "tempo", > "destination_address": "0xRecipientAddress" > } > ``` > **Exact output example** > > To send exactly 100 USDC to a recipient on Arbitrum from a Tempo wallet: > > **React** > > ```tsx theme={"system"} > import {useTransfer} from '@privy-io/react-auth'; > > const {transfer} = useTransfer(); > > const response = await transfer('insert-wallet-id', { > amount: '100.0', > amount_type: 'exact_output', > source: { > asset: 'usdc', > chain: 'tempo' > }, > destination: { > address: '0xRecipientAddress', > chain: 'arbitrum' > } > }); > > ``` > > **React Native** > > ```tsx theme={"system"} > import {useTransfer} from '@privy-io/expo'; > > const {transfer} = useTransfer(); > > const response = await transfer('insert-wallet-id', { > amount: '100.0', > amount_type: 'exact_output', > source: { > asset: 'usdc', > chain: 'tempo' > }, > destination: { > address: '0xRecipientAddress', > chain: 'arbitrum' > } > }); > ``` > > **NodeJS** > > ```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: [''] > } > }); > ``` > > **REST API** > > ```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" > } > ``` > > > [!NOTE] > > > > 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). > [!NOTE] > > 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. > [!NOTE] > > 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 > [!TIP] > > 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 | # Configure triggers and actions Source: https://docs.privy.io/wallets/automations/configuration Configure the triggers that activate wallet automations and the wallet actions they create. An automation's `config` pairs one trigger with one action. Triggers define when an automation runs; actions define what it does. New actions can reuse existing triggers whenever the combination is compatible. The config is reusable across wallets. Values that differ by wallet belong to the attachment instead. ## Triggers ### Deposit The `deposit` trigger activates when Privy detects a deposit to a wallet with an enabled attachment and the deposited asset matches the configured filter. A deposit that does not match an enabled attachment does not create an execution. If multiple attachments match, Privy selects the [oldest matching attachment](/wallets/automations/manage#matching-precedence). Privy evaluates recipient-side asset transfers. Outgoing activity does not activate a deposit trigger. Deposit matching has no minimum amount, and the transferred amount does not determine the action amount. Attaching an automation to a funded wallet does not process its existing balance. A new matching deposit or a [reindex request](/wallets/automations/lifecycle#recover-a-missed-or-failed-deposit) must start the execution. #### Supported chains Wallet automations can detect deposits on the following mainnet chains: | Chain | CAIP-2 identifier | | - | - | | Tempo | `eip155:4217` | | Ethereum | `eip155:1` | | Base | `eip155:8453` | | Polygon | `eip155:137` | | Arbitrum | `eip155:42161` | | Optimism | `eip155:10` | | BNB Smart Chain | `eip155:56` | | Monad | `eip155:143` | | Solana | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | | Robinhood Chain | `eip155:4663` | | Plasma | `eip155:9745` | | MegaETH | `eip155:4326` | Tron deposit automations are available to apps with Tron support enabled. Detection support does not guarantee that every action route is available. Use the [swap quote endpoint](/api-reference/wallets/swap/quote) to confirm the source and destination before creating an automation. [Crypto deposit addresses](/wallets/funding/crypto-deposits/overview#supported-chains) use this same deposit detection coverage for EVM and Solana sources. Their routes also require app-pays gas sponsorship. ### Filter assets The `assets.mode` field controls which deposits match: | Mode | Values | Behavior | | - | - | - | | `all` | None | Match every detected asset. | | `include` | 1–20 | Match only the listed assets. | | `exclude` | 1–20 | Match every asset except the listed assets. | Each `include` or `exclude` value identifies an asset using a human-readable alias or canonical identifiers: | Input | Scope | | - | - | | `{"asset": "usdc"}` | USDC on every supported deployment. | | `{"asset": "usdc", "chain": "base"}` | USDC on Base. | | `{"asset": "*", "chain": "base"}` | Every detected asset on Base. | | `{"asset_address": "0x...", "caip2": "eip155:8453"}` | A contract address on a specific chain. | Provide exactly one of `asset` or `asset_address` per `value`. A raw `asset_address` must include either `chain` or `caip2`. The `all` and `exclude` modes can match arbitrary deposited tokens. A matching action can still fail when the asset or route is unsupported. > [!WARNING] > > A matching deposit acts on the triggering asset's full current wallet balance, not only the amount > from the latest deposit. Any earlier balance of that asset can be included. If the balance is zero when the execution runs, Privy marks the execution as `skipped` and does not create a wallet action. ## Actions ### Swap The `swap` action converts the triggering asset into one configured destination asset and chain. `destination_chain_asset` accepts aliases or canonical identifiers, but it must identify exactly one asset on exactly one chain. ```json theme={"system"} { "type": "swap", "destination_chain_asset": { "asset": "usdc", "chain": "base" } } ``` Generated actions follow the [Swap API's behavior and considerations](/wallets/actions/swap/overview), including route, fee, and slippage behavior. The source wallet's [policies](/controls/policies/overview) continue to apply. Create-time validation does not quote the route or guarantee liquidity. Route, provider, balance, and policy errors can occur when an execution runs. Review the [supported chains](/wallets/actions/swap/overview#supported-chains) and [cross-chain routes](/wallets/actions/swap/overview#cross-chain-swaps) before configuring the action. ### Earn deposit The `earn_deposit` action deposits the triggering asset's full current wallet balance into one configured Earn vault. ```json theme={"system"} { "type": "earn_deposit", "vault_id": "vault-id" } ``` The deposit trigger must use `include` mode with exactly one asset. That asset and chain must match the vault's underlying asset. Privy rejects broader filters, multiple assets, and mismatched assets when the automation is created or updated. The vault must belong to the same Privy app as the automation. The app can create a separate automation for each vault and attach the appropriate automation to each wallet. Earn deposit attachments do not accept per-wallet `params`. The action gets its vault from `vault_id` and its amount from the wallet's current balance. When assets return to the wallet through an Earn withdrawal, Privy recognizes the vault's withdrawal source and skips the deposit trigger. This prevents an automated redeposit loop. Generated actions follow the [Earn deposit lifecycle](/wallets/actions/earn/deposit). The source wallet's [policies](/controls/policies/overview) continue to apply. ## Per-wallet parameters Only swap automations use per-wallet parameters. Swap output addresses belong to attachments rather than the reusable action config. This allows one automation to send each wallet's output to a different destination. ```json theme={"system"} { "automation_ids": ["automation-id"], "params": { "destination_address": "0x..." } } ``` Privy verifies that an automated swap uses the destination asset from the automation and the `destination_address` from the matched attachment. Omit `params` when attaching an Earn deposit automation. ## Example configuration This config swaps USDC deposits on any chain into pathUSD on Tempo: ```json theme={"system"} { "trigger": { "type": "deposit", "assets": { "mode": "include", "values": [{"asset": "usdc"}] } }, "action": { "type": "swap", "destination_chain_asset": { "asset": "pathusd", "chain": "tempo" } } } ``` Continue to [create and attach a swap automation](/wallets/automations/create-and-attach), or follow the [Earn deposit automation guide](/wallets/automations/earn-deposits). # Create and attach an automation Source: https://docs.privy.io/wallets/automations/create-and-attach Create a deposit-based swap automation and enable it for a wallet. This guide creates an automation that converts USDC deposits on any chain into pathUSD on Tempo. The app first creates a reusable automation definition, then attaches it to a source wallet. > [!WARNING] > > Run these examples from a trusted server. Never expose the Privy app secret in client-side code. ## Prerequisites The integration requires: * A Privy app ID and app secret. * A source [Privy wallet](/wallets/overview). Imported or previously exported wallets cannot receive automation attachments. * An authorization signature when the source wallet has an [owner](/controls/authorization-keys/owners/overview). ## 1. Create the automation Create an app-scoped automation with `POST /v1/wallet_automations`. See [configure triggers and actions](/wallets/automations/configuration) for supported triggers, filters, and actions. The source asset alias below does not specify a chain. It matches supported USDC deployments in Privy's asset registry. The destination includes a chain because every swap must have one destination. The required `owner_id` controls changes to the shared automation definition. Set it to `null` for an app-managed definition. Set it to a [key quorum](/controls/key-quorum/overview) ID to require that quorum's authorization for updates and deletion. **cURL** ```bash theme={"system"} curl --request POST 'https://api.privy.io/v1/wallet_automations' \ --user "$PRIVY_APP_ID:$PRIVY_APP_SECRET" \ --header "privy-app-id: $PRIVY_APP_ID" \ --header 'content-type: application/json' \ --data '{ "name": "Normalize deposits to Tempo PathUSD", "owner_id": null, "config": { "trigger": { "type": "deposit", "assets": { "mode": "include", "values": [ {"asset": "usdc"} ] } }, "action": { "type": "swap", "destination_chain_asset": { "asset": "pathusd", "chain": "tempo" } } } }' ``` **Node SDK** ```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 automation = await privy.walletAutomations().create({ name: 'Normalize deposits to Tempo PathUSD', owner_id: null, config: { trigger: { type: 'deposit', assets: { mode: 'include', values: [{asset: 'usdc'}] } }, action: { type: 'swap', destination_chain_asset: {asset: 'pathusd', chain: 'tempo'} } } }); ``` **Python** ```python theme={"system"} import os import requests app_id = os.environ["PRIVY_APP_ID"] app_secret = os.environ["PRIVY_APP_SECRET"] response = requests.post( "https://api.privy.io/v1/wallet_automations", auth=(app_id, app_secret), headers={"privy-app-id": app_id}, json={ "name": "Normalize deposits to Tempo PathUSD", "owner_id": None, "config": { "trigger": { "type": "deposit", "assets": { "mode": "include", "values": [{"asset": "usdc"}], }, }, "action": { "type": "swap", "destination_chain_asset": {"asset": "pathusd", "chain": "tempo"}, }, }, }, ) response.raise_for_status() automation = response.json() ``` Save the returned `id`. New automation definitions are enabled by default. API responses contain canonical `asset_address` and `caip2` values for the chain-specific aliases in this example. See the [create automation API reference](/api-reference/wallet-automations/create) for the complete schema. ## 2. Attach it to a wallet An attachment enables the automation for one wallet. Only enabled automation definitions can be attached. In the swap example, the params indicate that its `destination_address` receives the output of every generated swap. The destination address must be valid for the action's destination chain. Attaching the same automation again updates its parameters without changing its matching priority. **cURL** ```bash theme={"system"} curl --request POST \ "https://api.privy.io/v1/wallets/$WALLET_ID/automations/attach" \ --user "$PRIVY_APP_ID:$PRIVY_APP_SECRET" \ --header "privy-app-id: $PRIVY_APP_ID" \ --header 'content-type: application/json' \ --header "privy-authorization-signature: $PRIVY_AUTHORIZATION_SIGNATURE" \ --data "{ \"automation_ids\": [\"$AUTOMATION_ID\"], \"params\": { \"destination_address\": \"$DESTINATION_WALLET_ADDRESS\" } }" ``` **Node SDK** ```ts {skip-check} theme={"system"} const {data: attachments} = await privy.wallets().attachAutomations(process.env.WALLET_ID!, { automation_ids: [automation.id], params: { destination_address: process.env.DESTINATION_WALLET_ADDRESS! }, authorization_context: { signatures: [process.env.PRIVY_AUTHORIZATION_SIGNATURE!] } }); ``` **Python** ```python theme={"system"} import os import requests app_id = os.environ["PRIVY_APP_ID"] response = requests.post( f"https://api.privy.io/v1/wallets/{os.environ['WALLET_ID']}/automations/attach", auth=(app_id, os.environ["PRIVY_APP_SECRET"]), headers={ "privy-app-id": app_id, "privy-authorization-signature": os.environ[ "PRIVY_AUTHORIZATION_SIGNATURE" ], }, json={ "automation_ids": [automation["id"]], "params": { "destination_address": os.environ["DESTINATION_WALLET_ADDRESS"] }, }, ) response.raise_for_status() attachments = response.json()["data"] ``` The authorization signature must cover this exact request. Use Privy's [authorization-signature utilities](/controls/authorization-keys/using-owners/sign/utility-functions) to produce it. An ownerless wallet does not require a wallet-owner signature. > [!NOTE] > > Attaching an automation does not make it a general-purpose wallet signer. Privy authorizes each > execution against the attachment's trigger, action, wallet, and destination. ## 3. Send a matching deposit Send USDC to the source wallet on any chain. Privy detects the incoming transfer and evaluates enabled attachments from oldest to newest. Only the first matching automation creates a wallet action. Privy does not fall through to another matching automation if that action fails. Privy reads the current asset balance when the execution runs. **Earlier funds held by the wallet can therefore be included in the swap.** Attaching an automation does not process a balance already held by the wallet. Send a new matching deposit or [reindex the asset](/wallets/automations/lifecycle#recover-a-missed-or-failed-deposit) to move that balance. ## 4. Observe the execution Subscribe to [`wallet_automation.submitted`](/wallets/automations/webhooks) to receive both the automation execution ID and the generated wallet action ID. The app can also list executions for the source wallet: **cURL** ```bash theme={"system"} curl --request GET "https://api.privy.io/v1/wallet_automations/executions?wallet_id=$WALLET_ID" \ --user "$PRIVY_APP_ID:$PRIVY_APP_SECRET" \ --header "privy-app-id: $PRIVY_APP_ID" ``` **Node SDK** ```ts {skip-check} theme={"system"} const executions = []; for await (const execution of privy.walletAutomations().listExecutions({ wallet_id: process.env.WALLET_ID! })) { executions.push(execution); } ``` **Python** ```python theme={"system"} import os import requests app_id = os.environ["PRIVY_APP_ID"] params = {"wallet_id": os.environ["WALLET_ID"], "limit": 100} executions = [] while True: response = requests.get( "https://api.privy.io/v1/wallet_automations/executions", auth=(app_id, os.environ["PRIVY_APP_SECRET"]), headers={"privy-app-id": app_id}, params=params, ) response.raise_for_status() page = response.json() executions.extend(page["data"]) if not page["next_cursor"]: break params["cursor"] = page["next_cursor"] ``` Use the execution's `wallet_action_id` to track the generated wallet action through the [wallet action lifecycle](/wallets/actions/lifecycle). ## Handle errors Setup requests can fail when: * An asset alias, destination chain, or automation owner is invalid. * The wallet-owner authorization signature is missing or does not cover the exact attach request. * The destination address is invalid for the configured destination chain. An automation can still fail when it executes. Common causes include unavailable routes, insufficient liquidity or gas, policy rejection, and changed app configuration. Inspect `failure_reason` and the generated wallet action when one exists. After correcting a pre-action failure, [reindex the asset](/wallets/automations/lifecycle#recover-a-missed-or-failed-deposit). # Automate Earn deposits Source: https://docs.privy.io/wallets/automations/earn-deposits Automatically deposit a wallet balance into an Earn vault when funds arrive. An Earn deposit automation moves a wallet's full balance of one asset into a configured Earn vault when a matching deposit arrives. The app creates one reusable automation for each vault, then attaches the appropriate automation to each wallet. > [!WARNING] > > Run these examples from a trusted server. Never expose the Privy app secret in client-side code. ## Prerequisites The integration requires: * A Privy app ID and app secret. * A configured [Earn vault](/wallets/actions/earn/setup) and its `vault_id`. * A source [Privy wallet](/wallets/overview). Imported or previously exported wallets cannot receive automation attachments. * The vault's underlying asset and chain. The automation trigger must match both. * An authorization signature when the source wallet has an [owner](/controls/authorization-keys/owners/overview). ## 1. Create the automation Create an app-scoped automation with `POST /v1/wallet_automations`. The example below deposits OUSD on Tempo into the [Sentora OUSD Main vault on Morpho](https://app.morpho.org/tempo/vault/0x0D136e26B4D9e4836fc5cb202baeb49Cb6f5df48/sentora-ousd-main). The trigger must use `include` mode with exactly one asset. The asset and chain must match the configured vault. The required `owner_id` controls changes to the shared automation definition. **Node SDK** ```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 automation = await privy.walletAutomations().create({ name: 'Deposit Tempo OUSD into Earn', owner_id: null, config: { trigger: { type: 'deposit', assets: { mode: 'include', values: [{asset: 'ousd', chain: 'tempo'}] } }, action: { type: 'earn_deposit', vault_id: process.env.PRIVY_EARN_VAULT_ID! } } }); ``` **cURL** ```bash theme={"system"} curl --request POST 'https://api.privy.io/v1/wallet_automations' \ --user "$PRIVY_APP_ID:$PRIVY_APP_SECRET" \ --header "privy-app-id: $PRIVY_APP_ID" \ --header 'content-type: application/json' \ --data "{ \"name\": \"Deposit Tempo OUSD into Earn\", \"owner_id\": null, \"config\": { \"trigger\": { \"type\": \"deposit\", \"assets\": { \"mode\": \"include\", \"values\": [{\"asset\": \"ousd\", \"chain\": \"tempo\"}] } }, \"action\": { \"type\": \"earn_deposit\", \"vault_id\": \"$PRIVY_EARN_VAULT_ID\" } } }" ``` Save the returned automation `id`. New automation definitions are enabled by default. ## 2. Attach it to a wallet Attach the automation to each wallet that should deposit into the vault. Earn deposit automations do not accept per-wallet `params`. **Node SDK** ```ts {skip-check} theme={"system"} const {data: attachments} = await privy.wallets().attachAutomations(process.env.WALLET_ID!, { automation_ids: [automation.id], authorization_context: { signatures: [process.env.PRIVY_AUTHORIZATION_SIGNATURE!] } }); ``` **cURL** ```bash theme={"system"} curl --request POST \ "https://api.privy.io/v1/wallets/$WALLET_ID/automations/attach" \ --user "$PRIVY_APP_ID:$PRIVY_APP_SECRET" \ --header "privy-app-id: $PRIVY_APP_ID" \ --header "privy-authorization-signature: $PRIVY_AUTHORIZATION_SIGNATURE" \ --header 'content-type: application/json' \ --data "{\"automation_ids\": [\"$AUTOMATION_ID\"]}" ``` The authorization signature must cover the exact attach request. Omit `authorization_context` and the `privy-authorization-signature` header for an ownerless wallet. ## 3. Send a matching deposit Send the vault's underlying asset to the source wallet. Privy detects the transfer, reads the wallet's current asset balance, and submits an `earn_deposit` wallet action for the full balance. > [!WARNING] > > The automation deposits the wallet's full current balance, not only the latest transfer. Earlier > funds held by the wallet can be included. Attaching an automation does not process a balance already held by the wallet. Send a new matching deposit or [reindex the asset](/wallets/automations/lifecycle#recover-a-missed-or-failed-deposit) to process that balance. For multi-chain or different-asset funding, an app can chain a crypto deposit account with an Earn deposit automation. The crypto deposit account accepts funds on other chains or in other assets and swaps them into the vault's underlying asset in a destination wallet. Attach the Earn deposit automation to that destination wallet to sweep the funds into the vault. This lets the app accept assets across chains and deposit them into a vault. Learn more about [crypto deposit accounts](/wallets/funding/crypto-deposits/overview). ## 4. Observe the deposit Subscribe to [`wallet_automation.submitted`](/wallets/automations/webhooks) to receive the automation execution ID and generated wallet action ID. Use the action ID to track the [wallet action lifecycle](/wallets/actions/lifecycle). The [`wallet_action.earn_deposit.succeeded`](/api-reference/webhooks/wallet-action/earn-deposit/succeeded) webhook confirms that the assets entered the vault. Failed or rejected Earn actions emit the corresponding [Earn webhooks](/wallets/actions/earn/webhooks). ## Withdraw assets Use the [Earn withdrawal action](/wallets/actions/earn/withdraw) to return assets and accrued yield to the wallet. Privy recognizes transfers from the configured vault and does not redeposit them automatically. ## Handle errors Setup requests can fail when: * The vault does not belong to the app. * The trigger does not match exactly one underlying vault asset and chain. * The wallet-owner authorization signature is missing or does not cover the exact attach request. * The automation includes attachment `params`. An automation can still fail during execution because of an insufficient balance, policy rejection, unavailable vault, or onchain failure. Inspect the execution's `failure_reason` and generated wallet action. After correcting a pre-action failure, [reindex the asset](/wallets/automations/lifecycle#recover-a-missed-or-failed-deposit). # Automation lifecycle Source: https://docs.privy.io/wallets/automations/lifecycle Understand wallet automation matching, execution statuses, and wallet action outcomes. Privy creates an automation execution after a trigger is matched on an enabled attachment. The execution records why the automation ran and links to the wallet action that it executed. ## Execution flow ```mermaid theme={"system"} flowchart LR pending --> submitted pending --> failed pending --> skipped submitted --> completed submitted --> failed ``` | Status | Terminal | Description | | - | - | - | | `pending` | No | Privy matched the deposit and is preparing the wallet action. | | `submitted` | No | Privy created the wallet action and submitted it for processing. | | `completed` | Yes | The linked wallet action completed successfully. | | `failed` | Yes | Preparation or the linked wallet action failed. Inspect `failure_reason` and the action. | | `skipped` | Yes | The trigger matched, but no action was needed. This can occur if another automation already executed. | ## Relationship to wallet actions The execution's `wallet_action_id` identifies the action created by the automation. The wallet action contains the detailed onchain lifecycle, including steps and transaction identifiers. The [`wallet_automation.submitted`](/wallets/automations/webhooks) webhook is emitted when the execution reaches `submitted`. It does not indicate that the generated wallet action completed. To observe the final outcome: 1. Read `action_id` from the submitted webhook or `wallet_action_id` from the execution. 2. Fetch the [wallet action status](/wallets/actions/status). 3. Subscribe to the corresponding [`wallet_action.*` events](/wallets/actions/webhooks). ## List executions Use `GET /v1/wallet_automations/executions` to list executions across the app. Pass `wallet_id` to restrict results to one source wallet. **cURL** ```bash theme={"system"} curl --request GET "https://api.privy.io/v1/wallet_automations/executions?wallet_id=$WALLET_ID&limit=25" \ --user "$PRIVY_APP_ID:$PRIVY_APP_SECRET" \ --header "privy-app-id: $PRIVY_APP_ID" ``` **Node SDK** ```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 executions = []; for await (const execution of privy.walletAutomations().listExecutions({ wallet_id: process.env.WALLET_ID!, limit: 100 })) { executions.push(execution); } ``` **Python** ```python theme={"system"} import os import requests app_id = os.environ["PRIVY_APP_ID"] params = {"wallet_id": os.environ["WALLET_ID"], "limit": 100} executions = [] while True: response = requests.get( "https://api.privy.io/v1/wallet_automations/executions", auth=(app_id, os.environ["PRIVY_APP_SECRET"]), headers={"privy-app-id": app_id}, params=params, ) response.raise_for_status() page = response.json() executions.extend(page["data"]) if not page["next_cursor"]: break params["cursor"] = page["next_cursor"] ``` Results are ordered from newest to oldest. The Node SDK automatically requests subsequent pages during iteration, and the Python example explicitly follows `next_cursor`. With cURL, pass the returned `next_cursor` as `cursor` in the next request until it is `null`. Each execution includes: * The source `wallet_id` and matched `automation_attachment_id`. * The triggering information (e.g. transaction, block, chain, and asset). * The linked `wallet_action_id` after submission. * Status timestamps and an optional `failure_reason`. The execution response does not contain `automation_id`. Store submitted webhook payloads when the app must retain a direct association between an execution and automation after deletion. ## Recover a missed or failed deposit Use `POST /v1/wallet_automations/reindex` to recheck a wallet's current balance for one asset. This can recover a deposit that did not create an execution or whose execution failed before creating a wallet action. ```bash theme={"system"} curl --request POST "https://api.privy.io/v1/wallet_automations/reindex" \ --user "$PRIVY_APP_ID:$PRIVY_APP_SECRET" \ --header "privy-app-id: $PRIVY_APP_ID" \ --header "content-type: application/json" \ --data "{ \"wallet_id\": \"$WALLET_ID\", \"caip2\": \"eip155:4217\", \"asset_address\": \"$ASSET_ADDRESS\" }" ``` Identify the wallet with `wallet_id` or `deposit_address`. Identify the chain with `caip2` or a human-readable `chain` name. For a native asset, pass `native` as `asset_address`. Reindexing does not replay the original deposit amount. Privy reads the asset's current wallet balance and submits a new execution only when the balance is greater than zero and an enabled attachment matches. An existing `pending` or `submitted` execution blocks reindexing for the same wallet and asset. > [!WARNING] > > A recovered automation acts on the asset's full current wallet balance. Funds received after the > original deposit can therefore be included. Each result has one of the following statuses: | Status | Meaning | | - | - | | `submitted` | Privy submitted a new automation execution. | | `skipped_zero_balance` | The wallet had no balance for the requested asset. | | `skipped_no_match` | No enabled attachment matched the requested asset. | | `skipped_existing_execution` | A `pending` or `submitted` execution already exists. | | `failed` | Privy could not check the balance or submit the execution. | The endpoint can return HTTP `200` with a `failed` result. Inspect every result rather than treating the HTTP status as the recovery outcome. See the [reindex API reference](/api-reference/wallet-automations/reindex) for the complete request and response schemas. ## Delivery and concurrency Privy deduplicates detected deposits by wallet, block, chain, and asset. Multiple deposits of the same asset to one wallet in the same block can produce one full-balance execution. Apps should also process webhook deliveries idempotently by `trigger_id`. Privy serializes automation matching and wallet-action creation for each wallet. The generated wallet action continues asynchronously after creation, so another automation execution can be submitted before the previous action completes. Each execution reads the current balance. A later execution can become `skipped` if an earlier wallet action already consumed that balance. Privy retries transient errors during action preparation. If an execution remains `pending` and does not advance, reindexing cannot replace it. Contact Privy support with the execution and wallet IDs. ## Troubleshooting | Result | What to inspect | | - | - | | No execution | Confirm the automation and attachment are enabled and the trigger matches the asset filter. A pre-existing balance requires a new deposit or [reindex](#recover-a-missed-or-failed-deposit). | | `pending` | Allow transient retries to complete. Contact Privy support if the execution does not advance. | | `skipped` | Check whether an earlier execution already swept the balance. | | `failed` without an action | Inspect `failure_reason`, supported assets and chains, balance availability, and authorization. Reindex after correcting the cause. | | `failed` with an action | Fetch the wallet action with steps and inspect its policy, simulation, and onchain failure data before reindexing. | # Manage automations Source: https://docs.privy.io/wallets/automations/manage List, update, pause, attach, detach, and delete wallet automations safely. Automation definitions are app-scoped and reusable. Attachments activate those definitions for individual wallets. ## List automations List all definitions with `GET /v1/wallet_automations`. Pass `wallet_id` to return only automations attached to one wallet. Wallet create, get, and list responses can also include an `automations` array. Each entry contains the automation `id` and an `enabled` value that reflects both the definition and attachment state. The Node SDK and Python examples initialize reusable clients here. The remaining examples on this page reuse those clients. **cURL** ```bash theme={"system"} curl --request GET "https://api.privy.io/v1/wallet_automations?wallet_id=$WALLET_ID&limit=25" \ --user "$PRIVY_APP_ID:$PRIVY_APP_SECRET" \ --header "privy-app-id: $PRIVY_APP_ID" ``` **Node SDK** ```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 automations = []; for await (const automation of privy.walletAutomations().list({ wallet_id: process.env.WALLET_ID!, limit: 100 })) { automations.push(automation); } ``` **Python** ```python theme={"system"} import os import requests app_id = os.environ["PRIVY_APP_ID"] params = {"wallet_id": os.environ["WALLET_ID"], "limit": 100} automations = [] while True: response = requests.get( "https://api.privy.io/v1/wallet_automations", auth=(app_id, os.environ["PRIVY_APP_SECRET"]), headers={"privy-app-id": app_id}, params=params, ) response.raise_for_status() page = response.json() automations.extend(page["data"]) if not page["next_cursor"]: break params["cursor"] = page["next_cursor"] ``` ## Update an automation `PATCH /v1/wallet_automations/{automation_id}` can update `name`, `config`, `enabled`, or `owner_id`. Fields omitted from the request remain unchanged. Setting `name` or `owner_id` to `null` clears that field. The existing owner must authorize updates to an owned automation. > [!WARNING] > > Updating `config` changes the trigger and action for every attached wallet. Create a separate > automation when only some wallets should adopt the new behavior. ### Pause or resume an automation **cURL** ```bash theme={"system"} curl --request PATCH \ "https://api.privy.io/v1/wallet_automations/$AUTOMATION_ID" \ --user "$PRIVY_APP_ID:$PRIVY_APP_SECRET" \ --header "privy-app-id: $PRIVY_APP_ID" \ --header 'content-type: application/json' \ --data '{"enabled": false}' ``` **Node SDK** ```ts {skip-check} theme={"system"} const automation = await privy.walletAutomations().update(process.env.AUTOMATION_ID!, { enabled: false }); ``` **Python** ```python theme={"system"} response = requests.patch( f"https://api.privy.io/v1/wallet_automations/{os.environ['AUTOMATION_ID']}", auth=(app_id, os.environ["PRIVY_APP_SECRET"]), headers={"privy-app-id": app_id}, json={"enabled": False}, ) response.raise_for_status() automation = response.json() ``` Disabling an automation pauses it for every attached wallet. Attachments remain in place and resume when the automation is enabled again. Automation responses represent this state as `status: "enabled" | "disabled"`. Update requests use the boolean `enabled` field. ## Matching precedence A wallet can have multiple attached automations. Privy evaluates enabled attachments from oldest to newest and runs only the first matching automation. Privy does not try a lower-priority match if the selected automation fails. Use non-overlapping asset filters when every route must be unambiguous. If filters intentionally overlap, attach the highest-priority automation first. Attaching an automation that is already attached updates the existing attachment and preserves its priority. Detaching and later reattaching creates a new attachment that is evaluated after older attachments. ## Update attachment parameters Call the attach endpoint again with the existing automation ID to change its per-wallet parameters. For a swap automation, this updates `destination_address` without creating a duplicate attachment. The request's single `params` object applies to every automation ID. Batch together only automations that accept the same parameters. Earn deposit automations do not accept `params`. ## Detach from a wallet Detaching stops selected automations for one wallet without changing their other attachments. Detach requests are all or nothing. If any requested automation is not attached to the wallet, the request fails without detaching the others. **cURL** ```bash theme={"system"} curl --request POST \ "https://api.privy.io/v1/wallets/$WALLET_ID/automations/detach" \ --user "$PRIVY_APP_ID:$PRIVY_APP_SECRET" \ --header "privy-app-id: $PRIVY_APP_ID" \ --header 'content-type: application/json' \ --header "privy-authorization-signature: $PRIVY_AUTHORIZATION_SIGNATURE" \ --data "{\"automation_ids\": [\"$AUTOMATION_ID\"]}" ``` **Node SDK** ```ts {skip-check} theme={"system"} await privy.wallets().detachAutomations(process.env.WALLET_ID!, { automation_ids: [process.env.AUTOMATION_ID!], authorization_context: { signatures: [process.env.PRIVY_AUTHORIZATION_SIGNATURE!] } }); ``` **Python** ```python theme={"system"} response = requests.post( f"https://api.privy.io/v1/wallets/{os.environ['WALLET_ID']}/automations/detach", auth=(app_id, os.environ["PRIVY_APP_SECRET"]), headers={ "privy-app-id": app_id, "privy-authorization-signature": os.environ[ "PRIVY_AUTHORIZATION_SIGNATURE" ], }, json={"automation_ids": [os.environ["AUTOMATION_ID"]]}, ) response.raise_for_status() ``` > [!NOTE] > > A successful private-key or seed-phrase export also removes all automation attachments from that > wallet. The exported wallet cannot receive new automation attachments. ## Delete an automation Deleting an automation permanently removes its definition and all attachments. Existing execution and wallet action records remain, but a deleted attachment can no longer identify its automation through the execution list response. Persist the IDs from submitted webhooks when long-term correlation is required. Use disable when the automation might resume. Use detach to stop it for selected wallets. Delete only when the definition is no longer needed. **cURL** ```bash theme={"system"} curl --request DELETE \ "https://api.privy.io/v1/wallet_automations/$AUTOMATION_ID" \ --user "$PRIVY_APP_ID:$PRIVY_APP_SECRET" \ --header "privy-app-id: $PRIVY_APP_ID" ``` **Node SDK** ```ts {skip-check} theme={"system"} await privy.walletAutomations().delete(process.env.AUTOMATION_ID!); ``` **Python** ```python theme={"system"} response = requests.delete( f"https://api.privy.io/v1/wallet_automations/{os.environ['AUTOMATION_ID']}", auth=(app_id, os.environ["PRIVY_APP_SECRET"]), headers={"privy-app-id": app_id}, ) response.raise_for_status() ``` ## Advanced: automation ownership Set `owner_id` during creation or update to assign a [key quorum](/controls/key-quorum/overview) as the automation owner. The existing owner must authorize changing or clearing `owner_id`. | Operation | Required authorization | | - | - | | Create or read | App-secret authentication. | | Update or delete | App-secret authentication and the automation owner's authorization signature, if it has one. | | Attach or detach | Authorization from the target wallet's owner, when the wallet has one. | The owner protects the shared definition. The wallet owner separately controls whether that definition is attached to their wallet. ## Limits * Imported wallets and wallets previously exported from Privy cannot receive automation attachments. * An attach or detach request accepts up to 20 automation IDs. * An `include` or `exclude` filter accepts up to 20 asset specifications in an API request. * A wallet can have up to 100 automation attachments. * List endpoints accept `limit` values from 1 through 100 and default to 25. See the [wallet automation API reference](/api-reference/wallet-automations/create) for request and response schemas. # Wallet automations Source: https://docs.privy.io/wallets/automations/overview Define policy-controlled wallet actions that Privy runs in response to events. Wallet automations define how a wallet responds to an event. An automation is a reusable intent: when a configured **trigger** occurs, Privy submits a configured **action**. Attach the same automation to many wallets while controlling which wallets can run it. Wallet owner authorization and [wallet policies](/controls/policies/overview) continue to apply. Wallet automations support deposit triggers with `swap` and `earn_deposit` actions. Swap automations can power [crypto deposit addresses](/wallets/funding/crypto-deposits/overview). Earn deposit automations move funds into a configured [Earn vault](/wallets/actions/earn/overview). ## Why use automations Without an automation, an app's backend must monitor chains, deduplicate events, and submit wallet actions. Wallet automations move that operational loop to Privy while the app retains control over configuration, authorization, and policy. This is useful for wallet behavior that should run reliably outside your app's lifecycle. The following flow shows an automation processing an incoming deposit. See [configure triggers and actions](/wallets/automations/configuration) for available trigger and action types. ```mermaid theme={"system"} flowchart LR deposit[Deposit arrives] --> match[Match an enabled attachment] match --> execution[Create an automation execution] execution --> action[Submit a wallet action] action --> outcome[Track the action outcome] ``` ## How automations fit together | Resource | Scope | Purpose | | - | - | - | | Automation | App | Defines the trigger, asset filter, and action. | | Attachment | Wallet | Enables an automation for a wallet and stores action-specific parameters. | | Execution | Event | Records one matched trigger and the wallet action created to fulfill it. | | Wallet action | Wallet | Exposes the action's onchain status, steps, and transaction identifiers. | Each trigger match creates an [automation execution](/wallets/automations/lifecycle), which records why the automation ran and links to the wallet action that carries out the onchain operation. ## Authorization and policies Authorization applies at three levels: | Control | Purpose | | - | - | | Automation owner | A key quorum can authorize updates or deletion of the shared definition. | | Wallet owner | The wallet owner authorizes attaching or detaching the automation. | | Wallet policies | Privy evaluates every generated wallet action against the source wallet's policies. | An attachment authorizes future actions that match its configured trigger, action, wallet, and parameters. Executions do not require a new wallet-owner signature. An attachment does not grant an automation general-purpose signing authority. Imported wallets and wallets previously exported from Privy cannot receive automation attachments. ## Fees Automations are billed for the wallet actions they trigger. There is no separate fee per automation invocation or based on the amount flowing through the automation. An automated swap follows the same [fees](/wallets/actions/swap/overview#fees) and app sponsorship settings as other swaps. For crypto deposit addresses, use the [deposit quote API](/api-reference/crypto-deposits/quote) to preview current conversion pricing. [App-pays gas sponsorship](/wallets/funding/crypto-deposits/setup) is required for crypto deposit addresses. ## Matching behavior A wallet can have several attached automations, but Privy selects only the oldest enabled attachment that matches a detected deposit. Overlapping automations do not all run. Use non-overlapping asset filters unless attachment order intentionally defines priority. ## Next steps > **[Configure an automation](/wallets/automations/configuration)** > > Choose a trigger, filter, action, and per-wallet parameters. > **[Create and attach an automation](/wallets/automations/create-and-attach)** > > Configure a deposit trigger and enable it for a wallet. > **[Automate Earn deposits](/wallets/automations/earn-deposits)** > > Deposit a wallet's full balance into an Earn vault when funds arrive. > **[Understand the lifecycle](/wallets/automations/lifecycle)** > > Monitor automation executions and their wallet actions. > **[Use webhooks](/wallets/automations/webhooks)** > > Receive an event when an automation submits a wallet action. > **[Manage automations](/wallets/automations/manage)** > > Update, pause, detach, and delete automations safely. # Webhooks Source: https://docs.privy.io/wallets/automations/webhooks Receive a webhook when a wallet automation submits its wallet action. Privy emits `wallet_automation.submitted` after an automation creates a wallet action and submits it for processing. This event marks the handoff from automation matching to the [wallet action lifecycle](/wallets/actions/lifecycle). It does not mean the generated action succeeded. ## Setup Subscribe to `wallet_automation.submitted` from the **Configuration > Webhooks** page in the [Privy Dashboard](https://dashboard.privy.io/apps?page=webhooks). For signature verification, retries, and endpoint setup, see the [webhooks overview](/api-reference/webhooks/overview). > [!NOTE] > > Webhooks can be tested at no cost in development environments. Production webhooks require an > Enterprise plan. ## Payload ```json theme={"system"} { "type": "wallet_automation.submitted", "trigger_id": "fmfdj6yqly31huorjqzq38zc", "wallet_id": "cm4sourcewallet123", "action_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "automation_id": "a2h6k9m3p7q1r4t8v5x0y3z6b", "created_at": "2026-07-27T18:00:00.000Z", "trigger_caip2": "eip155:8453", "trigger_asset_address": "0x1234567890abcdef1234567890abcdef12345678", "trigger_tx_hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef" } ``` | Field | Description | | - | - | | `trigger_id` | ID of the automation execution. Use it to deduplicate webhook deliveries. | | `wallet_id` | ID of the source wallet where the trigger arrived. | | `action_id` | ID of the wallet action created to fulfill the automation. | | `automation_id` | ID of the matched automation definition. | | `created_at` | ISO 8601 timestamp when the wallet action was submitted. | | `trigger_caip2` | CAIP-2 identifier for the chain where the trigger arrived. | | `trigger_asset_address` | Trigger asset contract address, or `native` for the chain's native asset. | | `trigger_tx_hash` | Transaction hash of the triggering deposit, or `null` when triggered by a reindex. | See the [webhook API reference](/api-reference/webhooks/wallet-automation/submitted) for the complete schema. ## Track the final outcome Use `action_id` to correlate this event with the generated wallet action. The app can either fetch the [wallet action status](/wallets/actions/status) or subscribe to its terminal webhooks: | Event | Meaning | | - | - | | `wallet_action.*.succeeded` | The automated swap completed successfully. | | `wallet_action.*.rejected` | Privy rejected the swap before an onchain effect occurred. | | `wallet_action.*.failed` | The swap failed after processing began. | The initial automation webhook surface does not include separate completed, skipped, or failed events. Automation failures that occur before a wallet action exists are available through the [execution list API](/api-reference/wallet-automations/list-executions). Treat the execution list as the source of truth. Webhooks are delivery notifications and should not be the only record of automation activity. # 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`**. > [!WARNING] > > 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: **providers.tsx** ```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} ); } ``` **wagmiConfig.ts** ```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() } }); ``` **privyConfig.ts** ```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}

; } ``` > [!NOTE] > > 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](https://mintcdn.com/privy-c2af3412/YvGXGsI-T4KAqoan/images/Connectors3.png?fit=max\&auto=format\&n=YvGXGsI-T4KAqoan\&q=85\&s=c0e26042d5ea73c83d057b041e53b964) # 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 **EVM and Solana** > [!TIP] > > 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. **EVM** In your `PrivyProvider`, set the `config.appearance.walletChainType` to `'ethereum-only'`. ```tsx theme={"system"} import {PrivyProvider} from '@privy-io/react-auth'; {children} ``` **Solana** > [!TIP] > > 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 > [!TIP] > > **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} ``` > [!NOTE] > > 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 | > [!NOTE] > > **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`. > [!WARNING] > > **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 > [!WARNING] > > **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 > [!NOTE] > > * `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` **wallet\_connect** **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'] ``` **wallet\_connect\_qr** **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'] ` **wallet\_connect\_qr\_solana** **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:** > [!WARNING] > > 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 > **Wallet List Entry Cheat Sheet** > > | 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. *** > **Using the Base Account (formerly Coinbase Smart Wallet)** > > 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 **React** 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. > [!NOTE] > > 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 **React** 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 **React** 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: **EVM** ```tsx theme={"system"} import {useWallets} from '@privy-io/react-auth'; ... const {wallets} = useWallets(); ... wallets[0].loginOrLink(); ``` **Solana** ```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: **EVM** ```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 */} ); } ``` **Solana** ```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`. > [!WARNING] > > **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 ; } ``` > [!NOTE] > > 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 **React** 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. > [!NOTE] > > 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 ; } ``` > [!TIP] > > 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 **wallet** `ConnectedWallet` The most recently connected wallet. #### `onError` ```tsx theme={"system"} onError: (error: Error) => Promise; ``` ##### Parameters **error** `Error` 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 **React** > [!TIP] > > 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 > [!NOTE] > > 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 **description** `string` A description for the wallet connection prompt, which will be displayed in Privy's UI. **walletList** `WalletListEntry[]` A list of \[wallet option] that you would like Privy to display in the connection prompt. **walletChainType** `'solana-only' | 'ethereum-only' | 'ethereum-and-solana'` 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); }, }); ``` **onSuccess** `(\{wallet: Wallet\}) =\> void` An optional callback function that is called when a user successfully connects their wallet. **onError** `(\{error: string\}) =\> void` 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. > [!TIP] > > 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. > [!TIP] > > 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. > [!TIP] > > View the complete [Policies documentation](/controls/policies/overview) to learn about all available policy options and configuration. ## Next steps > **[Webhooks](/wallets/custodial-wallets/advanced/webhooks)** > > Monitor wallet events and transaction status > **[Send funds](/wallets/custodial-wallets/sending-funds)** > > 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/deposits). 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). > [!TIP] > > 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 > **[Webhooks](/wallets/custodial-wallets/advanced/webhooks)** > > Monitor wallet events and transaction status > **[Send funds](/wallets/custodial-wallets/sending-funds)** > > 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. > [!NOTE] > > 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). > [!NOTE] > > 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: **bridge\_metadata attributes** **method** `'virtual_account' | 'liquidation_address' | 'transfer'` *required* Method used to orchestrate the deposit. **type** `'fiat_deposit' | 'crypto_deposit' | 'refund'` *required* 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. **activity\_id** `string` ID of the activity that caused the deposit. Only applicable when `method` is `virtual_account`. **transfer\_id** `string` ID of the transfer that caused the deposit. Only applicable when `method` is `transfer`. **liquidation\_address\_id** `string` ID of the liquidation address that caused the deposit. Only applicable when `method` is `liquidation_address`. **drain\_id** `string` ID of the drain event of the deposit. Only applicable when `method` is `liquidation_address`. **source\_wallet\_address** `string` Address of the source wallet that initiated the deposit. Only applicable when `method` is `transfer` or `liquidation_address`. **original\_transaction\_hash** `string` Hash of the original transaction that caused the offramp attempt. Only applicable when `type` is `refund`. **virtual\_account\_id** `string` ID of the virtual account that caused the deposit. Only applicable when `method` is `virtual_account`. > **Example: Onramp from fiat deposit via virtual account** > > ```json theme={"system"} > { > "event": "wallet.funds_deposited", > "data": { > "amount": "2340000", > "asset": { > "address": "0x20c0000000000000000000006a37da5c996874be", > "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 > **[Wallet action webhooks](/wallets/actions/webhooks)** > > Complete guide to wallet action lifecycle webhooks > **[Funding](/wallets/custodial-wallets/advanced/funding)** > > Complete guide to using Bridge to onramp and offramp funds from your Privy custodial wallets > **[Balance event webhooks](/wallets/gas-and-asset-management/assets/balance-event-webhooks)** > > 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 **REST API** To create a custodial wallet via REST API, make a `POST` request to: ```bash theme={"system"} https://api.privy.io/v1/custodial_wallets ``` ### Body **chain** `'tempo' | 'base' | 'solana'` *required* The chain for the custodial wallet. Supported chains are Tempo, Base, and Solana mainnet. **provider** `'bridge'` *required* The custodian of the wallet. **provider\_user\_id** `string` *required* The user ID of the beneficiary of the custodial wallet, provided by the licensing provider after KYC. **owner** `\{user_id: string\} | \{public_key: string\}` The entity that can authorize transactions from the custodial wallet and configure additional signers. **additional\_signers** `\{signer_id: string, override_policy_ids: string[]\}[]` 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. **policy\_ids** `string[]` 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": "tempo", "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: **id** `string` Unique ID of the created wallet. This will be the primary identifier when using the wallet in the future. **address** `string` Address of the created wallet. **chain** `'tempo' | 'base' | 'solana'` Chain of the created wallet. **provider** `string` The custodian of the wallet. **owner\_id** `string` The key quorum ID of the owner of the wallet. If an `owner` was passed in, this field will be populated. **additional\_signers** `string[]` The key quorum IDs of the additional signers for the wallet. **policy\_ids** `string[]` List of policy IDs for policies that are enforced on the wallet. **created\_at** `number` The creation date of the wallet, in milliseconds since midnight, January 1, 1970 UTC. ```json theme={"system"} { "id": "p12aj1whizzmklph0b36xk6n", "address": "0x9f284C7Eaf97b0f9B5542d83Af7F785D12E803a", "chain": "tempo", "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). > **[Send funds](/wallets/custodial-wallets/sending-funds)** > > Execute transactions and send funds from custodial wallets > **[Transaction lifecycle](/wallets/custodial-wallets/transaction-lifecycle)** > > Learn about the transaction lifecycle for custodial wallets > **[Authorization controls](/wallets/custodial-wallets/advanced/authorization-controls)** > > 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). > [!TIP] > > 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 > **[Setup](/wallets/custodial-wallets/setup)** > > Get started with flexible custody > **[Create a custodial wallet](/wallets/custodial-wallets/create-custodial-wallet)** > > Learn how to create and provision custodial wallets > **[Send funds](/wallets/custodial-wallets/sending-funds)** > > Execute transactions and send funds from custodial wallets # Sending funds Source: https://docs.privy.io/wallets/custodial-wallets/sending-funds Custodial wallets support transferring assets using the [`/transfer`](/wallets/actions/transfer/usage) endpoint — the same interface used by non-custodial wallets. This provides a simple, chain-agnostic way to move funds without needing to encode raw transaction data. > [!NOTE] > > All transactions from custodial wallets are executed server-side. ## Initiate a transfer For custodial wallets, only assets that are supported by the custodian can be transferred. **Supported assets:** OUSD, USDC, USDB, and EURC **Supported chains:** Tempo, Base, and Solana > [!NOTE] > > Like non-custodial wallets, custodial wallets with an `owner` or `additional_signers` require an > [authorization signature](/controls/authorization-keys/using-owners/sign/overview) for transaction > requests. ### Usage Make a `POST` request to `/v1/wallets/{wallet_id}/transfer` with the source asset, amount, chain, and destination address: **Node.js SDK** ```js theme={"system"} const transfer = await privy.wallets().transfer('insert-wallet-id', { amount: '10.0', source: { asset: 'ousd', chain: 'tempo', }, destination: { address: '0xRecipientAddress...', }, }); console.log(transfer.id); // wallet action ID console.log(transfer.status); // 'pending' ``` **REST API** ```bash theme={"system"} curl -X POST https://api.privy.io/v1/wallets/{wallet_id}/transfer \ -H "Content-Type: application/json" \ -H "privy-app-id: YOUR_APP_ID" \ -H "Authorization: Basic BASE64_ENCODED_APP_SECRET" \ -d '{ "source": { "asset": "ousd", "amount": "10.0", "chain": "tempo" }, "destination": { "address": "0xRecipientAddress..." } }' ``` ### Parameters **walletId** `string` *required* The ID of the custodial wallet to send the transfer from. **source** `object` *required* The source asset, amount, and chain for the transfer. **properties** **source.asset** `string` *required* The asset to transfer (e.g. `"ousd"`, `"usdc"`, `"usdb"`, `"eurc"`). **source.amount** `string` *required* The amount to transfer as a decimal string in standard units (e.g. `"10.0"` for 10 OUSD). No need to convert to smallest units. **source.chain** `'tempo' | 'base' | 'solana'` *required* The chain to transfer from (e.g. `"tempo"` or `"solana"`). **destination** `object` *required* The destination for the transfer. **properties** **destination.address** `string` *required* The recipient wallet address. Use a hex address for EVM chains and a base58 address for Solana. **custody\_options** `object` Additional options for custodial transfers. **properties** **custody\_options.initiation** `object` *required* Payment initiation context. This should be provided depending on SCA requirements for the custodial wallet. This context is passed to Bridge. Learn more about [Strong Customer Authentication (SCA)](/wallets/custodial-wallets/advanced/strong-customer-authentication). **properties** **custody\_options.initiation.channel** `string` *required* How the payment was initiated. Use `p2p_mobile_payment` for peer-to-peer transfers initiated on a mobile device; `other_mobile_payment` for non-P2P mobile-initiated payments (e.g. a merchant payment via a mobile app); `other` for payments not relying on a mobile device. **custody\_options.initiation.subchannel** `string` *required* Whether the payment was made remotely or in person. Use `remote` for payments initiated from a distance (mobile app, online banking, or e-commerce checkout); `non_remote` for payments made in person (physical card, payment terminal, or contactless tap). **custody\_options.initiation.attestations** `object` *required* Payment initiation attestations for a transfer. **properties** **custody\_options.initiation.attestations.sca** `object` *required* Strong Customer Authentication attestation for a transfer. **properties** **custody\_options.initiation.attestations.sca.outcome** `string` *required* Whether Strong Customer Authentication (SCA) was applied or which regulatory exemption or non-applicability reason covers this payment. | Outcome | Description | | - | - | | `sca_used` | The user authenticated with SCA. | | `payment_to_self` | Payer and payee are the same person (remote only). | | `trusted_beneficiaries` | Payee is on the user's pre-approved list. | | `recurring_transaction` | Amount and payee match a previously SCA-authorized recurring series. | | `contactless_low_value` | Contactless card payment below the low-value threshold (non-remote only). | | `unattended_terminal_for_transport` | Automated terminal for transport fares or parking (non-remote only). | | `low_value` | Remote payment below the low-value threshold (remote only). | | `secure_corporate_payment` | Dedicated corporate payment process with controls equivalent to SCA (remote only). | | `transaction_risk_analysis` | PSP has performed real-time risk analysis and the transaction falls within permitted thresholds (remote only). | | `merchant_initiated_transaction` | Payment triggered by the merchant without the payer present, on a pre-authorized mandate (remote only). | | `not_applicable` | This flow requires initiation context but SCA and SCA exemptions do not apply. | | `other` | Another recognized exemption not listed above. | **custody\_options.initiation.attestations.sca.auth\_factors** `object[]` Authentication factor metadata. Optional when `outcome` is `sca_used`; if provided, it must contain at least two entries from different `category` values (e.g. one `possession` factor and one `knowledge` factor). Omit this field for any other outcome. **properties** **custody\_options.initiation.attestations.sca.auth\_factors\[].category** `string` *required* The type of authentication factor used: `knowledge` (something only the user knows, e.g. a PIN or password), `possession` (something only the user has, e.g. a phone receiving an OTP or a hardware token), or `inherence` (something the user is, e.g. a fingerprint or face scan). When `outcome` is `sca_used`, the two factors in `auth_factors` must belong to two different categories. **custody\_options.initiation.attestations.sca.auth\_factors\[].authenticated\_at** `string` *required* The ISO 8601 timestamp when this factor was authenticated. **custody\_options.initiation.attestations.sca.auth\_factors\[].reference** `string` *required* Your internal identifier for this authentication event (e.g. a session ID, transaction ID, or audit log reference). Used for reconciliation. ### Returns The response is a [wallet action](/wallets/actions/overview) object with `type: "transfer"`: **id** `string` The wallet action ID. Use this to poll for status updates via `GET /v1/wallets/{wallet_id} /actions/{action_id}`. **status** `string` The action status: `pending`, `succeeded`, `rejected`, or `failed`. **type** `'transfer'` The action type. **source\_asset** `string` The asset being transferred. **source\_amount** `string` The amount being transferred in standard units. **source\_chain** `string` The source chain. **destination\_address** `string` The recipient address. ## Next steps > **[Transfer lifecycle](/wallets/custodial-wallets/transaction-lifecycle)** > > Learn about the transfer lifecycle for custodial wallets > **[Sending USDC recipe](/recipes/send-usdc)** > > Learn how to format and encode ERC-20 token transfers > **[Authorization controls](/wallets/custodial-wallets/advanced/authorization-controls)** > > Configure policies and multi-party approvals for custodial wallets > **[Transfer webhooks](/wallets/custodial-wallets/advanced/webhooks)** > > Monitor transfer status and lifecycle events # Setup Source: https://docs.privy.io/wallets/custodial-wallets/setup > [!NOTE] > > Custodial wallets are an Enterprise plan feature. If you would like to upgrade your account, > please reach out to [sales@privy.io](mailto:sales@privy.io). In order to create and use Privy custodial wallets, you must go through the setup process with the custodian and enable Privy to create custodial wallets on your behalf. The following steps outlines the process for using Bridge as the custodian. ## Onboard with the custodian If you don’t already have a Bridge account, request access at Bridge [here](https://apidocs.bridge.xyz/get-started/introduction/quick-start/get-set-up-with-bridge). ### KYC the wallet beneficiaries A prerequisite for creating custodial wallets is that the wallet beneficiary must have been KYC'd by the custodian. Register your users with Bridge either [directly via the API or via KYC links](https://apidocs.bridge.xyz/get-started/introduction/quick-start/create-your-first-customer). The `customerId` of each beneficiary will be used as the `provider_user_id` when creating custodial wallets. > [!TIP] > > If you have already KYC'ed your users with Bridge, you do not need to KYC them again. Simply use > the `customerId` for the user as the `provider_user_id` when creating custodial wallets. ## Enabling custodial wallets on your Privy app ### Request access First, share your Privy app ID with the Privy team to get custodial wallets enabled on your account. You can find your app ID in the [Privy Dashboard](https://dashboard.privy.io/apps?page=settings). ### Generate and register Bridge API keys with Privy > [!NOTE] > > If you have already registered a Bridge API key with Privy for the onramp integration, you can > skip this step. 1. Get a Bridge API from the [Bridge dashboard](https://dashboard.bridge.xyz/). 2. Register the API key in the Privy dashboard: [Configuration > Integrations > Bridge > Production API key](https://dashboard.privy.io/apps?plugin=integrations\&page=integrations). ![Bridge API key registration](https://mintcdn.com/privy-c2af3412/4VSuXLv7V33ygiNl/images/bridge-api-keys-prod.png?fit=max\&auto=format\&n=4VSuXLv7V33ygiNl\&q=85\&s=55bf03e062917e2f5f7c1f38d380cd0f) ### Turn on custodial wallets for your specific app in the Privy dashboard Visit [Wallets > Advanced ](https://dashboard.privy.io/apps?tab=wallets\&page=wallets) and turn on the **Custodial wallets** toggle. Select Bridge as the provider. ![Custodial wallets setup](https://mintcdn.com/privy-c2af3412/EJYJ39KM0lLQxnl9/images/custodial_wallets_toggle.png?fit=max\&auto=format\&n=EJYJ39KM0lLQxnl9\&q=85\&s=3b123ae8aaabe90213bac7a872fb8787) > [!NOTE] > > If you don't see the custodial wallet settings, access to custodial wallets has not been granted > to your Privy account. Please make sure you've reached out to the Privy team and that they have > granted access. You're all set! You can now create and use custodial wallets in your Privy app. ### Next steps Now that you have enabled custodial wallets on your Privy account, you can [create a custodial wallet](/wallets/custodial-wallets/create-custodial-wallet). > **[Create a custodial wallet](/wallets/custodial-wallets/create-custodial-wallet)** > > Learn how to create and provision custodial wallets > **[Send funds](/wallets/custodial-wallets/sending-funds)** > > Execute transactions and send funds from custodial wallets > **[Transaction lifecycle](/wallets/custodial-wallets/transaction-lifecycle)** > > Learn about the transaction lifecycle for custodial wallets # Transfer lifecycle Source: https://docs.privy.io/wallets/custodial-wallets/transaction-lifecycle After initiating a transfer from a custodial wallet via `POST /v1/wallets/{wallet_id}/transfer`, the transfer is represented as a [wallet action](/wallets/actions/overview) with a `custodian_transaction` step. The custodian (e.g. Bridge) must conduct transaction screening and compliance checks before the transaction is executed on-chain. ## Action-level status The top-level wallet action status reflects the overall outcome: | Status | Description | | - | - | | `pending` | The transfer is in progress — the custodian is reviewing or executing it. | | `succeeded` | The transfer has been confirmed on-chain (terminal). | | `rejected` | The transfer was rejected before any on-chain effect, e.g. due to custodian compliance checks (terminal). Safe to retry. | | `failed` | The transfer failed after being initiated on-chain (terminal). May have partial on-chain effects. | ## Step-level status For custodial transfers, the wallet action has a single step of type `custodian_transaction`. The step status provides more granular detail about where the transfer is in its lifecycle: | Step status | Description | | - | - | | `preparing` | The transfer request is being prepared and validated. | | `queued` | The transfer is queued, waiting for a prior step to complete. | | `custodian_reviewing` | The custodian is conducting compliance screening and approval checks. The transaction has not yet been initiated on-chain. | | `pending` | The custodian has approved and initiated the transaction. It is now pending confirmation on the blockchain. | | `confirmed` | The transaction has been confirmed on-chain (terminal, success). | | `rejected` | The custodian rejected the transfer during review (terminal, failure). | | `failed` | The transaction failed after being initiated (terminal, failure). | ## Typical flow ``` preparing → custodian_reviewing → pending → confirmed ``` If the custodian rejects the transfer during review, the step transitions directly to `rejected`: ``` preparing → custodian_reviewing → rejected ``` ## Tracking status Use the wallet action ID returned from the transfer request to poll for status: ```bash theme={"system"} GET /v1/wallets/{wallet_id}/actions/{action_id} ``` > [!TIP] > > Add `?include=steps` to the request to see step-level status details for debugging. Or subscribe to [webhooks](/wallets/custodial-wallets/advanced/webhooks) to receive push notifications when the transfer reaches a terminal state. ## Next steps > **[Transfer webhooks](/wallets/custodial-wallets/advanced/webhooks)** > > Monitor transfer status and lifecycle events > **[Authorization controls](/wallets/custodial-wallets/advanced/authorization-controls)** > > Configure policies and multi-party approvals for custodial wallets # Create a crypto deposit account Source: https://docs.privy.io/wallets/funding/crypto-deposits/create-deposit-account Get reusable crypto deposit addresses for a wallet from React, React Native, NodeJS, or the REST API. A crypto deposit account gives a destination wallet reusable deposit addresses. Each address accepts the assets your app chooses, and Privy converts deposits to one target asset. Privy returns one deposit address for each source chain family in the request, EVM or Solana. Before creating a deposit account, [enable app-pays gas sponsorship](/wallets/funding/crypto-deposits/setup) on each source chain. The destination must be a Privy wallet owned by a single user. **React** Use `createCryptoDepositAccount` from the `useHeadlessCryptoDeposit` hook. The hook signs the request with the authenticated user's signer. > [!WARNING] > > `useHeadlessCryptoDeposit` is experimental and requires `@privy-io/react-auth` 3.46.0 or later. > The interface may change without a major SDK version bump. ```tsx {skip-check} theme={"system"} import {useHeadlessCryptoDeposit} from '@privy-io/react-auth'; const {createCryptoDepositAccount} = useHeadlessCryptoDeposit(); // Accept USDC on Base and deliver PathUSD on Tempo const {deposit_accounts} = await createCryptoDepositAccount({ walletId: '', type: 'inline_route', source: { mode: 'include', values: [{asset: 'usdc', chain: 'base'}] }, destination: {asset: 'pathusd', chain: 'tempo'} }); ``` To use the destination wallet's own address as the deposit address, set `depositAddressStrategy`: ```tsx {skip-check} theme={"system"} const {deposit_accounts} = await createCryptoDepositAccount({ walletId: '', type: 'inline_route', depositAddressStrategy: 'prefer_destination', source: { mode: 'include', values: [{asset: 'usdc', chain: 'base'}] }, destination: {asset: 'pathusd', chain: 'tempo'} }); ``` To reuse an existing deposit configuration, pass its ID: ```tsx {skip-check} theme={"system"} const {deposit_accounts} = await createCryptoDepositAccount({ walletId: '', type: 'deposit_config', depositConfigId: '' }); ``` **Parameters** `createCryptoDepositAccount` accepts an object with the following fields: **walletId** `string` *required* ID of the destination wallet. The authenticated user must own this wallet. **type** `'inline_route' | 'deposit_config'` *required* `inline_route` defines the route in the request with `source` and `destination`. `deposit_config` reuses an existing deposit configuration with `depositConfigId`. **source** `object` Required when `type` is `inline_route`. Assets the deposit addresses accept. Chains must be EVM or Solana. **properties** **source.mode** `'all' | 'include' | 'exclude'` *required* `include` accepts only the listed assets. `exclude` accepts all except the listed assets. `all` accepts every supported asset. **source.values** `object[]` Assets for `include` and `exclude`. Omit when `mode` is `all`. **properties** **source.values.asset** `string` Asset alias or token contract address, such as `usdc` or `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`. **source.values.chain** `string` Chain alias or CAIP-2 identifier, such as `base` or `eip155:8453`. Omit to match every supported chain for that asset. **destination** `object` Required when `type` is `inline_route`. The one asset, on one chain, delivered to the destination wallet. **properties** **destination.asset** `string` *required* Asset alias or token contract address, such as `usdc` or `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`. **destination.chain** `string` *required* Chain alias or CAIP-2 identifier, such as `base` or `eip155:8453`. Must be in the destination wallet's chain family. For example, `tempo` matches an Ethereum wallet. **depositConfigId** `string` Required when `type` is `deposit_config`. ID of an existing deposit configuration. **depositAddressStrategy** `'dedicated' | 'prefer_destination' | 'require_destination'` Where deposit addresses live. Defaults to `dedicated`, which always uses separate source wallets. See [where deposit addresses live](/wallets/funding/crypto-deposits/overview#where-deposit-addresses-live). **Returns** `createCryptoDepositAccount` returns a Promise for an object with a `deposit_accounts` array. See [response](#response). **Complete example** ```tsx {skip-check} theme={"system"} import {useState} from 'react'; import {useHeadlessCryptoDeposit} from '@privy-io/react-auth'; export const CryptoDepositPanel = ({walletId}: {walletId: string}) => { const {createCryptoDepositAccount} = useHeadlessCryptoDeposit(); const [depositAddress, setDepositAddress] = useState(null); const [isLoading, setIsLoading] = useState(false); const onCreate = async () => { setIsLoading(true); try { const {deposit_accounts} = await createCryptoDepositAccount({ walletId, type: 'inline_route', source: { mode: 'include', values: [{asset: 'usdc', chain: 'base'}] }, destination: {asset: 'pathusd', chain: 'tempo'} }); setDepositAddress(deposit_accounts[0]?.deposit_address ?? null); } catch (error) { console.error(error); } finally { setIsLoading(false); } }; return (
{depositAddress && (

Send USDC on Base to {depositAddress}

)}
); }; ``` **React Native** Use `createCryptoDepositAccount` from the `useHeadlessCryptoDeposit` hook. The hook signs the request with the authenticated user's signer. > [!WARNING] > > `useHeadlessCryptoDeposit` is experimental and requires `@privy-io/expo` 0.75.0 or later. The > interface may change without a major SDK version bump. ```tsx {skip-check} theme={"system"} import {useHeadlessCryptoDeposit} from '@privy-io/expo'; const {createCryptoDepositAccount} = useHeadlessCryptoDeposit(); // Accept USDC on Base and deliver PathUSD on Tempo const {deposit_accounts} = await createCryptoDepositAccount({ walletId: '', type: 'inline_route', source: { mode: 'include', values: [{asset: 'usdc', chain: 'base'}] }, destination: {asset: 'pathusd', chain: 'tempo'} }); ``` To use the destination wallet's own address as the deposit address, set `depositAddressStrategy`: ```tsx {skip-check} theme={"system"} const {deposit_accounts} = await createCryptoDepositAccount({ walletId: '', type: 'inline_route', depositAddressStrategy: 'prefer_destination', source: { mode: 'include', values: [{asset: 'usdc', chain: 'base'}] }, destination: {asset: 'pathusd', chain: 'tempo'} }); ``` To reuse an existing deposit configuration, pass its ID: ```tsx {skip-check} theme={"system"} const {deposit_accounts} = await createCryptoDepositAccount({ walletId: '', type: 'deposit_config', depositConfigId: '' }); ``` **Parameters** `createCryptoDepositAccount` accepts an object with the following fields: **walletId** `string` *required* ID of the destination wallet. The authenticated user must own this wallet. **type** `'inline_route' | 'deposit_config'` *required* `inline_route` defines the route in the request with `source` and `destination`. `deposit_config` reuses an existing deposit configuration with `depositConfigId`. **source** `object` Required when `type` is `inline_route`. Assets the deposit addresses accept. Chains must be EVM or Solana. **properties** **source.mode** `'all' | 'include' | 'exclude'` *required* `include` accepts only the listed assets. `exclude` accepts all except the listed assets. `all` accepts every supported asset. **source.values** `object[]` Assets for `include` and `exclude`. Omit when `mode` is `all`. **properties** **source.values.asset** `string` Asset alias or token contract address, such as `usdc` or `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`. **source.values.chain** `string` Chain alias or CAIP-2 identifier, such as `base` or `eip155:8453`. Omit to match every supported chain for that asset. **destination** `object` Required when `type` is `inline_route`. The one asset, on one chain, delivered to the destination wallet. **properties** **destination.asset** `string` *required* Asset alias or token contract address, such as `usdc` or `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`. **destination.chain** `string` *required* Chain alias or CAIP-2 identifier, such as `base` or `eip155:8453`. Must be in the destination wallet's chain family. For example, `tempo` matches an Ethereum wallet. **depositConfigId** `string` Required when `type` is `deposit_config`. ID of an existing deposit configuration. **depositAddressStrategy** `'dedicated' | 'prefer_destination' | 'require_destination'` Where deposit addresses live. Defaults to `dedicated`, which always uses separate source wallets. See [where deposit addresses live](/wallets/funding/crypto-deposits/overview#where-deposit-addresses-live). **Returns** `createCryptoDepositAccount` returns a Promise for an object with a `deposit_accounts` array. See [response](#response). **Complete example** ```tsx {skip-check} theme={"system"} import {useState} from 'react'; import {View, Text, Pressable} from 'react-native'; import {useHeadlessCryptoDeposit} from '@privy-io/expo'; export const CryptoDepositPanel = ({walletId}: {walletId: string}) => { const {createCryptoDepositAccount} = useHeadlessCryptoDeposit(); const [depositAddress, setDepositAddress] = useState(null); const [isLoading, setIsLoading] = useState(false); const onCreate = async () => { setIsLoading(true); try { const {deposit_accounts} = await createCryptoDepositAccount({ walletId, type: 'inline_route', source: { mode: 'include', values: [{asset: 'usdc', chain: 'base'}] }, destination: {asset: 'pathusd', chain: 'tempo'} }); setDepositAddress(deposit_accounts[0]?.deposit_address ?? null); } catch (error) { console.error(error); } finally { setIsLoading(false); } }; return ( {isLoading ? 'Creating...' : 'Get deposit address'} {depositAddress && Send USDC on Base to {depositAddress}} ); }; ``` **NodeJS** Use `privy.wallets().depositAccounts.crypto.create` with the destination wallet ID. The destination wallet's owner must authorize the request. Pass the user's JWT in `authorization_context`, and the SDK signs the request. Requires `@privy-io/node` 0.32.0 or later. `deposit_address_strategy` requires 0.35.0 or later. ```ts {skip-check} theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: 'insert-your-app-id', appSecret: 'insert-your-app-secret' }); // Accept USDC on Base and deliver PathUSD on Tempo const {deposit_accounts} = await privy .wallets() .depositAccounts.crypto.create('insert-destination-wallet-id', { type: 'inline_route', source: { mode: 'include', values: [{asset: 'usdc', chain: 'base'}] }, destination: {asset: 'pathusd', chain: 'tempo'}, authorization_context: {user_jwts: ['insert-user-jwt']} }); ``` To use the destination wallet's own address as the deposit address, set `deposit_address_strategy`: ```ts {skip-check} theme={"system"} const {deposit_accounts} = await privy .wallets() .depositAccounts.crypto.create('insert-destination-wallet-id', { type: 'inline_route', deposit_address_strategy: 'prefer_destination', source: { mode: 'include', values: [{asset: 'usdc', chain: 'base'}] }, destination: {asset: 'pathusd', chain: 'tempo'}, authorization_context: {user_jwts: ['insert-user-jwt']} }); ``` To reuse an existing deposit configuration, pass its ID: ```ts {skip-check} theme={"system"} const {deposit_accounts} = await privy .wallets() .depositAccounts.crypto.create('insert-destination-wallet-id', { type: 'deposit_config', deposit_config_id: 'insert-deposit-config-id', authorization_context: {user_jwts: ['insert-user-jwt']} }); ``` **Parameters** `create` accepts the destination wallet ID and an object with the following fields: **type** `'inline_route' | 'deposit_config'` *required* `inline_route` defines the route in the request with `source` and `destination`. `deposit_config` reuses an existing deposit configuration with `deposit_config_id`. **source** `object` Required when `type` is `inline_route`. Assets the deposit addresses accept. Chains must be EVM or Solana. **properties** **source.mode** `'all' | 'include' | 'exclude'` *required* `include` accepts only the listed assets. `exclude` accepts all except the listed assets. `all` accepts every supported asset. **source.values** `object[]` Assets for `include` and `exclude`. Omit when `mode` is `all`. **properties** **source.values.asset** `string` Asset alias or token contract address, such as `usdc` or `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`. **source.values.chain** `string` Chain alias or CAIP-2 identifier, such as `base` or `eip155:8453`. Omit to match every supported chain for that asset. **destination** `object` Required when `type` is `inline_route`. The one asset, on one chain, delivered to the destination wallet. **properties** **destination.asset** `string` *required* Asset alias or token contract address, such as `usdc` or `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`. **destination.chain** `string` *required* Chain alias or CAIP-2 identifier, such as `base` or `eip155:8453`. Must be in the destination wallet's chain family. For example, `tempo` matches an `ethereum` wallet. **deposit\_config\_id** `string` Required when `type` is `deposit_config`. ID of an existing deposit configuration. **deposit\_address\_strategy** `'dedicated' | 'prefer_destination' | 'require_destination'` Where deposit addresses live. Defaults to `dedicated`, which always uses separate source wallets. See [where deposit addresses live](/wallets/funding/crypto-deposits/overview#where-deposit-addresses-live). **authorization\_context** `object` *required* Authorization for the destination wallet's owner. Pass the owner's JWT in `user_jwts`. See [signing on the server](/controls/authorization-keys/using-owners/sign/signing-on-the-server). **Returns** `create` returns an object with a `deposit_accounts` array. See [response](#response). **REST API** Make a `POST` request to `/v1/wallets/{wallet_id}/deposit_accounts/crypto`, where `wallet_id` is the destination wallet ID. ```bash theme={"system"} curl --request POST https://api.privy.io/v1/wallets/{wallet_id}/deposit_accounts/crypto \ -u ":" \ -H "privy-app-id: " \ -H "privy-authorization-signature: " \ -H "Content-Type: application/json" \ -d '{ "type": "inline_route", "source": { "mode": "include", "values": [{"asset": "usdc", "chain": "base"}] }, "destination": {"asset": "pathusd", "chain": "tempo"} }' ``` The request requires an [authorization signature](/controls/authorization-keys/using-owners/sign/direct-implementation) from the destination wallet's owner. To sign for a user from a server, exchange the user's JWT for a signing key with [`/v1/wallets/authenticate`](/api-reference/wallets/authenticate). Sign the request body exactly as sent. Do not add default values after signing. To use the destination wallet's own address as the deposit address, add `deposit_address_strategy` to the body: ```json theme={"system"} { "type": "inline_route", "deposit_address_strategy": "prefer_destination", "source": { "mode": "include", "values": [{"asset": "usdc", "chain": "base"}] }, "destination": {"asset": "pathusd", "chain": "tempo"} } ``` To reuse an existing deposit configuration, pass its ID: ```json theme={"system"} { "type": "deposit_config", "deposit_config_id": "" } ``` **Parameters** The request body accepts the following fields. See the [API reference](/api-reference/crypto-deposits/create) for the complete schema. **type** `'inline_route' | 'deposit_config'` *required* `inline_route` defines the route in the request with `source` and `destination`. `deposit_config` reuses an existing deposit configuration with `deposit_config_id`. **source** `object` Required when `type` is `inline_route`. Assets the deposit addresses accept. Chains must be EVM or Solana. **properties** **source.mode** `'all' | 'include' | 'exclude'` *required* `include` accepts only the listed assets. `exclude` accepts all except the listed assets. `all` accepts every supported asset. **source.values** `object[]` Assets for `include` and `exclude`. Omit when `mode` is `all`. **properties** **source.values.asset** `string` Asset alias or token contract address, such as `usdc` or `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`. **source.values.chain** `string` Chain alias or CAIP-2 identifier, such as `base` or `eip155:8453`. Omit to match every supported chain for that asset. **destination** `object` Required when `type` is `inline_route`. The one asset, on one chain, delivered to the destination wallet. **properties** **destination.asset** `string` *required* Asset alias or token contract address, such as `usdc` or `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`. **destination.chain** `string` *required* Chain alias or CAIP-2 identifier, such as `base` or `eip155:8453`. Must be in the destination wallet's chain family. For example, `tempo` matches an `ethereum` wallet. **deposit\_config\_id** `string` Required when `type` is `deposit_config`. ID of an existing deposit configuration. **deposit\_address\_strategy** `'dedicated' | 'prefer_destination' | 'require_destination'` Where deposit addresses live. Defaults to `dedicated`, which always uses separate source wallets. See [where deposit addresses live](/wallets/funding/crypto-deposits/overview#where-deposit-addresses-live). ## Response Every SDK returns the API response unchanged, with snake\_case fields: **deposit\_accounts** `object[]` One entry per source route created for the destination wallet. **properties** **deposit\_address** `string` Address to send funds to. This is the source wallet's address. **wallet\_id** `string` ID of the source wallet. When the destination wallet is its own source, this is the destination wallet's ID. **source** `object` Assets this address accepts, using aliases when known. **destination** `object` Asset and chain delivered to the destination wallet, using aliases when known. ```json theme={"system"} { "deposit_accounts": [ { "wallet_id": "insert-source-wallet-id", "deposit_address": "0x1234...abcd", "source": { "mode": "include", "values": [{"asset": "usdc", "chain": "base"}] }, "destination": {"asset": "pathusd", "chain": "tempo"} } ] } ``` Show each `deposit_address` to users as the place to send the listed assets. ## Deposit address behavior **Destination requirements.** The destination must be a Privy wallet owned by a single user, with no authorization keys or nested key quorums. When a client SDK creates the account, the authenticated user must be that owner. **Source wallet reuse.** Repeated calls for the same route reuse the same source wallet while it remains eligible. A source wallet stays eligible while it serves only this route and has not been exported. Otherwise, Privy creates a new source wallet. **Exported wallets.** An exported wallet cannot be a source wallet. Exporting a source wallet for asset recovery stops conversions to it. An exported destination wallet can still receive converted funds from separate source wallets. **Destination reuse.** With `prefer_destination` or `require_destination`, Privy attaches the deposit route to the destination wallet. Other automations attached to the destination wallet are detached. If this route is already attached, Privy keeps it and re-enables it. The default `dedicated` strategy never changes the destination wallet's automations. > [!WARNING] > > Changing strategies can change the returned deposit address. Switching to `dedicated`, including > by omitting the option, returns a separate source wallet's address. Previous routes stay attached, > so deposits sent to earlier addresses are still converted. Always show users the latest returned > `deposit_address`. ## Error handling Create rejects on invalid configuration or failed requests. Common error cases include: * App-pays gas sponsorship is not enabled on a requested source chain. See [setup](/wallets/funding/crypto-deposits/setup). * The route is unsupported, or the destination chain is not in the destination wallet's chain family. * The destination wallet is not owned by a single user, or the request is not authorized by that user. * `require_destination` is set and the destination wallet was exported. * The signed request expires before Privy receives it. Some apps require the app secret for all wallet actions. For these apps, client SDK requests return a `401`. Create the deposit account from a server with NodeJS or the REST API instead. Your app should wrap calls in `try/catch` and show clear UI feedback. ## Next steps > **[Quote a route](/wallets/funding/crypto-deposits/quotes)** > > Preview the delivered amount before a deposit > **[Track orders](/wallets/funding/crypto-deposits/orders)** > > Optionally check conversion status after a deposit > **[Deposit modal](/wallets/funding/use-deposit-funds)** > > Show a prebuilt deposit UI in React or React Native > **[Set up crypto deposits](/wallets/funding/crypto-deposits/setup)** > > Enable app-pays gas sponsorship on source chains # Crypto deposits FAQ Source: https://docs.privy.io/wallets/funding/crypto-deposits/faq Common questions about crypto deposit source wallets, destination wallets, and recovering funds. ## Which wallet receives a crypto deposit? A deposit first arrives in a **source wallet**, whose address is the deposit address. Privy then converts the funds and delivers them to the **destination wallet**. See [how crypto deposits work](/wallets/funding/crypto-deposits/overview#how-crypto-deposits-work). By default, Privy creates a separate source wallet for the depositing user in each source chain family. The create response returns its ID as `wallet_id`. Privy reuses this wallet for later requests on the same route while it stays eligible. It appears in the user's linked accounts as an embedded wallet. If the user had no embedded wallet in that chain family, it becomes their primary one. The default `dedicated` strategy never uses the destination wallet or the user's other wallets as a source wallet. With `prefer_destination` or `require_destination`, the destination wallet is its own source wallet for its chain family. See [where deposit addresses live](/wallets/funding/crypto-deposits/overview#where-deposit-addresses-live). ## How can users recover funds from a source wallet? If your app meets the [Privy Home](/basics/get-started/dashboard/privy-home) prerequisites, users can see source wallets and their balances there. If your app also allows key export, users can export the private key and move funds with another wallet. Exporting a source wallet stops Privy from converting deposits sent to it. By default, later requests use a new source wallet, and `useDepositFunds` shows its address the next time it opens. # Track crypto deposit orders Source: https://docs.privy.io/wallets/funding/crypto-deposits/orders Optionally check whether a crypto deposit has been converted and delivered, by order ID or by timestamp. When funds arrive at a deposit address, Privy creates a [wallet action](/wallets/actions/overview) that converts them and delivers the target asset to the destination wallet. This wallet action is the deposit's **order**. The order ID is that wallet action's ID. Tracking orders is optional. Deposits are converted and delivered whether or not an app tracks them. Your app does not need to create, store, or show order IDs. Use orders to show conversion status to users or to reconcile deposits. ## Order statuses An order's status is one of the following: | Status | Meaning | | - | - | | `pending` | The conversion has not finished. It may not have started yet. | | `succeeded` | Every step completed, and the destination wallet received the funds. | | `rejected` | Privy rejected the conversion before any onchain effect. | | `failed` | The conversion failed after processing began. It may have onchain effects. | ## Get the order ID The order ID is the wallet action ID of a deposit's conversion. Not every wallet action ID is an order ID. Your app can get the order ID in any of the following ways: * **Automation webhook:** the `action_id` field of the [`wallet_automation.submitted`](/wallets/automations/webhooks) webhook. This webhook's `wallet_id` is the source wallet. Fetch the order with the destination wallet ID. * **Swap webhooks:** the `wallet_action_id` field of the conversion's [`wallet_action.swap.*`](/api-reference/webhooks/wallet-action/swap/created) webhooks. These webhooks also report status changes. * **Polling:** if your app has no order ID, [get the next order](#get-the-next-order) after a timestamp. The order endpoints keep the name `order_id`. Pass the wallet action ID as its value. ## Get an order Fetch an order by its order ID. Use the destination wallet ID from [create](/wallets/funding/crypto-deposits/create-deposit-account), not the source wallet ID. **React** Use `getOrder` from the `useHeadlessCryptoDeposit` hook. The authenticated user must own the destination wallet. > [!WARNING] > > `useHeadlessCryptoDeposit` is experimental and requires `@privy-io/react-auth` 3.46.0 or later. > The interface may change without a major SDK version bump. ```tsx {skip-check} theme={"system"} import {useHeadlessCryptoDeposit} from '@privy-io/react-auth'; const {getOrder} = useHeadlessCryptoDeposit(); const order = await getOrder({ walletId: '', orderId: '' }); ``` `getOrder` accepts an object with `walletId`, the destination wallet ID, and `orderId`. It returns a Promise for an [order](#order). It rejects when the wallet or order cannot be found. **React Native** Use `getOrder` from the `useHeadlessCryptoDeposit` hook. The authenticated user must own the destination wallet. > [!WARNING] > > `useHeadlessCryptoDeposit` is experimental and requires `@privy-io/expo` 0.75.0 or later. The > interface may change without a major SDK version bump. ```tsx {skip-check} theme={"system"} import {useHeadlessCryptoDeposit} from '@privy-io/expo'; const {getOrder} = useHeadlessCryptoDeposit(); const order = await getOrder({ walletId: '', orderId: '' }); ``` `getOrder` accepts an object with `walletId`, the destination wallet ID, and `orderId`. It returns a Promise for an [order](#order). It rejects when the wallet or order cannot be found. **NodeJS** Use `privy.wallets().depositAccounts.crypto.orders.get`. Requires `@privy-io/node` 0.35.0 or later. ```ts {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 order = await privy.wallets().depositAccounts.crypto.orders.get('insert-order-id', { wallet_id: 'insert-destination-wallet-id' }); ``` `orders.get` accepts the order ID and an object with `wallet_id`, the destination wallet ID. It returns an [order](#order). It rejects when the wallet or order cannot be found. **REST API** Make a `GET` request to `/v1/wallets/{wallet_id}/deposit_accounts/crypto/orders/{order_id}`, where `wallet_id` is the destination wallet ID. See the [API reference](/api-reference/crypto-deposits/get-order). ```bash cURL theme={"system"} curl --request GET \ https://api.privy.io/v1/wallets/{wallet_id}/deposit_accounts/crypto/orders/{order_id} \ -u ":" \ -H "privy-app-id: " ``` ```python Python theme={"system"} import os import requests app_id = os.environ["PRIVY_APP_ID"] app_secret = os.environ["PRIVY_APP_SECRET"] wallet_id = "insert-destination-wallet-id" order_id = "insert-order-id" response = requests.get( f"https://api.privy.io/v1/wallets/{wallet_id}/deposit_accounts/crypto/orders/{order_id}", auth=(app_id, app_secret), headers={"privy-app-id": app_id}, ) response.raise_for_status() order = response.json() ``` The request returns `404` when the wallet or order cannot be found. ### Order **id** `string` Order ID. This is the wallet action ID of the deposit's conversion. **status** `'pending' | 'succeeded' | 'rejected' | 'failed'` Order status. See [order statuses](#order-statuses). ## Get the next order Fetch the earliest order into the destination wallet after a timestamp. Use this to poll for a deposit's order when your app has no order ID. For example, record the time before showing a deposit address, then poll with that time. **React** Use `getNextOrder` from the `useHeadlessCryptoDeposit` hook. > [!WARNING] > > `useHeadlessCryptoDeposit` is experimental and requires `@privy-io/react-auth` 3.46.0 or later. > The interface may change without a major SDK version bump. ```tsx {skip-check} theme={"system"} import {useHeadlessCryptoDeposit} from '@privy-io/react-auth'; const {getNextOrder} = useHeadlessCryptoDeposit(); const {order} = await getNextOrder({ walletId: '', after: '2026-09-08T20:00:00.000Z' }); ``` `getNextOrder` accepts an object with `walletId` and `after`. It returns a Promise for `{order}`, where `order` is `null` when no order exists after `after`. **React Native** Use `getNextOrder` from the `useHeadlessCryptoDeposit` hook. > [!WARNING] > > `useHeadlessCryptoDeposit` is experimental and requires `@privy-io/expo` 0.75.0 or later. The > interface may change without a major SDK version bump. ```tsx {skip-check} theme={"system"} import {useHeadlessCryptoDeposit} from '@privy-io/expo'; const {getNextOrder} = useHeadlessCryptoDeposit(); const {order} = await getNextOrder({ walletId: '', after: '2026-09-08T20:00:00.000Z' }); ``` `getNextOrder` accepts an object with `walletId` and `after`. It returns a Promise for `{order}`, where `order` is `null` when no order exists after `after`. **NodeJS** Use `privy.wallets().depositAccounts.crypto.getNextOrder`. Requires `@privy-io/node` 0.35.0 or later. ```ts {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 {order} = await privy .wallets() .depositAccounts.crypto.getNextOrder('insert-destination-wallet-id', { after: '2026-09-08T20:00:00.000Z' }); ``` `getNextOrder` accepts the destination wallet ID and an object with `after`. It returns `{order}`, where `order` is `null` when no order exists after `after`. **REST API** Make a `GET` request to `/v1/wallets/{wallet_id}/deposit_accounts/crypto/next_order`, where `wallet_id` is the destination wallet ID. See the [API reference](/api-reference/crypto-deposits/get-next-order). ```bash cURL theme={"system"} curl --request GET \ "https://api.privy.io/v1/wallets/{wallet_id}/deposit_accounts/crypto/next_order?after=2026-09-08T20:00:00.000Z" \ -u ":" \ -H "privy-app-id: " ``` ```python Python theme={"system"} import os import requests app_id = os.environ["PRIVY_APP_ID"] app_secret = os.environ["PRIVY_APP_SECRET"] wallet_id = "insert-destination-wallet-id" response = requests.get( f"https://api.privy.io/v1/wallets/{wallet_id}/deposit_accounts/crypto/next_order", auth=(app_id, app_secret), headers={"privy-app-id": app_id}, params={"after": "2026-09-08T20:00:00.000Z"}, ) response.raise_for_status() order = response.json()["order"] ``` The response `order` is `null` when no order exists after `after`. **after** `string` *required* ISO 8601 timestamp in UTC, ending in `Z`. Returns the earliest order strictly after this time. **order** `object | null` An [order](#order) with `id` and `status`, or `null` if no order exists after `after`. ## Related > **[Create a deposit account](/wallets/funding/crypto-deposits/create-deposit-account)** > > Get deposit addresses for a destination wallet > **[Automation webhooks](/wallets/automations/webhooks)** > > Receive the order ID when a deposit is detected > **[Get order API](/api-reference/crypto-deposits/get-order)** > > `GET /v1/wallets/{wallet_id}/deposit_accounts/crypto/orders/{order_id}` > **[Get next order API](/api-reference/crypto-deposits/get-next-order)** > > `GET /v1/wallets/{wallet_id}/deposit_accounts/crypto/next_order` # Crypto deposits Source: https://docs.privy.io/wallets/funding/crypto-deposits/overview Accept crypto on EVM chains and Solana with reusable deposit addresses that convert funds to one target asset. Crypto deposits give a Privy wallet reusable deposit addresses. Users send supported tokens on supported chains to these addresses. Privy then swaps or bridges the funds into one target asset and delivers them to the wallet. With crypto deposits, an app can accept many assets across chains while holding a single stablecoin balance, such as PathUSD on Tempo. Privy orchestrates deposit detection, conversion, and delivery. ## How crypto deposits work ```mermaid theme={"system"} flowchart LR send[User sends crypto] --> receive[Source wallet receives funds] receive --> convert[Privy swaps or bridges] convert --> deliver[Destination wallet receives the target asset] ``` Every crypto deposit involves three pieces: | Term | Meaning | | - | - | | **Destination wallet** | The wallet where converted funds end up. Your app chooses it when creating the deposit account. | | **Source wallet** | The wallet that first receives a deposit. Funds arriving here trigger a [wallet automation](/wallets/automations/overview) that converts them. | | **Deposit address** | The address of the source wallet. Your app shows this address to users as the place to send funds. | 1. Your app [creates a crypto deposit account](/wallets/funding/crypto-deposits/create-deposit-account) for a destination wallet. The request names the assets to accept and the asset to deliver. 2. Privy returns one deposit address for each source chain family, EVM or Solana. 3. A user sends a supported asset to a deposit address. 4. Privy converts the funds and delivers the target asset to the destination wallet. ## Where deposit addresses live By default, Privy creates a separate source wallet for the depositing user in each chain family. The source wallet's address is the deposit address. Privy reuses this wallet for later requests on the same route. Alternatively, the destination wallet can be its own source wallet. The deposit address is then the destination wallet's own address, and Privy converts funds in place. This only works when the destination is in the same chain family as the deposit. The `deposit_address_strategy` option controls this choice: | Strategy | Deposit address | | - | - | | `dedicated` (default) | Always a separate source wallet. | | `prefer_destination` | The destination wallet's address for its own chain family. If the destination wallet was exported, Privy uses a separate source wallet. | | `require_destination` | The destination wallet's address for its own chain family. If the destination wallet was exported, the request fails. | Other chain families always use separate source wallets. For example, with an Ethereum destination wallet, Solana deposits still go to a separate Solana source wallet. Learn about [deposit address behavior](/wallets/funding/crypto-deposits/create-deposit-account#deposit-address-behavior), including what happens when your app changes strategies. ## SDK support | Operation | React | React Native | NodeJS | REST API | | - | - | - | - | - | | [Create a deposit account](/wallets/funding/crypto-deposits/create-deposit-account) | Experimental (3.46.0+) | Experimental (0.75.0+) | Stable (0.32.0+) | Stable | | [Quote a route](/wallets/funding/crypto-deposits/quotes) | Experimental (3.46.0+) | Experimental (0.75.0+) | Stable (0.35.0+) | Stable | | [Track orders](/wallets/funding/crypto-deposits/orders) | Experimental (3.46.0+) | Experimental (0.75.0+) | Stable (0.35.0+) | Stable | React requires `@privy-io/react-auth`, React Native requires `@privy-io/expo`, and NodeJS requires `@privy-io/node`. The `deposit_address_strategy` option requires `@privy-io/node` 0.35.0 or later. Experimental interfaces may change without a major SDK version bump. Apps on other platforms can integrate with the REST API directly. Each guide includes REST examples. To show a prebuilt deposit UI in React or React Native, use the [deposit modal](/wallets/funding/use-deposit-funds). ## Supported assets Crypto deposits support major assets through Privy's [canonical asset aliases](#canonical-asset-aliases), including USDC, USDT, ETH, and SOL. They also support many long-tail assets identified by an EVM token contract address or Solana token mint address. When funds arrive at a deposit address, Privy fetches a quote and forwards the funds to the destination when a liquidity pathway is available. Recognizing an asset or matching it with an `all` or `exclude` filter does not guarantee that it can be converted to every destination. To check whether a specific source asset, destination asset, and chain pair is currently supported, [request a deposit quote](/wallets/funding/crypto-deposits/quotes). A successful quote confirms that a route is available at that time and returns its estimated output. Route availability and liquidity can change before a later deposit. ### Canonical asset aliases Privy supports the following well-known assets natively on the chains where they are available. | Asset | API value | | - | - | | OUSD | `ousd` | | 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` | > [!TIP] > > 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. ## Supported chains Crypto deposit addresses use [wallet automations](/wallets/automations/configuration#supported-chains) to detect incoming deposits on the following mainnet chains: | Chain | CAIP-2 identifier | | - | - | | Tempo | `eip155:4217` | | Ethereum | `eip155:1` | | Base | `eip155:8453` | | Polygon | `eip155:137` | | Arbitrum | `eip155:42161` | | Optimism | `eip155:10` | | BNB Smart Chain | `eip155:56` | | Monad | `eip155:143` | | Solana | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | | Robinhood Chain | `eip155:4663` | | Plasma | `eip155:9745` | | MegaETH | `eip155:4326` | This table describes deposit detection. Conversion also requires an available liquidity pathway and [app-pays gas sponsorship](/wallets/funding/crypto-deposits/setup) on the source chain. Use the [deposit quote API](/api-reference/crypto-deposits/quote) to check a specific source asset, destination asset, and chain pair. ## Pricing and quotes [Request a deposit quote](/wallets/funding/crypto-deposits/quotes) for current pricing on a specific asset pair, chain pair, and deposit amount. The quote's estimated output previews the amount delivered after conversion. Deposit quotes are indicative. Creating a reusable address does not lock a price for future deposits. Pricing and liquidity can change before a deposit is converted, so fetch a fresh quote when showing a deposit estimate. ## Gas and fee sponsorship Gas sponsorship and swap provider fee sponsorship cover different costs. Configure them separately on the app's [**Fee sponsorship**](https://dashboard.privy.io/apps?page=fee-sponsorship) page in the Privy Dashboard. | Setting | What the app pays for | Guide | | - | - | - | | **Sponsor gas fees** | Network transaction fees on the selected chains. Gas sponsorship alone does not cover swap provider fees. | [Gas sponsorship setup](/wallets/gas-and-asset-management/gas/setup) | | **Sponsor swap provider fees** | Provider fees for supported transfers, swaps, and crypto deposits, rather than deducting those fees from the delivered amount. | [Fee sponsorship and fixed rates](/wallets/actions/transfer/fixed-rates) | Without swap provider fee sponsorship, provider fees are paid from the transferred or swapped amount. Provider fee sponsorship is configured per app and applies to all supported routes, rather than individual users or executions. Sponsorship does not expand supported assets, chains, or routes. Sponsored costs use the account's [shared prepaid credit balance or postpaid billing](/wallets/gas-and-asset-management/usage-billing/overview). Postpaid billing is available on the Enterprise plan. Crypto deposits **require app-pays gas sponsorship on every source chain**. Source wallets usually hold no native gas token, so Privy pays the gas to move deposits out of them. User-pays mode does not satisfy this requirement. Swap provider fee sponsorship is optional. Without it, conversion or bridging fees can still be deducted from the delivered amount. Follow the [crypto deposit setup guide](/wallets/funding/crypto-deposits/setup) to enable the required settings. ## Getting started > **[Set up crypto deposits](/wallets/funding/crypto-deposits/setup)** > > Enable app-pays gas sponsorship on source chains > **[Create a deposit account](/wallets/funding/crypto-deposits/create-deposit-account)** > > Get deposit addresses for a destination wallet > **[Quote a route](/wallets/funding/crypto-deposits/quotes)** > > Preview the delivered amount before a deposit > **[Track orders](/wallets/funding/crypto-deposits/orders)** > > Optionally check conversion status after a deposit > **[Deposit modal](/wallets/funding/use-deposit-funds)** > > Show a prebuilt deposit UI in React or React Native # Quote a crypto deposit route Source: https://docs.privy.io/wallets/funding/crypto-deposits/quotes Preview the amount a crypto deposit delivers after conversion, before creating a deposit address. A quote previews how much of the target asset a deposit delivers after conversion. Use quotes to check that a route is supported, or to show users an estimate before they send funds. Quotes do not require a wallet ID or an authorization signature. Quotes reflect the app's [gas and fee sponsorship settings](/wallets/funding/crypto-deposits/overview#gas-and-fee-sponsorship). Quotes are indicative. A deposit address does not lock this price for later deposits. Fetch a fresh quote when displaying an estimate, since market conditions and liquidity can change before conversion. **React** Use `getQuote` from the `useHeadlessCryptoDeposit` hook. > [!WARNING] > > `useHeadlessCryptoDeposit` is experimental and requires `@privy-io/react-auth` 3.46.0 or later. > The interface may change without a major SDK version bump. ```tsx {skip-check} theme={"system"} import {useHeadlessCryptoDeposit} from '@privy-io/react-auth'; const {getQuote} = useHeadlessCryptoDeposit(); // Quote 1.5 USDC on Base delivered as PathUSD on Tempo const quote = await getQuote({ source: {chain: 'base', asset: 'usdc'}, destination: {chain: 'tempo', asset: 'pathusd'}, inputAmount: '1.5' }); console.log(quote.estimated_output_amount); ``` **Parameters** `getQuote` accepts an object with the following fields: **source** `object` *required* Asset the user would deposit. Unlike create, both `chain` and `asset` are required. **properties** **source.chain** `string` *required* Chain alias or CAIP-2 identifier, such as `base` or `eip155:8453`. **source.asset** `string` *required* Asset alias or token contract address, such as `usdc` or `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`. **destination** `object` *required* Asset delivered to the destination wallet. **properties** **destination.chain** `string` *required* Chain alias or CAIP-2 identifier, such as `base` or `eip155:8453`. **destination.asset** `string` *required* Asset alias or token contract address, such as `usdc` or `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`. **inputAmount** `string` Amount as a decimal string in the source token's standard unit, such as `'1.5'` for 1.5 USDC. Omit to quote approximately \$50 of the source asset. **slippageBps** `number` Slippage tolerance in basis points. **Returns** `getQuote` returns a Promise for a [quote](#response). **React Native** Use `getQuote` from the `useHeadlessCryptoDeposit` hook. > [!WARNING] > > `useHeadlessCryptoDeposit` is experimental and requires `@privy-io/expo` 0.75.0 or later. The > interface may change without a major SDK version bump. ```tsx {skip-check} theme={"system"} import {useHeadlessCryptoDeposit} from '@privy-io/expo'; const {getQuote} = useHeadlessCryptoDeposit(); // Quote 1.5 USDC on Base delivered as PathUSD on Tempo const quote = await getQuote({ source: {chain: 'base', asset: 'usdc'}, destination: {chain: 'tempo', asset: 'pathusd'}, inputAmount: '1.5' }); console.log(quote.estimated_output_amount); ``` **Parameters** `getQuote` accepts an object with the following fields: **source** `object` *required* Asset the user would deposit. Unlike create, both `chain` and `asset` are required. **properties** **source.chain** `string` *required* Chain alias or CAIP-2 identifier, such as `base` or `eip155:8453`. **source.asset** `string` *required* Asset alias or token contract address, such as `usdc` or `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`. **destination** `object` *required* Asset delivered to the destination wallet. **properties** **destination.chain** `string` *required* Chain alias or CAIP-2 identifier, such as `base` or `eip155:8453`. **destination.asset** `string` *required* Asset alias or token contract address, such as `usdc` or `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`. **inputAmount** `string` Amount as a decimal string in the source token's standard unit, such as `'1.5'` for 1.5 USDC. Omit to quote approximately \$50 of the source asset. **slippageBps** `number` Slippage tolerance in basis points. **Returns** `getQuote` returns a Promise for a [quote](#response). **NodeJS** Use `privy.wallets().depositAccounts.crypto.quote`. Requires `@privy-io/node` 0.35.0 or later. ```ts {skip-check} theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: 'insert-your-app-id', appSecret: 'insert-your-app-secret' }); // Quote 1.5 USDC on Base delivered as PathUSD on Tempo const quote = await privy.wallets().depositAccounts.crypto.quote({ source: {chain: 'base', asset: 'usdc'}, destination: {chain: 'tempo', asset: 'pathusd'}, input_amount: '1.5' }); console.log(quote.estimated_output_amount); ``` **Parameters** `quote` accepts an object with the following fields: **source** `object` *required* Asset the user would deposit. Unlike create, both `chain` and `asset` are required. **properties** **source.chain** `string` *required* Chain alias or CAIP-2 identifier, such as `base` or `eip155:8453`. **source.asset** `string` *required* Asset alias or token contract address, such as `usdc` or `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`. **destination** `object` *required* Asset delivered to the destination wallet. **properties** **destination.chain** `string` *required* Chain alias or CAIP-2 identifier, such as `base` or `eip155:8453`. **destination.asset** `string` *required* Asset alias or token contract address, such as `usdc` or `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`. **input\_amount** `string` Amount as a decimal string in the source token's standard unit, such as `'1.5'` for 1.5 USDC. Omit to quote approximately \$50 of the source asset. **slippage\_bps** `number` Slippage tolerance in basis points. **Returns** `quote` returns a [quote](#response). **REST API** Make a `POST` request to `/v1/deposit_accounts/crypto/quote` with the app secret. Apps on platforms without a Privy SDK can call this endpoint from any HTTP client. ```bash cURL theme={"system"} curl --request POST https://api.privy.io/v1/deposit_accounts/crypto/quote \ -u ":" \ -H "privy-app-id: " \ -H "Content-Type: application/json" \ -d '{ "source": {"chain": "base", "asset": "usdc"}, "destination": {"chain": "tempo", "asset": "pathusd"}, "input_amount": "1.5" }' ``` ```python Python theme={"system"} import os import requests app_id = os.environ["PRIVY_APP_ID"] app_secret = os.environ["PRIVY_APP_SECRET"] response = requests.post( "https://api.privy.io/v1/deposit_accounts/crypto/quote", auth=(app_id, app_secret), headers={"privy-app-id": app_id}, json={ "source": {"chain": "base", "asset": "usdc"}, "destination": {"chain": "tempo", "asset": "pathusd"}, "input_amount": "1.5", }, ) response.raise_for_status() quote = response.json() ``` **Parameters** The request body accepts the following fields. See the [API reference](/api-reference/crypto-deposits/quote) for the complete schema. **source** `object` *required* Asset the user would deposit. Unlike create, both `chain` and `asset` are required. **properties** **source.chain** `string` *required* Chain alias or CAIP-2 identifier, such as `base` or `eip155:8453`. **source.asset** `string` *required* Asset alias or token contract address, such as `usdc` or `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`. **destination** `object` *required* Asset delivered to the destination wallet. **properties** **destination.chain** `string` *required* Chain alias or CAIP-2 identifier, such as `base` or `eip155:8453`. **destination.asset** `string` *required* Asset alias or token contract address, such as `usdc` or `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`. **input\_amount** `string` Amount as a decimal string in the source token's standard unit, such as `"1.5"` for 1.5 USDC. Omit to quote approximately \$50 of the source asset. **slippage\_bps** `number` Slippage tolerance in basis points. ## Response Every SDK returns the API response unchanged, with snake\_case fields: **input\_amount** `string` Quoted input as a decimal string in the source token's standard unit. **estimated\_output\_amount** `string` Estimated output as a decimal string in the destination token's standard unit. **created\_at** `string` ISO 8601 timestamp when the quote was created. ## Error handling The request rejects when: * the route is unsupported * the source and destination are the same asset on the same chain * the source and destination are not both mainnet or both testnet ## Related > **[Create a deposit account](/wallets/funding/crypto-deposits/create-deposit-account)** > > Get deposit addresses for a destination wallet > **[Track orders](/wallets/funding/crypto-deposits/orders)** > > Optionally check conversion status after a deposit # Set up crypto deposits Source: https://docs.privy.io/wallets/funding/crypto-deposits/setup Enable app-pays gas sponsorship on each source chain before creating crypto deposit addresses. Crypto deposits require one setting: **app-pays gas sponsorship** on every chain where the app accepts deposits. ## Why gas sponsorship is required Deposits first arrive in a [source wallet](/wallets/funding/crypto-deposits/overview#how-crypto-deposits-work). Source wallets usually hold only the deposited token, with no native token to pay gas. To move funds to the destination wallet, Privy sends a transaction from the source wallet. With app-pays sponsorship, the app covers that transaction's gas. User-pays mode does not satisfy this requirement. ## Enable app-pays gas sponsorship 1. Open the [gas sponsorship setup guide](/wallets/gas-and-asset-management/gas/setup) and enable gas sponsorship for the app. 2. Select **App pays** mode. 3. Enable sponsorship on every mainnet source chain where the app expects deposits. Gas sponsorship is required on source chains only. The destination chain does not need it unless it is also a source chain. Privy rejects requests to create a deposit account when a requested source chain is not sponsored. > [!WARNING] > > A source that accepts every asset (`mode: 'all'`), excludes assets (`mode: 'exclude'`), or omits a > chain includes every supported mainnet chain. Enable sponsorship on all [supported deposit > chains](/wallets/funding/crypto-deposits/overview#supported-chains) for these sources, or list > specific chains in `source.values`. ## Optional fee sponsorship App-pays gas sponsorship covers network transaction fees. It does not cover swap provider fees. To cover those costs, enable **Sponsor swap provider fees** on the app's [**Fee sponsorship**](https://dashboard.privy.io/apps?page=fee-sponsorship) page. This setting applies automatically to supported crypto deposit routes. See [fee sponsorship and fixed rates](/wallets/actions/transfer/fixed-rates) for billing and setup. [Request a deposit quote](/wallets/funding/crypto-deposits/quotes) after configuring the app to preview the delivered amount. Without provider fee sponsorship, provider fees can reduce that amount even when gas is sponsored. ## Next steps > **[Create a deposit account](/wallets/funding/crypto-deposits/create-deposit-account)** > > Get deposit addresses for a destination wallet > **[Deposit modal](/wallets/funding/use-deposit-funds)** > > Show a prebuilt deposit UI in React or React Native > **[Quote a route](/wallets/funding/crypto-deposits/quotes)** > > Preview the delivered amount before a deposit > **[Swap routing](/wallets/actions/swap/setup)** > > Optionally customize routing for incoming deposits # Create a deposit account Source: https://docs.privy.io/wallets/funding/fiat-deposits/create-deposit-account Provision a dedicated bank account for a wallet to receive fiat deposits A deposit account gives a wallet a dedicated set of bank details. Creating one requires a verified entity on the wallet — see [setup](/wallets/funding/fiat-deposits/setup). **REST API** To create a fiat deposit account for a wallet, make a `POST` request to: ```bash theme={"system"} https://api.privy.io/v1/wallets/{wallet_id}/deposit_accounts/fiat ``` See the [API reference](/api-reference/fiat/deposit-accounts/create) for the full request and response schema. In the body of the request, include the following fields: **provider** `'bridge'` *required* Provider to orchestrate the deposit account with. **environment** `'production' | 'sandbox'` Provider environment to use. Defaults to `production`. **source** `object` *required* The fiat the account receives. **properties** **source.currency** `string` *required* Fiat currency to receive deposits in, such as `usd` or `eur`. Privy returns the payment rails available for that currency in the response. **destination** `object` *required* The crypto the deposit is converted into and delivered as. **properties** **destination.asset** `string` *required* Asset to deliver to the wallet, such as `usdc`. **destination.chain** `string` *required* Chain to deliver the asset on, such as `tempo`, `base`, or `solana`. The chain must match the wallet's `chain_type` — an `ethereum` wallet cannot receive deposits on Solana. > [!NOTE] > > Deposit accounts are persistent, billed resources at the provider. Pass an `Idempotency-Key` > header to guarantee a retried request returns the same account instead of provisioning a second > one. Below is a sample cURL command for this request: ```bash theme={"system"} curl --request POST https://api.privy.io/v1/wallets/{wallet_id}/deposit_accounts/fiat \ -u ":" \ -H "privy-app-id: " \ -H 'Content-Type: application/json' \ -H 'Idempotency-Key: ' \ -d '{ "provider": "bridge", "environment": "sandbox", "source": { "currency": "usd" }, "destination": { "asset": "usdc", "chain": "tempo" } }' ``` A successful response includes the following fields: **fiat\_deposit\_account** `object` The deposit account. **properties** **id** `string` Unique ID of the deposit account. Referenced by deposit webhooks. **wallet\_id** `string` ID of the wallet deposits are delivered to. **provider** `'bridge'` Provider orchestrating the account. **environment** `'production' | 'sandbox'` Provider environment the account belongs to. **status** `'activated' | 'deactivated'` Whether the account can currently receive deposits. **source** `object` The fiat `currency` the account receives, and the `payment_rails` available to fund it. **destination** `object` The `asset` and `chain` deposits are delivered as. **deposit\_instructions** `object | null` Bank details the user sends funds to. Fields vary by currency and rail: US accounts return `bank_routing_number` and `bank_account_number`, SEPA returns `iban` and `bic`. `null` while the account is still being provisioned. **created\_at** `string` When the account was created, as an ISO 8601 timestamp. ```json theme={"system"} { "fiat_deposit_account": { "id": "da_xxxxx", "wallet_id": "dk3kud7kfo2jcnc5anoovdim", "provider": "bridge", "environment": "sandbox", "status": "activated", "source": { "currency": "usd", "payment_rails": ["ach_push", "wire"] }, "destination": { "asset": "usdc", "chain": "tempo" }, "deposit_instructions": { "bank_name": "Lead Bank", "bank_address": "1801 Main St., Kansas City, MO 64108", "bank_routing_number": "101019644", "bank_account_number": "1234567890", "bank_beneficiary_name": "Acme, Inc.", "payment_rails": ["ach_push", "wire"] }, "created_at": "2026-08-03T12:00:00Z" } } ``` **NodeJS** Use the `create` method from the `depositAccounts.fiat` resource on `wallets()`. ```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 {fiat_deposit_account: account} = await privy .wallets() .depositAccounts.fiat.create('', { provider: 'bridge', environment: 'sandbox', source: {currency: 'usd'}, destination: {asset: 'usdc', chain: 'tempo'} }); // account.deposit_instructions holds the bank details to display ``` **Python** Use `deposit_accounts.fiat.create` on the wallets service. ```python theme={"system"} response = client.wallets.deposit_accounts.fiat.create( "wallet-id", provider="bridge", environment="sandbox", source={"currency": "usd"}, destination={"asset": "usdc", "chain": "tempo"}, ) account = response.fiat_deposit_account ``` Display `deposit_instructions` to the user so they can send a transfer from their bank. No memo or reference code is required — the account is dedicated to this wallet. ## Next steps > **[Get deposit accounts](/wallets/funding/fiat-deposits/get-deposit-accounts)** > > Read a wallet's deposit accounts and their bank details > **[Deposit lifecycle](/wallets/funding/fiat-deposits/deposit-lifecycle)** > > Follow a deposit from bank transfer to on-chain settlement # Deposit lifecycle Source: https://docs.privy.io/wallets/funding/fiat-deposits/deposit-lifecycle Follow a fiat deposit from bank receipt to on-chain settlement with webhooks A deposit moves through the provider asynchronously: fiat lands in the bank account, converts to crypto, then settles on-chain. Privy emits a webhook at each stage, keyed to a stable `provider_deposit_id`. | Event | Fires when | | - | - | | `wallet.deposit_account.deposit_started` | Privy can confirm receipt of the deposit in the account, and has initiated the conversion. | | `wallet.deposit_account.deposit_completed` | Conversion finished and the crypto settled on-chain in the wallet. | | `wallet.deposit_account.deposit_failed` | Conversion failed and the fiat was refunded to the sender. | > [!NOTE] > > Webhooks can be tested at no cost in development environments. To enable webhooks in production, > upgrade to the Enterprise plan in the Privy Dashboard. Privy also emits the existing [`wallet.funds_deposited`](/wallets/gas-and-asset-management/assets/balance-event-webhooks) event when the crypto lands in the wallet. Every deposit event shares the same envelope, with the stage-specific details under `data`: **shared attributes** **type** `string` *required* Type of the webhook event. **provider\_deposit\_id** `string` The deposit's ID in the provider's system, not a Privy ID. Stable across every event for the same deposit. Omitted on `deposit_failed` when the provider never assigned one. **deposit\_account\_id** `string` *required* ID of the deposit account that received the funds. **wallet\_id** `string` *required* ID of the wallet the deposit is delivered to. **deposit\_type** `'fiat'` *required* Type of the deposit. **provider** `'bridge'` *required* Provider that orchestrated the deposit. **environment** `'production' | 'sandbox'` *required* Provider environment the deposit account belongs to. **Deposit started** **data attributes** **data.source** `object` *required* The fiat that was received. **properties** **data.source.amount** `string` *required* Fiat amount deposited, as a decimal string. **data.source.currency** `'usd' | 'eur'` *required* Currency of the fiat deposited. **data.source.payment\_rail** `string` Rail the deposit arrived over, such as `ach_push`, `wire`, `sepa`, `fednow`, or `faster_payments`. **data.source.sender\_name** `string` Name of the originator, when the rail provides it. **data.destination** `object` *required* The `asset` and `chain` the deposit is being converted into. **data.created\_at** `string` *required* When the deposit was received, as an ISO 8601 timestamp. ```json theme={"system"} { "type": "wallet.deposit_account.deposit_started", "provider_deposit_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "deposit_account_id": "da_xxxxx", "wallet_id": "dk3kud7kfo2jcnc5anoovdim", "deposit_type": "fiat", "provider": "bridge", "environment": "production", "data": { "source": { "amount": "1970.00", "currency": "usd", "payment_rail": "ach_push", "sender_name": "John Doe" }, "destination": { "asset": "usdc", "chain": "tempo" }, "created_at": "2026-08-03T14:30:00Z" } } ``` **Deposit completed** The completed event carries the same `source` as `deposit_started`, and its `destination` adds the delivered crypto amount and the settlement transaction. **data attributes** **data.source** `object` *required* The fiat that was deposited, containing `amount`, `currency`, and optionally `payment_rail` and `sender_name`. **data.destination** `object` *required* Settlement details of the delivered crypto. **properties** **data.destination.asset** `string` *required* Asset delivered to the wallet. **data.destination.chain** `string` *required* Chain the asset was delivered on. **data.destination.amount** `string` *required* Crypto amount delivered, after conversion and fees. **data.destination.transaction\_hash** `string` *required* Hash of the on-chain settlement transaction. **data.created\_at** `string` *required* When the crypto was delivered, as an ISO 8601 timestamp. ```json theme={"system"} { "type": "wallet.deposit_account.deposit_completed", "provider_deposit_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "deposit_account_id": "da_xxxxx", "wallet_id": "dk3kud7kfo2jcnc5anoovdim", "deposit_type": "fiat", "provider": "bridge", "environment": "production", "data": { "source": { "amount": "1970.00", "currency": "usd", "payment_rail": "ach_push", "sender_name": "John Doe" }, "destination": { "asset": "usdc", "chain": "tempo", "amount": "1968.40", "transaction_hash": "0xabc123..." }, "created_at": "2026-08-03T14:32:00Z" } } ``` **Deposit failed** **data attributes** **data.source** `object` *required* The fiat that was refunded, containing `amount`, `currency`, and optionally `payment_rail` and `sender_name`. **data.destination** `object` *required* The `asset` and `chain` the deposit would have been delivered as. **data.reason\_code** `string` *required* Machine-readable failure code from the provider. **data.reason** `string` *required* Human-readable explanation of the failure. **data.refunded\_at** `string` *required* When the refund settled, as an ISO 8601 timestamp. **data.created\_at** `string` *required* When the failure was reported, as an ISO 8601 timestamp. ```json theme={"system"} { "type": "wallet.deposit_account.deposit_failed", "provider_deposit_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "deposit_account_id": "da_xxxxx", "wallet_id": "dk3kud7kfo2jcnc5anoovdim", "deposit_type": "fiat", "provider": "bridge", "environment": "production", "data": { "source": { "amount": "1970.00", "currency": "usd", "payment_rail": "ach_push", "sender_name": "John Doe" }, "destination": { "asset": "usdc", "chain": "tempo" }, "reason_code": "conversion_failed", "reason": "Unable to convert funds — refund initiated", "refunded_at": "2026-08-03T14:35:00Z", "created_at": "2026-08-03T14:35:00Z" } } ``` ## Next steps > **[Handling webhook events](/user-management/users/webhooks/handling-events)** > > Configure an endpoint to receive Privy webhook events > **[Create a deposit account](/wallets/funding/fiat-deposits/create-deposit-account)** > > Provision bank details for a wallet # Get deposit accounts Source: https://docs.privy.io/wallets/funding/fiat-deposits/get-deposit-accounts Read a wallet's fiat deposit accounts and their bank details Deposit instructions never change, so your app can read them back at any time instead of storing them. **REST API** To list a wallet's fiat deposit accounts, make a `GET` request to: ```bash theme={"system"} https://api.privy.io/v1/wallets/{wallet_id}/deposit_accounts/fiat ``` See the API reference for [listing](/api-reference/fiat/deposit-accounts/list) and [reading](/api-reference/fiat/deposit-accounts/get) deposit accounts. The endpoint accepts the following query parameters: **provider** `'bridge'` *required* Provider to list accounts for. **environment** `'production' | 'sandbox'` Provider environment to list accounts for. Defaults to `production`. Below is a sample cURL command for this request: ```bash theme={"system"} curl --request GET 'https://api.privy.io/v1/wallets/{wallet_id}/deposit_accounts/fiat?provider=bridge&environment=sandbox' \ -u ":" \ -H "privy-app-id: " ``` The response contains a `deposit_accounts` array, each entry with the same fields as the create response. To read a single account, make a `GET` request to `/v1/wallets/{wallet_id}/deposit_accounts/fiat/{deposit_account_id}`. **NodeJS** Use the `list` and `get` methods from the `depositAccounts.fiat` resource on `wallets()`. ```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 {fiat_deposit_accounts: depositAccounts} = await privy .wallets() .depositAccounts.fiat.list('', { provider: 'bridge', environment: 'sandbox' }); const {fiat_deposit_account: account} = await privy .wallets() .depositAccounts.fiat.get('', {wallet_id: ''}); ``` **Python** Use `list` and `get` on `deposit_accounts.fiat`. ```python theme={"system"} accounts = client.wallets.deposit_accounts.fiat.list( "wallet-id", provider="bridge", environment="sandbox", ) account = client.wallets.deposit_accounts.fiat.get( "deposit-account-id", wallet_id="wallet-id", ) ``` ## Next steps > **[Deposit lifecycle](/wallets/funding/fiat-deposits/deposit-lifecycle)** > > Follow a deposit from bank transfer to on-chain settlement > **[Create a deposit account](/wallets/funding/fiat-deposits/create-deposit-account)** > > Provision bank details for a wallet # Fiat deposits Source: https://docs.privy.io/wallets/funding/fiat-deposits/overview Give a wallet a dedicated bank account that can be funded with a bank transfer To deposit fiat funds into a wallet, Privy creates a dedicated bank account that can be funded with simple bank transfers. Incoming fiat is converted to stablecoins and delivered on-chain automatically. The account is persistent, so it is safe and reusable: it requires no memo or reference code, and the same details work for every future deposit. Privy uses [Bridge](https://apidocs.bridge.xyz/get-started/introduction/overview) for fiat orchestration, and fiat deposits are backed by [Bridge virtual accounts](https://apidocs.bridge.xyz/platform/orchestration/virtual_accounts/virtual-account). ## How a fiat deposit works 1. For a given source fiat currency and destination cryptocurrency, a wallet is given a deposit account, which returns bank details. 2. Send a bank transfer to the generated bank account. 3. The provider converts the fiat to the configured asset and delivers it to the wallet on-chain. 4. Privy emits webhooks as the deposit progresses, so your app can reflect status without polling. > [!TIP] > > The [deposit modal](/wallets/funding/use-deposit-funds) can also collect a bank transfer. Privy > creates the fiat deposit account in that flow, so the app does not provision bank details itself. > [!TIP] > > See the [live demo](https://fiat-orchestration-demo.vercel.app/) for fiat deposits, payouts, and > KYC. [Source > code](https://github.com/privy-io/examples/tree/main/examples/privy-next-fiat-orchestration) ## Getting started > **[Setup](/wallets/funding/fiat-deposits/setup)** > > Verify the wallet's entity and configure the provider > **[Create a deposit account](/wallets/funding/fiat-deposits/create-deposit-account)** > > Provision bank details for a wallet > **[Deposit lifecycle](/wallets/funding/fiat-deposits/deposit-lifecycle)** > > Follow a deposit from bank transfer to on-chain settlement # Setup Source: https://docs.privy.io/wallets/funding/fiat-deposits/setup Verify the wallet entity and configure the provider for fiat deposits Fiat deposits require a verified entity on the wallet and a provider configured for server-side flows. ## Verify the wallet's entity A deposit account is created against 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 deposit account 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. In sandbox, Privy can auto-generate a sandbox key for you, so only the server-side checkbox needs to be selected. See [KYC and KYB setup](/kyc-kyb/setup) for the full walkthrough. ![Configure Bridge for server-side flows](https://mintcdn.com/privy-c2af3412/jeFcywMoBAht5Ddj/images/funding/bridge-configure-server-side.png?fit=max\&auto=format\&n=jeFcywMoBAht5Ddj\&q=85\&s=4f574bb44cac739777bb36e20104e5f8) > [!WARNING] > > Manage these resources through Privy only. Creating, modifying, or deleting the underlying > customers, virtual accounts, or external accounts directly in the Bridge API or Bridge dashboard > can leave Privy and Bridge out of sync and break the integration. ## Next steps > **[Create a deposit account](/wallets/funding/fiat-deposits/create-deposit-account)** > > Provision bank details for a wallet > **[Entities](/kyc-kyb/entities)** > > Learn how wallets are attributed to a verified user or organization # Deposit modal Source: https://docs.privy.io/wallets/funding/use-deposit-funds Let users fund wallets with fiat or crypto from a Privy modal in React or React Native **React** Privy provides a `useDepositFunds` hook in `@privy-io/react-auth` that opens a funding modal. Your app can use this hook to let authenticated users fund a destination wallet with fiat, crypto, or both. A crypto-only call opens the crypto deposit flow directly. A call that includes fiat shows a method picker first. ## Prerequisites For fiat, enable card and bank funding methods on the [Account Funding](https://dashboard.privy.io/apps?page=funding) page. See the [deposit configuration guide](/financial-flows/deposits/configuration). For crypto, enable app-pays gas sponsorship as described in [crypto deposit setup](/wallets/funding/crypto-deposits/setup). Crypto deposits require a Privy wallet. The hook builds the [create-deposit-account](/wallets/funding/crypto-deposits/create-deposit-account) request and signs it with the user's signer. ## Access the hook Import and initialize `useDepositFunds`: ```tsx {skip-check} theme={"system"} import {useDepositFunds} from '@privy-io/react-auth'; const {depositFunds} = useDepositFunds(); ``` ## Start a deposit flow Call `depositFunds` with a destination and at least one funding method. ```tsx {skip-check} theme={"system"} await depositFunds({ destination: { asset: 'pathusd', chain: 'tempo' }, crypto: { source: { mode: 'all' }, depositAddressStrategy: 'prefer_destination' } }); ``` The crypto-only call opens the crypto deposit modal. The user picks a source token and network. Privy then creates a [deposit account](/wallets/funding/crypto-deposits/create-deposit-account) and shows the address with an indicative quote. This example uses the destination wallet's own address as the deposit address where possible. Omit `depositAddressStrategy` to use a separate source wallet. > [!NOTE] > > `destination.chain` and `destination.asset` accept aliases or raw identifiers. Omit > `destination.wallet` to use the user's first embedded wallet on that chain. Pass both `fiat` and `crypto` to show a method picker: ```tsx {skip-check} theme={"system"} await depositFunds({ destination: { asset: 'usdc', chain: 'tempo' }, fiat: { source: { assets: ['usd', 'eur'], defaultAsset: 'usd' }, environment: 'production', defaultAmount: '50' }, crypto: { source: { mode: 'all' } } }); ``` ## Parameters `depositFunds` accepts an object with the following fields: **destination** `object` *required* Wallet and asset that receive the funds. **properties** **destination.asset** `string` *required* Asset alias or token contract address, such as `usdc` or `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`. **destination.chain** `string` *required* Chain alias or CAIP-2 identifier, such as `base` or `eip155:8453`. Must match the destination wallet's `chain_type` for crypto deposits. **destination.wallet** `string` Privy wallet ID or linked wallet address. When omitted, Privy uses the user's first embedded wallet on `destination.chain`. Crypto deposits require a Privy wallet. Fiat needs a resolvable address. **crypto** `object` Enables the crypto deposit flow. Required unless `fiat` is provided. Pass `{source: {mode: 'all'}}` to accept every supported source. **properties** **crypto.source** `object` Assets the modal lets the user send. Defaults to `{mode: 'all'}`. **properties** **crypto.source.mode** `'all' | 'include' | 'exclude'` *required* `include` accepts only the listed assets. `exclude` accepts all except the listed assets. `all` accepts every supported source. **crypto.source.values** `object[]` Asset specs for `include` and `exclude`. Omit when `mode` is `all`. **properties** **crypto.source.values.asset** `string` Asset alias or token contract address, such as `usdc` or `0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913`. **crypto.source.values.chain** `string` Chain alias or CAIP-2 identifier, such as `base` or `eip155:8453`. Omit to match every supported chain for that asset. **crypto.slippageBps** `number` Maximum slippage tolerance for the deposit route, in basis points. If omitted, Privy uses the default for the route. **crypto.depositAddressStrategy** `'dedicated' | 'prefer_destination' | 'require_destination'` Where the crypto deposit address lives. Defaults to `dedicated`, which always uses a separate source wallet. Use `prefer_destination` to use the destination wallet's own address for its chain family, or a separate source wallet if the destination was exported. Use `require_destination` to fail instead when the destination was exported. Using the destination wallet detaches other automations attached to it. See [where deposit addresses live](/wallets/funding/crypto-deposits/overview#where-deposit-addresses-live) and [deposit address behavior](/wallets/funding/crypto-deposits/create-deposit-account#deposit-address-behavior). **fiat** `object` Enables fiat funding with the card and bank onramp flow. Required unless `crypto` is provided. **properties** **fiat.source.assets** `SupportedFiatCurrency[]` Fiat source currencies your app allows. Defaults to all [supported currencies](#supported-fiat-currencies). When provided, must be non-empty. **fiat.source.defaultAsset** `SupportedFiatCurrency` Source currency selected when the flow opens. Falls back to the locale currency, then to the first item in `fiat.source.assets`. **fiat.environment** `'sandbox' | 'production'` Onramp environment for provider APIs. **fiat.defaultAmount** `string` Initial fiat amount displayed in the amount step. **fiat.preferredProvider** `'meld' | 'moonpay' | 'coinbase' | 'stripe'` Onramp provider to prefer when Privy selects the initial quote. If the provider has no quote for the selected payment method, Privy uses its first quote. If no quote matches, Privy ignores this option. At least one of `fiat` or `crypto` must be provided. ## Supported fiat currencies Once enabled in the Privy Dashboard, fiat onramps support USD, EUR, AUD, and BRL through Stripe and MoonPay. To unlock the full list of 50+ currencies below, open the [Funding page](https://dashboard.privy.io/apps?page=funding), click **Configure** on Meld, and complete its KYB. Meld adds card, bank transfer, and local payment coverage across 100+ countries. `SupportedFiatCurrency` accepts any of the following lowercase ISO 4217 codes. > **Full list of supported currencies** > > | Code | Currency | > | - | - | > | `usd` | US Dollar | > | `eur` | Euro | > | `gbp` | British Pound | > | `mxn` | Mexican Peso | > | `brl` | Brazilian Real | > | `cny` | Chinese Yuan | > | `jpy` | Japanese Yen | > | `inr` | Indian Rupee | > | `cad` | Canadian Dollar | > | `krw` | South Korean Won | > | `aud` | Australian Dollar | > | `idr` | Indonesian Rupiah | > | `sar` | Saudi Riyal | > | `try` | Turkish Lira | > | `chf` | Swiss Franc | > | `twd` | New Taiwan Dollar | > | `sek` | Swedish Krona | > | `ngn` | Nigerian Naira | > | `pln` | Polish Zloty | > | `ars` | Argentine Peso | > | `aed` | UAE Dirham | > | `thb` | Thai Baht | > | `zar` | South African Rand | > | `dkk` | Danish Krone | > | `egp` | Egyptian Pound | > | `myr` | Malaysian Ringgit | > | `sgd` | Singapore Dollar | > | `cop` | Colombian Peso | > | `php` | Philippine Peso | > | `clp` | Chilean Peso | > | `bdt` | Bangladeshi Taka | > | `vnd` | Vietnamese Dong | > | `czk` | Czech Koruna | > | `ils` | Israeli Shekel | > | `hkd` | Hong Kong Dollar | > | `nzd` | New Zealand Dollar | > | `pkr` | Pakistani Rupee | > | `ron` | Romanian Leu | > | `kzt` | Kazakhstani Tenge | > | `nok` | Norwegian Krone | > | `huf` | Hungarian Forint | > | `uah` | Ukrainian Hryvnia | > | `kwd` | Kuwaiti Dinar | > | `qar` | Qatari Riyal | > | `etb` | Ethiopian Birr | > | `mad` | Moroccan Dirham | > | `bgn` | Bulgarian Lev | > | `kes` | Kenyan Shilling | > | `npr` | Nepalese Rupee | > [!NOTE] > > Reach out to [sales@privy.io](mailto:sales@privy.io) with any questions about pricing or > additional geographic coverage. ## Return value `depositFunds` returns a Promise with one of the following results: | Result | Meaning | | - | - | | `{method: 'fiat', status: 'submitted'}` | The user completed the provider flow, then exited before Privy finished confirmation. | | `{method: 'fiat', status: 'confirmed'}` | The fiat flow reached provider confirmation, and the user completed the success step. | | `{method: 'crypto', status: 'address_shown'}` | The user viewed the crypto deposit address and closed the modal. | | `{method: 'crypto', status: 'completed'}` | Privy detected the crypto deposit and finished the flow. | ## Error handling `depositFunds` rejects on invalid configuration or incomplete flows. Common error cases include: * the call omits `destination.chain` or `destination.asset` * the call omits both `fiat` and `crypto` * the call sends an empty `fiat.source.assets` list * the user has no authenticated session * `destination.wallet` is not a Privy wallet on a crypto call * another funding flow is already in progress * the user cancels the flow * app-pays gas sponsorship is not enabled * no source tokens match `crypto.source` * `crypto.depositAddressStrategy` is `require_destination` and the destination wallet was exported * provider session, quote, or deposit account requests fail Your app should wrap calls in `try/catch` and show clear UI feedback. ## Complete example ```tsx {skip-check} theme={"system"} import {useState} from 'react'; import {useDepositFunds} from '@privy-io/react-auth'; export const DepositFundsButton = ({walletId}: {walletId: string}) => { const {depositFunds} = useDepositFunds(); const [isLoading, setIsLoading] = useState(false); const onDepositFunds = async () => { setIsLoading(true); try { const result = await depositFunds({ destination: { wallet: walletId, asset: 'usdc', chain: 'tempo' }, fiat: { source: { assets: ['usd', 'eur', 'gbp'], defaultAsset: 'usd' }, environment: 'production', defaultAmount: '50' }, crypto: { source: { mode: 'all' } } }); if (result.method === 'fiat') { // Handle submitted or confirmed fiat purchases. } if (result.method === 'crypto') { // Handle address_shown or completed crypto deposits. } } catch (error) { // Show retry UI or an error banner. console.error(error); } finally { setIsLoading(false); } }; return ( ); }; ``` ## Stripe Embedded Components onramp Privy supports [Stripe's Embedded Components for Crypto Onramp](https://docs.stripe.com/crypto/onramp/embedded-components) as a payment option within the deposit modal's fiat flow. Stripe provides an embedded UX that handles payment processing, KYC via [Link](https://link.com/), and crypto delivery directly to the user's wallet. > [!NOTE] > > Requires `@privy-io/react-auth` version **3.33.1** or later. Install the `@stripe/crypto` package as a required dependency: ```bash theme={"system"} pnpm install @stripe/crypto ``` Supported payment methods include credit, debit, Apple Pay, Google Pay, and ACH (US only). Supported destination currencies include OUSD on Tempo, Base, Ethereum, and Solana, USDC.e on Tempo, USDC on Base, Solana, Ethereum, Arbitrum, and Polygon, and USDT on Ethereum. Available in the US (excluding New York) and the EU. > **[Stripe docs](https://docs.stripe.com/crypto/onramp/embedded-components)** > > Official Stripe Embedded Components onramp documentation ### Test in sandbox mode Pass `fiat.environment: 'sandbox'` to `depositFunds` to run the flow against Stripe's test environment. No real funds move, and you can complete an end-to-end purchase with the test values below. | Field | Test value | | - | - | | Onramp amount | Less than \$200 | | Phone number | A US or EU phone number | | Phone verification code | `000000` | | Card number | `4242 4242 4242 4242` | | CVC | Any three digits | | Expiry date | Any date in the future | Set `destination.chain` to a mainnet chain. Stripe's onramp does not support testnets, so testnet chains fail even in sandbox mode. | Chain | Mainnet CAIP-2 | | - | - | | Tempo | `eip155:4217` | | Base | `eip155:8453` | | Solana | `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` | | Ethereum | `eip155:1` | | Arbitrum | `eip155:42161` | | Polygon | `eip155:137` | **React Native** > [!TIP] > > Make sure `` is mounted first, by following [this > guide](/authentication/user-authentication/ui-component). Privy provides `useFundWallet` and `useFundSolanaWallet` hooks in `@privy-io/expo/ui` that start a card-based fiat onramp flow in the Privy modal on React Native. Your app can use these hooks to let authenticated users buy crypto via MoonPay or Coinbase. `useFundWallet` funds EVM wallets, and `useFundSolanaWallet` funds Solana wallets. ## Start a fiat onramp flow **EVM** Use `useFundWallet` to fund an EVM wallet. Call `fundWallet` with the destination address, chain, amount, and optional asset configuration. ```tsx theme={"system"} import {useFundWallet} from '@privy-io/expo/ui'; import {base} from 'viem/chains'; const {fundWallet} = useFundWallet(); await fundWallet({ address: '', chain: base, amount: '50', asset: 'USDC' }); ``` ### Parameters | Parameter | Type | Description | | - | - | - | | `address` | `string` | Required. The destination wallet address to fund. | | `chain` | [`Chain`](https://viem.sh/docs/chains/introduction) | Optional. A `viem/chains` object for the network on which to fund. Defaults to the chain configured in the Dashboard. | | `asset` | `'native-currency'` \| `'USDC'` \| `{tokenAddress: string}` | Optional. The asset to fund with. Defaults to `'native-currency'`. | | `amount` | `string` | Required if `asset` is set, optional otherwise. The amount to fund as a decimal string. | | `defaultPaymentMethod` | `'card'` \| `'exchange'` | Optional. Skip payment method selection and trigger the specified flow directly. | | `card.preferredProvider` | `'coinbase'` \| `'moonpay'` | Optional. The preferred card onramp provider. | | `moonpay.useSandbox` | `boolean` | Optional. Use MoonPay sandbox mode for testing. | | `moonpay.uiConfig.accentColor` | `string` | Optional. Accent color for the MoonPay UI (hex value). | | `moonpay.uiConfig.theme` | `'light'` \| `'dark'` | Optional. Theme for the MoonPay UI. | **Solana** Use `useFundSolanaWallet` to fund a Solana wallet. Call `fundWallet` with the destination address, cluster, and amount. ```tsx theme={"system"} import {useFundSolanaWallet} from '@privy-io/expo/ui'; const {fundWallet} = useFundSolanaWallet(); await fundWallet({ address: '', cluster: {name: 'mainnet-beta'}, amount: '1', asset: 'native-currency' }); ``` ### Parameters | Parameter | Type | Description | | - | - | - | | `address` | `string` | Required. The destination Solana wallet address to fund. | | `cluster` | `SolanaCluster` | Optional. The Solana cluster to fund on. Defaults to `mainnet-beta`. | | `asset` | `'native-currency'` \| `'USDC'` | Optional. The asset to fund with. Defaults to `'native-currency'`. | | `amount` | `string` | Required if `asset` is set, optional otherwise. The amount to fund as a decimal string. | | `defaultPaymentMethod` | `'card'` \| `'exchange'` | Optional. Skip payment method selection and trigger the specified flow directly. | | `card.preferredProvider` | `'coinbase'` \| `'moonpay'` | Optional. The preferred card onramp provider. | | `moonpay.useSandbox` | `boolean` | Optional. Use MoonPay sandbox mode for testing. | | `moonpay.uiConfig.accentColor` | `string` | Optional. Accent color for the MoonPay UI (hex value). | | `moonpay.uiConfig.theme` | `'light'` \| `'dark'` | Optional. Theme for the MoonPay UI. | > [!NOTE] > > The React Native fiat onramp supports MoonPay and Coinbase as card providers. Stripe, Meld, and > the multi-provider routing available in the React `useDepositFunds` flow are not yet supported on > React Native. ## Related > **[Crypto deposits](/wallets/funding/crypto-deposits/overview)** > > Persistent deposit addresses, setup, and headless React or React Native. # Fetch deposits and withdrawals via webhook Source: https://docs.privy.io/wallets/gas-and-asset-management/assets/balance-event-webhooks Set up webhooks to receive notifications for wallet deposits and withdrawals > [!WARNING] > > The following functionality exists for [wallets reconstituted > server-side](/wallets/wallets/create/create-a-wallet). More on [Privy architecture > here](/security/wallet-infrastructure/architecture) **Privy emits webhooks whenever a wallet sends or receives a registered asset.** This helps your application stay in sync with the assets in your wallets, and easily track deposits and withdrawals. To set up deposit and/or withdrawal webhooks, follow the guide below. > [!NOTE] > > Webhooks can be tested at no cost in development environments. To enable webhooks in production, > upgrade to the Enterprise plan in the Privy Dashboard. > [!TIP] > > Deposit webhooks are available for select chains independently of their support tier. To see which > exact chains are supported, go to the [Dashboard Webhooks > page.](https://dashboard.privy.io/apps?page=webhooks) ## Setup a webhooks URL Go through our [webhooks setup guide](/api-reference/webhooks/overview#registering-an-endpoint) first, and make sure you enable the ['wallet.funds\_deposited'](/api-reference/webhooks/wallet/funds_deposited) and/or ['wallet.funds\_withdrawn'](/api-reference/webhooks/wallet/funds_withdrawn) events. You can next configure which assets Privy should emit these events for. Privy will emit a signed webhook to this URL whenever your wallets sends/receives a transaction for a registered asset, and will retry delivery if the endpoint does not successfully respond to the original webhook. Go through the [webhooks reference](/api-reference/webhooks/overview#webhook-delivery) for details on how delivery, idempotency, and retries work. ## Configure assets to track Once you've enabled the `'wallet.funds_deposited'` and/or `'wallet.funds_withdrawn'` events, you can then configure which assets you'd like to track via the Privy Dashboard. Privy supports webhooks for **native tokens** (e.g. ETH, SOL) on EVM and Solana, **ERC20 tokens** on EVM, and **SPL tokens** on Solana. #### Native tokens To configure webhooks for native token transactions, simply select the **Native token** asset type and provide the CAIP-2 chain ID for the network on which to track the native token. #### ERC20 tokens To configure webhooks for ERC20 token transfers, simply select the **ERC20 token** asset type and provide: * the contract address for the ERC20 token * the CAIP-2 chain ID for the network on which to track the ERC20 token #### SPL tokens To configure webhooks for SPL token transfers, simply select the **SPL token** asset type and provide: * the mint address for the SPL token * the CAIP-2 chain ID for the network on which to track the SPL token ## Events reference > **[Funds deposited](/api-reference/webhooks/wallet/funds_deposited)** > > Fired when funds are deposited into a wallet. > **[Funds withdrawn](/api-reference/webhooks/wallet/funds_withdrawn)** > > Fired when funds are withdrawn from a wallet. # Fetch transaction via API Source: https://docs.privy.io/wallets/gas-and-asset-management/assets/fetch-a-transaction Get transaction details by ID using the Node.js SDK transactions interface > [!WARNING] > > The following functionality exists for [wallets reconstituted > server-side](/wallets/wallets/create/create-a-wallet). More on [Privy architecture > here](/security/wallet-infrastructure/architecture) > [!NOTE] > > 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. **NodeJS** To get a transaction's details using the NodeJS SDK, use the `get` method on the `transactions()` interface of the Privy client: ### Usage ```typescript theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: 'insert-your-app-id', appSecret: 'insert-your-app-secret' }); const transaction = await privy.transactions().get('insert-transaction-id'); console.log(transaction); ``` ### Parameters and Returns Check out the [API reference](/api-reference/transactions/get) for more details. **Python** Use the `get` method on the transactions service. ```python theme={"system"} transaction = client.transactions.get("insert-transaction-id") print(transaction.status, transaction.transaction_hash) ``` See the [API reference](/api-reference/transactions/get) for parameter and return details. **Java** To get a transaction by ID, use the `getTransaction` method. ```java theme={"system"} try { TransactionRetrieveResponse response = privyClient .transactions() .retrieve('insert-transaction-id'); if (response.transaction().isPresent()) { Transaction transaction = response.transaction().get(); } } catch (APIException e) { String errorBody = e.bodyAsString().orElse(e.getMessage()); System.err.println(errorBody); } catch (Exception e) { System.err.println(e.getMessage()); } ``` ### Parameters **transactionId** `String` *required* The ID of the transaction to retrieve ### Returns The `TransactionRetrieveResponse` object contains an optional `transaction()` field, present if the transaction was retrieved successfully. **transaction()** `Optional\` The retrieved `Transaction` object. **id** `String` Unique ID of the transaction. **walletId** `String` ID for the wallet that sent the transaction. **caip2** `String` CAIP-2 chain ID for the network the transaction was broadcasted on. **transactionHash** `String` Hash for the transaction. **status** `Status` Current status of the transaction. **createdAt** `double` The creation date of the wallet, in milliseconds since midnight, January 1, 1970 UTC. **REST API** Privy supports fetching transaction status by the transaction ID. To do so, make a `GET` request to ```bash theme={"system"} https://api.privy.io/v1/transactions/ ``` replacing `` with the ID of your desired transaction. ## Response **id** `string` ID for the transaction. **wallet\_id** `string` ID for the wallet that sent the transaction. **status** `'broadcasted' | 'confirmed' | 'execution_reverted' | 'failed' | 'replaced' | 'pending' | 'provider_error'` Current status of the transaction. - `'broadcasted'` refers to when a transaction has been submitted to the network but has not yet been included in a block - `'confirmed'` refers to when a transaction has been included in a block that has been confirmed on the network. - `'execution_reverted'` refers to when a transaction has reverted in execution. - `'failed'` refers to when a transaction has been pending for too long, signaling that it will not be included on-chain. This can happen when the gas fee is too low given the current activity on the blockchain and is only triggered for chains that have a defined pending time limit (e.g. Base, Solana, Flow.). - `'replaced'` refers to when a transaction has been replaced by another transaction with the same nonce. This is only applicable to EVM chains. - `'pending'` irefers to when a custodial wallet transaction has been initiated with the custodian and is undergoing processing. - `'provider_error'` refers to when a custodial wallet transaction request has been rejected by the custodian or encountered an error. This can happen when you attempt to spend funds that haven't been fully screened by the custodian yet, or when a transaction does not meet the custodian's compliance requirements. **transaction\_hash** `string` Hash for the transaction. **caip2** `string` CAIP-2 chain ID for the network the transaction was broadcasted on. **gas\_credits\_charged** `object | null` Gas credits charged for this transaction, if sponsored; may be `null` on reads made immediately after confirmation, and always `0` on sponsored testnet transactions. See [Query gas spend](/wallets/gas-and-asset-management/gas/gas-spend#per-transaction). **properties** **value** `string` The amount as a decimal string. **currency** `'usd'` The currency of the amount. ## Example For example, you might fetch a transactions status using transaction ID using the `cURL` request below. ```bash theme={"system"} $ curl --request GET https://api.privy.io/v1/transactions/ \ -u ":" \ -H "privy-app-id: " \ -H 'Content-Type: application/json' \ ``` The response might look like ```json theme={"system"} { "id": "", "wallet_id": "fmfdj6yqly31huorjqzq38zc", "status": "confirmed", "transaction_hash": "0x2446f1fd773fbb9f080e674b60c6a033c7ed7427b8b9413cf28a2a4a6da9b56c", "caip2": "eip155:8453" } ``` **Rust** To get a transaction's details using the Rust SDK, use the `get` method on the `transactions()` interface of the Privy client: ### Usage ```rust theme={"system"} use privy_rs::PrivyClient; let client = PrivyClient::new(app_id, app_secret)?; let transaction = client.transactions().get("insert-transaction-id").await?; println!("Transaction: {:?}", transaction); ``` ### Parameters and Returns See the Rust SDK documentation for detailed parameter and return types, including embedded examples: * [TransactionsClient::get](https://docs.rs/privy-rs/latest/privy_rs/subclients/struct.TransactionsClient.html#method.get) * [Transaction](https://docs.rs/privy-rs/latest/privy_rs/generated/types/struct.Transaction.html) For REST API details, see the [API reference](/api-reference/transactions/get). # Fetch balance via API Source: https://docs.privy.io/wallets/gas-and-asset-management/assets/fetch-balance Fetch wallet balance by wallet ID using the Privy Node.js SDK > [!WARNING] > > The following functionality exists for [wallets reconstituted > server-side](/wallets/wallets/create/create-a-wallet). More on [Privy architecture > here](/security/wallet-infrastructure/architecture) **REST API** Privy supports fetching wallet balances by wallet ID. Balances can be queried for **named assets** (e.g. `eth`, `ousd`) or **custom assets** by contract address. To do so, make a `GET` request to ```bash theme={"system"} https://api.privy.io/v1/wallets//balance ``` replacing `` with the ID of your desired wallet. ## Query parameters **asset** `string | string[]` Named asset(s) to query (e.g. `eth`, `ousd`). Use together with `chain`. Cannot be used with `token`. Supported values: `usdc`, `usdc_e`, `eth`, `avax`, `pol`, `bnb`, `usdt`, `eurc`, `usdb`, `ousd`, `pathusd`, `sol`, `trx` Not every asset is deployed on every chain. If an asset has no canonical deployment on the requested chain, the request fails with a ` not supported on ` error — query that token by contract address with the `token` parameter instead. **chain** `string | string[]` Chain(s) to query named assets on. Use together with `asset`. Cannot be used with `token`. Supported mainnets: `ethereum`, `arbitrum`, `avalanche`, `base`, `bsc`, `hypercore`, `hyperevm`, `ink`, `linea`, `megaeth`, `optimism`, `polygon`, `robinhood`, `solana`, `soneium`, `tempo`, `tron`, `unichain`, `worldchain`, `zksync_era` Supported testnets: `arbitrum_sepolia`, `avalanche_fuji`, `base_sepolia`, `bsc_testnet`, `hoodi`, `ink_sepolia`, `linea_testnet`, `optimism_sepolia`, `polygon_amoy`, `robinhood_testnet`, `sepolia`, `solana_devnet`, `solana_testnet`, `tempo_testnet`, `tron_nile` **token** `string | string[]` Token contract address(es) in `chain:address` format. Use this to query balances for arbitrary ERC-20 or SPL tokens. * EVM example: `base:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913` * Solana example: `solana:EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` * Tron example: `tron:TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t` Cannot be used together with `asset`/`chain` or `include_currency`. Up to 10 tokens per request. **include\_currency** `string` Convert balances to a fiat currency. Supported values: `usd`, `eur`. Not supported when `token` is provided. ## Examples ### Named assets Fetch a named asset balance using `asset` and `chain`: ```bash theme={"system"} $ curl --request GET 'https://api.privy.io/v1/wallets//balance?asset=eth&chain=base&include_currency=usd' \ -u ":" \ -H "privy-app-id: " \ -H 'Content-Type: application/json' ``` ```json theme={"system"} { "balances": [ { "chain": "base", "asset": "eth", "raw_value": "1000000000000000000", "raw_value_decimals": 18, "display_values": { "eth": "0.001", "usd": "2.56" } } ] } ``` ### Custom tokens Fetch the balance of an arbitrary token by contract address using the `token` parameter: ```bash theme={"system"} $ curl --request GET 'https://api.privy.io/v1/wallets//balance?token=tempo:0x20c0000000000000000000006a37da5c996874be' \ -u ":" \ -H "privy-app-id: " \ -H 'Content-Type: application/json' ``` ```json theme={"system"} { "balances": [ { "chain": "tempo", "asset": "erc20:0x20c0000000000000000000006a37da5c996874be", "raw_value": "1000000", "raw_value_decimals": 6, "display_values": { "token": "1.0" } } ] } ``` ### Tron (TRX and TRC-20) Fetch TRX balance on Tron mainnet: ```bash theme={"system"} $ curl --request GET 'https://api.privy.io/v1/wallets//balance?asset=trx&chain=tron&include_currency=usd' \ -u ":" \ -H "privy-app-id: " \ -H 'Content-Type: application/json' ``` ```json theme={"system"} { "balances": [ { "chain": "tron", "asset": "trx", "raw_value": "1000000", "raw_value_decimals": 6, "display_values": { "trx": "1.0", "usd": "0.23" } } ] } ``` Fetch USDT on Tron mainnet: ```bash theme={"system"} $ curl --request GET 'https://api.privy.io/v1/wallets//balance?asset=usdt&chain=tron' \ -u ":" \ -H "privy-app-id: " \ -H 'Content-Type: application/json' ``` Fetch a custom TRC-20 token by contract address: ```bash theme={"system"} $ curl --request GET 'https://api.privy.io/v1/wallets//balance?token=tron:TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t' \ -u ":" \ -H "privy-app-id: " \ -H 'Content-Type: application/json' ``` > [!NOTE] > > Use `chain=tron_nile` for the Tron Nile testnet. Supported named Tron assets: `trx`, `usdt`, > `usdc`. All Tron assets use 6 decimal places. ### Full reference Check out the [API reference](/api-reference/wallets/get-balance) for more details. **NodeJS** To get a wallet's balance using the Node SDK, use the `balance.get` method on the `wallets()` interface of the Privy client. ### Named assets Fetch balances for named assets like `eth`, `ousd`, or `pathusd` using the `asset` and `chain` parameters: ```typescript theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: 'insert-your-app-id', appSecret: 'insert-your-app-secret' }); const balance = await privy.wallets().balance.get('insert-wallet-id', { asset: 'ousd', chain: 'tempo' }); console.log(balance); ``` ### Custom tokens Fetch balances for arbitrary ERC-20 or SPL tokens by contract address using the `token` parameter. The format is `chain:address`: ```typescript {skip-check} theme={"system"} const balance = await privy.wallets().balance.get('insert-wallet-id', { token: 'tempo:0x20c0000000000000000000006a37da5c996874be' }); console.log(balance); ``` Multiple tokens can be queried in a single request (up to 10). This example fetches OUSD and pathUSD on Tempo: ```typescript {skip-check} theme={"system"} const balance = await privy.wallets().balance.get('insert-wallet-id', { token: [ 'tempo:0x20c0000000000000000000006a37da5c996874be', 'tempo:0x20c0000000000000000000000000000000000000' ] }); console.log(balance); ``` > [!WARNING] > > The `token` parameter cannot be used together with `asset`/`chain` or `include_currency`. ### Tron Fetch TRX or TRC-20 balances on Tron using `asset` and `chain`: ```typescript {skip-check} theme={"system"} const balance = await privy.wallets().balance.get('insert-wallet-id', { asset: 'trx', chain: 'tron' }); console.log(balance); ``` Supported Tron assets: `trx`, `usdt`, `usdc`. Use `chain: 'tron_nile'` for the testnet. ### Parameters and returns Check out the [API reference](/api-reference/wallets/get-balance) for more details. **Java** To get a wallet's balance by ID, use the `getBalance` method. ### Named assets ```java theme={"system"} try { WalletBalanceResponse response = privyClient .wallets() .balance() .walletId("insert-wallet-id") .call(); if (response.object().isPresent()) { WalletBalanceResponseBody balanceBody = response.object().get(); // The response contains a list of balances (one per chain/asset combination) for (Balance balance : balanceBody.balances()) { String rawValue = balance.rawValue(); double rawValueDecimals = balance.rawValueDecimals(); System.out.println("Balance: " + rawValue); System.out.println("Balance (decimals): " + rawValueDecimals); System.out.println("Chain: " + balance.chain()); System.out.println("Asset: " + balance.asset()); } } } catch (APIException e) { String errorBody = e.bodyAsString().orElse(e.getMessage()); System.err.println(errorBody); } catch (Exception e) { System.err.println(e.getMessage()); } ``` ### Custom tokens Fetch balances for arbitrary ERC-20 or SPL tokens by contract address using the `token` parameter in `chain:address` format: ```java theme={"system"} try { WalletBalanceResponse response = privyClient .wallets() .balance() .walletId("insert-wallet-id") .token("tempo:0x20c0000000000000000000006a37da5c996874be") .call(); if (response.object().isPresent()) { WalletBalanceResponseBody balanceBody = response.object().get(); for (Balance balance : balanceBody.balances()) { System.out.println("Chain: " + balance.chain()); System.out.println("Asset: " + balance.asset()); System.out.println("Balance: " + balance.rawValue()); } } } catch (APIException e) { System.err.println(e.bodyAsString().orElse(e.getMessage())); } ``` > [!WARNING] > > The `token` parameter cannot be used together with `asset`/`chain` or `includeCurrency`. ### Parameters **walletId** `String` *required* The ID of the wallet to retrieve balance for. **token** `String` Token contract address in `chain:address` format (e.g., `tempo:0x20c0000000000000000000006a37da5c996874be`). Cannot be used with `asset`/`chain`. ### Returns The `WalletBalanceResponse` object contains balance information for the wallet. **balances()** `List\` List of balance objects for different chain/asset combinations. **rawValue()** `String` Balance value as a string in the smallest denomination to maintain precision. **rawValueDecimals()** `double` Balance value as a decimal number. **chain()** `BalanceChainType` The blockchain type (e.g., ETHEREUM, SOLANA, BASE, POLYGON). **asset()** `Asset` The asset type (e.g., NATIVE, USDC, USDT) or the token contract address for custom assets. **displayValues()** `Map\` Formatted display values for the balance in different currencies/formats. **Rust** To get a wallet's balance using the Rust SDK, use the `get_balance` method on the `wallets()` interface of the Privy client. ### Named assets ```rust theme={"system"} use privy_rs::PrivyClient; let client = PrivyClient::new(app_id, app_secret)?; // For a Solana wallet let balance = client .wallets() .balance() .get( "insert-wallet-id", &GetWalletBalanceAsset::String(GetWalletBalanceAssetString::Sol), &GetWalletBalanceChain::String(GetWalletBalanceChainString::Solana), None, // optional: currency conversion (e.g., Some(GetWalletBalanceIncludeCurrency::Usd)) ) .await?; ``` ### Custom tokens Fetch balances for arbitrary ERC-20 or SPL tokens by contract address using the `token` parameter in `chain:address` format: ```rust theme={"system"} use privy_rs::PrivyClient; let client = PrivyClient::new(app_id, app_secret)?; let balance = client .wallets() .balance() .get_with_token( "insert-wallet-id", &["tempo:0x20c0000000000000000000006a37da5c996874be"], ) .await?; ``` > [!WARNING] > > The `token` parameter cannot be used together with `asset`/`chain` or `include_currency`. ### Parameters and returns See the Rust SDK documentation for detailed parameter and return types, including embedded examples: * [WalletsBalanceClient::get](https://docs.rs/privy-rs/latest/privy_rs/subclients/struct.WalletsBalanceClient.html) * [GetWalletBalanceResponse](https://docs.rs/privy-rs/latest/privy_rs/generated/types/struct.GetWalletBalanceResponse.html) For REST API details, see the [API reference](/api-reference/wallets/get-balance). **Go** Use the `Get` method on the `Balance` service to fetch the balance of a wallet. ### Named assets ```go theme={"system"} balance, err := client.Wallets.Balance.Get( context.Background(), "wallet-id", privy.WalletBalanceGetParams{ Asset: privy.WalletBalanceGetParamsAssetUnion{ OfWalletBalanceGetsAssetString: privy.Opt(privy.WalletBalanceGetParamsAssetStringPathusd), }, Chain: privy.WalletBalanceGetParamsChainUnion{ OfWalletAssetChainNameInput: privy.Opt(privy.WalletAssetChainNameInputTempo), }, }, ) if err != nil { log.Fatalf("failed to get balance: %v", err) } fmt.Println("Balance:", balance) ``` ### Custom tokens Fetch balances for arbitrary ERC-20 or SPL tokens by contract address using the `Token` parameter in `chain:address` format: ```go theme={"system"} balance, err := client.Wallets.Balance.Get( context.Background(), "wallet-id", privy.WalletBalanceGetParams{ Token: privy.WalletBalanceGetParamsTokenUnion{ OfString: privy.String("tempo:0x20c0000000000000000000006a37da5c996874be"), }, }, ) if err != nil { log.Fatalf("failed to get balance: %v", err) } fmt.Println("Balance:", balance) ``` > [!WARNING] > > The `Token` parameter cannot be used together with `Asset`/`Chain` or `IncludeCurrency`. ### Parameters and returns See the [API reference](/api-reference/wallets/get-balance) for more details. **Python** Use the `get` method on the wallet balance service. ```python theme={"system"} balance = client.wallets.balance.get( "wallet-id", asset="pathusd", chain="tempo", ) for entry in balance.balances: print(entry.asset, entry.chain, entry.raw_value) ``` **Ruby** Use the `get` method on the `balance` service to fetch the balance of a wallet. ### Usage ```ruby theme={"system"} balance = client.wallets.balance.get( "wallet-id", asset: :pathusd, chain: :tempo ) balance.balances.each do |entry| puts(entry.asset, entry.chain, entry.raw_value) end ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/get-balance) for more details. # List transactions Source: https://docs.privy.io/wallets/gas-and-asset-management/assets/list-transactions Fetch paginated transaction history for an embedded wallet with the Swift and Android SDKs Privy's Swift and Android SDKs let an app fetch the transaction history for an embedded Ethereum or Solana wallet, with support for filtering by asset, token, or transaction hash, and paginating through results. > [!WARNING] > > Only TEE wallets support fetching transaction history. Calling `getTransactions` on a wallet that > isn't a TEE wallet returns an error. **Swift** Use the `getTransactions` method on an embedded wallet to fetch its paginated transaction history. ```swift theme={"system"} func getTransactions(_ params: GetTransactionsParams) async throws -> TransactionsPage ``` ### Usage ```swift theme={"system"} // Ethereum guard let ethereumWallet = privy.user?.embeddedEthereumWallets.first else { return } let params = GetTransactionsParams( chain: .base, assets: ["eth"], limit: 20 ) let page = try await ethereumWallet.getTransactions(params) for transaction in page.transactions { print(transaction.transactionHash ?? "pending", transaction.status) } // Fetch the next page, if any if let nextCursor = page.nextCursor { let nextPage = try await ethereumWallet.getTransactions( GetTransactionsParams(chain: .base, assets: ["eth"], limit: 20, cursor: nextCursor) ) } ``` ```swift theme={"system"} // Solana guard let solanaWallet = privy.user?.embeddedSolanaWallets.first else { return } let page = try await solanaWallet.getTransactions( GetTransactionsParams(chain: .solana, assets: ["sol"], limit: 20) ) ``` ### Parameters > [!NOTE] > > You must provide either `assets` or `tokens`. They are mutually exclusive — do not pass both. **params** `GetTransactionsParams` *required* The filters and pagination options for the transaction history request. **child attributes** **chain** `TransactionChain` *required* The chain to fetch transactions for, e.g. `.base`, `.ethereum`, `.solana`, or `.custom(chainSlug)` for other supported chains. **assets** `[String]?` *required* Filter by named asset, e.g. `["eth"]` or `["ousd"]`. Required if `tokens` is not provided. Mutually exclusive with `tokens`. **tokens** `[String]?` *required* Filter by token contract address (EVM) or mint address (Solana). Required if `assets` is not provided. Mutually exclusive with `assets`. **txHash** `String?` Filter to a single transaction by its hash. EVM hashes are `0x`-prefixed 64 hex characters; Solana hashes are 87-88 base58 characters. **limit** `Int?` The maximum number of transactions to return in this page. **cursor** `String?` A pagination cursor from a previous `TransactionsPage.nextCursor`, used to fetch the next page of results. **includeArchived** `Bool?` Whether to include transactions from archived wallets. Defaults to `false`. ### Returns **TransactionsPage** `TransactionsPage` The requested page of transaction history. **child attributes** **transactions** `[WalletTransaction]` The transactions in this page. **child attributes** **caip2** `String` The CAIP-2 chain ID for the network the transaction was broadcasted on, e.g. `eip155:8453`. **transactionHash** `String?` The hash of the transaction, if it has been broadcasted. **userOperationHash** `String?` The ERC-4337 user operation hash, for smart-contract wallets only. **status** `TransactionStatus` The current status of the transaction: `.broadcasted`, `.confirmed`, `.executionReverted`, `.failed`, `.replaced`, `.finalized`, `.providerError`, `.pending`, or `.unknown(rawValue)`. **createdAt** `Int` The creation time of the transaction, in milliseconds since the Unix epoch. **sponsored** `Bool` Whether gas for the transaction was sponsored. **privyTransactionId** `String?` Privy's internal ID for the transaction, if available. **walletId** `String` The ID of the wallet that sent the transaction. **details** `TransactionDetails?` Additional details about the transaction, present for transfer transactions. **child attributes** **type** `TransactionType` `.transferSent`, `.transferReceived`, or `.unknown(rawValue)`. **sender** `String` The address that sent the transaction. **senderPrivyUserId** `String?` The Privy user ID for the sender, if known. **recipient** `String` The address that received the transaction. **recipientPrivyUserId** `String?` The Privy user ID for the recipient, if known. **chain** `String` The chain the transaction occurred on. **asset** `String` The asset transferred, e.g. `"eth"` or a token contract/mint address. **rawValue** `String` The raw transferred amount, as a string to avoid floating-point precision loss. **rawValueDecimals** `Int` The number of decimals for `rawValue`. **displayValues** `[String: String]` Formatted, human-readable amounts for the transfer. **nextCursor** `String?` A cursor for fetching the next page of results. `nil` if there are no more results. **Android** Use the `getTransactions` method on an embedded wallet to fetch its paginated transaction history. ```kotlin theme={"system"} public suspend fun getTransactions( params: GetTransactionsParams ): Result public suspend fun getTransactions( params: GetTransactionsParams ): Result ``` ### Usage ```kotlin theme={"system"} // Ethereum val ethereumWallet = privy.user?.embeddedEthereumWallets?.first() ?: return val params = GetTransactionsParams( chain = TransactionChain.Evm.Base, assets = listOf("eth"), limit = 20, ) val result = ethereumWallet.getTransactions(params) result.fold( onSuccess = { page -> page.transactions.forEach { transaction -> println("${transaction.transactionHash}: ${transaction.status}") } // Fetch the next page, if any, reusing the same filters page.nextCursor?.let { nextCursor -> ethereumWallet.getTransactions(params.copy(cursor = nextCursor)) } }, onFailure = { error -> // Handle error } ) ``` ```kotlin theme={"system"} // Solana val solanaWallet = privy.user?.embeddedSolanaWallets?.first() ?: return val result = solanaWallet.getTransactions( GetTransactionsParams( chain = TransactionChain.Solana.Mainnet, assets = listOf("sol"), limit = 20, ) ) ``` ### Parameters > [!NOTE] > > You must provide either `assets` or `tokens`. They are mutually exclusive — do not pass both. **params** `GetTransactionsParams` *required* The filters and pagination options for the transaction history request. The type parameter is inferred automatically from the `chain` you pass in, so it doesn't need to be specified explicitly. **child attributes** **chain** `TransactionChain.Evm | TransactionChain.Solana` *required* The chain to fetch transactions for. Pass a `TransactionChain.Evm` case (e.g. `TransactionChain.Evm.Base`) when calling from an `EmbeddedEthereumWallet`, or a `TransactionChain.Solana` case (e.g. `TransactionChain.Solana.Mainnet`) when calling from an `EmbeddedSolanaWallet`. **assets** `List\?` *required* Filter by named asset, e.g. `listOf("eth")` or `listOf("ousd")`. Required if `tokens` is not provided. Mutually exclusive with `tokens`. **tokens** `List\?` *required* Filter by token contract address (EVM) or mint address (Solana). Required if `assets` is not provided. Mutually exclusive with `assets`. **txHash** `String?` Filter to a single transaction by its hash. EVM hashes are `0x`-prefixed 64 hex characters; Solana hashes are 87-88 base58 characters. **limit** `Int?` The maximum number of transactions to return in this page. **cursor** `String?` A pagination cursor from a previous `TransactionsPage.nextCursor`, used to fetch the next page of results. **includeArchived** `Boolean?` Whether to include transactions from archived wallets. Defaults to `false`. ### Returns **result** `Result\` A `Result` that, when successful, contains the requested page of transaction history. **child attributes** **transactions** `List\` The transactions in this page. **child attributes** **caip2** `String` The CAIP-2 chain ID for the network the transaction was broadcasted on, e.g. `eip155:8453`. **transactionHash** `String?` The hash of the transaction, if it has been broadcasted. **userOperationHash** `String?` The ERC-4337 user operation hash, for smart-contract wallets only. **status** `TransactionStatus` The current status of the transaction: `Broadcasted`, `Confirmed`, `ExecutionReverted`, `Failed`, `Replaced`, `Finalized`, `ProviderError`, `Pending`, or `Unknown(rawValue)`. **createdAt** `Long` The creation time of the transaction, in milliseconds since the Unix epoch. **sponsored** `Boolean` Whether gas for the transaction was sponsored. **privyTransactionId** `String?` Privy's internal ID for the transaction, if available. **walletId** `String` The ID of the wallet that sent the transaction. **details** `TransactionDetails?` Additional details about the transaction, present for transfer transactions. **child attributes** **type** `TransactionType` `TransferSent`, `TransferReceived`, or `Unknown(rawValue)`. **sender** `String` The address that sent the transaction. **senderPrivyUserId** `String?` The Privy user ID for the sender, if known. **recipient** `String` The address that received the transaction. **recipientPrivyUserId** `String?` The Privy user ID for the recipient, if known. **chain** `String` The chain the transaction occurred on. **asset** `String` The asset transferred, e.g. `"eth"` or a token contract/mint address. **rawValue** `String` The raw transferred amount, as a string to avoid floating-point precision loss. **rawValueDecimals** `Int` The number of decimals for `rawValue`. **displayValues** `Map\` Formatted, human-readable amounts for the transfer. **nextCursor** `String?` A cursor for fetching the next page of results. `null` if there are no more results. # Balances and webhooks Source: https://docs.privy.io/wallets/gas-and-asset-management/assets/overview Receive real-time notifications for deposits, withdrawals, and transactions > [!WARNING] > > The following functionality exists for [wallets reconstituted > server-side](/wallets/wallets/create/create-a-wallet). More on [Privy architecture > here](/security/wallet-infrastructure/architecture) Privy sends [webhooks](/api-reference/webhooks/overview) to notify your app when wallet activity occurs. Your app can track transaction status changes, incoming deposits, and outgoing withdrawals in real time. * [incoming deposits](/wallets/gas-and-asset-management/assets/balance-event-webhooks) * [outgoing withdrawals](/wallets/gas-and-asset-management/assets/balance-event-webhooks) * [transaction statuses](/wallets/gas-and-asset-management/assets/transaction-event-webhooks) In addition, you can use Privy's [REST API](/wallets/gas-and-asset-management/assets/fetch-balance) to query the transaction status and wallet balances. Follow the [webhook guide](/api-reference/webhooks/overview) to set up webhooks for your app. ![balances splash](https://mintcdn.com/privy-c2af3412/xvj7sxQ8SLxi5bGo/images/balances-splash.png?fit=max\&auto=format\&n=xvj7sxQ8SLxi5bGo\&q=85\&s=ad9fa007efb9df85714434190a99264f) # Fetch transaction via webhook Source: https://docs.privy.io/wallets/gas-and-asset-management/assets/transaction-event-webhooks Set up webhooks to receive notifications for wallet transaction events > [!WARNING] > > The following functionality exists for [wallets reconstituted > server-side](/wallets/wallets/create/create-a-wallet). More on [Privy architecture > here](/security/wallet-infrastructure/architecture) **Privy emits webhooks whenever the status of a transaction sent by a wallet changes.** This helps your application track the status of the transaction after it has been broadcasted to the network, and be notified when the transaction is confirmed or reverts. > [!NOTE] > > Webhooks can be tested at no cost in development environments. To enable webhooks in production, > upgrade to the Enterprise plan in the Privy Dashboard. To set up transaction webhooks, follow the guide below. ## Setup Go through our [webhooks setup guide](/api-reference/webhooks/overview#registering-an-endpoint) first, and make sure you enable the transaction events you want. Privy will emit a signed webhook to this URL whenever your wallets sends/receives a transaction for a registered asset, and will retry delivery if the endpoint does not successfully respond to the original webhook. Go through the [webhooks reference](/api-reference/webhooks/overview#webhook-delivery) for details on how delivery, idempotency, and retries work. ## Events reference > [!NOTE] > > Failures are uncommon overall, but more likely to occur on Base and Polygon than other chains > (\<1% of transactions). To ensure transactions get confirmed, follow our guide on [transaction > replacement](/recipes/speeding-up-transactions) to speed up stalled transactions. * [Transaction broadcasted](/api-reference/webhooks/transaction/broadcasted) refers to when a transaction has been submitted to the network but has not yet been included in a block. * [Transaction still pending](/api-reference/webhooks/transaction/still_pending) refers to when a transaction has been submitted to the network but is taking longer than expected to be confirmed. To ensure that the transaction gets included, listen to this webhook to trigger [transaction speed-ups](/recipes/speeding-up-transactions). * [Transaction confirmed](/api-reference/webhooks/transaction/confirmed) refers to when a transaction has been included in at least one block that has been confirmed on the network. * [Transaction execution reverted](/api-reference/webhooks/transaction/execution_reverted) refers to when a transaction has reverted in execution. * [Transaction replaced](/api-reference/webhooks/transaction/replaced) refers to when a transaction has been replaced by another transaction with the same nonce. This is only applicable to EVM chains. * [Transaction failed](/api-reference/webhooks/transaction/failed) refers to when a transaction has been pending for too long, signaling that it will not be included on-chain. This can happen when the gas fee is too low given the current activity on the blockchain and is only triggered for chains that have a defined pending time limit (e.g. Base, Solana, Flow). * [Transaction provider error](/api-reference/webhooks/transaction/provider_error) refers to when a custodial wallet transaction request has been rejected by the custodian or encountered an error. This can happen when attempting to spend funds that haven't been fully screened by the custodian yet, or when a transaction does not meet the custodian's compliance requirements. > [!TIP] > > Transaction webhooks don't include the [gas credits > charged](/wallets/gas-and-asset-management/gas/gas-spend#per-transaction) for sponsored > transactions. Fetch the transaction directly for up-to-date gas data. # Sponsoring transactions on Ethereum Source: https://docs.privy.io/wallets/gas-and-asset-management/gas/ethereum Sponsor gas fees on Ethereum using smart wallets with paymaster integration Privy makes it easy to create **smart wallets** for your users to sponsor gas fees for transactions on Ethereum. This is done by using a [paymaster](https://www.alchemy.com/docs/wallets/resources/terms#paymaster) to pay for users' gas fees. **NodeJS** This guide explains how to use smart wallets from your server to sponsor gas fees for transactions on Ethereum. Using wallets as smart wallets gives your application all the benefits of account abstraction, including gas sponsorship, with the flexibility of wallets. In this setup, an embedded wallet created on your wallet is the signer for the smart wallet. Note that the wallet's address is not the smart wallet address in this case - it simply authorizes actions taken by the smart wallet. All transactions and assets are tied to the smart wallet. > [!NOTE] > > This guide uses Kernel as the smart wallet contract provider, but your implementation can use any > smart wallet provider. ### 0: Install necessary dependencies This guide uses Privy's server SDK to create the wallet, Pimlico's `permissionless` package, and `viem`. Install these packages with the following command: ```sh theme={"system"} npm i @privy-io/node permissionless viem ``` ### 1: Create a wallet Create a `privy` client and use the `create` method from [Privy's SDK](/wallets/wallets/create/create-a-wallet) to create a wallet. If you already have a wallet you wish to use, you can skip this step. ```ts theme={"system"} import {PrivyClient} from '@privy-io/node'; // Initialize your Privy client const privy = new PrivyClient({ appId: 'your privy app id', appSecret: 'your privy app secret' }); // Create a wallet const { id: walletId, address, chain_type: chainType } = await privy.wallets().create({chain_type: 'ethereum'}); ``` ### 2: Get a `viem` `LocalAccount` for the wallet Create a viem `LocalAccount` object representing the wallet. Use the `createViemAccount` method from [Privy's SDK](/wallets/using-wallets/ethereum/web3-integrations#node-js). ```tsx theme={"system"} import {createViemAccount} from '@privy-io/node/viem'; // Create a viem account instance for a wallet const serverWalletAccount = await createViemAccount(privy, { walletId, address }); ``` > [!TIP] > > If your wallet requires an [authorization context](/controls/authorization-keys/using-owners/sign/signing-on-the-server), > you should pass it to the `createViemAccount` method like so: > > ```tsx theme={"system"} > const serverWalletAccount = await createViemAccount(privy, { > walletId, > address, > authorizationContext: { > authorization_private_keys: ['your authorization private key'] > } > }); > ``` ### 3: Create a smart wallet Use [`permissionless`](https://docs.pimlico.io/references/permissionless/) to create a smart wallet with the wallet as the signer. Learn more about creating smart wallet accounts in the `permissionless` [docs](https://docs.pimlico.io/references/permissionless/how-to/accounts/use-kernel-account). ```tsx theme={"system"} import {toKernelSmartAccount} from 'permissionless/accounts'; import {entryPoint07Address} from 'viem/account-abstraction'; const kernelSmartAccount = await toKernelSmartAccount({ client: publicClient, entryPoint: { address: entryPoint07Address, version: '0.7' }, owner: serverWalletAccount }); ``` ### 4: Create a client for the smart wallet You can use this smart wallet client to request signatures and transactions from the smart wallet. Learn more about making transactions on the client in the `permissonless` [docs](https://docs.pimlico.io/references/permissionless/how-to/accounts/use-kernel-account). ```tsx theme={"system"} import {createSmartAccountClient, walletClientToCustomSigner} from 'permissionless'; import {createPublicClient, http} from 'viem'; const bundlerUrl = 'your bundler URL'; const paymasterUrl = 'your paymaster URL'; // Create a viem public client for RPC calls const publicClient = createPublicClient({ chain: sepolia, // Replace this with the chain of your choice transport: http() // Optionally include an RPC override }); // Create the paymaster client to sponsor gas fees on transactions const paymasterClient = createPimlicoClient({ transport: http(paymasterUrl), entryPoint: kernelSmartAccount.entryPoint }); // Create the SmartAccountClient for requesting signatures and transactions (RPCs) const smartAccountClient = createSmartAccountClient({ account: kernelSmartAccount, chain: sepolia, paymaster: paymasterClient, bundlerTransport: http(bundlerUrl), userOperation: { estimateFeesPerGas: async () => (await paymasterClient.getUserOperationGasPrice()).fast } }); ``` **React** ### Gas sponsorship with paymasters With smart wallets, your app can pay for gas fees simply by registering a paymaster URL in the Privy Dashboard. Follow this guide to [setup smart wallets](/wallets/using-wallets/evm-smart-wallets/overview). Privy routes transaction requests to the registered paymaster, which pays gas instead of the user's wallet. This allows users to transact on-chain *instantly* –– even if they do not have a balance in their smart wallet. **React Native** ### Gas sponsorship with paymasters With smart wallets, your app can pay for gas fees simply by registering a paymaster URL in the Privy Dashboard. Follow this guide to [setup smart wallets](/wallets/using-wallets/evm-smart-wallets/overview). Privy routes transaction requests to the registered paymaster, which pays gas instead of the user's wallet. This allows users to transact on-chain *instantly* –– even if they do not have a balance in their smart wallet. # Query gas spend Source: https://docs.privy.io/wallets/gas-and-asset-management/gas/gas-spend Query gas credits charged per transaction and in total across wallets for monitoring sponsorship usage > [!WARNING] > > The following functionality exists for [wallets reconstituted > server-side](/wallets/wallets/create/create-a-wallet). More on [Privy architecture > here](/security/wallet-infrastructure/architecture) Your app can see the [Privy gas credits](https://dashboard.privy.io/gas-credits) charged for sponsored gas in two ways: * **Per transaction**: each sponsored transaction and wallet action step reports the gas credits charged for it. * **Across wallets**: the `GET /v1/apps/gas_spend` endpoint totals the gas credits charged across a set of wallets over a given time range. This is useful for tracking gas credit consumption per wallet, reconciling costs, and building internal usage dashboards. ## Per transaction Sponsored transactions include `gas_credits_charged`, the gas credits charged as a `value` and `currency` pair. The field is returned by: * [Get transaction](/api-reference/transactions/get) (`GET /v1/transactions/{transaction_id}`), and lookups by [reference ID](/transaction-management/transactions/reference-id). * Each step of a [wallet action](/api-reference/wallets/actions/get), when fetched with `?include=steps`. The field may be `null` on reads made immediately after confirmation. Its `value` is always `"0"` on sponsored testnet transactions. ```json Response theme={"system"} { "id": "6f9cc5aa-dfcf-4bf0-bd50-a43429cf5ee3", "wallet_id": "qvdqj2xspqpbf4cosvrcmjd6", "caip2": "eip155:4217", "transaction_hash": "0x6f95cf5ad71e2e93062926f72a4bb8f23f73d2c0d941ef3a8d52f96baf0b0f72", "status": "confirmed", "created_at": 1769556582588, "sponsored": true, "reference_id": null, "gas_credits_charged": { "value": "0.001543", "currency": "usd" } } ``` > [!TIP] > > Transaction webhooks don't include `gas_credits_charged`, and the field on wallet action webhook > steps may still be `null` when the webhook fires. Fetch the transaction or wallet action directly > for up-to-date gas data. ## Across wallets The total covers both RPC transactions and wallet actions. ### Request Send a `GET` request to `https://api.privy.io/v1/apps/gas_spend` with the following query parameters: **wallet\_ids** `string[]` *required* Array of wallet IDs to query. Must contain between 1 and 100 IDs. **start\_timestamp** `number` *required* Start of the time range in milliseconds (inclusive). **end\_timestamp** `number` *required* End of the time range in milliseconds (exclusive). The range between `start_timestamp` and `end_timestamp` must not exceed 30 days. ### Response **value** `string` Total Privy gas credits charged in USD, as a decimal string (e.g. `"12.345678"`). **currency** `string` The currency of the value. Always `"usd"`. ### Example ```bash theme={"system"} curl --request GET \ --url 'https://api.privy.io/v1/apps/gas_spend?wallet_ids=wallet-1&wallet_ids=wallet-2&start_timestamp=1712188800000&end_timestamp=1713398400000' \ --header 'Authorization: Basic ' \ --header 'privy-app-id: ' ``` ```json Response theme={"system"} { "value": "12.345678", "currency": "usd" } ``` ### Errors | Status | Description | | - | - | | 400 | Missing or invalid query parameters, or the date range exceeds 30 days | | 401 | Missing or invalid app secret | | 404 | One or more wallet IDs do not belong to the app | # Overview Source: https://docs.privy.io/wallets/gas-and-asset-management/gas/overview Sponsor transaction fees across all wallets using the Privy gas sponsorship engine. Sponsor transaction fees across all wallets using the Privy gas sponsorship engine. Powered by [Alchemy](https://www.alchemy.com/docs/wallets). Privy's **gas sponsorship** engine gives you flexible control over how transaction fees are paid across your wallets. Privy supports two modes: * **App pays** — your app covers gas fees for all users. Users transact without ever needing to hold ETH or native tokens. * **User pays** — gas fees are covered using the wallet's USDC or USDT balance, with no ETH required. Your app's gas credits are not charged. Gas sponsorship supports prepaid and [postpaid billing](/wallets/gas-and-asset-management/usage-billing/overview). Enable it from the [**Fee sponsorship**](https://dashboard.privy.io/apps?page=fee-sponsorship) page in the Dashboard. Prepaid accounts share a credit balance for gas and [swap provider fee sponsorship](/wallets/actions/transfer/fixed-rates). Postpaid accounts pay for sponsored gas through their monthly invoice. > [!NOTE] > > Ready to get started? See the [setup guide](/wallets/gas-and-asset-management/gas/setup). ![gas management splash](https://mintcdn.com/privy-c2af3412/xvj7sxQ8SLxi5bGo/images/gas-management-splash.png?fit=max\&auto=format\&n=xvj7sxQ8SLxi5bGo\&q=85\&s=ef0ae9ef9ea08a2f600e896cdec746b9) ## Supported networks **App pays** App pays sponsorship is available on all of the networks below. > **Ethereum networks** > > **Mainnets:** > > * ApeChain > * Arbitrum > * Base > * Berachain > * BNB Smart Chain > * Edge > * Ethereum > * Flow > * Fluent > * Gnosis > * Ink > * Linea > * MegaETH > * Monad > * Optimism > * Plasma > * Polygon > * Robinhood Chain > * Ronin > * Shape > * Soneium > * Stable > * Story > * Unichain > * Warden > * World Chain > * X Layer > > **Testnets:** > > * Arbitrum Sepolia > * Base Sepolia > * BNB Smart Chain Testnet > * Edge Testnet > * Fluent Testnet > * Ink Sepolia > * MegaETH Testnet (Deprecated) > * Monad Testnet > * OP Sepolia > * Polygon Amoy > * Robinhood Testnet > * Ronin Saigon > * Seismic Testnet > * Sepolia > * Shape Sepolia > * Soneium Minato > * Stable Testnet > * Unichain Sepolia > **Tempo networks** > > **Mainnets:** > > * Tempo > > **Testnets:** > > * Tempo Testnet > **Solana networks** > > **Mainnets:** > > * Solana > > **Testnets:** > > * Solana Devnet Want support for more networks? [Reach out to us!](mailto:sales@privy.io) **User pays** User pays (token gas) is supported on the following EVM chains only. Solana is not supported. | Chain | USDC | USDT | EURC | USDG | USDC.e | | :- | :-: | :-: | :-: | :-: | :-: | | Ethereum | ✓ | ✓ | ✓ | ✓ | — | | Base | ✓ | ✓ | ✓ | — | — | | Tempo | — | — | — | — | ✓ | | Optimism | ✓ | ✓ | — | — | — | | Arbitrum | ✓ | ✓ | — | — | — | | Polygon | ✓ | ✓ | — | — | ✓ | | Sepolia (testnet) | ✓ | ✓ | ✓ | ✓ | — | | Base Sepolia (testnet) | ✓ | ✓ | ✓ | — | — | | OP Sepolia (testnet) | ✓ | — | — | — | — | | Arbitrum Sepolia (testnet) | ✓ | — | — | — | — | | Polygon Amoy (testnet) | ✓ | — | — | — | — | | Tempo Moderato (testnet) | — | — | — | — | ✓ | Tempo also accepts PathUSD, OUSD, and USDT0, and Tempo Moderato also accepts PathUSD and OUSD. On Tempo, user pays mode is supported through the [transfer API](/wallets/actions/transfer/overview) only. ## How it works **App pays** ### Ethereum and EVM chains Privy leverages [EIP-7702](https://eip7702.io/) with **paymasters** to sponsor gas fees: * User wallets are upgraded to smart contracts * Paymasters cover gas costs automatically * Users can transact immediately without holding ETH or native tokens > [!NOTE] > > **Native gas sponsorship vs smart wallets** > > Our [smart contract wallets](/wallets/using-wallets/evm-smart-wallets/overview) offering > creates a separate 4337-compatible contract account, where the embedded wallet is the signer. > > Native gas sponsorship lets you: > > * Sponsor gas for embedded wallets without creating a separate contract account > * Get set up without managing extra bundler or paymaster providers > * Use a simpler developer experience — just pass `sponsor: true` > > *If you already have users with funds in smart contract wallets, we recommend continuing to > use smart contract wallets.* ### Tempo networks For Tempo transactions (type 118), Privy uses Tempo's native fee-payer signature flow: * Privy adds `fee_payer_signature` to the Tempo transaction before broadcast when `sponsor: true` * This flow is native to Tempo and does not use ERC-4337 user operations, EIP-7702 wallet upgrades, or EVM paymasters, reducing overhead for faster transactions > [!NOTE] > > Learn more about Tempo's native fee sponsorship in the [Tempo transaction > specification](https://docs.tempo.xyz/protocol/transactions/spec-tempo-transaction#fee-payer-signature-details). ### Solana and SVM chains Privy uses a **fee payer wallet** to cover transaction costs: * A Privy-managed fee payer wallet funded with SOL covers fees * The fee payer updates the transaction's `feePayer` field and latest blockhash * Users transact without needing to hold SOL ### Calculating gas fees Privy automatically handles gas fee calculation and optimization. Gas sponsorship includes the actual network gas cost plus a convenience fee for the managed service. > [!NOTE] > > Looking for more granular control over gas pricing? Consider exploring [custom > setup](/wallets/gas-and-asset-management/gas/ethereum) options. **User pays** When a user initiates a transaction, Privy: 1. **Gets a gas quote** from the Alchemy ERC-20 paymaster to estimate the maximum fee 2. **Checks the wallet balance** — verifies the wallet holds enough to cover both the transaction amount and the estimated gas fee 3. **Submits the transaction** — a paymaster approval is bundled atomically with the call or calls, as an ERC-4337 user operation 4. **Paymaster collects the fee** — after execution, the paymaster collects the exact gas fee from the wallet's configured token balance Through the [transfer API](/wallets/actions/transfer/overview), the gas token is always the token being transferred. For example, when a user sends USDC on Base, the gas fee is also deducted in USDC — no ETH is required. Through the [`eth_sendTransaction`](/api-reference/wallets/ethereum/eth-send-transaction) and [`wallet_sendCalls`](/api-reference/wallets/ethereum/wallet-send-calls) RPC methods, the gas token is named explicitly with `sponsor_options`. # Security best practices Source: https://docs.privy.io/wallets/gas-and-asset-management/gas/security Security best practices for gas sponsorship to prevent abuse and drainage of gas balances When implementing gas sponsorship, protecting the application from abuse is critical to prevent drainage of gas sponsorship balances. Privy natively offers controls to limit total spend and set logic for when to sponsor conditionally. ## Rate limiting To protect against abuse, consider implementing: * **Strict rate limits** — Set per-user and per-wallet transaction limits appropriate for the use case * **Spending pattern monitoring** — Track unusual activity and implement automatic circuit breakers for suspicious behavior * **Maximum sponsorship amounts** — Configure spending caps to limit potential losses from exploitation * **Backend transaction routing** — Privy aggressively rate limits transactions sent from the client. For more granular control over limiting, [relay transactions](/controls/authorization-keys/keys/create/user/request) from the server > [!NOTE] > > For a detailed implementation guide on custom rate limiting for spending controls, see the [custom > gas sponsorship rate limits recipe](/recipes/gas-sponsorship-rate-limits). ## Threat models Understanding common attack vectors helps prevent gas sponsorship abuse. Here are some known threat models to protect against: ### Solana rent refund vulnerability On Solana, when Associated Token Accounts (ATAs) are created during sponsored transactions, the rent deposit refund upon account closure goes to the account owner, not the fee payer. This creates a potential exploitation vector where users can profit from the rent refunds while the application pays the creation fees. **Example scenario:** A user swapping OUSD to SOL could gain approximately \$0.40 per transaction from the rent refund, effectively draining gas sponsorship funds through repeated transactions. **Why this happens:** Some APIs (like Jupiter) automatically include the `CloseAccount` instruction for transfers that will close the ATA and refund the rent to the user. ### Protection strategy If the application plans to reuse a limited set of tokens, strip the `CloseAccount` instruction from transactions to prevent users from profiting off the rent refunds. Here's an example of how to remove the `CloseAccount` instruction from a Solana transaction: ```typescript theme={"system"} import {Transaction, TransactionInstruction} from '@solana/web3.js'; function stripCloseAccountInstruction(transaction) { // Filter out any CloseAccount instructions from the transaction const filteredInstructions = transaction.instructions.filter((instruction) => { // CloseAccount instruction has discriminator 0x09 (9 in decimal) const discriminator = instruction.data[0]; return discriminator !== 0x09; }); // Create a new transaction with the filtered instructions const newTransaction = new Transaction(); for (const instruction of filteredInstructions) { newTransaction.add(instruction); } // Copy signers from original transaction newTransaction.feePayer = transaction.feePayer; newTransaction.recentBlockhash = transaction.recentBlockhash; return newTransaction; } ``` This approach keeps the ATAs open for future transactions, reducing creation costs and eliminating the rent refund exploit. ## Implementation recommendations > **Set up monitoring and alerts** > > Implement monitoring for: > > * Unusual transaction volume spikes from individual users or wallets > * Abnormal gas consumption patterns > * Repeated failed transactions (possible attack attempts) > * Total daily/weekly spending approaching configured limits > > Configure alerts to notify the team when suspicious activity is detected. > **Implement gradual limit increases** > > For new users or wallets, start with conservative transaction limits and gradually increase them based on: > > * Account age > * Transaction history > * User verification level > * Application-specific trust signals > > This minimizes risk from newly created accounts used for exploitation. > **Use transaction simulation** > > Before sponsoring transactions, simulate them to: > > * Verify they will succeed > * Check gas estimates are reasonable > * Detect unusual patterns in contract interactions > * Identify potentially malicious behavior > > This prevents wasting gas on transactions designed to fail or exploit gas estimation. > **Implement circuit breakers** > > Configure automatic circuit breakers that temporarily disable sponsorship when: > > * Spending exceeds expected thresholds > * Attack patterns are detected > * System anomalies occur > > This provides time to investigate and respond to potential threats without depleting funds. ## Security checklist Before deploying gas sponsorship to production: * [ ] Configure per-user and per-wallet transaction rate limits * [ ] Set up monitoring and alerting for unusual patterns * [ ] Implement spending caps appropriate for the use case * [ ] Review and mitigate chain-specific vulnerabilities (e.g., Solana rent refunds) * [ ] Test circuit breaker functionality * [ ] Document incident response procedures for detected abuse * [ ] Route transactions through the backend for better control * [ ] Review the [custom rate limits recipe](/recipes/gas-sponsorship-rate-limits) for advanced patterns > [!WARNING] > > Gas sponsorship can be a significant cost center if not properly protected. Implement multiple > layers of defense to prevent abuse and ensure sustainable operations. # Setting up gas sponsorship Source: https://docs.privy.io/wallets/gas-and-asset-management/gas/setup Choose how gas fees are paid — your app covers them, or users pay with stablecoins Privy supports two gas payment modes. Choose the one that fits your app: **App pays** Your app covers transaction fees through prepaid credits or postpaid billing. Users transact without needing to hold ETH or native tokens. > [!WARNING] > > Before enabling gas sponsorship in production, review the [security best > practices](/wallets/gas-and-asset-management/gas/security) to help prevent abuse and protect gas > balances. ## Getting started **1. Set up billing** For prepaid accounts, open [**Fee sponsorship**](https://dashboard.privy.io/apps?page=fee-sponsorship), select **Add credits**, and complete the purchase. These credits also cover [swap provider fee sponsorship](/wallets/actions/transfer/fixed-rates) when enabled. Manage credits and automated refills from [**Billing > Fee sponsorship**](https://dashboard.privy.io/billing?tab=fee-sponsorship). Prepaid accounts need a saved payment method to sponsor gas on mainnets. For postpaid billing, available on the Enterprise plan, [enable postpaid billing](/wallets/gas-and-asset-management/usage-billing/overview#choosing-your-plan) before configuring sponsorship. Sponsored gas fees appear on the monthly invoice. Prepaid credits are not required. **2. Enable gas sponsorship in the dashboard** Open **Fee sponsorship** in the [Privy Dashboard](https://dashboard.privy.io/apps?page=fee-sponsorship) and turn on **Sponsor gas fees**. This enables **App pays** mode for either billing model. **3. Configure chains** Under **Supported chains**, select the networks where the app will sponsor gas. Sponsored requests must use a configured chain. **4. Send transaction requests** > [!NOTE] > > Apps **must** use [TEE execution](/security/wallet-infrastructure/architecture) in order > to use our native gas sponsorship feature. Learn how to migrate > [here](/recipes/tee-wallet-migration-guide)! **Ethereum (React)** With the React SDK, use the `useSendTransaction` hook with `sponsor: true`: ```tsx highlight={12} theme={"system"} import {useSendTransaction, useWallets} from '@privy-io/react-auth'; const {sendTransaction} = useSendTransaction(); const {wallets} = useWallets(); sendTransaction( { to: '0xE3070d3e4309afA3bC9a6b057685743CF42da77C', value: 100000 }, { sponsor: true // Enable gas sponsorship } ); ``` **Tempo (React)** With the React SDK, use the Tempo `useSendTransaction` hook with `sponsor: true`: ```tsx highlight={17} theme={"system"} import {useWallets} from '@privy-io/react-auth'; import {useSendTransaction} from '@privy-io/react-auth/tempo'; const {sendTransaction} = useSendTransaction(); const {wallets} = useWallets(); const {hash} = await sendTransaction( { transaction: { type: 118, chainId: 4217, calls: [{to: '0xRecipientAddress', data: '0x'}] }, wallet: wallets[0] }, { sponsor: true } ); ``` > [!NOTE] > > For sponsored Tempo transactions, Privy adds `fee_payer_signature` before broadcast. > This flow does not use EIP-7702 wallet upgrades, ERC-4337 user operations, or EVM > paymasters. **Solana (React)** With the React SDK, use the `useSignAndSendTransaction` hook with `sponsor: true`: > [!WARNING] > > On Solana, sponsored transactions can be abused through ATA rent refunds when > transactions include `CloseAccount` instructions. Review [security best > practices](/wallets/gas-and-asset-management/gas/security) before shipping to production. ```tsx highlight={15} theme={"system"} import {useSignAndSendTransaction, useWallets} from '@privy-io/react-auth/solana'; const {signAndSendTransaction} = useSignAndSendTransaction(); const {wallets} = useWallets(); const selectedWallet = wallets[0]; const transaction = new Uint8Array([/* your encoded transaction */]); const result = await signAndSendTransaction({ transaction: transaction, wallet: selectedWallet, options: { sponsor: true // Enable gas sponsorship } }); ``` > [!NOTE] > > **Allow transactions from the client** > > To sponsor transactions from your client-side application, enable this setting in > your gas sponsorship dashboard configuration. When disabled, transactions can only > be sponsored from your server. **Ethereum (REST API)** ```bash highlight={9} 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:1", "sponsor": true, "params": { "transaction": { "to": "0xE3070d3e4309afA3bC9a6b057685743CF42da77C", "value": "0x2386F26FC10000" } } }' ``` > [!NOTE] > > To sponsor transactions initiated from a client SDK, you may [relay the > transaction](/controls/authorization-keys/keys/create/user/request) from your server. **Tempo (REST API)** ```bash highlight={9} 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": "0xRecipientAddress", "data": "0x" } ] } } }' ``` > [!NOTE] > > Passing `sponsor: true` makes Privy populate the Tempo `fee_payer_signature` before > broadcasting the transaction. **Ethereum (Node SDK)** ```typescript highlight={23} theme={"system"} import { PrivyClient } from '@privy-io/node'; const privy = new PrivyClient({ appId: 'your-app-id', appSecret: 'your-app-secret' }); const response = await privy .wallets() .ethereum() .sendTransaction(walletId, { caip2: 'eip155:1', params: { transaction: { to: '0x{address}', value: '0x2386F26FC10000', }, }, sponsor: true, }); ``` **Tempo (Node SDK)** ```typescript highlight={21} theme={"system"} import { PrivyClient } from '@privy-io/node'; const privy = new PrivyClient({ appId: 'your-app-id', appSecret: 'your-app-secret' }); const response = await privy .wallets() .ethereum() .sendTransaction(walletId, { caip2: 'eip155:4217', params: { transaction: { type: 118, calls: [ { to: '0xRecipientAddress', data: '0x', }, ], }, }, sponsor: true, }); ``` **Solana (REST API)** > [!WARNING] > > On Solana, sponsored transactions can be abused through ATA rent refunds when > transactions include `CloseAccount` instructions. Review [security best > practices](/wallets/gas-and-asset-management/gas/security) before shipping to production. ```bash highlight={9} 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": "signAndSendTransaction", "caip2": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", "sponsor": true, "params": { "transaction": "insert-base-64-encoded-serialized-transaction", "encoding": "base64" } }' ``` **Solana (Node SDK)** > [!WARNING] > > On Solana, sponsored transactions can be abused through ATA rent refunds when > transactions include `CloseAccount` instructions. Review [security best > practices](/wallets/gas-and-asset-management/gas/security) before shipping to production. ```typescript highlight={22} theme={"system"} import { PrivyClient } from '@privy-io/node'; const privy = new PrivyClient({ appId: 'your-app-id', appSecret: 'your-app-secret' }); const response = await privy .wallets() .solana() .signAndSendTransaction(walletId, { caip2: 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1', transaction, sponsor: true, }); ``` > [!NOTE] > > **Using gas sponsorship from other client SDKs** > > The `sponsor: true` parameter is currently supported in the React SDK and server-side > SDKs (REST API, Node, Rust). For other client SDKs (React Native, Swift, Android, > Flutter, Unity), you can achieve gas sponsorship by building the transaction in your > client, then [relaying it through your server](/controls/authorization-keys/keys/create/user/request) > with `sponsor: true`. > [!TIP] > > On-chain signature verification operations such as Permit2 and TransferWithAuthorization are > supported for EIP-7702 upgraded wallets via ERC-1271 signing. Learn how to enable this in the > [ERC-1271 signatures guide](/recipes/evm/erc-1271-signatures). ## Next steps > **[Transaction management](/wallets/gas-and-asset-management/gas/transaction-handling)** > > Learn how gas-sponsored transactions work and how to track transaction status with webhooks > **[Security best practices](/wallets/gas-and-asset-management/gas/security)** > > Protect your application from gas sponsorship abuse with rate limiting and threat mitigation > strategies **User pays** Gas fees are covered using the wallet's stablecoin balance (USDC, USDT, EURC, USDG, or USDC.e depending on chain) — no ETH required, and your app's gas credits are not charged. User pays mode works with the [transfer API](/wallets/actions/transfer/overview), [eth\_sendTransaction](/api-reference/wallets/ethereum/eth-send-transaction), and [wallet\_sendCalls](/api-reference/wallets/ethereum/wallet-send-calls). > [!NOTE] > > User pays mode is EVM only. It is not supported on Solana. ## Supported chains and tokens | Chain | USDC | USDT | EURC | USDG | USDC.e | | :- | :-: | :-: | :-: | :-: | :-: | | Ethereum | ✓ | ✓ | ✓ | ✓ | — | | Base | ✓ | ✓ | ✓ | — | — | | Tempo | ✓\* | ✓\* | — | — | ✓ | | Optimism | ✓ | ✓ | — | — | — | | Arbitrum | ✓ | ✓ | — | — | — | | Polygon | ✓ | ✓ | — | — | ✓ | | Sepolia (testnet) | ✓ | ✓ | ✓ | ✓ | — | | Base Sepolia (testnet) | ✓ | ✓ | ✓ | — | — | | OP Sepolia (testnet) | ✓ | — | — | — | — | | Arbitrum Sepolia (testnet) | ✓ | — | — | — | — | | Polygon Amoy (testnet) | ✓ | — | — | — | — | | Tempo Moderato (testnet) | ✓\* | — | — | — | ✓ | \* On Tempo and Tempo Moderato, USDC is named USDC.e. On Tempo, USDT is named USDT0; USDT0 is not supported on Tempo Moderato. Both chains also accept PathUSD and OUSD, and Tempo additionally accepts USDB. On Tempo, user pays mode is supported through the [transfer API](/wallets/actions/transfer/overview) only. ## Getting started **1. Enable user pays mode in the dashboard** Open **Fee sponsorship** in the [Privy Dashboard](https://dashboard.privy.io/apps?page=fee-sponsorship) and turn off **Sponsor gas fees** to configure **User pays** mode. **2. Configure which chains and tokens to accept** Under **Custom gas payment tokens**, click **+ Add token** to add one or more chain/token pairs. Each row specifies a chain and the token users will pay gas with on that chain. Click **Save** when done. > [!NOTE] > > The token dropdown only shows tokens deployed on the selected chain. If you change the > chain after selecting a token, the token selection resets automatically. **3. Ensure wallets hold sufficient balance** Before executing a transaction, the user's wallet must hold enough of the configured token to cover **both the transaction amount and the gas fee**. Privy estimates the gas fee automatically and returns a clear error if the balance is insufficient — the transaction will not be submitted. **4. Execute a transaction** **Transfer API** Call the [transfer API](/wallets/actions/transfer/overview) as normal — no extra parameters are required. When the app is in user pays mode and the source token is configured for that chain, Privy automatically uses that token to pay gas. ```bash theme={"system"} curl --request POST https://api.privy.io/v1/wallets//transfer \ -u ":" \ -H "privy-app-id: " \ -H "privy-authorization-signature: " \ -H 'Content-Type: application/json' \ -d '{ "source": { "asset": "usdc", "amount": "10.0", "chain": "base" }, "destination": { "address": "0xRecipientAddress" } }' ``` On the transfer API, the gas token is always the same as the token being transferred. For example, when a user sends USDC on Base and you have configured Base with USDC, the gas fee is deducted in USDC — no ETH is required. **eth\_sendTransaction** Pass `sponsor: true` along with `sponsor_options.asset` to name the token the wallet pays gas with. Unlike the transfer API, the gas token is chosen explicitly and does not need to relate to what the transaction does. ```bash highlight={10,11,12} 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:8453", "sponsor": true, "sponsor_options": { "asset": "usdc" }, "params": { "transaction": { "to": "0xRecipientAddress", "value": "0x0", "data": "0x" } } }' ``` The response returns a `user_operation_hash` immediately. The `hash` field is an empty string until the user operation confirms on-chain. **wallet\_sendCalls** Batched calls follow the same pattern — add `sponsor: true` and `sponsor_options`. The paymaster approval is bundled atomically with your calls. ```bash highlight={10,11,12} 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": "wallet_sendCalls", "caip2": "eip155:8453", "sponsor": true, "sponsor_options": { "asset": "usdc" }, "params": { "calls": [ {"to": "0xTokenAddress", "data": "0x..."}, {"to": "0xContractAddress", "data": "0x..."} ] } }' ``` The response returns a `transaction_id` you can use to track the batch. > [!NOTE] > > `sponsor_options` is supported on the REST API and server-side SDKs. Client SDKs do not > expose it yet — build the transaction in your client, then [relay it through your > server](/controls/authorization-keys/keys/create/user/request). ## How it works When a qualifying transaction is submitted, Privy: 1. **Gets a gas quote** from the Alchemy ERC-20 paymaster to estimate the maximum fee 2. **Checks the wallet balance** — verifies the wallet holds enough to cover both the transaction amount and the estimated gas fee, returning an error immediately if not 3. **Submits the transaction** — an approval for the paymaster is bundled atomically with the call or calls, as an ERC-4337 user operation 4. **Paymaster collects the fee** — after execution, the paymaster collects the exact gas fee from the wallet's configured token balance ## Errors | Error | Cause | | :- | :- | | `400` — insufficient balance | The wallet doesn't have enough of the token to cover both the transaction amount and gas | | `400` — unsupported chain/token | The chain or token combination is not supported for token gas (e.g. USDT on OP Sepolia) | | `400` — asset not configured | The asset in `sponsor_options` is not enabled for that chain in the dashboard | | `400` — `sponsor_options` required | The app is in user pays mode but the RPC request passed `sponsor: true` without `sponsor_options` | | `400` — unsupported method | `sponsor_options` was passed on a method other than `eth_sendTransaction` or `wallet_sendCalls` | ## Gas spend reporting Transactions executed in user pays mode are **not included** in your app's gas credit spend reported by `GET /v1/apps/gas_spend`. Since the user's tokens cover the fee directly, these transactions do not affect your Privy gas credits balance. ## Limitations * **EVM only.** User pays mode is not supported on Solana. * **Tempo is transfer API only.** RPC requests for Tempo reject `sponsor_options` — use the transfer API instead. * **Transfer API pays gas in the transferred token.** Gas is always paid using the same token being transferred — you cannot, for example, transfer ETH and pay gas in USDC. On the RPC endpoint, you choose the gas token explicitly. * **Named assets only.** Transfers must specify `source.asset`. Transfers identified by `source.asset_address` do not qualify for user pays gas. * **No contract deployment.** `eth_sendTransaction` requests must include a `to` address. * **Server-side only.** `sponsor_options` is not yet available in the client SDKs. # Sponsoring transactions on Solana Source: https://docs.privy.io/wallets/gas-and-asset-management/gas/solana Sponsor gas fees for Solana transactions allowing users to transact without SOL balance With embedded wallets, your app can sponsor gas fees for transactions on Solana, allowing users to transact without a SOL balance. This is done by configuring the `feePayer` property of the sponsored transaction to be a fee payer wallet that your app manages to pay users' gas fees. ## Overview Sponsoring transactions on Solana involves the following steps: **1. Set up a fee payer wallet** Create a fee payer wallet in your backend to pay for users' gas fees. **2. Prepare and sign the transaction** Prepare a transaction with a custom fee payer, sign it with the user's wallet, and send it to your backend. **3. Verify and complete the transaction** Verify the transaction, sign it with the fee payer wallet, and broadcast it to the network. > [!NOTE] > > To prepare transactions with a fee payer, we recommend using the > [@solana/web3.js](https://solana-foundation.github.io/solana-web3.js/) library. > [!WARNING] > > On Solana, rent refunds from Associated Token Account (ATA) closures go to the account owner > rather than the fee payer. This can be exploited to drain gas sponsorship funds through repeated > transactions opening and closing ATAs. See [Security best > practices](/wallets/gas-and-asset-management/gas/security#solana-rent-refund-vulnerability) for > details and mitigation strategies before enabling sponsorship. ## Setting up a fee payer wallet To start, create a fee payer wallet in your backend to sponsor transactions sent by users. You can either: 1. Generate a new keypair directly: ```javascript theme={"system"} import {Keypair} from '@solana/web3.js'; import bs58 from 'bs58'; // Generate a new keypair const feePayerWallet = new Keypair(); const feePayerAddress = feePayerWallet.publicKey.toBase58(); const feePayerPrivateKey = bs58.encode(feePayerWallet.secretKey); // Make sure to store the private key securely; it should never leave your server console.log('Fee Payer Address:', feePayerAddress); console.log('Fee Payer Private Key:', feePayerPrivateKey); ``` 2. Or [create a Solana wallet](/wallets/wallets/create/create-a-wallet) to act as your fee payer for better security and key management. Ensure you fund this wallet with SOL to pay for transaction fees. ## Implementing Sponsored Transactions **React** With the React SDK, follow these steps to prepare and send a sponsored transaction: ```javascript theme={"system"} import {useWallets} from '@privy-io/react-auth/solana'; import { TransactionMessage, PublicKey, VersionedTransaction, Connection } from '@solana/web3.js'; // This function prepares and signs a sponsored transaction async function prepareSponsoredTransaction(instructions, feePayerAddress) { // Find the user's embedded wallet const { wallets } = useWallets(); const embeddedWallet = wallets.find(wallet => wallet.standardWallet.name === 'Privy'); if (!embeddedWallet) { throw new Error('No embedded wallet found'); } // Create a connection to Solana const connection = new Connection('https://api.mainnet-beta.solana.com'); const { blockhash } = await connection.getLatestBlockhash(); // Create the transaction message with fee payer set to the backend wallet const message = new TransactionMessage({ payerKey: new PublicKey(feePayerAddress), recentBlockhash: blockhash, instructions }).compileToV0Message(); // Create transaction const transaction = new VersionedTransaction(message); // Serialize message for signing const serializedMessage = Buffer.from(transaction.message.serialize()).toString('base64'); // Get provider and sign const {signature: serializedUserSignature} = await embeddedWallet.signMessage({message: serializedMessage}) // Add user signature to transaction const userSignature = Buffer.from(serializedUserSignature, 'base64'); transaction.addSignature(new PublicKey(embeddedWallet.address), userSignature); // Serialize the transaction to send to backend const serializedTransaction = Buffer.from(transaction.serialize()).toString('base64'); // Send to your backend const response = await fetch('your-backend-endpoint', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ transaction: serializedTransaction }) }); const { transactionHash } = await response.json(); return transactionHash; } ``` **React Native** With the React Native SDK, follow these steps to prepare and send a sponsored transaction: ```javascript theme={"system"} import {useEmbeddedSolanaWallet} from '@privy-io/expo'; import { TransactionMessage, PublicKey, VersionedTransaction, Connection } from '@solana/web3.js'; // This function prepares and signs a sponsored transaction async function prepareSponsoredTransaction(instructions, feePayerAddress) { // Get the embedded wallet const wallet = useEmbeddedSolanaWallet(); const embeddedWallet = wallet.wallets[0]; if (!embeddedWallet) { throw new Error('No embedded wallet found'); } // Create a connection to Solana const connection = new Connection('https://api.mainnet-beta.solana.com'); const { blockhash } = await connection.getLatestBlockhash(); // Create the transaction message with fee payer set to the backend wallet const message = new TransactionMessage({ payerKey: new PublicKey(feePayerAddress), recentBlockhash: blockhash, instructions }).compileToV0Message(); // Create transaction const transaction = new VersionedTransaction(message); // Serialize message for signing const serializedMessage = Buffer.from(transaction.message.serialize()).toString('base64'); // Get provider and sign const provider = await embeddedWallet.getProvider(); const { signature: serializedUserSignature } = await provider.request({ method: 'signMessage', params: { message: serializedMessage } }); // Add user signature to transaction const userSignature = Buffer.from(serializedUserSignature, 'base64'); transaction.addSignature(new PublicKey(embeddedWallet.address), userSignature); // Serialize the transaction to send to backend const serializedTransaction = Buffer.from(transaction.serialize()).toString('base64'); // Send to your backend const response = await fetch('your-backend-endpoint', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ transaction: serializedTransaction }) }); const { transactionHash } = await response.json(); return transactionHash; } ``` **Swift** With the Swift SDK, follow these steps to prepare and send a sponsored transaction: ```swift theme={"system"} import Foundation import SolanaSwift import PrivySDK // This function prepares and signs a sponsored transaction func prepareSponsoredTransaction( wallet: EmbeddedSolanaWallet, instructions: [TransactionInstruction], feePayerAddress: String, rpcUrl: String ) async throws -> String { // Create a Solana RPC client let solana = JSONRPCAPIClient(endpoint: URL(string: rpcUrl)!) // Get the latest blockhash let latestBlockhash = try await solana.getLatestBlockhash() // Create public keys let walletPK = try PublicKey(string: wallet.address) let feePayerPK = try PublicKey(string: feePayerAddress) // Create transaction var tx = Transaction() // Add instructions for instruction in instructions { tx.instructions.append(instruction) } // Set the fee payer and blockhash tx.recentBlockhash = latestBlockhash tx.feePayer = feePayerPK // Serialize the transaction message to base64 let message = try tx.compileMessage().serialize().base64EncodedString() // Get the provider for wallet let provider = wallet.provider // Sign using the embedded wallet let userSignature = try await provider.signMessage(message: message) // Add the signature to the transaction try tx.addSignature(signature: Data(base64Encoded: userSignature)!, publicKey: walletPK) // Serialize the transaction for transmission to backend let serializedTransaction = tx.serialize().base64EncodedString() // Send to backend let url = URL(string: "your-backend-endpoint")! var request = URLRequest(url: url) request.httpMethod = "POST" request.addValue("application/json", forHTTPHeaderField: "Content-Type") let requestBody: [String: Any] = ["transaction": serializedTransaction] request.httpBody = try JSONSerialization.data(withJSONObject: requestBody) let (data, response) = try await URLSession.shared.data(for: request) guard let httpResponse = response as? HTTPURLResponse, (200...299).contains(httpResponse.statusCode) else { throw NSError(domain: "HTTP Error", code: (response as? HTTPURLResponse)?.statusCode ?? 0) } let jsonResponse = try JSONSerialization.jsonObject(with: data) as? [String: Any] guard let transactionHash = jsonResponse?["transactionHash"] as? String else { throw NSError(domain: "Response Error", code: 0, userInfo: [NSLocalizedDescriptionKey: "Invalid response"]) } return transactionHash } ``` **Android** With the Android SDK, follow these steps to prepare and send a sponsored transaction: ```kotlin theme={"system"} import android.util.Base64 import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.withContext import org.json.JSONObject import java.net.HttpURLConnection import java.net.URL // This function prepares and signs a sponsored transaction suspend fun prepareSponsoredTransaction( solanaWallet: EmbeddedSolanaWallet, instructions: List, feePayerAddress: String, rpcUrl: String ): Result { return try { // Create connection to Solana val connection = Connection(rpcUrl) // Get recent blockhash val recentBlockhash = connection.getLatestBlockhash() // Create wallet public key val walletPublicKey = PublicKey(solanaWallet.address) val feePayerPublicKey = PublicKey(feePayerAddress) // Build transaction val transaction = Transaction() // Add instructions to transaction instructions.forEach { instruction -> transaction.add(instruction) } // Set blockhash and fee payer transaction.recentBlockhash = recentBlockhash transaction.feePayer = feePayerPublicKey // Serialize transaction message to base64 val serializedMessage = transaction.serializeMessage().base64() // Sign the message with user's wallet val signatureResult = solanaWallet.provider.signMessage(serializedMessage) when (signatureResult) { is Result.Success -> { // Get the signature val userSignature = signatureResult.data // Add signature to transaction transaction.addSignature( walletPublicKey, Base64.decode(userSignature, Base64.DEFAULT) ) // Serialize transaction for sending to backend val serializedTransaction = Base64.encodeToString( transaction.serialize(), Base64.NO_WRAP ) // Send to backend val url = URL("your-backend-endpoint") val connection = url.openConnection() as HttpURLConnection connection.requestMethod = "POST" connection.setRequestProperty("Content-Type", "application/json") connection.doOutput = true // Create request body val requestBody = JSONObject().apply { put("transaction", serializedTransaction) }.toString() // Send request connection.outputStream.use { os -> os.write(requestBody.toByteArray()) } // Read response val responseCode = connection.responseCode if (responseCode in 200..299) { val response = connection.inputStream.bufferedReader().use { it.readText() } val jsonResponse = JSONObject(response) val transactionHash = jsonResponse.getString("transactionHash") Result.Success(transactionHash) } else { val error = connection.errorStream?.bufferedReader()?.use { it.readText() } ?: "Unknown error" Result.Failure(Exception("HTTP Error: $responseCode - $error")) } } is Result.Failure -> { Result.Failure(signatureResult.error) } } } catch (e: Exception) { Result.Failure(e) } } ``` **Flutter** With the Flutter SDK, follow these steps to prepare and send a sponsored transaction: ```dart theme={"system"} import 'dart:convert'; import 'package:http/http.dart' as http; import 'package:solana/solana.dart'; // This function prepares and signs a sponsored transaction Future> prepareSponsoredTransaction({ required EmbeddedSolanaWallet solanaWallet, required List instructions, required String feePayerAddress, required String rpcUrl, required String backendUrl, }) async { try { // Create connection to Solana final connection = Connection(rpcUrl); // Get recent blockhash final recentBlockhash = await connection.getLatestBlockhash(); // Create wallet public key final walletPublicKey = PublicKey(solanaWallet.address); final feePayerPublicKey = PublicKey(feePayerAddress); // Build transaction final transaction = Transaction(); // Add instructions to transaction for (final instruction in instructions) { transaction.add(instruction); } // Set blockhash and fee payer transaction.recentBlockhash = recentBlockhash; transaction.feePayer = feePayerPublicKey; // Serialize transaction message to base64 final serializedMessage = base64Encode(transaction.serializeMessage()); // Sign the message with user's wallet final signatureResult = await solanaWallet.provider.signMessage(serializedMessage); return signatureResult.when( success: (userSignature) async { // Add signature to transaction transaction.addSignature( walletPublicKey, base64Decode(userSignature) ); // Serialize transaction for sending to backend final serializedTransaction = base64Encode(transaction.serialize()); // Send to backend final response = await http.post( Uri.parse(backendUrl), headers: {'Content-Type': 'application/json'}, body: jsonEncode({'transaction': serializedTransaction}), ); if (response.statusCode >= 200 && response.statusCode < 300) { final jsonResponse = jsonDecode(response.body); final transactionHash = jsonResponse['transactionHash'] as String; return Success(transactionHash); } else { return Failure( Exception('HTTP Error: ${response.statusCode} - ${response.body}') ); } }, failure: (error) { return Failure(Exception('Failed to sign transaction: $error')); }, ); } catch (e) { return Failure(Exception('Error preparing transaction: $e')); } } ``` ## Backend Implementation Here's how to implement the server-side portion that receives the partially signed transaction, adds the fee payer signature, and broadcasts it: ```javascript theme={"system"} // Backend implementation (Node.js with Express) import express from 'express'; import {Keypair, VersionedTransaction, Connection, clusterApiUrl, PublicKey} from '@solana/web3.js'; import bs58 from 'bs58'; const app = express(); app.use(express.json()); // Your fee payer wallet's private key (Keep this secure!) const FEE_PAYER_PRIVATE_KEY = 'your-base58-encoded-private-key'; const FEE_PAYER_ADDRESS = 'your-fee-payer-address'; // Initialize fee payer keypair const feePayerWallet = Keypair.fromSecretKey(bs58.decode(FEE_PAYER_PRIVATE_KEY)); // Connect to Solana const connection = new Connection(clusterApiUrl('mainnet-beta')); // Endpoint to handle sponsored transactions app.post('/sponsor-transaction', async (req, res) => { try { // Get the partially signed transaction from the request const {transaction: serializedTransaction} = req.body; if (!serializedTransaction) { return res.status(400).json({error: 'Missing transaction data'}); } // Deserialize the transaction const transactionBuffer = Buffer.from(serializedTransaction, 'base64'); const transaction = VersionedTransaction.deserialize(transactionBuffer); // Verify the transaction // 1. Check that it's using the correct fee payer const message = transaction.message; const accountKeys = message.getAccountKeys(); const feePayerIndex = 0; // Fee payer is always the first account const feePayer = accountKeys.get(feePayerIndex); if (!feePayer || feePayer.toBase58() !== FEE_PAYER_ADDRESS) { return res.status(403).json({ error: 'Invalid fee payer in transaction' }); } // 2. Check for any unauthorized fund transfers for (const instruction of message.compiledInstructions) { const programId = accountKeys.get(instruction.programIndex); // Check if instruction is for System Program (transfers) if (programId && programId.toBase58() === '11111111111111111111111111111111') { // Check if it's a transfer (command 2) if (instruction.data[0] === 2) { const senderIndex = instruction.accountKeyIndexes[0]; const senderAddress = accountKeys.get(senderIndex); // Don't allow transactions that transfer tokens from fee payer if (senderAddress && senderAddress.toBase58() === FEE_PAYER_ADDRESS) { return res.status(403).json({ error: 'Transaction attempts to transfer funds from fee payer' }); } } } } // 3. Sign with fee payer transaction.sign([feePayerWallet]); // 4. Send transaction const signature = await connection.sendTransaction(transaction); // Return the transaction hash return res.status(200).json({ transactionHash: signature, message: 'Transaction sent successfully' }); } catch (error) { console.error('Error processing transaction:', error); return res.status(500).json({ error: 'Failed to process transaction', details: error.message }); } }); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); }); ``` ## Security Considerations When implementing transaction sponsorship, be mindful of these security considerations: > **Verify Transaction Contents** > > Always verify the transaction contents in your backend before signing with the fee payer. Ensure > there are no unauthorized fund transfers. > **Rate Limiting** > > Implement rate limiting to prevent abuse of your sponsorship service. Consider limits per user, > per session, or per wallet. > **Amount Validation** > > Validate the transaction amount if applicable. Consider setting maximum sponsorship amounts to > prevent excessive spending. > **Program ID Whitelisting** > > Only sponsor transactions for specific whitelisted program IDs that your app interacts with to > prevent abuse. > [!NOTE] > > Be extremely careful with your fee payer wallet's private key. Never expose it in client-side code > or store it in unsecured environments. Consider using environment variables, secret management > services, or HSMs to securely store private keys in production. # Transaction management Source: https://docs.privy.io/wallets/gas-and-asset-management/gas/transaction-handling Transaction management for gas-sponsored Solana transactions including signature handling ## Solana When your app sends a gas-sponsored Solana transaction, the API returns the final transaction signature in the `hash` field immediately. Your app can subscribe to [webhooks](/wallets/gas-and-asset-management/assets/transaction-event-webhooks) to receive status updates as the transaction confirms on-chain. ## EVM When your app sends a standard gas-sponsored EVM transaction, the API returns `transaction_id` and `user_operation_hash` immediately after broadcast. Privy delivers the final transaction hash via webhook once the transaction confirms on-chain. For sponsored Tempo transactions, the API returns the broadcast transaction hash in `hash`. > [!NOTE] > > **Client SDK behavior**: Web and mobile SDK clients wait for on-chain confirmation before > returning. The backend API returns immediately after broadcast. Your app receives the transaction hash and status updates via [webhooks](/wallets/gas-and-asset-management/assets/transaction-event-webhooks) as the transaction processes. *** ## How standard EVM transactions work When your app sends a standard gas-sponsored EVM transaction, the API returns the following response immediately after broadcast: ```json theme={"system"} { "transaction_id": "tx-xyz789", "user_operation_hash": "0x...", "hash": "" } ``` **Key fields:** * `transaction_id` — Privy's internal transaction identifier for tracking * `user_operation_hash` — Unique identifier for the broadcasted user operation * `hash` — Empty string until the transaction confirms on-chain Privy delivers the final transaction hash via webhook as `transaction_hash` once the transaction confirms. ### Transaction lifecycle **1. Transaction broadcast** The API validates and broadcasts the transaction to the network, then immediately returns with a `user_operation_hash`. **2. Pending state** The transaction is pending on-chain. Applications should display a loading or pending state to users. **3. Confirmation** Once confirmed on-chain, Privy sends a webhook with the final `transaction_hash` and status. ## Tracking transaction status Your app can track transaction status via webhooks for real-time notifications or the API for on-demand queries. ### Querying status Your app can query transaction status at any time using the `transaction_id`. The response includes the current status and transaction hash once available. For sponsored transactions, it also includes the [gas credits charged](/wallets/gas-and-asset-management/gas/gas-spend#per-transaction). **Node SDK** ```typescript theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: 'your-app-id', appSecret: 'your-app-secret' }); const transactionId = 'tx-xyz789'; const transaction = await privy.transactions().get(transactionId); console.log(transaction.status); // "pending", "confirmed", "reverted", etc. console.log(transaction.transaction_hash); // Available once confirmed ``` **Python SDK** ```python theme={"system"} transaction = client.transactions.get("tx-xyz789") print(transaction.status) print(transaction.transaction_hash) ``` **REST API** ```bash theme={"system"} curl --request GET \ https://api.privy.io/v1/transactions/ \ -u ":" \ -H "privy-app-id: " ``` **Response:** ```json theme={"system"} { "id": "tx-xyz789", "wallet_id": "wallet-abc123", "status": "confirmed", "transaction_hash": "0x...", "caip2": "eip155:1", "sponsored": true, "gas_credits_charged": { "value": "0.001543", "currency": "usd" } } ``` **Rust SDK** ```rust theme={"system"} use privy_rs::PrivyClient; let client = PrivyClient::new(app_id, app_secret)?; let transaction = client.transactions().get(transaction_id).await?; println!("Status: {:?}", transaction.status); println!("Hash: {:?}", transaction.transaction_hash); ``` Learn more in the [API reference](/api-reference/transactions/get). ### Notified via webhooks Webhooks deliver real-time notifications as transaction status changes. Configure a webhook endpoint in the Dashboard to receive automatic updates. **Setting up webhooks:** 1. Navigate to the [Webhooks tab](https://dashboard.privy.io/apps?page=webhooks) in the Privy Dashboard 2. Add a webhook endpoint URL 3. Subscribe to transaction events 4. Implement webhook handlers in your application **Transaction webhook events:** | Event | Description | | - | - | | `transaction.broadcasted` | Transaction was submitted to the network | | `transaction.confirmed` | Transaction was confirmed on-chain | | `transaction.execution_reverted` | Transaction was reverted by the network | | `transaction.failed` | Transaction failed to be included | | `transaction.replaced` | Transaction was replaced by another transaction | **Webhook payloads:** **`transaction.broadcasted`** — Sent immediately after broadcast: ```json theme={"system"} { "type": "transaction.broadcasted", "data": { "wallet_id": "wallet-abc123", "transaction_id": "tx-xyz789", "caip2": "eip155:1", "user_operation_hash": "0x..." } } ``` **`transaction.confirmed`** — Sent when confirmed on-chain: ```json theme={"system"} { "type": "transaction.confirmed", "data": { "wallet_id": "wallet-abc123", "transaction_id": "tx-xyz789", "caip2": "eip155:1", "transaction_hash": "0x...", "user_operation_hash": "0x..." } } ``` **`transaction.execution_reverted`** — Sent when the transaction reverts: ```json theme={"system"} { "type": "transaction.execution_reverted", "data": { "wallet_id": "wallet-abc123", "transaction_id": "tx-xyz789", "caip2": "eip155:1", "transaction_hash": "0x...", "user_operation_hash": "0x..." } } ``` ## Common questions > **Why does the API return immediately instead of waiting for confirmation?** > > The backend API returns immediately after broadcast to prevent timeout issues during network congestion. This enables high transaction volumes and real-time status updates via webhooks. Web and mobile SDK clients wait for on-chain confirmation before returning for a simpler developer experience. > **How long does confirmation typically take?** > > Confirmation times vary by network: > > * **Ethereum mainnet**: 12-60 seconds > * **Layer 2s** (Base, Optimism, Arbitrum): 1-10 seconds > * **Solana**: 1-5 seconds > > During network congestion, confirmation may take longer. Applications should implement appropriate timeout handling. > **What if the webhook fails to deliver?** > > Privy retries webhook delivery with exponential backoff. Your app should also implement a polling > fallback using the `transaction_id` to fetch status from Privy's API. > **Can I get the transaction hash immediately?** > > No. The `transaction_hash` becomes available only after the transaction is included in a block on-chain. Your app must use webhooks or polling to retrieve the final hash. > [!NOTE] > > Learn more about transaction webhooks in the [webhook events > reference](/wallets/gas-and-asset-management/assets/transaction-event-webhooks). # Overview Source: https://docs.privy.io/wallets/gas-and-asset-management/usage-billing/overview Manage fee sponsorship with prepaid credits or postpaid billing Fee sponsorship lets an app cover network gas and swap provider fees for eligible wallet activity. It is available self-serve from the [Privy Dashboard](https://dashboard.privy.io/apps?page=fee-sponsorship) using prepaid credits. Privy supports two billing models. Prepaid billing uses credits purchased in advance. Postpaid billing meters usage and invoices it at the end of the month. ## Setup ### Choosing your plan | Model | How it works | When usage is paid for | | - | - | - | | Prepaid | Sponsored gas and swap provider fees use the account's prepaid credit balance. | In advance, as a top-up | | Postpaid | Privy meters each charge and adds it to a monthly invoice. | In arrears, at the end of the month | > [!NOTE] > > Postpaid billing is available on the Enterprise plan. Contact > [sales@privy.io](mailto:sales@privy.io) to enable it for an account. ### Purchasing credits (prepaid) Prepaid credits cover gas and swap provider fee sponsorship across the account's apps. **1. Add credits** Open [**Billing > Fee sponsorship**](https://dashboard.privy.io/billing?tab=fee-sponsorship) and select **Add credits**. Choose an amount and confirm the purchase. Add a payment method when prompted, if one is not already saved. **2. Configure sponsorship for an app** Open the app's [**Fee sponsorship**](https://dashboard.privy.io/apps?page=fee-sponsorship) page. Enable [**Sponsor gas fees**](/wallets/gas-and-asset-management/gas/setup), [**Sponsor swap provider fees**](/wallets/actions/transfer/fixed-rates#setup), or both. The available balance must cover the sponsored fees. If credits are insufficient, add credits before retrying the transaction. Manage the balance, **Automated credit refill**, and notification settings from **Billing > Fee sponsorship**. > [!NOTE] > > To query prepaid gas credit consumption for a set of wallets, use the [gas spend > endpoint](/wallets/gas-and-asset-management/gas/gas-spend). ## Sponsored fees Both billing models support the following fees. On postpaid accounts, each usage type appears separately on the monthly invoice. | Usage type | What it covers | | - | - | | Gas sponsorship | Network gas the app sponsored for a transaction, on apps that use [gas sponsorship](/wallets/gas-and-asset-management/gas/overview). | | Swap provider fees | Provider fees on all supported routes for [transfers](/wallets/actions/transfer/bridging), [swaps](/wallets/actions/swap/overview), and [crypto deposit addresses](/wallets/funding/crypto-deposits/overview) with [fee sponsorship](/wallets/actions/transfer/fixed-rates). Slippage is covered only for [fixed-rate routes](/wallets/actions/transfer/supported-sponsored-routes). | A single wallet action can produce more than one charge. A transfer with both sponsorship settings enabled incurs gas and swap provider fee charges. ## Consuming charge events Usage webhooks help apps track sponsored gas and swap provider fee charges. Each event includes the charge amount and the wallet action that incurred it. > **[Usage webhooks](/wallets/gas-and-asset-management/usage-billing/usage-webhooks)** > > Review event availability, payloads, and guidance for reconciling charges against wallet actions. # Usage webhooks Source: https://docs.privy.io/wallets/gas-and-asset-management/usage-billing/usage-webhooks Receive an event each time Privy records a usage charge, and reconcile it against a wallet action Privy publishes webhooks for [sponsored fee charges](/wallets/gas-and-asset-management/usage-billing/overview). Each event carries the amount and the wallet action that incurred it, so an app can attribute costs to users. > [!NOTE] > > Swap provider fee events are available for prepaid and postpaid accounts. Prepaid accounts receive > an event after the charge is deducted from their credits. Gas sponsorship events currently support > postpaid accounts. Support for prepaid accounts is coming soon. ## Event types | Event name | Type | Action | | - | - | - | | Gas sponsorship charge recorded | `usage.gas_sponsorship.recorded` | Privy recorded a charge for sponsored network gas. | | Swap provider fee charge recorded | `usage.swap_provider_fee.recorded` | Privy recorded a charge for a cross-chain transfer or swap. | ## Subscribing Swap provider fee charges use the `usage.swap_provider_fee.recorded` event type. In the Privy Dashboard, go to the **Configuration > Webhooks** page and select the events under **Usage charges**. Both events require the wallet API to be enabled for the app, since usage arises from wallet action traffic. > [!NOTE] > > See the [webhooks overview](/api-reference/webhooks/overview) for registering an endpoint and > verifying payload signatures. ## Payload Both events share the same payload. **type** `string` The event type — either `usage.gas_sponsorship.recorded` or `usage.swap_provider_fee.recorded`. **event\_id** `string` A unique identifier for the usage event. Use it to deduplicate charge events. **source\_id** `string` The ID of the wallet action that incurred the charge. Use this to look up the action and reconcile the charge. **source\_type** `'wallet-action-transfer' | 'wallet-action-swap'` The type of wallet action that incurred the charge. **amount\_usd** `string` The charge amount in USD, as a decimal string with six decimal places (e.g. `"0.004213"`). A charge smaller than a cent keeps its exact value rather than rounding, so charges total accurately across a billing period. **recorded\_at** `number` When Privy recorded the charge, in milliseconds since the Unix epoch. ## Examples ```json usage.gas_sponsorship.recorded theme={"system"} { "type": "usage.gas_sponsorship.recorded", "event_id": "insert-gas-charge-event-id", "source_id": "wa_9f3c...7b", "source_type": "wallet-action-swap", "amount_usd": "0.004213", "recorded_at": 1722787200000 } ``` ```json usage.swap_provider_fee.recorded theme={"system"} { "type": "usage.swap_provider_fee.recorded", "event_id": "insert-swap-provider-sponsorship-event-id", "source_id": "wa_9f3c...7b", "source_type": "wallet-action-swap", "amount_usd": "0.750000", "recorded_at": 1722787200000 } ``` ## Reconciling charges A sponsored cross-chain transfer can report gas and swap provider fees as separate events. Both carry the same `source_id`, with different event types and `event_id` values. Delivery is at least once, so the same event may arrive more than once. Use `event_id` to deduplicate charges and the `svix-id` header to identify a webhook redelivery. > [!NOTE] > > See [webhook delivery](/api-reference/webhooks/overview#webhook-delivery) for the retry schedule. ## API reference > **[Gas sponsorship charge recorded](/api-reference/webhooks/usage/gas_sponsorship_recorded)** > > Full schema for `usage.gas_sponsorship.recorded`. > **[Swap provider fee charge recorded](/api-reference/webhooks/usage/swap_provider_fee_recorded)** > > Full schema for `usage.swap_provider_fee.recorded`. # Connectkit connector Source: https://docs.privy.io/wallets/global-wallets/integrate-a-global-wallet/connectkit-connector Privy makes it easy to integrate cross-app wallets from various Privy apps into existing setups with ConnectKit, wagmi and more using the [`@privy-io/cross-app-connect`](https://www.npmjs.com/package/@privy-io/cross-app-connect) SDK. Integrating apps do not need to use Privy themselves to integrate cross-app wallets; instead, they can create a custom wagmi connector using the SDK to configure with ConnectKit. Simply follow the instructions below to get set up! > [!TIP] > > See our > [`privy-io/cross-app-connect-demo`](https://github.com/privy-io/cross-app-connect-demo/tree/connectkit-demo) > repo's ConnectKit branch for an example setup. ## 1. Install dependencies Install the [`@privy-io/cross-app-connect`](https://www.npmjs.com/package/@privy-io/cross-app-connect) SDK and its peer dependencies: ```bash theme={"system"} npm i @privy-io/cross-app-connect wagmi connectkit @tanstack/react-query viem ``` ## 2. Create the custom connector Next, you'll create a custom wagmi connector that integrates with Privy's cross-app-connect functionality. This connector will be fully compatible with ConnectKit and handles all the connection logic for you: ```tsx privy-global-connector.ts expandable theme={"system"} import {createConnector} from 'wagmi'; import {toPrivyWalletProvider} from '@privy-io/cross-app-connect'; import type {EIP1193Provider} from 'viem'; export interface PrivyGlobalWalletOptions { appId: string; name: string; iconUrl?: string; smartWalletMode?: boolean; } /** * Creates a Privy Global Wallet connector for use with wagmi and ConnectKit * * @param options - Configuration options for the Privy wallet * @returns A wagmi connector configured for Privy cross-app wallets */ export function privyGlobalWalletConnector(options: PrivyGlobalWalletOptions) { return createConnector((config) => ({ id: options.appId, name: options.name, icon: options.iconUrl, type: 'injected', // Critical: Use 'injected' type for ConnectKit compatibility async setup() { // Setup is called when the connector is first created // No special setup needed for cross-app-connect }, async connect({chainId} = {}) { const provider = await this.getProvider(); // Request accounts - this triggers the Privy popup await provider.request({method: 'eth_requestAccounts'}); // Switch to requested chain if provided if (chainId) { try { await this.switchChain({chainId}); } catch (error) { console.warn('Failed to switch to requested chain:', error); } } const accounts = await this.getAccounts(); const currentChainId = await this.getChainId(); return { accounts, chainId: currentChainId }; }, async disconnect() { const provider = await this.getProvider(); try { await provider.request({ method: 'wallet_revokePermissions', params: [{eth_accounts: {}}] }); } catch (error) { console.warn('Failed to revoke permissions:', error); } }, async getAccounts() { const provider = await this.getProvider(); const accounts = await (provider as EIP1193Provider).request({ method: 'eth_accounts' }); return accounts as `0x${string}`[]; }, async getChainId() { const provider = await this.getProvider(); const chainId = await (provider as EIP1193Provider).request({ method: 'eth_chainId' }); return Number(chainId); }, async getProvider(): Promise { return toPrivyWalletProvider({ providerAppId: options.appId, chains: config.chains, smartWalletMode: options.smartWalletMode || false }) as EIP1193Provider; }, async isAuthorized() { try { const accounts = await this.getAccounts(); return accounts.length > 0; } catch { return false; } }, async switchChain({chainId}) { const provider = await this.getProvider(); const chain = config.chains.find((c) => c.id === chainId); if (!chain) { throw new Error(`Chain ${chainId} not configured`); } await provider.request({ method: 'wallet_switchEthereumChain', params: [{chainId: `0x${chainId.toString(16)}`}] }); return chain; }, onAccountsChanged(accounts) { if (accounts.length === 0) { config.emitter.emit('disconnect'); } else { config.emitter.emit('change', { accounts: accounts as `0x${string}`[] }); } }, onChainChanged(chainId) { const id = Number(chainId); config.emitter.emit('change', {chainId: id}); }, onConnect(connectInfo) { config.emitter.emit('connect', { accounts: [], chainId: Number(connectInfo.chainId) }); }, onDisconnect() { config.emitter.emit('disconnect'); } })); } ``` ## 3. Configure wagmi with ConnectKit Now you'll set up your wagmi configuration using ConnectKit's `getDefaultConfig` and pass in your custom Privy connector. This step brings everything together: ```tsx theme={"system"} import {WagmiProvider, createConfig, http} from 'wagmi'; import {mainnet} from 'wagmi/chains'; import {QueryClient, QueryClientProvider} from '@tanstack/react-query'; import {ConnectKitProvider, getDefaultConfig} from 'connectkit'; import {privyGlobalWalletConnector} from './privy-global-connector'; const privyWalletConnector = privyGlobalWalletConnector({ appId: 'privy-wallet-app-id', name: 'Privy wallet app name', iconUrl: 'https://example.com/image.png' }); const config = createConfig( getDefaultConfig({ // Your dApp's chains chains: [mainnet], transports: { // RPC URL for each chain [mainnet.id]: http() }, // Required API Keys walletConnectProjectId: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID, // Required App Info appName: 'Your App Name', connectors: [privyWalletConnector], // Optional App Info appDescription: 'Your App Description', appUrl: 'https://yourapp.com', appIcon: 'https://yourapp.com/logo.png' }) ); const queryClient = new QueryClient(); export const Providers = ({children}: {children: React.ReactNode}) => { return ( {children} ); }; ``` ## 4. Wrap your app with providers At the highest level of your application, wrap your components with the `Providers` component you just created: ```tsx theme={"system"} import {Providers} from './providers'; function MyApp({Component, pageProps}) { return ( ); } export default MyApp; ``` ### Complete example Here's how all the pieces fit together in your project structure: **providers.tsx** ```tsx theme={"system"} import {WagmiProvider, createConfig, http} from 'wagmi'; import {mainnet} from 'wagmi/chains'; import {QueryClient, QueryClientProvider} from '@tanstack/react-query'; import {ConnectKitProvider, getDefaultConfig} from 'connectkit'; import {privyGlobalWalletConnector} from './privy-global-connector'; const privyWalletConnector = privyGlobalWalletConnector({ appId: 'privy-wallet-app-id', name: 'Privy wallet app name', iconUrl: 'https://example.com/image.png' }); const config = createConfig( getDefaultConfig({ chains: [mainnet], transports: { [mainnet.id]: http() }, walletConnectProjectId: process.env.NEXT_PUBLIC_WALLETCONNECT_PROJECT_ID, appName: 'Your App Name', connectors: [privyWalletConnector], appDescription: 'Your App Description', appUrl: 'https://yourapp.com', appIcon: 'https://yourapp.com/logo.png' }) ); const queryClient = new QueryClient(); export const Providers = ({children}: {children: React.ReactNode}) => { return ( {children} ); }; ``` **privy-global-connector.ts** ```tsx privy-global-connector.ts expandable theme={"system"} import {createConnector} from 'wagmi'; import {toPrivyWalletProvider} from '@privy-io/cross-app-connect'; import type {EIP1193Provider} from 'viem'; export interface PrivyGlobalWalletOptions { appId: string; name: string; iconUrl?: string; smartWalletMode?: boolean; } /** * Creates a Privy Global Wallet connector for use with wagmi and ConnectKit */ export function privyGlobalWalletConnector(options: PrivyGlobalWalletOptions) { return createConnector((config) => ({ id: options.appId, name: options.name, icon: options.iconUrl, type: 'injected', async connect({chainId} = {}) { const provider = await this.getProvider(); await provider.request({method: 'eth_requestAccounts'}); if (chainId) { try { await this.switchChain({chainId}); } catch (error) { console.warn('Failed to switch to requested chain:', error); } } const accounts = await this.getAccounts(); const currentChainId = await this.getChainId(); return {accounts, chainId: currentChainId}; }, async getProvider(): Promise { return toPrivyWalletProvider({ providerAppId: options.appId, chains: config.chains, smartWalletMode: options.smartWalletMode || false }) as EIP1193Provider; } // ... other connector methods })); } ``` ## 5. Use the `ConnectKitButton` Finally, import the `ConnectKitButton` and use it anywhere in your app to let users connect to their Privy global wallets: ```tsx theme={"system"} import {ConnectKitButton} from 'connectkit'; function Page() { return (

My app

...
); } ``` That's it! You're all set up with Privy global wallets and ConnectKit. You can now use any wagmi hook in your application to interact with the connected wallet. When users connect and transact with their wallet, Privy will open a secure pop-up for users to authorize any actions, ensuring their private keys never leave the original app. This integration gives your users seamless access to their existing wallets from other Privy-powered apps, reducing onboarding friction and improving the overall user experience. # Getting global wallets Source: https://docs.privy.io/wallets/global-wallets/integrate-a-global-wallet/getting-global-wallets Once a user has successfully linked their account from a provider app, an account of `type: 'cross_app'` will be added to the `linkedAccounts` array of their `user` object. This `crossAppAccount` is an object with the following properties: **properties** **type** `'cross_app'` *required* Indicates that the linked account is a cross-app account. **embeddedWallets** `Array\<\{address: string\}\>` An array of the user's embedded wallet(s) from the provider app for the cross-app account. **smartWallets** `Array\<\{address: string\}\>` An array of the user's smart wallet(s) from the provider app for the cross-app account, if the provider has smart wallets enabled. **providerApp** `ProviderAppMetadata` Metadata about the provider app for the cross-app account. **id** `string` *required* Privy app ID of the source app. **name** `string` Name of the source app. **logoUrl** `string` Logo of the source app. **firstVerifiedAt** `Date` Datetime when the cross-app account was first linked to the user. **latestVerifiedAt** `Date` Datetime for when the user last logged in/linked the cross-app account. To find a user's cross-app embedded wallet, first find the account of `type: 'cross_app'` from their `linkedAccounts` array. If your app permits users to link cross-app accounts from *multiple* provider apps, you should also filter the `linkedAccounts` by the `providerApp.id` on the cross-app account. ```tsx theme={"system"} // Replace `providerAppId` with the Privy app ID of your desired provider app const providerAppId = 'insert-provider-app-id'; const crossAppAccount = user.linkedAccounts.find( (account) => account.type === 'cross_app' && account.providerApp.id === providerAppId ); ``` Next, from the `crossAppAccount`, inspect the `embeddedWallets` array to get the user's embedded wallet address(es) from the source app. ```tsx theme={"system"} const address = crossAppAccount.embeddedWallets[0].address; ``` > [!NOTE] > > Most users only have one embedded wallet per app. Certain apps, however, leverage multiple > embedded wallets (e.g. HD wallets) per user to support certain use cases. In kind, the > `embeddedWallets` field is designed as an array, but generally will be a singleton containing one > entry. > > Similarly, the `smartWallets` field is designed as an array, but generally will be a singleton or empty > depending on whether the provider app has smart wallets enabled. # Linking a global wallet Source: https://docs.privy.io/wallets/global-wallets/integrate-a-global-wallet/linking-a-global-wallet **React** To prompt users to link their embedded wallet from a provider app, use the `linkCrossAppAccount` method from the `useCrossAppAccounts` hook: ```tsx theme={"system"} import {usePrivy, useCrossAppAccounts} from '@privy-io/react-auth'; function Button() { const {ready, authenticated} = usePrivy(); const {linkCrossAppAccount} = useCrossAppAccounts(); return ( ); } ``` ## Parameters **appId** `string` *required* The Privy app ID of the provider app from which you'd like a user to link their account. You can find a list of Privy app IDs for provider apps in the **Cross-app ecosystem** page of the Privy Dashboard. ## Behavior When `linkCrossAppAccount` is invoked, the user will be redirected to a page hosted on the domain of the provider app you specified to authorize access to your own app. If the user successfully authorizes access, the user will be redirected back to your app, and an account of `type: 'cross_app'` will be added to the `linkedAccounts` array of their `user` object. `linkCrossAppAccount` will throw an error if: * The user does not authorize access to your app or exits the flow prematurely. * The provider app you request has not opted-in to share their wallets. * The user is not `authenticated` and thus cannot link an account from the provider app to an existing account within your requester's app. If the user is already logged in on the domain of the source `appId` you specify in `linkCrossAppAccount`, they will not have to login again and will only have to consent to sharing access to that account in your app. ## Unlinking a cross-app account To unlink a previously linked cross-app account, use the `unlinkCrossAppAccount` method from the `useCrossAppAccounts` hook: ```tsx theme={"system"} import {useCrossAppAccounts, usePrivy} from '@privy-io/react-auth'; function UnlinkCrossAppButton() { const {user} = usePrivy(); const {unlinkCrossAppAccount} = useCrossAppAccounts(); const crossAppAccount = user?.linkedAccounts?.find((account) => account.type === 'cross_app'); const handleUnlink = async () => { if (crossAppAccount) { await unlinkCrossAppAccount({subject: crossAppAccount.subject}); } }; return ( ); } ``` ### Parameters **subject** `string` *required* The subject identifier of the cross-app account to unlink. This can be found in the `subject` field of the linked cross-app account in the user's `linkedAccounts` array. ### Returns Returns a `Promise` containing the updated user object with the cross-app account removed from `linkedAccounts`. **React Native** To prompt users to link their embedded wallet from a provider app, use the `linkWithCrossApp` method from the `useLinkWithCrossApp` hook: ```tsx theme={"system"} import {usePrivy, useLinkWithCrossApp} from '@privy-io/expo'; function LinkWithCrossAppButton() { const {isReady, user} = usePrivy(); const {linkWithCrossApp} = useLinkWithCrossApp(); return ( ); } ``` ## Parameters **appId** `string` *required* The Privy app ID of the provider app from which you'd like a user to link their account. You can find a list of Privy app IDs for provider apps in the **Cross-app ecosystem** page of the Privy Dashboard. **redirectUri** `string` A URL path that the provider will automatically redirect to after successful authentication. This defaults to a link back to your app root, eg. `'/'`, if not provided. ## Behavior When `linkWithCrossApp` is invoked, the user will be redirected to a page hosted on the domain of the provider app you specified to authorize access to your own app. If the user successfully authorizes access, the user will be redirected back to your app, and an account of `type: 'cross_app'` will be added to the `linked_accounts` array of their `user` object. `linkWithCrossApp` will throw an error if: * The user does not authorize access to your app or exits the flow prematurely. * The provider app you request has not opted-in to share their wallets. * The user is not `authenticated` and thus cannot link an account from the provider app to an existing account within your requester's app.   # Login with a global wallet Source: https://docs.privy.io/wallets/global-wallets/integrate-a-global-wallet/login-with-a-global-wallet **React** To prompt users to log into your app with an account from a provider app, use the `loginWithCrossAppAccount` method from the `useCrossAppAccounts` hook: ```tsx {5,9} theme={"system"} import {usePrivy, useCrossAppAccounts} from '@privy-io/react-auth'; function Button() { const {ready, authenticated} = usePrivy(); const {loginWithCrossAppAccount} = useCrossAppAccounts(); return ( ); } ``` ### Parameters **appId** `string` *required* The Privy app ID of the provider app from which you'd like a user to link their account. You can find a list of Privy app IDs for provider apps in the **Cross-app ecosystem** page of the Privy Dashboard. ### Behavior When `loginWithCrossAppAccount` is invoked, the user will be redirected to a page hosted on the domain of the provider app you specified to authorize access to your own app. If the user successfully authorizes access, the user will be redirected back to your app, and an account of `type: 'cross_app'` will be added to the `linkedAccounts` array of their `user` object. `loginWithCrossAppAccount` will throw an error if: * The user does not authorize access to your app or exits the flow prematurely. * The provider app you request has not opted-in to share their wallets. * The user does not already have an account with the provider app. > [!TIP] > > If the user is already logged in on the domain of the source `appId` you specify in > `loginWithCrossAppAccount`, they will not have to login again and will only have to consent to > sharing access to that account in your app. ## Using the Privy login modal You can add any provider app to the list of login options in the Privy login modal by adding `"privy:"` + the provider's app ID to `loginMethodsAndOrder` or the `loginMethods` array in the Privy SDK configuration. ```tsx loginMethodsAndOrder theme={"system"} ``` ```tsx loginMethods theme={"system"} ``` **React Native** 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 LoginWithCrossAppButton() { const {ready, user} = usePrivy(); const {loginWithCrossApp} = useLoginWithCrossApp(); return ( ); } ``` **appId** `string` *required* The Privy app ID of the provider app from which you'd like a user to link their account. You can find a list of Privy app IDs for provider apps in the **Cross-app ecosystem** page of the Privy Dashboard. **redirectUri** `string` A URL path that the provider will automatically redirect to after successful authentication. This defaults to a link back to your app root, eg. `'/'`, if not provided. When `loginWithCrossApp` is invoked, an in-app browser will open for a page hosted on the domain of the provider app you specified to authorize access to your own app. If the user successfully authorizes access, the user will be redirected back to your app, and an account of `type: 'cross_app'` will be added to the `linked_accounts` array of their `user` object. `loginWithCrossApp` will throw an error if: * The user does not authorize access to your app or exits the flow prematurely. * The provider app you request has not opted-in to share their wallets. * The user does not already have an account with the provider app. > [!TIP] > > If the user is already logged in on the domain of the target `appId` you specify in `loginWithCrossApp`, they will not have to login again and will only have to consent to sharing access to that account in your app. ## Using the access token Once the user has authenticated with the provider app. You can get their access token for making requests directly to the provider app using `useGetAccessTokenForProvider`. ```tsx {4,8-13} theme={"system"} import {useGetAccessTokenForProvider} from '@privy-io/react-auth'; function Button() { const {getAccessTokenForProvider} = useGetAccessTokenForProvider(); return ( ); } ``` # Overview Source: https://docs.privy.io/wallets/global-wallets/integrate-a-global-wallet/overview Overview of integrating Privy global wallets into your application for cross-app wallet sharing. Privy allows apps to easily integrate embedded wallets from other apps. Users can verify ownership of wallet addresses or request signatures and transactions from wallets in other Privy apps. This reduces friction around having users transact onchain in your app, as users can easily pull in their assets and identity from other apps where they may already have embedded wallets. Within this setup, your app is known as the **requester** app. ## Quick start: 2 lines of code For apps already using RainbowKit, adding a global wallet takes just 2 lines: ```tsx theme={"system"} import {toPrivyWallet} from '@privy-io/cross-app-connect/rainbow-kit'; const abstractWallet = toPrivyWallet({ id: 'clq4ktn2s0008pjq97k77ixav', name: 'Abstract', iconUrl: 'https://app.abs.xyz/icon.png' }); ``` Add `abstractWallet` to the existing RainbowKit `wallets` array and users can connect. See the [RainbowKit integration guide](/wallets/global-wallets/integrate-a-global-wallet/rainbowkit-connector) for complete details. > **[RainbowKit connector](/wallets/global-wallets/integrate-a-global-wallet/rainbowkit-connector)** > > Using rainbowkit connector > **[ConnectKit connector](/wallets/global-wallets/integrate-a-global-wallet/connectkit-connector)** > > Custom connector for ConnectKit apps ## Integration approaches Apps can integrate global wallets in two ways: 1. **Using wallet connectors (recommended)**: Add global wallets to existing RainbowKit or ConnectKit setups without needing the Privy SDK 2. **Using Privy SDK hooks**: Full control over login, linking, and wallet interactions using `useCrossAppAccounts` Choose wallet connectors for the simplest integration. Choose Privy SDK hooks for custom authentication flows or when the app already uses Privy. ## Finding available providers To integrate embedded wallets from another app, first visit the [**Privy Dashboard**](https://dashboard.privy.io) and navigate to User management > Global Wallet > [**Integrations**](https://dashboard.privy.io/apps?tab=integrations\&page=ecosystem) tab to see a list of provider app IDs that consent to sharing their wallets with other apps. For any providers you'd like to integrate, note down the provider's Privy app ID, as you will use this value in the interfaces outlined below. > [!TIP] > > Some providers may only consent to sharing their users' wallets in read-only mode, in which case > your app can verify that the user owns a particular address, but cannot request signatures or > transactions from it. # RainbowKit connector Source: https://docs.privy.io/wallets/global-wallets/integrate-a-global-wallet/rainbowkit-connector Integrate global wallets into RainbowKit using the cross-app connect SDK ## Complete integration guide The sections below show the full integration steps. If the app already has RainbowKit configured, skip to [step 2](#2-add-the-global-wallet-connector). Otherwise, follow all steps to set up RainbowKit from scratch. > [!TIP] > > See our > [`privy-io/cross-app-connect-demo`](https://github.com/privy-io/examples/tree/main/examples/privy-next-cross-app-connect) > repo for an example set up. > **Don't have RainbowKit yet? Start here** > > If the app doesn't use RainbowKit yet, follow steps 1-4 below to set up both RainbowKit and the global wallet connector together. > > If the app already has RainbowKit, skip to [step 2](#2-add-the-global-wallet-connector) to just add the global wallet. ## 1. Install dependencies Install the [`@privy-io/cross-app-connect`](https://www.npmjs.com/package/@privy-io/cross-app-connect) SDK and its peer dependencies: ```bash theme={"system"} npm i @privy-io/cross-app-connect wagmi @rainbow-me/rainbowkit @tanstack/react-query ``` ## 2. Create the global wallet connector Import the `toPrivyWallet` connector function from `@privy-io/cross-app-connect/rainbow-kit`: ```tsx theme={"system"} import {toPrivyWallet} from '@privy-io/cross-app-connect/rainbow-kit'; ``` Use the `toPrivyWallet` method to create a wallet connector. The function takes `id`, `name`, and `iconUrl` (described below) and returns a connector that RainbowKit will use to connect to the provider wallet. ### Parameters **id** `string` *required* Privy app ID for the provider application. **name** `string` *required* The name of the Privy provider application. **iconUrl** `string` The URL for the icon that will appear in the modal. Call this method within RainbowKit's `connectorsForWallets` function like so: ```tsx theme={"system"} import {connectorsForWallets} from '@rainbow-me/rainbowkit'; import {toPrivyWallet} from '@privy-io/cross-app-connect/rainbow-kit'; const connectors = connectorsForWallets( [ { groupName: 'Recommended', wallets: [ toPrivyWallet({ id: 'privy-wallet-app-id', name: 'Privy wallet app name', iconUrl: 'https://example.com/image.png' }) ] } ], { appName: 'Privy', projectId: 'Demo' } ); ``` Then, pass this array of connectors to your wagmi configuration. ```tsx theme={"system"} import {createConfig, http} from 'wagmi'; import {mainnet} from 'wagmi/chains'; export const config = createConfig({ chains: [mainnet], transports: { [mainnet.id]: http() }, connectors, ssr: true }); ``` This `config` will be passed to the `WagmiProvider` in the next step. ## 3. Wrap app with providers At the highest level of your applications, wrap the component with the `wagmi`, `QueryClient`, and `RainbowKit` providers. Pass the configuration you created in step 2 to the `wagmi` provider. ```tsx theme={"system"} import {RainbowKitProvider} from '@rainbow-me/rainbowkit'; import {QueryClient, QueryClientProvider} from '@tanstack/react-query'; import type {AppProps} from 'next/app'; import {WagmiProvider} from 'wagmi'; import {config} from '../wagmi'; const client = new QueryClient(); function MyApp({Component, pageProps}: AppProps) { return ( ); } export default MyApp; ``` ### Complete example All together, this should look like: **config.ts** ```tsx theme={"system"} import {connectorsForWallets} from '@rainbow-me/rainbowkit'; import {createConfig, http} from 'wagmi'; import {mainnet} from 'wagmi/chains'; import {toPrivyWallet} from '@privy-io/cross-app-connect/rainbow-kit'; const connectors = connectorsForWallets( [ { groupName: 'Recommended', wallets: [ toPrivyWallet({ id: 'privy-wallet-app-id', name: 'Privy wallet app name', iconUrl: 'https://example.com/image.png' }) ] } ], { appName: 'Privy', projectId: 'Demo' } ); export const config = createConfig({ chains: [mainnet], transports: { [mainnet.id]: http() }, connectors, ssr: true }); ``` **providers.tsx** ```tsx theme={"system"} import {RainbowKitProvider} from '@rainbow-me/rainbowkit'; import {QueryClient, QueryClientProvider} from '@tanstack/react-query'; import type {AppProps} from 'next/app'; import {WagmiProvider} from 'wagmi'; import {config} from '../wagmi'; const client = new QueryClient(); function MyApp({Component, pageProps}: AppProps) { return ( ); } ``` ## 4. Use the `ConnectButton` Import the `ConnectButton` and use to prompt users to connect to their provider Privy wallet. ```tsx theme={"system"} import {ConnectButton} from '@rainbow-me/rainbowkit'; function Page() { return (

My app

...
); } ``` Thats it! You can now use any wagmi hook in your application to interact with the connected wallet. When users connect and transact with their wallet, Privy will open a pop-up for users to authorize any actions. # Using global wallets Source: https://docs.privy.io/wallets/global-wallets/integrate-a-global-wallet/using-global-wallets **React** > [!NOTE] > > When you request a signature or transaction from a user's embedded wallet from another app, Privy **requires** the user to explicitly confirm the signature or transaction. This is accomplished by opening a popup to the provider app's domain, where the user confirms the action in an isolated environment. To request signatures and transactions from a user's embedded wallet from a provider app, use the `signMessage`, `signTypedData`, and `sendTransaction` methods returned by Privy's `useCrossAppAccounts` hook ```tsx theme={"system"} import {useCrossAppAccounts} from '@privy-io/react-auth'; const {signMessage, signTypedData, sendTransaction} = useCrossAppAccounts(); ``` These methods are similar to the [`signMessage`](/wallets/using-wallets/ethereum/sign-a-message), [`signTypedData`](/wallets/using-wallets/ethereum/sign-typed-data), and [`sendTransaction`](/wallets/using-wallets/ethereum/send-a-transaction) methods returned by `usePrivy` except they all require an additional `CrossAppWalletOptions` object of the following type: **address** `string` *required* The address for the cross-app embedded wallet that you'd like to request a signature/transaction from. If the `address` you specify in this `CrossAppWalletOptions` object is not a valid embedded wallet that has been linked to the current user from a provider app, these wallet methods will error. ## signMessage To the `signMessage` method returned by `useCrossAppAccounts`, pass the following parameters: **message** `string` *required* The message the user must sign as a `string`. **options** `CrossAppWalletOptions` *required* Options for the cross-app embedded wallet, which must include the requested wallet's address. **properties** **address** `string` *required* The address for the cross-app embedded wallet that you'd like to request a signature/transaction from. ## signTypedData To the `signTypedData` method returned by `useCrossAppAccounts`, pass the following parameters: **typedData** `SignedTypedDataParams` *required* A JSON object that conforms to the EIP-712 [`TypedData JSON schema.`](https://eips.ethereum.org/EIPS/eip-712#specification-of-the-eth_signtypeddata-json-rpc) **options** `CrossAppWalletOptions` *required* Options for the cross-app embedded wallet, which must include the requested wallet's address. **properties** **address** `string` *required* The address for the cross-app embedded wallet that you'd like to request a signature/transaction from. ## sendTransaction To the `sendTransaction` method returned by `useCrossAppAccounts`, pass the following parameters: ```tsx theme={"system"} import {usePrivy, useCrossAppAccounts} from '@privy-io/react-auth'; function Button() { const {user} = usePrivy(); const {sendTransaction} = useCrossAppAccounts(); const crossAppAccount = user.linkedAccounts.find((account) => account.type === 'cross_app'); const address = crossAppAccount.embeddedWallets[0].address; return ( ); } ``` **requestData** `UnsignedTransactionRequest` *required* The transaction request to be sent. **options** `CrossAppWalletOptions` *required* Options for the cross-app embedded wallet, which must include the requested wallet's address. **properties** **address** `string` *required* The address for the cross-app embedded wallet that you'd like to request a signature/transaction from. ### Example signature request As an example, you might request a signature from a user's cross-app wallet like so: ```tsx theme={"system"} import {usePrivy, useCrossAppAccounts} from '@privy-io/react-auth'; function Button() { const {user} = usePrivy(); const {signMessage} = useCrossAppAccounts(); const crossAppAccount = user.linkedAccounts.find((account) => account.type === 'cross_app'); const address = crossAppAccount.embeddedWallets[0].address; return ( ); } ``` **React Native** > [!NOTE] > > When you request a signature or transaction from a user's embedded wallet from another app, Privy > **requires** the user to explicitly confirm the signature or transaction. This is accomplished by > opening a popup to the provider app's domain, where the user confirms the action in an isolated > environment. To request signatures and transactions from a user's embedded wallet from a provider app, use the `signMessage`, `signTypedData`, and `sendTransaction` methods returned by Privy's hooks: `useSignMessageWithCrossApp`, `useSignTypedDataWithCrossApp` and `useSendTransactionWithCrossApp`. ```tsx theme={"system"} const {signMessage} = useSignMessageWithCrossApp(); const {signTypedData} = useSignTypedDataWithCrossApp(); const {sendTransaction} = useSendTransactionWithCrossApp(); ``` These methods all require an additional `address` property, and an optional `redirectUri`, of the following types: **address** `string` *required* The address for the cross-app embedded wallet that you'd like to request a signature/transaction from. **redirectUri** `string` A URL path that the provider will automatically redirect to after successful authentication. This defaults to a link back to your app root, eg. `'/'`, if not provided. If the `address` you specify in this `CrossAppWalletOptions` object is not a valid embedded wallet that has been linked to the current user from a provider app, these wallet methods will error. ### signMessage To the `signMessage` method returned by \`useSignMessageWithCrossApp\`\`, pass the following parameters: ```tsx theme={"system"} import {usePrivy, useSignMessageWithCrossApp} from '@privy-io/expo'; function Button() { 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 ( ); } ``` **message** `string` *required* The message the user must sign as a `string`. **address** `string` *required* The address for the cross-app embedded wallet that you'd like to request a signature/transaction from. **redirectUri** `string` A URL path that the provider will automatically redirect to after successful authentication. This defaults to a link back to your app root, eg. `'/'`, if not provided. ### signTypedData To the `signTypedData` method returned by `useSignTypedDataWithCrossApp`, pass the following parameters: ```tsx theme={"system"} import {usePrivy, useSignMessageWithCrossApp} from '@privy-io/expo'; function Button() { const {user} = usePrivy(); const {signTypedData} = useSignTypedDataWithCrossApp(); const crossAppAccount = user.linked_accounts.find((account) => account.type === 'cross_app'); const address = crossAppAccount.embedded_wallets[0].address; return ( ); } ``` **typedData** `TypedDataDefinition` *required* A JSON object that conforms to the EIP-712 [`TypedData JSON schema.`](https://eips.ethereum.org/EIPS/eip-712#specification-of-the-eth_signtypeddata-json-rpc) **address** `string` *required* The address for the cross-app embedded wallet that you'd like to request a signature/transaction from. **redirectUri** `string` A URL path that the provider will automatically redirect to after successful authentication. This defaults to a link back to your app root, eg. `'/'`, if not provided. ### sendTransaction To the `sendTransaction` method returned by `useSendTransactionWithCrossApp`, pass the following parameters: ```tsx theme={"system"} import {usePrivy, useSendTransactionWithCrossApp} from '@privy-io/expo'; import {Button as RNButton} from 'react-native'; function Button() { const {user} = usePrivy(); const {sendTransaction} = useSendTransactionWithCrossApp(); const crossAppAccount = user.linked_accounts.find((account) => account.type === 'cross_app'); const address = crossAppAccount.embedded_wallets[0].address; return ( sendTransaction({ address, transaction: { to: '0xRecipientAddress', value: 1000000000000000000, gasLimit: 21000, gasPrice: 20000000000, nonce: 0, data: '0x' } }) } disabled={!address} /> ); } ``` **transaction** `UnsignedTransactionRequest` *required* The transaction request to be sent. **address** `string` *required* The address for the cross-app embedded wallet that you'd like to request a signature/transaction from. **redirectUri** `string` A URL path that the provider will automatically redirect to after successful authentication. This defaults to a link back to your app root, eg. `'/'`, if not provided. # Custom experience Source: https://docs.privy.io/wallets/global-wallets/launch-your-wallet/custom-experience > [!WARNING] > > **This is an advanced feature.** It is recommended that you reach out to the Privy team if you > are interested in building a custom global wallet experience. Privy allows you to build your own custom global wallet experience, instead of using Privy's default user interfaces. To build your own experience, you will create two new pages using methods from the `@privy-io/cross-app-provider` library to communicate via the Privy cross-app protocol. Your app handles the user interfaces, and Privy handles the rest. > [!NOTE] > > The `@privy-io/cross-app-provider` SDK is a vanilla JavaScript library. We recommend using this > library alongside `@privy-io/react-auth` for the best development experience. This guide highlights the core interfaces of the `@privy-io/cross-app-provider` SDK and how they integrate with the **React SDK**. For a more complete reference implementation, see the [**cross-app provider starter repo**](https://github.com/privy-io/examples/tree/main/examples/privy-next-cross-app-provider). There are two clients in this SDK depending on what style of cross app you are looking to implement. Privy supports both connect and auth mode, where connect allows users to only connect their wallet via a Wagmi connector and auth mode which allows users to login or link with their Privy account. Connect mode uses the [Mobile Wallet Protocol](https://mobilewalletprotocol.github.io/wallet-mobile-sdk/docs/spec) to connect the wallet, and auth mode uses the [OAuth2 protocol](https://oauth.net/2/) to securely authenticate the user across apps. **Connect mode** **1. Set up a cross-app provider client** ```tsx theme={"system"} // client.ts import {createClient} from '@privy-io/cross-app-provider/connect'; export const client = createClient({ appId: '', appClientId: '', // e.g. https://privy.your-app.com privyDomain: '' }); ``` **2. Build the connect experience** Allow user's to accept (or reject) a cross-app connection request by using the `client` to handle parsing input and responging to the requesting app. First, use the `client.getConnectionRequestFromUrlParams` method when the page loads to read the request into your app. ```tsx theme={"system"} const connectionRequest = client.getConnectionRequestFromUrlParams(); ``` > **The connection request** > > A connection request object contains: > > * the `requesterPublickey`, used to generate the shared secret for encrypted communication > * the `callbackUrl`, used to identify the requester and as the `targetOrigin` > in calls to `window.postMessage` Then, use `client.acceptConnection` and `client.rejectConnection` to allow your users to explicitly approve or deny the connection request respectively via clicking a button like so: ```tsx theme={"system"} // Approve ``` ```tsx theme={"system"} // Deny ``` **3. Build the request experience** This page allows you to control the experience of a global wallet request. First read the request using `client.getVerifiedWalletRequest` to decrypt, parse and verify the input. This method will also return the `connection` object used to communicate a result back to the requester. ```tsx theme={"system"} const {request, connection} = await client.getVerifiedWalletRequest({ userId: user.id }); ``` Once you have the result of handling this request you can return the response to the requester using `client.handleRequestResult`, for example responding to a signature request: ```tsx theme={"system"} const message = request.params[0]; const {signature} = await signMessage({message}); await client.handleRequestResult({ accessToken, result: signature, connection }); ``` Similarly, if the user rejects the request use `client.rejectRequest` to send an error message to the requester: ```tsx theme={"system"} await client.rejectRequest({ accessToken, callbackUrl: connection.callbackUrl }); ``` If there are other errors (e.g. not enough funds for a transaction) use the `client.handleError` method to pass the error back to the requester: ```tsx theme={"system"} await client.handleError({ accessToken, error: new Error(''), callbackUrl: connection.callbackUrl }); ``` **4. Upgrade your clients** Ensure client apps are using the [latest version](/changelogs/cross-app-connect) of `@privy-io/cross-app-connect` **5. Set up your custom URLs in the Privy Dashboard** > [!TIP] > > Prior to enabling the custom URLs in your dashboard, ensure that your integration is ready to > receive production traffic, including [setting a strong > CSP](/security/implementation-guide/content-security-policy) and adding your custom URLs as > allowed domains for your Privy app. From the Privy dashboard, navigate to [**Global Wallet > Advanced**](https://dashboard.privy.io/apps?page=ecosystem\&tab=advanced). Enable `Custom URLs`, input the URL(s) where your pages are hosted, and save your changes. **Auth mode** **1. Set up a cross-app provider client** ```tsx theme={"system"} // client.ts import {createClient} from '@privy-io/cross-app-provider/auth'; export const client = createClient({ appId: '', appClientId: '', // e.g. https://privy.your-app.com privyDomain: '' }); ``` **2. Build the connect experience** Allow user's to accept (or reject) a cross-app connection request by using the `client` to handle parsing input and responging to the requesting app. First, use the `client.getConnectionRequestFromUrlParams` method when the page loads to read the request into your app. ```tsx theme={"system"} const connectionRequest = client.getConnectionRequestFromUrlParams(); ``` Then, use `client.acceptConnection` and `client.rejectConnection` to allow your users to explicitly approve or deny the connection request respectively via clicking a button like so: ```tsx theme={"system"} // Approve ``` ```tsx theme={"system"} // Deny ``` **3. Build the request experience** This page allows you to control the experience of a global wallet request. First read the request using `client.getVerifiedTransactionRequest` to validate the JWT and request. This method will return a boolean `verified` and a `data` object. `verified` will be false if the JWT is invalid and the request should be ignored. In this case, the `data` object will contain a `callbackUrl` you can passed to `handleError` to gracefully exit the page. If the request is valid, the `data` object will contain a `request` and a `token` object which you can use to handle the request. ```tsx theme={"system"} const {verified, data} = await client.getVerifiedTransactionRequest({ accessToken }); ``` Once you have the result of handling this request you can return the response to the requester using `client.handleRequestResult`, for example responding to a signature request: ```tsx theme={"system"} const message = request.params[0]; const {signature} = await signMessage({message}); await client.handleRequestResult({ accessToken, result: signature, connection }); ``` Similarly, if the user rejects the request use `client.rejectRequest` to send an error message to the requester: ```tsx theme={"system"} await client.rejectRequest({ accessToken, callbackUrl: request.callbackUrl }); ``` If there are other errors (e.g. not enough funds for a transaction) use the `client.handleError` method to pass the error back to the requester: ```tsx theme={"system"} await client.handleError({ accessToken, error: new Error(''), callbackUrl: request.callbackUrl }); ``` **4. Upgrade your clients** Ensure client apps are using the [latest version](/changelogs/cross-app-connect) of `@privy-io/cross-app-connect` **5. Set up your custom URLs in the Privy Dashboard** > [!TIP] > > Prior to enabling the custom URLs in your dashboard, ensure that your integration is ready to > receive production traffic, including [setting a strong > CSP](/security/implementation-guide/content-security-policy) and adding your custom URLs as > allowed domains for your Privy app. From the Privy dashboard, navigate to [**Global Wallet > Advanced**](https://dashboard.privy.io/apps?page=ecosystem\&tab=advanced). Enable `Custom URLs`, input the URL(s) where your pages are hosted, and save your changes. That's it, now your global wallet has a fully custom experience! # Overview Source: https://docs.privy.io/wallets/global-wallets/launch-your-wallet/overview Launch your app as a global wallet provider by enabling a base domain for shared wallets. **Providers** are apps that enable their users' embedded wallets to be used in other apps. Becoming a provider enables your users to use their embedded wallets and assets from your app elsewhere, allowing them to transact from an existing balance, aggregate assets, and more. > [!NOTE] > > Global wallet provider access is a gated feature. Before implementing global wallets as a > provider, request access by navigating to [Global wallet > My > app](https://dashboard.privy.io/apps?page=ecosystem) in the Privy Dashboard and clicking the > **Request access** button. > [!TIP] > > Before sharing your users' wallets as a provider app, you must enable a **base domain** for your app. > > **Enabling a base domain helps secure your users' wallets in a cross-app context.** Read more about [security for cross-app wallets](/wallets/global-wallets/overview) and follow [this guide](/recipes/react/cookies) to set up a base domain! ## Configuring your app as a provider Once approved to be a global wallet provider, you can configure your app to share your users' embedded wallets with other apps. To become a provider and share your users' embedded wallets with other apps, confirm that the following prerequisites are complete in the **Privy Dashboard** (navigate to the **User management > [Global Wallet](https://dashboard.privy.io/apps?page=ecosystem)** section): 1. App must be production. 2. HTTPOnly cookie domain must be enabled and verified. 3. App logo must be uploaded. You can then enable **Make my wallets available for other apps to integrate** to see your app displayed on the [Integrations](https://dashboard.privy.io/apps?tab=integrations\&page=ecosystem) tab for other Privy apps to integrate. ## Transaction scanning As an additional security feature, Privy is integrated with [Blockaid](https://www.blockaid.io/) transaction scanning to ensure that all transactions from your users' global wallets are safe and secure. Transaction scanning has two levels of security; **validation** and **simulation**. With transaction validation, users will be notified if the transaction has been flagged by Blockaid as suspicious or malicious. Blockaid will also simulate the transaction with the wallet, showing the token and USD value or exposure difference if the transaction is approved. ![Transaction scanning UI](https://mintcdn.com/privy-c2af3412/zlmLhiIqRR7ViKN0/images/transaction-scan.png?fit=max\&auto=format\&n=zlmLhiIqRR7ViKN0\&q=85\&s=7eb889f9851cd020bee447b17c5c6c19) To enable transaction scanning for your global wallet users, navigate to the [Global wallet > My app](https://dashboard.privy.io/apps?page=ecosystem) page of the Privy Dashboard and toggle the **Blockaid transaction simulation** setting on. Privy has a default API key your app can use for testing but to prevent rate limiting your app should **configure its own Blockaid API key** in the dashboard. ## Read-only mode When sharing your users' embedded wallets with other apps, your users are protected from any malicious developer. Usage of embedded wallets across apps are strictly domain-segregated and every user action requires explicit consent in a third-party app. If you would like to limit the scope of your users' wallets to only be read-only in other apps, click the **Read-only mode** checkbox within the [**My app**](https://dashboard.privy.io/apps?tab=app\&page=ecosystem) tab. With this setting enabled, requester apps may see your user's wallet addresses, but not request signatures or transactions from them. This may be particularly useful for setups where users verifying ownership of assets from your app in other apps, but not necessarily transacting with them. # Overview Source: https://docs.privy.io/wallets/global-wallets/overview Global wallets enable users to use a single wallet identity across multiple Privy-powered applications. Privy embedded wallets can be made interoperable across apps, making it easy for you to launch your own **global wallet**. In this setup, global wallets foster a cross-app ecosystem where users can easily port their wallets from one app to another, including by integrating wallet connector solutions like RainbowKit and wagmi. Using **global wallets**, users can seamlessly move assets between different apps and can easily prove ownership of, sign messages, or send transactions with their existing wallets. > [!NOTE] > > Global wallet provider access is an Enterprise feature. Before implementing global wallets as a > provider, request access by navigating to [Global wallet > My > app](https://dashboard.privy.io/apps?page=ecosystem) in the Privy Dashboard and clicking the > **Request access** button. After requesting access, reach out to > [sales@privy.io](mailto:sales@privy.io) with your app ID. ![images/Crossapp.png](https://mintcdn.com/privy-c2af3412/YvGXGsI-T4KAqoan/images/Crossapp.png?fit=max\&auto=format\&n=YvGXGsI-T4KAqoan\&q=85\&s=a56d26b5b4a09624aec9726697762e15) ## Providers and requesters Suppose that Alice is logged in to **App A** and wants to connect with her **App B** wallet to prove she owns an asset. In this setup: * App A is the **requester app**: it *requests* access to a third-party wallet. * App B is the **provider app**: it *provides* access to embedded wallets generated on its app. The **provider** and **requester** nomenclature will be used throughout this documentation and the SDK interfaces. ## Choosing your role Apps can be a provider, a requester, or both, depending on the cross-app wallet experience they want to enable. Use the flowchart below to determine which role fits the app's needs. ```mermaid theme={"system"} %%{init: {'theme':'base', 'themeVariables': {'primaryColor':'#e1f5ff','primaryTextColor':'#1a1a1a','primaryBorderColor':'#0066cc','lineColor':'#666666','secondaryColor':'#d4f1d4','tertiaryColor':'#fff4d4','noteBkgColor':'#f0e5ff','noteTextColor':'#1a1a1a'}}}%% flowchart TD Start([What does the app need?]) --> Question1{Does the app have users
with embedded wallets?} Question1 -->|No| Requester1[Integrate as a
REQUESTER] Question1 -->|Yes| Question2{Should users access
their wallets from
this app in other apps?} Question2 -->|Yes| Provider[Become a PROVIDER
Share app ID] Question2 -->|No| Question3{Should the app accept
wallets from other apps?} Question3 -->|No| Neither[No global wallet
integration needed] Question3 -->|Yes| Both[Configure as BOTH
Provider + Requester] Requester1 --> RequesterDetails[Requester integration
━━━━━━━━━━━━━━━
✓ Reference provider app IDs
✓ Users login/link wallets
✓ 2 lines of code RainbowKit
✓ No Privy SDK required] Provider --> ProviderQ{Allow transactions or
just address verification?} ProviderQ -->|Just verification| ProviderRO[Provider: Read-only mode
━━━━━━━━━━━━━━━
✓ Enable in Dashboard
✓ Set read-only mode
✓ Users prove ownership
✗ No signatures/transactions] ProviderQ -->|Allow transactions| ProviderFull[Provider: Full mode
━━━━━━━━━━━━━━━
✓ Enable in Dashboard
✓ Full transaction mode
✓ Enable Blockaid scanning
✓ Users explicitly consent] Both --> BothDetails[Dual integration
━━━━━━━━━━━━━━━
As provider:
✓ Share app ID
✓ Users port wallets out

As requester:
✓ Accept partner wallets
✓ Reduce onboarding friction] classDef startClass fill:#e1f5ff,stroke:#0066cc,stroke-width:2px,color:#1a1a1a classDef providerClass fill:#d4f1d4,stroke:#2d8659,stroke-width:2px,color:#1a1a1a classDef requesterClass fill:#fff4d4,stroke:#cc9900,stroke-width:2px,color:#1a1a1a classDef bothClass fill:#f0e5ff,stroke:#8855cc,stroke-width:2px,color:#1a1a1a classDef neitherClass fill:#f5f5f5,stroke:#999999,stroke-width:2px,color:#1a1a1a classDef providerFullClass fill:#a8e6a8,stroke:#2d8659,stroke-width:2px,color:#1a1a1a class Start startClass class Provider,ProviderRO providerClass class Requester1,RequesterDetails requesterClass class Both,BothDetails bothClass class Neither neitherClass class ProviderFull providerFullClass ``` ### Provider vs. requester comparison | Feature | Provider | Requester | | - | - | - | | **Setup** | Enable toggle in Dashboard | Reference provider app IDs in code | | **Code required** | Dashboard configuration only | 2 lines with RainbowKit | | **Privy SDK needed** | Yes | Optional | | **User flow** | Users consent to share wallets | Users link or login with wallets | | **Security control** | Set read-only mode, enable Blockaid | Inherits provider's settings | | **Dashboard location** | My app tab | Integrations tab | | **Use case** | Enable cross-app wallet ecosystem | Reduce onboarding friction | > **When to become a provider** > > An app should become a provider if it: > > * Has existing users with embedded wallets that could be valuable in other apps > > * Wants to build network effects by enabling users to leverage their wallets across partner apps > > * Wants to increase user retention through cross-app utility > > * Wants to control security settings like read-only mode or Blockaid transaction scanning > > > [!NOTE] > > > > Global wallet provider access is an Enterprise feature. Before implementing global wallets as a > > provider, request access by navigating to [Global wallet > My > > app](https://dashboard.privy.io/apps?page=ecosystem) in the Privy Dashboard and clicking the > > **Request access** button. After requesting access, reach out to > > [sales@privy.io](mailto:sales@privy.io) with your app ID. > > **Setup:** Navigate to Dashboard > Global Wallet > My app and enable the toggle. > **When to integrate as a requester** > > An app should integrate as a requester if it: > > * Wants to reduce onboarding friction by letting users sign in with existing wallets > * Does not want to manage wallet creation, recovery, or infrastructure > * Wants users to bring assets and identity from other apps > * Wants quick integration using standard wallet connectors like RainbowKit or ConnectKit > > **Setup:** Reference provider app IDs and use `toPrivyWallet()` or Privy hooks. > **When to be both provider and requester** > > An app should configure both roles if it: > > * Wants maximum wallet interoperability in both directions > * Is building a partnership ecosystem with other apps > * Wants users to both import wallets from partners and export wallets to partners > * Wants to tap into other ecosystems while building its own > > **Setup:** Enable provider settings in Dashboard and integrate requester code for partner wallets. ## User consent and security > [!NOTE] > > Privy requires that users explicitly confirm all wallet actions in a cross-app context. **Global wallets are built to safeguard user privacy and security.** No app developer can view user assets or learn about their address without both: * The provider app opting into cross-app flows. * The user explicitly consenting to share their wallet information with the requester app. By enabling cross-app functionality, the provider's Privy app (hosted on an isolated subdomain) acts as an OAuth-compliant authentication provider. This means requesting apps can initiate the connection, and if the user approves: * Users are granted a custom access token to make future requests to the provider wallet * The user's wallet addresses are then attached to the requester's user object as a new cross-app linked account * If the provider allows for the wallet to be used for signatures and transactions, the requester can request signatures and transactions using the custom access token. Providers can also choose to make their wallets available in read-only mode. Privy enables the provider to opt into cross-app wallets in **read-only** mode, enabling the requester app to view the user's wallet address but not prompt the user to transact. If transactions are enabled, the user will always be redirected to the isolated subdomain to explicitly approve them, in addition to needing to be logged in to the provider site and holding the custom access token. Concretely, this means that when a requester app requests a signature or transaction from a user's cross-app wallet, Privy will open up a pop-up to the isolated subdomain, where **the user must confirm the action explicitly.** This means requesters cannot customize wallet prompts when interacting with a provider wallet, and cannot prompt users to export private keys from a provider wallet. # Overview Source: https://docs.privy.io/wallets/overview Overview of Privy wallet infrastructure for transacting on Ethereum, Solana, Tempo, Base, and hundreds of blockchains Privy builds wallet infrastructure that empowers users and applications to transact on hundreds of blockchains, including Ethereum, Solana, Tempo, Base, [and more](/wallets/overview/chains). These wallets can be **embedded** within your application to have users interact with them directly, or they can be **controlled by your servers** via Privy's API. Use Privy to instantly spin up non-custodial wallets for your users or create a wallet fleet of your own. Privy embedded wallets are built on **globally distributed infrastructure to ensure high uptime and low latency**. They leverage **secure hardware (TEEs)** to ensure only the rightful owner can control their wallet or access its keys. Privy also supports users connecting external wallets (like Metamask or Phantom) to your app so they can bring their assets and online identity with them to your product if they already have a wallet. ![images/wallets-splash.png](https://mintcdn.com/privy-c2af3412/IQmti1WCL7AzN0e1/images/wallets-splash.png?fit=max\&auto=format\&n=IQmti1WCL7AzN0e1\&q=85\&s=0bf8d5b7a1a494f7de5aa5cde903919e) # Chain support Source: https://docs.privy.io/wallets/overview/chains Supported blockchains, support tiers, and integration guides for Privy wallets Privy wallets support multiple blockchain ecosystems across **three tiers**. Each tier builds on the one before it. The higher the tier, the more Privy manages on your app's behalf. ## Capabilities by tier | Capability | Tier 1 | Tier 2 | Tier 3 | | - | :-: | :-: | :-: | | Create and export wallets | ✓ | ✓ | ✓ | | Sign hashes and messages | ✓ | ✓ | ✓ | | Sign transactions | — | ✓ | ✓ | | Send transactions | — | — | ✓ | Additional capabilities — policies, gas sponsorship, observability, wallet actions — vary independently by chain and are not part of the tier model. Some still require a minimum tier. See [Additional capabilities](#additional-capabilities). ## Supported chains ### Tier 3: Send transactions Privy signs, broadcasts, and tracks transactions on these chains. > **[Ethereum](/wallets/using-wallets/ethereum/send-a-transaction)** > > Includes EVM-compatible networks > **[Solana](/wallets/using-wallets/solana/send-a-transaction)** > > Includes SVM-compatible networks > **[Tempo](/wallets/using-wallets/tempo/send-a-transaction)** > > Includes Tempo mainnet and Tempo Moderato > **[Tron](/wallets/using-wallets/tron/send-a-transaction)** > > TRC-20 assets including USDT > [!TIP] > > Learn more about configuring your app for different > [EVM](/basics/react/advanced/configuring-evm-networks) and > [Solana](/basics/react/advanced/configuring-solana-networks) networks. ### Tier 2: Sign transactions Privy decodes and signs transactions. > **[XRPL](/wallets/using-wallets/xrpl/overview)** > > XRP Ledger transactions with policy controls > **[Sui](/wallets/using-wallets/other-chains/sui)** > > Move-based layer 1 > **[Stellar](/recipes/tier-2-wallet-integration#stellar)** > > Payment-focused layer 1 > **[Aptos](/recipes/tier-2-wallet-integration#aptos)** > > Move-based layer 1 > **[Near](/recipes/tier-2-wallet-integration#near)** > > Sharded layer 1 ### Tier 1: Sign Privy creates wallets and produces cryptographic signatures. > **[Bitcoin](/wallets/using-wallets/bitcoin/sign-transaction-inputs)** > > Segwit and taproot transaction inputs > **[Cosmos](/recipes/tier-1-wallet-integration#cosmos)** > > Cosmos SDK app chains > **[Ton](/recipes/tier-1-wallet-integration#ton)** > > The Open Network > **[Starknet](/recipes/tier-1-wallet-integration#starknet)** > > Starknet curve wallets > **[Pearl](/wallets/using-wallets/other-chains)** > > HD-derived Pearl wallets > **[Other Ed25519 and secp256k1 chains](/wallets/using-wallets/other-chains)** > > Any chain on a supported curve ### Specialized integrations Some managed integrations do not map directly to the transaction lifecycle represented by the tier model. > **[Spark](/recipes/spark-btc-guide)** > > Managed Spark transfers, Lightning payments, and Bitcoin deposits ## Tier definitions ### Tier 3: Send transactions * **Transaction construction** — Privy constructs supported transactions from higher-level inputs and resolves chain-dependent fields when required. * **Managed submission** — Privy signs, broadcasts, and tracks transactions, with [transaction-status webhooks](/wallets/gas-and-asset-management/assets/transaction-event-webhooks) for lifecycle updates. Tier 3 is the minimum threshold for anything that acts at broadcast time, such as fee payment, bundler routing, batching, and lifecycle tracking. ### Tier 2: Sign transactions * **Transaction-aware signing** — Privy decodes and validates the transaction before signing. Tier 2 is the minimum threshold for transaction-level policy controls, because Privy must decode a transaction before it can evaluate conditions on the transaction's contents. ### Tier 1: Sign Privy provides these capabilities across supported chains, through the signing interface that each chain exposes. * **Wallet creation** — generate a new keypair for a supported curve (secp256k1, Ed25519, or Starknet) * **Private key export** — retrieve the raw private key for non-custodial use or migration * **HD seed export** — export the seed phrase for HD-derived wallets (Ethereum, Solana, Bitcoin Taproot, and Pearl only) * **Import** — bring an existing [private key](/wallets/wallets/import-a-wallet/private-key) or [HD seed](/wallets/wallets/import-a-wallet/hd-wallets) under Privy management, on a narrower set of chains than export * **Low-level signing** — produce signatures appropriate to the wallet type ## Additional capabilities A capability that depends on Privy decoding, building, or submitting a transaction is unavailable below the tier that provides it. Reaching that tier does not by itself make the capability available. | Feature | Description | Minimum tier | | - | - | :-: | | Gas sponsorship, smart wallets, and batching | Chain-dependent managed transaction features. See [gas management](/wallets/gas-and-asset-management/gas/overview) and [EVM smart wallets](/wallets/using-wallets/evm-smart-wallets/overview). | Tier 3 | | Wallet actions | Managed [transfer, swap, and earn flows](/wallets/actions/overview). Cross-chain availability depends on provider coverage and liquidity; see the dynamically maintained [supported routes](/wallets/actions/swap/overview#cross-chain-swaps). | Tier 3 | | Transaction policies | [Controls](/controls/policies/overview) based on decoded transaction fields. Available conditions vary by chain and method. | Tier 2 | | Balances, history, and deposit/withdrawal webhooks | [Coverage](/wallets/gas-and-asset-management/assets/overview) varies by network and event. | Varies | | Fiat funding | [Provider-, region-, and configuration-dependent](/financial-flows/deposits). | Outside tier model | > [!NOTE] > > Privy is continuously adding new chains and expanding capabilities on existing chains. Contact > [support@privy.io](mailto:support@privy.io) with questions about current coverage or to request > that Privy prioritize a chain or capability. # Embedded wallets Source: https://docs.privy.io/wallets/overview/embedded Self-custodial wallets built into your app for Ethereum, Solana, and other chains — powered by secure hardware (TEEs) Privy's embedded wallet system lets you build wallets directly into your app whether you're building non-custodial wallets for your users or a wallet fleet you control. Privy's wallet infrastructure ensures only the appropriate party controls the wallet. This means you can set up wallets for any user, customer, business, or agent under a range of custody options. Embedded wallets can be configured as [non-custodial](/wallets/overview/embedded) or [custodial](/wallets/custodial-wallets/overview). Privy surfaces both user-centric abstractions enabling you to authenticate users and generate wallets for them, as well as wallet-centric abstractions whereby you can create wallets with assigned authorization keys to control them. ![images/embedded-wallets-splash.png](https://mintcdn.com/privy-c2af3412/IQmti1WCL7AzN0e1/images/embedded-wallets-splash.png?fit=max\&auto=format\&n=IQmti1WCL7AzN0e1\&q=85\&s=6a3d0548d716f4899efe6e4f9d7bc192) ## Features Privy's wallet infrastructure gives you the flexibility to manage key signing directly or integrate onchain infrastructure like smart accounts out of the box. Wallets ship with: * **Cross-chain usage**: Create and manage wallets on all EVM- and SVM-compatible blockchains, including Ethereum, Solana, Tempo, Base, Arbitrum, HyperEVM, and Eclipse. Privy also supports [many other chains](/wallets/overview/chains), such as Bitcoin, Spark, TRON, Stellar, and more. * **Robust transaction controls**: Execute arbitrary transactions with wallets, such as transferring funds and interactions with smart contracts. Make transactions idempotent to ensure that they are only submitted once in case of a retry. * **Onchain indexing**: Broadcast transactions onchain and register event listeners (via [webhooks](/wallets/gas-and-asset-management/assets/transaction-event-webhooks)) on transaction status, deposits, and withdrawals. * **Powerful policy engine**: Enforce granular policies what actions a wallet can take, set allowlisted contracts or recipients, maximum amounts to be transferred, restrictions on smart contract calldata, and more. Enforce MFA on transactions, require approval signatures from a quorum of parties, and more. [Learn more](/controls/overview) * **Authorization controls**: Cryptographically enforce a chain of required approvals on wallets from `m-of-n` parties to execute certain wallet actions. * **Automated gas sponsorship**: Never worry about topping up a wallet. Keep wallets loaded to pay for transactions at all times. [Learn more](/wallets/gas-and-asset-management/overview) * **Rich onchain integrations**: Leverage features like Privy's wallet UI components, RainbowKit connector, transaction and balance webhooks, or automated gas management to streamline your integration with the blockchain. ## Common use cases View some common use cases for Privy. ### User wallets You can generate non-custodial wallets for your users for a wallet experience that is directly embedded in your application—no separate wallet client, like a browser extension or a mobile app, required. This means users have full custody of their wallets without needing to manage secret keys. Neither Privy nor your application ever sees the user's keys; secrets are only ever reconstituted in a secure environment under the user's control so they can sign messages or transactions. Users can manage their embedded wallet seamlessly with their account; they never need to handle any unnecessary technical complexity. Your application can even [pregenerate wallets](/recipes/pregenerate-wallets) for an account, like an email address or phone number, before the user logs in. Users can also [export the key](/wallets/wallets/export) for their embedded wallet, providing an escape hatch to leave Privy at any time. Your application can easily guide users to use their wallet with simple abstractions to prompt users to fund, transact, and sign with their wallet. Learn more about support for [user wallets](/wallets/overview/solutions/user-wallets). ### Organization wallets Set up a fleet of non-custodial and/or custodial wallets wallets to enable secure asset management for organizations and businesses. Multiple members of an organization can be provisioned access to the same wallet. Secure funds via quorums of pre-approved signers and policies with granular approval workflows. Register webhooks to automate events based on onchain actions, assign specific policies to wallet signers to manage risk, and more. Wallets leverage secure enclaves and key splitting to ensure secure key reconstitution and appropriate custody for all use cases. Learn more about support for [organization wallets](/wallets/overview/solutions/organization-wallets). ### Treasury Set up non-custodial or custodial wallets to manage your organization's treasury. Configure granular approval workflows to assign specific permissions to users over transactions, policy updates, workflow updates, and more. Initiate actions programmatically via the Privy API or manually via the Dashboard. Learn more about support for [treasury](/wallets/overview/solutions/treasury-wallets). ### Agentic wallets Privy enables you to securely provision wallets for agents. Agents can execute actions on users' behalf, like payments or trades, via natural language prompts from users or asynchronously while the user is offline. Users can configure granular policies to restrict agent spend and ensure that transactions are aligned with user preferences and intent. Learn more about support for [agent wallets](/wallets/overview/solutions/agent-wallets). ## Get started > **[Create a wallet](/wallets/wallets/create/create-a-wallet)** > > Create a wallet > **[Connect external wallets](/wallets/connectors/usage/connecting-external-wallets)** > > Connect external wallets to your app like MetaMask or Phantom > **[Provision signers](/wallets/using-wallets/signers/overview)** > > Take actions on behalf of your users by adding signers # Features Source: https://docs.privy.io/wallets/overview/features Key features of Privy wallets including signing, gas sponsorship, MFA, and cross-chain support Privy's wallet infrastructure gives you the flexibility to manage key signing directly or integrate onchain infrastructure like smart accounts out of the box. Wallets ship with: * **Cross-chain usage**: Create and manage wallets on all EVM- and SVM-compatible blockchains, including Ethereum, Solana, Tempo, Base, Arbitrum, HyperEVM, and Eclipse. Privy also supports [many other chains](/wallets/overview/chains), such as Bitcoin, Spark, TRON, Stellar, and more. * **Robust transaction controls**: Execute arbitrary transactions with wallets, such as transferring funds and interactions with smart contracts. Make transactions idempotent to ensure that they are only submitted once in case of a retry. * **Onchain indexing**: Broadcast transactions onchain and register event listeners (via [webhooks](/wallets/gas-and-asset-management/assets/transaction-event-webhooks)) on transaction status, deposits, and withdrawals. * **Powerful policy engine**: Enforce granular policies what actions a wallet can take, set allowlisted contracts or recipients, maximum amounts to be transferred, restrictions on smart contract calldata, and more. Enforce MFA on transactions, require approval signatures from a quorum of parties, and more. [Learn more](/controls/overview) * **Authorization controls**: Cryptographically enforce a chain of required approvals on wallets from `m-of-n` parties to execute certain wallet actions. * **Automated gas sponsorship**: Never worry about topping up a wallet. Keep wallets loaded to pay for transactions at all times. [Learn more](/wallets/gas-and-asset-management/overview) * **Rich onchain integrations**: Leverage features like Privy's wallet UI components, RainbowKit connector, transaction and balance webhooks, or automated gas management to streamline your integration with the blockchain. # Flexible custody Source: https://docs.privy.io/wallets/overview/flexible-custody Privy wallets support flexible custody options and can be operated under different models: * **Non-custodial wallets** ensure end users retain ultimate control of their private keys. * **Developer-controlled wallets** enable organizations to securely manage their treasury or run onchain infrastructure at scale. * [Custodial wallets](/wallets/custodial-wallets/overview) are backed by licensed providers that meet specific compliance or operational requirements. This list is non-exhaustive; blended configurations (such as a [non-custodial wallet granting signing permission to the developer](/wallets/using-wallets/signers/overview)) are also possible. This flexibility enables developers to tailor custody to the operational, regulatory, and risk requirements of their products and users. **Concretely, this means you can serve custodial and non-custodial experiences side-by-side within a single application, all with one API.** This approach reflects the diverse needs of products operating at global scale — from consumer-facing apps to enterprise systems requiring institutional-grade custody controls and compliance. Flexible custody empowers teams to operate globally and across product lines, delivering a best-in-class, secure experience throughout. ![Flexible custody](https://mintcdn.com/privy-c2af3412/pXSml95_xr74sgtS/images/flexible_custody.png?fit=max\&auto=format\&n=pXSml95_xr74sgtS\&q=85\&s=5e2e24d2717c2ad8a4b5d21a3a13ef38) ## Next steps > **[Custodial wallets](/wallets/custodial-wallets/overview)** > > Learn about Privy custodial wallets > **[Self-custodial and developer-controlled wallets](/wallets/wallets/create/create-a-wallet)** > > Create non-custodial and developer-controlled wallets # Agent wallets Source: https://docs.privy.io/wallets/overview/solutions/agent-wallets Privy enables you to provision wallets for AI agents. Agent wallets are designed for builders shipping agents that need to pay for APIs, move funds between accounts, execute onchain transactions, or interact with DeFi protocols. Privy provides the key management, authorization model, and policy controls to let agents transact safely within human-defined boundaries. Read below to learn how Privy powers agent wallets for autonomous trading systems, AI-powered payment pipelines, API monetization infrastructure, and general-purpose agentic applications. ## Authorization models Privy supports two authorization models depending on whether an agent controls its own wallet or acts on behalf of a user. **Agent-owned wallets** are wallets your agent provisions and controls. The agent holds the credential needed to trigger signing and can transact independently without prompting a user. This model suits fully autonomous agents that manage their own funds and operate outside any user session. **Delegated signing** lets an agent transact on behalf of a user's existing wallet. Users grant the agent an [authorization key](/controls/authorization-keys/owners/overview) with a defined scope via policies. the agent signs transactions within that scope, and the user can revoke access at any time. This model suits agents that act as a user's financial representative while the user retains ultimate custody. ## Policies Privy's [policy engine](/controls/policies/overview) allow you to enforce guardrails on how agents spend funds.Policies can restrict which tokens and chains an agent can transact on, cap transfer values per transaction or over a time window, allowlist recipient addresses and smart contracts, and more. ## Agent payments Privy supports both [MPP](/recipes/agent-integrations/mpp) and [x402](/recipes/agent-integrations/x402). These are HTTP-native payment protocols that let agents pay for APIs, data feeds, and digital content autonomously without a manual checkout flow. Privy's x402 and MPP clients intercept the 402 HTTP responses, sign payment authorizations with an agent's wallet, and retry the request automatically. This makes autonomous API monetization a drop-in behavior rather than a separate payment integration. Both protocols work with Privy server wallets and are supported across React (via `useX402Fetch`) and Node.js (via `createX402Client`), with per-request `maxValue` caps to prevent runaway spending. ## Tooling and framework integrations The [Privy Agent CLI](/recipes/agent-integrations/agent-cli) lets agents spin up, fund, and transact with wallets directly from the command line — a natural fit since agents already execute shell commands. The CLI uses a device authorization flow so a human approves wallet access in a browser once; subsequent transactions use short-lived ephemeral signing keys without ever exposing the private key to the agent process. Sessions last up to 30 days with automatic token rotation. Privy's wallet infrastructure is framework-agnostic and integrates with LangChain, Vercel AI SDK, OpenAI Agents SDK, AWS Bedrock AgentCore, and custom agent runtimes. For MCP-based agents, Privy wallets can be connected to [MCP servers](/recipes/agent-integrations/overview) to expose signing as a tool call. Agents provisioned through [AWS AgentCore](/recipes/agent-integrations/agentcore-payments) can delegate wallet access to AgentCore's payment runtime while retaining user-controlled custody through Privy. ## Get started > **[Get started with agent wallets](/recipes/agent-integrations/overview)** > > Provision wallets for autonomous agents > **[Authorization keys](/controls/authorization-keys/owners/overview)** > > Grant agents scoped signing access to user wallets > **[Set up policies](/controls/policies/overview)** > > Define spending limits and behavioral guardrails > **[Agent CLI](/recipes/agent-integrations/agent-cli)** > > Spin up and fund agent wallets from the command line # Organization wallets Source: https://docs.privy.io/wallets/overview/solutions/organization-wallets Privy enables you to provision both [custodial](/wallets/custodial-wallets/overview) and [non-custodial wallets](/organizations/overview) belonging to organizations instead of individuals, allowing groups of people (like a business) to hold funds, manage team access, and run financial operations programmatically. Multiple individuals within an organization can interact with the same wallet, with [granular permissions and approval workflows](/controls/policies/overview) that enforce how funds move. Neither Privy nor your product ever holds complete control over the wallet's private key. Read below to learn how Privy powers organization wallets for payments platforms, neobanks, payroll providers, spend management tools, and more. ## Policies and approval workflows Organization wallets are designed for teams, not individuals. Privy allows multiple users within an organization to be granted access to the same wallet, each with different levels of permission. Privy's policy engine enables different users to have different levels of access to the same wallet. Different policies can be applied to [different users or groups of users](/controls/authorization-keys/owners/overview) to ensure permissions are set up appropriately for the organization. Wallets can also be configured to require [multi-user quorum approvals](/controls/key-quorum/overview) for certain transactions and workflows. Privy's policy engine also lets you define [secure rules](/controls/policies/overview) that govern how funds move from an organization's wallet. Examples include allowed tokens and chains, maximum transfer values, recipient allowlists, and more. ## Transactions Privy supports transactions across [all EVM chains, all SVM (Solana) chains, Tempo, Bitcoin, Spark, Tron, Stellar, and more](/wallets/overview/chains). Organization wallets can execute transfers, batch payouts, and cross-border payments programmatically via Privy's server-side APIs — no frontend required. Privy also offers native [gas management](/wallets/gas-and-asset-management/gas/overview) on EVM and Solana, so your platform can sponsor gas on behalf of business customers or route payments through non-native tokens like USDC. ## Webhooks and automation Privy supports [webhooks](/wallets/gas-and-asset-management/assets/balance-event-webhooks) that fire on onchain events — incoming deposits, outbound transfers, policy approvals, and more. Your product can use these events to automate downstream workflows: updating ledger balances, triggering payouts, notifying approvers, or reconciling transactions against your own records. Build event-driven financial operations without polling the chain directly. ## Funding Privy supports funding wallets via [bank deposits](/wallets/funding/fiat-deposits/overview), [universal crypto deposit addresses](/wallets/funding/crypto-deposits/overview), and more. Privy's funding integrations also support programmatic transfers between wallets with native support for swapping and bridging, making it simple to orchestrate funds between various organizations. ## Get started > **[Organizations](/organizations/overview)** > > Get started with organization wallets > **[Set up organizations](/organizations/setup/users)** > > Set up organizations of users with granular access control > **[Quorum approvals](/organizations/setup/organizations#create-default-key-quorum)** > > Configure multi-party quorum approval workflows > **[Policies](/controls/policies/overview)** > > Define policies and spending rules # Treasury Source: https://docs.privy.io/wallets/overview/solutions/treasury-wallets 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. 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. With treasury, your business can manage reserves, automate recurring transfers, enforce multi-party authorization, execute high-throughput, bursty transactions, and more. Read below to learn how Privy powers treasury for trading desks, settlement reserves, DeFi protocols, and dozens of business running financial operations at scale. ## Authorization and governance Treasury operations require distributed control: routine transfers executed automatically, while high-value or one-off actions require multiple approvals. Privy supports [key quorums](/controls/key-quorum/overview) that require m-of-n signers to authorize a transaction before it executes. With key quorums, your business can set up custom approval workflows across different employees, requiring different approval schemes for different types of transactions. Your business can also configure an authorization key to execute certain lower-value, recurring transactions entirely programmatically. ## Policies Treasury can also be configured with [policies](/controls/policies/overview) to ensure guardrails for spend and usage by your treasury team. Your product may configure rules including allowed tokens and chains, allowed recipients, maximum transfer values, and more. ## Transactions and throughput Privy supports transactions across [all EVM chains, all SVM (Solana) chains, Tempo, Bitcoin, Spark, Tron, Stellar, and more](/wallets/overview/chains). Transactions can be initiated via the Privy Dashboard or the API, provided the transaction is authorized by the correct party. Privy also offers execution wallets for high-throughput use cases. With execution wallets, your business can spend from a single treasury's balance using a fleet of auxiliary wallets, allowing transactions to be broadcasted in parallel without serialization bottlenecks. Privy offers native [gas management](/wallets/gas-and-asset-management/gas/overview) on EVM and Solana. Treasury can pay for gas with a credit card, pay gas in non-native tokens like USDC, or pay gas in native tokens. ## Automation Privy supports [webhooks](/wallets/gas-and-asset-management/assets/balance-event-webhooks) for onchain events including deposits, withdrawals, transaction events, treasury approvals, policy updates, and more. Your business can use these events to automate downstream workflows: balance sweeps, liquidity rebalancing, disbursement triggers, and reconciliation against your own ledger. ## Funding Privy's [funding integrations](/financial-flows/overview) support programmatic on- and off-ramps via bank transfer and univerrsal crypto deposit addresses, enabling wallets to seamlessly move funds between fiat and digital assets. Privy's treasury management dashboard also supports native bridging and swapping of stablecoins, making it straightforward to orchestrate funds across different asset types. ## Get started > **[Get started with treasury](/recipes/wallets/wallet-infrastructure)** > > Provision a non-custodial wallet for treasury operations > **[Configure approval workflows](/controls/dashboard/overview)** > > Require m-of-n approvals for high-value transactions > **[Set up policies](/controls/policies/overview)** > > Define transfer limits, allowlists, and spending rules # User wallets Source: https://docs.privy.io/wallets/overview/solutions/user-wallets Privy enables you to embed non-custodial wallets for users directly in your product — no separate wallet client, like a browser extension or a mobile app, required. Users retain full custody of their wallets without needing to manage seed phrases or private keys directly. Neither Privy nor your application ever sees a user's private key. Read below to learn how Privy powers non-custodial embedded wallets for users across onchain social experiences, earning yield, trading, modern neobanking, cross-border remittances, and more. ## Key management Privy's [key management system](/security/wallet-infrastructure/architecture) uses distributed key sharding so that no single party, including Privy, ever holds a complete private key. Keys are only reconstituted in memory inside secure trusted execution environments (TEEs) at the moment a signature is required and only with the user's explicit authorization. This gives users full custody of their assets while eliminating the operational burden of key management themselves. ## Policies and controls Privy ties wallet usage directly to [user authentication](/authentication/overview). Given a valid user access token from your authentication service ([Privy](/authentication/user-authentication/privy-auth), [in-house](http://localhost:3000/authentication/user-authentication/jwt-based-auth/overview), or otherwise), Privy authenticates the user before their wallet is reconstituted in the TEE. This ensures that actions with wallets can only be taken with explicit authorization by the user. Embedded wallets can also be configured with [policies](/controls/policies/overview) to ensure guardrails for users. Your product may configure rules including allowed tokens and chains, allowed recipients, maximum transfer values, and more. ## Transactions Privy supports transactions across [all EVM chains, all SVM (Solana) chains, Tempo, Bitcoin, Spark, Tron, Stellar, and more](/wallets/overview/chains). Privy offers both low-level RPC and high-level wallet actions interfaces for executing transfers and swaps across these chains. With embedded wallets, users sign transactions directly from your product; not an external surface. You can choose to prompt users for transaction authorization using Privy's [default UIs](/wallets/using-wallets/ui-components#react) or [your own](/wallets/using-wallets/whitelabel#react). Once users authorize a transaction, Privy executes the signature and broadcasts the transaction onchain. Privy also offers native [gas management](/wallets/gas-and-asset-management/gas/overview) on EVM and Solana. Your product can sponsor gas for all users with, or you can have users pay gas in non-native tokens like USDC. ## Funding Privy offers several [deposit integrations](/financial-flows/deposits/overview) to enable users to smoothly onramp funds to their wallet, across various source currencies, destination currencies, and geographies. [Deposit methods](/financial-flows/deposits/configuration) include card payments, bank transfers, and [crypto deposits](/wallets/funding/crypto-deposits/overview) with universal deposit addresses across EVM chains, Solana, and more. Privy uses a network of funding providers including [Stripe](https://stripe.com/), [Bridge](https://bridge.xyz/), [Meld](https://www.meld.io/), and [MoonPay](https://www.moonpay.com/) to over global support for account funding. ## Get started > **[Create a wallet](/wallets/wallets/create/create-a-wallet)** > > Provision a non-custodial wallet for a user > **[Using wallets](/wallets/using-wallets/overview)** > > Execute transfers, swaps, signatures, and more > **[Fund accounts](/wallets/wallets/offline-actions)** > > Enable actions with wallets when users are offline > **[Fund accounts](/wallets/funding)** > > Enable wallet funding for users. # Types of wallets Source: https://docs.privy.io/wallets/overview/types Privy offers the following types of wallets. ## Embedded wallets Privy's embedded wallet system lets you build wallets directly into your app whether you're building non-custodial wallets for your users or a wallet fleet you control. Privy's wallet infrastructure ensures only the appropriate party controls the wallet. This means you can set up wallets for any user, customer, business, or agent under a range of custody options. Embedded wallets can be configured as [non-custodial](/wallets/overview/embedded) or [custodial](/wallets/custodial-wallets/overview). Privy surfaces both user-centric abstractions enabling you to authenticate users and generate wallets for them, as well as wallet-centric abstractions whereby you can create wallets with assigned authorization keys to control them. > **[Get started with embedded wallets](/wallets/overview/embedded)** ## Digital asset accounts Digital asset accounts are a high-level abstraction for securing and managing digital assets, including across multiple chains and custody configurations > [!TIP] > > Accounts are in private beta and are designed for teams building consumer and B2B fintech > applications across multiple different chains and custody configurations. Reach out to > [sales@privy.io](mailto:sales@privy.io) to request access. Accounts are comprised of a group of [embedded wallets](/wallets/overview/types#embedded-wallets) and enable you to represent a single unit of balance across multiple wallet configurations ([EVM, SVM, BTC](/wallets/overview/chains), [custodial, non-custodial](/wallets/overview/flexible-custody), and more). You can both use the individual wallets within the account directly or the account as a multiplexed abstraction over the wallets. This enables you to compose features of multiple different wallets, like chain-specific functionality, on- and off-ramps, card issuance, access to DeFi, and more into a single unified financial flow for your users. > **[Get started with accounts](/wallets/accounts/overview)** ## External wallets External wallets are managed by a third-party client, such as MetaMask, Phantom, or Rainbow. All browser extension wallets, hardware wallets, and mobile app wallets fall into this category. Privy enables users to connect external wallets to your application to execute signatures and transactions. If they choose, users may use multiple external wallets within your app and may link these wallets to their account. You can request signatures and transactions from an external wallet directly, or by integrating Privy alongside a library like `wagmi`, `viem`, or `@solana/web3.js`. [Learn more](/wallets/connectors/overview) > **[Get started with external wallets](/wallets/connectors/overview)** # Common use cases Source: https://docs.privy.io/wallets/overview/use-cases Common use cases for Privy wallets including payments, trading, DeFi, and NFTs View some common use cases for Privy. ### User wallets You can generate non-custodial wallets for your users for a wallet experience that is directly embedded in your application—no separate wallet client, like a browser extension or a mobile app, required. This means users have full custody of their wallets without needing to manage secret keys. Neither Privy nor your application ever sees the user's keys; secrets are only ever reconstituted in a secure environment under the user's control so they can sign messages or transactions. Users can manage their embedded wallet seamlessly with their account; they never need to handle any unnecessary technical complexity. Your application can even [pregenerate wallets](/recipes/pregenerate-wallets) for an account, like an email address or phone number, before the user logs in. Users can also [export the key](/wallets/wallets/export) for their embedded wallet, providing an escape hatch to leave Privy at any time. Your application can easily guide users to use their wallet with simple abstractions to prompt users to fund, transact, and sign with their wallet. Learn more about support for [user wallets](/wallets/overview/solutions/user-wallets). ### Organization wallets Set up a fleet of non-custodial and/or custodial wallets wallets to enable secure asset management for organizations and businesses. Multiple members of an organization can be provisioned access to the same wallet. Secure funds via quorums of pre-approved signers and policies with granular approval workflows. Register webhooks to automate events based on onchain actions, assign specific policies to wallet signers to manage risk, and more. Wallets leverage secure enclaves and key splitting to ensure secure key reconstitution and appropriate custody for all use cases. Learn more about support for [organization wallets](/wallets/overview/solutions/organization-wallets). ### Treasury Set up non-custodial or custodial wallets to manage your organization's treasury. Configure granular approval workflows to assign specific permissions to users over transactions, policy updates, workflow updates, and more. Initiate actions programmatically via the Privy API or manually via the Dashboard. Learn more about support for [treasury](/wallets/overview/solutions/treasury-wallets). ### Agentic wallets Privy enables you to securely provision wallets for agents. Agents can execute actions on users' behalf, like payments or trades, via natural language prompts from users or asynchronously while the user is offline. Users can configure granular policies to restrict agent spend and ensure that transactions are aligned with user preferences and intent. Learn more about support for [agent wallets](/wallets/overview/solutions/agent-wallets). # Signatures and transactions Source: https://docs.privy.io/wallets/signatures-and-transactions To take an action (such as executing a signature, transfer, or other transaction), there are four 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 | | [Wallet automations](/wallets/automations/overview) | Execute wallet actions automatically when configured events occur. Privy monitors events and submits matching actions outside the app's lifecycle. | Event-driven wallet workflows | | [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** wherever possible for your use case. > **[Get started with wallet actions](/wallets/actions/overview)** > **[Get started with RPCs](/wallets/using-wallets/rpc)** > **[Get started with intents](/transaction-management/intents/overview)** # Sign transaction inputs Source: https://docs.privy.io/wallets/using-wallets/bitcoin/sign-transaction-inputs Sign Bitcoin transaction inputs using Privy raw sign functionality for UTXO-based transactions > [!NOTE] > > With Privy, you can create Bitcoin segwit and taproot wallets and sign over transactions and any > other arbitrary input. See the [raw signing guide](/wallets/using-wallets/other-chains) for more > details. ## Signing transaction inputs Bitcoin uses the UTXO model, where each transaction consumes one or more inputs and produces one or more outputs. To sign a transaction using Privy, use Privy's raw sign functionality to sign each input hash, and then add the signature(s) to the transaction. ## Segwit Segwit (SegWit v0) wallets use the ECDSA signing algorithm with the secp256k1 curve. Use Privy's raw sign functionality to sign each input UTXO for your Bitcoin segwit transaction. ### Example The following is an example using the [scure/btc-signer](https://github.com/paulmillr/scure-btc-signer) library to sign a segwit transaction input. ```typescript {skip-check} theme={"system"} import {p2pkh, OutScript} from '@scure/btc-signer/payment'; import {getInputType, getPrevOut, Transaction} from '@scure/btc-signer/transaction'; import {concatBytes} from '@scure/btc-signer/utils'; 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 tx.addOutputAddress(outputAddress, outputAmount); tx.addInput({ txid, // buffer of utxo txid index: 0, // index of the output in the tx witnessUtxo: { amount: inputAmount, script: p2pkh(publicKeyBuffer).script } }); // Loop through each input and sign it 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; 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...' // convert to DER format const derSig = secp256k1.signatureImport(signature); // update the input with the signature tx.updateInput( i, { partialSig: [[publicKeyBuffer, concatBytes(derSig, new Uint8Array([inputType.sighash]))]] }, true ); } // finalize the transaction. It is now ready to be sent! tx.finalize(); ``` ## Taproot Taproot (SegWit v1) wallets use the Schnorr signing algorithm (BIP-340) with the secp256k1 curve. Privy automatically applies the BIP-341 key tweak when signing with a taproot wallet, so the resulting signature is valid for key-path spends against the wallet's P2TR output. > [!WARNING] > > 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 at this > time. ### Example The following is an example using the [scure/btc-signer](https://github.com/paulmillr/scure-btc-signer) library to sign a taproot transaction input. Note that taproot uses `preimageWitnessV1` for the BIP-341 sighash and attaches the 64-byte Schnorr signature as `tapKeySig` (rather than `partialSig` as with segwit). ```typescript {skip-check} theme={"system"} import {p2tr} from '@scure/btc-signer/payment'; import {Transaction, getInputType, getPrevOut} from '@scure/btc-signer/transaction'; import {bytesToHex} from 'viem'; // 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 by stripping the first byte (compression prefix) 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 (as many as needed) tx.addOutputAddress(outputAddress, outputAmount); // Add a taproot input 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 }); // Loop through each input and sign it 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 const sighash = tx.preimageWitnessV1(i, [prevOut.script], inputType.sighash, [prevOut.amount]); const signature = ''; // call Privy's raw sign function with bytesToHex(sighash), returns '0x...' // Decode the 64-byte Schnorr signature and attach it as tapKeySig const signatureBytes = Buffer.from(signature.slice(2), 'hex'); tx.updateInput(i, {tapKeySig: signatureBytes}, true); } // Finalize the transaction. finalize() validates the Schnorr signature // against the tweaked output key and throws if the signature is invalid. tx.finalize(); ``` # Ethereum provider Source: https://docs.privy.io/wallets/using-wallets/ethereum/ethereum-provider ## Getting an EIP-1193 provider All of Privy's **`ConnectedWallet`** objects export a standard [EIP-1193 provider](https://eips.ethereum.org/EIPS/eip-1193) object. This allows your app to request signatures and transactions from the wallet, using familiar JSON-RPC requests like `personal_sign` or `eth_sendTransaction`. > [!NOTE] > > [EIP-1193](https://eips.ethereum.org/EIPS/eip-1193), also known as the Ethereum JavaScript API, is > a standardized interface for how applications can request information, signatures, and > transactions from a connected wallet. To request signatures or transactions from a connected wallet, you can either: * use the wallet's EIP-1193 provider to send JSON-RPC requests to the wallet directly * pass the wallet to a library like `viem`, `ethers`, or `wagmi` * for the embedded wallet specifically, use Privy's native **`signMessage`** and **`sendTransaction`** methods to customize the signature and transaction prompts for users To get a wallet's EIP-1193 provider, use the **`ConnectedWallet`** object's **`getEthereumProvider`** method: ```tsx theme={"system"} const provider = await wallet.getEthereumProvider(); ``` When requesting signatures and transactions from the wallet, you can either choose to interface with the EIP1193 provider directly, or to pass it to a library like `wagmi` or `viem`. # Send an Ethereum transaction Source: https://docs.privy.io/wallets/using-wallets/ethereum/send-a-transaction Sign and broadcast an Ethereum/EVM transaction with Privy > [!NOTE] > > When using Privy's server-side SDKs to sign transactions, you can use the authorization context to > automatically sign requests. Learn more about [signing on the > server](/controls/authorization-keys/using-owners/sign/signing-on-the-server). **React** To send a transaction from a wallet using the React SDK, use the `sendTransaction` method from the `useSendTransaction` hook: ```javascript theme={"system"} sendTransaction: (input: UnsignedTransactionRequest, options?: { sponsor?: boolean; uiOptions?: SendTransactionModalUIOptions; fundWalletConfig?: FundWalletConfig; address?: string; }) => Promise<{ hash: HexString }> ``` ### Usage ```javascript theme={"system"} import {useSendTransaction, useWallets} from '@privy-io/react-auth'; const {sendTransaction} = useSendTransaction(); const {wallets} = useWallets(); sendTransaction( { to: '0xE3070d3e4309afA3bC9a6b057685743CF42da77C', value: 100000 }, { address: wallets[0].address // Optional: Specify the wallet to use for signing. If not provided, the first wallet will be used. } ); ``` ### Parameters **input** `UnsignedTransactionRequest` *required* The details of the transaction to send on the chain. **options.sponsor** `boolean` Optional parameter to enable gas sponsorship for this transaction. [Learn more.](/wallets/gas-and-asset-management/gas/overview) **options.uiOptions** `SendTransactionModalUIOptions` The options for the UI of the send transaction modal. [Learn more](/wallets/using-wallets/ui-components). > [!TIP] > > To hide confirmation modals, set `options.uiOptions.showWalletUIs` to `false`. Learn more about > configuring modal prompts [here](/recipes/react/manage-wallet-UIs). **options.fundWalletConfig** `FundWalletConfig` The configuration for funding the wallet. **options.address** `string` The address of the wallet to use for sending the transaction. **Recommended when working with external wallets** to ensure reliable functionality. If not provided, the first wallet will be used. ### Returns **hash** `HexString` The hash for the broadcasted transaction. **React Native** To send a transaction from a wallet using the React Native SDK use the `request` method from the wallets EIP1193 provider: ```javascript theme={"system"} request: (request: { method: 'eth_sendTransaction', params: [SendTransactionParams] }) => Promise ``` > [!NOTE] > > The Expo SDK does not support built-in UIs for sending transactions. > The `eth_sendTransaction` method gives you complete control over the experience and UI. ### Usage ```javascript 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: '0x0000000000000000000000000000000000000000', value: '0x2386F26FC10000', }, ], }); ``` ### Parameters **request.method** `'eth_sendTransaction'` *required* The RPC method executed with the wallet. **request.params** `SendTransactionParams` *required* The details of the transaction to send on the chain. ### Returns **response** `HexString` The hash for the broadcasted transaction. **Swift** Use the `request` method on the Ethereum provider to send a transaction with an Ethereum wallet. ```swift theme={"system"} func request(_ request: EthereumRpcRequest) async throws -> String ``` ### Usage ```swift theme={"system"} let provider = wallet.provider let transaction = EthereumRpcRequest.UnsignedEthTransaction( from: wallet.address, to: "0xE3070d3e4309afA3bC9a6b057685743CF42da77C", value: .int(10000), chainId: .hexadecimal("0x2105") // Base, in hex ) let transactionHash = try await provider.request(.ethSendTransaction(transaction: transaction) ``` ### Parameters **transaction** `EthereumRpcRequest.UnsignedEthTransaction` *required* The transaction to send. **child attributes** **from** `string` The address sending the transaction (the user's wallet address). **to** `string` *required* The address to send the transaction to. **value** `Quantity` The amount of wei to send, as an integer or hexadecimal string. **chainId** `Quantity` The chain ID as an integer or hexadecimal string. Defaults to mainnet (0x1) if omitted. **data** `string` The data to include with the transaction, as a hexadecimal string. **gasLimit** `string` The gas limit for the transaction, as a hexadecimal string. ### Returns **transactionHash** `string` The hash of the broadcasted transaction. **Android** Use the `request` method on the Ethereum wallet provider to send a transaction with an Ethereum wallet. ```kotlin theme={"system"} public suspend fun request(request: EthereumRpcRequest): Result ``` ### Usage ```kotlin theme={"system"} val transaction = JSONObject().apply { put("to", "0xE3070d3e4309afA3bC9a6b057685743CF42da77C") put("value", "0x186a0") // 100000 in hex put("chainId", "0x2105") // 8453 (Base) in hex put("from", ethereumWallet.address) }.toString() val result = ethereumWallet.provider.request( request = EthereumRpcRequest.ethSendTransaction(transaction), ) when (result) { is Result.Success -> { val transactionHash = result.data.data // Handle successful transaction } is Result.Failure -> { // Handle error } } ``` ### Parameters **method** `'eth_sendTransaction'` *required* The RPC method to execute with the wallet. **params** `List\` *required* Array containing the transaction JSON as a single string element. **child attributes** **to** `string` *required* The address to send the transaction to. **value** `string` The amount of wei to send, as a hexadecimal string. **chainId** `string` The chain ID as a hexadecimal string. Defaults to mainnet (0x1) if omitted. **from** `string` The address sending the transaction (the user's wallet address). **data** `string` The data to include with the transaction, as a hexadecimal string. **gasLimit** `string` The gas limit for the transaction, as a hexadecimal string. ### Returns **result** `Result\` A Result that, when successful, contains the EthereumRpcResponse with: **child attributes** **method** `string` The RPC method that was executed (eth\_sendTransaction). **data** `string` The transaction hash for the broadcasted transaction. **Unity** Use the `Request` method on the wallet's RPC provider to send a transaction with an Ethereum wallet. ```csharp theme={"system"} public Task Request(RpcRequest request); ``` ### Usage ```csharp theme={"system"} // Create transaction JSON string transactionJson = JsonUtility.ToJson(new { to = "0xE3070d3e4309afA3bC9a6b057685743CF42da77C", value = "0x186a0", // 100000 in hex chainId = "0x2105", // 8453 (Base) in hex from = embeddedWallet.Address }); // Create RPC request var rpcRequest = new RpcRequest { Method = "eth_sendTransaction", Params = new string[] { transactionJson } }; // Send transaction RpcResponse transactionResponse = await embeddedWallet.RpcProvider.Request(rpcRequest); // Transaction hash is in the response data string transactionHash = transactionResponse.Data; ``` ### Parameters **Method** `'eth_sendTransaction'` *required* The RPC method to execute with the wallet. **Params** `string[]` *required* Array containing the transaction JSON as a single string element. **child attributes** **to** `string` *required* The address to send the transaction to. **value** `string` The amount of wei to send, as a hexadecimal string. **chainId** `string` The chain ID as a hexadecimal string. Defaults to mainnet (0x1) if omitted. **from** `string` The address sending the transaction (the user's wallet address). **data** `string` The data to include with the transaction, as a hexadecimal string. **gasLimit** `string` The gas limit for the transaction, as a hexadecimal string. ### Returns **RpcResponse** `RpcResponse` An RPC response object with: **child attributes** **method** `string` The RPC method that was executed (eth\_sendTransaction). **data** `string` The transaction hash for the broadcasted transaction. **Flutter** Use the `request` method on the Ethereum wallet provider to send a transaction with an Ethereum wallet. ```dart theme={"system"} Future> request(EthereumRpcRequest request); ``` ### Usage ```dart theme={"system"} // Create transaction parameters as a Map final transactionMap = { 'to': '0xE3070d3e4309afA3bC9a6b057685743CF42da77C', 'value': '0x186a0', // 100000 in hex 'chainId': '0x2105', // 8453 (Base) in hex 'from': ethereumWallet.address }; // Convert Map to JSON string final transactionJson = jsonEncode(transactionMap); // Create the RPC request final rpcRequest = EthereumRpcRequest( method: 'eth_sendTransaction', params: [transactionJson], ); // Send the transaction final result = await ethereumWallet.provider.request(rpcRequest); // Handle the result result.when( success: (response) { final transactionHash = response.data; print('Transaction sent with hash: $transactionHash'); }, failure: (error) { print('Failed to send transaction: $error'); }, ); ``` ### Parameters **method** `'eth_sendTransaction'` *required* The RPC method to execute with the wallet. **params** `List\` *required* List containing the transaction JSON as a single string element. **child attributes** **to** `string` *required* The address to send the transaction to. **value** `string` The amount of wei to send, as a hexadecimal string. **chainId** `string` The chain ID as a hexadecimal string. Defaults to mainnet (0x1) if omitted. **from** `string` The address sending the transaction (the user's wallet address). **data** `string` The data to include with the transaction, as a hexadecimal string. **gasLimit** `string` The gas limit for the transaction, as a hexadecimal string. ### Returns **result** `Result\` A Result that, when successful, contains the EthereumRpcResponse with: **child attributes** **method** `string` The RPC method that was executed (eth\_sendTransaction). **data** `string` The transaction hash for the broadcasted transaction. **NodeJS** > [!TIP] > > Privy is fully compatible with popular web3 libraries for interfacing wallets, including [`viem`](https://viem.sh/), learn more [here](/wallets/using-wallets/ethereum/web3-integrations#node-js). Use the `sendTransaction` method on the Ethereum interface to send a transaction with an Ethereum wallet. ### Usage ```js theme={"system"} const {hash, caip2} = await privy.wallets().ethereum().sendTransaction('insert-wallet-id', { caip2: 'eip155:8453', params: { transaction: { to: '0xE3070d3e4309afA3bC9a6b057685743CF42da77C', value: '0x2386F26FC10000', chain_id: 8453, }, }, sponsor: true, }); ``` ### Parameters and Returns Check out the [API reference](/api-reference/wallets/ethereum/eth-send-transaction) for more details. > [!WARNING] > > 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). **REST API** To send a transaction, make a `POST` request to ```bash theme={"system"} https://api.privy.io/v1/wallets//rpc ``` > [!NOTE] > > Wallets with `owner_id` present must provide an [authorization signature](/api-reference/authorization-signatures) as a request header. > > If you are transacting with a user wallet, you must provide the user's [authorization signature](/api-reference/authorization-signatures#get-an-authorization-key). ### Usage ```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:1", "params": { "transaction": { "to": "0xE3070d3e4309afA3bC9a6b057685743CF42da77C", "value": "0x2386F26FC10000", } }, "sponsor": true }' ``` A successful response will look like the following: ```json theme={"system"} { "method": "eth_sendTransaction", "data": { "hash": "", "caip2": "eip155:1" } } ``` ### Parameters **method** `'eth_sendTransaction'` *required* The RPC method executed with the wallet. **caip2** `eip155:$\{number\}` *required* The CAIP2 chain ID of the chain the transaction is being sent on. **params.transaction** `EthereumTransactionType` *required* The details of the transaction to send on the chain. **sponsor** `boolean` Optional parameter to enable gas sponsorship for this transaction. [Learn more.](/wallets/gas-and-asset-management/gas/overview) ### Returns **method** `'eth_sendTransaction'` The RPC method executed with the wallet. **data.hash** `string` The hash for the broadcasted transaction. **data.caip2** `eip155:$\{number\}` The CAIP2 chain ID of the chain the transaction was sent on. > [!WARNING] > > 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). **Java** To send a transaction from your wallet, use the `sendTransaction` method. It will populate missing network-related values (gas limit, gas fee values, nonce, type), sign your transaction, broadcast it to the network, and return the transaction hash to you. ### Usage ```java theme={"system"} try { String caip2 = "eip155:11155111"; // Sepolia testnet UnsignedStandardEthereumTransaction txn = UnsignedStandardEthereumTransaction.builder() .to(recipientAddress) .value(Quantity.of("0x1")) // 1 wei .chainId(Quantity.of(11_155_111)) // Sepolia testnet .build(); // Example: If wallet's owner is an authorization private key AuthorizationContext authorizationContext = AuthorizationContext.builder() .addAuthorizationPrivateKey("authorization-key") .build(); EthereumSendTransactionRpcResponseData response = privyClient .wallets() .ethereum() .sendTransaction( walletId, caip2, txn, authorizationContext ); String transactionHash = response.hash(); } catch (APIException e) { String errorBody = e.bodyAsString().orElse(e.getMessage()); System.err.println(errorBody); } catch (Exception e) { System.err.println(e.getMessage()); } ``` ### Parameters When defining a transaction, you may specify the following values on the `UnsignedStandardEthereumTransaction` builder: **from** `String` The address sending the transaction (the user's wallet address). **to** `String` The address to send the transaction to. **chainId** `Quantity` The ID of the chain the transaction is being sent on. **nonce** `Quantity` The nonce for the transaction. **data** `String` The data to include with the transaction, as a hexadecimal string. **value** `Quantity` The amount of wei to send, as an integer or hexadecimal string. **type** `Object` The type of transaction to send. **gasLimit** `Quantity` The gas limit for the transaction, as a hexadecimal string. **gasPrice** `Quantity` The gas price for the transaction, as a hexadecimal string. **maxFeePerGas** `Quantity` The maximum fee per gas for the transaction, as a hexadecimal string. **maxPriorityFeePerGas** `Quantity` The maximum priority fee per gas for the transaction, as a hexadecimal string. ### Returns The `EthereumSendTransactionRpcResponseData` object contains a `hash()` field **hash()** `String` The hash of the broadcasted transaction. **Rust** Use the `send_transaction` method on the Ethereum service to sign and broadcast a transaction. ### Usage ```rust theme={"system"} use privy_rs::{AuthorizationContext, PrivyClient}; let client = PrivyClient::new("app_id".to_string(), "app_secret".to_string())?; let ethereum_service = client.wallets().ethereum(); let auth_ctx = AuthorizationContext::new(); let transaction = EthereumSendTransactionRpcInputParamsTransaction { to: Some("0x742d35Cc6635C0532925a3b8c17d6d1E9C2F7ca".to_string()), value: None, gas_limit: None, max_fee_per_gas: None, max_priority_fee_per_gas: None, data: None, chain_id: None, from: None, gas_price: None, nonce: None, type_: None, }; let result = ethereum_service .send_transaction( &wallet_id, "eip155:1", transaction, &auth_ctx, None, ) .await?; println!("Transaction sent successfully"); ``` ### 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 REST API details, see the [API reference](/api-reference/wallets/ethereum/eth-send-transaction). > [!WARNING] > > 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). **Go** Use the `SendTransaction` method on the Ethereum service to sign and broadcast a transaction. ### Usage ```go theme={"system"} caip2 := "eip155:11155111" // Sepolia testnet response, err := client.Wallets.Ethereum.SendTransaction( context.Background(), "wallet-id", caip2, privy.EthereumSendTransactionRpcInputParams{ Transaction: privy.EthereumSendTransactionRpcInputParamsTransaction{ To: privy.String("0xE3070d3e4309afA3bC9a6b057685743CF42da77C"), Value: privy.EthereumSendTransactionRpcInputParamsTransactionValueUnion{ OfString: privy.String("0x2386F26FC10000"), }, ChainID: privy.EthereumSendTransactionRpcInputParamsTransactionChainIDUnion{ OfInt: privy.Int(11155111), }, }, }, ) if err != nil { log.Fatalf("failed to send transaction: %v", err) } fmt.Println("Transaction hash:", response.Hash) ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/ethereum/eth-send-transaction) for more details. > [!WARNING] > > 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). **Ruby** Use the `rpc` method on the `wallets` service with the `eth_sendTransaction` method to sign and broadcast a transaction. ### Usage ```ruby theme={"system"} response = client.wallets.rpc( "wallet-id", wallet_rpc_request_body: { method: "eth_sendTransaction", chain_type: "ethereum", caip2: "eip155:11155111", # Sepolia testnet params: { transaction: { to: "0xE3070d3e4309afA3bC9a6b057685743CF42da77C", value: "0x2386F26FC10000", chain_id: 11_155_111 } } }, authorization_context: ctx ) puts(response.data.hash_) ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/ethereum/eth-send-transaction) for more details. > [!WARNING] > > 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). > [!NOTE] > > For a complete example of sending USDC with Privy's SDKs, see our [Sending USDC > recipe](/recipes/send-usdc). **Python** Use the `send_transaction` method on the Ethereum wallet service. ```python theme={"system"} response = client.wallets.ethereum.send_transaction( wallet_id, caip2="eip155:8453", params={ "transaction": { "to": "insert-recipient-address", "value": "0x1", } }, ) transaction_hash = response.hash ``` # Sign EIP-7702 authorization Source: https://docs.privy.io/wallets/using-wallets/ethereum/sign-7702-authorization Sign EIP-7702 authorizations to upgrade embedded wallets into smart contract accounts [EIP-7702](https://eips.ethereum.org/EIPS/eip-7702) enables externally owned accounts (EOAs) to delegate their execution to smart contract code. This allows EOA wallets to gain account abstraction capabilities such as transaction bundling, gas sponsorship, and custom permissions. Privy provides methods to sign EIP-7702 authorizations, which allows your embedded wallets to be upgraded into any smart contract. Learn more about [EIP-7702](/recipes/react/eip-7702) and how to use it with various account abstraction providers. > [!NOTE] > > EIP-7702 authorization signing is supported in the React SDK, REST API, and Node SDK. > [!NOTE] > > When using Privy's server-side SDKs to sign EIP-7702 authorizations, you can use the authorization > context to automatically sign requests. Learn more about [signing on the > server](/controls/authorization-keys/using-owners/sign/signing-on-the-server). **React** Use the `useSign7702Authorization` hook to sign an EIP-7702 authorization with your user's wallet. ```tsx theme={"system"} import {useSign7702Authorization} from '@privy-io/react-auth'; const {signAuthorization} = useSign7702Authorization(); ``` ### Usage ```tsx theme={"system"} import {useSign7702Authorization, useWallets} from '@privy-io/react-auth'; function Sign7702Button() { const {signAuthorization} = useSign7702Authorization(); const {wallets} = useWallets(); const handleSign = async () => { try { const authorization = await signAuthorization({ contractAddress: '0x1234567890abcdef1234567890abcdef12345678', chainId: 1, // Ethereum mainnet nonce: 0 // Optional, defaults to current nonce }, { address: wallets[0].address // Optional: Specify the wallet to use for signing. If not provided, the first wallet will be used. }); console.log('Signed authorization:', authorization); // Use the authorization with your AA provider } catch (error) { console.error('Failed to sign authorization:', error); } }; return ( ); } ``` ### Parameters **contractAddress** `string` *required* The address of the smart contract whose code the EOA will delegate to. This is typically an account implementation contract from your AA provider. **chainId** `number` *required* The chain ID where the authorization will be used. **nonce** `number` The nonce for the authorization. If not provided, the current transaction count for the wallet will be used. **options.address** `string` The address of the wallet to use for signing the authorization. **Recommended when working with external wallets** to ensure reliable functionality. If not provided, the first wallet will be used. ### Returns **authorization** `Authorization` The signed EIP-7702 authorization object containing: **properties** **contractAddress** `string` The smart contract address the EOA is delegating to. **chainId** `number` The chain ID for the authorization. **nonce** `number` The nonce used in the authorization. **r** `string` The r value of the ECDSA signature. **s** `string` The s value of the ECDSA signature. **yParity** `number` The yParity value of the ECDSA signature. **React Native** Sign an EIP-7702 authorization using viem and the `@privy-io/expo` SDK. ### Usage ```ts theme={"system"} import {useEmbeddedEthereumWallet} from '@privy-io/expo'; import {createWalletClient, custom} from 'viem; import {hashAuthorization} from 'viem/utils'; import {mainnet} from 'viem/chains'; const {wallets} = useEmbeddedEthereumWallet(); const wallet = wallets[0] const provider = await wallet.getProvider() const client = createWalletClient({ account: wallet.address as `0x${string}`, chain: mainnet, transport: custom(provider) // or another provider }); const authorizationPayload = await walletClient.prepareAuthorization({ account: selectedWallet.address as `0x${string}`, contract: '0x1234567890abcdef1234567890abcdef12345678', chainId: 1, }); const hashedAuthorization = hashAuthorization(authorizationPayload); const authorization = provider.request({ method: 'secp256k1_sign', params: [hashedAuthorization] }); ``` ### Parameters and Returns Check out the [sign raw hash doc](/wallets/using-wallets/ethereum/sign-a-raw-hash#react-native) for more details. **REST API** Sign an EIP-7702 authorization by making a `POST` request to: ```bash theme={"system"} https://api.privy.io/v1/wallets//rpc ``` ### Usage ```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_sign7702Authorization", "params": { "contract": "0x1234567890abcdef1234567890abcdef12345678", "chain_id": 1, "nonce": 0 } }' ``` A successful response will look like: ```json theme={"system"} { "method": "eth_sign7702Authorization", "data": { "authorization": { "address": "0x1234567890abcdef1234567890abcdef12345678", "chain_id": 1, "nonce": 0, "r": "0x...", "s": "0x...", "y_parity": "0" } } } ``` ### Parameters **method** `'eth_sign7702Authorization'` *required* The RPC method to execute. **params** `Object` *required* **contract** `string` *required* The address of the smart contract whose code the EOA will delegate to. **chain\_id** `number` *required* The chain ID where the authorization will be used. **nonce** `number` The nonce for the authorization. If not provided, the current transaction count will be used. ### Returns **method** `'eth_sign7702Authorization'` The RPC method that was executed. **data.authorization** `Object` The signed EIP-7702 authorization containing: **properties** **contract** `Hex` The smart contract address the EOA is delegating to. **chain\_id** `number` The chain ID for the authorization. **nonce** `number` The nonce used in the authorization. **r** `Hex` The r value of the ECDSA signature. **s** `Hex` The s value of the ECDSA signature. **y\_parity** `number` The yParity value of the ECDSA signature. **NodeJS** Use the `sign7702Authorization` method on the Ethereum interface to sign an EIP-7702 authorization with an Ethereum wallet. ### Usage ```tsx theme={"system"} const authorization = await privy.wallets().ethereum().sign7702Authorization('insert-wallet-id', { params: { contract: '0x1234567890abcdef1234567890abcdef12345678', chainId: 1, nonce: 0 // Optional } }); ``` ### Parameters and Returns Check out the [API reference](/api-reference/wallets/ethereum/eth-sign-7702-authorization) for more details. **Java** To sign an EIP-7702 authorization from your wallet, use the `sign7702Authorization` method. ### Usage ```java theme={"system"} try { EthereumSign7702AuthorizationRpcInputParams authorization = EthereumSign7702AuthorizationRpcInputParams.builder() .contract("0x1234567890123456789012345678901234567890") .chainId(EthereumSign7702AuthorizationRpcInputChainId.of(1)) .nonce(EthereumSign7702AuthorizationRpcInputNonce.of(1)) .build(); // Example: If wallet's owner is an authorization private key AuthorizationContext authorizationContext = AuthorizationContext.builder() .addAuthorizationPrivateKey("authorization-key") .build(); EthereumSign7702AuthorizationRpcResponseData response = privyClient .wallets() .ethereum() .sign7702Authorization( walletId, authorization, authorizationContext ); Authorization authorization = response.authorization(); } catch (APIException e) { String errorBody = e.bodyAsString().orElse(e.getMessage()); System.err.println(errorBody); } catch (Exception e) { System.err.println(e.getMessage()); } ``` ### Parameters When defining an EIP-7702 authorization, you may specify the following values on the `EthereumSign7702AuthorizationRpcInputParams` builder: **contract** `String` The address of the smart contract whose code the EOA will delegate to. **chainId** `EthereumSign7702AuthorizationRpcInputChainId` The chain ID where the authorization will be used. **nonce** `EthereumSign7702AuthorizationRpcInputNonce` The nonce for the authorization. ### Returns The `EthereumSign7702AuthorizationRpcResponseData` object contains a `authorization()` field **authorization()** `Authorization` The signed EIP-7702 authorization. **contract** `String` The smart contract address the EOA is delegating to. **chainId** `EthereumSign7702AuthorizationRpcInputChainId` The chain ID for the authorization. **nonce** `EthereumSign7702AuthorizationRpcInputNonce` The nonce used in the authorization. **r** `String` The r value of the ECDSA signature. **s** `String` The s value of the ECDSA signature. **yParity** `double` The yParity value of the ECDSA signature. **Rust** Use the `sign_7702_authorization` method on the Ethereum service to sign an EIP-7702 authorization. ### Usage ```rust theme={"system"} use privy_rs::{AuthorizationContext, PrivyClient}; let client = PrivyClient::new("app_id".to_string(), "app_secret".to_string())?; let ethereum_service = client.wallets().ethereum(); let auth_ctx = AuthorizationContext::new(); let params = EthereumSign7702AuthorizationRpcInputParams { chain_id: EthereumSign7702AuthorizationRpcInputParamsChainId::Integer(1), contract: "0x1234567890abcdef1234567890abcdef12345678".to_string(), nonce: None, }; let authorization = ethereum_service .sign_7702_authorization(&wallet_id, params, &auth_ctx, None) .await?; println!("7702 authorization signed successfully"); ``` ### Parameters and Returns See the Rust SDK documentation for detailed parameter and return types, including embedded examples: * [EthereumService::sign\_7702\_authorization](https://docs.rs/privy-rs/latest/privy_rs/ethereum/struct.EthereumService.html#method.sign_7702_authorization) For REST API details, see the [API reference](/api-reference/wallets/ethereum/eth-sign-7702-authorization). **Go** Use the `Sign7702Authorization` method on the Ethereum service to sign an EIP-7702 authorization with an Ethereum wallet. ### Usage ```go theme={"system"} response, err := client.Wallets.Ethereum.Sign7702Authorization( context.Background(), "wallet-id", privy.EthereumSign7702AuthorizationRpcInputParams{ ChainID: privy.EthereumSign7702AuthorizationRpcInputParamsChainIDUnion{ OfString: privy.String("eip155:11155111"), }, Contract: "0xcontract-address", }, ) if err != nil { log.Fatalf("failed to sign 7702 authorization: %v", err) } fmt.Println("Authorization:", response) ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/ethereum/eth-sign-7702-authorization) for more details. **Python** Use the `sign_7702_authorization` method on the Ethereum wallet service. ```python theme={"system"} response = client.wallets.ethereum.sign_7702_authorization( wallet_id, params={ "chain_id": 1, "contract": "insert-contract-address", }, ) authorization = response.authorization ``` **Ruby** Use the `rpc` method on the `wallets` service with the `eth_sign7702Authorization` method to sign an EIP-7702 authorization with an Ethereum wallet. ### Usage ```ruby theme={"system"} response = client.wallets.rpc( "wallet-id", wallet_rpc_request_body: { method: "eth_sign7702Authorization", chain_type: "ethereum", params: { chain_id: 11_155_111, # Sepolia testnet contract: "0xcontract-address" } }, authorization_context: ctx ) puts(response.data.authorization.to_h) ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/ethereum/eth-sign-7702-authorization) for more details. # Sign a message Source: https://docs.privy.io/wallets/using-wallets/ethereum/sign-a-message > [!NOTE] > > This method uses Ethereum's `personal_sign` RPC method. If you are looking for a low-level raw > signature over a input hash, see > [secp256k1\_sign](/wallets/using-wallets/ethereum/sign-a-raw-hash). > [!NOTE] > > When using Privy's server-side SDKs to sign messages, you can use the authorization context to > automatically sign requests. Learn more about [signing on the > server](/controls/authorization-keys/using-owners/sign/signing-on-the-server). **React** Use the `signMessage` method exported from the `useSignMessage` hook to sign a message with an Ethereum embedded wallet. ### Usage ```tsx theme={"system"} import {useSignMessage, useWallets} from '@privy-io/react-auth'; const {signMessage} = useSignMessage(); const {wallets} = useWallets(); const uiOptions = { title: 'You are voting for foobar project' }; const {signature} = await signMessage( {message: 'I hereby vote for foobar'}, { uiOptions, address: wallets[0].address // Optional: Specify the wallet to use for signing. If not provided, the first wallet will be used. } ); ``` ### Parameters **message** `string` *required* Message to be signed. **options** `Object` **properties** **uiOptions** `SignMessageModalUIOptions` UI options to customize the signature prompt modal. [Learn more](/wallets/using-wallets/ui-components) > [!TIP] > > To hide confirmation modals, set `options.uiOptions.showWalletUIs` to `false`. Learn more > about configuring modal prompts [here](/recipes/react/manage-wallet-UIs). **address** `string` Address of the wallet to use for signing the message. **Recommended when working with external wallets** to ensure reliable functionality. If not provided, the first wallet will be used. ### Returns **signature** `string` The signature produced by the wallet. ### Callbacks Configure callbacks for Privy's `signMessage` method on the `useSignMessage` hook: ```tsx theme={"system"} import {useSignMessage} from '@privy-io/react-auth'; const {signMessage} = useSignMessage({ onSuccess: ({signature}) => { console.log(signature); // Any logic you'd like to execute after a user successfully signs a message }, onError: (error) => { console.log(error); // Any logic you'd like to execute after a user exits the message signing flow or there is an error } }); // Then call `signMessage` in your code, which will invoke these callbacks on completion ``` As parameters to **`useSignMessage`**, you may include an **`onSuccess`** callback and/or an **`onError`** callback. While this component is mounted, any invocation of **`signMessage`** will trigger the **`onSuccess`** callback or **`onError`** callback on completion, depending on if the message was successfully signed or not. #### onSuccess If set, the **`onSuccess`** callback will execute after a user has successfully signed the message. Within this callback, you can access a **`signature`** parameter, which is the **`signature`** string value generated by the wallet to sign the message. #### onError If set, the **`onError`** callback will execute after a user attempts to sign a message and there is an error, or if the user exits the signature flow prematurely. Within this callback, you may access an **`error`** code with more information about the error. **React Native** Request a message signature on the wallets Ethereum provider. ### Usage ```tsx theme={"system"} import {useEmbeddedEthereumWallet} from '@privy-io/expo'; const {wallets} = useEmbeddedEthereumWallet(); const wallet = wallets[0]; // Get an EIP-1193 Provider const provider = await wallet.getProvider(); // 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]] }); ``` ### Parameters **method** `'personal_sign'` *required* The method for the wallet request. For signing messages, this is `'personal_sign'`. **params** `Object` *required* **properties** **message** `string | Uint8Array` *required* The string or bytes to sign with the wallet. **address** `string` The address of the wallet to sign the message with. ### Returns **signature** `string` The signature produced by the wallet. **Swift** Request a message signature on the wallet's Ethereum provider. ### Usage ```swift theme={"system"} guard let user = privy.user else { // If user is null, user is not authenticated return } // Retrieve list of user's embedded Ethereum wallets let ethereumWallets = user.embeddedEthereumWallets // Grab the desired wallet. Here, we retrieve the first wallet guard let wallet = ethereumWallets.first else { // No ETH wallets return } let signature = try await wallet.provider.request(.personalSign(message: "A message to sign", address: wallet.add)) print("Result signature: \(signature)") ``` ### Parameters **message** `string` *required* The string to sign with the wallet. **address** `string` The address of the wallet to sign the message with. ### Returns **signature** `string` The signature produced by the wallet. **Android** ### Usage ```kotlin theme={"system"} // Get Privy user val user = privy.user // check if user is authenticated if (user != null) { // Retrieve list of user's embedded Ethereum wallets val ethereumWallets = user.embeddedEthereumWallets if (ethereumWallets.isNotEmpty()) { // Grab the desired wallet. Here, we retrieve the first wallet val ethereumWallet = ethereumWallets.first() // Make an rpc request ethereumWallet.provider.request( request = EthereumRpcRequest.personalSign("A message to sign", ethereumWallet.address), ) } } ``` ### Parameters **method** `'personal_sign'` *required* The method for the wallet request. For signing messages, this is `'personal_sign'`. **params** `Object` *required* **properties** **message** `string | Uint8Array` *required* The string or bytes to sign with the wallet. **address** `string` The address of the wallet to sign the message with. ### Returns **signature** `string` The signature produced by the wallet. **Unity** ### Usage ```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}"); } ``` ### Parameters **method** `'personal_sign'` *required* The method for the wallet request. For signing messages, this is `'personal_sign'`. **params** `Object` *required* **properties** **message** `string | Uint8Array` *required* The string or bytes to sign with the wallet. **address** `string` The address of the wallet to sign the message with. ### Returns **signature** `string` The signature produced by the wallet. **Flutter** ### Usage ```dart theme={"system"} // Get an EIP-1193 Provider final ethereumWallet = privy.user.embeddedEthereumWallets.first; final provider = ethereumWallet.provider; // Sign message final message = 'A message to you Rudy'; final request = EthereumRpcRequest( method: 'personal_sign', params: [message, ethereumWallet.address], ); final result = await provider.request(request); result.fold( onSuccess: (response) { final signature = response.data; print('Signature: $signature'); }, onFailure: (error) { print('Error signing message: ${error.message}'); }, ); ``` ### Parameters **method** `'personal_sign'` *required* The method for the wallet request. For signing messages, this is `'personal_sign'`. **params** `Object` *required* **properties** **message** `string | Uint8Array` *required* The string or bytes to sign with the wallet. **address** `string` The address of the wallet to sign the message with. ### Returns **method** `'personal_sign'` The RPC method executed with the wallet. **data** `Object` Outputs for the RPC method executed with the wallet. **properties** **signature** `string` An encoded string serializing the signature produced by the user's wallet. **NodeJS** Use the `signMessage` method on the Ethereum interface to sign a message with an Ethereum wallet. ### Usage ```tsx theme={"system"} const {signature, encoding} = await privy.wallets().ethereum().signMessage('insert-wallet-id', { message: 'Hello world' }); ``` ### Parameters and Returns Check out the [API reference](/api-reference/wallets/ethereum/personal-sign) for more details. **Java** To sign a message from your wallet, use the `signMessage` method. It will sign your message, and return the signature to you. ### Usage ```java theme={"system"} try { 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(); } catch (APIException e) { String errorBody = e.bodyAsString().orElse(e.getMessage()); System.err.println(errorBody); } catch (Exception e) { System.err.println(e.getMessage()); } ``` ### Parameters When signing a message with your Ethereum wallet, you can sign over the raw bytes of the message or a hex string encoding. **message** `byte[]` The raw bytes of the message to sign over. **message** `String` Alternatively, a hex string encoding of the message to sign over. ### Returns The `EthereumPersonalSignRpcResponseData` object contains a `signature()` field **signature()** `String` The signature produced by the wallet. **Rust** Use the `sign_message` method on the Ethereum service to sign a UTF-8 message with an Ethereum wallet. ### Usage ```rust theme={"system"} use privy_rs::{AuthorizationContext, PrivyClient}; let client = PrivyClient::new("app_id".to_string(), "app_secret".to_string())?; 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"); ``` ### Parameters and Returns See the Rust SDK documentation for detailed parameter and return types, including embedded examples: * [EthereumService::sign\_message](https://docs.rs/privy-rs/latest/privy_rs/ethereum/struct.EthereumService.html#method.sign_message) For REST API details, see the [API reference](/api-reference/wallets/ethereum/personal-sign). **Go** Use the `SignMessage` method on the Ethereum service to sign a message with an Ethereum wallet. ### Usage ```go theme={"system"} response, err := client.Wallets.Ethereum.SignMessage( context.Background(), "wallet-id", "Hello, Ethereum!", ) if err != nil { log.Fatalf("failed to sign message: %v", err) } fmt.Println("Signature:", response.Signature) ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/ethereum/personal-sign) for more details. **Ruby** Use the `rpc` method on the `wallets` service with the `personal_sign` method to sign a message with an Ethereum wallet. ### Usage ```ruby theme={"system"} response = client.wallets.rpc( "wallet-id", wallet_rpc_request_body: { method: "personal_sign", chain_type: "ethereum", params: {message: "Hello, Ethereum!", encoding: "utf-8"} }, authorization_context: ctx ) puts(response.data.signature) ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/ethereum/personal-sign) for more details. **REST API** To sign a message make a POST request to ```bash theme={"system"} https://api.privy.io/v1/wallets//rpc ``` ### Parameters **method** `'personal_sign'` *required* RPC method to execute with the wallet. **params** `Object` *required* Parameters for the RPC method to execute with the wallet. **properties** **message** `string` *required* The message to sign with the wallet. If the message to sign is raw bytes, you must serialize the message as a hexadecimal string. **encoding** `'utf-8' | 'hex'` *required* The encoding format for `params.message`. Use `utf-8` for a string message and `hex` for bytes. ### Returns **method** `'personal_sign'` The RPC method executed with the wallet. **data** `Object` Outputs for the RPC method executed with the wallet. **properties** **signature** `string` An encoded string serializing the signature produced by the user's wallet. **encoding** `'hex'` The encoding format for the returned `signature`. Currently, only `'hex'` is supported for Ethereum. ### Usage ```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 '{ "chain_type": "ethereum", "method": "personal_sign", "params": { "message": "Hello, Ethereum.", "encoding": "utf-8" } }' ``` > [!NOTE] > > Looking to send USDC or another ERC-20 token? See our [Send USDC](/recipes/send-usdc) recipe. **Python** Use the `sign_message` method on the Ethereum wallet service. ```python theme={"system"} response = client.wallets.ethereum.sign_message( wallet_id, "Hello, Privy!", ) signature = response.signature ``` # Sign a raw hash Source: https://docs.privy.io/wallets/using-wallets/ethereum/sign-a-raw-hash > [!NOTE] > > This interface is for raw signing over a hash, which primarily applies to Coinbase Smart Wallet > integrations, EIP-7702 authorizations and other scenarios requiring basic curve-level signing. For > most use cases, such as authenticating to a website or proving ownership, we recommend [signing a > message](/wallets/using-wallets/ethereum/sign-a-message). > [!NOTE] > > When using Privy's server-side SDKs to sign raw hashes, you can use the authorization context to > automatically sign requests. Learn more about [signing on the > server](/controls/authorization-keys/using-owners/sign/signing-on-the-server). **React** To sign a raw hash from a wallet using the React SDK use the `request` method from the wallets EIP1193 provider: ```javascript theme={"system"} request: (request: { method: 'secp256k1_sign', params: [hash: Hex] }) => Promise ``` ### Usage ```javascript theme={"system"} import {useWallets} from '@privy-io/react-auth'; const {wallets} = useWallets(); const wallet = wallets[0]; const provider = await wallet.getEthereumProvider(); // Sign raw hash const response = await provider.request({ method: 'secp256k1_sign', params: ['0xTheRawHash'] }); ``` ### Parameters **request.method** `'secp256k1_sign'` *required* The RPC method executed with the wallet. **request.params** `[hash: Hex]` *required* The raw hash to sign over. ### Returns **response** `string` The signature produced by the wallet. **Go** Use the `SignSecp256k1` method on the Ethereum service to sign a raw hash with an Ethereum wallet. ### Usage ```go theme={"system"} hash := "0x6503b027a625549f7be691646404f275f149d17a119a6804b855bac3030037aa" response, err := client.Wallets.Ethereum.SignSecp256k1( context.Background(), "wallet-id", privy.EthereumSecp256k1SignRpcInputParams{ Hash: hash, }, ) if err != nil { log.Fatalf("failed to sign hash: %v", err) } fmt.Println("Signature:", response.Signature) ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/ethereum/secp256k1-sign) for more details. **Ruby** Use the `raw_sign` method on the `wallets` service to sign a raw hash with an Ethereum wallet. ### Usage ```ruby theme={"system"} hash = "0x6503b027a625549f7be691646404f275f149d17a119a6804b855bac3030037aa" response = client.wallets.raw_sign( "wallet-id", raw_sign_input: {params: {hash: hash}}, authorization_context: ctx ) puts(response.data.signature) ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/ethereum/secp256k1-sign) for more details. **React Native** Request a message signature on the wallets Ethereum provider. ### Usage ```tsx theme={"system"} import {useEmbeddedEthereumWallet} from '@privy-io/expo'; const {wallets} = useEmbeddedEthereumWallet(); const wallet = wallets[0]; // Get an EIP-1193 Provider const provider = await wallet.getProvider(); // Sign raw hash const signature = await provider.request({ method: 'secp256k1_sign', params: ['0xTheRawHash'] }); ``` ### Parameters **request** `Object` *required* **properties** **method** `'secp256k1_sign'` *required* The method for the wallet request. For signing a raw hash, this is `'secp256k1_sign'`. **params** `[string]` *required* The raw hash to sign over, as the first and only element of the array. ### Returns **signature** `string` The signature produced by the wallet. **Swift** To sign a raw hash from a wallet using the Swift SDK use the wallet's Ethereum provider. ### Usage ```swift theme={"system"} import PrivySDK guard let user = privy.user else { // If user is null, user is not authenticated return } // Retrieve list of user's embedded Ethereum wallets let ethereumWallets = user.embeddedEthereumWallets // Grab the desired wallet. Here, we retrieve the first wallet guard let wallet = ethereumWallets.first else { // No ETH wallets return } let signature = try await wallet.provider.request(.secp256k1Sign(hash: "0xTheRawHash")) print("Result signature: \(signature)") ``` ### Parameters **request** `Object` *required* **properties** **method** `'secp256k1_sign'` *required* The method for the wallet request. For signing a raw hash, this is `'secp256k1_sign'`. **params** `[string]` *required* The raw hash to sign over, as the first and only element of the array. ### Returns **signature** `string` The signature produced by the wallet. **Android** To sign a raw hash from a wallet using the Android SDK use the wallet's Ethereum provider. ### Usage ```kotlin theme={"system"} // Get Privy user val user = privy.getUser() // Check if user is authenticated if (user != null) { // Retrieve list of user's embedded Ethereum wallets val ethereumWallets = user.embeddedEthereumWallets if (ethereumWallets.isNotEmpty()) { // Grab the desired wallet. Here, we retrieve the first wallet val ethereumWallet = ethereumWallets.first() // Sign raw hash val result = ethereumWallet.provider.request( request = EthereumRpcRequest.secp256k1Sign("0xTheRawHash") ) when (result) { is Result.Success -> { val signature = result.data.data println("Signature: $signature") } is Result.Failure -> { // Handle error } } } } ``` ### Parameters **request** `Object` *required* **properties** **method** `'secp256k1_sign'` *required* The method for the wallet request. For signing a raw hash, this is `'secp256k1_sign'`. **params** `[string]` *required* The raw hash to sign over, as the first and only element of the array. ### Returns **result** `Result\` A Result that, when successful, contains the EthereumRpcResponse with: **child attributes** **method** `string` The RPC method that was executed (secp256k1\_sign). **data** `string` The signature produced by the wallet. **Unity** ### Usage ```csharp theme={"system"} IPrivyUser privyUser = await PrivyManager.Instance.GetUser(); if (privyUser == null) { // If user is null, user is not authenticated return; } // Grab the desired wallet. Here, we retrieve the first wallet var wallet = privyUser.EmbeddedEthereumWallets[0]; if (wallet == null) { // User has no ETH wallets return; } try { var rpcRequest = new RpcRequest { Method = "secp256k1_sign", Params = new string[] { "0xTheRawHash" } }; var rpcResponse = await wallet.RpcProvider.Request(rpcRequest); var signature = rpcResponse.Data; Debug.Log(signature); } catch (PrivyWalletException ex) { Debug.LogError($"Could not sign message due to error: {ex.Error} {ex.Message}"); } catch (Exception ex) { Debug.LogError($"Could not sign hash exception {ex.Message}"); } ``` ### Parameters **method** `'secp256k1_sign'` *required* The method for the wallet request. For signing a raw hash, this is `'secp256k1_sign'`. **params** `Object` *required* **properties** **hash** `string` *required* The hash to sign with the wallet. Must start with '0x'. ### Returns **signature** `string` The signature produced by the wallet. **Flutter** To sign a raw hash from a wallet using the Flutter SDK use the wallet's Ethereum provider. ### Usage ```dart theme={"system"} // Get the Privy User final user = await privy.getUser(); // Get an EIP-1193 Provider final ethereumWallet = user.embeddedEthereumWallets.first; final provider = ethereumWallet.provider; // Sign raw hash final request = EthereumRpcRequest( method: 'secp256k1_sign', params: ['0xTheRawHash'], ); final result = await provider.request(request); result.fold( onSuccess: (response) { final signature = response.data; print('Signature: $signature'); }, onFailure: (error) { print('Error signing hash: ${error.message}'); }, ); ``` ### Parameters **request** `Object` *required* **properties** **method** `'secp256k1_sign'` *required* The method for the wallet request. For signing a raw hash, this is `'secp256k1_sign'`. **params** `[string]` *required* The raw hash to sign over, as the first and only element of the array. ### Returns **result** `Result\` A Result that, when successful, contains the EthereumRpcResponse with: **child attributes** **method** `string` The RPC method that was executed (secp256k1\_sign). **data** `string` The signature produced by the wallet. **NodeJS** Use the `secp256k1Sign` method on the Ethereum interface to sign a raw hash along the secp256k1 curve. ### Usage ```tsx theme={"system"} const {signature, encoding} = await privy .wallets() .ethereum() .secp256k1Sign('insert-wallet-id', { params: { hash: '0x6503b027a625549f7be691646404f275f149d17a119a6804b855bac3030037aa' } }); ``` ### Parameters and Returns Check out the [API reference](/api-reference/wallets/ethereum/secp256k1-sign) for more details. **Java** To sign a raw hash from your wallet, use the `signSecp256k1` method. It will sign your hash, and return the signature to you. ### Usage ```java theme={"system"} try { String hash = "0x6503b027a625549f7be691646404f275f149d17a119a6804b855bac3030037aa"; // Example: If wallet's owner is an authorization private key AuthorizationContext authorizationContext = AuthorizationContext.builder() .addAuthorizationPrivateKey("authorization-key") .build(); EthereumSecp256k1SignRpcResponseData response = privyClient .wallets() .ethereum() .signSecp256k1( walletId, hash, authorizationContext ); String signature = response.signature(); } catch (APIException e) { String errorBody = e.bodyAsString().orElse(e.getMessage()); System.err.println(errorBody); } catch (Exception e) { System.err.println(e.getMessage()); } ``` ### Parameters **hash** `String` The hash to sign. Must start with '0x'. ### Returns The `EthereumSecp256k1SignRpcResponseData` object contains a `signature()` field **signature()** `String` The signature produced by the wallet. **Rust** Use the `sign_secp256k1` method on the Ethereum service to sign a raw hash with an Ethereum wallet. ### Usage ```rust theme={"system"} use privy_rs::{AuthorizationContext, PrivyClient}; let client = PrivyClient::new("app_id".to_string(), "app_secret".to_string())?; let ethereum_service = client.wallets().ethereum(); let auth_ctx = AuthorizationContext::new(); // Pre-computed keccak256 hash let hash = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; let signature = ethereum_service .sign_secp256k1(&wallet_id, hash, &auth_ctx, None) .await?; println!("Hash signed with secp256k1"); ``` ### Parameters and Returns See the Rust SDK documentation for detailed parameter and return types, including embedded examples: * [EthereumService::sign\_secp256k1](https://docs.rs/privy-rs/latest/privy_rs/ethereum/struct.EthereumService.html#method.sign_secp256k1) For REST API details, see the [API reference](/api-reference/wallets/ethereum/secp256k1-sign). **REST API** To sign a message make a POST request to ```bash theme={"system"} https://api.privy.io/v1/wallets//rpc ``` ### Parameters **method** `'secp256k1_sign'` *required* RPC method to execute with the wallet. **params** `Object` *required* Parameters for the RPC method to execute with the wallet. **properties** **hash** `string` *required* The hash to sign with the wallet. Must start with '0x'. ### Returns **method** `'secp256k1_sign'` The RPC method executed with the wallet. **data** `Object` Outputs for the RPC method executed with the wallet. **properties** **signature** `string` An encoded string serializing the signature produced by the user's wallet. **encoding** `'hex'` The encoding format for the returned `signature`. Currently, only `'hex'` is supported for Ethereum. ### Usage ```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 '{ "chain_type": "ethereum", "method": "secp256k1_sign", "params": { "hash": "0x6503b027a625549f7be691646404f275f149d17a119a6804b855bac3030037aa", "encoding": "hex" } }' ``` **Python** Use the `sign_secp256k1` method on the Ethereum wallet service. ```python theme={"system"} response = client.wallets.ethereum.sign_secp256k1( wallet_id, params={ "hash": "0x6503b027a625549f7be691646404f275f149d17a119a6804b855bac3030037aa" }, ) signature = response.signature ``` # Sign an Ethereum transaction Source: https://docs.privy.io/wallets/using-wallets/ethereum/sign-a-transaction Sign an Ethereum/EVM transaction without broadcasting it using Privy > [!NOTE] > > When using Privy's server-side SDKs to sign transactions, you can use the authorization context to > automatically sign requests. Learn more about [signing on the > server](/controls/authorization-keys/using-owners/sign/signing-on-the-server). **React** To send a transaction from a wallet using the React SDK, use the `signTransaction` method from the `useSignTransaction` hook: ```javascript theme={"system"} signTransaction: (input: UnsignedTransactionRequest, options?: SendTransactionOptions) => Promise<{ signature: HexString }> ``` ### Usage ```javascript theme={"system"} import {useSignTransaction, useWallets} from '@privy-io/react-auth'; const {signTransaction} = useSignTransaction(); const {wallets} = useWallets(); signTransaction( { to: '0xE3070d3e4309afA3bC9a6b057685743CF42da77C', value: 100000 }, { address: wallets[0].address // Optional: Specify the wallet to use for signing. If not provided, the first wallet will be used. } ); ``` ### Parameters **input** `UnsignedTransactionRequest` *required* The details of the transaction to sign. **options.uiOptions** `SendTransactionModalUIOptions` The options for the UI of the send transaction modal. [Learn more](/wallets/using-wallets/ui-components). > [!TIP] > > To hide confirmation modals, set `options.uiOptions.showWalletUIs` to `false`. Learn more about > configuring modal prompts [here](/recipes/react/manage-wallet-UIs). **options.fundWalletConfig** `FundWalletConfig` The configuration for funding the wallet. **options.address** `string` The address of the wallet to use for signing the transaction. **Recommended when working with external wallets** to ensure reliable functionality. If not provided, the first wallet will be used. ### Returns **signature** `HexString` The signed transaction hash. **React Native** To sign a transaction from a wallet using the React Native SDK use the `request` method from the wallets EIP1193 provider: ```javascript theme={"system"} request: (request: { method: 'eth_signTransaction', params: [SignTransactionParams] }) => Promise ``` > [!NOTE] > > The Expo SDK does not support built-in UIs for sending transactions. > The `eth_signTransaction` method gives you complete control over the experience and UI. ### Usage ```javascript 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', }); // Sign transaction (will be signed and populated) const response = await provider.request({ method: 'eth_signTransaction', params: [ { from: accounts[0], to: '0x0000000000000000000000000000000000000000', value: '1', }, ], }); ``` ### Parameters **request.method** `'eth_signTransaction'` *required* The RPC method executed with the wallet. **request.params** `SignTransactionParams` *required* The details of the transaction to sign. ### Returns **response** `HexString` The hash for the broadcasted transaction. **Swift** Use the `request` method on the Ethereum provider to sign a transaction with an Ethereum wallet. ```swift theme={"system"} func request(_ request: EthereumRpcRequest) async throws -> String ``` ### Usage ```swift theme={"system"} let provider = wallet.provider let transaction = EthereumRpcRequest.UnsignedEthTransaction( from: wallet.address, to: "0xE3070d3e4309afA3bC9a6b057685743CF42da77C", value: .int(100000), chainId: .hexadecimal("0x2105"), // Base, in hex ) let transactionSignature = try await provider.request(.ethSignTransaction(transaction: transaction)) ``` ### Parameters **transaction** `EthereumRpcRequest.UnsignedEthTransaction` *required* The transaction to send. **child attributes** **from** `string` The address sending the transaction (the user's wallet address). **to** `string` *required* The address to send the transaction to. **value** `Quantity` The amount of wei to send, as an integer or hexadecimal string. **chainId** `Quantity` The chain ID as an integer or hexadecimal string. Defaults to mainnet (0x1) if omitted. **data** `string` The data to include with the transaction, as a hexadecimal string. **gasLimit** `string` The gas limit for the transaction, as a hexadecimal string. ### Returns **transactionSignature** `string` The signature of the transaction. **Android** Use the `request` method on the Ethereum wallet provider to sign a transaction with an Ethereum wallet. ```kotlin theme={"system"} public suspend fun request(request: EthereumRpcRequest): Result ``` ### Usage ```kotlin theme={"system"} val transaction = JSONObject().apply { put("to", "0xE3070d3e4309afA3bC9a6b057685743CF42da77C") put("value", "0x186a0") // 100000 in hex put("chainId", "0x2105") // 8453 (Base) in hex put("from", ethereumWallet.address) }.toString() val result = ethereumWallet.provider.request( request = EthereumRpcRequest.ethSignTransaction(transaction), ) when (result) { is Result.Success -> { val transactionHash = result.data.data // Handle successful transaction } is Result.Failure -> { // Handle error } } ``` ### Parameters **method** `'eth_signTransaction'` *required* The RPC method to execute with the wallet. **params** `List\` *required* Array containing the transaction JSON as a single string element. **child attributes** **to** `string` *required* The address to send the transaction to. **value** `string` The amount of wei to send, as a hexadecimal string. **chainId** `string` The chain ID as a hexadecimal string. Defaults to mainnet (0x1) if omitted. **from** `string` The address sending the transaction (the user's wallet address). **data** `string` The data to include with the transaction, as a hexadecimal string. **gasLimit** `string` The gas limit for the transaction, as a hexadecimal string. ### Returns **result** `Result\` A Result that, when successful, contains the EthereumRpcResponse with: **child attributes** **method** `string` The RPC method that was executed (eth\_signTransaction). **data** `string` The transaction hash for the broadcasted transaction. **Unity** Use the `Request` method on the wallet's RPC provider to sign a transaction with an Ethereum wallet. ```csharp theme={"system"} public Task Request(RpcRequest request); ``` ### Usage ```csharp theme={"system"} // Create transaction JSON string transactionJson = JsonUtility.ToJson(new { to = "0xE3070d3e4309afA3bC9a6b057685743CF42da77C", value = "0x186a0", // 100000 in hex chainId = "0x2105", // 8453 (Base) in hex from = embeddedWallet.Address }); // Create RPC request var rpcRequest = new RpcRequest { Method = "eth_signTransaction", Params = new string[] { transactionJson } }; // Sign transaction RpcResponse transactionResponse = await embeddedWallet.RpcProvider.Request(rpcRequest); // Transaction hash is in the response data string transactionHash = transactionResponse.Data; ``` ### Parameters **Method** `'eth_signTransaction'` *required* The RPC method to execute with the wallet. **Params** `string[]` *required* Array containing the transaction JSON as a single string element. **child attributes** **to** `string` *required* The address to send the transaction to. **value** `string` The amount of wei to send, as a hexadecimal string. **chainId** `string` The chain ID as a hexadecimal string. Defaults to mainnet (0x1) if omitted. **from** `string` The address sending the transaction (the user's wallet address). **data** `string` The data to include with the transaction, as a hexadecimal string. **gasLimit** `string` The gas limit for the transaction, as a hexadecimal string. ### Returns **RpcResponse** `RpcResponse` An RPC response object with: **child attributes** **method** `string` The RPC method that was executed (eth\_signTransaction). **data** `string` The transaction hash for the broadcasted transaction. **Flutter** Use the `request` method on the Ethereum wallet provider to sign a transaction with an Ethereum wallet. ```dart theme={"system"} Future> request(EthereumRpcRequest request); ``` ### Usage ```dart theme={"system"} // Create transaction parameters as a Map final transactionMap = { 'to': '0xE3070d3e4309afA3bC9a6b057685743CF42da77C', 'value': '0x186a0', // 100000 in hex 'chainId': '0x2105', // 8453 (Base) in hex 'from': ethereumWallet.address }; // Convert Map to JSON string final transactionJson = jsonEncode(transactionMap); // Create the RPC request final rpcRequest = EthereumRpcRequest( method: 'eth_signTransaction', params: [transactionJson], ); // Sign the transaction final result = await ethereumWallet.provider.request(rpcRequest); // Handle the result result.when( success: (response) { final transactionHash = response.data; print('Transaction sent with hash: $transactionHash'); }, failure: (error) { print('Failed to sign transaction: $error'); }, ); ``` ### Parameters **method** `'eth_signTransaction'` *required* The RPC method to execute with the wallet. **params** `List\` *required* List containing the transaction JSON as a single string element. **child attributes** **to** `string` *required* The address to send the transaction to. **value** `string` The amount of wei to send, as a hexadecimal string. **chainId** `string` The chain ID as a hexadecimal string. Defaults to mainnet (0x1) if omitted. **from** `string` The address sending the transaction (the user's wallet address). **data** `string` The data to include with the transaction, as a hexadecimal string. **gasLimit** `string` The gas limit for the transaction, as a hexadecimal string. ### Returns **result** `Result\` A Result that, when successful, contains the EthereumRpcResponse with: **child attributes** **method** `string` The RPC method that was executed (eth\_signTransaction). **data** `string` The transaction hash for the broadcasted transaction. **Python** Use the `sign_transaction` method on the Ethereum wallet service. ```python theme={"system"} response = client.wallets.ethereum.sign_transaction( wallet_id, params={ "transaction": { "type": 2, "chain_id": 1, "to": "insert-recipient-address", "value": "0x1", "gas_limit": "0x5208", "data": "0x", } }, ) signed_transaction = response.signed_transaction ``` **NodeJS** > [!TIP] > > Privy is fully compatible with popular web3 libraries for interfacing wallets, including [`viem`](https://viem.sh/), learn more [here](/wallets/using-wallets/ethereum/web3-integrations#node-js). Use the `signTransaction` method on the Ethereum interface to sign a transaction with an Ethereum wallet. ### Usage ```js theme={"system"} const {signed_transaction, encoding} = await privy.wallets().ethereum().signTransaction('insert-wallet-id', { params: { transaction: { to: '0xE3070d3e4309afA3bC9a6b057685743CF42da77C', value: '0x2386F26FC10000', chain_id: 8453, }, } }); ``` ### Parameters and Returns Check out the [API reference](/api-reference/wallets/ethereum/eth-sign-transaction) for more details. **Java** To sign a transaction from your wallet, use the `signTransaction` method. It will sign your transaction, and return the signed transaction to you. ### Usage ```java theme={"system"} try { UnsignedStandardEthereumTransaction txn = UnsignedStandardEthereumTransaction.builder() .to(recipientAddress) .value(Quantity.of("0x1")) // 1 wei .chainId(Quantity.of(11_155_111)) // Sepolia testnet .build(); // Example: If wallet's owner is an authorization private key AuthorizationContext authorizationContext = AuthorizationContext.builder() .addAuthorizationPrivateKey("authorization-key") .build(); EthereumSignTransactionRpcResponseData response = privyClient .wallets() .ethereum() .signTransaction( walletId, txn, authorizationContext ); String signedTransaction = response.signedTransaction(); } catch (APIException e) { String errorBody = e.bodyAsString().orElse(e.getMessage()); System.err.println(errorBody); } catch (Exception e) { System.err.println(e.getMessage()); } ``` ### Parameters When defining a transaction, you may specify the following values on the `UnsignedStandardEthereumTransaction` builder: **from** `String` The address sending the transaction (the user's wallet address). **to** `String` The address to send the transaction to. **chainId** `Quantity` The ID of the chain the transaction is being sent on. **nonce** `Quantity` The nonce for the transaction. **data** `String` The data to include with the transaction, as a hexadecimal string. **value** `Quantity` The amount of wei to send, as an integer or hexadecimal string. **type** `Object` The type of transaction to send. **gasLimit** `Quantity` The gas limit for the transaction, as a hexadecimal string. **gasPrice** `Quantity` The gas price for the transaction, as a hexadecimal string. **maxFeePerGas** `Quantity` The maximum fee per gas for the transaction, as a hexadecimal string. **maxPriorityFeePerGas** `Quantity` The maximum priority fee per gas for the transaction, as a hexadecimal string. ### Returns The `EthereumSignTransactionRpcResponseData` object contains a `signedTransaction()` field **signedTransaction()** `String` The signed transaction. **Rust** > [!TIP] > > Privy is fully compatible with popular libraries for interfacing wallets, including [`alloy`](https://crates.io/crates/alloy), learn more [here](/wallets/using-wallets/ethereum/web3-integrations#rust). Use the `sign_transaction` method on the Ethereum service to sign a transaction with an Ethereum wallet. ### Usage ```rust theme={"system"} use privy_rs::{AuthorizationContext, PrivyClient}; let client = PrivyClient::new("app_id".to_string(), "app_secret".to_string())?; let ethereum_service = client.wallets().ethereum(); let auth_ctx = AuthorizationContext::new(); let transaction = EthereumSignTransactionRpcInputParamsTransaction { to: Some("0x742d35Cc6635C0532925a3b8c17d6d1E9C2F7ca".to_string()), value: None, gas_limit: None, gas_price: None, nonce: None, chain_id: None, data: None, from: None, max_fee_per_gas: None, max_priority_fee_per_gas: None, type_: None, }; let signed_tx = ethereum_service .sign_transaction(&wallet_id, transaction, &auth_ctx, None) .await?; println!("Transaction signed successfully"); ``` ### Parameters and Returns See the Rust SDK documentation for detailed parameter and return types, including embedded examples: * [EthereumService::sign\_transaction](https://docs.rs/privy-rs/latest/privy_rs/ethereum/struct.EthereumService.html#method.sign_transaction) For REST API details, see the [API reference](/api-reference/wallets/ethereum/eth-sign-transaction). **Go** Use the `SignTransaction` method on the Ethereum service to sign a transaction with an Ethereum wallet. ### Usage ```go theme={"system"} response, err := client.Wallets.Ethereum.SignTransaction( context.Background(), "wallet-id", privy.EthereumSignTransactionRpcInputParams{ Transaction: privy.EthereumSignTransactionRpcInputParamsTransaction{ To: privy.String("0xE3070d3e4309afA3bC9a6b057685743CF42da77C"), Value: privy.EthereumSignTransactionRpcInputParamsTransactionValueUnion{ OfString: privy.String("0x2386F26FC10000"), }, ChainID: privy.EthereumSignTransactionRpcInputParamsTransactionChainIDUnion{ OfInt: privy.Int(11155111), // Sepolia }, }, }, ) if err != nil { log.Fatalf("failed to sign transaction: %v", err) } fmt.Println("Signed transaction:", response.SignedTransaction) ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/ethereum/eth-sign-transaction) for more details. **Ruby** Use the `rpc` method on the `wallets` service with the `eth_signTransaction` method to sign a transaction with an Ethereum wallet. ### Usage ```ruby theme={"system"} response = client.wallets.rpc( "wallet-id", wallet_rpc_request_body: { method: "eth_signTransaction", chain_type: "ethereum", params: { transaction: { to: "0xE3070d3e4309afA3bC9a6b057685743CF42da77C", value: "0x2386F26FC10000", chain_id: 11_155_111 # Sepolia testnet } } }, authorization_context: ctx ) puts(response.data.signed_transaction) ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/ethereum/eth-sign-transaction) for more details. **REST API** To sign a transaction make a `POST` request to ```bash theme={"system"} https://api.privy.io/v1/wallets//rpc ``` ### Usage ```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_signTransaction", "params": { "transaction": { "to": "0xE3070d3e4309afA3bC9a6b057685743CF42da77C", "value": "0x2386F26FC10000", "chain_id": 1, "type": 2, "gas_limit": "0x5208", "nonce": 1, "max_fee_per_gas": "0x14bf7dadac", "max_priority_fee_per_gas": "0xf4240" } } }' ``` A successful response will look like the following: ```json theme={"system"} { "method": "eth_signTransaction", "data": { "signed_transaction": "0x28eac519bf4051a624d4246a5788667baf84dcd7d2a439b314b339013b5cdb4c", "encoding": "rlp" } } ``` ### Parameters **method** `'eth_signTransaction'` *required* The RPC method executed with the wallet. **params** `Object` *required* **transaction** `EthereumTransactionType` *required* The details of the transaction to send on the chain. ### Returns **method** `'eth_signTransaction'` The RPC method executed with the wallet. **data.signed\_transaction** `string` The signed transaction. **data.encoding** `'rlp'` The encoding format for the signed transaction. Currently, only `'rlp'` is supported for Ethereum. # Sign typed data (EIP-712) Source: https://docs.privy.io/wallets/using-wallets/ethereum/sign-typed-data > [!NOTE] > > When using Privy's server-side SDKs to sign typed data, you can use the authorization context to > automatically sign requests. Learn more about [signing on the > server](/controls/authorization-keys/using-owners/sign/signing-on-the-server). **React** Use the `signTypedData` method exported from the `useSignTypedData` hook to generate an EIP-712 typed data signature with an Ethereum embedded wallet. ```javascript theme={"system"} signTypedData(input: SignTypedDataParams, options?: SignTypedDataOptions): Promise<{signature: string}> ``` ### Usage ```tsx theme={"system"} import {useSignTypedData, useWallets} from '@privy-io/react-auth'; const {signTypedData} = useSignTypedData(); const {wallets} = useWallets(); const {signature} = await signTypedData({...}, { address: wallets[0].address // Optional: Specify the wallet to use for signing. If not provided, the first wallet will be used. }); ``` ### Parameters **input** `Object` *required* The typed data object to sign with the wallet, with the properties defined in [EIP-712](https://eips.ethereum.org/EIPS/eip-712). **options** `Object` **properties** **uiOptions** `SignMessageModalUIOptions` UI options to customize the signature prompt modal. [Learn more](/wallets/using-wallets/ui-components) > [!TIP] > > To hide confirmation modals, set `options.uiOptions.showWalletUIs` to `false`. Learn more > about configuring modal prompts [here](/recipes/react/manage-wallet-UIs). **address** `string` Address of the wallet to use for signing the typed data. **Recommended when working with external wallets** to ensure reliable functionality. If not provided, the first wallet will be used. ### Response **signature** `string` The signature produced by the wallet. ### Callbacks Configure callbacks for `signTypedData` with the `useSignTypedData` hook. ```tsx theme={"system"} import {useSignTypedData} from '@privy-io/react-auth'; const {signTypedData} = useSignTypedData({ onSuccess: ({signature}) => { console.log(signature); // Any logic you'd like to execute after a user successfully signs the EIP-712 typed data }, onError: (error) => { console.log(error); // Any logic you'd like to execute after a user exits the signing flow or there is an error } }); // Then call `signTypedData` in your code, which will invoke these callbacks on completion ``` As parameters to **`useSignTypedData`**, you may include an **`onSuccess`** callback and/or an **`onError`** callback. While this component is mounted, any invocation of **`signTypedData`** will trigger the **`onSuccess`** callback or **`onError`** callback on completion, depending on if the data was successfully signed or not. ### onSuccess If set, the **`onSuccess`** callback will execute after a user has successfully signed the message. Within this callback, you can access a **`signature`** parameter, which is the **`signature`** string value generated by the wallet to sign the data. ### onError If set, the **`onError`** callback will execute after a user attempts to sign the typed data and there is an error, or if the user exits the signature flow prematurely. Within this callback, you may access an **`error`** code with more information about the error. **React Native** Request a message signature on the wallets Ethereum provider. ```javascript theme={"system"} request: ({method: 'eth_signTypedData_v4', params: [address, typedData]}) => Promise<{signature: string}> ``` > [!NOTE] > > The Expo SDK does not support built-in UIs for signing typed data. The `eth_signTypedData_v4` > method gives you complete control over the experience and UI. ### Usage ```tsx theme={"system"} import {useEmbeddedEthereumWallet} from '@privy-io/expo'; const {wallets} = useEmbeddedEthereumWallet(); const wallet = wallets[0]; // Get an EIP-1193 Provider const provider = await wallet.getProvider(); // Get address const accounts = await provider.request({ method: 'eth_requestAccounts' }); const signature = await provider.request({ method: 'eth_signTypedData_v4', params: [accounts[0], typedData] }); ``` ### Parameters **method** `'eth_signTypedData_v4'` *required* The method for the wallet request. For signing messages, this is `'eth_signTypedData_v4'`. **params** `Object` *required* **properties** **address** `string` The address of the wallet to sign the message with. **typeData** `Object` *required* The typed data to sign with the wallet. ### Returns **signature** `string` The signature produced by the wallet. **Swift** To request an [EIP712](https://eips.ethereum.org/EIPS/eip-712) signature from a user's embedded wallet, send an [`eth_signTypedData_v4`](https://docs.metamask.io/wallet/reference/eth_signtypeddata_v4/) JSON-RPC request to the wallet's EIP1193 provider. ```swift theme={"system"} struct Message: Encodable { struct W: Encodable { let name: String let wallet: String } let from: W let to: W let contents: String } func typedData() async throws { let wallets = user.embeddedEthereumWallets // Grab the desired wallet. Here, we retrieve the first wallet guard let wallet = wallets.first else { // No ETH wallets return } let typedData = EthereumRpcRequest.EIP712TypedData( types: [ "EIP712Domain": [ .init("name", type: "string"), .init("version", type: "string"), .init("chainId", type: "uint256"), .init("verifyingContract", type: "address"), ], "Person": [ .init("name", type: "string"), .init("wallet", type: "address"), ], "Mail": [ .init("from", type: "Person"), .init("to", type: "Person"), .init("contents", type: "string"), ], ], primaryType: "Mail", domain: .init( name: "Ether Mail", version: "1", chainId: 1, verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", ), message: Message( from: Message.W(name: "Cow", wallet: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"), to: Message.W(name: "Bob", wallet: "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"), contents: "Hello, Bob!" ) ) let signature = try await provider.request(.ethSignTypedDataV4(address: wallet.address, typedData: typedData)) print(signature) } ``` ### Parameters **address** `string` The address of the wallet to sign the message with. **typeddata** `EthereumRpcRequest.EIP712TypedData` *required* The typed data to sign with the wallet. **child attributes** **types** `[String: [EIP712Type]]` *required* The types of the typed data, as defined by mapping the type names to an array of `EIP712Type` objects for each of its fields. Make sure to include the `EIP712Domain` type. **primaryType** `string` *required* The primary type of the typed data. **domain** `EIP712Domain` *required* The domain of the typed data, per the [EIP-712](https://eips.ethereum.org/EIPS/eip-712) specification. Use the `EIP712Domain` struct to define the domain. **message** `Encodable` *required* The message of the typed data. This can be any struct that conforms to the `Encodable` protocol. ### Returns **signature** `string` The signature produced by the wallet. **Android** ```kotlin theme={"system"} // Get Privy user ethereumWallet.provider.request( request = EthereumRpcRequest.ethSignTypedDataV4(ethereumWallet.address, typedData), ) ``` ### Parameters **method** `'eth_signTypedData_v4'` *required* The method for the wallet request. For signing messages, this is `'eth_signTypedData_v4'`. **params** `Object` *required* **properties** **address** `string` The address of the wallet to sign the message with. **typeData** `Object` *required* The typed data to sign with the wallet. ### Returns **signature** `string` The signature produced by the wallet. **Unity** ```csharp theme={"system"} try { IPrivyUser privyUser = await PrivyManager.Instance.GetUser(); IEmbeddedEthereumWallet embeddedWallet = privyUser.EmbeddedEthereumWallets[0]; var rpcRequest = new RpcRequest { Method = "eth_signTypedData_v4", Params = new string[] {embeddedWallet.Address, typedData } }; RpcResponse signTypedDataResponse = await embeddedWallet.RpcProvider.Request(rpcRequest); Debug.Log(signTypedDataResponse.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}"); } ``` ### Parameters **method** `'eth_signTypedData_v4'` *required* The method for the wallet request. For signing messages, this is `'eth_signTypedData_v4'`. **params** `Object` *required* **properties** **address** `string` The address of the wallet to sign the message with. **typedData** `Object` *required* The typed data to sign with the wallet. ### Returns **signature** `string` The signature produced by the wallet. **Flutter** ```dart theme={"system"} // Get an EIP-1193 Provider final ethereumWallet = privy.user.embeddedEthereumWallets.first; final provider = ethereumWallet.provider; // Define the typed data final typedData = { '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'}, ], 'Mail': [ {'name': 'from', 'type': 'Person'}, {'name': 'to', 'type': 'Person'}, {'name': 'contents', 'type': 'string'}, ], }, 'primaryType': 'Mail', 'domain': { 'name': 'Ether Mail', 'version': '1', 'chainId': 1, 'verifyingContract': '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC', }, 'message': { 'from': { 'name': 'Cow', 'wallet': '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826', }, 'to': { 'name': 'Bob', 'wallet': '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB', }, 'contents': 'Hello, Bob!', }, }; // Create the RPC request final request = EthereumRpcRequest( method: 'eth_signTypedData_v4', params: [ethereumWallet.address, jsonEncode(typedData)], ); // Sign the typed data final result = await provider.request(request); result.fold( onSuccess: (response) { final signature = response.data; print('Signature: $signature'); }, onFailure: (error) { print('Error signing typed data: ${error.message}'); }, ); ``` ### Parameters **method** `'eth_signTypedData_v4'` *required* The method for the wallet request. For signing typed data, this is `'eth_signTypedData_v4'`. **params** `Object` *required* **properties** **address** `string` The address of the wallet to sign the message with. **typedData** `Object` *required* The typed data to sign with the wallet. ### Returns **method** `'eth_signTypedData_v4'` The RPC method executed with the wallet. **data** `Object` Outputs for the RPC method executed with the wallet. **properties** **signature** `string` An encoded string serializing the signature produced by the user's wallet. **NodeJS** Use the `signTypedData` method on the Ethereum interface to sign a message with an Ethereum wallet. ### Usage ```tsx theme={"system"} // Get the signature and encoding from the response const {signature, encoding} = await privy .wallets() .ethereum() .signTypedData('insert-wallet-id', { params: { typed_data: { domain: { name: 'Ether Mail', version: '1', chainId: 1, verifyingContract: '0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC' }, 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'} ], Mail: [ {name: 'from', type: 'Person'}, {name: 'to', type: 'Person'}, {name: 'contents', type: 'string'} ] }, primary_type: 'Mail', message: { from: { name: 'Cow', wallet: '0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826' }, to: { name: 'Bob', wallet: '0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB' }, contents: 'Hello, Bob!' } } } }); ``` ### Parameters and Returns Check out the [API reference](/api-reference/wallets/ethereum/eth-signtypeddata-v4) for more details. **Java** To sign typed data from your wallet, use the `signTypedData` method. It will sign your payload, and return the signature to you. ### Usage ```java theme={"system"} try { // Create typed data for EIP-712 signing Map domain = Map.of( "name", "Ether Mail", "version", "1", "chainId", 1, "verifyingContract", "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" ); Map> types = Map.of( "EIP712Domain", List.of( EthereumSignTypedDataRpcInputType.builder().name("name").type("string").build(), EthereumSignTypedDataRpcInputType.builder().name("version").type("string").build(), EthereumSignTypedDataRpcInputType.builder().name("chainId").type("uint256").build(), EthereumSignTypedDataRpcInputType.builder().name("verifyingContract").type("address").build() ), "Person", List.of( EthereumSignTypedDataRpcInputType.builder().name("name").type("string").build(), EthereumSignTypedDataRpcInputType.builder().name("wallet").type("address").build() ), "Mail", List.of( EthereumSignTypedDataRpcInputType.builder().name("from").type("Person").build(), EthereumSignTypedDataRpcInputType.builder().name("to").type("Person").build(), EthereumSignTypedDataRpcInputType.builder().name("contents").type("string").build() ) ); Map message = Map.of( "from", Map.of( "name", "Cow", "wallet", "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826" ), "to", Map.of( "name", "Bob", "wallet", "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB" ), "contents", "Hello, Bob!" ); EthereumSignTypedDataRpcInputTypedData typedData = EthereumSignTypedDataRpcInputTypedData.builder() .domain(domain) .types(types) .primaryType("TestMessage") .message(message) .build(); // Example: If wallet's owner is an authorization private key AuthorizationContext authorizationContext = AuthorizationContext.builder() .addAuthorizationPrivateKey("authorization-key") .build(); EthereumSignTypedDataRpcResponseData response = privyClient .wallets() .ethereum() .signTypedData( walletId, typedData, authorizationContext ); String signature = response.signature(); } catch (APIException e) { String errorBody = e.bodyAsString().orElse(e.getMessage()); System.err.println(errorBody); } catch (Exception e) { System.err.println(e.getMessage()); } ``` ### Parameters When defining a typed data payload, you may specify the following values on the `EthereumSignTypedDataRpcInputTypedData` builder: **domain** `Map\` The domain of the typed data payload. **types** `Map\\>` The types of the typed data payload. **message** `Map\` The message of the typed data payload. **primaryType** `String` The primary type of the typed data payload. ### Returns The `EthereumSignTypedDataRpcResponseData` object contains a `signature()` field **signature()** `String` The signature produced by the wallet. **Rust** Use the `sign_typed_data` method on the Ethereum service to sign EIP-712 typed data with an Ethereum wallet. ### Usage ```rust theme={"system"} use privy_rs::{AuthorizationContext, PrivyClient}; let client = PrivyClient::new("app_id".to_string(), "app_secret".to_string())?; let ethereum_service = client.wallets().ethereum(); let auth_ctx = AuthorizationContext::new(); // Create EIP-712 typed data structure let typed_data = EthereumSignTypedDataRpcInputParamsTypedData { domain: Default::default(), message: Default::default(), primary_type: "Mail".to_string(), types: Default::default(), }; let signature = ethereum_service .sign_typed_data(&wallet_id, typed_data, &auth_ctx, None) .await?; println!("Typed data signed successfully"); ``` ### Parameters and Returns See the Rust SDK documentation for detailed parameter and return types, including embedded examples: * [EthereumService::sign\_typed\_data](https://docs.rs/privy-rs/latest/privy_rs/ethereum/struct.EthereumService.html#method.sign_typed_data) For REST API details, see the [API reference](/api-reference/wallets/ethereum/eth-signtypeddata-v4). **Go** Use the `SignTypedData` method on the Ethereum service to sign EIP-712 typed data with an Ethereum wallet. ### Usage ```go theme={"system"} response, err := client.Wallets.Ethereum.SignTypedData( context.Background(), "wallet-id", privy.EthereumSignTypedDataRpcInputParams{ TypedData: privy.EthereumSignTypedDataRpcInputParamsTypedData{ Domain: map[string]any{ "name": "Ether Mail", "version": "1", "chainId": 1, "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC", }, Types: map[string][]privy.EthereumSignTypedDataRpcInputParamsTypedDataType{ "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"}, }, "Mail": { {Name: "from", Type: "Person"}, {Name: "to", Type: "Person"}, {Name: "contents", Type: "string"}, }, }, PrimaryType: "Mail", Message: map[string]any{ "from": map[string]string{ "name": "Cow", "wallet": "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826", }, "to": map[string]string{ "name": "Bob", "wallet": "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB", }, "contents": "Hello, Bob!", }, }, }, ) if err != nil { log.Fatalf("failed to sign typed data: %v", err) } fmt.Println("Signature:", response.Signature) ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/ethereum/eth-signtypeddata-v4) for more details. **Ruby** Use the `rpc` method on the `wallets` service with the `eth_signTypedData_v4` method to sign EIP-712 typed data with an Ethereum wallet. ### Usage ```ruby theme={"system"} typed_data = { domain: { name: "Ether Mail", version: "1", chainId: 1, verifyingContract: "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" }, 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"} ], Mail: [ {name: "from", type: "Person"}, {name: "to", type: "Person"}, {name: "contents", type: "string"} ] }, primary_type: "Mail", message: { from: {name: "Cow", wallet: "0xCD2a3d9F938E13CD947Ec05AbC7FE734Df8DD826"}, to: {name: "Bob", wallet: "0xbBbBBBBbbBBBbbbBbbBbbbbBBbBbbbbBbBbbBBbB"}, contents: "Hello, Bob!" } } response = client.wallets.rpc( "wallet-id", wallet_rpc_request_body: { method: "eth_signTypedData_v4", chain_type: "ethereum", params: {typed_data: typed_data} }, authorization_context: ctx ) puts(response.data.signature) ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/ethereum/eth-signtypeddata-v4) for more details. **REST API** To sign typed data make a POST request to ```bash theme={"system"} https://api.privy.io/v1/wallets//rpc ``` ### Parameters **method** `'eth_signTypedData_v4'` *required* RPC method to execute with the wallet. **params** `Object` *required* Parameters for the RPC method to execute with the wallet. **properties** **message** `string` *required* The message to sign with the wallet. If the message to sign is raw bytes, you must serialize the message as a hexadecimal string. **encoding** `'utf-8' | 'hex'` *required* The encoding format for `params.message`. Use `utf-8` for a string message and `hex` for bytes. ### Returns **method** `'eth_signTypedData_v4'` The RPC method executed with the wallet. **data** `Object` Outputs for the RPC method executed with the wallet. **properties** **signature** `string` An encoded string serializing the signature produced by the user's wallet. **encoding** `'hex'` The encoding format for the returned `signature`. Currently, only `'hex'` is supported for Ethereum. ### Usage ```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_signTypedData_v4", "params": { "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" } ], "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": 1, "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" } } } }' ``` **Python** Use the `sign_typed_data` method on the Ethereum wallet service. ```python theme={"system"} response = client.wallets.ethereum.sign_typed_data( wallet_id, params={ "typed_data": { "domain": {"name": "Example", "version": "1", "chainId": 1}, "types": {"Message": [{"name": "content", "type": "string"}]}, "primary_type": "Message", "message": {"content": "Hello, Privy!"}, } }, ) signature = response.signature ``` # Switch chains Source: https://docs.privy.io/wallets/using-wallets/ethereum/switch-chain Switch the active network of a connected Ethereum wallet to a different chain **React** To switch the network of a connected wallet, first [find the corresponding **`ConnectedWallet`** object](/wallets/wallets/get-a-wallet) from the **`wallets`** array. ```tsx theme={"system"} const {wallets} = useWallets(); const wallet = wallets[0]; // Replace this with your desired wallet ``` Then, call the wallet's **`switchChain`** method. As a parameter to the method, pass the chain ID for your desired network as a `number` or hexadecimal `string`: ```tsx theme={"system"} await wallet.switchChain(7777777); ``` For embedded wallets, **`switchChain`** will update the network of the embedded wallet behind the scenes. For external wallets, **`switchChain`** will prompt the user to switch to the target network within the external wallet's client (e.g. their browser extension or mobile app). **`switchChain`** returns a Promise that resolves to `void` once the wallet has successfully been switched to the target network. The Promise will reject with an error if: * The target chain has [not been configured](/basics/react/advanced/configuring-evm-networks). * The user declines the request to switch their network, if using an external wallet. **React Native** By default, embedded wallets will be connected to the first network specified in your **`supportedChains`** array, and to Ethereum mainnet if no **`supportedChains`** are specified. To switch the embedded wallet to a different network, **send a [`wallet_switchEthereumChain`](https://docs.metamask.io/wallet/reference/wallet_switchethereumchain/) JSON-RPC request to the wallet's EIP-1193 provider.** In the request's **`params`**, specify your target **`chainId`** as a hexadecimal string. ```ts theme={"system"} import {useEmbeddedEthereumWallet} from '@privy-io/expo'; const {wallets} = useEmbeddedEthereumWallet(); const wallet = wallets[0]; // Replace this with your desired wallet const provider = await wallet.getProvider(); await provider.request({ method: 'wallet_switchEthereumChain', // Replace '0x5' with the chainId of your target network params: [{chainId: '0x5'}] }); ``` You can also use the **`eth_chainId`** request (with no **`params`**) to get the current network of the embedded wallet. # Interfacing with common libraries Source: https://docs.privy.io/wallets/using-wallets/ethereum/web3-integrations > [!NOTE] > > If you are looking to integrate Privy with a Solana library instead, please see our [Solana web3 > integrations guide](/wallets/using-wallets/solana/kit-integrations). **React** > [!TIP] > > Privy is fully compatible with popular web3 libraries for interfacing wallets, including [`viem`](https://viem.sh/), [`wagmi`](https://wagmi.sh/), [`ethers`](https://docs.ethers.org/), and [`web3js`](https://web3js.readthedocs.io/en/v1.10.0/). Read below to learn how to best integrate Privy alongside these libraries. ## Viem Viem represents connected wallets as either an [**account**](https://viem.sh/docs/accounts/local.html) object, which can sign with the wallet, or a [**wallet client**](https://viem.sh/docs/clients/wallet.html) object, which can also send transactions from the wallet. ### Getting an account To get an account for a user's connected wallet, import the `toViemAccount` method and the `useWallets` hook from the React SDK. ```tsx theme={"system"} import {toViemAccount, useWallets} from '@privy-io/react-auth'; ``` Then, pass the `ConnectedWallet` object for your user's wallet to the method, which will return a `LocalAccount` instance. ```tsx theme={"system"} const {wallets} = useWallets(); const wallet = wallets.find((wallet) => (wallet.address === 'your-desired-address')); const account = await toViemAccount({wallet}); ``` You can then use the `account` to sign messages, typed data payloads, and transactions. ### Getting a wallet client 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. ## Wagmi Privy is fully compatible with **`wagmi`**. Please see our [**wagmi guide**](/wallets/connectors/ethereum/integrations/wagmi) for setting up the integration. ## Ethers ### 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(); ``` ## Web3.js Web3.js represents connected wallets as a [**Web3**](https://docs.web3js.org/guides/web3_providers_guide/#providers-types) object, which you can use to get information about the current wallet or the request signatures and transactions. To get a Web3js 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: ```ts theme={"system"} const {wallets} = useWallets(); const wallet = wallets[0]; // Replace this with your desired wallet await wallet.switchChain(sepolia.id); ``` Then, get the wallet's EIP1193 provider using the wallet's **`getEthereumProvider`** method and pass it to Web3js's **`Web3`** constructor like so: ```ts theme={"system"} const provider = await wallet.getEthereumProvider(); const web3 = new Web3(provider); ``` You can then use the [**Web3 provider**](https://docs.web3js.org/guides/web3_providers_guide/) to get information about the wallet or request signatures and transactions. **React Native** > [!TIP] > > Privy is fully compatible with popular web3 libraries for interfacing wallets, including [`viem`](https://viem.sh/), [`ethers`](https://docs.ethers.org/), and [`web3js`](https://web3js.readthedocs.io/en/v1.10.0/). > [!WARNING] > > Third-party libraries may require additional shims to be used in a React Native environment. ### Integrating with `viem` First, import the necessary methods, objects, and networks from `viem`: ```ts theme={"system"} import {createWalletClient, custom} from 'viem'; // Replace 'mainnet' with your desired network import {mainnet} from 'viem/chains'; ``` Next, get an EIP-1193 provider for the user's embedded wallet, and switch its network to your desired network: ```ts theme={"system"} const provider = await wallet.getProvider(); await provider.request({ method: 'wallet_switchEthereumChain', // Replace '0x1' with the chain ID of your desired network params: [{chainId: '0x1'}], }); ``` Lastly, initialize a viem Wallet Client from the EIP-1193 provider: ```ts theme={"system"} const walletClient = createWalletClient({ // Replace this with your desired network that you imported from viem chain: mainnet, transport: custom(provider), }); ``` You can now use methods implemented by viem's [Wallet Client](https://viem.sh/docs/clients/wallet.html), including [`signMessage`](https://viem.sh/docs/actions/wallet/signMessage.html#signmessage), [`signTypedData`](https://viem.sh/docs/actions/wallet/signTypedData.html#signtypeddata), and [`sendTransaction`](https://viem.sh/docs/actions/wallet/sendTransaction.html#sendtransaction)! ### Integrating with `ethers` First, import `ethers`: ```ts theme={"system"} import {ethers} from 'ethers'; ``` Next, get an EIP-1193 provider for the user's embedded wallet, and switch its network to your desired network: ```ts theme={"system"} await provider.request({ method: 'wallet_switchEthereumChain', // Replace '0x1' with the chain ID of your desired network params: [{chainId: '0x1'}], }); ``` Lastly, initialize an ethers provider and signer from this EIP-1193 provider: ```ts theme={"system"} const ethersProvider = new ethers.providers.Web3Provider(provider); const ethersSigner = ethersProvider.getSigner(); ``` You can then use methods implemented by ethers' [providers](https://docs.ethers.org/v5/api/providers/) and [signers](https://docs.ethers.org/v5/api/signer/), including [`signMessage`](https://docs.ethers.org/v5/api/signer/#Signer-signMessage) and [`sendTransaction`](https://docs.ethers.org/v5/api/signer/#Signer-sendTransaction). ### Integrating with `web3.js` First, import `web3`: ```ts theme={"system"} import {Web3} from 'web3'; ``` Next, get an EIP-1193 provider for the user's embedded wallet, and switch its network to your desired network: ```ts theme={"system"} await wallet.provider.request({ method: 'wallet_switchEthereumChain', // Replace '0x1' with the chain ID of your desired network params: [{chainId: '0x1'}], }); ``` Lastly, initialize an ethers provider and signer from this EIP-1193 provider: ```ts theme={"system"} const web3 = new Web3(wallet.getEthereumProvider()); ``` You can then use interfaces by web3.js for [signing messages](https://docs.web3js.org/guides/wallet/signing), [sending transactions](https://docs.web3js.org/guides/wallet/transactions), and [more](https://docs.web3js.org/guides/web3_eth/eth). **NodeJS** ### Viem [`viem`](https://viem.sh/docs/accounts/local.html) is a popular TypeScript library on EVM for executing onchain actions with wallets. Privy's wallets on EVM natively integrate with `viem`, allowing you to use the library's interfaces for signing messages, signing typed data, sending transactions, and more. To integrate with `viem`, first install version `2^` of the library as a peer dependency: ```sh theme={"system"} npm i viem@latest ``` Then, use Privy's `createViemAccount` method to initialize an instance of a viem [`Account`](https://viem.sh/docs/accounts/local) for an EVM wallet. As a parameter to this method, pass an object with the following: | Field | Type | Description | | - | - | - | | `privy` | `PrivyClient` | Instance of the Privy client for your app. | | `walletId` | `string` | ID of the wallet. | | `address` | `0x${string}` | Ethereum address of the wallet. | As an example, you can initialize an `Account` like so: ```tsx theme={"system"} import {PrivyClient} from '@privy-io/node'; import {createViemAccount} from '@privy-io/node/viem'; // Initialize your Privy client const privy = new PrivyClient(...); // Create a viem account instance for a wallet const account = await createViemAccount(privy, { walletId: 'insert-wallet-id', address: 'insert-address' }); ``` > [!TIP] > > If your wallet requires an [authorization context](/controls/authorization-keys/using-owners/sign/signing-on-the-server), > you should pass it to the `createViemAccount` method like so: > > ```tsx theme={"system"} > const serverWalletAccount = await createViemAccount(privy, { > walletId: 'insert-wallet-id', > address: 'insert-address', > authorizationContext: { > authorization_private_keys: ['your authorization private key'] > } > }); > ``` From the returned `Account`, you can then initialize a viem [`WalletClient`](https://viem.sh/docs/clients/wallet) to sign messages and execute transactions with the wallet like so: ```tsx theme={"system"} import {createWalletClient, http, parseEther} from 'viem'; import {base} from 'viem/chains'; const client = createWalletClient({ account, // `Account` instance from above chain: base, // Replace with your desired network transport: http() }); const hash = await client.sendTransaction({ to: '0x59D3eB21Dd06A211C89d1caBE252676e2F3F2218', value: parseEther('0.001') }); ``` **Rust** ### Alloy [`alloy`](https://alloy.rs/) is a comprehensive Rust library for Ethereum that provides type-safe, high-performance blockchain interactions. Privy's Rust SDK integrates seamlessly with alloy, allowing you to leverage alloy's powerful types and utilities while using Privy for wallet management and signing. Coming soon. # Smart wallets Source: https://docs.privy.io/wallets/using-wallets/evm-smart-wallets/overview Create programmable smart wallets with account abstraction for gas sponsorship and batched transactions Privy makes it easy to create **smart wallets** for your users. Smart wallets are **programmable, onchain accounts** that incorporate the features of [account abstraction](https://ethereum.org/en/roadmap/account-abstraction/). With just a few lines of code, you can create smart wallets for your users to sponsor gas payments, send batched transactions, and more. > [!TIP] > > We recommend using our native gas sponsorship offering to sponsor transactions on EVM and Solana. > [Learn more](/wallets/gas-and-asset-management/gas/overview) > [!NOTE] > > Please note that this native smart wallet integration is only available in the React and React > Native SDKs. To configure smart wallets with wallets created using server-side SDKs or APIs, > follow [this guide](/wallets/gas-and-asset-management/gas/ethereum). ![Sample enable smart wallets](https://mintcdn.com/privy-c2af3412/zeEEdrxSbmZQCA-7/images/wallets/smart-wallets.png?fit=max\&auto=format\&n=zeEEdrxSbmZQCA-7\&q=85\&s=af4dd0c0e048cb8fe71daf516ee1e85e) To set up with smart wallets, start by [enabling smart wallets in the Privy Dashboard](/wallets/using-wallets/evm-smart-wallets/setup/configuring-dashboard). This will configure your app to create smart wallets for your users controlled by Privy embedded signers. ### Native ERC-4337 support with embedded signers Under the hood, a smart wallet is an [ERC-4337](https://www.erc4337.io/)-compatible smart contract deployed onchain. This smart contract can be programmed to support features like transaction batching, gas sponsorship, delegating permissions, and more. When using a smart wallet, a user's assets are held by the smart contract itself. This smart contract is controlled by an **embedded signer** (an externally-owned account) secured by Privy's non-custodial wallet infrastructure. Privy automatically takes care of creating signers for users and generating smart contract wallets controlled by these signers. Your app can customize which ERC-4337 account *implementation* powers your users' smart wallets, between [Alchemy](https://dashboard.alchemy.com/products/transactions?utm_source=privy\&utm_medium=docs\&utm_campaign=signer-migration), Kernel (ZeroDev), Safe, Biconomy, Thirdweb, and the Coinbase Smart Wallet. > [!NOTE] > > Privy partners with the ERC-4337 account providers above to ensure a smooth experience. If you'd > like us to add support for another, please [reach out](https://privy.io/slack)! ### Gas sponsorship with paymasters With smart wallets, your app can pay for gas fees by registering a paymaster URL in the Privy Dashboard. Privy routes transaction requests to the registered paymaster, which pays gas instead of the user's wallet. This allows users to transact on-chain *instantly* –– even if they do not have a balance in their smart wallet. ### Future-proofed for the latest standards Privy works closely with the teams building the next generation of account abstraction standards on top of ERC-4337, such as permissions & session keys ([ERC-7715](https://ethereum-magicians.org/t/erc-7715-grant-permissions-from-wallets/20100)), smart wallet modules ([ERC-7579](https://erc7579.com/)), and smart account discovery ([ERC-7555](https://ethereum-magicians.org/t/erc-7555-single-sign-on-for-account-discovery/16536)) across different applications. As these standards become ratified, Privy will incorporate native support for these features. # Configure smart wallets in the dashboard Source: https://docs.privy.io/wallets/using-wallets/evm-smart-wallets/setup/configuring-dashboard Enable and configure smart wallet type (Alchemy, Kernel, Safe) in the Privy Dashboard > [!TIP] > > Enable smart wallets in the [Privy Dashboard](https://dashboard.privy.io/apps?page=smart-wallets) > before implementing this feature. ## 1. Enable smart wallets First, enable the smart wallets toggle and select a smart wallet type. Privy currently supports [Alchemy](https://www.alchemy.com/docs/wallets/smart-contracts/choosing-a-smart-account), [Kernel](https://zerodev.app/), [Biconomy](https://www.biconomy.io/smart-accounts), [Safe](https://safe.global/), [Thirdweb](https://thirdweb.com/contracts), and the [Coinbase Smart Wallet](https://github.com/coinbase/smart-wallet) as smart wallet types. ![Sample enable smart wallets](https://mintcdn.com/privy-c2af3412/zeEEdrxSbmZQCA-7/images/wallets/configure-smart-wallets.png?fit=max\&auto=format\&n=zeEEdrxSbmZQCA-7\&q=85\&s=53ae6c45633e66dafc817bea19ea6ad4) > [!NOTE] > > If you modify your smart wallet type after users have already created smart wallets, Privy will > provision the original smart wallet type for existing users to ensure they can access the accounts > they already use. ## 2. Configure the supported networks Next, configure the networks for your smart wallets. You should do this for *any* network that your app plans to use smart wallets on. ![Sample enable smart wallets](https://mintcdn.com/privy-c2af3412/zeEEdrxSbmZQCA-7/images/wallets/configure-smart-wallets-configure-chain.png?fit=max\&auto=format\&n=zeEEdrxSbmZQCA-7\&q=85\&s=af3c0c9712235a26d2afa9fa3ee7c2d1) For each configured network, you can optionally provide a bundler URL and/or a paymaster URL. #### Bundler The **bundler URL** specifies the node you want to use bundle operations from multiple users into a single transaction. If a bundler URL is not set for a network, Privy defaults to Pimlico's public bundler (`https://public.pimlico.io/v2/{chainId}/rpc`). > [!TIP] > > **We strongly recommend setting your own bundler URL when taking smart wallets to production**, to > give you more control over bundler rate limits. Privy's default bundler is heavily rate limited > and is not suitable for production usage. #### Paymaster The **paymaster URL** specifies the paymaster used to sponsor gas fees for the smart wallets on the network. If a paymaster URL is set, Privy will use that paymaster to sponsor gas fees for your users' transactions. If a paymaster URL is not set, your users' smart wallets must have a balance of the network's native currency to pay for gas fees for transactions. For additional security, we strongly recommend setting **Allowed domains** for your paymaster and/or bundler through your provider's dashboard, to restrict usage of these URLs to only your website. #### Recommended providers If you are looking to set up a paymaster or bundler for your app, we suggest the following providers: | Provider | Get started | | - | - | | Pimlico | [https://dashboard.pimlico.io](https://dashboard.pimlico.io) | | ZeroDev | [https://dashboard.zerodev.app/](https://dashboard.zerodev.app/) | | Alchemy | [https://www.alchemy.com/docs/wallets/smart-contracts/choosing-a-smart-account](https://www.alchemy.com/docs/wallets/smart-contracts/choosing-a-smart-account) | | Biconomy | [https://dashboard.biconomy.io/](https://dashboard.biconomy.io/) | | Thirdweb | [https://thirdweb.com](https://thirdweb.com) | | Coinbase | [https://www.coinbase.com/developer-platform/products/paymaster](https://www.coinbase.com/developer-platform/products/paymaster) | > [!TIP] > > If using **Alchemy** for your paymaster, please provide your Alchemy gas policy ID. Each gas > policy ID is tied to a specific chain and Alchemy project. > [!TIP] > > If using **Biconomy** as a paymaster with `@privy-io/react-auth`, you can override our default > paymaster context in the `SmartAccountsProvider`. View more in [our setup > notes](/wallets/using-wallets/evm-smart-wallets/usage). #### Custom chains If you do not see the chain you wish to configure on the drop down, you can configure a custom chain. Before configuring a custom chain on the Privy dashboard, please ensure the chain is supported by your smart wallet provider. Custom chain configuration requires an EIP155 chain ID, chain name, paymaster URL, bundler URL, and RPC URL; none of these values can be defaulted as they are in supported chains. # Configure smart wallets in the SDK Source: https://docs.privy.io/wallets/using-wallets/evm-smart-wallets/setup/configuring-sdk Configure smart wallets in your application SDK after Dashboard setup Once you have [configured](/wallets/using-wallets/evm-smart-wallets/setup/configuring-dashboard) smart wallets in the Privy Dashboard, you can use them in your application with just a few lines of code. **React** > [!NOTE] > > Looking to get started quickly? Check out our [Smart Wallets starter > repo](https://github.com/privy-io/examples/tree/main/examples/privy-next-smart-wallets). You can > see a deployed version of the starter app [here](https://smart-wallets-starter.privy.io/). ## Setup First install the necessary peer dependencies: ```bash theme={"system"} npm install permissionless viem ``` To set up your app with smart wallets, first import the `SmartWalletsProvider` component from `@privy-io/react-auth/smart-wallets` and wrap your app with it. The `SmartWalletsProvider` must wrap any component or page that will use smart wallets. We recommend rendering it as close to the root of your application as possible, nested *within* your `PrivyProvider`. ```tsx theme={"system"} import {PrivyProvider} from '@privy-io/react-auth'; import {SmartWalletsProvider} from '@privy-io/react-auth/smart-wallets'; export default function Providers({children}: {children: React.ReactNode}) { return ( {children} ); } ``` > [!TIP] > > Make sure that the networks you've configured for smart wallets in the Dashboard are also > configured for your app's [`defaultChain` and > `supportedChains`](/wallets/using-wallets/evm-smart-wallets/setup/configuring-dashboard). ## Creating smart wallets Once the `SmartWalletsProvider` component is rendered and a smart wallet configuration has been set up for your app in the [Dashboard](/wallets/using-wallets/evm-smart-wallets/setup/configuring-dashboard), Privy will automatically generate smart wallets for your users once they have an embedded wallet. The embedded wallet is used as the primary **signer** controlling the smart wallet. You can configure your app to [create embedded wallets](/wallets/wallets/create/create-a-wallet) automatically on login or manually; smart wallets will be created following the same configuration. ## Deploying smart wallets By default, smart wallets are **lazily deployed**, meaning the smart contract is deployed onchain when the user sends their first transaction. This is the recommended approach as it optimizes costs—you only pay deployment fees when the wallet is actually used. However, if you need the smart wallet to be deployed immediately after creation (for example, to enable certain integrations or to ensure the wallet address is active onchain), you can trigger deployment by sending a "dummy transaction" right after the wallet is created: ```tsx theme={"system"} import {useSmartWallets} from '@privy-io/react-auth/smart-wallets'; import {useWallets} from '@privy-io/react-auth'; function DeploySmartWallet() { const {client} = useSmartWallets(); const {wallets} = useWallets(); const deployWallet = async () => { if (!client) return; // Send a minimal transaction to trigger deployment // This sends 0 ETH to the wallet's own address const hash = await client.sendTransaction({ to: client.account.address, value: 0n, data: '0x' }); console.log('Smart wallet deployed with transaction:', hash); }; return ; } ``` > [!NOTE] > > Keep in mind that immediate deployment incurs gas costs upfront. Only use this approach if your > use case specifically requires the smart contract to be deployed before the user's first actual > transaction. ## Overriding paymaster context Certain paymasters, like Alchemy and Biconomy, use an additional `paymasterContext` for gas sponsorship. Privy constructs this paymaster context based on either dashboard provided gas policy ID for Alchemy or a default set of values for Biconomy. However, you can override these defaults by passing a `paymasterContext` prop to the `SmartWalletsProvider`. See an example of how to set this below: ```tsx theme={"system"} {children} ``` **React Native** ## Setup ### 0. Set up your build configuration (for old React native and Expo Versions) > [!WARNING] > > If you are running an Expo version before SDK 53 (or a React Native version before 0.79.0) then **please make sure to follow this step.** > > Otherwise, skip to step #1 ("Install peer dependencies"). Ensure you've followed the steps in [custom build configuration](/basics/react-native/installation#metro-build-configuration). Additionally, add `permissionless` to the list of modules that require package exports in your `metro.config.js` file. ```js theme={"system"} //...other config logic // Enable package exports for select libraries ... const resolveRequestWithPackageExports = (context, moduleName, platform) => { if (moduleName.startsWith('@privy-io/') || moduleName.startsWith('permissionless')) { const ctx = { ...context, unstable_enablePackageExports: true, }; return ctx.resolveRequest(ctx, moduleName, platform); } if ( moduleName.endsWith(".js") && context.originModulePath.includes("node_modules/ox/") ) { const newModuleName = moduleName.replace(/\.js$/, ""); return context.resolveRequest(context, newModuleName, platform); } return context.resolveRequest(context, moduleName, platform); }; ``` ### 1. Install peer dependencies ```sh theme={"system"} npx expo install viem permissionless ``` ### 2. Import and wrap your app with the `SmartWalletsProvider` The `SmartWalletsProvider` must wrap any component or page that will use smart wallets. We recommend rendering it as close to the root of your application as possible, nested *within* your `PrivyProvider`. ```tsx theme={"system"} import {PrivyProvider} from '@privy-io/expo'; import {SmartWalletsProvider} from '@privy-io/expo/smart-wallets'; export default function Providers({children}: {children: React.ReactNode}) { return ( {children} ); } ``` ## Creating smart wallets Once the `SmartWalletsProvider` component is rendered and a smart wallet configuration has been set up for your app in the [Dashboard](/wallets/using-wallets/evm-smart-wallets/setup/configuring-dashboard), Privy will automatically generate smart wallets for your users once they have an embedded wallet. The embedded wallet is used as the primary **signer** controlling the smart wallet. You can configure your app to [create embedded wallets](/wallets/wallets/create/create-a-wallet) manually; smart wallets will be created following the same configuration. ## Deploying smart wallets By default, smart wallets are **lazily deployed**, meaning the smart contract is deployed onchain when the user sends their first transaction. This is the recommended approach as it optimizes costs—you only pay deployment fees when the wallet is actually used. However, if you need the smart wallet to be deployed immediately after creation (for example, to enable certain integrations or to ensure the wallet address is active onchain), you can trigger deployment by sending a "dummy transaction" right after the wallet is created: ```tsx theme={"system"} import {useSmartWallets} from '@privy-io/expo/smart-wallets'; import {useWallets} from '@privy-io/expo'; function DeploySmartWallet() { const {client} = useSmartWallets(); const {wallets} = useWallets(); const deployWallet = async () => { if (!client) return; // Send a minimal transaction to trigger deployment // This sends 0 ETH to the wallet's own address const hash = await client.sendTransaction({ to: client.account.address, value: 0n, data: '0x' }); console.log('Smart wallet deployed with transaction:', hash); }; return ; } ``` > [!NOTE] > > Keep in mind that immediate deployment incurs gas costs upfront. Only use this approach if your > use case specifically requires the smart contract to be deployed before the user's first actual > transaction. # Using smart wallets Source: https://docs.privy.io/wallets/using-wallets/evm-smart-wallets/usage Use smart wallets for sponsored transactions, batched operations, and UserOperation flows **React** > [!TIP] > > Follow the [smart wallets setup guide](/wallets/using-wallets/evm-smart-wallets/setup/configuring-dashboard) to configure smart wallets for your application. ## Get the smart wallet address Once a smart wallet has been created for a user, you can get the address for the smart wallet by finding the account of `type: 'smart_wallet'` from the user's `linkedAccounts` array. ```jsx theme={"system"} const {user} = usePrivy(); const smartWallet = user.linkedAccounts.find((account) => account.type === 'smart_wallet'); console.log(smartWallet.address); // Logs the smart wallet's address console.log(smartWallet.type); // Logs the smart wallet type (e.g. 'safe', 'kernel', 'light_account', 'biconomy', 'thirdweb', 'coinbase_smart_wallet') ``` ## Sign a message Use the `signMessage` function from the `client` returned by `useSmartWallets` hook in your React component to sign a message using the user's smart wallet. ```jsx theme={"system"} signMessage: (input: {message: SignableMessage}, opts?: {uiOptions?: SignMessageModalUIOptions}) => Promise ``` ### Usage ```jsx theme={"system"} import {useSmartWallets} from '@privy-io/react-auth/smart-wallets'; const {client} = useSmartWallets(); const uiOptions = { title: 'Sample title text', description: 'Sample description text', buttonText: 'Sample button text' }; client.signMessage({message: 'Hello, world!'}, {uiOptions}).then((signature) => { console.log(signature); }); ``` ### Parameters The `signMessage` method accepts the following parameters: **input.message** `string | \{raw: Hex | ByteArray\}` *required* The message to sign by the smart account. **opts.uiOptions** `SignMessageModalUIOptions` Optional UI customization options for the signature prompt. ### Returns **signature** `Hex` The signed message by the smart wallet. ## Sign typed data Use the `signTypedData` function from the `client` returned by `useSmartWallets` hook in your React component to sign structured data using the user's smart wallet. ```jsx theme={"system"} signTypedData: (input: SignTypedDataParameters, opts?: {uiOptions?: SignMessageModalUIOptions}) => Promise ``` ### Usage ```jsx theme={"system"} import {useSmartWallets} from '@privy-io/react-auth/smart-wallets'; const {client} = useSmartWallets(); const uiOptions = { title: 'Sample title text', description: 'Sample description text', buttonText: 'Sample button text' }; client.signTypedData(typedDataRequestParams, {uiOptions}).then((signature) => { console.log(signature); }); ``` ### Parameters The `signTypedData` method accepts the following parameters: **input** `SignTypedDataParameters` *required* The typed data to sign by the smart account. **opts.uiOptions** `SignMessageModalUIOptions` Optional UI customization options for the signature prompt. ### Returns **signature** `Hex` The signed message by the smart wallet. ## Send a transaction Use the `sendTransaction` function from the `client` returned by `useSmartWallets` hook in your React component to send a transaction using the user's smart wallet. ```jsx theme={"system"} sendTransaction: (input: SendTransactionParameters, opts?: {uiOptions?: SendTransactionModalUIOptions}) => Promise ``` ### Usage ```jsx theme={"system"} import {useSmartWallets} from '@privy-io/react-auth/smart-wallets'; const {client} = useSmartWallets(); const uiOptions = { title: 'Sample title text', description: 'Sample description text', buttonText: 'Sample button text' }; client.sendTransaction({ chain: base, to: 'insert-recipient-address', value: 0.1 }, {uiOptions}).then((txHash) => { console.log(txHash); }); ``` ### Parameters The `sendTransaction` method accepts the following parameters: **input** `SendTransactionParameters` *required* The transaction to send by the smart account. **opts.uiOptions** `SendTransactionModalUIOptions` Optional UI customization options for the transaction prompt. ### Returns **txHash** `Hex` The transaction hash of the sent transaction. ## Batch transactions Smart wallets support sending a batch of transactions in a single, atomic submission to the network. ```jsx theme={"system"} sendTransaction: (input: {calls: Array<{to: string, value?: bigint, data?: string}>}, opts?: {uiOptions?: SendTransactionModalUIOptions}) => Promise ``` ### Usage ```jsx theme={"system"} import {useSmartWallets} from '@privy-io/react-auth/smart-wallets'; const {client} = useSmartWallets(); client.sendTransaction({ calls: [ // Approve transaction { to: USDC_ADDRESS, data: encodeFunctionData({ abi: USDC_ABI, functionName: 'approve', args: ['insert-spender-address', BigInt(1e6)] }) }, // Transfer transaction { to: USDC_ADDRESS, data: encodeFunctionData({ abi: USDC_ABI, functionName: 'transfer', args: ['insert-recipient-address', BigInt(1e6)] }) } ] }).then((txHash) => { console.log(txHash); }); ``` ### Parameters The `sendTransaction` method for batching accepts the following parameters: **input.calls** `Array\<\{to: string, value?: bigint, data?: string\}\>` *required* Array of transactions to batch together. **opts.uiOptions** `SendTransactionModalUIOptions` Optional UI customization options for the transaction prompt. ### Returns **txHash** `Hex` The transaction hash of the batched transaction. ## Switch chains Use the `getClientForChain` method to create a new smart wallet client for a specific chain. ```jsx theme={"system"} getClientForChain: ({id: number}) => Promise ``` ### Usage ```jsx theme={"system"} import {base} from 'viem/chains'; const {getClientForChain} = useSmartWallets(); const baseClient = await getClientForChain({ id: base.id, }); // Client will send transaction on Base baseClient.sendTransaction({ ... }); ``` ### Parameters The `getClientForChain` method accepts the following parameters: **id** `number` *required* The chain ID to create a client for. ### Returns **client** `SmartWalletClient` A new smart wallet client configured for the specified chain. > [!TIP] > > If configured `defaultChain` does not have a smart wallet network configuration, the smart wallet client will default to using the first configured chain that has a smart wallet network configuration. **React Native** > [!TIP] > > Follow the [React Native setup guide](/wallets/using-wallets/evm-smart-wallets/setup/configuring-dashboard) to configure smart wallets for your React Native application. ## Get the smart wallet address Once a smart wallet has been created for a user, you can get the address for the smart wallet by finding the account of `type: 'smart_wallet'` from the user's `linked_accounts` array. ```jsx theme={"system"} const {user} = usePrivy(); const smartWallet = user.linked_accounts.find((account) => account.type === 'smart_wallet'); console.log(smartWallet.address); // Logs the smart wallet's address console.log(smartWallet.type); // Logs the smart wallet type (e.g. 'safe', 'kernel', 'light_account', 'biconomy', 'thirdweb', 'coinbase_smart_wallet') ``` ## Sign a message Use the `signMessage` function from the `client` returned by `useSmartWallets` hook in your React Native component to sign a message using the user's smart wallet. ```jsx theme={"system"} signMessage: ({message: SignableMessage}) => Promise ``` ### Usage ```jsx theme={"system"} import {useSmartWallets} from '@privy-io/expo/smart-wallets'; const {client} = useSmartWallets(); client.signMessage({message: 'Hello, world!'}).then((signature) => { console.log(signature); }); ``` ### Parameters The `signMessage` method accepts the following parameters: **input.message** `string | \{raw: Hex | ByteArray\}` *required* The message to sign by the smart account. ### Returns **signature** `Hex` The signed message by the smart wallet. ## Sign typed data Use the `signTypedData` function from the `client` returned by `useSmartWallets` hook in your React Native component to sign structured data using the user's smart wallet. ```jsx theme={"system"} signTypedData: (input: SignTypedDataParameters) => Promise ``` ### Usage ```jsx theme={"system"} import {useSmartWallets} from '@privy-io/expo/smart-wallets'; const {client} = useSmartWallets(); client.signTypedData(...).then((signature) => { console.log(signature); }); ``` ### Parameters The `signTypedData` method accepts the following parameters: **input** `SignTypedDataParameters` *required* The typed data to sign by the smart account. ### Returns **signature** `Hex` The signed message by the smart wallet. ## Send a transaction Use the `sendTransaction` function from the `client` returned by `useSmartWallets` hook in your React Native component to send a transaction using the user's smart wallet. ```jsx theme={"system"} sendTransaction: (input: SendTransactionParameters) => Promise ``` ### Usage ```jsx theme={"system"} import {useSmartWallets} from '@privy-io/expo/smart-wallets'; const {client} = useSmartWallets(); client.sendTransaction({ account: client.account, chain: base, to: 'insert-recipient-address', value: 0.1 }).then((txHash) => { console.log(txHash); }); ``` ### Parameters The `sendTransaction` method accepts the following parameters: **input** `SendTransactionParameters` *required* The transaction to send by the smart account. ### Returns **txHash** `Hex` The transaction hash of the sent transaction. ## Batch transactions Smart wallets support sending a batch of transactions in a single, atomic submission to the network. ```jsx theme={"system"} sendTransaction: (input: {calls: Array<{to: string, value?: bigint, data?: string}>}) => Promise ``` ### Usage ```jsx theme={"system"} import {useSmartWallets} from '@privy-io/expo/smart-wallets'; const {client} = useSmartWallets(); client.sendTransaction({ account: client.account, calls: [ // Approve transaction { to: USDC_ADDRESS, data: encodeFunctionData({ abi: USDC_ABI, functionName: 'approve', args: ['insert-spender-address', BigInt(1e6)] }) }, // Transfer transaction { to: USDC_ADDRESS, data: encodeFunctionData({ abi: USDC_ABI, functionName: 'transfer', args: ['insert-recipient-address', BigInt(1e6)] }) } ] }).then((txHash) => { console.log(txHash); }); ``` ### Parameters The `sendTransaction` method for batching accepts the following parameters: **input.calls** `Array\<\{to: string, value?: bigint, data?: string\}\>` *required* Array of transactions to batch together. ### Returns **txHash** `Hex` The transaction hash of the batched transaction. ## Switch chains Use the `getClientForChain` method to create a new smart wallet client for a specific chain. ```jsx theme={"system"} getClientForChain: ({chainId: number}) => Promise ``` ### Usage ```jsx theme={"system"} import {base} from 'viem/chains'; const {getClientForChain} = useSmartWallets(); const baseClient = await getClientForChain({ chainId: base.id, }); // Client will send transaction on Base baseClient.sendTransaction({ account: baseClient.account, ... }); ``` ### Parameters The `getClientForChain` method accepts the following parameters: **chainId** `number` *required* The chain ID to create a client for. ### Returns **client** `SmartWalletClient` A new smart wallet client configured for the specified chain. > [!TIP] > > The smart wallet client will default to using the first configured chain that has a smart wallet network configuration. # Raw signing Source: https://docs.privy.io/wallets/using-wallets/other-chains/index Integrate chains with Privy raw sign functionality over transaction hashes For chains integrated through [raw signing](/wallets/overview/chains), your app can invoke Privy's raw sign functionality to sign over a hash, e.g. the hash of a transaction. Your app can then use the returned signature to submit and send the transaction. Alternatively, you can provide the `bytes` to hash with a specific `encoding` and a `hash_function`, and get the same response. For examples of how to use raw sign for a specific chain, see the [Tier 1 integration guide](/recipes/tier-1-wallet-integration). > [!NOTE] > > For Tron transactions, use the structured > [`tron_signTransaction`](/wallets/using-wallets/tron/sign-a-transaction) or > [`tron_sendTransaction`](/wallets/using-wallets/tron/send-a-transaction) methods. Use raw signing > only when your client-side or low-level flow requires it. **REST API** Sign a raw hash or `bytes` (with `encoding` and `hash_function`) along the blockchain’s cryptographic curve using the wallet’s private key. Make a `POST` request to ```bash theme={"system"} https://api.privy.io/v1/wallets/{wallet_id}/raw_sign ``` ## Usage ```bash theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/raw_sign \ --header 'Authorization: Basic Y20xNWh4eDUyMDVlNWx2NHVkdmE3enBqejoybWhVOWhhVTFQYjhYNXV1cVdxQVRIdW5xTWIyUlBKRm5GSHRzWDVzbXNkdUJRUDZtTW05YldmalBwS3hocjJQZHNHY0Q5NkFUeDc5em03WWhicUZLWkM4' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "params": { "hash": "0x0775aeed9c9ce6e0fbc4db25c5e4e6368029651c905c286f813126a09025a21e" } }' ``` A successful response will look like: ```json theme={"system"} { "data": { "signature": "0x0775aeed9c9ce6e0fbc4db25c5e4e6368029651c905c286f813126a09025a21e", "encoding": "hex" } } ``` Alternatively, you can provide bytes to hash with a specific encoding and hash function, and gets a similar response. ```bash theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/raw_sign \ --header 'Authorization: Basic Y20xNWh4eDUyMDVlNWx2NHVkdmE3enBqejoybWhVOWhhVTFQYjhYNXV1cVdxQVRIdW5xTWIyUlBKRm5GSHRzWDVzbXNkdUJRUDZtTW05YldmalBwS3hocjJQZHNHY0Q5NkFUeDc5em03WWhicUZLWkM4' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --data '{ "params": { "bytes": "0a0234ea220809701d7a17a77e04408093e981a6335a66080112620a2d747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e5472616e73666572436f6e747261637412310a15417009bf59e27d2031a23a61e1590289fc3d21b3cd121541132b98ed6fb80a2d45f177cdef091ae2d9dc115418e80770a0bee581a633", "encoding": "hex", "hash_function": "sha256" } }' ``` ## Authorization **Authorization** `string` *required* App secret authentication. ## Headers **privy-app-id** `string` *required* ID of your Privy app. **privy-authorization-signature** `string` Request authorization signature. If multiple signatures are required, they should be comma separated. ## Path **wallet\_id** `string` *required* ID of the wallet. ## Body **params** `object` *required* **child attributes** **Option 1: Sign a pre-computed hash** **hash** `string` The hex-encoded hash to sign, prefixed with `0x`. Use this option when you have already computed the hash. **Option 2: Hash bytes before signing** **bytes** `string` The bytes to hash and sign. Use this option to provide raw data that will be decoded using the specified `encoding` then hashed using the specified `hash_function`. **encoding** `string` The encoding scheme for the bytes. Required when using `bytes`. Available options: `utf-8`, `hex` **hash\_function** `string` The hash function to hash the bytes. Required when using `bytes`. Available options: `keccak256`, `sha256` > [!NOTE] > > You must provide either `hash` OR the combination of `bytes`, `encoding`, and `hash_function`. These parameter sets are mutually exclusive. ## Response **data** `object` *required* **child attributes** **signature** `string` The signature, prefixed with `0x`. **encoding** `string` The encoding of the signature. **error** `object` **child attributes** **code** `string` The error code. **message** `string` The error message. **NodeJS** Use the `rawSign` method on the `wallets()` interface to sign a hash or `bytes` (with `encoding` and `hash_function`) with a wallet. ### Usage ```tsx theme={"system"} const {signature, encoding} = await privy.wallets().rawSign('insert-wallet-id', { params: { hash: '0x0775aeed9c9ce6e0fbc4db25c5e4e6368029651c905c286f813126a09025a21e' }, }); ``` Alternatively, you can provide bytes to hash with a specific encoding and hash function, and gets a similar response. ```tsx theme={"system"} const {signature, encoding} = await privy.wallets().rawSign('insert-wallet-id', { params: { bytes: "0a0234ea220809701d7a17a77e04408093e981a6335a66080112620a2d747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e5472616e73666572436f6e747261637412310a15417009bf59e27d2031a23a61e1590289fc3d21b3cd121541132b98ed6fb80a2d45f177cdef091ae2d9dc115418e80770a0bee581a633", encoding: "hex", hash_function: "sha256" }, }); ``` ### Parameters and Returns Check out the [API reference](/api-reference/wallets/raw-sign) for more details. **Java** To sign a raw hash or `bytes` (with `encoding` and `hash_function`) from your wallet, use the `rawSign` method. It will sign your hash, and return the signature to you. ### Usage ```java theme={"system"} try { RawSignInput request = RawSignInput.builder() .params( RawSignInputParams.of(RawSignHashParams.builder() .hash("0x0775aeed9c9ce6e0fbc4db25c5e4e6368029651c905c286f813126a09025a21e") .build()) ) .build(); // Alternatively, you can provide bytes to hash with a specific encoding and hash function, and gets a similar response. /* RawSignInput request = RawSignInput.builder() .params( RawSignInputParams.of(RawSignBytesParams.builder() .bytes("0a0234ea220809701d7a17a77e04408093e981a6335a66080112620a2d747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e5472616e73666572436f6e747261637412310a15417009bf59e27d2031a23a61e1590289fc3d21b3cd121541132b98ed6fb80a2d45f177cdef091ae2d9dc115418e80770a0bee581a633") .encoding("hex") .hashFunction("sha256") .build()) ) .build(); */ // Example: If wallet's owner is an authorization private key AuthorizationContext authorizationContext = AuthorizationContext.builder() .addAuthorizationPrivateKey("authorization-key") .build(); WalletRawSignResponse response = privyClient .wallets() .rawSign( walletId, request, authorizationContext ); if (response.rawSignResponse().isPresent()) { RawSignResponse rawSignResponse = response.rawSignResponse().get(); if (rawSignResponse.data().isPresent()) { RawSignResponseData data = rawSignResponse.data().get(); String signature = data.signature(); } } } catch (APIException e) { String errorBody = e.bodyAsString().orElse(e.getMessage()); System.err.println(errorBody); } catch (Exception e) { System.err.println(e.getMessage()); } ``` ### Parameters **request** `RawSignInput` *required* **params** `RawSignInputParams` *required* **Option 1: Sign a pre-computed hash** **hash** `String` The hex-encoded hash to sign, prefixed with `0x`. Use this option when you have already computed the hash. **Option 2: Hash bytes before signing** **bytes** `String` The bytes to hash and sign. Use this option to provide raw data that will be decoded using the specified `encoding` then hashed using the specified `hash_function`. **encoding** `String` The encoding scheme for the bytes. Required when using `bytes`. Available options: `utf-8`, `hex` **hashFunction** `String` The hash function to hash the bytes. Required when using `bytes`. Available options: `keccak256`, `sha256` > [!NOTE] > > You must provide either `hash` OR the combination of `bytes`, `encoding`, and `hash_function`. These parameter sets are mutually exclusive. ### Returns The `WalletRawSignResponse` object contains a `rawSignResponse()` field **rawSignResponse()** `Optional\` **data()** `Optional\` **signature()** `String` The signature produced by the wallet, prefixed with `0x`. **Python** Use the `raw_sign` method on the wallets service. ```python theme={"system"} response = client.wallets.raw_sign( "wallet-id", wallet_raw_sign_params={ "params": { "hash": "0x6503b027a625549f7be691646404f275f149d17a119a6804b855bac3030037aa" } }, ) signature = response.data.signature ``` **Go** Use the `RawSign` method on the `Wallets` service to sign a raw payload with a wallet on any supported chain. ### Usage ```go theme={"system"} response, err := client.Wallets.RawSign( context.Background(), "wallet-id", privy.WalletRawSignParams{ RawSignInput: privy.RawSignInput{ Params: privy.RawSignInputParamsUnion{ OfRawSignHashs: &privy.RawSignHashParams{ Hash: "0x6503b027a625549f7be691646404f275f149d17a119a6804b855bac3030037aa", }, }, }, }, ) if err != nil { log.Fatalf("failed to sign: %v", err) } fmt.Println("Signature:", response.Data.Signature) ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/raw-sign) for more details. **Ruby** Use the `raw_sign` method on the `wallets` service to sign a raw payload with a wallet on any supported chain. ### Usage ```ruby theme={"system"} response = client.wallets.raw_sign( "wallet-id", raw_sign_input: { params: { hash: "0x6503b027a625549f7be691646404f275f149d17a119a6804b855bac3030037aa" } }, authorization_context: ctx ) puts(response.data.signature) ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/raw-sign) for more details. **React** Sign a raw hash or `bytes` (with `encoding` and `hash_function`) along the blockchain's cryptographic curve using the wallet's private key. ```typescript theme={"system"} signRawHash: ({address: string, chainType: CurveSigningChainType, hash: HexString}) => Promise<{signature: HexString}>; ``` ### Usage ```typescript theme={"system"} import {useSignRawHash} from '@privy-io/react-auth/extended-chains'; const {signRawHash} = useSignRawHash(); const {signature} = await signRawHash({ address: 'insert-wallet-address', chainType: 'cosmos', // or 'stellar', 'sui', etc. hash: '0x1acab030f479bda7829de07e9db4138cec5d38574df17d65af1617b7268541c0' }); // Alternatively, you can provide bytes to hash with a specific encoding and hash function, and gets a similar response. /* const {signature} = await signRawHash({ address: 'insert-wallet-address', chainType: 'cosmos', // or 'stellar', 'sui', etc. bytes: '0a0234ea220809701d7a17a77e04408093e981a6335a66080112620a2d747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e5472616e73666572436f6e747261637412310a15417009bf59e27d2031a23a61e1590289fc3d21b3cd121541132b98ed6fb80a2d45f177cdef091ae2d9dc115418e80770a0bee581a633', encoding: 'hex', hash_function: 'sha256' }); */ console.log(signature); // 0x4d349c0c04abec47f2af0929b6dd5abcef2f29cb90b596772af928b3ef6e34316108eab1c47fac6d1d5ec51da59bd9124a80c4d353e130a8e675a3ad952e4c46 ``` ### Parameters **opts.address** `string` *required* The address of the wallet to use for signing the raw hash. **opts.chainType** `CurveSigningChainType` *required* The chain type of the wallet to use for signing the raw hash. **Option 1: Sign a pre-computed hash** **opts.hash** `HexString` The raw hash to sign over, hex-encoded and prefixed with `0x`. Use this option when you have already computed the hash. **Option 2: Hash bytes before signing** **opts.bytes** `string` The bytes to hash and sign. Use this option to provide raw data that will be decoded using the specified `encoding` then hashed using the specified `hash_function`. **opts.encoding** `'utf-8' | 'hex'` The encoding scheme for the bytes. Required when using `bytes`. **opts.hashFunction** `'keccak256' | 'sha256'` The hash function to hash the bytes. Required when using `bytes`. > [!NOTE] > > You must provide either `hash` OR the combination of `bytes`, `encoding`, and `hash_function`. These parameter sets are mutually exclusive. ### Returns **child properties** **signature** `HexString` The signature produced by the wallet. Hex-encoded and prefixed with `0x`. **React Native** Sign a raw hash or `bytes` (with `encoding` and `hash_function`) along the blockchain's cryptographic curve using the wallet's private key. ```typescript theme={"system"} signRawHash: ({address: string, chainType: CurveSigningChainType, hash: HexString}) => Promise<{signature: HexString}>; ``` ### Usage ```typescript theme={"system"} import {useSignRawHash} from '@privy-io/expo/extended-chains'; const {signRawHash} = useSignRawHash(); const {signature} = await signRawHash({ address: 'insert-wallet-address', chainType: 'cosmos', // or 'stellar', 'sui', etc. hash: '0x1acab030f479bda7829de07e9db4138cec5d38574df17d65af1617b7268541c0' }); // Alternatively, you can provide bytes to hash with a specific encoding and hash function, and gets a similar response. /* const {signature} = await signRawHash({ address: 'insert-wallet-address', chainType: 'cosmos', // or 'stellar', 'sui', etc. bytes: '0a0234ea220809701d7a17a77e04408093e981a6335a66080112620a2d747970652e676f6f676c65617069732e636f6d2f70726f746f636f6c2e5472616e73666572436f6e747261637412310a15417009bf59e27d2031a23a61e1590289fc3d21b3cd121541132b98ed6fb80a2d45f177cdef091ae2d9dc115418e80770a0bee581a633', encoding: 'hex', hash_function: 'sha256' }); */ console.log(signature); // 0x4d349c0c04abec47f2af0929b6dd5abcef2f29cb90b596772af928b3ef6e34316108eab1c47fac6d1d5ec51da59bd9124a80c4d353e130a8e675a3ad952e4c46 ``` ### Parameters **opts.address** `string` *required* The address of the wallet to use for signing the raw hash. **opts.chainType** `CurveSigningChainType` *required* The chain type of the wallet to use for signing the raw hash. **Option 1: Sign a pre-computed hash** **opts.hash** `HexString` The raw hash to sign over, hex-encoded and prefixed with `0x`. Use this option when you have already computed the hash. **Option 2: Hash bytes before signing** **opts.bytes** `string` The bytes to hash and sign. Use this option to provide raw data that will be decoded using the specified `encoding` then hashed using the specified `hash_function`. **opts.encoding** `'utf-8' | 'hex'` The encoding scheme for the bytes. Required when using `bytes`. **opts.hashFunction** `'keccak256' | 'sha256'` The hash function to hash the bytes. Required when using `bytes`. > [!NOTE] > > You must provide either `hash` OR the combination of `bytes`, `encoding`, and `hash_function`. These parameter sets are mutually exclusive. ### Returns **child properties** **signature** `HexString` The signature produced by the wallet. Hex-encoded and prefixed with `0x`. # Sui Source: https://docs.privy.io/wallets/using-wallets/other-chains/sui Create Sui wallets, sign transactions and messages, and enforce policies with Privy With Privy, your app can create embedded Sui wallets, sign transactions and messages, and enforce Privy's [policy engine](/controls/policies/overview) to constrain what each wallet can do. Sui uses the [raw\_sign](/wallets/using-wallets/other-chains) endpoint for signing, and its policies can inspect individual transaction commands and transfers. Sui wallets use the Ed25519 curve and the EdDSA signing algorithm. Your app signs Sui transaction commands and transfers. Then, submit the signed transaction to the network using the Sui SDK. ## Features Privy's Sui support includes everything your app needs to onboard users and move assets on Sui: * **Embedded Sui wallets**: create non-custodial Sui wallets for users with the `sui` chain type. * **Transaction signing**: sign the full Sui [intent message](https://docs.sui.io/develop/transactions/transaction-auth/intent-signing) for any transaction, then submit it with the Sui SDK. * **Message signing**: sign arbitrary messages for authentication or offchain verification. * **Policy enforcement**: constrain transactions and message signing with Privy's policy engine. * **Server and client SDKs**: manage Sui wallets from your backend with Privy's [server SDKs](/basics/get-started/platforms) or directly from your app. ## Sign a transaction Sign the full [intent message](https://docs.sui.io/develop/transactions/transaction-auth/intent-signing) for a transaction with Privy's raw\_sign endpoint, then verify or submit it with the Sui SDK. ```typescript theme={"system"} import {PrivyClient} from '@privy-io/node'; import {messageWithIntent} from '@mysten/sui/cryptography'; import {Transaction} from '@mysten/sui/transactions'; const privy = new PrivyClient({appId: 'insert-your-app-id', appSecret: 'insert-your-app-secret'}); const walletId = 'insert-wallet-id'; // Build the transaction, then wrap its BCS bytes in a Sui intent message const tx = new Transaction(); // Add transaction commands here. const txBytes = await tx.build(); const intentMessage = messageWithIntent('TransactionData', txBytes); const bytes = Buffer.from(intentMessage).toString('hex'); // Sign the intent message with Privy raw sign. Sui uses `blake2b256` and `hex` or `base64`. const {signature} = await privy.wallets().rawSign(walletId, { params: {bytes, encoding: 'hex', hash_function: 'blake2b256'} }); // Submit the bytes and signature using the Sui SDK ``` ### Python Build the Sui intent message with a Sui Python library, then sign its bytes with Privy: ```python theme={"system"} response = client.wallets.raw_sign( "insert-wallet-id", wallet_raw_sign_params={ "params": { "bytes": intent_message.hex(), "encoding": "hex", "hash_function": "blake2b256", } }, ) signature = response.data.signature ``` ## Enforce policies Sui wallets support Privy's policy engine, so your app can constrain both transactions and message signing: * **Transaction commands**: allowlist the supported commands — `TransferObjects`, `SplitCoins`, and `MergeCoins` — using the `sui_transaction_command` field source. * **Transfer limits**: cap amounts and restrict recipients using the `sui_transfer_objects_command` field source. * **Message signing**: constrain the messages a wallet can sign using the `message` field source. * **System conditions**: gate signing on values such as the current timestamp using the `system` field source. Configure decoded transaction rules with the `signTransactionBytes` method, and message-signing rules with the `signRawMessageBytes` method. See [Sui policy documentation](/controls/policies/example-policies/sui) for more information and example policies. # Using wallets Source: https://docs.privy.io/wallets/using-wallets/overview Overview of using Privy wallets for signing, transactions, and RPC interactions To take an action (such as executing a signature, transfer, or other transaction), there are four 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 | | [Wallet automations](/wallets/automations/overview) | Execute wallet actions automatically when configured events occur. Privy monitors events and submits matching actions outside the app's lifecycle. | Event-driven wallet workflows | | [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** wherever possible for your use case. > **[Get started with wallet actions](/wallets/actions/overview)** > **[Get started with RPCs](/wallets/using-wallets/rpc)** > **[Get started with intents](/transaction-management/intents/overview)** # Signing and RPCs Source: https://docs.privy.io/wallets/using-wallets/rpc Send JSON-RPC requests and sign messages or transaction payloads directly with Privy wallets Privy enables you to interface directly with wallets by sending JSON-RPC requests or directly requesting signatures over messages and transaction payloads. This allows you to use Privy for **low-level key management** and achieve ***any* onchain use case** with Privy wallets. ![RPC splash](https://mintcdn.com/privy-c2af3412/xvj7sxQ8SLxi5bGo/images/rpc-splash.png?fit=max\&auto=format\&n=xvj7sxQ8SLxi5bGo\&q=85\&s=7c732769c8e92f4ab80fa3d478e13f18) ## Taking actions with wallets To take an action (such as executing a signature, transfer, or other transaction), there are four 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 | | [Wallet automations](/wallets/automations/overview) | Execute wallet actions automatically when configured events occur. Privy monitors events and submits matching actions outside the app's lifecycle. | Event-driven wallet workflows | | [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 | While using **wallet actions** is generally recommended for most use cases, your application may need to interface with wallets directly for more custom functionality. This is fully supported in parallel with wallet actions via RPCs. ## Using RPCs For Tier 3 chain types, Privy supports the standard set of JSON-RPC methods for that chain type (e.g. [EIP-1193](https://eips.ethereum.org/EIPS/eip-1193) for EVM chains including [Tempo](https://tempo.xyz)) – including message signing, transaction signing, and transaction broadcasting. View guides for using each of these JSON-RPC methods with Privy in the left navigation. For Tier 2 chain types, Privy decodes and signs transactions. Your application is responsible for broadcasting the signed transaction to the chain. For Tier 1 chain types, Privy supports using the wallet's private key to sign custom payloads – allowing you to construct transactions of your choosing and sign them with the wallet. Your application can then broadcast this signed transaction to the chain to execute it. > [!WARNING] > > 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). # Add signers Source: https://docs.privy.io/wallets/using-wallets/signers/add-signers Add third-party signers to wallets allowing delegated transaction execution To allow a third-party to transact on wallets, follow the guide below. > [!NOTE] > > This guide assumes your application has already [configured signers](/wallets/using-wallets/signers/configure-signers) in the Dashboard. **React** To provision server-side access for user's wallets, use the `addSigners` method from the `useSigners` hook: ```tsx theme={"system"} addSigners: async ({address: string, signers: {signerId: string, policyIds: string[]}[]}) => Promise<{user: User}> ``` ### Usage ```tsx theme={"system"} import {useSigners} from '@privy-io/react-auth'; const {addSigners} = useSigners(); ``` > [!TIP] > > Check out the [starter repo](https://github.com/privy-io/examples/blob/main/privy-next-starter/src/components/sections/session-signers.tsx) for an end to end example of how to use signers. ### Parameters The `addSigners` method accepts a `params` object with the following fields: **address** `string` *required* Address of the embedded wallet to add a signer to. **signers** `object[]` *required* **signerId** `string` *required* The key quorum ID that will be allowed to transact on the wallet. This is the same key quorum ID you generated in the [Generate an authorization key](/wallets/using-wallets/signers/configure-signers) step. **policyIds** `string[]` An ID for a policy that any transaction from the signer must satisfy to be signed. This is an optional field, if not provided, no policies will apply to the signers requests. Note that at this time, each signer can only have one override policy. **React Native** To provision server-side access for user's wallets, use the `addSigners` method from the `useSigners` hook: ```tsx theme={"system"} addSigners: async ({address: string, signers: {signerId: string, policyIds: string[]}[]}) => Promise<{user: PrivyUser}> ``` ### Usage ```tsx theme={"system"} import {useSigners} from '@privy-io/expo'; const {addSigners} = useSigners(); ``` ### Parameters The `addSigners` method accepts a `params` object with the following fields: **address** `string` *required* Address of the embedded wallet to add a signer to. **signers** `object[]` *required* **signerId** `string` *required* The key quorum ID that will be allowed to transact on the wallet. This is the same key quorum ID you generated in the [Generate an authorization key](/wallets/using-wallets/signers/configure-signers) step. **policyIds** `string[]` An ID for a policy that any transaction from the signer must satisfy to be signed. This is an optional field, if not provided, no policies will apply to the signers requests. Note that at this time, each signer can only have one override policy. **Swift** To provision server-side access for user's wallets, use the `addSigners` or `addSigner` method on an `EmbeddedWallet`: ```swift theme={"system"} func addSigners(_ signers: [SignerInput]) async throws func addSigner(_ signer: SignerInput) async throws ``` ### Usage ```swift theme={"system"} guard let user = await privy.getUser() else { return } // Retrieve the desired embedded wallet guard let wallet = user.embeddedEthereumWallets.first else { return } // Add multiple signers let signers = [ SignerInput(signerId: "your-key-quorum-id", policyIds: ["your-policy-id"]), SignerInput(signerId: "another-key-quorum-id") ] try await wallet.addSigners(signers) // Or add a single signer let signer = SignerInput(signerId: "your-key-quorum-id", policyIds: ["your-policy-id"]) try await wallet.addSigner(signer) ``` ### Parameters The `addSigners` method accepts an array of `SignerInput` objects with the following fields: **signerId** `String` *required* The key quorum ID that will be allowed to transact on the wallet. This is the same key quorum ID generated in the [Generate an authorization key](/wallets/using-wallets/signers/configure-signers) step. **policyIds** `[String]?` An optional list of policy IDs that any transaction from the signer must satisfy to be signed. If `nil`, the wallet's default policies apply. Note that at this time, each signer can only have one override policy. The `addSigner` method is a convenience method that accepts a single `SignerInput` object with the same fields. **Android** To provision server-side access for user's wallets, use the `addSigners` or `addSigner` method on an `EmbeddedWallet`: ```kotlin theme={"system"} suspend fun addSigners(signers: List): Result suspend fun EmbeddedWallet.addSigner(signer: SignerInput): Result ``` ### Usage ```kotlin theme={"system"} val user = privy.getUser() ?: return // Retrieve the desired embedded wallet val wallet = user.embeddedEthereumWallets.firstOrNull() ?: return // Add multiple signers val signers = listOf( SignerInput(signerId = "your-key-quorum-id", policyIds = listOf("your-policy-id")), SignerInput(signerId = "another-key-quorum-id") ) wallet.addSigners(signers).fold( onSuccess = { /* Signers added successfully */ }, onFailure = { error -> println("Error adding signers: ${error.message}") } ) // Or add a single signer val signer = SignerInput(signerId = "your-key-quorum-id", policyIds = listOf("your-policy-id")) wallet.addSigner(signer).fold( onSuccess = { /* Signer added successfully */ }, onFailure = { error -> println("Error adding signer: ${error.message}") } ) ``` ### Parameters The `addSigners` method accepts a list of `SignerInput` objects with the following fields: **signerId** `String` *required* The key quorum ID that will be allowed to transact on the wallet. This is the same key quorum ID generated in the [Generate an authorization key](/wallets/using-wallets/signers/configure-signers) step. **policyIds** `List\?` An optional list of policy IDs that any transaction from the signer must satisfy to be signed. If `null`, the wallet's default policies apply. Note that at this time, each signer can only have one override policy. The `addSigner` method is a convenience extension that accepts a single `SignerInput` object with the same fields. **Flutter** To provision server-side access for user's wallets, use the `addSigners` or `addSigner` method on an `EmbeddedWallet`: ```dart theme={"system"} Future> addSigners(List signers) Future> addSigner(SignerInput signer) ``` ### Usage ```dart theme={"system"} final user = await privy.getUser(); if (user == null) return; // Retrieve the desired embedded wallet final wallet = user.embeddedEthereumWallets.firstOrNull; if (wallet == null) return; // Add multiple signers final signers = [ SignerInput(signerId: 'your-key-quorum-id', policyIds: ['your-policy-id']), SignerInput(signerId: 'another-key-quorum-id'), ]; final result = await wallet.addSigners(signers); result.fold( onSuccess: (_) { // Signers added successfully }, onFailure: (error) { print('Error adding signers: $error'); }, ); // Or add a single signer final signer = SignerInput(signerId: 'your-key-quorum-id', policyIds: ['your-policy-id']); final singleResult = await wallet.addSigner(signer); singleResult.fold( onSuccess: (_) { // Signer added successfully }, onFailure: (error) { print('Error adding signer: $error'); }, ); ``` ### Parameters The `addSigners` method accepts a list of `SignerInput` objects with the following fields: **signerId** `String` *required* The key quorum ID that will be allowed to transact on the wallet. This is the same key quorum ID generated in the [Generate an authorization key](/wallets/using-wallets/signers/configure-signers) step. **policyIds** `List\?` An optional list of policy IDs that any transaction from the signer must satisfy to be signed. If `null`, the wallet's default policies apply. Note that at this time, each signer can only have one override policy. The `addSigner` method is a convenience wrapper that accepts a single `SignerInput` object with the same fields. **NodeJS & REST API** Make a request to [update the wallet](/wallets/wallets/update-a-wallet) with the desired `additional_signers` you'd like to add. The wallet owner must [sign](/controls/authorization-keys/using-owners/sign) the request. # Configure signers Source: https://docs.privy.io/wallets/using-wallets/signers/configure-signers Configure which signers your app can use to transact on user embedded wallets Follow the guide below to configure signers your app can use to transact on user's embedded wallets. ## 1. Create a key quorum Fundamentally, a signer is a [key quorum](/controls/authorization-keys/overview) that is authorized to submit transaction request for signature from a user's wallet. You can create key quorums in the [Privy Dashboard](https://dashboard.privy.io//apps?page=authorization-keys) or via an [API request](/api-reference/key-quorums/create), To generate a key quorum in the [Dashboard](https://dashboard.privy.io//apps?page=authorization-keys), navigate to the **Wallet infrastructure > Authorization keys** page and click on the **Create new key** button. The modal will show the key quorum ID and the private key used for signing. Save both these values in a secure location. **Privy never sees this private key and cannot help you recover it.** The private key will be used to sign transaction requests on the wallet. > [!NOTE] > > The **authorization key** is the private key of a P-256 keypair. Privy never sees the private key, > and verifies signatures on your requests against the corresponding public key to ensure your > server authorizes the action to take with a user's delegated wallet. ## 2. Create policies to configure permissions \[optional] Signers can also have policies that any transaction must satisfy to be signed by the wallet. To configure permissions to control what kinds of actions your app can take with users wallets, set up [policies](/controls/policies/overview) in your Privy dashboard under **Wallet infrastructure > Policies**. ## 3. Add the signer to the wallet To use signers, the owner of a wallet must first grant consent for your app to take certain actions on their behalf. To have owners grant consent, follow the guides below depending on the SDK(s) you integrate. # Overview Source: https://docs.privy.io/wallets/using-wallets/signers/overview Overview of Privy wallet signers for delegating transaction execution to users or servers. Privy enables your app to add **signers** to wallets that can take actions within the scope of certain permissions. You can use signers to enable various use cases, like: * **Offline actions**: execute limit orders or agentic trades even while a user is offline in your app. * **Recurring actions**: implement subscriptions, portfolio rebalancing, and more. * **Scoping wallet policies to specific parties**: set specific policies on wallets that apply to specific signers (authorization keys, users, or key quorums) * **Delegating access to third-parties**: allow third-parties to execute certain actions on behalf of a wallet. Signers can be added to wallets owned by [users, authorization keys, or key quorums](/controls/authorization-keys/owners/overview). Privy’s architecture guarantees that a **signer** will never see the wallet's private key. All signing takes place in a secure enclave that only your application can make authorized requests to. Follow the guides below to provision signers for your users' wallets and enable your app to securely interact with these wallets from your servers. ## Get started > **[Add a signer](/wallets/using-wallets/signers/add-signers)** > > Add a signer to a user's wallet and start interacting with the wallet from your servers. > **[Send transactions from your server](/wallets/using-wallets/signers/use-signers)** > > Send transactions on behalf of your users from a server environment. # Enabling users or servers to execute transactions Source: https://docs.privy.io/wallets/using-wallets/signers/quickstart Quickstart guide for enabling users or servers to execute transactions on embedded wallets 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. > [!TIP] > > 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. > [!TIP] > > 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. > [!TIP] > > 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. **React (web apps and Telegram mini apps)** 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} ``` **React Native (mobile apps)** 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. > [!TIP] > > 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. **React (web apps and Telegram mini apps)** 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'] }] }) ``` > **Add a signer to a user's wallet immediately after they login.** > > 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 > ``` > **Add a signer for limit orders.** > > If your app offers limit orders to users, we recommend the following flow for using signers to execute limit orders. > > **1. Create your limit order policy** > > Create a policy that allows your signer to execute the limit order from the user's wallet. > > **2. Add your signer** > > Add your signer to the user's wallet via the `addSessionSigner` method, using the policy from step 1. > > **3. Execute the limit order** > > 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. **React Native (mobile apps)** 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: [] }] }) ``` > **Add a signer for limit orders.** > > 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 > **[React](/wallets/using-wallets/ethereum/send-a-transaction)** > > Send Ethereum transactions from a web app or a Telegram mini-app using Privy's React SDK. > **[React Native](/wallets/using-wallets/ethereum/send-a-transaction#react-native)** > > Send Ethereum transactions from a mobile app or a Telegram mini-app using Privy's React Native > SDK. #### Solana > **[React](/wallets/using-wallets/solana/send-a-transaction)** > > Send Solana transactions from a web app or a Telegram mini-app using Privy's React SDK. > **[React Native](/wallets/using-wallets/solana/send-a-transaction#react-native)** > > 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. > [!NOTE] > > 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). **REST API** 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. > [!TIP] > > **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. # Remove signers Source: https://docs.privy.io/wallets/using-wallets/signers/remove-signers Remove signers from wallets to revoke delegated transaction permissions **Once** a wallet has [signers](/wallets/using-wallets/signers/add-signers), they may also revoke consent to prevent your app from taking any further wallet actions on their behalf. **React** To remove all the signers on the wallet, use the `removeSigners` method from the `useSigners` hook: ```tsx theme={"system"} removeSigners: async ({address: string}) => Promise<{user: User}> ``` ### Usage ```tsx theme={"system"} import {useSigners} from '@privy-io/react-auth'; ... const {removeSigners} = useSigners(); ``` When invoked, the `removeSigners` method will remove all the signers, so only the user can transact on the wallet. > [!WARNING] > > After this action, your app will no longer be able to take actions on behalf of the user with > their wallet unless the user adds more [session > signers](/wallets/using-wallets/signers/add-signers). > [!TIP] > > Check out the [starter repo](https://github.com/privy-io/examples/blob/main/privy-next-starter/src/components/sections/session-signers.tsx) > for an end to end example of how to use signers. ### Parameters The `removeSigners` method accepts a `params` object with the following fields: **address** `string` *required* Address of the embedded wallet to delegate. As an example, you might have a button within your app to allow users to remove all signers like so: ```tsx Example remove signers button theme={"system"} import {usePrivy, useSigners, type WalletWithMetadata} from '@privy-io/react-auth'; function RemoveSessionSignersButton() { const {user} = usePrivy(); const {removeSigners} = useSigners(); // Check if the user's wallets already has signers by searching the linkedAccounts array for wallets // with `delegated: true` set const delegatedWallet = user.linkedAccounts.filter( (account): account is WalletWithMetadata => account.type === 'wallet' && account.delegated ); const onRevoke = async () => { if (!hasDelegatedWallets) return; // Button is disabled to prevent this case await removeSigners({address: delegatedWallet.address}); }; return ( ); } ``` **React Native** To remove all the signers on the wallet, use the `removeSigners` method from the `useSigners` hook: ```tsx theme={"system"} removeSigners: async ({address: string}) => Promise<{user: PrivyUser}}; ``` ### Usage ```tsx theme={"system"} import {useSigners} from '@privy-io/expo'; ... const {removeSigners} = useSigners(); ``` When invoked, the `removeSigners` method will remove all the signers, so only the user can transact on the wallet. > [!WARNING] > > After this action, your app will no longer be able to take actions on behalf of the user with > their wallet unless the user adds more [session > signers](/wallets/using-wallets/signers/add-signers). ### Parameters The `removeSigners` method accepts a `params` object with the following fields: **address** `string` *required* Address of the embedded wallet to delegate. As an example, you might have a button within your app to allow users to remove all signers like so: ```tsx Example remove signers button theme={"system"} import {usePrivy, useSigners, type PrivyEmbeddedWalletAccount} from '@privy-io/expo'; function RemoveSessionSignersButton() { const {user} = usePrivy(); const {removeSigners} = useSigners(); // Check if the user's wallets already has signers by searching the linked_accounts array for wallets // with `delegated: true` set const delegatedWallet = user.linked_accounts.find( (account): account is PrivyEmbeddedWalletAccount => account.type === 'wallet' && account.delegated ); const onRevoke = async () => { if (!delegatedWallet) return; // Button is disabled to prevent this case await removeSigners({address: delegatedWallet.address}); }; return ( ); } ``` **Swift** To remove a specific signer or all signers from a wallet, use the `removeSigner` or `removeAllSigners` method on an `EmbeddedWallet`: ```swift theme={"system"} func removeSigner(signerId: String) async throws func removeAllSigners() async throws ``` ### Usage ```swift theme={"system"} guard let user = await privy.getUser() else { return } // Retrieve the desired embedded wallet guard let wallet = user.embeddedEthereumWallets.first else { return } // Remove a specific signer by ID try await wallet.removeSigner(signerId: "your-key-quorum-id") // Or remove all signers try await wallet.removeAllSigners() ``` When invoked, `removeAllSigners` removes all signers so only the user can transact on the wallet. The `removeSigner` method removes only the signer matching the provided ID. > [!WARNING] > > After removing all signers, your app will no longer be able to take actions on behalf of the user > with their wallet unless the user adds more [signers](/wallets/using-wallets/signers/add-signers). ### Parameters #### `removeSigner` **signerId** `String` *required* The identifier of the signer to remove from the wallet. #### `removeAllSigners` No parameters required. **Android** To remove a specific signer or all signers from a wallet, use the `removeSigner` or `removeAllSigners` method on an `EmbeddedWallet`: ```kotlin theme={"system"} suspend fun removeSigner(signerId: String): Result suspend fun removeAllSigners(): Result ``` ### Usage ```kotlin theme={"system"} val user = privy.getUser() ?: return // Retrieve the desired embedded wallet val wallet = user.embeddedEthereumWallets.firstOrNull() ?: return // Remove a specific signer by ID wallet.removeSigner(signerId = "your-key-quorum-id").fold( onSuccess = { /* Signer removed successfully */ }, onFailure = { error -> println("Error removing signer: ${error.message}") } ) // Or remove all signers wallet.removeAllSigners().fold( onSuccess = { /* All signers removed successfully */ }, onFailure = { error -> println("Error removing signers: ${error.message}") } ) ``` When invoked, `removeAllSigners` removes all signers so only the user can transact on the wallet. The `removeSigner` method removes only the signer matching the provided ID. > [!WARNING] > > After removing all signers, your app will no longer be able to take actions on behalf of the user > with their wallet unless the user adds more [signers](/wallets/using-wallets/signers/add-signers). ### Parameters #### `removeSigner` **signerId** `String` *required* The identifier of the signer to remove from the wallet. #### `removeAllSigners` No parameters required. **Flutter** To remove a specific signer or all signers from a wallet, use the `removeSigner` or `removeAllSigners` method on an `EmbeddedWallet`: ```dart theme={"system"} Future> removeSigner(String signerId) Future> removeAllSigners() ``` ### Usage ```dart theme={"system"} final user = await privy.getUser(); if (user == null) return; // Retrieve the desired embedded wallet final wallet = user.embeddedEthereumWallets.firstOrNull; if (wallet == null) return; // Remove a specific signer by ID final result = await wallet.removeSigner('your-key-quorum-id'); result.fold( onSuccess: (_) { // Signer removed successfully }, onFailure: (error) { print('Error removing signer: $error'); }, ); // Or remove all signers final removeAllResult = await wallet.removeAllSigners(); removeAllResult.fold( onSuccess: (_) { // All signers removed successfully }, onFailure: (error) { print('Error removing signers: $error'); }, ); ``` When invoked, `removeAllSigners` removes all signers so only the user can transact on the wallet. The `removeSigner` method removes only the signer matching the provided ID. > [!WARNING] > > After removing all signers, your app will no longer be able to take actions on behalf of the user > with their wallet unless the user adds more [signers](/wallets/using-wallets/signers/add-signers). ### Parameters #### `removeSigner` **signerId** `String` *required* The identifier of the signer to remove from the wallet. #### `removeAllSigners` No parameters required. **NodeJS & REST API** Make a request to [update the wallet](/wallets/wallets/update-a-wallet) with the updated list of `additional_signers` you'd like on the wallet. The wallet owner must [sign](/controls/authorization-keys/using-owners/sign) the request. # Use signers Source: https://docs.privy.io/wallets/using-wallets/signers/use-signers Execute transactions on user wallets from your server using provisioned signers Once your user's have signers added on their wallets, your app can take actions on their behalf. This is done by sending requests to the Privy API to sign transactions with the user's wallet. Follow the guide below to get started with signing transactions on behalf of users. ## Requesting signatures Wallets provisioned with signers can be used to transact and sign messages on behalf of a user from your server. To get started, configure the [NodeJS SDK](/basics/nodeJS/setup) or the [REST API](/basics/rest-api/setup). This is how your application will make requests to the Privy API to sign transactions on behalf of users. The signing key you configured in the dashboard is the authorization signing key used to produce authorization signatures when submitting requests. Once you have configured the NodeJS SDK or REST API, your application can send or sign transactions from a users wallet. Follow the NodeJS or REST API guides in the `Using wallets` section to learn more about signing requests with wallets. ## Getting wallets From your server, you can query Privy to determine what wallets have been provisioned signers by a given user to allow your app to take actions on their behalf. **NodeJS** Use the Privy client's `_get` method on the users interface to get the user object for your user. As a parameter to this method, pass the user's ID as a `string`: ```tsx theme={"system"} const user = await privy.users()._get('insert-user-did'); ``` To get a list of the user's wallets, find all of the user's wallets from the user's linked accounts. Filter the `user.linkedAccounts` array for wallet entries with `type: 'wallet'`: ```tsx theme={"system"} const walletsWithSessionSigners = user.linked_accounts.filter( (account) => account.type === 'wallet' && 'id' in account && account.delegated ); ``` This constitutes the user's wallets with any added signers. Wallets with signers will always have the `delegated` flag set to `true`. For wallets included in this array, your app may make requests to Privy to execute actions on behalf of the user. **Python** Use the `get` method on the users service to retrieve the user, then select linked wallet accounts that have been delegated to a signer. ```python theme={"system"} user = client.users.get("insert-user-did") wallets_with_signers = [ account for account in user.linked_accounts if account.type == "wallet" and getattr(account, "id", None) is not None and getattr(account, "delegated", False) ] ``` Your app can make server-side wallet requests for the wallets in this list, using the provisioned authorization key in the request's authorization context. **REST API** Make a `GET` request to: ```bash theme={"system"} https://auth.privy.io/api/v1/users/ ``` 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 GET https://auth.privy.io/api/v1/users/ \ -u ":" \ -H "privy-app-id: " ``` Then, to get a list of the user's delegated wallets, inspect the `linked_accounts` field of the response body for all entries with the fields `type: 'wallet'` and `delegated: true`. # Integrating with @solana/kit Source: https://docs.privy.io/wallets/using-wallets/solana/kit-integrations **React** 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. > [!NOTE] > > 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; ``` **NodeJS** Privy's wallets on Solana natively integrate with `@solana/kit` via the `@privy-io/node` SDK, allowing you to use the library's signer interfaces for signing messages and transactions, as well as sending those transactions to the blockchain. ## Setup To integrate with `@solana/kit`, first install the library as a peer dependency: ```sh theme={"system"} npm i @solana/kit ``` Then, use Privy's `createSolanaKitSigner` method to initialize a signer for a Solana wallet. To this method, pass the following fields: **walletId** `string` *required* The ID of the wallet to sign with. **address** `string` *required* The address of the wallet to sign with. **authorizationContext** `object` The authorization context for the wallet. Refer to the [authorization context](/controls/authorization-context) guide for more details. **caip2** `string` The CAIP-2 identifier for the Solana network. This is only required when sending transactions with the Signer. ### Example usage As an example, you can initialize a signer like so: ```ts theme={"system"} import {PrivyClient} from '@privy-io/node'; import {createSolanaKitSigner} from '@privy-io/node/solana-kit'; import {address} from '@solana/kit'; // Set up the Privy client and signer const privy = new PrivyClient({appId: 'your-app-id', appSecret: 'your-app-secret'}); const signer = createSolanaKitSigner(privy, { walletId: 'insert-wallet-id', address: address('insert-wallet-address') }); ``` > [!TIP] > > If your wallet requires an [authorization context](/controls/authorization-context), > you should pass it to the `createSolanaKitSigner` method like so, and it will be used > throughout the lifetime of the signer: > > ```ts theme={"system"} > import {PrivyClient} from '@privy-io/node'; > import {createSolanaKitSigner} from '@privy-io/node/solana-kit'; > import {address} from '@solana/kit'; > > // Set up the Privy client and signer > const privy = new PrivyClient({appId: 'your-app-id', appSecret: 'your-app-secret'}); > const signer = createSolanaKitSigner(privy, { > walletId: 'insert-wallet-id', > address: address('insert-wallet-address'), > authorizationContext: { > authorization_private_keys: ['authorization-key'] > } > }); > ``` ## Signing messages You can sign messages using helper functions provided by `@solana/kit`, or by calling the `signMessages` method directly: ```ts Ergonomic theme={"system"} import {PrivyClient} from '@privy-io/node'; import {createSolanaKitSigner} from '@privy-io/node/solana-kit'; import {address, signOffchainMessageWithSigners, type OffchainMessage} from '@solana/kit'; // Set up the Privy client and signer const privy = new PrivyClient({appId: 'your-app-id', appSecret: 'your-app-secret'}); const signer = createSolanaKitSigner(privy, { walletId: 'insert-wallet-id', address: address('insert-wallet-address') }); const offchainMessage: OffchainMessage = { version: 1, content: 'Hello, world!', requiredSignatories: [signer] }; const signedEnvelope = await signOffchainMessageWithSigners(offchainMessage); ``` ```ts Direct theme={"system"} import {PrivyClient} from '@privy-io/node'; import {createSolanaKitSigner} from '@privy-io/node/solana-kit'; import {address, createSignableMessage} from '@solana/kit'; // Set up the Privy client and signer const privy = new PrivyClient({appId: 'your-app-id', appSecret: 'your-app-secret'}); const signer = createSolanaKitSigner(privy, { walletId: 'insert-wallet-id', address: address('insert-wallet-address') }); const message = createSignableMessage('Hello, world!'); const signatures = await signer.signMessages([message]); ``` ## Signing transactions You can sign transactions using `@solana/kit`'s transaction building utilities: ```ts theme={"system"} import {PrivyClient} from '@privy-io/node'; import {createSolanaKitSigner} from '@privy-io/node/solana-kit'; import { address, createTransactionMessage, pipe, setTransactionMessageFeePayerSigner, signTransactionMessageWithSigners } from '@solana/kit'; // Set up the Privy client and signer const privy = new PrivyClient({appId: 'your-app-id', appSecret: 'your-app-secret'}); const signer = createSolanaKitSigner(privy, { walletId: 'insert-wallet-id', address: address('insert-wallet-address') }); const transactionMessage = pipe( createTransactionMessage({version: 0}), // This sets both the fee payer address *and* uses the underlying signer. (m) => setTransactionMessageFeePayerSigner(signer, m) // Add instructions, lifetime, etc. ); const signedTransaction = await signTransactionMessageWithSigners(transactionMessage); ``` ## Signing and sending transactions To sign and send transactions, you must provide the `caip2` parameter when creating the signer: ```ts theme={"system"} import {PrivyClient} from '@privy-io/node'; import {createSolanaKitSigner} from '@privy-io/node/solana-kit'; import { address, createTransactionMessage, pipe, setTransactionMessageFeePayerSigner, signAndSendTransactionMessageWithSigners } from '@solana/kit'; // Set up the Privy client and signer const privy = new PrivyClient({appId: 'your-app-id', appSecret: 'your-app-secret'}); const signer = createSolanaKitSigner(privy, { walletId: 'insert-wallet-id', address: address('insert-wallet-address'), caip2: 'solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1' // Solana Devnet }); // The signer can now be used with signAndSendTransactionMessageWithSigners const transactionMessage = pipe( createTransactionMessage({version: 0}), (m) => setTransactionMessageFeePayerSigner(signer, m) // Add instructions, lifetime, etc. ); const signedTransaction = await signAndSendTransactionMessageWithSigners(transactionMessage); ``` > [!TIP] > > Common CAIP-2 identifiers for Solana: > > * Mainnet: `solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp` > * Testnet: `solana:4uhcVJyU9pJkvQyS88uRDiswHXSCkY3z` > * Devnet: `solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1` # Send a Solana transaction Source: https://docs.privy.io/wallets/using-wallets/solana/send-a-transaction Sign and broadcast a Solana transaction with Privy > [!TIP] > > To rely on Privy's API to fill in the recentBlockhash field of the Solana transaction, pass in the > dummy value `11111111111111111111111111111111` for the recentBlockhash field. > [!NOTE] > > When using Privy's server-side SDKs to send transactions, you can use the authorization context to > automatically sign requests. Learn more about [signing on the > server](/controls/authorization-keys/using-owners/sign/signing-on-the-server). **React** To send a transaction from a wallet using the React SDK, use the `signAndSendTransaction` method from the `useSignAndSendTransaction` hook: ```javascript theme={"system"} signAndSendTransaction: (input: { transaction: Uint8Array; wallet: ConnectedStandardSolanaWallet; chain?: SolanaChain; options?: SolanaSignAndSendTransactionOptions & { uiOptions?: SendTransactionModalUIOptions; sponsor?: boolean; optimisticBroadcast?: boolean; skipSimulation?: boolean; }; }) => Promise<{ signature: Uint8Array }> ``` ### Usage ```javascript theme={"system"} import {useSignAndSendTransaction, useWallets} 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'; // Inside your component const {signAndSendTransaction} = useSignAndSendTransaction(); const {wallets} = useWallets(); const selectedWallet = wallets[0]; const amount = 1; const transferInstruction = getTransferSolInstruction({ amount: BigInt(parseFloat(amount) * 1_000_000_000), // Convert SOL to lamports destination: address('RecipientAddressHere'), source: createNoopSigner(address(selectedWallet.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 a transaction using @solana/kit const transaction = pipe( createTransactionMessage({version: 0}), (tx) => setTransactionMessageFeePayer(address(selectedWallet.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 ); // Send the transaction const result = await signAndSendTransaction({ transaction: transaction, wallet: selectedWallet }); console.log('Transaction sent with signature:', result.signature); ``` ### Parameters **parameters** **transaction** `Uint8Array` *required* The encoded transaction to be sent. **wallet** `ConnectedStandardSolanaWallet` *required* The Solana wallet to use for sending the transaction. **chain** `SolanaChain` Type of all Solana chains supported by Privy. **options** `SolanaSignAndSendTransactionOptions & \{uiOptions?: SendTransactionModalUIOptions; sponsor?: boolean; optimisticBroadcast?: boolean; skipSimulation?: boolean\}` Additional options for sending the transaction. **child attributes** **sponsor** `boolean` Optional parameter to enable gas sponsorship for this transaction. [Learn more.](/wallets/gas-and-asset-management/gas/overview) **optimisticBroadcast** `boolean` When true, returns the signed transaction hash without waiting for on-chain confirmation. For sponsored transactions, Privy handles this server-side. For non-sponsored transactions, the send is still awaited but confirmation polling is skipped. **skipSimulation** `boolean` When true, skips preflight simulation before broadcasting. For sponsored transactions, Privy forwards this as the `x-privy-skip-simulation` header. For non-sponsored transactions, this maps to `skipPreflight` on the local RPC send call. **uiOptions** `SendTransactionModalUIOptions` UI options to customize the transaction request modal. > [!TIP] > > To hide confirmation modals, set `options.uiOptions.showWalletUIs` to `false`. Learn more > about configuring modal prompts [here](/recipes/react/manage-wallet-UIs). ### Returns **signature** `Uint8Array` The signature of the transaction. ## Sign and send all transactions To sign and send multiple transactions in a single call, use the `signAndSendTransaction` method with multiple inputs: ```tsx theme={"system"} signAndSendTransaction(...inputs: SignAndSendTransactionInput[]): Promise ``` ### Usage ```typescript theme={"system"} import {useSignAndSendTransaction, useWallets} 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'; // Inside your component const {signAndSendTransaction} = useSignAndSendTransaction(); const {wallets} = useWallets(); const selectedWallet = wallets[0]; const amount = 1; // Amount of SOL to send const transferInstruction = getTransferSolInstruction({ amount: BigInt(amount * 1_000_000_000), // Convert SOL to lamports destination: address('RecipientAddressHere'), source: createNoopSigner(address(selectedWallet.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 transactions using @solana/kit const transactions = [ pipe( createTransactionMessage({version: 0}), (tx) => setTransactionMessageFeePayer(address(selectedWallet.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 ), pipe( createTransactionMessage({version: 0}), (tx) => setTransactionMessageFeePayer(address(selectedWallet.address), tx), (tx) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, tx), (tx) => appendTransactionMessageInstructions([transferInstruction], tx), (tx) => compileTransaction(tx), (tx) => new Uint8Array(getTransactionEncoder().encode(tx)) ) ]; // Send multiple transactions const results = await signAndSendTransaction( { transaction: transactions[0], wallet: selectedWallet }, { transaction: transactions[1], wallet: selectedWallet } ); console.log( 'Transactions sent with signatures:', results.map((result) => result.signature.toString()).join(',') ); ``` ### Parameters **parameters** **inputs** `SignAndSendTransactionInput[]` *required* **transaction** `Uint8Array` *required* The encoded transaction to be sent. **wallet** `ConnectedStandardSolanaWallet` *required* The Solana wallet to use for sending the transaction. **chain** `SolanaChain` Type of all Solana chains supported by Privy. **options** `SolanaSignAndSendTransactionOptions & \{uiOptions?: SendTransactionModalUIOptions; sponsor?: boolean; optimisticBroadcast?: boolean; skipSimulation?: boolean\}` Additional options for sending the transaction. **child attributes** **sponsor** `boolean` Optional parameter to enable gas sponsorship for this transaction. [Learn more.](/wallets/gas-and-asset-management/gas/overview) **optimisticBroadcast** `boolean` When true, returns the signed transaction hash without waiting for on-chain confirmation. For sponsored transactions, Privy handles this server-side. For non-sponsored transactions, the send is still awaited but confirmation polling is skipped. **skipSimulation** `boolean` When true, skips preflight simulation before broadcasting. For sponsored transactions, Privy forwards this as the `x-privy-skip-simulation` header. For non-sponsored transactions, this maps to `skipPreflight` on the local RPC send call. **uiOptions** `SendTransactionModalUIOptions` UI options to customize the transaction request modal. **React Native** To send a transaction from a wallet using the React Native SDK, use the `request` method from the wallet's provider: ```javascript theme={"system"} request: (request: { method: 'signAndSendTransaction', params: { transaction: Transaction | VersionedTransaction, connection: Connection } }) => Promise<{ signature: string }> ``` > [!NOTE] > > The Expo SDK does not support built-in UIs for sending transactions. > The `signAndSendTransaction` method gives you complete control over the experience and UI. ### Usage ```javascript theme={"system"} import {Connection} from '@solana/web3.js'; import {useEmbeddedSolanaWallet} from '@privy-io/expo'; const { wallets } = useEmbeddedSolanaWallet(); const wallet = wallets[0]; // Get the provider const provider = await wallet.getProvider(); // 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, }, }); console.log("Transaction sent with signature:", signature); ``` ### Parameters **method** `'signAndSendTransaction'` *required* The RPC method executed with the wallet. **params** `Object` *required* Parameters for the transaction. **child attributes** **transaction** `Transaction | VersionedTransaction` *required* The transaction to sign and send. This can be either a legacy Transaction or a VersionedTransaction object from [@solana/web3.js](https://solana-foundation.github.io/solana-web3.js/classes/Transaction.html). **connection** `Connection` *required* Connection to an SVM (Solana) network. ### Returns **signature** `string` The signature of the transaction. **Swift** Sending a Solana transaction will sign it using the embedded wallet and then submit it to the network. ```swift theme={"system"} public protocol EmbeddedSolanaWalletProvider { func signAndSendTransaction(transaction: Data, cluster: SolanaCluster, options: SolanaSendOptions?) async throws -> String } ``` ### A note on Solana clusters When sending a transaction you must specify which cluster the transaction is being sent on. For it, the Privy SDK provides the `SolanaCluster` struct, with the following options: * `SolanaCluster.mainnet` for the Solana mainnet-beta. * `SolanaCluster.devnet` for the Solana devnet. * `SolanaCluster.testnet` for the Solana testnet. * `SolanaCluster.mainnet.with(rpcUrl: String)` for using the Solana mainnet cluster but broadcasting to a different RPC URL than the default one. * `SolanaCluster(caip2: String, rpcUrl: String)` for a fully custom Solana cluster. ### Usage In this example, we're using the `SolanaSwift` SDK to build the transaction, to be sent on the mainnet cluster. You can use whichever method you prefer for building and serializing the transaction before handing off to Privy. ```swift theme={"system"} import SolanaSwift // Get the provider for wallet (assumes wallet is already obtained) let provider = wallet.provider let cluster = SolanaCluster.mainnet // Create a Solana RPC client let solana = JSONRPCAPIClient(endpoint: URL(string: cluster.rpcUrl)!) // Build the transaction using your preferred method let latestBlockhash = try await solana.getLatestBlockhash() // If your application is set to TEE execution, you can allow Privy to set the recent blockhash for // you by passing in `11111111111111111111111111111111` as the blockhash. // let latestBlockhash = "11111111111111111111111111111111" let walletPK = try PublicKey(string: wallet.address) var tx = Transaction() tx.instructions.append( SystemProgram.transferInstruction( from: walletPK, to: try PublicKey(string: "9NvE68JVWHHHGLp5NNELtM5fiBw6SXHrzqQJjUqaykC1"), lamports: 100000000000000 ) ) tx.recentBlockhash = latestBlockhash tx.feePayer = walletPK // Sign and broadcast the transaction using the Privy Embedded Wallet let txHash = try await provider.signAndSendTransaction(transaction: tx.serialize(), cluster: cluster) print("Transaction sent with hash: \(txHash)") ``` ### Parameters **transaction** `Data` *required* The serialized transaction to sign and send. **cluster** `SolanaCluster` *required* The cluster the transaction is being sent on. This determines both the CAIP-2 formatted ID for the cluster, and the RPC URL to use for broadcasting the transaction. Note that the RPC URL set here is only used for applications running in on device execution mode. **options** `SolanaSendOptions?` Additional options for the RPC broadcast. **child attributes** **skipPreflight** `Bool?` *required* Disable the transaction verification step. **preflightCommitment** `Commitment?` *required* The preflight commitment level. **maxRetries** `Int?` *required* The maximum number of times for the RPC node to retry sending the transaction to the leader. **minContextSlot** `Int?` *required* The minimum slot that the request can be evaluated at. ### Returns **txHash** `String` The hash of the broadcasted transaction. **Android** Use the `signAndSendTransaction` method to sign and submit a Solana transaction in a single call. ```kotlin theme={"system"} public suspend fun signAndSendTransaction( transaction: ByteArray, cluster: SolanaCluster = SolanaCluster.MainNet, rpcUrl: String? = null, sendOptions: SendOptions? = null ): Result ``` ### Usage ```kotlin theme={"system"} val solanaWallet = user.embeddedSolanaWallets.first() // Fetch latest blockhash from network val blockhash = fetchLatestBlockhash(SolanaCluster.DevNet).getOrThrow() // For apps with TEE execution: use "11111111111111111111111111111111" instead - Privy auto-populates it // Build the transaction val fromPubkey = SolanaPublicKey.from(solanaWallet.address) val instruction = SystemProgram.transfer( fromPubkey = fromPubkey, toPubkey = SolanaPublicKey.from(recipientAddress), lamports = 1000000 ) val txMessage = Message.Builder() .addInstruction(instruction) .setRecentBlockhash(blockhash) .build() val transaction = Transaction(txMessage).serialize() // Sign and send val result = solanaWallet.provider.signAndSendTransaction( transaction = transaction, cluster = SolanaCluster.DevNet ) result.fold( onSuccess = { signature -> println("Success: https://explorer.solana.com/tx/$signature?cluster=devnet") }, onFailure = { error -> println("Failed: ${error.message}") } ) ``` ### With SendOptions ```kotlin theme={"system"} val result = solanaWallet.provider.signAndSendTransaction( transaction = transaction, cluster = SolanaCluster.DevNet, sendOptions = SendOptions( preflightCommitment = "confirmed", maxRetries = 5 ) ) ``` ### Parameters **transaction** `ByteArray` *required* The serialized transaction bytes to sign and send. **cluster** `SolanaCluster` The Solana network cluster. Defaults to `SolanaCluster.MainNet`. **rpcUrl** `String?` Optional custom RPC URL. If not provided, uses the default RPC for the specified cluster. **sendOptions** `SendOptions?` Optional configuration for transaction submission. **child attributes** **skipPreflight** `Boolean?` Whether to skip preflight transaction checks. Defaults to `false`. **preflightCommitment** `String?` Commitment level for preflight checks (e.g., `"confirmed"`, `"finalized"`). **maxRetries** `Int?` Maximum number of retry attempts for sending the transaction. **minContextSlot** `Long?` Minimum slot number at which the transaction can be evaluated. > [!NOTE] > > The `cluster`, `rpcUrl`, and `sendOptions` parameters are only valid for apps using [on-device execution](/security/wallet-infrastructure/advanced/user-device). ### Returns **result** `Result\` A Result that, when successful, contains the transaction signature. **Flutter** Use the `signAndSendTransaction` method on the Solana wallet provider to sign a transaction and submit it to the network in a single call. ```dart theme={"system"} Future> signAndSendTransaction({ required Uint8List transaction, SolanaCluster? cluster, String? rpcUrl, SendOptions? sendOptions, }); ``` ### Usage ```dart theme={"system"} import 'dart:convert'; import 'package:solana/solana.dart'; // Retrieve the user's Solana wallet (assumes wallet is already obtained) final solanaWallet = user.embeddedSolanaWallets.first; // Create a Solana RPC client final rpcClient = RpcClient('https://api.mainnet-beta.solana.com'); // Build the transaction final walletPublicKey = Ed25519HDPublicKey.fromBase58(solanaWallet.address); final recipientPublicKey = Ed25519HDPublicKey.fromBase58(recipientAddress); final instruction = SystemInstruction.transfer( fundingAccount: walletPublicKey, recipientAccount: recipientPublicKey, lamports: amount, ); // Get recent blockhash final recentBlockhash = await rpcClient.getLatestBlockhash(); // For apps with TEE execution: use "11111111111111111111111111111111" instead - Privy auto-populates it // Create transaction final transaction = Transaction.v0( payer: walletPublicKey, recentBlockhash: recentBlockhash.value.blockhash, instructions: [instruction], ); // Serialize transaction message final serializedTransaction = transaction.serializeMessage(); // Sign and send the transaction final result = await solanaWallet.provider.signAndSendTransaction( transaction: serializedTransaction ); // Handle the result result.when( success: (txHash) { print('Transaction sent with hash: $txHash'); }, failure: (error) { print('Failed to sign and send transaction: $error'); }, ); ``` ### With SendOptions ```dart theme={"system"} final result = await solanaWallet.provider.signAndSendTransaction( transaction: serializedTransaction, cluster: SolanaCluster.devNet, sendOptions: SendOptions( preflightCommitment: 'confirmed', maxRetries: 5, ), ); ``` ### Parameters **transaction** `Uint8List` *required* The serialized transaction message bytes to sign and send. **cluster** `SolanaCluster?` The Solana network cluster. Defaults to `SolanaCluster.MainNet`. **rpcUrl** `String?` Optional custom RPC URL. If not provided, uses the default RPC for the specified cluster. **sendOptions** `SendOptions?` Optional configuration for transaction submission. **child attributes** **skipPreflight** `bool?` Whether to skip preflight transaction checks. Defaults to `false`. **preflightCommitment** `String?` Commitment level for preflight checks (e.g., `"confirmed"`, `"finalized"`). **maxRetries** `int?` Maximum number of retry attempts for sending the transaction. **minContextSlot** `int?` Minimum slot number at which the transaction can be evaluated. > [!NOTE] > > The `cluster`, `rpcUrl`, and `sendOptions` parameters are only valid for apps using [on-device execution](/security/wallet-infrastructure/advanced/user-device). ### Returns **result** `Result\` A Result that, when successful, contains the transaction hash. **NodeJS** Use the `signAndSendTransaction` method on the Solana interface to send a transaction with a Solana wallet. ### Usage ```js theme={"system"} import { PublicKey, SystemProgram, VersionedTransaction, TransactionMessage, } from '@solana/web3.js'; const walletPublicKey = new PublicKey(wallet.address); const instruction = SystemProgram.transfer({ fromPubkey: walletPublicKey, toPubkey: new PublicKey(recipientAddress), lamports: amount, }); const message = new TransactionMessage({ payerKey: walletPublicKey, instructions: [instruction], recentBlockhash, }); const transaction = new VersionedTransaction(message.compileToV0Message()); const {hash} = await privy.wallets().solana().signAndSendTransaction('insert-wallet-id', { caip2: 'solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp', // Mainnet transaction: Buffer.from(transaction.serialize()).toString('base64'), sponsor: true, }); ``` ### Parameters and Returns Check out the [API reference](/api-reference/wallets/solana/sign-and-send-transaction) for more details. **Java** To send a transaction from your wallet, use the `signAndSendTransaction` method. It will sign your transaction, broadcast it to the network, and return the transaction hash to you. ### Usage ```java theme={"system"} try { String caip2 = "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp"; // Solana Mainnet // A base64 encoded serialized transaction to sign String transaction = "insert-base-64-encoded-serialized-transaction"; // Example: If wallet's owner is an authorization private key AuthorizationContext authorizationContext = AuthorizationContext.builder() .addAuthorizationPrivateKey("authorization-key") .build(); SolanaSignAndSendTransactionRpcResponseData response = privyClient.wallets().solana() .signAndSendTransaction( walletId, caip2, transaction, authorizationContext, true // sponsor ); String transactionHash = response.hash(); } catch (APIException e) { String errorBody = e.bodyAsString().orElse(e.getMessage()); System.err.println(errorBody); } catch (Exception e) { System.err.println(e.getMessage()); } ``` ### Parameters **transaction** `String` A base64 encoded serialized transaction to sign. **caip2** `String` The CAIP2 chain ID of the chain the transaction is being sent on. **sponsor** `Boolean` Optional parameter to enable gas sponsorship for this transaction. [Learn more.](/wallets/gas-and-asset-management/gas/overview) ### Returns The `SolanaSignAndSendTransactionRpcResponseData` object contains the following fields: **hash()** `String` The hash of the broadcasted transaction. **caip2()** `String` The CAIP2 chain ID of the chain the transaction was sent on. **transactionId()** `Optional\` The transaction ID of the broadcasted transaction. **REST API** To send a transaction make a POST request to ```bash theme={"system"} https://api.privy.io/v1/wallets//rpc ``` ### Usage ```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": "signAndSendTransaction", "caip2": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", "sponsor": true, "params": { "transaction": "insert-base-64-encoded-serialized-transaction", "encoding": "base64" } }' ``` A successful response will look like the following: ```json theme={"system"} { "method": "signAndSendTransaction", "data": { "hash": "insert-transaction-hash", "caip2": "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1" } } ``` ### Parameters **method** `'signAndSendTransaction'` *required* The RPC method executed with the wallet. **caip2** `string` *required* The CAIP2 chain ID to broadcast the transaction on. **params** `Object` *required* Parameters for the RPC method to execute with the wallet. **child attributes** **transaction** `string` *required* An encoded string serializing the transaction to be signed with the wallet. **encoding** `'base64'` *required* The encoding format for `params.transaction`. Currently, only `'base64'` is supported. **sponsor** `boolean` Optional parameter to enable gas sponsorship for this transaction. [Learn more.](/wallets/gas-and-asset-management/gas/overview) ### Returns **method** `'signAndSendTransaction'` The RPC method executed with the wallet. **data.hash** `string` The hash for the broadcasted transaction. **data.caip2** `string` The CAIP2 chain ID the transaction was submitted on. **Rust** Use the `sign_and_send_transaction` method on the Solana service to sign and broadcast a transaction. Use the [`solana_sdk`](https://docs.rs/solana-sdk/) crate to construct your transactions. ### Usage ```rust theme={"system"} use privy_rs::{AuthorizationContext, PrivyClient}; use solana_sdk::{pubkey::Pubkey, transaction::Transaction}; use base64::{engine::general_purpose::STANDARD, Engine}; use std::str::FromStr; let client = PrivyClient::new("app_id".to_string(), "app_secret".to_string())?; let solana_service = client.wallets().solana(); let auth_ctx = AuthorizationContext::new(); // Build transaction using solana_sdk and solana_system_interface let from_pubkey = Pubkey::from_str("DTeASnDsQ1z9Le77MjuiPH4MyqLDWa9vB6R3ZZKRd8d3")?; let to_pubkey = Pubkey::from_str("9NvE68JVWHHHGLp5NNELtM5fiBw6SXHrzqQJjUqaykC1")?; let lamports = 1000000; // 0.001 SOL let transaction = Transaction::new_with_payer( &[solana_system_interface::instruction::transfer( &from_pubkey, &to_pubkey, lamports, )], Some(&from_pubkey), ); // Serialize and encode transaction as base64 let serialized_transaction = STANDARD.encode(bincode::serialize(&transaction)?); let result = solana_service.sign_and_send_transaction( &wallet_id, "solana:EtWTRABZaYq6iMfeYKouRu166VU2xqa1", // Solana devnet &serialized_transaction, &auth_ctx, Some(true) // sponsor ).await?; println!("Transaction sent successfully"); ``` For transaction construction, use the [`solana_sdk`](https://docs.rs/solana-sdk/) crate for core transaction types and the [`solana_system_interface`](https://docs.rs/solana-system-interface/) crate for system instructions like transfers. ### Parameters and Returns See the Rust SDK documentation for detailed parameter and return types, including embedded examples: * [SolanaService::sign\_and\_send\_transaction](https://docs.rs/privy-rs/latest/privy_rs/solana/struct.SolanaService.html#method.sign_and_send_transaction) * [solana\_sdk crate documentation](https://docs.rs/solana-sdk/) * [solana\_system\_interface crate documentation](https://docs.rs/solana-system-interface/) For REST API details, see the [API reference](/api-reference/wallets/solana/sign-and-send-transaction). **Go** Use the `SignAndSendTransaction` method on the Solana service to sign and broadcast a transaction. ### Usage ```go theme={"system"} caip2 := "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp" // Solana Mainnet response, err := client.Wallets.Solana.SignAndSendTransaction( context.Background(), "wallet-id", caip2, privy.SolanaSignAndSendTransactionRpcInputParams{ Encoding: "base64", Transaction: "base64-encoded-serialized-transaction", }, ) if err != nil { log.Fatalf("failed to send transaction: %v", err) } fmt.Println("Transaction hash:", response.Hash) ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/solana/sign-and-send-transaction) for more details. **Ruby** Use the `rpc` method on the `wallets` service with the `signAndSendTransaction` method to sign and broadcast a transaction. ### Usage ```ruby theme={"system"} response = client.wallets.rpc( "wallet-id", wallet_rpc_request_body: { method: "signAndSendTransaction", chain_type: "solana", caip2: "solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", params: { transaction: "", encoding: "base64" } }, authorization_context: ctx ) puts(response.data.hash_) ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/solana/sign-and-send-transaction) for more details. **Python** Use the `sign_and_send_transaction` method on the Solana wallet service. Pass a base64-encoded serialized transaction or the serialized transaction bytes. ```python theme={"system"} response = client.wallets.solana.sign_and_send_transaction( wallet_id, serialized_transaction, caip2="solana:5eykt4UsFv8P8NJdTREpY1vzqKqZKvdp", ) transaction_hash = response.hash ``` > [!NOTE] > > For complete examples of sending SOL and SPL tokens using Privy's SDKs, check out the [sending a > SOL transaction recipe](/recipes/solana/send-sol) and the [sending SPL tokens > recipe](/recipes/solana/send-spl-tokens). > [!WARNING] > > 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). # Sign a Solana message Source: https://docs.privy.io/wallets/using-wallets/solana/sign-a-message Sign an arbitrary message with a Solana embedded wallet using Privy > [!NOTE] > > When using Privy's server-side SDKs to sign messages, you can use the authorization context to > automatically sign requests. Learn more about [signing on the > server](/controls/authorization-keys/using-owners/sign/signing-on-the-server). **React** Use the `signMessage` method exported from the `useSignMessage` hook to sign a message with a Solana wallet. ```tsx theme={"system"} signMessage: ({ message, options }: { message: Uint8Array; wallet: ConnectedStandardSolanaWallet; options?: { uiOptions?: SignMessageModalUIOptions; }; }) => Promise<{ signature: Uint8Array; }>; ``` ### Usage ```tsx theme={"system"} import {useWallets, useSignMessage} from '@privy-io/react-auth/solana'; import bs58 from 'bs58'; const {wallets} = useWallets(); const {signMessage} = useSignMessage(); const selectedWallet = wallets[0]; const message = 'Hello world'; const signatureUint8Array = ( await signMessage({ message: new TextEncoder().encode(message), wallet: selectedWallet, options: { uiOptions: { title: 'Sign this message' } } }) ).signature; const signature = bs58.encode(signatureUint8Array); ``` ### Parameters **message** `Uint8Array` *required* Message to be signed as a Uint8Array. **wallet** `string` The Solana wallet to use for signing the message. **options** `\{uiOptions?: SignMessageModalUIOptions\}` Additional options for signing the message. **uiOptions** **uiOptions** `SignMessageModalUIOptions` UI options to customize the transaction request modal. > [!TIP] > > To hide confirmation modals, set `options.uiOptions.showWalletUIs` to `false`. Learn more > about configuring modal prompts [here](/recipes/react/manage-wallet-UIs). ### Response **signature** `Uint8Array` The signature produced by the wallet. **signedMessage** `Uint8Array` The original message that was signed. **React Native** Request a message signature on the wallets Ethereum provider. ```tsx theme={"system"} import {useEmbeddedSolanaWallet} from '@privy-io/expo'; const wallet = useEmbeddedSolanaWallet(); const provider = await wallet.getProvider(); const message = 'Hello world'; const {signature} = await provider.request({ method: 'signMessage', params: {message} }); ``` ### Parameters **method** `'signMessage'` *required* The method for the wallet request. For signing messages, this is `'signMessage'`. **params** `Object` *required* **properties** **message** `string` *required* The string to sign with the wallet. If the message is a string, you should pass the string as the message directly. If the message is an array of bytes (Uint8Array), you should base64-encode the array as a string before passing it to message. **address** `string` The address of the wallet to sign the message with. ### Returns **signature** `string` The signature produced by the wallet. **Swift** Request a message signature on the wallet's Solana provider. ```swift theme={"system"} guard let user = privy.user else { // If user is null, user is not authenticated return } // Retrieve list of user's embedded Solana wallets let solanaWallets = user.embeddedSolanaWallets // Grab the desired wallet. Here, we retrieve the first wallet guard let wallet = solanaWallets.first else { // No SOL wallets return } // Sign a Base64 encoded message let signature = try await wallet.provider.signMessage(message: "SGVsbG8hIEkgYW0gdGhlIGJhc2U2NCBlbmNvZGVkIG1lc3NhZ2UgdG8gYmUgc2lnbmVkLg==") print("Result signature: \(signature)") ``` ### Parameters **message** `string` *required* The message to sign, as a base64-encoded string. ### Returns **signature** `string` The signature produced by the wallet. **Android** Use the `signMessage` method on the Solana wallet provider to sign a message with the wallet. ```kotlin theme={"system"} // Get current auth state val user = privy.user // check if user is authenticated if (user != null) { // Retrieve list of user's Solana wallet val solanaWallet = user.embeddedSolanaWallets.first() if (solanaWallet != null) { // Sign a message val result = solanaWallet.provider.signMessage("SGVsbG8hIEkgYW0gdGhlIGJhc2U2NCBlbmNvZGVkIG1lc3NhZ2UgdG8gYmUgc2lnbmVkLg==") } } ``` ### Parameters **message** `string` *required* The message to sign, as a base64-encoded string. ### Returns **signature** `string` The signature produced by the wallet. **Unity** ### Usage ```csharp theme={"system"} try { IPrivyUser privyUser = await PrivyManager.Instance.GetUser(); IEmbeddedSolanaWallet embeddedWallet = privyUser.EmbeddedSolanaWallets[0]; IEmbeddedSolanaWalletProvider provider = embeddedWallet.EmbeddedSolanaWalletProvider; string signature = await provider.SignMessage("A message to sign"); Debug.Log(signature); } 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}"); } ``` ### Parameters **message** `string` *required* The base64 encoded bytes of the message or transaction to sign. ### Returns **signature** `string` The base64 encoded signature of the message, produced by the wallet. **Flutter** Use the `signMessage` method on the Solana wallet provider to sign a message with the wallet. ```dart theme={"system"} // Get the current user final user = privy.user; // Check if the user is authenticated if (user != null) { // Retrieve the user's Solana wallet final solanaWallet = user.embeddedSolanaWallets.first; if (solanaWallet != null) { // Sign a message final result = solanaWallet.provider.signMessage("SGVsbG8hIEkgYW0gdGhlIGJhc2U2NCBlbmNvZGVkIG1lc3NhZ2UgdG8gYmUgc2lnbmVkLg==") } } ``` ### Parameters **message** `string` *required* The message to sign, as a base64-encoded string. ### Returns **signature** `string` The signature produced by the wallet. **NodeJS** Use the `signMessage` method on the solana interface to sign a message with an Solana wallet. ### Usage ```tsx theme={"system"} const {signature, encoding} = await privy.wallets().solana().signMessage('insert-wallet-id', { message: 'Hello world' }); ``` ### Parameters and Returns Check out the [API reference](/api-reference/wallets/solana/sign-message) for more details. **Java** To sign a message from your wallet, use the `signMessage` method. It will sign your message, and return the signature to you. ### Usage ```java theme={"system"} try { // Base64 encoded message String message = "SGVsbG8hIEkgYW0gdGhlIGJhc2U2NCBlbmNvZGVkIG1lc3NhZ2UgdG8gYmUgc2lnbmVkLg"; // Example: If wallet's owner is an authorization private key AuthorizationContext authorizationContext = AuthorizationContext.builder() .addAuthorizationPrivateKey("authorization-key") .build(); SolanaSignMessageRpcResponseData response = privyClient .wallets() .solana() .signMessage( walletId, message, authorizationContext ); String signature = response.signature(); } catch (APIException e) { String errorBody = e.bodyAsString().orElse(e.getMessage()); System.err.println(errorBody); } catch (Exception e) { System.err.println(e.getMessage()); } ``` ### Parameters When signing a message with your solana wallet, you can sign over a base64 string encoding. **message** `String` The base64 encoded message to sign over. ### Returns The `SolanaSignMessageRpcResponseData` object contains a `signature()` field **signature()** `String` The signature produced by the wallet. **Rust** Use the `sign_message` method on the Solana service to sign a message with a Solana wallet. ### Usage ```rust theme={"system"} use privy_rs::{AuthorizationContext, PrivyClient}; let client = PrivyClient::new("app_id".to_string(), "app_secret".to_string())?; let solana_service = client.wallets().solana(); let auth_ctx = AuthorizationContext::new(); // Base64 encode your message first let message = base64::encode("Hello, Solana!"); let signature = solana_service .sign_message( &wallet_id, &message, &auth_ctx, Some("unique-request-id-456"), ) .await?; println!("Message signed successfully"); ``` ### Parameters and Returns See the Rust SDK documentation for detailed parameter and return types, including embedded examples: * [SolanaService::sign\_message](https://docs.rs/privy-rs/latest/privy_rs/solana/struct.SolanaService.html#method.sign_message) For REST API details, see the [API reference](/api-reference/wallets/solana/sign-message). **Go** Use the `SignMessage` method on the Solana service to sign a message with a Solana wallet. The message must be a base64-encoded string. ### Usage ```go theme={"system"} response, err := client.Wallets.Solana.SignMessage( context.Background(), "wallet-id", "SGVsbG8hIEkgYW0gdGhlIGJhc2U2NCBlbmNvZGVkIG1lc3NhZ2UgdG8gYmUgc2lnbmVkLg", ) if err != nil { log.Fatalf("failed to sign message: %v", err) } fmt.Println("Signature:", response.Signature) ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/solana/sign-message) for more details. **Ruby** Use the `rpc` method on the `wallets` service with the `signMessage` method to sign a message with a Solana wallet. The message must be a base64-encoded string. ### Usage ```ruby theme={"system"} require "base64" message = "Hello, Privy!" 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"} }, authorization_context: ctx ) puts(response.data.signature) ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/solana/sign-message) for more details. **REST API** To sign a message make a POST request to ```bash theme={"system"} https://api.privy.io/v1/wallets//rpc ``` ### Parameters **method** `'signMessage'` *required* RPC method to execute with the wallet. **params** `Object` *required* Parameters for the RPC method to execute with the wallet. **properties** **message** `string` *required* The message to sign with the wallet. If the message to sign is raw bytes, you must serialize the message as a base64 string. **encoding** `'base64'` *required* The encoding format for `message`. Currently, only `'base64'` is supported for Solana. ### Returns **method** `'signMessage'` The RPC method executed with the wallet. **data** `Object` Outputs for the RPC method executed with the wallet. **properties** **signature** `string` An encoded string serializing the signature produced by the user's wallet. **encoding** `'base64'` The encoding format for the returned `signature`. Currently, only `'base64'` is supported for Solana. ### Usage ```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 '{ "chain_type": "solana", "method": "signMessage", "params": { "message": "Hello, Solana.", "encoding": "base64" } }' ``` **Python** Use the `sign_message` method on the Solana wallet service. The SDK base64-encodes byte input. ```python theme={"system"} response = client.wallets.solana.sign_message( wallet_id, b"Hello, Privy!", ) signature = response.signature ``` # Sign a Solana transaction Source: https://docs.privy.io/wallets/using-wallets/solana/sign-a-transaction Sign a Solana transaction without broadcasting it using Privy > [!NOTE] > > When using Privy's server-side SDKs to sign transactions, you can use the authorization context to > automatically sign requests. Learn more about [signing on the > server](/controls/authorization-keys/using-owners/sign/signing-on-the-server). **React** Use the `signTransaction` method exported from the `useSignTransaction` hook to sign a transaction with a Solana wallet. ```tsx theme={"system"} signTransaction: (input: { transaction: SupportedSolanaTransaction; wallet: ConnectedStandardSolanaWallet; options?: SolanaSignTransactionOptions & {uiOptions?: SendTransactionModalUIOptions}; }) => Promise<{ signedTransaction: Uint8Array; }>; ``` ### Usage ```tsx theme={"system"} import {useSignTransaction, useWallets} from '@privy-io/react-auth/solana'; import {pipe, createSolanaRpc, getTransactionEncoder, createTransactionMessage} from '@solana/kit'; // Inside your component const {signTransaction} = useSignTransaction(); const {wallets} = useWallets(); const selectedWallet = wallets[0]; // Configure your connection to point to the correct Solana network const {getLatestBlockhash} = createSolanaRpc('https://api.mainnet-beta.solana.com'); // Get the latest blockhash const {value} = await getLatestBlockhash().send(); // Create your transaction (either legacy Transaction or VersionedTransaction) const transaction = pipe( createTransactionMessage({version: 0}), // Set the message fee payer... // Set recent blockhash... // Add your instructions to the transaction... // Finally encode the transaction (tx) => new Uint8Array(getTransactionEncoder().encode(tx)) ); // Sign the transaction const signedTransaction = await signTransaction({ transaction: transaction, wallet: selectedWallet }); console.log('Transaction signed successfully'); ``` ### Parameters **transaction** `Uint8Array` *required* The encoded transaction to be signed. **wallet** `ConnectedStandardSolanaWallet` *required* The Solana wallet to use for signing the transaction. **chain** `SolanaChain` Type of all Solana chains supported by Privy. **options** `SolanaSignTransactionOptions & \{uiOptions?: SendTransactionModalUIOptions\}` Additional options for signing the transaction. **uiOptions** **uiOptions** `SendTransactionModalUIOptions` UI options to customize the transaction request modal. > [!TIP] > > To hide confirmation modals, set `options.uiOptions.showWalletUIs` to `false`. Learn more > about configuring modal prompts [here](/recipes/react/manage-wallet-UIs). ### Response **signedTransaction** `Uint8Array` The signed transaction that can be sent to the network. ### External wallets that only support signing (not sign-and-send) > [!WARNING] > > 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, the `signedTransaction` returned by > `signTransaction` is a **raw 64-byte Ed25519 signature**, not a fully signed serialized > transaction. > > Passing this value directly to `sendTransaction` will result in a deserialization error, because > the network expects a complete wire transaction (message + signatures), not a bare signature. To send a transaction signed by one of these wallets, attach the returned signature to the original compiled transaction before encoding and submitting it. Below is a complete example using `@solana/kit`: ```tsx theme={"system"} import { pipe, createTransactionMessage, setTransactionMessageFeePayer, setTransactionMessageLifetimeUsingBlockhash, appendTransactionMessageInstruction, compileTransaction, createSolanaRpc, getTransactionEncoder, getBase64EncodedWireTransaction, address, signatureBytes, type Transaction } from '@solana/kit'; import {useSignTransaction, useWallets} from '@privy-io/react-auth/solana'; const {signTransaction} = useSignTransaction(); const {wallets} = useWallets(); const wallet = wallets[0]; const rpc = createSolanaRpc('YOUR_SOLANA_RPC_URL'); const {value: latestBlockhash} = await rpc.getLatestBlockhash().send(); // 1. Build and compile the transaction const compiledTransaction = pipe( createTransactionMessage({version: 0}), (m) => setTransactionMessageFeePayer(address(wallet.address), m), (m) => setTransactionMessageLifetimeUsingBlockhash(latestBlockhash, m), (m) => appendTransactionMessageInstruction(/* your instruction */, m), (m) => compileTransaction(m) ); // 2. Encode the compiled transaction for signing const encodedTransaction = new Uint8Array( getTransactionEncoder().encode(compiledTransaction) ); // 3. Sign via Privy — for sign-only wallets, this returns a 64-byte signature const {signedTransaction: signature} = await signTransaction({ transaction: encodedTransaction, wallet }); // 4. Attach the signature to the compiled transaction const signedMessage: Transaction = { ...compiledTransaction, signatures: { [address(wallet.address)]: signatureBytes(signature) } }; // 5. Encode as a base64 wire transaction and send await rpc .sendTransaction(getBase64EncodedWireTransaction(signedMessage), { preflightCommitment: 'confirmed', encoding: 'base64' }) .send(); ``` > [!TIP] > > A raw Ed25519 signature is exactly 64 bytes. A serialized Solana transaction is always longer than > 64 bytes. Checking the byte length of the `signedTransaction` response can help determine which > type the wallet returned. **React Native** To sign a transaction from a wallet using the React Native SDK, use the `request` method from the wallet's provider: ```javascript theme={"system"} request: (request: { method: 'signTransaction', params: { transaction: Transaction | VersionedTransaction, } }) => Promise<{ signedTransaction: Transaction }> ``` > [!NOTE] > > The Expo SDK does not support built-in UIs for signing transactions. > The `signTransaction` method gives you complete control over the experience and UI. ### Usage ```javascript theme={"system"} import {useEmbeddedSolanaWallet} from '@privy-io/expo'; const { wallets } = useEmbeddedSolanaWallet(); const wallet = wallets[0]; // Get the provider const provider = await wallet.getProvider(); // Create your transaction (either legacy Transaction or VersionedTransaction) // transaction = ... // Sign the transaction const { signedTransaction } = await provider.request({ method: 'signTransaction', params: { transaction: transaction, }, }); console.log("Transaction signed successfully", signedTransaction); ``` ### Parameters **method** `'signTransaction'` *required* The RPC method executed with the wallet. **params** `Object` *required* Parameters for the transaction. **child attributes** **transaction** `Transaction | VersionedTransaction` *required* The transaction to sign. This can be either a legacy Transaction or a VersionedTransaction object from [@solana/web3.js](https://solana-foundation.github.io/solana-web3.js/classes/Transaction.html). ### Returns **signedTransaction** `Transaction` The signature of the transaction. **Swift** Sign a Solana transaction without submitting it to the network. ```swift theme={"system"} public protocol EmbeddedSolanaWalletProvider { func signTransaction(transaction: Data) async throws -> String } ``` ### Usage ```swift theme={"system"} import SolanaSwift // Get the provider for wallet (assumes wallet is already obtained) let provider = wallet.provider // Create a Solana RPC client let solana = JSONRPCAPIClient(endpoint: URL(string: SolanaCluster.mainnet.rpcUrl)!) // Build the transaction using your preferred method // In this example, we're using the SolanaSwift SDK let latestBlockhash = try await solana.getLatestBlockhash() let walletPK = try PublicKey(string: wallet.address) var tx = Transaction() tx.instructions.append( SystemProgram.transferInstruction( from: walletPK, to: try PublicKey(string: "9NvE68JVWHHHGLp5NNELtM5fiBw6SXHrzqQJjUqaykC1"), lamports: 100000000000000 ) ) tx.recentBlockhash = latestBlockhash tx.feePayer = walletPK // Sign using the Privy Embedded Wallet // Get back the signed transaction serialized as a base64-encoded string let signedTxBase64 = try await provider.signTransaction(transaction: tx.serialize()) let signedTx = try Transaction.from(data: Data(base64Encoded: signedTxBase64)!) ``` ### Parameters **transaction** `Data` *required* The serialized transaction to sign. ### Returns **signedTransaction** `String` The base64-encoded signed transaction. **Android** Use the `signTransaction` method on the Solana wallet provider to sign a transaction and then submit it to the network. ```kotlin theme={"system"} public suspend fun signTransaction( transaction: ByteArray ): Result ``` ### Usage ```kotlin theme={"system"} // Retrieve user's Solana wallet (assumes wallet is already obtained) val solanaWallet = user.embeddedSolanaWallets.first() // Create a Solana RPC client with cluster RPC URL val connection = Connection(SolanaCluster.MainNet.rpcUrl()) // Build the transaction val walletPublicKey = PublicKey(solanaWallet.address) val instruction = SystemProgram.transfer( fromPubkey = walletPublicKey, toPubkey = PublicKey(recipientAddress), lamports = amount ) // Get recent blockhash val recentBlockhash = connection.getLatestBlockhash() // Create transaction val transaction = Transaction() transaction.add(instruction) transaction.recentBlockhash = recentBlockhash transaction.feePayer = walletPublicKey // Serialize transaction to byte array val serializedTransaction = transaction.serialize() // Sign the transaction (returns base64-encoded signed transaction) val signResult = solanaWallet.provider.signTransaction(serializedTransaction) signResult.onSuccess { signedTransactionBase64 -> // Decode the signed transaction and submit to network println("Transaction signed with signature: $signature") }.onFailure { error -> println("Failed to sign transaction: ${error.message}") } ``` ### Parameters **transaction** `ByteArray` *required* The serialized transaction to sign. ### Returns **result** `Result\` A Result that, when successful, contains the base64-encoded signed transaction. **Flutter** Use the `signTransaction` method on the Solana wallet provider to sign a transaction and then submit it to the network. ```dart theme={"system"} Future> signTransaction(Uint8List transaction); ``` ### Usage ```dart theme={"system"} // Retrieve the user's Solana wallet (assumes wallet is already obtained) final solanaWallet = user.embeddedSolanaWallets.first; // Create a Solana RPC client final connection = Connection('https://api.mainnet-beta.solana.com'); // Build the transaction final walletPublicKey = PublicKey(solanaWallet.address); final instruction = SystemProgram.transfer( fromPubkey: walletPublicKey, toPubkey: PublicKey(recipientAddress), lamports: amount ); // Get recent blockhash final recentBlockhash = await connection.getLatestBlockhash(); // Create transaction final transaction = Transaction(); transaction.add(instruction); transaction.recentBlockhash = recentBlockhash; transaction.feePayer = walletPublicKey; // Serialize transaction to bytes final serializedTransaction = transaction.serializeMessage(); // Sign the transaction final result = await solanaWallet.provider.signTransaction(serializedTransaction); ``` ### Parameters **transaction** `Uint8List` *required* The serialized transaction message bytes to sign. ### Returns **result** `Result\` A Result that, when successful, contains the base64-encoded signature. **NodeJS** Use the `signTransaction` method on the Solana interface to sign a transaction with an Solana wallet. ### Usage ```js theme={"system"} import { clusterApiUrl, Connection, LAMPORTS_PER_SOL, PublicKey, SystemProgram, Transaction, VersionedTransaction, TransactionMessage } from '@solana/web3.js'; const walletPublicKey = new PublicKey(wallet.address); const connection = new Connection(clusterApiUrl('devnet')); const instruction = SystemProgram.transfer({ fromPubkey: walletPublicKey, toPubkey: new PublicKey(address), lamports: value * LAMPORTS_PER_SOL }); const {blockhash: recentBlockhash} = await connection.getLatestBlockhash(); const message = new TransactionMessage({ payerKey: walletPublicKey, // Replace with your desired instructions instructions: [instruction], recentBlockhash }); const yourSolanaTransaction = new VersionedTransaction(message.compileToV0Message()); // Get the signed transaction object from the response const {signedTransaction} = await privy.wallets().solana().signTransaction(wallet.id, { transaction: Buffer.from(yourSolanaTransaction.serialize()).toString('base64') }); ``` ### Parameters and Returns Check out the [API reference](/api-reference/wallets/solana/sign-transaction) for more details. **Java** To sign a transaction from your wallet, use the `signTransaction` method. It will sign your transaction, and return the signed transaction to you. ### Usage ```java theme={"system"} try { // A base64 encoded serialized transaction to sign String transaction = "insert-base-64-encoded-serialized-transaction"; // Example: If wallet's owner is an authorization private key AuthorizationContext authorizationContext = AuthorizationContext.builder() .addAuthorizationPrivateKey("authorization-key") .build(); SolanaSignTransactionRpcResponseData response = privyClient.wallets().solana() .signTransaction( transaction, authorizationContext ); String signedTransaction = response.signedTransaction(); } catch (APIException e) { String errorBody = e.bodyAsString().orElse(e.getMessage()); System.err.println(errorBody); } catch (Exception e) { System.err.println(e.getMessage()); } ``` ### Parameters **transaction** `String` A base64 encoded serialized transaction to sign. ### Returns The `SolanaSignTransactionRpcResponseData` object contains the following fields: **signedTransaction()** `String` The signed transaction. **Rust** Use the `sign_transaction` method on the Solana service to sign a transaction. ### Usage ```rust theme={"system"} use privy_rs::{AuthorizationContext, PrivyClient}; let client = PrivyClient::new("app_id".to_string(), "app_secret".to_string())?; let solana_service = client.wallets().solana(); let auth_ctx = AuthorizationContext::new(); // Base64-encoded Solana transaction (example) let transaction = "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAAEDArczbMia1tLmq7zz4DinMNN0pJ1JtLdqIJPUw3YrGCzYAMHBsgN27lcgB6H2WQvFgyZuJYHa46puOQo9yQ8CVQbd9uHXZaGT2cvhRs7reawctIXtX1s3kTqM9YV+/wCp20C7Wj2aiuk5TReAXo+VTVg8QTHjs0UjNMMKCvpzZ+ABAgEBARU="; let signed_tx = solana_service.sign_transaction( &wallet_id, transaction, &auth_ctx, None ).await?; println!("Transaction signed successfully"); ``` > [!TIP] > > For transaction construction, you'll typically use the > [@solana/web3.js](https://solana-foundation.github.io/solana-web3.js/) library or > [solana\_sdk](https://docs.rs/solana-sdk/) to create your transaction, serialize it to base64, and > then sign it with Privy. ### Parameters and Returns See the Rust SDK documentation for detailed parameter and return types, including embedded examples: * [SolanaService::sign\_transaction](https://docs.rs/privy-rs/latest/privy_rs/solana/struct.SolanaService.html#method.sign_transaction) For REST API details, see the [API reference](/api-reference/wallets/solana/sign-transaction). **Go** Use the `SignTransaction` method on the Solana service to sign a transaction with a Solana wallet. ### Usage ```go theme={"system"} response, err := client.Wallets.Solana.SignTransaction( context.Background(), "wallet-id", privy.SolanaSignTransactionRpcInputParams{ Encoding: "base64", Transaction: "base64-encoded-serialized-transaction", }, ) if err != nil { log.Fatalf("failed to sign transaction: %v", err) } fmt.Println("Signed transaction:", response.SignedTransaction) ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/solana/sign-transaction) for more details. **Ruby** Use the `rpc` method on the `wallets` service with the `signTransaction` method to sign a transaction with a Solana wallet. ### Usage ```ruby theme={"system"} response = client.wallets.rpc( "wallet-id", wallet_rpc_request_body: { method: "signTransaction", chain_type: "solana", params: { transaction: "", encoding: "base64" } }, authorization_context: ctx ) puts(response.data.signed_transaction) ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/solana/sign-transaction) for more details. **REST API** To sign a transaction make a `POST` request to ```bash theme={"system"} https://api.privy.io/v1/wallets//rpc ``` ### Usage ```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": "signTransaction", "params": { "transaction": "insert-base-64-encoded-serialized-transaction", "encoding": "base64" } }' ``` A successful response will look like the following: ```json theme={"system"} { "method": "signTransaction", "data": { "signed_transaction": "base64-encoded-serialized-signed-transaction", "encoding": "base64" } } ``` ### Parameters **method** `'signTransaction'` *required* The RPC method executed with the wallet. **params** `Object` *required* **transaction** `string` *required* The transaction to sign with the wallet. If the transaction to sign is raw bytes, you must serialize the transaction as a base64 string. **encoding** `'base64'` *required* The encoding format for `transaction`. Currently, only `'base64'` is supported for Solana. ### Returns **method** `'signTransaction'` The RPC method executed with the wallet. **data.signed\_transaction** `string` The signed transaction. **data.encoding** `'base64'` The encoding format for the signed transaction. Currently, only `'base64'` is supported for Ethereum. **Python** Use the `sign_transaction` method on the Solana wallet service. Pass a base64-encoded serialized transaction or the serialized transaction bytes. ```python theme={"system"} response = client.wallets.solana.sign_transaction( wallet_id, serialized_transaction, ) signed_transaction = response.signed_transaction ``` # Integrating with @solana/web3.js Source: https://docs.privy.io/wallets/using-wallets/solana/web3-integrations **React** 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 }) ) }) ); ``` # Send a Tempo transaction Source: https://docs.privy.io/wallets/using-wallets/tempo/send-a-transaction Send a Tempo transaction using Privy embedded wallets Tempo transactions enable your app to take advantage of Tempo's native features: * Configurable fee tokens: set `feeToken` to pay fees with a supported TIP-20 token. [Learn more](/recipes/tempo/send-transactions#3-send-custom-transactions-on-tempo) * Fee sponsorship: set `sponsor: true` to use [Privy gas sponsorship](/recipes/tempo/send-transactions#using-gas-sponsorship) on Tempo * Concurrent transactions: set `nonceKey` to send parallel transactions. Most apps can omit this since Privy uses [expiring nonces](https://tempo.xyz/developers/docs/protocol/tips/tip-1009) by default * Batched calls: batch multiple calls in the transaction's `calls` array * Scheduled transactions: set `validBefore` or `validAfter` to control when the network can include the transaction View the [Tempo transaction recipe](/recipes/tempo/send-transactions#using-privy-sdk-or-rest-api) for full examples. > [!NOTE] > > Unlike standard EVM transactions, Tempo transactions use a `calls` array instead of top-level `to` > and `data` fields. This enables multiple contract calls to execute atomically in one transaction. **React** ```tsx theme={"system"} import {useWallets} from '@privy-io/react-auth'; import {useSendTransaction} from '@privy-io/react-auth/tempo'; const {wallets} = useWallets(); const {sendTransaction} = useSendTransaction(); const {hash} = await sendTransaction({ transaction: { type: 118, chainId: 4217, // use 42431 for Tempo Moderato (testnet) feeToken: '0x20c000000000000000000000b9537d11c60e8b50', // pay Tempo fees in USDC-e calls: [{to: '0xRecipientAddress', data: '0x...'}] }, wallet: wallets[0] }); ``` ### Parameters **transaction.type** `118` *required* Transaction type for Tempo's native envelope. **transaction.chainId** `number` *required* The Tempo chain ID. Use `4217` for mainnet or `42431` for Moderato (testnet). **transaction.calls** `TempoCall[]` *required* An array of calls to execute atomically. At least one call is required. Each call has `to` (required), `data`, and `value`. **transaction.feeToken** `Hex` 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. **transaction.nonceKey** `bigint` 2D nonce key. Use `0` for the protocol-managed sequential nonce, or a non-zero value to use a parallel user nonce slot, enabling concurrent transactions. **transaction.nonce** `bigint` Sequential nonce within the nonce key slot. Populated automatically if omitted. **transaction.validBefore** `bigint` Transaction expiration as a Unix timestamp in seconds. The transaction will not be included after this time. **transaction.validAfter** `bigint` Earliest inclusion time as a Unix timestamp in seconds. The transaction will not be included before this time. **transaction.gasLimit** `bigint` Gas limit for the transaction. Estimated automatically if omitted. **transaction.maxFeePerGas** `bigint` Maximum total fee per gas unit in wei. Estimated automatically if omitted. **transaction.maxPriorityFeePerGas** `bigint` Maximum priority fee per gas unit in wei. Estimated automatically if omitted. **wallet** `ConnectedWallet` *required* The wallet to send the transaction from. **options.sponsor** `boolean` Optional parameter to enable gas sponsorship for this Tempo transaction. [Learn more.](/wallets/gas-and-asset-management/gas/overview) ### Returns **hash** `HexString` The hash for the broadcasted transaction. **NodeJS** ```typescript {skip-check} theme={"system"} const {hash} = await privy .wallets() .ethereum() .sendTransaction(walletId, { caip2: 'eip155:4217', // use 'eip155:42431' for Tempo Moderato (testnet) params: { transaction: { type: 118, fee_token: '0x20c000000000000000000000b9537d11c60e8b50', calls: [{to: '0xRecipientAddress', data: '0x...'}] } } }); ``` ### Parameters and Returns View the [API reference](/api-reference/wallets/ethereum/eth-send-transaction) for details. **Python** ```python theme={"system"} response = client.wallets.ethereum.send_transaction( wallet_id, caip2="eip155:4217", # Use eip155:42431 for Tempo Moderato (testnet) params={ "transaction": { "type": 118, "fee_token": "insert-fee-token-address", "calls": [{"to": "0xRecipientAddress", "data": "0x"}], } }, ) transaction_hash = response.hash ``` ### Parameters and Returns View the [API reference](/api-reference/wallets/ethereum/eth-send-transaction) for details. **REST API** ```bash theme={"system"} curl --request POST https://api.privy.io/v1/wallets//rpc \ -u ":" \ -H "privy-app-id: " \ -H "Content-Type: application/json" \ -d '{ "method": "eth_sendTransaction", "caip2": "eip155:4217", "params": { "transaction": { "type": 118, "fee_token": "0x20c000000000000000000000b9537d11c60e8b50", "calls": [{"to": "0xRecipientAddress", "data": "0x..."}] } } }' ``` ### Parameters and Returns View the [API reference](/api-reference/wallets/ethereum/eth-send-transaction) for details. For full examples including viem `tempoActions` integration, view the [Tempo transaction recipe](/recipes/tempo/send-transactions#3-send-custom-transactions-on-tempo). # Sign a Tempo transaction Source: https://docs.privy.io/wallets/using-wallets/tempo/sign-a-transaction Sign a Tempo transaction without broadcasting it using Privy embedded wallets Sign a Tempo transaction without broadcasting it—useful for relaying through a custom broadcaster or inspecting the signed bytes before submission. > [!NOTE] > > Unlike standard EVM transactions, Tempo transactions use a `calls` array instead of a single > `to`/`data`/`value`. This enables multiple contract calls to execute atomically in one > transaction. View the [Tempo transaction > recipe](/recipes/tempo/send-transactions#using-privy-sdk-or-rest-api) for full examples. **React** ```tsx theme={"system"} import {useWallets} from '@privy-io/react-auth'; import {useSignTransaction} from '@privy-io/react-auth/tempo'; const {wallets} = useWallets(); const {signTransaction} = useSignTransaction(); const {signedTransaction} = await signTransaction({ transaction: { type: 118, chainId: 4217, // use 42431 for Tempo Moderato (testnet) feeToken: '0x20c000000000000000000000b9537d11c60e8b50', // pay Tempo fees in USDC-e calls: [{to: '0xRecipientAddress', data: '0x...'}] }, wallet: wallets[0] }); ``` ### Parameters **transaction.type** `118` *required* Transaction type for Tempo's native envelope. **transaction.chainId** `number` *required* The Tempo chain ID. Use `4217` for mainnet or `42431` for Moderato (testnet). **transaction.calls** `TempoCall[]` *required* An array of calls to execute atomically. At least one call is required. Each call has `to` (required), `data`, and `value`. **transaction.feeToken** `Hex` 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. **transaction.nonceKey** `bigint` 2D nonce key. Use `0` for the protocol-managed sequential nonce, or a non-zero value to use a parallel user nonce slot, enabling concurrent transactions. **transaction.nonce** `bigint` Sequential nonce within the nonce key slot. Populated automatically if omitted. **transaction.validBefore** `bigint` Transaction expiration as a Unix timestamp in seconds. The transaction will not be included after this time. **transaction.validAfter** `bigint` Earliest inclusion time as a Unix timestamp in seconds. The transaction will not be included before this time. **transaction.gasLimit** `bigint` Gas limit for the transaction. Estimated automatically if omitted. **transaction.maxFeePerGas** `bigint` Maximum total fee per gas unit in wei. Estimated automatically if omitted. **transaction.maxPriorityFeePerGas** `bigint` Maximum priority fee per gas unit in wei. Estimated automatically if omitted. **wallet** `ConnectedWallet` *required* The wallet to sign the transaction with. > [!TIP] > > Most apps should omit `nonceKey`. It defaults to `0`, using Tempo's sequential protocol nonce. > Only set a non-zero value if you need concurrent transactions in parallel nonce slots. [Learn > more.](/controls/policies/example-policies/tempo#restrict-nonce-key) > [!TIP] > > When building a relay flow, set `validBefore` to enforce a submission deadline—signed bytes > submitted after that timestamp will be rejected by the network. [Learn > more.](/controls/policies/example-policies/tempo#time-bound-a-validity-window) ### Returns **signedTransaction** `HexString` The signed transaction, ready to broadcast. **REST API** ```bash theme={"system"} curl --request POST https://api.privy.io/v1/wallets//rpc \ -u ":" \ -H "privy-app-id: " \ -H "Content-Type: application/json" \ -d '{ "method": "eth_signTransaction", "caip2": "eip155:4217", "params": { "transaction": { "type": 118, "fee_token": "0x20c000000000000000000000b9537d11c60e8b50", "calls": [{"to": "0xRecipientAddress", "data": "0x..."}] } } }' ``` ### Parameters and Returns View the [API reference](/api-reference/wallets/ethereum/eth-sign-transaction) for details. For full examples, view the [Tempo transaction recipe](/recipes/tempo/send-transactions#3-send-custom-transactions-on-tempo). # Send a Tron transaction Source: https://docs.privy.io/wallets/using-wallets/tron/send-a-transaction Sign and broadcast a Tron transaction in one call using Privy Sign and broadcast a Tron transaction with Privy. Privy automatically fetches fresh block reference fields (`ref_block_bytes`, `ref_block_hash`, `expiration`) if you omit them. > [!NOTE] > > To sign without broadcasting — for example when using a gas-sponsoring RPC like > [Transatron](/recipes/tron/transatron) — use > [`tron_signTransaction`](/api-reference/wallets/tron/tron-sign-transaction) instead. **REST API** To send a transaction, make a `POST` request to ```bash theme={"system"} theme={"system"} https://api.privy.io/v1/wallets//rpc ``` All addresses in `raw_data.contract` must be **41-prefixed hex** — use `TronWeb.address.toHex()` to convert from base58check. ### TRX transfer ```bash theme={"system"} 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": "tron_sendTransaction", "caip2": "tron:mainnet", "params": { "raw_data": { "contract": [ { "type": "TransferContract", "owner_address": "41a614f803b6fd780986a42c78ec9c7f77e6ded13c", "to_address": "41e552f6487585c2b58bc2c9bb4492bc1f17132cd0", "amount": 1000000 } ] } } }' ``` ### TRC-20 transfer ```bash theme={"system"} 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": "tron_sendTransaction", "caip2": "tron:mainnet", "params": { "raw_data": { "contract": [ { "type": "TriggerSmartContract", "owner_address": "41a614f803b6fd780986a42c78ec9c7f77e6ded13c", "contract_address": "41a614f803b6fd780986a42c78ec9c7f77e6ded13c", "data": "a9059cbb000000000000000041e552f6487585c2b58bc2c9bb4492bc1f17132cd000000000000000000000000000000000000000000000000000000000000f4240", "call_value": 0 } ], "fee_limit": 100000000 } } }' ``` A successful response looks like: ```json theme={"system"} theme={"system"} { "method": "tron_sendTransaction", "data": { "hash": "8a3f2b1c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1", "caip2": "tron:mainnet", "transaction_id": "8a3f2b1c4d5e6f7a8b9c0d1e2f3a4b5c6d7e8f9a0b1c2d3e4f5a6b7c8d9e0f1" } } ``` ### Parameters **method** `string` *required* Must be `"tron_sendTransaction"`. **caip2** `string` Tron CAIP-2 chain. Defaults to `tron:mainnet`. Use `tron:nile` for the Nile testnet. **params.raw\_data.contract** `array` *required* Array of exactly one contract — either `TransferContract` (native TRX) or `TriggerSmartContract` (TRC-20 / smart contract call). All addresses must be 41-prefixed hex. **params.raw\_data.fee\_limit** `number` Maximum energy fee in sun. Required for `TriggerSmartContract`. `100_000_000` (100 TRX) is a safe default for standard TRC-20 transfers. **params.raw\_data.ref\_block\_bytes** `string` 4 hex-character block reference. Optional — Privy fetches fresh values if omitted. **params.raw\_data.ref\_block\_hash** `string` 16 hex-character block reference hash. Optional — Privy fetches fresh values if omitted. **params.raw\_data.expiration** `number` Expiration timestamp in Unix milliseconds. Optional — Privy sets `now + 60s` if omitted. **params.reference\_id** `string` Optional idempotency key (max 64 characters). Re-submitting the same `reference_id` returns the cached response. ### Returns **data.hash** `string` Tron transaction ID — SHA-256 of the raw transaction bytes, lowercase hex, no `0x` prefix. **data.caip2** `string` The CAIP-2 chain the transaction was broadcast on. **data.transaction\_id** `string` Privy internal transaction ID. Check out the [API reference](/api-reference/wallets/tron/tron-send-transaction) for more details. **NodeJS** Use the typed `wallets().tron().sendTransaction()` method to sign and broadcast a transaction in one call. ### TRX transfer ```typescript theme={"system"} theme={"system"} import {PrivyClient} from '@privy-io/node'; import {TronWeb} from 'tronweb'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID, appSecret: process.env.PRIVY_APP_SECRET }); const {hash, transaction_id} = await privy .wallets() .tron() .sendTransaction('insert-wallet-id', { caip2: 'tron:mainnet', params: { raw_data: { contract: [ { type: 'TransferContract', owner_address: TronWeb.address.toHex('insert-wallet-address'), to_address: TronWeb.address.toHex('insert-recipient-address'), amount: 1_000_000 // 1 TRX in sun } ] } } }); console.log('txID:', hash); // lowercase hex, no 0x prefix ``` ### TRC-20 transfer ```typescript theme={"system"} theme={"system"} import {PrivyClient} from '@privy-io/node'; import {TronWeb, utils} from 'tronweb'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID, appSecret: process.env.PRIVY_APP_SECRET }); const USDT_CONTRACT = 'TR7NHqjeKQxGTCi8q8ZY4pL8otSzgjLj6t'; // USDT on Tron mainnet // ABI-encode transfer(address,uint256) calldata const recipientHex = TronWeb.address.toHex('insert-recipient-address').replace(/^41/, '0x'); const calldata = utils.abi.encodeParamsV2ByABI( { name: 'transfer', type: 'function', inputs: [ {name: 'to', type: 'address'}, {name: 'value', type: 'uint256'} ], outputs: [{name: '', type: 'bool'}], stateMutability: 'nonpayable' }, [recipientHex, BigInt(1_000_000)] ); const data = 'a9059cbb' + calldata.slice(2); // transfer(address,uint256) selector const {hash} = await privy .wallets() .tron() .sendTransaction('insert-wallet-id', { caip2: 'tron:mainnet', params: { raw_data: { contract: [ { type: 'TriggerSmartContract' as const, owner_address: TronWeb.address.toHex('insert-wallet-address'), contract_address: TronWeb.address.toHex(USDT_CONTRACT), data, call_value: 0 } ], fee_limit: 100_000_000 // 100 TRX — safe default for TRC-20 transfers } } }); console.log('txID:', hash); ``` ### Parameters and Returns Check out the [API reference](/api-reference/wallets/tron/tron-send-transaction) for more details. **Go** Use the typed `Wallets.Tron.SendTransaction` method to sign and broadcast a transaction in one call. The `caip2` argument identifies the target network. ```go theme={"system"} theme={"system"} data, err := client.Wallets.Tron.SendTransaction(context.Background(), walletID, "tron:mainnet", privy.TronSendTransactionRpcInputParams{ RawData: privy.TronRawDataForSend{ Contract: []privy.TronContractUnion{ { OfTransferContract: &privy.TronTransferContract{ Type: privy.TronTransferContractTypeTransferContract, OwnerAddress: ownerAddressHex, // 41-prefixed hex ToAddress: recipientAddressHex, // 41-prefixed hex Amount: 1_000_000, // 1 TRX in sun }, }, }, }, }, ) if err != nil { log.Fatalf("failed to send transaction: %v", err) } fmt.Println("txID:", data.Hash) // lowercase hex, no 0x prefix ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/tron/tron-send-transaction) for more details. **Ruby** Use the typed `wallets.tron.send_transaction` method to sign and broadcast a transaction in one call. The `caip2` argument identifies the target network. ```ruby theme={"system"} theme={"system"} response = client.wallets.tron.send_transaction( wallet_id, params: { raw_data: { contract: [ { type: "TransferContract", owner_address: owner_hex_address, # 41-prefixed hex to_address: recipient_hex_address, # 41-prefixed hex amount: 1_000_000 # 1 TRX in sun } ] } }, caip2: "tron:mainnet" ) puts("txID:", response.hash) # lowercase hex, no 0x prefix ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/tron/tron-send-transaction) for more details. **Python** Use the `send_transaction` method on the Tron wallet service. ```python theme={"system"} response = client.wallets.tron.send_transaction( wallet_id, params={"raw_data": raw_data}, ) transaction_hash = response.hash ``` > [!WARNING] > > 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). # Sign a Tron transaction Source: https://docs.privy.io/wallets/using-wallets/tron/sign-a-transaction Sign a Tron transaction without broadcasting it using Privy Sign a Tron transaction without broadcasting it. Use this when you need to broadcast via a custom RPC or gas sponsor like [Transatron](/recipes/tron/transatron). > [!NOTE] > > To sign and broadcast in a single call, use > [`tron_sendTransaction`](/wallets/using-wallets/tron/send-a-transaction) instead. **REST API** To sign a transaction, make a `POST` request to ```bash theme={"system"} theme={"system"} https://api.privy.io/v1/wallets//rpc ``` Block reference fields (`ref_block_bytes`, `ref_block_hash`, `expiration`) are **required** for `tron_signTransaction` — fetch them fresh using `TronWeb.trx.getCurrentRefBlockParams()` before every request. They expire in about 60 seconds. ### Usage ```bash theme={"system"} 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": "tron_signTransaction", "params": { "raw_data": { "contract": [ { "type": "TransferContract", "owner_address": "41a614f803b6fd780986a42c78ec9c7f77e6ded13c", "to_address": "41e552f6487585c2b58bc2c9bb4492bc1f17132cd0", "amount": 1000000 } ], "ref_block_bytes": "a2b4", "ref_block_hash": "1234567890abcdef", "expiration": 1735689600000, "timestamp": 1735689540000 } } }' ``` A successful response looks like: ```json theme={"system"} theme={"system"} { "method": "tron_signTransaction", "data": { "signed_transaction": "0a02a2b42208...", "encoding": "hex" } } ``` The `signed_transaction` is `rawDataHex + signatureHex` (signature = last 130 hex chars / 65 bytes). To broadcast, pass it to `POST /wallet/broadcasthex` on any Tron full node. ### Parameters **method** `string` *required* Must be `"tron_signTransaction"`. **params.raw\_data.contract** `array` *required* Array of exactly one contract — `TransferContract` (TRX) or `TriggerSmartContract` (TRC-20 / smart contract call). All addresses must be 41-prefixed hex. **params.raw\_data.ref\_block\_bytes** `string` *required* Block reference bytes — 4 hex characters. Must be fetched fresh from chain before signing. **params.raw\_data.ref\_block\_hash** `string` *required* Block reference hash — 16 hex characters. Fetched alongside `ref_block_bytes`. **params.raw\_data.expiration** `number` *required* Transaction expiration in Unix milliseconds. **params.raw\_data.fee\_limit** `number` Maximum energy fee in sun. Required for `TriggerSmartContract`. `100_000_000` (100 TRX) is a safe default for TRC-20 transfers. **params.raw\_data.timestamp** `number` Transaction creation timestamp in Unix milliseconds. Defaults to `Date.now()` if omitted. ### Returns **data.signed\_transaction** `string` The signed transaction hex: `rawDataHex + signatureHex` (65 bytes). Pass directly to `/wallet/broadcasthex` on a Tron full node. **data.encoding** `string` Always `"hex"`. Check out the [API reference](/api-reference/wallets/tron/tron-sign-transaction) for more details. **NodeJS** Use the typed `wallets().tron().signTransaction()` method to sign a transaction. It returns the fully signed transaction hex — broadcast it yourself via `/wallet/broadcasthex`. ```typescript theme={"system"} theme={"system"} import {PrivyClient} from '@privy-io/node'; import {TronWeb} from 'tronweb'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID, appSecret: process.env.PRIVY_APP_SECRET }); const tronWeb = new TronWeb({fullHost: 'https://api.trongrid.io'}); // Build a transaction to source fresh block reference fields — they expire in ~60 seconds const tx = await tronWeb.transactionBuilder.sendTrx( 'insert-recipient-address', 1_000_000, // 1 TRX in sun 'insert-wallet-address' ); const {signed_transaction, encoding} = await privy .wallets() .tron() .signTransaction('insert-wallet-id', { params: { raw_data: { contract: [ { type: 'TransferContract', owner_address: TronWeb.address.toHex('insert-wallet-address'), to_address: TronWeb.address.toHex('insert-recipient-address'), amount: 1_000_000 // 1 TRX in sun } ], ref_block_bytes: tx.raw_data.ref_block_bytes, ref_block_hash: tx.raw_data.ref_block_hash, expiration: tx.raw_data.expiration } } }); // signed_transaction is `rawDataHex + signatureHex` — broadcast via POST /wallet/broadcasthex console.log(signed_transaction, encoding); ``` > [!NOTE] > > Block reference fields (`ref_block_bytes`, `ref_block_hash`, `expiration`) are required for > `tron_signTransaction`. Fetch them fresh before every request — they expire in about 60 seconds. ### Parameters and Returns Check out the [API reference](/api-reference/wallets/tron/tron-sign-transaction) for more details. **Go** Use the typed `Wallets.Tron.SignTransaction` method to sign a transaction. It returns the fully signed transaction hex — broadcast it yourself via `/wallet/broadcasthex`. ```go theme={"system"} theme={"system"} data, err := client.Wallets.Tron.SignTransaction(context.Background(), walletID, privy.TronSignTransactionRpcInputParams{ RawData: privy.TronRawDataForSign{ Contract: []privy.TronContractUnion{ { OfTransferContract: &privy.TronTransferContract{ Type: privy.TronTransferContractTypeTransferContract, OwnerAddress: ownerAddressHex, // 41-prefixed hex ToAddress: recipientAddressHex, // 41-prefixed hex Amount: 1_000_000, // 1 TRX in sun }, }, }, RefBlockBytes: refBlockBytes, // fetched fresh from chain RefBlockHash: refBlockHash, Expiration: time.Now().UnixMilli() + 60_000, }, }, ) if err != nil { log.Fatalf("failed to sign transaction: %v", err) } // data.SignedTransaction is `rawDataHex + signatureHex` — broadcast via POST /wallet/broadcasthex fmt.Println(data.SignedTransaction, data.Encoding) ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/tron/tron-sign-transaction) for more details. **Ruby** Use the typed `wallets.tron.sign_transaction` method to sign a transaction. It returns the fully signed transaction hex — broadcast it yourself via `/wallet/broadcasthex`. ```ruby theme={"system"} theme={"system"} response = client.wallets.tron.sign_transaction( wallet_id, params: { raw_data: { contract: [ { type: "TransferContract", owner_address: owner_hex_address, # 41-prefixed hex to_address: recipient_hex_address, # 41-prefixed hex amount: 1_000_000 # 1 TRX in sun } ], ref_block_bytes: ref_block_bytes, # fetched fresh from chain ref_block_hash: ref_block_hash, expiration: (Time.now.to_i * 1000) + 60_000 } } ) # response.signed_transaction is `rawDataHex + signatureHex` — broadcast via POST /wallet/broadcasthex puts(response.signed_transaction, response.encoding) ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/tron/tron-sign-transaction) for more details. **React** Use `useSignRawHash` from `@privy-io/react-auth/extended-chains` to sign the transaction's `txID` in the browser. Send the 64-byte signature to your server to attach the recovery byte and broadcast. ```tsx theme={"system"} theme={"system"} import {useSignRawHash} from '@privy-io/react-auth/extended-chains'; const {signRawHash} = useSignRawHash(); const signTronTxId = async (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; // 64-byte hex — send to your server to attach recovery byte }; ``` On the server, attach the correct recovery byte before broadcasting: ```typescript theme={"system"} theme={"system"} import type {TronWeb, Types} from 'tronweb'; function attachRecoveryByte( 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; } ``` For a complete gas-sponsored Tron flow, see the [Transatron recipe](/recipes/tron/transatron). **Python** Use the `sign_transaction` method on the Tron wallet service. ```python theme={"system"} response = client.wallets.tron.sign_transaction( wallet_id, params={"raw_data": raw_data}, ) signed_transaction = response.signed_transaction ``` # UI components Source: https://docs.privy.io/wallets/using-wallets/ui-components Prebuilt UI components for wallet funding, sending, and receiving in React applications **React** Privy comes with out-of-the-box UIs for signing messages and sending transactions. These wallet UIs are highly-customizable, allowing your application to communicate relevant context to the user or abstract away the fact that a wallet is being used under the hood. ## Sign message Below is a sample message signature UI. ![images/Sign.png](https://mintcdn.com/privy-c2af3412/YvGXGsI-T4KAqoan/images/Sign.png?fit=max\&auto=format\&n=YvGXGsI-T4KAqoan\&q=85\&s=d53afa26ed87352a61dbf1477c651442) This UI can also be customized by passing a `uiOptions` object of the following type to the method. ### Parameters **showWalletUIs** `boolean` Whether to overwrite the configured wallet UI for the signature prompt. Defaults to `undefined`, which will respect the server-side or SDK configured option. **title** `string` The title text for the signature prompt. Defaults to 'Sign message'. **description** `string` The description text for the signature prompt. Defaults to 'Signing this message will not cost you any fees.'. **buttonText** `string` The description text for the signature prompt. Defaults to 'Sign and continue'. ## Send transaction Below is a sample transaction UI. ![images/Trans.png](https://mintcdn.com/privy-c2af3412/YvGXGsI-T4KAqoan/images/Trans.png?fit=max\&auto=format\&n=YvGXGsI-T4KAqoan\&q=85\&s=e8c2eaf51c49ab7d5bd0bb300a6153f2) This UI can also be customized by passing a `uiOptions` object of the following type to the\ method. ### Parameters **showWalletUIs** `boolean` Whether or not to show wallet UIs for this action. Defaults to the wallet UI setting enabled for your app. **description** `string` Description of the transaction being sent. **buttonText** `string` Text to show on CTA button for Send Transaction screen. Defaults to 'Submit' or 'Approve'. **transactionInfo** `Object` **title** `string` Title for transaction details accordion. **action** `string` Short action description (e.g., 'Buy NFT'). **contractInfo** `Object` **url** `string` Smart contract information URL. **name** `string` Smart contract name. **imgUrl** `string` Contract image URL. **imgAltText** `string` Alternative text for contract image. **imgSize** `'sm' | 'lg'` Image size for contract ('sm' or 'lg'). **successHeader** `string` Text displayed at the top of the success screen. Defaults to 'Transaction complete!'. **successDescription** `string` Description for the success screen. Defaults to 'You're all set.'. **isCancellable** `boolean` Whether to display a cancel button on the confirmation screen. **React Native** When building an application using the React Native SDK, Privy gives you complete control over the experience and UI. If you do with to use Privy's default UIs for message signing or login, make sure you have [properly configured `PrivyElements`](/basics/react-native/advanced/setup-privyelements), and use the hooks exported from `@privy-io/expo/ui`, such as `useLogin`, `useSignMessage`, or `useFundWallet`. > [!NOTE] > > The React Native SDK does not yet support default UIs for signing typed data or sending > transactions. Consumers of the SDK typically attach their own UIs to the Privy SDK methods for > signing messages and sending transactions | Use case | Hook | | - | - | | Login users | `useLogin` | | Sign message (Ethereum) | `useSignMessage` | | Sign message (Solana) | `useSolanaSignMessage` | | Fund wallets (Ethereum) | `useFundWallet` | | Fund wallets (Solana) | `useFundSolanaWallet` | | Enroll in MFA | `useMfaEnrollmentUI` | | Verify MFA | Set `enableMfaVerificationUIs: true` | # Whitelabel Source: https://docs.privy.io/wallets/using-wallets/whitelabel Customize embedded wallet experiences to match your app design with whitelabel wallet UIs Privy’s frontend SDKs let you fully customize embedded wallet experiences. Match wallet flows to your app’s look and feel. * Use your own components and branding to customize wallet creation, signing, and transaction flows. * Create seamless, one-click signature and transaction experiences by disabling modals entirely. **React** 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. > **Provider config (globally)** > > In your `PrivyProvider` config you can control the default wallet UI for all flows in your app. > > ```tsx {5} theme={"system"} > appId={process.env.NEXT_PUBLIC_PRIVY_APP_ID || ''} > config={{ > embeddedWallets: { > showWalletUIs: false, > priceDisplay: {primary: 'native-token', secondary: null} > } > }} > > > > > ``` > > For more granular control, you can also control wallet UIs for specific flows in the sections below. > **Create a wallet** > > Privy supports whitelabeling wallet creation for Ethereum, Solana, and other chains. > > **Ethereum** > > ```tsx theme={"system"} > import {useCreateWallet} from '@privy-io/react-auth'; > ``` > > ```tsx theme={"system"} > const {createWallet} = useCreateWallet(); > createWallet(); > ``` > > **Solana** > > ```tsx theme={"system"} > import {useWallets} from '@privy-io/react-auth/solana'; > ``` > > ```tsx theme={"system"} > const {createWallet} = useWallets(); > createWallet(); > ``` > > **Other chains** > > ```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. > ``` > **Signing a message** > > To whitelabel Privy's message signing functionality, use the `useSignMessage` hook and call `signMessage` with your desired message. > > **Ethereum** > > ```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}} > ); > ``` > > **Solana** > > ```tsx theme={"system"} > import {useSignMessage} from '@privy-io/react-auth/solana'; > ``` > > ```tsx theme={"system"} > const {signMessage} = useSignMessage(); > signMessage({ > message: 'messageinUint8Array', > options: {uiOptions: {showWalletUIs: false}} > }); > ``` > > **Other chains** > > ```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' > }); > ``` > **Sending a transaction** > > To whitelabel Privy's transaction sending functionality, use the `useSendTransaction` hook and call `sendTransaction` with your desired transaction. > > **Ethereum** > > ```tsx theme={"system"} > import {useSendTransaction} from '@privy-io/react-auth'; > ``` > > ```tsx theme={"system"} > const {sendTransaction} = useSendTransaction(); > sendTransaction( > { > to: '0xE3070d3e4309afA3bC9a6b057685743CF42da77C', > value: 100000 > }, > { > uiOptions: {showWalletUIs: false} > } > ); > ``` > > **Solana** > > ```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} > }); > ``` **React Native** Privy’s React Native SDK is whitelabel by default, enabling apps to implement custom authentication UI and flows using the SDK’s functions. Get started with `sendTransaction` [here](/wallets/using-wallets/ethereum/send-a-transaction#react-native). **Swift** Privy’s Swift SDK is whitelabel by default, enabling apps to implement custom wallet UI and flows using the SDK’s functions. Get started with sending a transaction [here](/wallets/using-wallets/ethereum/send-a-transaction#swift). **Android** Privy’s Android SDK is whitelabel by default, enabling apps to implement custom wallet UI and flows using the SDK’s functions. Get started with sending a transaction [here](/wallets/using-wallets/ethereum/send-a-transaction#android). **Flutter** Privy’s Flutter SDK is whitelabel by default, enabling apps to implement custom wallet UI and flows using the SDK’s functions. Get started with sending a transaction [here](/wallets/using-wallets/ethereum/send-a-transaction#flutter). **Unity** Privy’s Unity SDK is whitelabel by default, enabling apps to implement custom wallet UI and flows using the SDK’s functions. Get started with sending a transaction [here](/wallets/using-wallets/ethereum/send-a-transaction#unity). # Overview Source: https://docs.privy.io/wallets/using-wallets/xrpl/overview Create XRPL wallets, sign transactions, and enforce policies with Privy With Privy, your app can create embedded XRPL wallets, sign transactions, and enforce Privy's [policy engine](/controls/policies/overview) to constrain what each wallet can do. XRPL wallets use the secp256k1 curve with BIP-44 derivation path `m/44'/144'/0'/0/{index}`. Your app signs XRPL transactions via the `xrpl_signTransaction` RPC method or via [raw hash signing](/wallets/using-wallets/other-chains). Then, submit the signed transaction to the network using [xrpl.js](https://js.xrpl.org/). ## Features * **Embedded XRPL wallets**: create wallets with the `xrpl` chain type. Addresses use the standard `r`-prefix format. * **Transaction signing**: sign any XRPL transaction via `xrpl_signTransaction`, which returns a DER-encoded signature and a ready-to-submit transaction blob. * **Raw hash signing**: sign arbitrary hashes with `raw_sign` for full control over serialization. * **Policy enforcement**: constrain transactions with Privy's policy engine — restrict destinations, cap amounts, and limit transaction types. * **Key export**: export the hex private key or BIP-39 seed phrase for full portability. ## Account activation > [!WARNING] > > XRPL wallets do not exist on-ledger until they receive the base reserve (currently 1 XRP). Calling > `account_info` on an unfunded address returns `actNotFound`. Your app must fund the wallet before > it can submit transactions. On testnet, fund wallets using the [XRPL faucet](https://xrpl.org/resources/dev-tools/xrp-faucets): ```bash theme={"system"} POST https://faucet.altnet.rippletest.net/accounts ``` ## Create a wallet **NodeJS** ```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 wallet = await privy.wallets().create({chain_type: 'xrpl'}); // wallet.address: r-address (e.g. "rHb9CJAWyB4rj91VRWn96DkukG4bwdtyTh") // wallet.public_key: 33-byte compressed secp256k1 pubkey, hex, no 0x prefix ``` **Python** ```python theme={"system"} wallet = client.wallets.create(chain_type="xrpl") # wallet.address is an XRPL r-address. # wallet.public_key is a 33-byte compressed secp256k1 public key encoded as hex. ``` ## Sign a transaction Sign an XRPL transaction using the `xrpl_signTransaction` RPC method. Your app builds and encodes the transaction with xrpl.js, passes the prefixed bytes to Privy, and receives a signed transaction blob ready to submit. **REST API** To sign a transaction, make a `POST` request to: ```bash theme={"system"} theme={"system"} https://api.privy.io/v1/wallets//rpc ``` ### Usage ```bash theme={"system"} 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": "xrpl_signTransaction", "params": { "transaction": "53545800", "encoding": "hex" } }' ``` A successful response looks like: ```json theme={"system"} theme={"system"} { "method": "xrpl_signTransaction", "data": { "txn_signature": "", "signed_transaction": "", "encoding": "hex" } } ``` ### Parameters **method** `string` *required* Must be `"xrpl_signTransaction"`. **params.transaction** `string` *required* The XRPL signing prefix `53545800` concatenated with the STObject-encoded transaction bytes, as an even-length hex string. Use `encodeForSigning()` from xrpl.js to produce this directly. **params.encoding** `string` *required* Must be `"hex"`. ### Returns **data.txn\_signature** `string` DER-encoded ECDSA signature (hex, no `0x` prefix). This is the value for the `TxnSignature` field in the signed transaction. **data.signed\_transaction** `string` The fully signed XRPL transaction blob (hex, no `0x` prefix), ready to pass to `client.submit()` from xrpl.js. **data.encoding** `string` Always `"hex"`. **NodeJS** ```typescript theme={"system"} {skip-check} theme={"system"} import {PrivyClient} from '@privy-io/node'; import {Client as XrplClient, encodeForSigning, xrpToDrops} from 'xrpl'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID, appSecret: process.env.PRIVY_APP_SECRET }); const walletId = 'insert-wallet-id'; const walletAddress = 'insert-wallet-address'; const walletPublicKey = 'insert-wallet-public-key'; // 1. Connect to XRPL and build a transaction const xrpl = new XrplClient('wss://s.altnet.rippletest.net:51233'); await xrpl.connect(); const prepared = await xrpl.autofill({ TransactionType: 'Payment', Account: walletAddress, Destination: 'ra5nK24KXen9AHvsdFTKHSANinZseWnPcX', Amount: xrpToDrops('10') }); // 2. Add SigningPubKey and encode with the signing prefix const forSigning = {...prepared, SigningPubKey: walletPublicKey}; const prefixedHex = encodeForSigning(forSigning); // 3. Sign via xrpl_signTransaction RPC const {data} = await privy.wallets().rpc(walletId, { method: 'xrpl_signTransaction', params: { transaction: prefixedHex, encoding: 'hex' } }); // 4. Submit the signed transaction blob const result = await xrpl.submit(data.signed_transaction); console.log('Result:', result.result.engine_result); await xrpl.disconnect(); ``` **Python** Build and encode the transaction with your XRPL library, then pass the signing-prefixed hex to Privy. The response includes the complete signed transaction blob. ```python theme={"system"} response = client.wallets.rpc( wallet_id, wallet_rpc_request_body={ "method": "xrpl_signTransaction", "params": { "transaction": prefixed_hex, "encoding": "hex", }, }, ) result = xrpl_client.submit(response.data.signed_transaction) ``` ## Sign a raw hash For full control over serialization, your app can compute the XRPL signing hash and sign it directly with `raw_sign` in hash mode. **REST API** To sign a raw hash, make a `POST` request to: ```bash theme={"system"} theme={"system"} https://api.privy.io/v1/wallets//raw_sign ``` ### Usage ```bash theme={"system"} theme={"system"} $ curl --request POST https://api.privy.io/v1/wallets//raw_sign \ -u ":" \ -H "privy-app-id: " \ -H "privy-authorization-signature: " \ -H 'Content-Type: application/json' \ -d '{ "params": { "hash": "0x" } }' ``` A successful response looks like: ```json theme={"system"} theme={"system"} { "method": "raw_sign", "data": { "signature": "", "encoding": "hex" } } ``` ### Parameters **params.hash** `string` *required* The hash to sign, as an even-length hex string prefixed with `0x`. For XRPL, this is the SHA-512Half of the signing-prefixed, STObject-encoded transaction bytes. ### Returns **data.signature** `string` Compact (`r||s`) ECDSA signature, hex, no `0x` prefix. XRPL requires a DER-encoded signature for `TxnSignature`, so convert this before attaching it to the transaction. **data.encoding** `string` Always `"hex"`. **NodeJS** ```typescript theme={"system"} {skip-check} theme={"system"} import {PrivyClient} from '@privy-io/node'; import {Signature} from '@noble/secp256k1'; import {createHash} from 'node:crypto'; import {encode, encodeForSigning} from 'xrpl'; const privy = new PrivyClient({ appId: process.env.PRIVY_APP_ID, appSecret: process.env.PRIVY_APP_SECRET }); // Compute the SHA-512Half of the signing-prefixed transaction const forSigning = {...prepared, SigningPubKey: walletPublicKey}; const prefixedHex = encodeForSigning(forSigning); const sha512Half = createHash('sha512') .update(Buffer.from(prefixedHex, 'hex')) .digest() .subarray(0, 32); // Sign the hash with Privy const {signature} = await privy.wallets().rawSign(walletId, { params: { hash: `0x${sha512Half.toString('hex')}` } }); // Convert compact signature (r||s) to DER for XRPL's TxnSignature const compactHex = signature.replace(/^0x/, ''); const derHex = Signature.fromCompact(compactHex).toDERHex(); // Attach the signature, encode the final blob, and submit const signedTx = encode({...forSigning, TxnSignature: derHex}); const result = await xrpl.submit(signedTx); console.log('Result:', result.result.engine_result); ``` **Python** ```python theme={"system"} import hashlib from cryptography.hazmat.primitives.asymmetric.utils import encode_dss_signature from xrpl.core.binarycodec import encode from xrpl.models.requests import Submit for_signing = { **transaction, "SigningPubKey": wallet.public_key.upper(), } prefixed_hex = f"53545800{encode(for_signing)}" sha512_half = hashlib.sha512(bytes.fromhex(prefixed_hex)).digest()[:32] response = client.wallets.raw_sign( wallet_id, wallet_raw_sign_params={"params": {"hash": f"0x{sha512_half.hex()}"}}, ) # Convert Privy's compact r || s signature to the DER encoding expected by XRPL. compact_signature = bytes.fromhex(response.data.signature.removeprefix("0x")) r = int.from_bytes(compact_signature[:32], "big") s = int.from_bytes(compact_signature[32:], "big") der_signature = encode_dss_signature(r, s).hex().upper() signed_transaction = encode({**for_signing, "TxnSignature": der_signature}) result = xrpl_client.request(Submit(tx_blob=signed_transaction)) ``` > [!NOTE] > > `xrpl_signTransaction` handles DER encoding and blob assembly automatically. Use `raw_sign` only > when your app needs to control the full signing pipeline. ## Export keys XRPL wallets support both private key export (raw hex format) and BIP-39 seed phrase export. The exported private key is a raw hex secp256k1 key. XRPL's native "family seed" format (base58-encoded 16-byte seed) is not compatible with BIP-44 derivation and is not supported. ## Enforce policies XRPL wallets support Privy's policy engine, so your app can constrain transactions signed via `xrpl_signTransaction`: * **Transaction types**: restrict which transaction types a wallet can sign using the `TransactionType` field. * **Payment destinations**: allowlist or denylist recipient addresses using `Payment.Destination`. * **Amount limits**: cap XRP payment amounts using `Payment.Amount.drops`, or IOU values using `Payment.Amount.value`. * **Offer limits**: constrain DEX offers using `OfferCreate.TakerPays` and `OfferCreate.TakerGets` fields. * **Trust line limits**: restrict trust line creation using `TrustSet.LimitAmount` fields. * **System conditions**: gate signing on values such as the current timestamp using the `system` field source. See [XRPL policy examples](/controls/policies/example-policies/xrpl) for complete configuration examples. ### Supported transaction types for policy evaluation Policies can evaluate decoded fields on these transaction types: `Payment`, `OfferCreate`, `OfferCancel`, and `TrustSet`. > [!NOTE] > > Wallets with policies can sign any valid XRPL transaction type. However, if the transaction type > is not supported for policy evaluation, the request is denied unless a matching system-level rule > (e.g., a time-based condition) allows it. # Archive a wallet Source: https://docs.privy.io/wallets/wallets/archive-a-wallet Soft-delete wallets to remove them from active use while preserving their history Privy allows your app to archive wallets that are no longer needed. Archiving performs a reversible soft-delete: Privy retains the wallet's keys, history, and address, but hides the wallet from read endpoints and blocks all write and signing operations. ## When to archive Archive a wallet when: * The wallet's purpose is complete (e.g., a one-time payment wallet) * Your app is rotating to a new wallet and decommissioning the old one * A compliance or security event requires deactivating a wallet without destroying its audit trail ## Archiving a wallet To archive a wallet, make a `POST` request to: ``` https://api.privy.io/v1/wallets//archive ``` > [!TIP] > > If the wallet has an [external ID](/wallets/wallets/external-ids), your app can use > `ext_wal_` in place of the wallet ID. ```bash theme={"system"} curl --request POST https://api.privy.io/v1/wallets/rbokq6mmq5f8j1cgyr6a5g4n/archive \ -u ":" \ -H "privy-app-id: " \ -H 'Content-Type: application/json' \ -d '{}' ``` The response returns the wallet with `archived_at` populated (Unix timestamp in milliseconds). ## What happens when your app archives a wallet | Behavior | Detail | | - | - | | Hidden from reads | `GET /v1/wallets`, `GET /v1/wallets/{id}`, and `POST /v1/wallets/address` return 404 by default. | | Blocked from writes | RPC, signing, and transfer endpoints return 400. | | Address preserved | The on-chain address remains unchanged and can still receive funds. Your app cannot reuse it. | | External ID reserved | The `external_id` remains assigned. Your app cannot assign it to a new wallet. | | Webhook dispatched | Privy sends a `wallet.archived` event to your configured webhook endpoint. | > [!WARNING] > > Custodial wallets (wallets managed by a third-party custodian) cannot be archived. Attempting to > archive a custodial wallet returns a 400 error. ## Reading archived wallets To include archived wallets in read operations, pass the `include_archived=true` query parameter: ```bash theme={"system"} # Get a specific archived wallet curl --request GET "https://api.privy.io/v1/wallets/rbokq6mmq5f8j1cgyr6a5g4n?include_archived=true" \ -u ":" \ -H "privy-app-id: " ``` ```bash theme={"system"} # List all wallets including archived ones curl --request GET "https://api.privy.io/v1/wallets?include_archived=true" \ -u ":" \ -H "privy-app-id: " ``` Archived wallets include an `archived_at` field in the response, allowing your app to distinguish them from active wallets. ## Restoring an archived wallet Wallet restoration is available to Privy administrators. If your app needs to restore an accidentally archived wallet, contact Privy support. When Privy restores a wallet: * The wallet becomes visible in all read endpoints again * Write and signing operations resume * Privy sends a `wallet.restored` webhook event * The wallet counts toward your app's active wallet total ## API reference > **[Archive wallet](/api-reference/wallets/archive)** > > `POST /v1/wallets/{wallet_id}/archive` > **[wallet.archived webhook](/api-reference/webhooks/wallet/archived)** > > Fired when a wallet is archived. # Create a wallet Source: https://docs.privy.io/wallets/wallets/create/create-a-wallet Programmatically create embedded wallets for users, organizations, or servers on Ethereum, Solana, and other chains Privy enables you to programmatically create wallets embedded within your application. When you create a wallet, you can specify its `owner`, which defines who controls the wallet. * To create a user wallet, specify a **user ID** as an owner of the wallet. This ensures only the authenticated user has access to their wallet. All client-side SDKs create user wallets by default and automatically set the wallet owner as the authenticated user. * To create an organization wallet, specify the organization as the wallet's `entity`. Privy automatically sets the organization's default key quorum as the wallet's owner, unless you specify a different `owner` or `owner_id`. * Or, you can specify an **authorization key** as an `owner` on a wallet. The holder of the authorization key, typically your application backend, controls the wallet. You must use a server-side SDK to create wallets owned by an authorization key. You can also optionally set a wallet's `entity` to record the user that it belongs to. Entity assignment does not grant control of the wallet and cannot be changed after it is set. These wallets appear in the user's linked accounts with [`user_can_sign`](/kyc-kyb/entities#check-whether-the-user-can-sign) set to `false`. > [!TIP] > > 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. **React** > [!TIP] > > The React SDK supports automatically creating embedded wallets for your users when they log in to your app. View [this guide](/basics/react/advanced/automatic-wallet-creation) to learn more and configure automatic wallet creation. **Ethereum** To create a wallet with the React SDK, use the `createWallet` method from the `useCreateWallet` hook: ```tsx theme={"system"} createWallet: async ({createAdditional?: boolean, signers?: {signerId: string, policyIds?: string[]}[]}) => Promise ``` ### Usage ```tsx theme={"system"} import {useCreateWallet} from '@privy-io/react-auth'; const {createWallet} = useCreateWallet(); ``` ### Parameters **opts.createAdditional** `boolean` Whether or not to create an additional Ethereum wallet for the user if they already have an existing Ethereum embedded wallet. Must be set to `true` to create additional wallets. Defaults to `false`. [Learn more](/recipes/hd-wallets) **opts.signers** `\{signerId: string, policyIds?: string[]\}[]` An array of [signers](/wallets/using-wallets/signers/overview) to add to the wallet at creation time. Each signer object accepts a `signerId` (the key quorum ID) and an optional `policyIds` array to scope the signer's permissions. ### Returns **wallet** `Promise\` A `Promise` for the linked account object for the created wallet. ### Callbacks You can optionally register an `onSuccess` or `onError` callback on the `useCreateWallet` hook. ```tsx theme={"system"} const {createWallet} = useCreateWallet({ onSuccess: ({wallet}) => { console.log('Created wallet ', wallet); }, onError: (error) => { console.error('Failed to create wallet with error ', e) } }) ``` **onSuccess** `(\{wallet: Wallet\}) =\> void` Optional callback to run after a user successfully creates a wallet. **onError** `(error: string) =\> void` Optional callback to run after there is an error during wallet creation. **Solana** To create a wallet with the React SDK, use the `createWallet` method from the `useCreateWallet` hook: ```tsx theme={"system"} createWallet: async ({createAdditional?: boolean, signers?: {signerId: string, policyIds?: string[]}[]}) => Promise ``` ### Usage ```tsx theme={"system"} import {useCreateWallet} from '@privy-io/react-auth/solana'; const {createWallet} = useCreateWallet(); ``` ### Parameters The `createWallet` method optionally accepts as a parameter an `opts` object with the following fields: **opts.createAdditional** `boolean` Whether or not to create an additional Solana wallet for the user if they already have an existing Solana embedded wallet. Must be set to `true` to create additional wallets. Defaults to `false`. [Learn more](/recipes/hd-wallets) **opts.signers** `\{signerId: string, policyIds?: string[]\}[]` An array of [signers](/wallets/using-wallets/signers/overview) to add to the wallet at creation time. Each signer object accepts a `signerId` (the key quorum ID) and an optional `policyIds` array to scope the signer's permissions. ### Returns **wallet** `Wallet` The linked account object for the created wallet. **Other chains** To create a wallet for any supported [extended chain type](/wallets/overview/chains) with the React SDK, use the `createWallet` method from the `useCreateWallet` hook, imported from `@privy-io/react-auth/extended-chains`: ```tsx theme={"system"} createWallet: async ({chainType: ExtendedChainType}) => Promise<{user: User; wallet: Wallet}> ``` ### Usage ```tsx theme={"system"} import {useCreateWallet} from '@privy-io/react-auth/extended-chains'; const {createWallet} = useCreateWallet(); const {user, wallet} = await createWallet({chainType: 'cosmos'}); // or 'stellar', 'sui', etc. ``` ### Parameters **opts.chainType** `ExtendedChainType` The extended chain type of the wallet to create. See the [chain support overview](/wallets/overview/chains) for available chain types. ### Returns **child properties** **user** `User` The updated user object with the new wallet added to the user's `linkedAccounts` array. **wallet** `Wallet` The newly created wallet. **React Native** > [!TIP] > > The React Native SDK supports automatically creating embedded wallets for your users when they log in to your app. View [this guide](/basics/react/advanced/automatic-wallet-creation) to learn more and configure automatic wallet creation. **Ethereum** To create a wallet with the React Native SDK, use the `create` method from the `useEmbeddedEthereumWallet` hook: ```tsx theme={"system"} create: async ({createAdditional?: boolean}) => Promise<{user: User}> ``` ### Usage ```tsx theme={"system"} import {useEmbeddedEthereumWallet} from '@privy-io/expo'; const {create} = useEmbeddedEthereumWallet(); ``` ### Parameters The `create` method optionally accepts as a parameter an `opts` object with the following fields: **opts.createAdditional** `boolean` Whether or not to create an additional Ethereum wallet for the user if they already have an existing Ethereum embedded wallet. Must be set to `true` to create additional wallets. Defaults to `false`. [Learn more](/recipes/hd-wallets) ### Returns **user** `Promise\<\{user: User\}\>` A `Promise` for an object containing the updated `user` object for the user. **Solana** To create a wallet with the React Native SDK, use the `create` method from the `useEmbeddedSolanaWallet` hook: ```tsx theme={"system"} create: async ({createAdditional?: boolean}) => Promise<{user: User}> ``` ### Usage ```tsx theme={"system"} import {useEmbeddedSolanaWallet} from '@privy-io/expo'; const {create} = useEmbeddedSolanaWallet(); ``` ### Parameters The `create` method optionally accepts as a parameter an `opts` object with the following fields: **opts.createAdditional** `boolean` Whether or not to create an additional Solana wallet for the user if they already have an existing Solana embedded wallet. Must be set to `true` to create additional wallets. Defaults to `false`. [Learn more](/recipes/hd-wallets) ### Returns **user** `Promise\<\{user: User\}\>` A `Promise` for an object containing the updated `user` object. **Other chains** To create a wallet for any supported [extended chain type](/wallets/overview/chains) with the React Native SDK, use the `createWallet` method from the `useCreateWallet` hook, imported from `@privy-io/expo/extended-chains`: ```tsx theme={"system"} createWallet: async ({chainType: ExtendedChainType}) => Promise<{user: PrivyUser; wallet: Wallet}> ``` ### Usage ```tsx theme={"system"} import {useCreateWallet} from '@privy-io/expo/extended-chains'; const {createWallet} = useCreateWallet(); const {user, wallet} = await createWallet({chainType: 'cosmos'}); // or 'stellar', 'sui', etc. ``` ### Parameters **opts.chainType** `ExtendedChainType` The extended chain type of the wallet to create. See the [chain support overview](/wallets/overview/chains) for available chain types. ### Returns **child properties** **user** `PrivyUser` The updated user object with the new wallet added to the user's `linked_accounts` array. **wallet** `Wallet` The newly created wallet. **Swift** **Ethereum** To create a wallet with the Swift SDK, use the `createEthereumWallet` method from the `PrivyUser` instance: ```swift theme={"system"} func createEthereumWallet(allowAdditional: Bool) async throws -> EmbeddedEthereumWallet ``` ### Parameters The `createEthereumWallet` method optionally accepts the following parameters: **allowAdditional** `Boolean` Whether or not to create an additional Ethereum wallet for the user if they already have an existing Ethereum embedded wallet. Must be set to `true` to create additional wallets. Defaults to `false`. ### Returns **wallet** `EmbeddedEthereumWallet` The newly created `EmbeddedEthereumWallet`. **Solana** To create a wallet with the Swift SDK, use the `createSolanaWallet` method from the `PrivyUser` instance: ```swift theme={"system"} func createSolanaWallet() async throws -> EmbeddedSolanaWallet ``` ### Returns **wallet** `EmbeddedSolanaWallet` The newly created `EmbeddedSolanaWallet`. **Android** **Ethereum** To create a wallet with the Android SDK, use the `createEthereumWallet` method from the `PrivyUser` instance: ```kotlin theme={"system"} public suspend fun createEthereumWallet: (allowAdditional?: Boolean) => Result ``` ### Parameters The `createEthereumWallet` method optionally accepts the following parameters: **allowAdditional** `Boolean` Whether or not to create an additional Ethereum wallet for the user if they already have an existing Ethereum embedded wallet. Must be set to `true` to create additional wallets. Defaults to `false`. ### Returns **wallet** `Result\` A `Result` containing the `EmbeddedEthereumWallet` for the user. **Solana** To create a wallet with the Android SDK, use the `createSolanaWallet` method from the `PrivyUser` instance: ```kotlin theme={"system"} public suspend fun createEthereumWallet: (allowAdditional?: Boolean) => Result ``` ### Returns **wallet** `Result\` A `Result` containing the `EmbeddedSolanaWallet` for the user. **Unity** **Ethereum** To create a wallet with the Unity SDK, use the `CreateEthereumWallet` method on your `IPrivyUser` instance: ```csharp theme={"system"} Task CreateEthereumWallet(bool allowAdditional = false) ``` ### Parameters The `CreateEthereumWallet` method accepts the following parameter: **allowAdditional** `Boolean` Whether or not to create an additional wallet for the user if they already have an existing ethereum wallet. Must be set to `true` to create additional wallets. Defaults to `false`. ### Returns **wallet** `Task\` A `Task` for the created ethereum wallet object. **Solana** To create a wallet with the Unity SDK, use the `CreateSolanaWallet` method on your `IPrivyUser` instance: ```csharp theme={"system"} Task CreateSolanaWallet(bool allowAdditional) ``` ### Parameters The `CreateSolanaWallet` method accepts the following parameter: **allowAdditional** `Boolean` Whether or not to create an additional wallet for the user if they already have an existing solana wallet. Must be set to `true` to create additional wallets. Defaults to `false`. ### Returns **wallet** `Task\` A `Task` for the created solana wallet object. **Flutter** **Ethereum** To create a wallet with the Flutter SDK, use the `createEthereumWallet` method from the `PrivyUser` instance: ```dart theme={"system"} Future> createEthereumWallet({bool allowAdditional = false}); ``` ### Parameters The `createEthereumWallet` method optionally accepts as a parameter an `opts` object with the following fields: **allowAdditional** `bool` Whether or not to create an additional Ethereum wallet for the user if they already have an existing Ethereum embedded wallet. Must be set to `true` to create additional wallets. Defaults to `false`. ### Returns **wallet** `Future\\>` A `Result` containing the `EmbeddedEthereumWallet` for the user. **Solana** To create a wallet with the Android SDK, use the `createSolanaWallet` method from the `PrivyUser` instance: ```dart theme={"system"} Future> createSolanaWallet() ``` ### Parameters The `createSolanaWallet` method optionally accepts the following parameters: **allowAdditional** `Boolean` Whether or not to create an additional Solana wallet for the user if they already have an existing Solana embedded wallet. Must be set to `true` to create additional wallets. Defaults to `false`. ### Returns **wallet** `Future\\>` A `Result` containing the `EmbeddedSolanaWallet` for the user. **NodeJS** To create a new wallet with the NodeJS SDK, use the `create` method on the `wallets()` interface of the Privy client. > [!NOTE] > > If you are creating a user wallet, you must specify the user ID as the owner of the wallet. You can obtain a user ID by first [creating a user](/user-management/migrating-users-to-privy/create-or-import-a-user) before creating the wallet. > > Or, you can [create a user and wallet at the same time](/user-management/migrating-users-to-privy/create-or-import-a-user). ### Usage ```ts theme={"system"} const {id, address, chain_type} = await privy.wallets().create({chain_type: 'ethereum', owner: {user_id: 'privy:did:xxxxx'}}); ``` To create an organization wallet, set `entity` to the organization. If you omit `owner` and `owner_id`, Privy uses the organization's default key quorum as the owner. ```ts theme={"system"} const wallet = await privy.wallets().create({ chain_type: 'ethereum', entity: { id: 'cm7zx4k9a0000l308abcd1234', type: 'organization' } }); ``` ### Parameters and Returns Check out the [API reference](/api-reference/wallets/create) for more details. > [!TIP] > > Your app can provision a smart account with the wallet as a signer by following [this guide](/wallets/gas-and-asset-management/gas/ethereum). **Java** To create a new wallet with the Java SDK, use the `create` method from the Privy client's `wallets()` helper: ### Usage ```java theme={"system"} try { // Example 1: Create an ethereum wallet with no owner WalletCreateRequestBody walletRequest1 = WalletCreateRequestBody.builder() .chainType(WalletChainType.ETHEREUM) .build(); // Example 2: Create an ethereum wallet with a user owner WalletCreateRequestBody walletRequest2 = WalletCreateRequestBody.builder() .chainType(WalletChainType.ETHEREUM) .owner(OwnerInput.of(OwnerInputUser.builder().userId("privy-user-id").build())) .build(); // Example 3: Create an ethereum wallet with a public key owner WalletCreateRequestBody walletRequest3 = WalletCreateRequestBody.builder() .chainType(WalletChainType.ETHEREUM) .owner(OwnerInput.of(OwnerInputPublicKey.builder().publicKey("authorization-key").build())) .build(); // Example 4: Create an ethereum wallet with a key quorum owner WalletCreateRequestBody walletRequest4 = WalletCreateRequestBody.builder() .chainType(WalletChainType.ETHEREUM) .ownerId("key-quorum-id") .build(); WalletCreateResponse response = privyClient.wallets() .create(walletRequest1); // or walletRequest2, walletRequest3, or walletRequest4 if (response.wallet().isPresent()) { Wallet createdWallet = response.wallet().get(); } } catch (APIException e) { String errorBody = e.bodyAsString().orElse(e.getMessage()); System.err.println(errorBody); } catch (Exception e) { System.err.println(e.getMessage()); } ``` ### Parameters When creating a wallet, you may specify the following values on the `WalletCreateRequestBody`: **chainType** `String` *required* The chain to create the wallet on. **policyIds** `List\` List of policy IDs for policies that should be enforced on the wallet. Currently, only one policy is supported per wallet. **owner** `OwnerInput` The owner of the resource, which can either be a public key from a p256 keypair, or a user ID. If you provide this, do not specify an `ownerId` as it will be generated automatically. **ownerId** `String` The key quorum ID to set as the owner of the resource. If you provide this, do not specify an`owner`. **additionalSigners** `WalletAdditionalSignerItem` Additional signers for the wallet. ### Returns The `WalletCreateResponse` object contains an optional `wallet()` field, present if the wallet was created successfully. **wallet()** `Optional\` The newly created `Wallet` object. **id** `String` Unique ID of the created wallet. This will be the primary identifier when using the wallet in the future. **address** `String` Address of the created wallet. **chainType** `WalletChainType` Chain type of the created wallet. **policyIds** `List\` List of policy IDs for policies that are enforced on the wallet. **ownerId** `String` The key quorum ID of the owner of the wallet. If an `ownerId` was passed in, this response is the input `ownerId`. If a user ID or authorization key was passed in as the `owner`, this response is a newly created key quorum containing the input user ID or authorization key. **additionalSigners** `List\` The key quorum IDs of the additional signers for the wallet. **createdAt** `double` The creation date of the wallet, as Unix time. **REST API** To create a new wallet for a user, make a `POST` request to ```bash theme={"system"} https://api.privy.io/v1/wallets ``` > [!NOTE] > > If you are creating a user wallet, you must specify the user ID as the owner of the wallet. You can obtain a user ID by first [creating a user](/user-management/migrating-users-to-privy/create-or-import-a-user) before creating the wallet. > > Or, you can [create a user and wallet at the same time](/user-management/migrating-users-to-privy/create-or-import-a-user). ### Body In the request body, include the following fields. **chain\_type** `'ethereum' | 'solana' | 'cosmos' | 'stellar' | 'sui' | 'aptos' | 'movement' | 'tron' | 'bitcoin-segwit' | 'near' | 'ton' | 'starknet' | 'spark'` *required* Chain type of the wallet to create. **owner** `\{user_id: string\} | \{public_key: string\} | null` The user ID to set as the owner of the wallet, or the P-256 public key to set as the owner of the wallet. 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. **owner\_id** `string | null` The key quorum ID of the owner of the wallet. If you provide this, do not specify an `owner`. View [this guide](/controls/authorization-keys/owners/overview) to learn more about owners. **entity** `\{id: string; type: 'user' | 'organization'\}` The user or organization that the wallet belongs to. Entity assignment is permanent and does not grant control of the wallet. If the entity is an organization and you do not explicitly pass `owner` or `owner_id`, Privy sets the organization's `default_key_quorum_id` as the wallet owner. **policy\_ids** `string[]` List of policy IDs for policies that should be enforced on the wallet. Currently, only one policy is supported per wallet. **idempotency\_key** `string` [Idempotency key](/api-reference/idempotency-keys) to identify a unique request. **additional\_signers** `\{signer_id: string\}[]` The key quorum IDs to add as additional signers for the wallet. **external\_id** `string` An optional identifier for the wallet from your system. Must be unique per app, URL-safe (`a-z`, `A-Z`, `0-9`, `_`, `-`), and at most 64 characters. Write-once: cannot be changed after creation. [Learn more](/wallets/wallets/external-ids) **display\_name** `string` An optional human-readable name for the wallet. At most 100 characters. Can be updated after creation. ### Response In the response, Privy will send back the following if successful: **id** `string` Unique ID of the created wallet. This will be the primary identifier when using the wallet in the future. **address** `string` Address of the created wallet. **chain\_type** `'ethereum' | 'solana' | 'stellar' | 'cosmos' | 'sui' | 'tron' | 'bitcoin-segwit' | 'near' | 'ton' | 'starknet' | 'aptos'` Chain type of the created wallet. **policy\_ids** `string[]` List of policy IDs for policies that are enforced on the wallet. **owner\_id** `string | null` The key quorum ID of the owner of the wallet. If an `ownerId` was passed in, this response is the input `ownerId`. If a user ID or authorization key was passed in as the `owner`, this response is a newly created key quorum containing the input user ID or authorization key. **entity** `\{id: string; type: 'user' | 'organization'\} | null` The user or organization that the wallet belongs to, if one was assigned. **additional\_signers** `\{signer_id: string\}[]` The key quorum IDs of the additional signers for the wallet. **created\_at** `number` The creation date of the wallet, in milliseconds since midnight, January 1, 1970 UTC. **external\_id** `string | null` The external identifier assigned to the wallet, if set at creation. **display\_name** `string | null` The display name assigned to the wallet, if set. ### Example A sample request might look like the following: ```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" }' ``` A successful response will look like the following: ```json theme={"system"} { "id": "fmfdj6yqly31huorjqzq38zc", "address": "0xf9f284C7Eaf97b0f9B5542d83Af7F785D12E803a", "chain_type": "ethereum", "policy_ids": [], "owner_id": null, "additional_signers": [], "created_at": 1733923425155 } ``` ### Rate limits Wallet creation endpoints are subject to rate limiting. If you encounter rate limits (HTTP 429), implement exponential backoff in your retry logic. > [!TIP] > > Learn more about handling rate limits effectively in our [optimizing your setup](/recipes/dashboard/optimizing#handling-rate-limits) guide. **Rust** To create a new wallet with the Rust SDK, use the `create` method on the `wallets()` interface of the Privy client. > [!NOTE] > > If you are creating a user wallet, you must specify the user ID as the owner of the wallet. You can obtain a user ID by first [creating a user](/user-management/migrating-users-to-privy/create-or-import-a-user) before creating the wallet. > > Or, you can [create a user and wallet at the same time](/user-management/migrating-users-to-privy/create-or-import-a-user). ### Usage ```rust theme={"system"} use privy_rs::{PrivyClient, generated::types::*}; let client = PrivyClient::new(app_id, app_secret)?; // Create an Ethereum wallet with a user owner let wallet = client .wallets() .create( None, // idempotency key (optional) &CreateWalletBody { chain_type: WalletChainType::Ethereum, owner: Some(Owner::UserOwner(UserOwner { user_id: "did:privy:xxxxx".to_string(), })), policy_ids: vec![], additional_signers: vec![], }, ) .await?; println!("Created wallet {} with address {}", wallet.id, wallet.address); ``` ### Parameters and Returns See the Rust SDK documentation for detailed parameter and return types, including embedded examples: * [WalletsClient::create](https://docs.rs/privy-rs/latest/privy_rs/subclients/struct.WalletsClient.html#method.create) * [CreateWalletBody](https://docs.rs/privy-rs/latest/privy_rs/generated/types/struct.CreateWalletBody.html) * [Wallet](https://docs.rs/privy-rs/latest/privy_rs/generated/types/struct.Wallet.html) For REST API details, see the [API reference](/api-reference/wallets/create). > [!TIP] > > Your app can provision a smart account with the wallet as a signer by following [this guide](/wallets/gas-and-asset-management/gas/ethereum). **Go** To create a new wallet with the Go SDK, use the `New` method on the `Wallets` service. > [!NOTE] > > If you are creating a user wallet, you must specify the user ID as the owner of the wallet. You can obtain a user ID by first [creating a user](/user-management/migrating-users-to-privy/create-or-import-a-user) before creating the wallet. > > Or, you can [create a user and wallet at the same time](/user-management/migrating-users-to-privy/create-or-import-a-user). ### Usage ```go theme={"system"} // Create an Ethereum wallet with a user owner wallet, err := client.Wallets.New(context.Background(), privy.WalletNewParams{ ChainType: privy.WalletChainTypeEthereum, Owner: privy.WalletNewParamsOwnerUnion{ OfUserOwner: &privy.WalletNewParamsOwnerUserOwner{ UserID: "did:privy:xxxxx", }, }, }) if err != nil { log.Fatalf("failed to create wallet: %v", err) } fmt.Println("Created wallet:", wallet.ID, wallet.Address) ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/create) for more details. > [!TIP] > > Your app can provision a smart account with the wallet as a signer by following [this > guide](/wallets/gas-and-asset-management/gas/ethereum). **Python** Use the `create` method on the wallets service. ```python theme={"system"} wallet = client.wallets.create(chain_type="ethereum") wallet_id = wallet.id ``` To create a user-owned wallet, pass the user's Privy ID as the owner: ```python theme={"system"} wallet = client.wallets.create( chain_type="ethereum", owner={"user_id": user_id}, ) ``` **Ruby** To create a new wallet with the Ruby SDK, use the `create` method on the `wallets` service. > [!NOTE] > > If you are creating a user wallet, you must specify the user ID as the owner of the wallet. You can obtain a user ID by first [creating a user](/user-management/migrating-users-to-privy/create-or-import-a-user) before creating the wallet. > > Or, you can [create a user and wallet at the same time](/user-management/migrating-users-to-privy/create-or-import-a-user). ### Usage ```ruby theme={"system"} # Create an Ethereum wallet with a user owner wallet = client.wallets.create( wallet_create_params: { chain_type: :ethereum, owner: {user_id: "did:privy:xxxxx"} } ) puts(wallet.id, wallet.address) ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/create) for more details. > [!TIP] > > Your app can provision a smart account with the wallet as a signer by following [this > guide](/wallets/gas-and-asset-management/gas/ethereum). # Export a wallet Source: https://docs.privy.io/wallets/wallets/export Export a wallet private key for user self-custody depending on wallet creation environment **Privy enables your users to export the private key for their embedded wallet**. This allows them to use their embedded wallet address with another wallet client, such as MetaMask or Phantom. ## Availability Key export is only available in certain environments depending on how the wallet was created. **environments where key export is available.** The environments where key export is available depends on the environment where the wallet was created. ### Wallets created client-side Wallets created client-side (e.g. through Privy's React, React Native, Swift, etc. SDKs) can only be exported via the `exportWallet` method in the React SDK.\ \ Due to the sensitive nature of key export, it is only available in web environments where the operation can be secured with strict browser security guarantees.\ \ If you'd like to enable key export with one of Privy's other client-side SDKs, we encourage setting up a minimal web page with Privy's React SDK and export wallet functionality that you can direct users to. > [!TIP] > > For a step-by-step guide on enabling key export from React Native, Swift, Android, and Flutter > apps using a WebView, see the [mobile key export recipe](/recipes/mobile-key-export). > [!NOTE] > > By default, to use wallets, users authenticate to the Privy API directly from their client using > their access token. This means that users can send transactions or export their keys using their > access token with the Privy API, even if an application does not implement Privy's `exportWallet` > method. This design ensures users always retain self-sovereign access to their wallet keys. > > To prevent users from unilaterally exporting keys, applications can set up a [2-of-2 key quorum](/controls/key-quorum/overview) as the wallet owner, consisting of the user and an authorization key controlled by the application's server. This requires authorization from both parties to export the wallet. ### Wallets created server-side Wallets created server-side can only be exported via Privy's server-side SDKs or REST API. This ensures that wallets are securely exported in an encrypted interaction between your app's and Privy's infrastructure. ## Supported chains Key export is available for [all wallets on every supported tier](/wallets/overview/chains). ## Usage **React** To have your user export their embedded wallet's private key, **use Privy's `exportWallet` method:** **EVM** ```tsx theme={"system"} import {useExportWallet} from '@privy-io/react-auth'; ... const {exportWallet} = useExportWallet(); ``` **Solana** ```tsx theme={"system"} import {useExportWallet} from '@privy-io/react-auth/solana'; ... const {exportWallet} = useExportWallet(); ``` When invoked, **`exportWallet`** will open a modal where your user can copy the full private key for their embedded wallet. The modal will also link your user to [a guide](https://privy-io.notion.site/Transferring-Your-App-Account-9dab9e16c6034a7ab1ff7fa479b02828) for how to load their embedded wallet into another wallet client, such as MetaMask or Phantom. If your user is not **`authenticated`** or has not yet created an embedded wallet in your app, this method will fail. As an example, you might attach **`exportWallet`** to an export wallet button in your app: **EVM** ```tsx theme={"system"} import {usePrivy} from '@privy-io/react-auth'; import {useExportWallet} from '@privy-io/react-auth'; function ExportWalletButton() { const {ready, authenticated, user} = usePrivy(); const {exportWallet} = useExportWallet(); // Check that your user is authenticated const isAuthenticated = ready && authenticated; // Check that your user has an embedded wallet const hasEmbeddedWallet = !!user.linkedAccounts.find( (account) => account.type === 'wallet' && account.walletClientType === 'privy' && account.chainType === 'ethereum' ); return ( ); } ``` **Solana** ```tsx theme={"system"} import {usePrivy, type WalletWithMetadata} from '@privy-io/react-auth'; import {useExportWallet} from '@privy-io/react-auth/solana'; function ExportWalletButton() { const {ready, authenticated, user} = usePrivy(); const {exportWallet} = useExportWallet(); // Check that your user is authenticated const isAuthenticated = ready && authenticated; // Check that your user has an embedded wallet const hasEmbeddedWallet = !!user.linkedAccounts.find( (account): account is WalletWithMetadata => account.type === 'wallet' && account.walletClientType === 'privy' && account.chainType === 'solana' ); return ( ); } ``` > [!NOTE] > > If your application uses [smart wallets](/wallets/using-wallets/evm-smart-wallets/overview) on EVM > networks, exporting the wallet will export the private key for the **smart wallet's signer**, and > not the smart wallet itself. Users can control their smart wallet via this private key, but will > be required to manually use it to sign calls to the contract for their smart wallet directly to > use the smart wallet outside of your app. ### Exporting HD wallets If your user has multiple embedded wallets, you can export the private key for a specific wallet by passing the address of your desired wallet as an `address` parameter to the `exportWallet` method: **EVM** ```tsx theme={"system"} import {useExportWallet} from '@privy-io/react-auth'; ... const {exportWallet} = useExportWallet(); await exportWallet({address: 'insert-your-desired-address'}); ``` **Solana** ```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 wallet at `walletIndex: 0`. > [!NOTE] > > When your user exports their embedded wallet, their private key is assembled on a different origin > than your app's origin. This means neither you nor Privy can ever access your user's private key. > **Your user is the only party that can ever access their full private key.** **NodeJS** To export a wallet's private key with the NodeJS / Typescript SDK, use the `exportPrivateKey` method from the client’s `wallets()` interface. To export a wallet's seed phrase, use `exportSeedPhrase` instead. These methods internally handle HPKE encryption and decryption, returning the plaintext key or phrase directly. ```ts theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: 'your-app-id', appSecret: 'your-app-secret' }); try { const {private_key} = await privy.wallets().exportPrivateKey('wallet-id', {}); } catch (error) { console.error('Failed to export wallet:', error); } ``` If the wallet is owned by an authorization key, a user, or a key quorum, you must provide the owner's information in the [authorization context](/controls/authorization-keys/using-owners/sign/signing-on-the-server) when exporting the wallet. ```ts theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: 'your-app-id', appSecret: 'your-app-secret' }); const {private_key} = await privy.wallets().exportPrivateKey('wallet-id', { authorization_context: { authorization_private_keys: ['authorization-key'], user_jwts: ['user-jwt', 'user-jwt-2'] } }); ``` **REST API** To export a wallet's private key via the REST API, use the `/v1/wallets/{wallet_id}/export` endpoint. This endpoint uses Hybrid Public Key Encryption (HPKE) to securely transmit the private key. > [!WARNING] > > Wallet export is restricted to wallet owners and enabled by default, unless explicitly disabled by > a [`DENY` policy](/controls/policies/overview#policies). Wallets without owners cannot be > exported. **cURL** ```bash theme={"system"} curl --request POST \ --url https://api.privy.io/v1/wallets/{wallet_id}/export \ --header 'Authorization: Basic ' \ --header 'Content-Type: application/json' \ --header 'privy-app-id: ' \ --header 'privy-authorization-signature: ' --data '{ "encryption_type": "HPKE", "recipient_public_key": "" }' ``` **Typescript/Javascript** ```ts theme={"system"} import {generateAuthorizationSignature} from "@privy-io/node" // Generate a base64-encoded key pair for the recipient const keypair = await crypto.subtle.generateKey( { name: "ECDH", namedCurve: "P-256" }, true, ["deriveKey", "deriveBits"] ) const [publicKey, privateKey] = await Promise.all([ crypto.subtle.exportKey("spki", keypair.value.publicKey), crypto.subtle.exportKey("pkcs8", keypair.value.privateKey) ]) const [publicKeyBase64, privateKeyBase64] = [ Buffer.from(publicKey).toString("base64"), Buffer.from(privateKey).toString("base64") ] // Create the signature for the request const input = { headers: { "privy-app-id": "your-privy-app-id", }, method: "POST", url: `https://api.privy.io/v1/wallets/${walletId}/export`, version: 1, body: { encryption_type: "HPKE", recipient_public_key: publicKeyBase64, }, }; const signature = generateAuthorizationSignature({ input: input, authorizationPrivateKey: "your-privy-authorization-private-key" // This should be the private key of your authorization key }) // Make the request to export the wallet const res = await fetch( `https://api.privy.io/v1/wallets/${walletId}/export`, { method: input.method, headers: { ...input.headers, "Content-Type": "application/json", "privy-authorization-signature": signature as string, Authorization: generateBasicAuthHeader( "your-privy-app-id", "your-privy-app-secret" ), }, body: JSON.stringify(input.body), } ); ``` The endpoint will return the encrypted private key along with the encapsulation information needed for decryption: ```json theme={"system"} { "encryption_type": "HPKE", "ciphertext": "Zb2XqqIpPlQKJhkb9GRoXa8N6pKLAlozYnXg713g7mCu5vvn6tGIRbeJj4XOUQkFeB9DRxKg", "encapsulated_key": "BLplgxEpMz+WMxDSOzGZe+Oa5kkt9FTxUudRRyO5zRj/OaDbUaddlE18uNv8UKxpecnrSy+UByG2C3oJTgTnGNk=" } ``` ### Decrypting the Private Key The exported private key is encrypted using Hybrid Public Key Encryption (HPKE) with the following configuration: * KEM: DHKEM\_P256\_HKDF\_SHA256 * KDF: HKDF\_SHA256 * AEAD: CHACHA20\_POLY1305 * Mode: BASE To decrypt the private key, you'll need to use these same parameters along with your recipient private key. Here's how to implement the decryption in several languages: **TypeScript** ```ts decrypt.ts theme={"system"} import {CipherSuite, DhkemP256HkdfSha256, HkdfSha256} from '@hpke/core'; import {Chacha20Poly1305} from '@hpke/chacha20poly1305'; /** * Decrypts a message using HPKE (Hybrid Public Key Encryption). * * Uses P-256 keys with HPKE to decrypt an encrypted message. The function expects base64-encoded * inputs and handles all necessary key imports and context creation for HPKE decryption. * * @param privateKeyBase64 Base64-encoded private key in PKCS8 format used for decryption. * @param encapsulatedKeyBase64 Base64-encoded raw public key bytes representing the * encapsulated key. * @param ciphertextBase64 Base64-encoded encrypted message using base64url encoding that * will be decrypted. * @returns A Promise that resolves to the decrypted message as a UTF-8 string. * @throws {Error} If decryption fails or if any of the inputs are incorrectly formatted. */ async function decryptHPKEMessage( privateKeyBase64: string, encapsulatedKeyBase64: string, ciphertextBase64: string ): Promise { // 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(privateKeyBase64), { name: 'ECDH', namedCurve: 'P-256' }, true, ['deriveKey', 'deriveBits'] ); // Create recipient context and decrypt const recipient = await suite.createRecipientContext({ recipientKey: privateKey, enc: base64ToBuffer(encapsulatedKeyBase64) }); return new TextDecoder().decode(await recipient.open(base64ToBuffer(ciphertextBase64))); } ``` ```ts example.ts {skip-check} theme={"system"} // The response from the export API endpoint const response = { encryption_type: 'HPKE', ciphertext: 'Zb2XqqIpPlQKJhkb9GRoXa8N6pKLAlozYnXg713g7mCu5vvn6tGIRbeJj4XOUQkFeB9DRxKg', encapsulated_key: 'BLplgxEpMz+WMxDSOzGZe+Oa5kkt9FTxUudRRyO5zRj/OaDbUaddlE18uNv8UKxpecnrSy+UByG2C3oJTgTnGNk=' }; // Replace with your base64-encoded private key const privateKeyBase64 = 'MIGHAgEAMBMGByqGSM49AgEGCCqGSM49AwEHBG0wawIBAQQgU4RP++trEcTL2CPSgX9dKZAQ+XW3Pt49PNI4Elwia1qhRANCAAROjlGlaxrE9rIRc2jy0xmW7ajzkRqccVJsc2WMsXKoB2lG5NllwkSHwWsZkDbmqhYFk/WlDYl/MCiLnYYVtJf+'; try { const privateKey = await decryptHPKEMessage( privateKeyBase64, response.encapsulated_key, response.ciphertext ); console.log('Decrypted private key:', privateKey); } catch (error) { console.error('Failed to decrypt:', error); } ``` **Rust** To export a wallet's private key with the Rust SDK, use the `export` method from the client's `wallets()` interface. This will internally generate an ECDH P-256 keypair and use it to perform an encrypted HPKE operation with the Wallet API to export the wallet's private key. > [!WARNING] > > Wallet export is restricted to wallet owners and enabled by default, unless explicitly disabled by a [`DENY` policy](/controls/policies/overview#policies). Wallets without owners cannot be exported. ### Usage ```rust theme={"system"} use privy_rs::{PrivyClient, AuthorizationContext, JwtUser}; let client = PrivyClient::new(app_id, app_secret)?; // Create authorization context with user JWT let jwt_user = JwtUser(client.clone(), "user-jwt-token".to_string()); let ctx = AuthorizationContext::new().push(jwt_user); // Export the wallet - encryption is handled automatically let exported = client .wallets() .export("wallet-id", &ctx) .await?; println!("Wallet exported successfully: {:?}", exported); ``` ### Parameters and Returns See the Rust SDK documentation for detailed parameter and return types, including embedded examples: * [WalletsClient::export](https://docs.rs/privy-rs/latest/privy_rs/subclients/struct.WalletsClient.html#method.export) * [AuthorizationContext](https://docs.rs/privy-rs/latest/privy_rs/struct.AuthorizationContext.html) For REST API details, see the [API reference](/api-reference/wallets/export). > [!NOTE] > > The Rust SDK automatically handles HPKE encryption internally. The `export` method returns the decrypted private key directly, simplifying the export process while maintaining security through encrypted transmission. **Go** To export a wallet's private key with the Go SDK, use the `Export` method on the `Wallets` service. > [!WARNING] > > Wallet export is restricted to wallet owners and enabled by default, unless explicitly disabled by > a [`DENY` policy](/controls/policies/overview#policies). Wallets without owners cannot be > exported. ### Usage ```go theme={"system"} exported, err := client.Wallets.Export( context.Background(), "wallet-id", privy.WalletExportParams{ EncryptionType: privy.HpkeEncryptionHpke, RecipientPublicKey: "base64-encoded-public-key", }, ) if err != nil { log.Fatalf("failed to export wallet: %v", err) } fmt.Println("Ciphertext:", exported.Ciphertext) fmt.Println("Encapsulated key:", exported.EncapsulatedKey) ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/export) for more details. > [!NOTE] > > The `Export` method returns the encrypted private key (`Ciphertext`) and the encapsulated key > (`EncapsulatedKey`). Your app must decrypt the ciphertext using the corresponding private key for > the `RecipientPublicKey` provided in the request. **Ruby** To export a wallet's private key with the Ruby SDK, use the `export` method on the `wallets` service. > [!WARNING] > > Wallet export is restricted to wallet owners and enabled by default, unless explicitly disabled by > a [`DENY` policy](/controls/policies/overview#policies). Wallets without owners cannot be > exported. ### Usage ```ruby theme={"system"} response = client.wallets.export( "wallet-id", encryption_type: :HPKE, recipient_public_key: "base64-encoded-public-key" ) puts("Ciphertext:", response.ciphertext) puts("Encapsulated key:", response.encapsulated_key) ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/export) for more details. > [!NOTE] > > The `export` method returns the encrypted private key (`ciphertext`) and the encapsulated key > (`encapsulated_key`). Your app must decrypt the ciphertext using the corresponding private key for > the `recipient_public_key` provided in the request. ## Private key formats The format of the exported private key varies depending on the chain type of the wallet: | Chain | Format | Example | | - | - | - | | Ethereum | Hex | `0x...` | | Solana | Base58 | `5K...` | | Spark | Mnemonic | `word1 word2 word3 ...` | | NEAR | ed25519: prefixed | `ed25519:...` | | Sui | suiprivkey bech32 | `suiprivkey...` | | Bitcoin | WIF | `KwDi...` | | All others | Hex | `0x...` | # External IDs Source: https://docs.privy.io/wallets/wallets/external-ids Assign custom external IDs to wallets for referencing with your own identifiers Privy allows your app to assign an `external_id` to any wallet at creation time. External IDs enable your app to reference wallets using your own identifiers, such as order IDs, user references, or other application-specific keys, without maintaining a separate mapping. ## Setting an external ID To assign an external ID, include the `external_id` field when [creating a wallet](/wallets/wallets/create/create-a-wallet). External IDs are **write-once** and cannot be changed after wallet creation. ```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", "external_id": "order-12345", "display_name": "Treasury wallet" }' ``` ### Constraints | Constraint | Detail | | - | - | | Characters | URL-safe only: `a-z`, `A-Z`, `0-9`, `_`, `-` | | Max length | 64 characters | | Uniqueness | Unique per app. No two wallets in the same app can share an `external_id`. | | Mutability | Write-once. Set only at creation and cannot be updated. | ## Addressing wallets by external ID Once a wallet has an `external_id`, your app can use the `ext_wal_` prefix in place of the wallet ID on any endpoint that accepts a wallet ID in the URL path. Prefix `ext_wal_` to the external ID value to form the identifier. For example, if a wallet has `external_id` set to `order-12345`, your app can reference it as `ext_wal_order-12345`: ```bash theme={"system"} # Get a wallet by external ID curl --request GET https://api.privy.io/v1/wallets/ext_wal_order-12345 \ -u ":" \ -H "privy-app-id: " ``` ```bash theme={"system"} # Transfer from a wallet by external ID curl --request POST https://api.privy.io/v1/wallets/ext_wal_order-12345/rpc \ -u ":" \ -H "privy-app-id: " \ -H 'Content-Type: application/json' \ -d '{ ... }' ``` The `ext_wal_` prefix works on all wallet endpoints that include a wallet ID in the path. > [!NOTE] > > When using [authorization signatures](/api-reference/authorization-signatures), sign the request > using the URL exactly as sent, including the `ext_wal_` prefix. ## Display name Your app can also assign a `display_name` to a wallet. Unlike `external_id`, the display name is updatable after creation. | Constraint | Detail | | - | - | | Max length | 100 characters | | Mutability | Can be set at creation and updated via `PATCH`. | To update a display name after creation, use the [update wallet](/wallets/wallets/update-a-wallet) endpoint: ```bash theme={"system"} curl --request PATCH https://api.privy.io/v1/wallets/ \ -u ":" \ -H "privy-app-id: " \ -H 'Content-Type: application/json' \ -d '{ "display_name": "Updated treasury wallet" }' ``` ## Filtering wallets by external ID The [list wallets](/wallets/wallets/get-a-wallet/get-all-wallets) endpoint supports filtering by `external_id` as a query parameter: ```bash theme={"system"} curl --request GET "https://api.privy.io/v1/wallets?external_id=order-12345" \ -u ":" \ -H "privy-app-id: " ``` # Get all wallets Source: https://docs.privy.io/wallets/wallets/get-a-wallet/get-all-wallets Get all wallets for your application using the Privy API Get all wallets for your application. **NodeJS** To fetch all your application's wallets, use the `list` method on the `wallets()` interface of the Privy client. This is a paginated query. ### Usage ```ts theme={"system"} // Will iterate automatically until all wallets are fetched for await (const wallet of privy.wallets().list({chain_type: 'ethereum'})) { // Do something with the wallet } ``` To fetch wallets assigned to a specific user or organization, filter by its entity ID: ```ts theme={"system"} for await (const wallet of privy.wallets().list({ entity_id: 'cm7zx4k9a0000l308abcd1234' })) { // Do something with the wallet } ``` ### Parameters and Returns Check out the [API reference](/api-reference/wallets/get-all) for more details. **Java** To fetch all of your application's wallets, use the `list` method. ```java theme={"system"} try { WalletListRequest request = WalletListRequest.builder() .chainType(WalletChainType.ETHEREUM) .build(); WalletListResponse response = privyClient.wallets().list(request); if (response.wallets().isPresent()) { List wallets = response.wallets().get().data(); } } catch (APIException e) { String errorBody = e.bodyAsString().orElse(e.getMessage()); System.err.println(errorBody); } catch (Exception e) { System.err.println(e.getMessage()); } ``` ### Parameters The `WalletListRequest` object accepts the following parameters, all of which are optional: **chainType** `WalletChainType` The chain type to filter by. **userId** `String` The user ID to filter by. **cursor** `String` The cursor to use for fetching the next page of results, if any. **limit** `Double` The maximum number of wallets to fetch per page. Defaults to `100`. ### Returns The `WalletListResponse` object contains an optional `object()` field, present if the wallets were retrieved successfully. **object()** `Optional\` The retrieved list of wallets. Each of the elements in the list under `.data()` is a `Wallet` object. **id** `String` Unique ID of the created wallet. This will be the primary identifier when using the wallet in the future. **address** `String` Address of the created wallet. **chainType** `WalletChainType` Chain type of the created wallet. **policyIds** `List\` List of policy IDs for policies that are enforced on the wallet. **ownerId** `String` The key quorum ID of the owner of the wallet. **additionalSigners** `List\` The key quorum IDs of the additional signers for the wallet. **createdAt** `double` The creation date of the wallet, as Unix time. **REST API** To fetch your wallets by pages, make a `GET` request to: ``` https://api.privy.io/v1/wallets ``` ### Query In the request query parameters, include any of the following: **cursor** `string` ID of the wallet from which start the search **limit** `number` Max amount of wallets per page **chain\_type** `'ethereum' | 'solana'` Chain type to filter by. **entity\_id** `string` User or organization entity ID to filter by. ### Response In the response, Privy will send back the following if successful: **data** `Array\` List of wallets in the current page **id** `string` Unique ID of the wallet **address** `string` Address of the wallet **chain\_type** `'ethereum' | 'solana'` Chain type of the wallet **policy\_ids** `string[]` List of policy IDs associated with the wallet **owner\_id** `string | null` The key quorum ID of the owner of the wallet. **entity** `\{id: string; type: 'user' | 'organization'\} | null` The user or organization that the wallet belongs to, if one was assigned. **additional\_signers** `\{signer_id: string\}[]` The key quorum IDs of the additional signers for the wallet. **created\_at** `number` The creation date of the wallet, in milliseconds since midnight, January 1, 1970 UTC. **next\_cursor** `string` ID of the wallet from which start the next page ### Example As an example, a sample request to fetch an organization's wallets might look like the following: ```bash theme={"system"} $ curl --request GET https://api.privy.io/v1/wallets?entity_id=cm7zx4k9a0000l308abcd1234&limit=1 \ -u ":" \ -H "privy-app-id: " \ -H 'Content-Type: application/json' \ ``` A successful response will look like the following: ```json theme={"system"} { "data": [ { "id": "yepf6384cu2nkup42gvrwdqh", "address": "0x2F3eb40872143b77D54a6f6e7Cc120464C764c09", "chain_type": "ethereum", "authorization_threshold": 2, "owner_id": "rkiz0ivz254drv1xw982v3jq", "entity": { "id": "cm7zx4k9a0000l308abcd1234", "type": "organization" }, "additional_signers": [], "created_at": 1733923425155 } ], "next_cursor": "u67nttpkeeti2hm9w7aoxdcc" } ``` **Rust** To fetch all your application's wallets, use the `list` method on the `wallets()` interface of the Privy client. This is a paginated query that requires manual pagination. ### Usage ```rust theme={"system"} use privy_rs::{PrivyClient, generated::types::*}; let client = PrivyClient::new(app_id, app_secret)?; // Manual pagination through all wallets let mut all_wallets = Vec::new(); let mut cursor = None; loop { let response = client.wallets().list(&WalletListQuery { chain_type: Some(WalletChainType::Ethereum), user_id: None, limit: Some(50), // Fetch 50 at a time cursor: cursor.clone(), }).await?; all_wallets.extend(response.data); if let Some(next_cursor) = response.next_cursor { cursor = Some(next_cursor); } else { break; // No more pages } } for wallet in all_wallets { println!("Wallet {}: {}", wallet.id, wallet.address); } ``` ### Parameters and Returns See the Rust SDK documentation for detailed parameter and return types, including embedded examples: * [WalletsClient::list](https://docs.rs/privy-rs/latest/privy_rs/subclients/struct.WalletsClient.html#method.list) * [WalletListQuery](https://docs.rs/privy-rs/latest/privy_rs/generated/types/struct.WalletListQuery.html) * [Wallet](https://docs.rs/privy-rs/latest/privy_rs/generated/types/struct.Wallet.html) For REST API details, see the [API reference](/api-reference/wallets/get-all). ### Helper Function Example ```rust theme={"system"} use privy_rs::{PrivyClient, generated::types::*}; async fn fetch_all_wallets_by_chain( client: &PrivyClient, chain_type: WalletChainType, ) -> Result, Box> { let mut all_wallets = Vec::new(); let mut cursor = None; loop { let response = client .wallets() .list(&WalletListQuery { chain_type: Some(chain_type.clone()), user_id: None, limit: Some(50), // Fetch 50 at a time cursor: cursor.clone(), }) .await?; all_wallets.extend(response.data); if let Some(next_cursor) = response.next_cursor { cursor = Some(next_cursor); } else { break; // No more pages } } Ok(all_wallets) } ``` **Go** To fetch all wallets with the Go SDK, use the `ListAutoPaging` method on the `Wallets` service. This handles pagination automatically. ### Usage ```go theme={"system"} iter := client.Wallets.ListAutoPaging(context.Background(), privy.WalletListParams{ ChainType: privy.WalletChainTypeEthereum, }) for iter.Next() { wallet := iter.Current() fmt.Println("Wallet:", wallet.ID, wallet.Address) } if err := iter.Err(); err != nil { log.Fatalf("error listing wallets: %v", err) } ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/get-all) for more details. **Python** Use the `list` method on the wallets service. The returned cursor loads additional pages while iterating. ```python theme={"system"} for wallet in client.wallets.list(): print(wallet.id, wallet.address) ``` **Ruby** To fetch all wallets with the Ruby SDK, use the `list` method on the `wallets` service. The returned cursor handles pagination automatically via `auto_paging_each`. ### Usage ```ruby theme={"system"} page = client.wallets.list(chain_type: :ethereum) page.auto_paging_each do |wallet| puts(wallet.id, wallet.address) end ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/get-all) for more details. # Get user connected wallets Source: https://docs.privy.io/wallets/wallets/get-a-wallet/get-connected-wallet Find all connected wallets for a user including embedded and external wallets A user may come in with both embedded and external wallets. Privy makes it easy to find all of a user's connected wallets so you can help them take an onchain action with the appropriate wallet. It's worth distinguishing connected vs. linked wallets: * **Linked wallets** are embedded or external wallets tied to a user object. They may or may not be connected. * **Connected wallets** are embedded or external wallets currently available for the web client. They may or may not be linked to a user's account. > [!TIP] > > Not seeing all of the external wallets connected to the application? Make sure you have [completed > the steps to connect](/wallets/connectors/setup/configuring-external-connector-wallets) any > wallets to your application. Both **external wallets** (that users *connect* to your site) and **embedded wallets** (that users *create* within your app) result in a unified object representing the wallet. **React** **Ethereum** To access connected wallets with the React SDK, use the `wallets` array from the `useWallets` hook: ```tsx theme={"system"} const wallets: ConnectedWallet[] ``` ### Usage ```tsx theme={"system"} import {useWallets} from '@privy-io/react-auth'; const { wallets } = useWallets(); const desiredWallet = wallets.find((wallet) => wallet.address === desiredAddress); ``` The **`wallets`** array includes an object for all wallets a user has connected to your site. **Solana** To access connected wallets with the React SDK, use the `wallets` array from the `useWallets` hook: ```tsx theme={"system"} const wallets: ConnectedStandardSolanaWallet[] ``` ### Usage ```tsx theme={"system"} import {useWallets} from '@privy-io/react-auth/solana'; const { wallets } = useWallets(); const desiredWallet = wallets.find((wallet) => wallet.address === desiredAddress); ``` The **`wallets`** array includes an object for all wallets a user has connected to your site. The array is ordered from most recently connected to least recently connected. ## Waiting for wallets to be `ready` When your page loads in the user's browser, the Privy SDK determines what wallets the user has connected to your app in two ways: * For **external wallets**, Privy determines what wallets are connected via EIP-6963 for injected wallets (e.g. browser extension wallets) and via WalletConnect for mobile wallets. * For **embedded wallets**, Privy determines if the user has an embedded wallet by loading the Privy iframe which stores the private key material used for the wallet. To determine if Privy has fully processed all external and embedded wallet connections, use the **`ready`** boolean returned by the **`useWallets`** hooks. Concretely, **`ready`** will be `false` while Privy is determining what wallets are available for the user, and will be `true` once Privy has settled on the current set of connected wallets. *** ## `useWallets` vs. `usePrivy` The **`useWallets`** and **`usePrivy`** hooks all return information about a user's wallets. The key difference between the them is: * **`useWallets`** will return all *connected* wallets (EVM and Solana), which you can use to request signatures or take onchain actions (via transactions). * **`usePrivy`** will return all *linked* wallets, which you can use to verify that a user owns a given wallet address. Linked wallets are not necessarily actively connected to your site, so you may not always be able to request a signature or transaction from them. Similarly, connected wallets are not necessarily linked, as a user may have connected their wallet without signing a message to verify that they own the wallet address. Concretely, if your use case only requires you to verify that a user owns a given wallet address, you should use the wallets information returned by the **`usePrivy`** hook. ### Accessing linked wallets To access linked wallets (both embedded and external wallets tied to the user account), use `user.linkedAccounts` from the **`usePrivy`** hook: ```tsx theme={"system"} import {usePrivy} from '@privy-io/react-auth'; const { user } = usePrivy(); // Filter for wallet accounts from all linked accounts const linkedWallets = user?.linkedAccounts.filter( (account) => account.type === 'wallet' || account.type === 'smart_wallet' ); // Access wallet addresses linkedWallets?.forEach((wallet) => { console.log(wallet.address); }); ``` Otherwise, if your use case requires you to take actions on a *connected* wallet, such as getting its network or requesting a signature or transaction, you should use the wallets information returned by the **`useWallets`** hooks instead. **React Native** Wait for `isReady` from `usePrivy` before using these wallets. Expo `isReady` already includes embedded wallet initialization, so there is no separate `useWallets` `ready` flag. **Ethereum** To access connected wallets with the React Native SDK, use the `wallets` array from the `useEmbeddedEthereumWallet` hook: ```tsx theme={"system"} const wallets: ConnectedEthereumWallet[] ``` ### Usage ```tsx theme={"system"} import {useEmbeddedEthereumWallet} from '@privy-io/expo'; const {wallets} = useEmbeddedEthereumWallet(); const desiredWallet = wallets.find((wallet) => wallet.address === desiredAddress); ``` **Solana** To access connected wallets with the React Native SDK, use the `wallets` array from the `useEmbeddedSolanaWallet` hook: ```tsx theme={"system"} const wallets: ConnectedSolanaWallet[] ``` ### Usage ```tsx theme={"system"} import {useEmbeddedSolanaWallet} from '@privy-io/expo'; const {wallets} = useEmbeddedSolanaWallet(); const desiredWallet = wallets.find((wallet) => wallet.address === desiredAddress); ``` **Swift** **Ethereum** To access connected Ethereum wallets using the Swift SDK, use the `embeddedEthereumWallets` property from the `PrivyUser` object: ```swift theme={"system"} var embeddedEthereumWallets: [EmbeddedEthereumWallet] { get } ``` ### Usage ```swift theme={"system"} guard let user = privy.user else { // If user is null, user is not authenticated return } // Retrieve list of user's embedded Ethereum wallets let ethereumWallets = user.embeddedEthereumWallets // Grab the desired wallet. Here, we retrieve the first wallet guard let ethereumWallet = ethereumWallets.first else { // No ETH wallets return } ``` **Solana** To access connected Solana wallets using the Swift SDK, use the `embeddedSolanaWallets` property from the `PrivyUser` object: ```swift theme={"system"} var embeddedSolanaWallets: [EmbeddedSolanaWallet] { get } ``` ### Usage ```swift theme={"system"} guard let user = privy.user else { // User is not authenticated return } // Retrieve list of user's embedded Solana wallets let solanaWallets = user.embeddedSolanaWallets // Grab the desired wallet. Here, we retrieve the first wallet guard let solanaWallet = solanaWallets.first else { // No SOL wallets return } ``` **Android** **Ethereum** To access connected Ethereum wallets using the Android SDK, use the `embeddedEthereumWallets` property from the `PrivyUser` object: ```kotlin theme={"system"} val embeddedEthereumWallets: List ``` ### Usage ```kotlin theme={"system"} val user = privy.user if (user != null) { val ethereumWallets = user.embeddedEthereumWallets if (ethereumWallets.isNotEmpty()) { val ethereumWallet = ethereumWallets.first() } } ``` The **`embeddedEthereumWallets`** property includes an array of objects for all Ethereum wallets a user has connected to your app. The array is ordered from most recently connected to least recently connected. **Solana** To access connected Solana wallets using the Android SDK, use the `embeddedSolanaWallets` property from the `PrivyUser` object: ```kotlin theme={"system"} val embeddedSolanaWallets: List ``` ### Usage ```kotlin theme={"system"} val user = privy.user if (user != null) { val solanaWallets = user.embeddedSolanaWallets if (solanaWallets.isNotEmpty()) { val solanaWallet = solanaWallets.first() } } ``` The **`embeddedSolanaWallets`** property includes an array of objects for all Solana wallets a user has connected to your app. The array is ordered from most recently connected to least recently connected. **Flutter** **Ethereum** To retrieve a user's Ethereum wallet using the Flutter SDK, follow these steps: ```dart theme={"system"} final user = privy.user; final EmbeddedEthereumWallet ethereumWallet = user.embeddedEthereumWallets.first; ``` ### Usage ```dart theme={"system"} import 'package:privy_flutter/privy_flutter.dart'; // Get the user's first Ethereum wallet final user = privy.user; if (user != null && user.embeddedEthereumWallets.isNotEmpty) { final wallet = user.embeddedEthereumWallets.first; // Access wallet properties final address = wallet.address; } ``` The **`embeddedEthereumWallets`** property includes an array of objects for all Ethereum wallets a user has connected to your app. The array is ordered from most recently connected to least recently connected. **Solana** To retrieve a user's Solana wallet using the Flutter SDK, follow these steps: ```dart theme={"system"} final user = privy.user; final EmbeddedSolanaWallet solanaWallet = user.embeddedSolanaWallets.first; ``` ### Usage ```dart theme={"system"} import 'package:privy_flutter/privy_flutter.dart'; // Get the user's first Solana wallet final user = privy.user; if (user != null && user.embeddedSolanaWallets.isNotEmpty) { final wallet = user.embeddedSolanaWallets.first; // Access wallet properties final address = wallet.address; } ``` The **`embeddedSolanaWallets`** property includes an array of objects for all Solana wallets a user has connected to your app. The array is ordered from most recently connected to least recently connected. # Get wallet by external ID Source: https://docs.privy.io/wallets/wallets/get-a-wallet/get-wallet-by-external-id Look up a wallet using its external ID with the ext_wal_ prefix If a wallet has an [external ID](/wallets/wallets/external-ids), your app can use the `ext_wal_` prefix in place of the wallet ID to look it up. Prefix `ext_wal_` to the external ID value to form the identifier. For example, if a wallet has `external_id` set to `order-12345`, your app can reference it as `ext_wal_order-12345`: ```bash theme={"system"} curl --request GET https://api.privy.io/v1/wallets/ext_wal_order-12345 \ -u ":" \ -H "privy-app-id: " ``` The `ext_wal_` prefix works on all wallet endpoints that include a wallet ID in the path, including [get wallet by ID](/wallets/wallets/get-a-wallet/get-wallet-by-id), [update wallet](/wallets/wallets/update-a-wallet), and RPC endpoints. > [!NOTE] > > When using [authorization signatures](/api-reference/authorization-signatures), sign the request > using the URL exactly as sent, including the `ext_wal_` prefix. # Get wallet by ID Source: https://docs.privy.io/wallets/wallets/get-a-wallet/get-wallet-by-id Get a specific wallet by its unique ID from the Privy API You can get a specific wallet by its ID from the Privy API. This is a wallet-centric abstraction. To find all wallets belonging to a user or organization, [filter wallets by the entity ID](/wallets/wallets/get-a-wallet/get-all-wallets). **NodeJS** To get a wallet by ID, use the `get` method on the `wallets()` interface of the Privy client. ### Usage ```tsx theme={"system"} const wallet = await privy.wallets().get(walletId); ``` ### Parameters and Returns Check out the [API reference](/api-reference/wallets/get) for more details. **Java** To get a wallet by ID, use the `retrieve` method. ```java theme={"system"} try { WalletRetrieveResponse response = privyClient.wallets().retrieve(walletId); if (response.wallet().isPresent()) { Wallet wallet = response.wallet().get(); } } catch (APIException e) { String errorBody = e.bodyAsString().orElse(e.getMessage()); System.err.println(errorBody); } catch (Exception e) { System.err.println(e.getMessage()); } ``` ### Parameters **walletId** `String` *required* The ID of the wallet to retrieve ### Returns The `WalletRetrieveResponse` object contains an optional `wallet()` field, present if the wallet was retrieved successfully. **wallet()** `Optional\` The retrieved `Wallet` object. **id** `String` Unique ID of the created wallet. This will be the primary identifier when using the wallet in the future. **address** `String` Address of the created wallet. **chainType** `WalletChainType` Chain type of the created wallet. **policyIds** `List\` List of policy IDs for policies that are enforced on the wallet. **ownerId** `String` The key quorum ID of the owner of the wallet. **additionalSigners** `List\` The key quorum IDs of the additional signers for the wallet. **createdAt** `double` The creation date of the wallet, in milliseconds since midnight, January 1, 1970 UTC. **REST API** To get a wallet by ID, make a `GET` request to: ``` https://api.privy.io/v1/wallets/[wallet-id] ``` ### Response **data** `WalletApiWalletResponseType` The wallet object **id** `string` Unique ID of the wallet **address** `string` Address of the wallet **chain\_type** `'ethereum' | 'solana'` Chain type of the wallet **policy\_ids** `string[]` List of policy IDs associated with the wallet **owner\_id** `string | null` The key quorum ID of the owner of the wallet. **entity** `\{id: string; type: 'user' | 'organization'\} | null` The user or organization that the wallet belongs to, if one was assigned. **additional\_signers** `\{signer_id: string\}[]` The key quorum IDs of the additional signers for the wallet. **created\_at** `number` The creation date of the wallet, in milliseconds since midnight, January 1, 1970 UTC. **external\_id** `string | null` The external identifier assigned to the wallet, if set. **display\_name** `string | null` The display name assigned to the wallet, if set. **Rust** To get a wallet by ID, use the `get` method on the `wallets()` interface of the Privy client. ### Usage ```rust theme={"system"} use privy_rs::PrivyClient; let client = PrivyClient::new(app_id, app_secret)?; let wallet = client.wallets().get("wallet-id").await?; println!("Retrieved wallet {} with address {}", wallet.id, wallet.address); ``` ### Parameters and Returns See the Rust SDK documentation for detailed parameter and return types, including embedded examples: * [WalletsClient::get](https://docs.rs/privy-rs/latest/privy_rs/subclients/struct.WalletsClient.html#method.get) * [Wallet](https://docs.rs/privy-rs/latest/privy_rs/generated/types/struct.Wallet.html) For REST API details, see the [API reference](/api-reference/wallets/get). **Go** To get a wallet by ID with the Go SDK, use the `Get` method on the `Wallets` service. ### Usage ```go theme={"system"} wallet, err := client.Wallets.Get(context.Background(), "wallet-id") if err != nil { log.Fatalf("failed to get wallet: %v", err) } fmt.Println("Wallet:", wallet.ID, wallet.Address, wallet.ChainType) ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/get) for more details. **Python** Use the `get` method on the wallets service. ```python theme={"system"} wallet = client.wallets.get(wallet_id) ``` **Ruby** To get a wallet by ID with the Ruby SDK, use the `get` method on the `wallets` service. ### Usage ```ruby theme={"system"} wallet = client.wallets.get("wallet-id") puts(wallet.id, wallet.address, wallet.chain_type) ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/get) for more details. # Architecture Source: https://docs.privy.io/wallets/wallets/import-a-wallet/architecture Architecture for securely importing wallets without exposing entropy to intermediary services **Privy uses end-to-end encryption to securely transmit your wallet entropy into Privy's TEE infrastructure.** This enables you to import a wallet from your client, server, or TEE without exposing your wallet entropy to any intermediary services. The wallet import process takes place over three steps: ### Step 1. Initializing key import In order to end-to-end encrypt an imported wallet directly to the TEE, an asymmetric encryption key is required. Send an initialization request to the Privy API to retrieve a temporary public key used for encrypting private key material before transmission. ### Step 2: Encrypting your private key Encrypt the wallet’s private key using Hybrid Public Key Encryption (HPKE) with the retrieved public key. This ensures the private key remains encrypted during transit and can only be decrypted within the TEE. ### Step 3: Submitting the encrypted key Send the encrypted payload to the Privy API. Within the TEE, the encrypted payload is decrypted and the private key is used to initialize a new wallet. The temporary keypair used for the import process is destroyed to maintain forward secrecy. # HD wallets Source: https://docs.privy.io/wallets/wallets/import-a-wallet/hd-wallets Import HD wallets using seed phrases to migrate wallets from external providers to Privy **Privy enables you to import HD wallets for use via the Privy API.** This allows you to migrate wallets from external sources to Privy, including from a different wallet provider. Or, your app can enable users to bring an existing external wallet into your application in order to access and manage their assets within your app seamlessly. ## Importing a wallet To import an HD wallet, you'll provide your 12 or 24 word BIP39 mnemonic and the specific address and index you'd like to import. > [!NOTE] > > If you'd like to import multiple addresses for the same HD wallet, you can make multiple import calls with the same mnemonic and each index/address pair you'd like to import. **React** To import an HD wallet with the React SDK, use `importSeedPhrase` from the `useImportSeedPhrase` hook. The SDK validates the seed phrase and derives the address. It then encrypts the seed phrase for the TEE. See [architecture](/wallets/wallets/import-a-wallet/architecture) for more details. ```tsx theme={"system"} importSeedPhrase: (input: { mnemonic: string; chainType: 'ethereum' | 'solana'; additionalSigners?: SessionSignerInput; }) => Promise<{user: User; wallet: Wallet}>; ``` > [!NOTE] > > Seed phrase imports require [TEE execution](/security/wallet-infrastructure/architecture). ### Usage ```tsx theme={"system"} import {useImportSeedPhrase} from '@privy-io/react-auth'; const {importSeedPhrase} = useImportSeedPhrase(); const {user, wallet} = await importSeedPhrase({ mnemonic: 'your twelve or twenty four word seed phrase', chainType: 'ethereum' // or 'solana' }); ``` ### Parameters **mnemonic** `string` The 12 or 24 word BIP39 mnemonic of the wallet to import. Extra whitespace is ignored. **chainType** `'ethereum' | 'solana'` The chain to derive the imported wallet for. Use `'ethereum'` for EVM wallets. **additionalSigners** `SessionSignerInput` Optional additional signers for the wallet. ### Returns **user** `User` The updated user, including the newly imported wallet account. **wallet** `Wallet` The imported wallet account. The method throws if the seed phrase is not a valid English BIP39 mnemonic. It also throws if the user is not authenticated. **Swift** To import an HD wallet with the Swift SDK, use the `importWallet` method from the `PrivyUser` instance with a `.mnemonic` secret. The SDK encrypts the seed phrase on the device for secure transmission to the TEE. See [architecture](/wallets/wallets/import-a-wallet/architecture) for more details. ```swift theme={"system"} func importWallet( chainType: WalletImportSupportedChainType, address: String, secret: WalletImportSecret ) async throws -> any EmbeddedWallet ``` > [!NOTE] > > Wallet import requires [TEE execution](/security/wallet-infrastructure/architecture) and version > 2.18.0 or later of the Swift SDK. ### Usage **Ethereum** ```swift theme={"system"} import PrivySDK let wallet = try await user.importWallet( chainType: .ethereum, address: "", secret: .mnemonic("", index: 0) ) let ethereumWallet = wallet as? EmbeddedEthereumWallet ``` **Solana** ```swift theme={"system"} import PrivySDK let wallet = try await user.importWallet( chainType: .solana, address: "", secret: .mnemonic("", index: 0) ) let solanaWallet = wallet as? EmbeddedSolanaWallet ``` ### Parameters **chainType** `WalletImportSupportedChainType` The chain of the wallet to import: `.ethereum` or `.solana`. **address** `String` The address of the wallet to import: a `0x`-prefixed address for Ethereum, or a base58 address for Solana. Privy rejects the import if it does not match the wallet derived at `index`. **secret** `WalletImportSecret` The seed phrase to import, as `.mnemonic(String, index: Int)`. The phrase is a 12 or 24 word English BIP39 mnemonic, with words separated by single spaces. BIP39 passphrases are not supported. Privy imports the wallet at `index` of the standard derivation path: `m/44'/60'/0'/0/{index}` for Ethereum and `m/44'/501'/{index}'/0'` for Solana. Use `0` for the first address of the wallet. ### Returns **wallet** `any EmbeddedWallet` The imported wallet: an `EmbeddedEthereumWallet` for Ethereum, or an `EmbeddedSolanaWallet` for Solana. Its `imported` property is `true`. ### Errors The method throws a `PrivyError` with an `embeddedWalletFailure` reason when the import fails: | Reason | When | | - | - | | `importNotSupported` | The app does not use TEE execution. | | `invalidAddress` | The address is not valid for the chain. | | `invalidSeedPhrase` | The seed phrase is not a valid English BIP39 mnemonic. | | `addressMismatch` | The wallet at `index` does not have this address. | | `walletAlreadyExists` | The wallet was already imported, by this user or another user. | | `importFailed` | The import failed for another reason. The message has the detail. | The method also throws if the user is not authenticated. **Android** To import an HD wallet with the Android SDK, use the `importWallet` method from the `PrivyUser` instance with a `WalletImportSecret.Mnemonic` secret. The SDK encrypts the seed phrase on the device for secure transmission to the TEE. See [architecture](/wallets/wallets/import-a-wallet/architecture) for more details. ```kotlin theme={"system"} public suspend fun importWallet( chainType: WalletImportSupportedChainType, address: String, secret: WalletImportSecret, ): Result ``` > [!NOTE] > > Wallet import requires [TEE execution](/security/wallet-infrastructure/architecture) and version > 0.18.0 or later of the Android SDK. ### Usage **Ethereum** ```kotlin theme={"system"} import io.privy.wallet.WalletImportSecret import io.privy.wallet.WalletImportSupportedChainType import io.privy.wallet.ethereum.EmbeddedEthereumWallet user.importWallet( chainType = WalletImportSupportedChainType.Ethereum, address = "", secret = WalletImportSecret.Mnemonic("", index = 0), ).onSuccess { wallet -> val ethereumWallet = wallet as EmbeddedEthereumWallet }.onFailure { error -> // error is a WalletImportException } ``` **Solana** ```kotlin theme={"system"} import io.privy.wallet.WalletImportSecret import io.privy.wallet.WalletImportSupportedChainType import io.privy.wallet.solana.EmbeddedSolanaWallet user.importWallet( chainType = WalletImportSupportedChainType.Solana, address = "", secret = WalletImportSecret.Mnemonic("", index = 0), ).onSuccess { wallet -> val solanaWallet = wallet as EmbeddedSolanaWallet }.onFailure { error -> // error is a WalletImportException } ``` ### Parameters **chainType** `WalletImportSupportedChainType` The chain of the wallet to import: `WalletImportSupportedChainType.Ethereum` or `WalletImportSupportedChainType.Solana`. **address** `String` The address of the wallet to import: a `0x`-prefixed address for Ethereum, or a base58 address for Solana. Privy rejects the import if it does not match the wallet derived at `index`. **secret** `WalletImportSecret` The seed phrase to import, as `WalletImportSecret.Mnemonic(phrase, index)`. The phrase is a 12 or 24 word English BIP39 mnemonic, with words separated by single spaces. BIP39 passphrases are not supported. Privy imports the wallet at `index` of the standard derivation path: `m/44'/60'/0'/0/{index}` for Ethereum and `m/44'/501'/{index}'/0'` for Solana. Use `0` for the first address of the wallet. ### Returns **wallet** `Result\` A `Result` containing the imported wallet: an `EmbeddedEthereumWallet` for Ethereum, or an `EmbeddedSolanaWallet` for Solana. Its `imported` property is `true`. ### Errors When the import fails, the `Result` contains a `WalletImportException`: | Exception | When | | - | - | | `ImportNotSupported` | The app does not use TEE execution. | | `InvalidAddress` | The address is not valid for the chain. | | `InvalidSeedPhrase` | The seed phrase is not a valid English BIP39 mnemonic. | | `AddressMismatch` | The wallet at `index` does not have this address. | | `WalletAlreadyExists` | The wallet was already imported, by this user or another user. | | `ImportFailed` | The import failed for another reason, including when the user is not authenticated. | **NodeJS** To import an HD wallet with the NodeJS SDK, use the `import` method from the Privy client's `wallets()` interface. The Privy client will encrypt your mnemonic for secure transmission to the TEE. See [architecture](/wallets/wallets/import-a-wallet/architecture) for more details. **Ethereum** ```ts theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: 'your-app-id', appSecret: 'your-app-secret' }); try { const wallet = await privy.wallets().import({ wallet: { entropy_type: 'hd', address: '', chain_type: 'ethereum', private_key: '', index: 0 // your wallet index, typically 0 for the first address } }); } catch (error) { console.error('Failed to import wallet:', error); } ``` **Solana** ```ts theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: 'your-app-id', appSecret: 'your-app-secret' }); try { const wallet = await privy.wallets().import({ wallet: { entropy_type: 'hd', address: '', chain_type: 'solana', private_key: '', index: 0 // your wallet index, typically 0 for the first address } }); } catch (error) { console.error('Failed to import wallet:', error); } ``` The returned wallet is type `Wallet`. See [get wallet by ID](/api-reference/wallets/get) for type definition. When importing a wallet, it is also possible to provide an owner, policies, or signers. ```ts theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: 'your-app-id', appSecret: 'your-app-secret' }); const wallet = await privy.wallets().import({ wallet: { entropy_type: 'hd', address: '', chain_type: 'solana', // or "ethereum" private_key: '', index: 0 // your wallet index, typically 0 for the first address }, owner_id: '', additional_signers: [{signer_id: ''}], policy_ids: [''] }); ``` **REST API** > [!NOTE] > > The REST API flow is the same for both EVM and Solana chains. Simply use `"chain_type": > "ethereum"` for EVM wallets or `"chain_type": "solana"` for Solana wallets in the requests below. ### 1. Initialization Initialize a key import flow by calling the `/v1/wallets/import/init` endpoint with your wallet address, index, and chain type. See [authentication](/api-reference/introduction#authentication) for how to encode your app credentials. ```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": "hd", "index": , "encryption_type": "HPKE" }' ``` The endpoint will return a public key to encrypt your private key with: ```json theme={"system"} { "encryption_public_key": "", "encryption_type": "HPKE" } ``` ### 2. Encryption Encrypt your private key using Hybrid Public Key Encryption (HPKE) with the following configuration: * KEM: DHKEM\_P256\_HKDF\_SHA256 * KDF: HKDF\_SHA256 * AEAD: CHACHA20\_POLY1305 * Mode: BASE There are two outputs from the encryption step that you'll provide to Privy during submission: * `ciphertext`: The encrypted private key * `encapsulated_key`: The encapsulated key Here's an example of how to encrypt a private key in TypeScript: ```ts theme={"system"} import {Chacha20Poly1305} from '@hpke/chacha20poly1305'; import {CipherSuite, DhkemP256HkdfSha256, HkdfSha256} from '@hpke/core'; import {base64} from '@scure/base'; const encryptWithHpke = async ({ encryptionPublicKey, plaintextMnemonic }: { encryptionPublicKey: Uint8Array; plaintextMnemonic: Uint8Array; }) => { // Deserialize the raw key returned by the `init` request to the Privy API to a public key object const suite = new CipherSuite({ kem: new DhkemP256HkdfSha256(), kdf: new HkdfSha256(), aead: new Chacha20Poly1305() }); const publicKeyObject = await suite.kem.deserializePublicKey( Buffer.from(encryptionPublicKey).buffer ); // Encrypt the plaintext wallet mnemonic const sender = await suite.createSenderContext({ recipientPublicKey: publicKeyObject }); const ciphertext = await sender.seal(Buffer.from(plaintextMnemonic).buffer); // Return the encapsulated key and ciphertext, converting ArrayBuffer to Uint8Array return { encapsulatedKey: new Uint8Array(sender.enc), ciphertext: new Uint8Array(ciphertext) }; }; // The encryption public key is returned by the `init` request to the Privy API // For example: BPoOQ5k9nRk37v+XQWkmFEjpvW6RS0HQsPF3+IbhgMlc2Qwp/vz7lln1h0MJj/l0crLUhyyjdmC9RnAcpAkUNVQ= const base64EncodedEncryptionPublicKey = ''; // The wallet's BIP39 mnemonic. // For example: "solution tree shed picnic exile caught pluck hammer flag strategy surprise fiber" const plaintextMnemonic = ''; const {encapsulatedKey, ciphertext} = await encryptWithHpke({ encryptionPublicKey: base64.decode(base64EncodedEncryptionPublicKey), plaintextMnemonic: new Uint8Array(Buffer.from(plaintextMnemonic, 'utf-8')) }); ``` ### 3. Submission Submit your encrypted mnemonic to the Privy API by calling the `/v1/wallets/import/submit` endpoint with the rest of your wallet configuration (e.g. an owner, policies, or signers). See [creating a wallet](/api-reference/wallets/create) for more information on configuration options. ```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": "hd", "index": , "encryption_type": "HPKE", "ciphertext": "", "encapsulated_key": "" }, // Optional additional configuration "owner": {...}, "policy_ids": [...], "additional_singers": [...] }' ``` The endpoint will return the wallet object of your imported wallet: ```json theme={"system"} { "id": "", "address": "", "chain_type": "ethereum", "policy_ids": [], "additional_signers": [], "exported_at": null, "imported_at": 1753300563195, "created_at": 1753300563197, "owner_id": null } ``` **Go** To import an HD wallet with the Go SDK, use the `Import` method. The SDK automatically handles HPKE encryption for secure transmission to the TEE. See [architecture](/wallets/wallets/import-a-wallet/architecture) for more details. **Ethereum** ```go theme={"system"} import ( "context" "fmt" ) wallet, err := client.Wallets.Import(context.Background(), privy.WalletImportParams{ Wallet: privy.WalletImportParamsWalletUnion{ OfHD: &privy.WalletImportParamsWalletHD{ Address: "", ChainType: "ethereum", Index: 0, // your wallet index, typically 0 for the first address PrivateKey: []byte(""), }, }, }) if err != nil { log.Fatalf("failed to import wallet: %v", err) } fmt.Println("Imported wallet:", wallet.ID, wallet.Address) ``` **Solana** ```go theme={"system"} import ( "context" "fmt" ) wallet, err := client.Wallets.Import(context.Background(), privy.WalletImportParams{ Wallet: privy.WalletImportParamsWalletUnion{ OfHD: &privy.WalletImportParamsWalletHD{ Address: "", ChainType: "solana", Index: 0, // your wallet index, typically 0 for the first address PrivateKey: []byte(""), }, }, }) if err != nil { log.Fatalf("failed to import wallet: %v", err) } fmt.Println("Imported wallet:", wallet.ID, wallet.Address) ``` The returned wallet is type `*Wallet`. See [get wallet by ID](/api-reference/wallets/get) for type definition. When importing a wallet, it is also possible to provide an owner, policies, or signers. ```go theme={"system"} wallet, err := client.Wallets.Import(context.Background(), privy.WalletImportParams{ Wallet: privy.WalletImportParamsWalletUnion{ OfHD: &privy.WalletImportParamsWalletHD{ Address: "", ChainType: "ethereum", // or "solana" Index: 0, PrivateKey: []byte(""), }, }, OwnerID: param.NewOpt(""), AdditionalSigners: []privy.WalletSubmitImportParamsAdditionalSigner{{SignerID: ""}}, PolicyIDs: []string{""}, }) ``` ## Using imported wallets Imported wallets function the same way as Privy-generated wallets. See the API reference for [Ethereum](/api-reference/wallets/ethereum/eth-send-transaction) or [Solana](/api-reference/wallets/solana/sign-and-send-transaction) for information about how to send transactions and execute other wallet operations. # Private keys Source: https://docs.privy.io/wallets/wallets/import-a-wallet/private-key Import wallets using private keys to migrate from external providers to Privy **Privy enables you to import wallet private keys for use via the Privy API.** This allows you to migrate wallets from external sources to Privy, including from a different wallet provider. Or, your app can enable users to bring an existing external wallet into your application in order to access and manage their assets within your app seamlessly. ## Importing a wallet **React** To import a private key wallet with the React SDK, use the `importWallet` method from the `useImportWallet` hook: ```tsx theme={"system"} importWallet: (input: {privateKey: string}) => Promise; ``` **EVM** ### Usage ```tsx theme={"system"} import {useImportWallet} from '@privy-io/react-auth'; const {importWallet} = useImportWallet(); const wallet = await importWallet({ privateKey: 'your-wallet-private-key' }); ``` ### Parameters **privateKey** `string` The hex-encoded private key of the ethereum wallet to import, with or without a `0x` prefix. ### Returns **wallet** `Promise\` A `Promise` for the imported wallet object. **Solana** ### Usage ```tsx theme={"system"} import {useImportWallet} from '@privy-io/react-auth/solana'; const {importWallet} = useImportWallet(); const wallet = await importWallet({ privateKey: 'your-wallet-private-key' }); ``` ### Parameters **privateKey** `string` The base58-encoded private key of the solana wallet to import. ### Returns **wallet** `Promise\` A `Promise` for the imported wallet object. **React Native** To import a private key wallet with the Expo SDK, use the `importWallet` method from the `useImportWallet` hook. A single hook handles both EVM and Solana wallets via the `chainType` parameter: ```tsx theme={"system"} importWallet: (input: { privateKey: string; chainType: 'ethereum' | 'solana'; additionalSigners?: SessionSignerInput; }) => Promise<{user: User; wallet: WalletWithMetadata}>; ``` **EVM** ### Usage ```tsx theme={"system"} import {useImportWallet} from '@privy-io/expo'; const {importWallet} = useImportWallet(); const {user, wallet} = await importWallet({ privateKey: 'your-wallet-private-key', chainType: 'ethereum' }); ``` ### Parameters **privateKey** `string` The hex-encoded private key of the ethereum wallet to import, with or without a `0x` prefix. **chainType** `'ethereum' | 'solana'` The chain type of the wallet to import. Use `'ethereum'` for EVM wallets. **additionalSigners** `SessionSignerInput` Optional additional signers for the wallet. Only supported for TEE wallets. ### Returns **user** `User` The updated user, including the newly imported wallet account. **wallet** `WalletWithMetadata` The imported wallet account. **Solana** ### Usage ```tsx theme={"system"} import {useImportWallet} from '@privy-io/expo'; const {importWallet} = useImportWallet(); const {user, wallet} = await importWallet({ privateKey: 'your-wallet-private-key', chainType: 'solana' }); ``` ### Parameters **privateKey** `string` The base58-encoded private key of the solana wallet to import. **chainType** `'ethereum' | 'solana'` The chain type of the wallet to import. Use `'solana'` for Solana wallets. **additionalSigners** `SessionSignerInput` Optional additional signers for the wallet. Only supported for TEE wallets. ### Returns **user** `User` The updated user, including the newly imported wallet account. **wallet** `WalletWithMetadata` The imported wallet account. **Swift** To import a private key wallet with the Swift SDK, use the `importWallet` method from the `PrivyUser` instance. The SDK encrypts the private key on the device for secure transmission to the TEE. See [architecture](/wallets/wallets/import-a-wallet/architecture) for more details. ```swift theme={"system"} func importWallet( chainType: WalletImportSupportedChainType, address: String, secret: WalletImportSecret ) async throws -> any EmbeddedWallet ``` > [!NOTE] > > Wallet import requires [TEE execution](/security/wallet-infrastructure/architecture) and version > 2.18.0 or later of the Swift SDK. **EVM** ### Usage ```swift theme={"system"} import PrivySDK // The raw 32-byte private key. Decode a hex-encoded key to bytes first. let privateKey: Data = let wallet = try await user.importWallet( chainType: .ethereum, address: "", secret: .privateKey(privateKey) ) let ethereumWallet = wallet as? EmbeddedEthereumWallet ``` ### Parameters **chainType** `WalletImportSupportedChainType` The chain of the wallet to import. Use `.ethereum` for EVM wallets. **address** `String` The `0x`-prefixed address of the wallet to import. Privy rejects the import if the private key does not control this address. **secret** `WalletImportSecret` The private key to import, as `.privateKey(Data)` with the raw 32-byte secp256k1 private key. ### Returns **wallet** `any EmbeddedWallet` The imported wallet, an `EmbeddedEthereumWallet`. Its `imported` property is `true`. **Solana** ### Usage ```swift theme={"system"} import PrivySDK // The raw 64-byte secret key, or its first 32 bytes. Decode a base58-encoded key to bytes first. let secretKey: Data = let wallet = try await user.importWallet( chainType: .solana, address: "", secret: .privateKey(secretKey) ) let solanaWallet = wallet as? EmbeddedSolanaWallet ``` ### Parameters **chainType** `WalletImportSupportedChainType` The chain of the wallet to import. Use `.solana` for Solana wallets. **address** `String` The base58 address of the wallet to import. Privy rejects the import if the private key does not control this address. **secret** `WalletImportSecret` The private key to import, as `.privateKey(Data)`. Pass the 64-byte secret key (the 32-byte seed followed by the public key, as wallets such as Phantom export it) or the 32-byte seed. ### Returns **wallet** `any EmbeddedWallet` The imported wallet, an `EmbeddedSolanaWallet`. Its `imported` property is `true`. ### Errors The method throws a `PrivyError` with an `embeddedWalletFailure` reason when the import fails: | Reason | When | | - | - | | `importNotSupported` | The app does not use TEE execution. | | `invalidAddress` | The address is not valid for the chain. | | `invalidPrivateKey` | The private key is not valid for the chain. | | `addressMismatch` | The private key does not control the address. | | `walletAlreadyExists` | The wallet was already imported, by this user or another user. | | `importFailed` | The import failed for another reason. The message has the detail. | The method also throws if the user is not authenticated. **Android** To import a private key wallet with the Android SDK, use the `importWallet` method from the `PrivyUser` instance. The SDK encrypts the private key on the device for secure transmission to the TEE. See [architecture](/wallets/wallets/import-a-wallet/architecture) for more details. ```kotlin theme={"system"} public suspend fun importWallet( chainType: WalletImportSupportedChainType, address: String, secret: WalletImportSecret, ): Result ``` > [!NOTE] > > Wallet import requires [TEE execution](/security/wallet-infrastructure/architecture) and version > 0.18.0 or later of the Android SDK. **EVM** ### Usage ```kotlin theme={"system"} import io.privy.wallet.WalletImportSecret import io.privy.wallet.WalletImportSupportedChainType import io.privy.wallet.ethereum.EmbeddedEthereumWallet // The raw 32-byte private key. Decode a hex-encoded key to bytes first. val privateKey: ByteArray = user.importWallet( chainType = WalletImportSupportedChainType.Ethereum, address = "", secret = WalletImportSecret.PrivateKey(privateKey), ).onSuccess { wallet -> val ethereumWallet = wallet as EmbeddedEthereumWallet }.onFailure { error -> // error is a WalletImportException } ``` ### Parameters **chainType** `WalletImportSupportedChainType` The chain of the wallet to import. Use `WalletImportSupportedChainType.Ethereum` for EVM wallets. **address** `String` The `0x`-prefixed address of the wallet to import. Privy rejects the import if the private key does not control this address. **secret** `WalletImportSecret` The private key to import, as `WalletImportSecret.PrivateKey(bytes)` with the raw 32-byte secp256k1 private key. ### Returns **wallet** `Result\` A `Result` containing the imported wallet, an `EmbeddedEthereumWallet`. Its `imported` property is `true`. **Solana** ### Usage ```kotlin theme={"system"} import io.privy.wallet.WalletImportSecret import io.privy.wallet.WalletImportSupportedChainType import io.privy.wallet.solana.EmbeddedSolanaWallet // The raw 64-byte secret key, or its first 32 bytes. Decode a base58-encoded key to bytes first. val secretKey: ByteArray = user.importWallet( chainType = WalletImportSupportedChainType.Solana, address = "", secret = WalletImportSecret.PrivateKey(secretKey), ).onSuccess { wallet -> val solanaWallet = wallet as EmbeddedSolanaWallet }.onFailure { error -> // error is a WalletImportException } ``` ### Parameters **chainType** `WalletImportSupportedChainType` The chain of the wallet to import. Use `WalletImportSupportedChainType.Solana` for Solana wallets. **address** `String` The base58 address of the wallet to import. Privy rejects the import if the private key does not control this address. **secret** `WalletImportSecret` The private key to import, as `WalletImportSecret.PrivateKey(bytes)`. Pass the 64-byte secret key (the 32-byte seed followed by the public key, as wallets such as Phantom export it) or the 32-byte seed. ### Returns **wallet** `Result\` A `Result` containing the imported wallet, an `EmbeddedSolanaWallet`. Its `imported` property is `true`. ### Errors When the import fails, the `Result` contains a `WalletImportException`: | Exception | When | | - | - | | `ImportNotSupported` | The app does not use TEE execution. | | `InvalidAddress` | The address is not valid for the chain. | | `InvalidPrivateKey` | The private key is not valid for the chain. | | `AddressMismatch` | The private key does not control the address. | | `WalletAlreadyExists` | The wallet was already imported, by this user or another user. | | `ImportFailed` | The import failed for another reason, including when the user is not authenticated. | **NodeJS** To import a private key wallet with the NodeJS SDK, use the `import` method from the Privy client’s `wallets()` interface. The Privy client will encrypt your private key for secure transmission to the TEE. See [architecture](/wallets/wallets/import-a-wallet/architecture) for more details. **EVM** For EVM wallets, the Privy client accepts a hex-encoded private key, with or without a `0x` prefix. ```ts theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: 'your-app-id', appSecret: 'your-app-secret' }); try { const wallet = await privy.wallets().import({ wallet: { entropy_type: 'private-key', chain_type: 'ethereum', address: '', private_key: '' } }); } catch (error) { console.error('Failed to import wallet:', error); } ``` Alternatively, you can also pass the private key directly as binary (`Uint8Array`). ```ts theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: 'your-app-id', appSecret: 'your-app-secret' }); const wallet = await privy.wallets().import({ wallet: { entropy_type: 'private-key', chain_type: 'ethereum', address: '', private_key: '' } }); ``` **Solana** For Solana wallets, the Privy client accepts a base58-encoded private key. ```ts theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: 'your-app-id', appSecret: 'your-app-secret' }); try { const wallet = await privy.wallets().import({ wallet: { entropy_type: 'private-key', chain_type: 'solana', address: '', private_key: '' } }); } catch (error) { console.error('Failed to import wallet:', error); } ``` Alternatively, you can also pass the private key directly as binary (`Uint8Array`). ```ts theme={"system"} import {PrivyClient} from '@privy-io/node'; const privy = new PrivyClient({ appId: 'your-app-id', appSecret: 'your-app-secret' }); const wallet = await privy.wallets().import({ wallet: { entropy_type: 'private-key', chain_type: 'solana', address: '', private_key: '' } }); ``` The returned wallet is type `Wallet`. See [get wallet by ID](/api-reference/wallets/get) for type definition. **REST API** ### 1. Initialization Initialize a key import flow by calling the `/v1/wallets/import/init` endpoint with your wallet address and chain type. See [authentication](/api-reference/introduction#authentication) for how to encode your app credentials. **EVM** ```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" }' ``` **Solana** ```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": "solana", "entropy_type": "private-key", "encryption_type": "HPKE" }' ``` The endpoint will return a public key to encrypt your private key with: ```json theme={"system"} { "encryption_public_key": "", "encryption_type": "HPKE" } ``` ### 2. Encryption Encrypt your private key using Hybrid Public Key Encryption (HPKE) with the following configuration: * KEM: DHKEM\_P256\_HKDF\_SHA256 * KDF: HKDF\_SHA256 * AEAD: CHACHA20\_POLY1305 * Mode: BASE There are two outputs from the encryption step that you'll provide to Privy during submission: * `ciphertext`: The encrypted private key * `encapsulated_key`: The encapsulated key Here's an example of how to encrypt a private key in TypeScript: **EVM** ```ts theme={"system"} import {Chacha20Poly1305} from '@hpke/chacha20poly1305'; import {CipherSuite, DhkemP256HkdfSha256, HkdfSha256} from '@hpke/core'; import {base64} from '@scure/base'; const encryptWithHpke = async ({ encryptionPublicKey, plaintextPrivateKey }: { encryptionPublicKey: Uint8Array; plaintextPrivateKey: Uint8Array; }) => { // Deserialize the raw key returned by the `init` request to the Privy API to a public key object const suite = new CipherSuite({ kem: new DhkemP256HkdfSha256(), kdf: new HkdfSha256(), aead: new Chacha20Poly1305() }); const publicKeyObject = await suite.kem.deserializePublicKey( Buffer.from(encryptionPublicKey).buffer ); // Encrypt the plaintext wallet private key const sender = await suite.createSenderContext({ recipientPublicKey: publicKeyObject }); const ciphertext = await sender.seal(Buffer.from(plaintextPrivateKey).buffer); // Return the encapsulated key and ciphertext, converting ArrayBuffer to Uint8Array return { encapsulatedKey: new Uint8Array(sender.enc), ciphertext: new Uint8Array(ciphertext) }; }; // The encryption public key is returned by the `init` request to the Privy API // For example: BPoOQ5k9nRk37v+XQWkmFEjpvW6RS0HQsPF3+IbhgMlc2Qwp/vz7lln1h0MJj/l0crLUhyyjdmC9RnAcpAkUNVQ= const base64EncodedEncryptionPublicKey = ''; // The wallet private key in hex format. This may have a '0x' prefix or may not depending on the // provider you're importing from, so make sure to remove it before encrypting if present as shown below. // For example: 0xe42f4dc0c8396e93891f05c3a34228c395f9b02505ffd0e79d7d2098af8b20a3 const hexEncodedPlaintextPrivateKey = ''; const {encapsulatedKey, ciphertext} = await encryptWithHpke({ encryptionPublicKey: base64.decode(base64EncodedEncryptionPublicKey), plaintextPrivateKey: new Uint8Array( Buffer.from( // Be sure to remove the `0x` prefix if present hexEncodedPlaintextPrivateKey.replace(/^0x/, ''), 'hex' ) ) }); ``` **Solana** ```ts theme={"system"} import {Chacha20Poly1305} from '@hpke/chacha20poly1305'; import {CipherSuite, DhkemP256HkdfSha256, HkdfSha256} from '@hpke/core'; import {base58, base64} from '@scure/base'; const encryptWithHpke = async ({ encryptionPublicKey, plaintextPrivateKey }: { encryptionPublicKey: Uint8Array; plaintextPrivateKey: Uint8Array; }) => { // Deserialize the raw key returned by the `init` request to the Privy API to a public key object const suite = new CipherSuite({ kem: new DhkemP256HkdfSha256(), kdf: new HkdfSha256(), aead: new Chacha20Poly1305() }); const publicKeyObject = await suite.kem.deserializePublicKey( Buffer.from(encryptionPublicKey).buffer ); // Encrypt the plaintext wallet private key const sender = await suite.createSenderContext({ recipientPublicKey: publicKeyObject }); const ciphertext = await sender.seal(Buffer.from(plaintextPrivateKey).buffer); // Return the encapsulated key and ciphertext, converting ArrayBuffer to Uint8Array return { encapsulatedKey: new Uint8Array(sender.enc), ciphertext: new Uint8Array(ciphertext) }; }; // The encryption public key is returned by the `init` request to the Privy API // For example: BPoOQ5k9nRk37v+XQWkmFEjpvW6RS0HQsPF3+IbhgMlc2Qwp/vz7lln1h0MJj/l0crLUhyyjdmC9RnAcpAkUNVQ= const base64EncodedEncryptionPublicKey = ''; // The wallet private key in base58 format. This is commonly the format used when exporting a Solana wallet from another provider. // For example: 4ANrq8ysACrNqmjWnekSPpud8GEdPN9YB7isMBbCxKWYrzcyAWnttYe8dPxfkhkR9mwLh4SfyZx4cUXnJAmZvbQ2 const base58EncodedPlaintextPrivateKey = ''; const {encapsulatedKey, ciphertext} = await encryptWithHpke({ encryptionPublicKey: base64.decode(base64EncodedEncryptionPublicKey), plaintextPrivateKey: base58.decode(base58EncodedPlaintextPrivateKey) }); ``` ### 3. Submission Submit your encrypted private key to the Privy API by calling the `/v1/wallets/import/submit` endpoint with the rest of your wallet configuration (e.g. an owner, policies, or signers). See [creating a wallet](/api-reference/wallets/create) for more information on configuration options. **EVM** ```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": "" }, // Optional additional configuration "owner": {...}, "policy_ids": [...], "additional_singers": [...] }' ``` **Solana** ```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": "solana", "entropy_type": "private-key", "encryption_type": "HPKE", "ciphertext": "", "encapsulated_key": "" }, // Optional additional configuration "owner": {...}, "policy_ids": [...], "additional_singers": [...] }' ``` The endpoint will return the wallet object of your imported wallet: **EVM** ```json theme={"system"} { "id": "", "address": "", "chain_type": "ethereum", "policy_ids": [], "additional_signers": [], "exported_at": null, "imported_at": 1753300563195, "created_at": 1753300563197, "owner_id": null } ``` **Solana** ```json theme={"system"} { "id": "", "address": "", "chain_type": "solana", "policy_ids": [], "additional_signers": [], "exported_at": null, "imported_at": 1753300563195, "created_at": 1753300563197, "owner_id": null } ``` **Rust** To import a private key wallet with the Rust SDK, use the `import()` method. The Rust SDK automatically handles the HPKE encryption for secure transmission to the TEE. See [architecture](/wallets/wallets/import-a-wallet/architecture) for more details. ### Usage ```rust theme={"system"} use privy_rs::{PrivyClient, generated::types::*}; let client = PrivyClient::new(app_id, app_secret)?; // Import an Ethereum wallet from private key let imported_wallet = client .wallets() .import( "0x742d35Cc6635C0532925a3b8D2dB4C5e64b1C0dB", // wallet address "your-hex-encoded-private-key", // private key (with or without 0x prefix) WalletImportSupportedChains::Ethereum, None, // owner vec![], // policy_ids vec![], // additional_signers ) .await? .into_inner(); println!("Imported wallet {} with address {}", imported_wallet.id, imported_wallet.address); ``` ### Parameters and Returns See the Rust SDK documentation for detailed parameter and return types, including embedded examples: * [WalletsClient::import](https://docs.rs/privy-rs/latest/privy_rs/subclients/struct.WalletsClient.html#method.import) * [WalletImportBody](https://docs.rs/privy-rs/latest/privy_rs/generated/types/struct.WalletImportBody.html) * [WalletImportData](https://docs.rs/privy-rs/latest/privy_rs/generated/types/struct.WalletImportData.html) * [Wallet](https://docs.rs/privy-rs/latest/privy_rs/generated/types/struct.Wallet.html) For REST API details, see the [API reference](/api-reference/wallets/import). > [!NOTE] > > The Rust SDK automatically handles the HPKE encryption process internally when importing wallets. You don't need to manually encrypt the private key - simply provide the raw private key string and the SDK will handle secure transmission to Privy's infrastructure. **Go** To import a private key wallet with the Go SDK, use the `Import` method. The SDK automatically handles HPKE encryption for secure transmission to the TEE. See [architecture](/wallets/wallets/import-a-wallet/architecture) for more details. **EVM** For EVM wallets, provide the raw private key as a byte slice. If the key is hex-encoded, decode it first. ```go theme={"system"} import ( "context" "encoding/hex" "fmt" "strings" ) // Decode hex-encoded private key to raw bytes (strip 0x prefix if present) hexKey := strings.TrimPrefix("", "0x") privKeyBytes, err := hex.DecodeString(hexKey) if err != nil { log.Fatalf("failed to decode private key: %v", err) } wallet, err := client.Wallets.Import(context.Background(), privy.WalletImportParams{ Wallet: privy.WalletImportParamsWalletUnion{ OfPrivateKey: &privy.WalletImportParamsWalletPrivateKey{ Address: "", ChainType: "ethereum", PrivateKey: privKeyBytes, }, }, }) if err != nil { log.Fatalf("failed to import wallet: %v", err) } fmt.Println("Imported wallet:", wallet.ID, wallet.Address) ``` **Solana** For Solana wallets, provide the raw private key as a byte slice. ```go theme={"system"} import ( "context" "fmt" ) wallet, err := client.Wallets.Import(context.Background(), privy.WalletImportParams{ Wallet: privy.WalletImportParamsWalletUnion{ OfPrivateKey: &privy.WalletImportParamsWalletPrivateKey{ Address: "", ChainType: "solana", PrivateKey: , }, }, }) if err != nil { log.Fatalf("failed to import wallet: %v", err) } fmt.Println("Imported wallet:", wallet.ID, wallet.Address) ``` The returned wallet is type `*Wallet`. See [get wallet by ID](/api-reference/wallets/get) for type definition. ## Using imported wallets Imported wallets function the same way as Privy-generated wallets. See the API reference for [Ethereum](/api-reference/wallets/ethereum/eth-send-transaction) or [Solana](/api-reference/wallets/solana/sign-and-send-transaction) for information about how to send transactions and execute other wallet operations. # Offline actions with user wallets Source: https://docs.privy.io/wallets/wallets/offline-actions Enable offline wallet actions using Privy control abstractions with scoped permissions Privy's [control abstractions](/controls/overview) allow you to enable offline actions with user wallets with a specific scope of permissions. This supports several use cases, such as executing **limit orders**, enabling **agentic trading**, or implementing **subscriptions** and other recurring transactions. Learn more about how to configure wallets for offline access in the **Policies & controls** section of the docs. > **[Enabling offline actions](/wallets/using-wallets/signers/overview)** > > Enable server-side access to user wallets to support offline transactions, offline updates, custom > approval configurations, and more. # Server-side access to user wallets Source: https://docs.privy.io/wallets/wallets/server-side-access Enable server-side access to user wallets with policy-restricted controls Privy's [control abstractions](/controls/overview) allow you to interact with wallets from your app's server, even without the user in the loop. These interactions can be restricted by policies that configure the scope of actions that can be taken without user authorization. This supports several use cases, such as: * executing transactions when a user is offline, e.g for limit orders, agentic trading, portfolio rebalancing, etc. * updating wallets when a user is offline, such as updating policies or assigning specific permissions to third-parties * requiring both users and servers to approve user transactions and more. Learn more about how to configure wallets for server-side access in the **Policies & controls** section of the docs. > **[Enabling server-side access](/wallets/using-wallets/signers/overview)** > > Enable server-side access to user wallets to support offline transactions, offline updates, custom > approval configurations, and more. # Update a wallet Source: https://docs.privy.io/wallets/wallets/update-a-wallet Update wallet policies, owners, and signers using the Privy API Privy enables you to update the policies, owners, and signers for a wallet. **React** Use the `updateWallet` method from the `useUpdateWallet` hook to update a wallet linked to the current user. Privy signs the request with the current user's authorization signature and refreshes the user after the update. ### Usage ```tsx theme={"system"} import {useUpdateWallet} from '@privy-io/react-auth'; const {updateWallet} = useUpdateWallet(); const wallet = await updateWallet('wallet-id', { display_name: 'My wallet', policy_ids: ['policy-id'], }); ``` ### Parameters **walletId** `string` *required* The ID of the user's linked wallet to update. **input** `UpdateWalletInput` *required* The wallet fields to update. Omitted fields remain unchanged. Client-side ownership transfers (`owner` and `owner_id`) are not supported. **input.policy\_ids** `string[]` New policy IDs to enforce on the wallet. Currently, only one policy is supported per wallet. **input.additional\_signers** `\{signer_id: string\}[]` The key quorum IDs to add as additional signers for the wallet. **input.display\_name** `string` A human-readable name for the wallet. At most 100 characters. ### Returns **wallet** `Promise\` The updated wallet from the refreshed user's linked accounts. The promise rejects if the update or user refresh fails, or if the wallet is not found in the refreshed user's linked accounts. **REST API** To update an existing wallet, make a `PATCH` request to: ``` https://api.privy.io/v1/wallets/ ``` > [!TIP] > > If the wallet has an [external ID](/wallets/wallets/external-ids), your app can use `ext_wal_` in place of the wallet ID. For example: `PATCH /v1/wallets/ext_wal_order-12345` > [!NOTE] > > Wallets with `owner_id` present must provide an [authorization signature](/api-reference/authorization-signatures) as a request header. Privy SDKs offer utility functions such as `generateAuthorizationSignature` to generate the authorization signature. ### Body In the request body, include the following fields: **policy\_ids** `string[]` New policy IDs to enforce on the wallet. Currently, only one policy is supported per wallet. **owner** `\{user_id: string\} | \{public_key: string\} | null` The user ID or P-256 public key of the owner of the wallet. 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. **owner\_id** `string | null` The key quorum ID of the owner of the wallet. If you provide this, do not specify an owner. View [this guide](/controls/authorization-keys/owners/overview) to learn more about owners. **additional\_signers** `\{signer_id: string\}[]` The key quorum IDs to add as additional signers for the wallet. View [this guide](/controls/key-quorum/overview) to learn more about key quorums. **display\_name** `string` A human-readable name for the wallet. At most 100 characters. Any fields not included in the `PATCH` request body will remain unchanged from the original wallet. ### Response If the wallet is updated successfully, the response will include the updated wallet. **id** `string` Unique ID for the wallet. **address** `string` Address of the wallet. **chain\_type** `'ethereum'` Chain type for the wallet. **policy\_ids** `MethodRule` Updated policy IDs to enforce on the wallet. **owner\_id** `string | null` The key quorum ID of the owner of the wallet. **additional\_signers** `\{signer_id: string\}[]` The key quorum IDs of the additional signers for the wallet. **external\_id** `string | null` The external identifier assigned to the wallet, if set. **display\_name** `string | null` The display name assigned to the wallet, if set. ### Example A sample request might look like the following: ```tsx theme={"system"} $ curl --request PATCH https://api.privy.io/v1/wallets/rbokq6mmq5f8j1cgyr6a5g4n \ -u ":" \ -H "privy-app-id: " \ -H "privy-authorization-signature: " \ -H 'Content-Type: application/json' \ --data '{ "policy_ids": ["fmfdj6yqly31huorjqzq38zc"], "owner_id": "yhu8t1fjns9rtc2o702ub3vt", "additional_signers": [{"signer_id": "trt9syg5k19jvxwbnt6t8rd0"}] }' ``` A successful response will look like the following: ```tsx theme={"system"} { "id": "rbokq6mmq5f8j1cgyr6a5g4n", "address": "0xE315ce0854CcbdB0E33e71af1190F48Eb5d4f5a4", "chain_type": "ethereum", "policy_ids": ["fmfdj6yqly31huorjqzq38zc"], "owner_id": "yhu8t1fjns9rtc2o702ub3vt", "additional_signers": [ { "signer_id": "trt9syg5k19jvxwbnt6t8rd0" } ], "created_at": 1737492220389 } ``` **Java** To update an existing wallet, use the `update` method, passing in a `walletId` along with a `WalletUpdateRequestBody`. ### Usage ```java theme={"system"} try { WalletUpdateRequestBody request = WalletUpdateRequestBody.builder() .policyIds(List.of("new-policy-id")) .build(); // Use the AuthorizationContext to sign the request with the wallet's owner. // Example if the wallet's owner is an authorization private key AuthorizationContext authorizationContext = AuthorizationContext.builder() .addAuthorizationPrivateKey("authorization-key") .build(); WalletUpdateResponse response = privyClient.wallets().update( "wallet-id", request, authorizationContext ); if (response.wallet().isPresent()) { Wallet updatedWallet = response.wallet().get(); } } catch (APIException e) { String errorBody = e.bodyAsString().orElse(e.getMessage()); System.err.println(errorBody); } catch (Exception e) { System.err.println(e.getMessage()); } ``` ### Parameters When updating a wallet, you may specify the following values on the `WalletUpdateRequestBody`: **policyIds** `List\` List of policy IDs for policies that should be enforced on the wallet. Currently, only one policy is supported per wallet. **owner** `OwnerInput` The owner of the resource, which can either be a public key from a p256 keypair, or a user ID. If you provide this, do not specify an `ownerId` as it will be generated automatically. **ownerId** `String` The key quorum ID to set as the owner of the resource. If you provide this, do not specify an`owner`. **additionalSigners** `WalletAdditionalSignerItem` Additional signers for the wallet. ### Returns The `WalletUpdateResponse` object contains an optional `wallet()` field, present if the wallet was updated successfully. **wallet()** `Optional\` The updated `Wallet` object. **id** `String` Unique ID of the created wallet. This will be the primary identifier when using the wallet in the future. **address** `String` Address of the created wallet. **chainType** `WalletChainType` Chain type of the created wallet. **policyIds** `List\` List of policy IDs for policies that are enforced on the wallet. **ownerId** `String` The key quorum ID of the owner of the wallet. If an `ownerId` was passed in, this response is the input `ownerId`. If a user ID or authorization key was passed in as the `owner`, this response is a newly created key quorum containing the input user ID or authorization key. **additionalSigners** `List\` The key quorum IDs of the additional signers for the wallet. **createdAt** `double` The creation date of the wallet, in milliseconds since midnight, January 1, 1970 UTC. **Rust** To update an existing wallet, use the `update` method on the `wallets()` interface, along with an `AuthorizationContext` for signing the request. > [!NOTE] > > Wallets with an `owner_id` must provide authorization from the owner to perform updates. Use the `AuthorizationContext` to include the necessary credentials for request signing. ### Usage ```rust theme={"system"} use privy_rs::{PrivyClient, generated::types::*, AuthorizationContext}; let client = PrivyClient::new(app_id, app_secret)?; // Create authorization context with the wallet owner's key let ctx = AuthorizationContext::new() .push(PrivateKey("authorization-private-key".to_string())); let updated_wallet = client .wallets() .update( "wallet-id", &ctx, &WalletUpdateBody { policy_ids: Some(vec!["new-policy-id".to_string()]), owner: None, owner_id: Some("new-owner-quorum-id".to_string()), additional_signers: Some(vec![ WalletAdditionalSigner { signer_id: "additional-signer-id".to_string(), }, ]), }, ) .await?; println!("Updated wallet: {}", updated_wallet.id); ``` ### Parameters and Returns See the Rust SDK documentation for detailed parameter and return types, including embedded examples: * [WalletsClient::update](https://docs.rs/privy-rs/latest/privy_rs/subclients/struct.WalletsClient.html#method.update) * [WalletUpdateBody](https://docs.rs/privy-rs/latest/privy_rs/generated/types/struct.WalletUpdateBody.html) * [AuthorizationContext](https://docs.rs/privy-rs/latest/privy_rs/struct.AuthorizationContext.html) * [Wallet](https://docs.rs/privy-rs/latest/privy_rs/generated/types/struct.Wallet.html) For REST API details, see the [API reference](/api-reference/wallets/update). **Go** To update an existing wallet with the Go SDK, use the `Update` method on the `Wallets` service, along with an authorization context for signing the request. > [!NOTE] > > Wallets with an `owner_id` must provide authorization from the owner to perform updates. Use > `privy.WithAuthorizationContext` to include the necessary credentials for request signing. ### Usage ```go theme={"system"} import "github.com/privy-io/go-sdk/authorization" // Create authorization context with the wallet owner's key authCtx := &authorization.AuthorizationContext{ PrivateKeys: []string{"authorization-key"}, } updatedWallet, err := client.Wallets.Update( context.Background(), "wallet-id", privy.WalletUpdateParams{ WalletUpdateRequestBody: privy.WalletUpdateRequestBody{ PolicyIDs: []string{"new-policy-id"}, }, }, privy.WithAuthorizationContext(authCtx), ) if err != nil { log.Fatalf("failed to update wallet: %v", err) } fmt.Println("Updated wallet:", updatedWallet.ID) ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/update) for more details. **Python** Use the `update` method on the wallets service. Include the owner's authorization context for an owned wallet. ```python theme={"system"} from privy import AuthorizationContext, PrivyRequestOptions request_options = PrivyRequestOptions( authorization_context=AuthorizationContext( authorization_private_keys=["authorization-private-key"] ) ) wallet = client.wallets.update( "wallet-id", wallet_update_params={"policy_ids": ["new-policy-id"]}, request_options=request_options, ) ``` **Ruby** To update an existing wallet with the Ruby SDK, use the `update` method on the `wallets` service, along with an authorization context for signing the request. > [!NOTE] > > Wallets with an `owner_id` must provide authorization from the owner to perform updates. Pass an > `authorization_context` to include the necessary credentials for request signing. ### Usage ```ruby theme={"system"} # Create authorization context with the wallet owner's key ctx = Privy::Authorization::AuthorizationContext.build( authorization_private_keys: ["authorization-key"] ) updated_wallet = client.wallets.update( "wallet-id", wallet_update_params: {policy_ids: ["new-policy-id"]}, authorization_context: ctx ) puts(updated_wallet.id) ``` ### Parameters and Returns See the [API reference](/api-reference/wallets/update) for more details.