Skip to main content
A signer added to a wallet after creation handles day-to-day operations on behalf of the recovery signer. By default, an added signer has full control. Scopes narrow that control by restricting which tokens the signer can transfer, in what amounts, and to which recipients. The signer-level expiresAt field denies the signer after a given timestamp. For an overview of signer types and roles, see Signers. To register a signer without scopes, see Add Signers to a Wallet.

Prerequisites

  • Wallet: Create a wallet on EVM, Solana, or Stellar
  • Recovery signer: The recovery signer must be available to approve the registration transaction
  • API key with the wallets.create scope. In staging, all scopes are included by default.

What Scopes Restrict

A scope applies to a single token on a single chain and can combine two optional restrictions:
  • Spending limit: a maximum amount the signer can transfer, optionally resetting on a fixed interval
  • Recipient whitelist: a list of addresses or wallet locators the signer is allowed to transfer to
The only supported scope type is transfer. A signer with no scopes has unrestricted token spending; a signer with one or more scopes can only transfer the tokens listed in its scopes, and only within the configured restrictions. In addition to scopes, the signer accepts a top-level expiresAt (ISO 8601) after which every transaction from the signer is rejected. expiresAt is not a scope field. It sits alongside the scopes array in the request body.
Scopes are checked before the transaction is broadcast onchain. A transfer that exceeds the spending limit, targets a non-whitelisted recipient, or is signed after expiresAt is rejected at validation time.

Add a Signer with Scopes

Two ways to attach scopes to a signer:
  • @crossmint/wallets-sdk: pass a scopes array to addSigner(). The SDK handles approval through the wallet’s recovery signer automatically. expiresAt and wallet-creation-time scope registration require the REST API
  • REST API: direct control over the full registration request, including expiresAt and registering scopes at wallet creation time

Using the SDK

Use addSigner() and pass scopes in the options object. The wallet’s chain is inferred from the wallet itself; only the tokenLocator needs to match.
Node.js
import { createCrossmint, CrossmintWallets } from "@crossmint/wallets-sdk";

const crossmint = createCrossmint({ apiKey: "YOUR_SERVER_API_KEY" });
const wallets = CrossmintWallets.from(crossmint);

const wallet = await wallets.getWallet("YOUR_WALLET_ADDRESS", { chain: "base-sepolia" });

await wallet.useSigner({
    type: "server",
    secret: process.env.CROSSMINT_SIGNER_SECRET,
});

const signer = await wallet.addSigner(
    { type: "external-wallet", address: "0x1234567890123456789012345678901234567890" },
    {
        scopes: [
            {
                type: "transfer",
                tokenLocator: "base-sepolia:usdc",
                spendingLimit: { amount: "10", interval: 86400 },
                recipients: ["0xABCDEF0123456789ABCDEF0123456789ABCDEF01"],
            },
        ],
    },
);

console.log("Scoped signer added:", signer.locator);
For Solana, use chain: "solana" when fetching the wallet and a solana:<symbol-or-mint> token locator (for example, solana:usdc). For Stellar, use chain: "stellar" and a stellar:<symbol-or-contract-id> locator. To set expiresAt or to attach scopes at wallet creation, use the REST API below.

Using the REST API

Use REST when you need expiresAt or when registering scopes at wallet creation.
Call the register delegated key endpoint with a scopes array. Each scope’s tokenLocator must target the same EVM chain as the wallet.
1

Register the scoped signer

const url = "https://staging.crossmint.com/api/2025-06-09/wallets/YOUR_WALLET_ADDRESS/signers";

const payload = {
    signer: "external-wallet:0x1234567890123456789012345678901234567890",
    chain: "base-sepolia",
    expiresAt: "2027-08-31T16:34:33.854Z",
    scopes: [
        {
            type: "transfer",
            tokenLocator: "base-sepolia:usdc",
            spendingLimit: { amount: "10", interval: 86400 },
            recipients: ["0xABCDEF0123456789ABCDEF0123456789ABCDEF01"],
        },
    ],
};

const response = await fetch(url, {
    method: "POST",
    headers: {
        "X-API-KEY": "YOUR_API_KEY",
        "Content-Type": "application/json",
    },
    body: JSON.stringify(payload),
});

const data = await response.json();
console.log(data);
The response includes a transaction object with an approvals field describing what must be signed by the recovery signer.
2

Sign the approval message with the recovery signer

Sign the message field returned under transaction.approvals using the wallet’s recovery signer.
3

Submit the approval

Call the approve transaction endpoint with the signature from the previous step and the transaction ID returned in step 1. The signer becomes active once the transaction is confirmed onchain.
Amounts in spendingLimit.amount are decimal strings expressed in the token’s display units (for example, "10" USDC means 10 USDC, not 10 base units). Crossmint reads the token’s decimals() onchain to convert to the wei amount the policy contract expects.

Add Scopes at Wallet Creation

Scopes and expiresAt can also be set on each signer entry when creating a wallet. The signer is registered alongside the wallet. No separate approval call is required because the recovery signer authorizes the wallet creation.
The REST API uses adminSigner and delegatedSigners as field names. These map to recovery and signers in the SDK. See the V1 migration guide for details.
{
    "chainType": "evm",
    "type": "smart",
    "config": {
        "adminSigner": {
            "type": "external-wallet",
            "address": "0x1234567890123456789012345678901234567890"
        },
        "delegatedSigners": [
            {
                "signer": "external-wallet:0x9999999999999999999999999999999999999999",
                "expiresAt": "2027-08-31T16:34:33.854Z",
                "scopes": [
                    {
                        "type": "transfer",
                        "tokenLocator": "base-sepolia:usdc",
                        "spendingLimit": { "amount": "10", "interval": 86400 }
                    }
                ]
            }
        ]
    }
}

Common Patterns

One-Time Spending Allowance

Omit interval to set an absolute, non-resetting cap. Once the signer has spent up to amount, every subsequent transfer is rejected until the signer is removed and re-registered.
{
    "type": "transfer",
    "tokenLocator": "base-sepolia:usdc",
    "spendingLimit": { "amount": "10" }
}

Recurring Daily Allowance

Set interval to the number of seconds in the period (for example, 86400 for 24 hours). The counter resets the first time the signer transfers after the interval elapses, measured from the moment the scope was registered onchain.
{
    "type": "transfer",
    "tokenLocator": "base-sepolia:usdc",
    "spendingLimit": { "amount": "10", "interval": 86400 }
}

Recipient Whitelist

Pass a non-empty recipients array to restrict transfers to specific addresses. Wallet locators (for example, "email:user@example.com:evm") are supported in addition to raw addresses. An empty or omitted recipients array allows any recipient.
{
    "type": "transfer",
    "tokenLocator": "base-sepolia:usdc",
    "recipients": [
        "0xABCDEF0123456789ABCDEF0123456789ABCDEF01",
        "email:partner@example.com:evm"
    ]
}

Restrict the Token Without Capping the Amount

A transfer scope with no spendingLimit and no recipients restricts the signer to that one token but otherwise allows unlimited transfers of it. This is useful when the spending limit is enforced off-chain by your application.
{
    "type": "transfer",
    "tokenLocator": "base-sepolia:usdc"
}

Expire the Signer

Set expiresAt at the signer level (not inside a scope). After the timestamp, every transaction signed by this signer is rejected.
{
    "signer": "external-wallet:0x9999999999999999999999999999999999999999",
    "chain": "base-sepolia",
    "expiresAt": "2027-08-31T16:34:33.854Z"
}

See Also