Skip to main content

Overview

Delegated signing lets your backend sign transactions on behalf of end users without requiring them to be online or have an active session. The end user grants a time-bound delegation from your frontend, and your server can then take actions on their account using only your CDP API key and Wallet Secret. This is useful for:
  • Automated transactions — Execute transactions triggered by webhooks or onchain events when the user is offline
  • Agentic wallets — Allow a backend service or agent to operate a user’s wallet with scoped permissions
  • Background operations — Process user-initiated flows that complete asynchronously after the user has left your app

How it works

Two parties are involved:
  • End user — Authenticates on your frontend and grants a time-bound delegation to your app
  • Developer — Uses their CDP API key, CDP Wallet Secret, and the active delegation to sign transactions from the backend with no user interaction required

Delegation scopes

There are two available delegation scopes:
User-scopedAccount-scoped
Grant coversAll of a user’s accountsOne specific account
Active delegations per userOneOne per account
Revocation granularityAll accounts at onceOne account at a time
User-scoped and account-scoped delegations are mutually exclusive per user. To switch scopes, revoke all existing delegations first. The flow:
  1. Grant — The authenticated end user calls createDelegation on the frontend, specifying an expiry time
  2. Sign — Your backend uses the CDP SDK with your API key to perform actions on behalf of the user
  3. Revoke — The delegation can be revoked before expiry by the end user or the developer

Available delegated signing methods

With an active delegation, your backend can call the following methods via cdp.endUser.* on behalf of an end user:

EVM: Sign

MethodDescription
signEvmTransaction({ userId, address, transaction })Sign an EVM transaction
signEvmMessage({ userId, address, message })Sign an EIP-191 message
signEvmTypedData({ userId, address, typedData })Sign EIP-712 typed data

EVM: Send

MethodDescription
sendEvmTransaction({ userId, address, transaction, network })Send a signed EVM transaction
sendEvmAsset({ userId, address, to, amount, network })Send an EVM asset (e.g. USDC)
sendUserOperation({ userId, address, network, calls })Send a smart account user operation
createEvmEip7702Delegation({ userId, address, network })Create an EIP-7702 delegation

Solana: Sign

MethodDescription
signSolanaMessage({ userId, address, message })Sign a Solana message
signSolanaTransaction({ userId, address, transaction })Sign a Solana transaction

Solana: Send

MethodDescription
sendSolanaTransaction({ userId, address, transaction, network })Send a signed Solana transaction
sendSolanaAsset({ userId, address, to, amount, network })Send a Solana asset (e.g. USDC)

Prerequisites

  1. In the CDP Portal, go to Embedded Wallets → Policies and enable the Delegated Signing toggle.
Delegated Signing toggle
  1. Install the required packages:
Frontend (@coinbase/cdp-hooks):
npm install @coinbase/cdp-core @coinbase/cdp-hooks
Backend (cdp-sdk):
npm install @coinbase/cdp-sdk dotenv
Sign in to the CDP Portal, create a CDP API key and generate a Wallet Secret. Add them to your .env file:
.env
CDP_API_KEY_ID=your-api-key-id
CDP_API_KEY_SECRET=your-api-key-secret
CDP_WALLET_SECRET=your-wallet-secret
Then instantiate the CDP client in your backend:
import { CdpClient } from "@coinbase/cdp-sdk";
import "dotenv/config";

const cdp = new CdpClient();
TypeScript only: Set moduleResolution: "node16" or "nodenext" in your tsconfig.json to avoid compilation errors with the CDP SDK.

Step 1: End user creates a delegation (React)

The authenticated end user grants your app a time-bound delegation. This is the only step that requires the user to be present.

User-scoped

A user-scoped delegation covers all of a user’s accounts.
Only one active user-scoped delegation is allowed per user at a time. If a delegation already exists, revoke it or let it expire before creating a new one.
React
import { useCreateDelegation, useCurrentUser } from "@coinbase/cdp-hooks";

function DelegateButton() {
  const { currentUser } = useCurrentUser();
  const { createDelegation } = useCreateDelegation();

  const handleDelegate = async () => {
    // Delegation expires in 24 hours
    const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
    const result = await createDelegation({ expiresAt });
    console.log("Delegation ID:", result.delegationId);
  };

  return (
    <button disabled={!currentUser} onClick={handleDelegate}>
      Grant Delegation
    </button>
  );
}

Account-scoped

An account-scoped delegation covers one specific account. This lets you revoke access for a single account without affecting other delegations you hold for the same user.
One active account-scoped delegation is allowed per account at a time. If a delegation already exists for that account, revoke it or let it expire before creating a new one.
React
import { useCreateDelegationForAccount, useCurrentUser } from "@coinbase/cdp-hooks";

function DelegateAccountButton({ address }: { address: string }) {
  const { currentUser } = useCurrentUser();
  const { createDelegationForAccount } = useCreateDelegationForAccount();

  const handleDelegate = async () => {
    // Delegation expires in 24 hours
    const expiresAt = new Date(Date.now() + 24 * 60 * 60 * 1000).toISOString();
    const result = await createDelegationForAccount({ address, expiresAt });
    console.log("Delegation ID:", result.delegationId);
  };

  return (
    <button disabled={!currentUser} onClick={handleDelegate}>
      Grant Delegation
    </button>
  );
}

Step 2: Developer signs on behalf of the end user

Once a delegation is active, your backend can submit transactions for the end user using your CDP API key and Wallet Secret — no user session needed. The signing API is the same regardless of delegation scope.
import { CdpClient } from "@coinbase/cdp-sdk";

const cdp = new CdpClient();

const endUser = await cdp.endUser.getEndUser({ userId: "<USER_UUID>" });
const address = endUser.evmAccountObjects[0].address;

const result = await cdp.endUser.sendEvmTransaction({
  userId: endUser.userId,
  address: address,
  transaction: "0x02...", // RLP-serialized EIP-1559 transaction
  network: "base-sepolia",
});
console.log("Transaction hash:", result.transactionHash);

Step 3: Revoke a delegation

A delegation can be revoked before it expires — either by the end user from the frontend, or by the developer from the backend.

User-scoped

Revoking a user-scoped delegation removes access across all of the user’s accounts. End user revokes (React)
React
import { useRevokeDelegation } from "@coinbase/cdp-hooks";

function RevokeButton() {
  const { revokeDelegation } = useRevokeDelegation();

  const handleRevoke = async () => {
    await revokeDelegation();
    console.log("All delegations revoked.");
  };

  return <button onClick={handleRevoke}>Revoke All Delegations</button>;
}
Developer revokes
import { CdpClient } from "@coinbase/cdp-sdk";

const cdp = new CdpClient();

await cdp.endUser.revokeDelegationForEndUser({
  userId: "<USER_UUID>",
});

Account-scoped

Revoking an account-scoped delegation removes access for one account and leaves all other account delegations for that user intact. End user revokes (React)
React
import { useRevokeDelegationForAccount } from "@coinbase/cdp-hooks";

function RevokeAccountButton({ address }: { address: string }) {
  const { revokeDelegationForAccount } = useRevokeDelegationForAccount();

  const handleRevoke = async () => {
    await revokeDelegationForAccount({ address });
    console.log("Delegation revoked for", address);
  };

  return <button onClick={handleRevoke}>Revoke Delegation</button>;
}
Developer revokes
import { CdpClient } from "@coinbase/cdp-sdk";

const cdp = new CdpClient();

await cdp.endUser.revokeDelegationForEndUserAccount({
  userId: "<USER_UUID>",
  address: "0xabc...123",
});