# User + server signers

Sometimes the user owns the wallet, but you still want the server to act for them — resubscribe them next month, rebalance a position overnight, or let an agent trade on their behalf. Instead of taking custody, have the user's [embedded wallet](/docs/products/embedded-wallet) authorize your backend's signer as a **scoped [session key](/docs/products/embedded-wallet/react/wallet/actions/session-keys)**: a signer that can only do what the user approved, only until it expires.

The wallet stays non-custodial. The user keeps their key; your server's key never leaves your infrastructure. What connects them is a session key — bounded by the scope and expiry the user set — that lets your backend sign for the user's account within those limits.

:::warning
Session keys require a **smart account** (a [delegated account](/docs/products/embedded-wallet/react/wallet), EIP-7702). By default the SDK creates EOA wallets; enable a delegated/smart account first, since only programmable accounts can register and enforce session keys.
:::

## The pattern

This is the shape to replicate — three parts, each holding its own key:

1. **Server signer** — your backend owns a [backend wallet](/docs/products/server). Its **address** is what the user authorizes; its private key stays in your infrastructure and never crosses the network.
2. **Authorize (client)** — the user's embedded smart account registers that address as a session key, scoped to specific contracts/methods with a short expiry.
3. **Execute (server)** — your backend signs [UserOperations](https://eips.ethereum.org/EIPS/eip-4337) for the user's account with the server signer and submits them gas-sponsored. Calls outside the scope, or after expiry, revert on-chain — so the server can never exceed the delegation.

The key idea: **you authorize an address, not hand over a secret.** Because the server signs with a wallet it already controls, no session-key private key is ever transmitted or stored twice.

::::steps
## Provision the server signer

Create a backend wallet to act as the signer (reuse one, or one per user/agent so each can be audited and revoked independently). Expose only its **address** to the client.

```ts [openfort.ts]
import Openfort from '@openfort/openfort-node'

export const openfort = new Openfort(process.env.OPENFORT_SECRET_KEY!, {
  walletSecret: process.env.OPENFORT_WALLET_SECRET!,
})

// The server signer — the user will authorize this address.
const signer = await openfort.accounts.evm.backend.create()
// Send signer.address to the client; keep the key server-side.
```

## Authorize the signer on the user's account (client)

On the client, the user's embedded smart account registers the server signer's **address** as a session key — scoped to only the contracts the server needs, with a short expiry so authority lapses on its own. See [Session keys](/docs/products/embedded-wallet/react/wallet/actions/session-keys) for the grant call and [`useRevokePermissions`](/docs/products/embedded-wallet/react/hooks/useRevokePermissions) to let the user revoke early.

Register the signer address (not a throwaway key you generate and ship), so the server can sign with the backend wallet it already holds.

## Execute within scope (server)

With the signer authorized, your backend submits UserOperations for the user's account: build a session account bound to **(server signer, user's account, the granted key)**, then send calls through a bundler with your [paymaster policy](/docs/configuration/gas-sponsorship) so the user needs no gas.

```ts
// Illustrative shape — construct the session account for your smart-account
// implementation, then submit UserOperations through a bundler + paymaster.
const userOpHash = await bundlerClient.sendUserOperation({
  account: sessionAccount, // server signer, acting for the user's account
  calls: [{ to: TARGET_CONTRACT, value: 0n, data: CALLDATA }],
})
```

Because the session key is bounded by the scope and expiry the user set, any call outside it reverts. The server can't drain the wallet, touch contracts the user didn't approve, or act past the expiry — and the user can revoke at any time.

:::tip[Complete working example]
The [Onchain DCA recipe](/docs/recipes/agent-permissions) implements this pattern end to end: a backend agent wallet is registered as a time-limited session key on the user's smart account, and a cron job submits gas-sponsored swaps within that window. Use it as the reference implementation — the code here is the boilerplate around it.
:::
::::

## Choosing between this and a standalone wallet

| | User + server signers | [Server-side user wallets](/docs/products/server/workflows/server-side-user-wallets) |
| :--- | :--- | :--- |
| **Key ownership** | User (non-custodial) | Your server (custodial) |
| **Server's authority** | Only the granted scope, until expiry | Full control of the wallet |
| **User can revoke** | Yes, any time | No — the server holds the key |
| **Best when** | The user owns funds and delegates a slice | The wallet should be invisible to the user |

## Next steps

* [Onchain DCA recipe](/docs/recipes/agent-permissions) — the full reference implementation.
* [Session keys](/docs/products/embedded-wallet/react/wallet/actions/session-keys) — permissions, scoping, and revocation.
* [Agentic wallets](/docs/products/server/workflows/agentic-wallets) — an agent with its own wallet instead of a delegated one.
