Skip to main content
This document describes how lets 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, 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 users need to let a self-hosted agent (such as Claude Code, Codex, OpenClaw, or a custom CLI) perform wallet operations on their behalf inside your Privy app. 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
  • Policies should constrain what the agent can do (transfer limits, allowlisted contracts, and so on)
  • App secrets must not be distributed to end-user machines

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.
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, open Authentication -> Advanced and toggle Enable for CLI and agent access on. Set the Verification URI, the URL where users will approve agent requests. The verification URI must point to a page your app hosts. Privy returns this URI to the agent in the device authorization response so the agent can display it to the user.
The verification URI is typically a dedicated route in your web app, for example https://your-app.com/authorize.

Build the verification page

The verification page is the only browser-side step in the flow. It reads the user_code from the query string, prompts the user to log in if needed, and calls device_verify to approve or deny the agent’s request. Privy’s device authorization endpoint returns a verification_uri_complete that pre-fills the user code as a query parameter, so users arriving via that link will not need to type a code manually.
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 <p>No code found. Open the link provided by the agent.</p>;
  }

  return (
    <div>
      <p>
        Code: <strong>{userCode}</strong>
      </p>
      {!authenticated ? (
        <button type="button" onClick={() => login()}>
          Log in to continue
        </button>
      ) : (
        <>
          <button type="button" onClick={() => submit('approve')}>
            Approve
          </button>
          <button type="button" onClick={() => submit('deny')}>
            Deny
          </button>
        </>
      )}
    </div>
  );
}
The device_verify endpoint always returns {"success": true} even for unknown or expired codes, to avoid leaking code validity. If the agent reports expired_token or access_denied, 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.

Request a device code

Call this once at the start of login to receive the codes the agent will use for polling and display.
POST https://auth.privy.io/api/oauth/v2/device_authorization
privy-app-id: <your-app-id>
Content-Type: application/json

{}
{
  "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.
POST https://auth.privy.io/api/oauth/v2/token
privy-app-id: <your-app-id>
Content-Type: application/json

{
  "grant_type": "device_code",
  "device_code": "a3f8c2e1d4b7..."
}
{
  "access_token": "eyJhbGci...",
  "token_type": "Bearer",
  "expires_in": 900,
  "refresh_token": "d9e3f1a2b4c6..."
}
While polling, the endpoint returns 400 with one of the following errors:
ErrorAction
authorization_pendingUser has not approved yet; keep polling
slow_downPolling too fast; increase interval by 5 seconds
expired_tokenDevice code expired; restart from the previous step
access_deniedUser 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.
POST https://auth.privy.io/api/oauth/v2/token
privy-app-id: <your-app-id>
Content-Type: application/json

{
  "grant_type": "refresh_token",
  "refresh_token": "d9e3f1a2b4c6..."
}
{
  "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.
POST https://auth.privy.io/api/v1/wallets/device-auth/authenticate
Authorization: Bearer <access_token>
privy-app-id: <your-app-id>
Content-Type: application/json

{
  "public_key": {
    "type": "hpke",
    "key": "<base64-encoded HPKE public key>"
  }
}
{
  "signing_key": "<HPKE-encrypted signing key material>"
}
Decrypt signing_key with the corresponding HPKE private key. Cache and reuse the key until it expires. It produces the privy-authorization-signature header on RPC requests.

Submit a wallet RPC

Use the Privy wallet ID (for example, wallet_abc123), not the on-chain address.
POST https://auth.privy.io/api/v1/wallets/device-auth/{wallet_id}/rpc
Authorization: Bearer <access_token>
privy-app-id: <your-app-id>
privy-authorization-signature: <signature>
Content-Type: application/json
{
  "method": "eth_sendTransaction",
  "params": {
    "transaction": {
      "to": "0xdeadbeef...",
      "value": "0x5af3107a4000",
      "chain_id": 8453
    }
  }
}
{
  "method": "eth_sendTransaction",
  "data": {
    "hash": "0x7f8e9d..."
  }
}
ErrorAction
401 access token expiredRefresh via the token endpoint, then retry
403 wallet not accessibleWallet does not belong to the authenticated user

Token lifetimes

ArtifactLifetime
device_code / user_code10 minutes
Access token15 minutes
Refresh token30 days (rotated on each use)

Learn more

Agent CLI

Give any agent a wallet with a CLI command. No integration code needed.

Agentic wallets

Create developer-controlled agent wallets with policy guardrails.

Policies

Constrain agent behavior with transfer limits, allowlists, and time-based controls.

x402 payments

Enable HTTP-native payments for APIs and digital content.