> For the complete documentation index, see [llms.txt](https://docs.osero.com/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.osero.com/osero-sdk/core-concepts/privy-server-wallets.md).

# Privy Server Wallets

## Privy Server Wallets

`@osero/client/privy` is a wallet adapter for [Privy server wallets](https://docs.privy.io/wallets/overview). It broadcasts the same `ExecutionPlan` as the viem and ethers adapters, but signing happens in Privy's Wallet API instead of against a local private key: the adapter submits each pre-encoded transaction to Privy, then waits for the transaction receipt with a viem public client before starting the next one.

Use it when the broadcasting side of an integration runs on a server and the keys are custodied by Privy — for example, a backend that mints or redeems on behalf of users.

***

### Installation

The adapter needs Privy's Node SDK, declared by `@osero/client` as an optional peer dependency:

```bash
pnpm add @privy-io/node
```

`viem` is still required — the adapter uses it for receipt polling. [See Installation.](#installation)

***

### Usage

```typescript
import { OseroClient } from '@osero/client';
import { mintUsds } from '@osero/client/actions';
import { sendWith, type PrivyWallet } from '@osero/client/privy';
import { PrivyClient } from '@privy-io/node';
import { http, parseUnits } from 'viem';

// 1. Authenticate against Privy and describe the server wallet
const privy = new PrivyClient({
  appId: process.env.PRIVY_APP_ID!,
  appSecret: process.env.PRIVY_APP_SECRET!,
});

const wallet = {
  id: process.env.PRIVY_WALLET_ID!,
  address: process.env.PRIVY_WALLET_ADDRESS as `0x${string}`,
  authorizationContext: {
    authorization_private_keys: [process.env.PRIVY_AUTHORIZATION_PRIVATE_KEY!],
  },
} satisfies PrivyWallet;

// 2. The OseroClient is still needed — actions read PSM state through it
const client = OseroClient.create({
  transports: { 8453: http(process.env.RPC_URL_BASE!) },
});

// 3. Mint 100 USDC worth of USDS on Base through the Privy wallet
const result = await mintUsds(client, {
  chainId: 8453,
  amount: parseUnits('100', 6),
  sender: wallet.address,
}).andThen(
  sendWith(privy, wallet, {
    transports: { 8453: http(process.env.RPC_URL_BASE!) },
  }),
);

if (result.isErr()) {
  console.error(result.error.name, result.error.message);
  process.exit(1);
}

console.log('USDS minted in tx', result.value.txHash);
```

`sendWith` has the same two call forms as the other adapters: curried — `sendWith(privy, wallet, options?)` returns a handler for `.andThen` — or direct — `sendWith(privy, wallet, plan, options?)` runs an already-obtained plan.

{% hint style="warning" %}
`PRIVY_APP_SECRET` and authorization private keys grant signing authority over your server wallets. Keep them in server-side secret storage; never expose them to a browser or mobile client.
{% endhint %}

***

### The PrivyWallet descriptor

`sendWith` identifies the signing wallet with a `PrivyWallet` object:

| Field                  | Type                   | Required | Description                                                                                                    |
| ---------------------- | ---------------------- | -------- | -------------------------------------------------------------------------------------------------------------- |
| `id`                   | `string`               | yes      | Privy's wallet ID, as returned by `privy.wallets().create(...)` or `privy.wallets().get(...)`                  |
| `address`              | `` `0x${string}` ``    | yes      | The wallet's EVM address. Checked against every transaction's `from` before broadcasting                       |
| `authorizationContext` | `AuthorizationContext` | no       | Privy request authorization — authorization private keys or user JWTs. Sent with every transaction in the plan |

If `address` does not match the `sender` the plan was built for, the adapter fails with `UnexpectedError` before anything is sent. Approvals and balance checks are owner-specific, so a plan built for one wallet must not be broadcast from another.

Omit `authorizationContext` only if your Privy wallet configuration does not require request authorization. The key in `authorization_private_keys` is the base64-encoded PKCS8 private key registered for your Privy app, without PEM headers.

***

### Options

| Option            | Type                                  | Default         | Description                                                                                                 |
| ----------------- | ------------------------------------- | --------------- | ----------------------------------------------------------------------------------------------------------- |
| `confirmations`   | `number`                              | `1`             | Confirmations to wait for after each transaction                                                            |
| `transports`      | `Partial<Record<chainId, Transport>>` | viem public RPC | viem transports for receipt polling, keyed by chain ID — same shape as `OseroClient.create({ transports })` |
| `idempotencyKeys` | `readonly string[]`                   | none            | Privy idempotency keys, one per transaction in execution order — see below                                  |

For production use, configure `transports`. Receipt polling otherwise falls back to viem's default public RPC for each chain, which is rate-limited.

***

### How broadcasting works

For each transaction in the plan, in order:

1. The adapter checks the wallet's `address` against the transaction's `from`.
2. It submits the pre-encoded transaction to Privy's Wallet API with a CAIP-2 chain ID (`eip155:<chainId>`). Privy selects gas and nonce, signs, and broadcasts.
3. It waits for the receipt with a viem public client for that chain. A reverted receipt fails the run with `TransactionError`; otherwise the next transaction starts.

Because the target network travels with each request, the wallet is not bound to a chain and there is nothing to switch — unlike the viem adapter, which requires the `WalletClient` to already be on the plan's chain.

Plans must target a chain in `SUPPORTED_CHAIN_IDS`; the adapter cannot poll receipts for chains outside the SDK's registry. Every plan built by the SDK's actions satisfies this.

***

### Idempotency keys and retries

Privy deduplicates requests that carry an idempotency key. For 24 hours, resending the same key with the same request body returns the original result instead of broadcasting again; reuse with a different body is rejected.

The adapter exposes this through `idempotencyKeys` — one key per transaction, in the order `flattenExecutionPlan(plan)` returns. When keys are provided, their count must match the plan's transaction count exactly; otherwise the run fails before any transaction is sent, so no transaction is ever broadcast without its retry protection.

Keys are caller-managed. The SDK does not derive them from transaction contents, because two unrelated operations can produce identical calldata — two mints of the same amount, for example. Generate and persist one key per transaction alongside the operation you are about to execute, and reuse the same keys only when retrying that operation:

```typescript
// mintUsds on an L2 flattens to two transactions: approve, then swap.
const idempotencyKeys = [`${orderId}-approve`, `${orderId}-swap`];

const result = await mintUsds(client, request).andThen(
  sendWith(privy, wallet, { idempotencyKeys }),
);
```

If a run fails partway — the approval landed but the swap did not — retrying the whole plan with the same keys is safe within the 24-hour window: Privy replays the already-executed transactions, returning their original hashes, and only the remaining transactions broadcast. After the window expires the same keys broadcast again for real, so do not retry stale operations with old keys.

***

### Error mapping

The adapter returns the same error union as the other adapters — see [Error Handling](/osero-sdk/reference/error-handling.md):

| Condition                                                                                                      | Error              |
| -------------------------------------------------------------------------------------------------------------- | ------------------ |
| The Privy API request was aborted                                                                              | `CancelError`      |
| Privy rejected the transaction — policy violation, authorization failure, any API error                        | `SigningError`     |
| The transaction was broadcast but reverted                                                                     | `TransactionError` |
| Wallet/sender mismatch, invalid idempotency keys, unsupported chain, missing transaction hash, receipt failure | `UnexpectedError`  |

A server wallet has no wallet prompt, so `CancelError` never means a user clicked "Reject" — it corresponds to an aborted API request.

***

### Limitations

* **Gas-sponsored transactions are not supported.** Privy returns no transaction hash for sponsored (ERC-4337) transactions, which the adapter cannot wait on — the run fails with `UnexpectedError`. Disable gas sponsorship for wallets used with this adapter.
* **Receipt polling is registry-bound.** Plans must target one of the SDK's supported chains.

***

### Runnable example

The examples package includes a Privy variant of the Base USDS mint:

```bash
pnpm --filter @osero/examples privy:mint-usds
```

It requires `PRIVY_APP_ID`, `PRIVY_APP_SECRET`, `PRIVY_WALLET_ID`, and `PRIVY_WALLET_ADDRESS` in `examples/.env`, plus `PRIVY_AUTHORIZATION_PRIVATE_KEY` if the wallet requires request authorization. [See Examples](/osero-sdk/reference/examples.md).


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation by asking a question.

Perform an HTTP GET request on the following URL with the `ask` and `goal` query parameters:

```
GET https://docs.osero.com/osero-sdk/core-concepts/privy-server-wallets.md?ask=<question>&goal=<user_goal>
```

`ask` is the immediate question: it should be specific, self-contained, and written in natural language.
`goal` is what the user is ultimately trying to achieve, the reason they need the answer. Sharing it helps GitBook give you a better, more relevant answer. A goal is most helpful when it describes the outcome the user wants rather than restating the question. For example, with `ask=how do I create an API token`, a goal like `automate deployments from our CI pipeline` lets GitBook tailor the answer to that use case.

The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
