> ## Documentation Index
> Fetch the complete documentation index at: https://docs.crossmint.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Restrict a Signer with Scopes

> Set spending limits, recipient whitelists, and an expiry to signers.

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](/wallets/concepts/signers). To register a signer without scopes, see [Add Signers to a Wallet](/wallets/guides/signers/add-signers).

## Prerequisites

* **Wallet**: [Create a wallet](/wallets/guides/create-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.

<Info>
  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.
</Info>

## 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()`](/sdk-reference/wallets/typescript/classes/Wallet#addsigner) and pass `scopes` in the options object. The wallet's chain is inferred from the wallet itself; only the `tokenLocator` needs to match.

```typescript Node.js theme={null}
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.

<Tabs>
  <Tab title="EVM">
    Call the [register delegated key](/api-reference/wallets/register-delegated-key) endpoint with a `scopes` array. Each scope's `tokenLocator` must target the same EVM chain as the wallet.

    <Steps>
      <Step title="Register the scoped signer">
        <CodeGroup>
          ```js Node.js theme={null}
          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);
          ```

          ```bash cURL theme={null}
          curl --request POST \
              --url https://staging.crossmint.com/api/2025-06-09/wallets/YOUR_WALLET_ADDRESS/signers \
              --header 'Content-Type: application/json' \
              --header 'X-API-KEY: YOUR_API_KEY' \
              --data '{
                  "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"]
                      }
                  ]
              }'
          ```

          ```python Python theme={null}
          import requests

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

          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"],
                  }
              ],
          }
          headers = {
              "X-API-KEY": "YOUR_API_KEY",
              "Content-Type": "application/json",
          }

          response = requests.post(url, json=payload, headers=headers)
          print(response.json())
          ```
        </CodeGroup>

        The response includes a `chains` entry for the target chain with a pending signature `id` and an `approvals` field describing what must be signed by the recovery signer.
      </Step>

      <Step title="Sign the approval message with the recovery signer">
        Sign the message field returned under the signature's `approvals` using the wallet's recovery signer.
      </Step>

      <Step title="Submit the approval">
        Call the [approve signature](/api-reference/wallets/approve-signature) endpoint with the signature from the previous step and the signature ID returned in step 1. The signer becomes active once the signature is approved.
      </Step>
    </Steps>

    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.
  </Tab>

  <Tab title="Solana">
    Call the [register delegated key](/api-reference/wallets/register-delegated-key) endpoint with a `scopes` array. The wallet's chain is inferred from the wallet locator, so do not pass `chain`.

    <Steps>
      <Step title="Register the scoped signer">
        <CodeGroup>
          ```js Node.js theme={null}
          const url = "https://staging.crossmint.com/api/2025-06-09/wallets/YOUR_WALLET_ADDRESS/signers";

          const payload = {
              signer: {
                  type: "external-wallet",
                  address: "GbA2NZfpAnRVM2G2BG29qooqsYbdV5c2WVFymJ8MMir7",
              },
              expiresAt: "2027-08-31T16:34:33.854Z",
              scopes: [
                  {
                      type: "transfer",
                      tokenLocator: "solana:sol",
                      spendingLimit: { amount: "1", interval: 86400 },
                  },
              ],
          };

          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);
          ```

          ```bash cURL theme={null}
          curl --request POST \
              --url https://staging.crossmint.com/api/2025-06-09/wallets/YOUR_WALLET_ADDRESS/signers \
              --header 'Content-Type: application/json' \
              --header 'X-API-KEY: YOUR_API_KEY' \
              --data '{
                  "signer": {
                      "type": "external-wallet",
                      "address": "GbA2NZfpAnRVM2G2BG29qooqsYbdV5c2WVFymJ8MMir7"
                  },
                  "expiresAt": "2027-08-31T16:34:33.854Z",
                  "scopes": [
                      {
                          "type": "transfer",
                          "tokenLocator": "solana:sol",
                          "spendingLimit": { "amount": "1", "interval": 86400 }
                      }
                  ]
              }'
          ```

          ```python Python theme={null}
          import requests

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

          payload = {
              "signer": {
                  "type": "external-wallet",
                  "address": "GbA2NZfpAnRVM2G2BG29qooqsYbdV5c2WVFymJ8MMir7",
              },
              "expiresAt": "2027-08-31T16:34:33.854Z",
              "scopes": [
                  {
                      "type": "transfer",
                      "tokenLocator": "solana:sol",
                      "spendingLimit": {"amount": "1", "interval": 86400},
                  }
              ],
          }
          headers = {
              "X-API-KEY": "YOUR_API_KEY",
              "Content-Type": "application/json",
          }

          response = requests.post(url, json=payload, headers=headers)
          print(response.json())
          ```
        </CodeGroup>
      </Step>

      <Step title="Sign the approval transaction with the recovery signer">
        The response includes a `transaction` object. Sign the transaction returned under `transaction.approvals` with the wallet's recovery signer.
      </Step>

      <Step title="Submit the approval">
        Call the [approve transaction](/api-reference/wallets/approve-transaction) endpoint with the signature and the transaction ID. The signer becomes active once the transaction is confirmed onchain.
      </Step>
    </Steps>

    On Solana, spending-limit reset intervals are measured in seconds but enforced at the slot level (Solana produces a slot roughly every 400 milliseconds). The reset can drift by a few minutes per day relative to wall-clock time.
  </Tab>

  <Tab title="Stellar">
    Call the [register delegated key](/api-reference/wallets/register-delegated-key) endpoint with a `scopes` array. The wallet's chain is inferred from the wallet locator, so do not pass `chain`.

    <Steps>
      <Step title="Register the scoped signer">
        <CodeGroup>
          ```js Node.js theme={null}
          const url = "https://staging.crossmint.com/api/2025-06-09/wallets/YOUR_WALLET_ADDRESS/signers";

          const payload = {
              signer: {
                  type: "external-wallet",
                  address: "GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVSGZ",
              },
              expiresAt: "2027-08-31T16:34:33.854Z",
              scopes: [
                  {
                      type: "transfer",
                      tokenLocator: "stellar:usdc",
                      spendingLimit: { amount: "100", interval: 86400 },
                      recipients: ["GDUCX62IF76FFIYRNI25LJDS2V43QEZDY4YNF2DJWVIASN5LKX76Q4TW"],
                  },
              ],
          };

          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);
          ```

          ```bash cURL theme={null}
          curl --request POST \
              --url https://staging.crossmint.com/api/2025-06-09/wallets/YOUR_WALLET_ADDRESS/signers \
              --header 'Content-Type: application/json' \
              --header 'X-API-KEY: YOUR_API_KEY' \
              --data '{
                  "signer": {
                      "type": "external-wallet",
                      "address": "GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVSGZ"
                  },
                  "expiresAt": "2027-08-31T16:34:33.854Z",
                  "scopes": [
                      {
                          "type": "transfer",
                          "tokenLocator": "stellar:usdc",
                          "spendingLimit": { "amount": "100", "interval": 86400 },
                          "recipients": ["GDUCX62IF76FFIYRNI25LJDS2V43QEZDY4YNF2DJWVIASN5LKX76Q4TW"]
                      }
                  ]
              }'
          ```

          ```python Python theme={null}
          import requests

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

          payload = {
              "signer": {
                  "type": "external-wallet",
                  "address": "GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVSGZ",
              },
              "expiresAt": "2027-08-31T16:34:33.854Z",
              "scopes": [
                  {
                      "type": "transfer",
                      "tokenLocator": "stellar:usdc",
                      "spendingLimit": {"amount": "100", "interval": 86400},
                      "recipients": ["GDUCX62IF76FFIYRNI25LJDS2V43QEZDY4YNF2DJWVIASN5LKX76Q4TW"],
                  }
              ],
          }
          headers = {
              "X-API-KEY": "YOUR_API_KEY",
              "Content-Type": "application/json",
          }

          response = requests.post(url, json=payload, headers=headers)
          print(response.json())
          ```
        </CodeGroup>
      </Step>

      <Step title="Sign the approval transaction with the recovery signer">
        Sign the transaction returned under `transaction.approvals` with the wallet's recovery signer.
      </Step>

      <Step title="Submit the approval">
        Call the [approve transaction](/api-reference/wallets/approve-transaction) endpoint with the signature and the transaction ID. The signer becomes active once the transaction is confirmed onchain.
      </Step>
    </Steps>
  </Tab>
</Tabs>

## Add Scopes at Wallet Creation

Scopes and `expiresAt` can also be set on each signer entry when [creating a wallet](/api-reference/wallets/create-wallet). The signer is registered alongside the wallet. No separate approval call is required because the recovery signer authorizes the wallet creation.

<Note>
  The REST API uses `adminSigner` and `delegatedSigners` as field names. These map to `recovery` and `signers` in the SDK. See the [V1 migration guide](/wallets/guides/migrate-to-v1) for details.
</Note>

<Tabs>
  <Tab title="EVM">
    ```json theme={null}
    {
        "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 }
                        }
                    ]
                }
            ]
        }
    }
    ```
  </Tab>

  <Tab title="Solana">
    ```json theme={null}
    {
        "chainType": "solana",
        "type": "smart",
        "config": {
            "adminSigner": {
                "type": "external-wallet",
                "address": "BWBcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCcCc"
            },
            "delegatedSigners": [
                {
                    "signer": {
                        "type": "external-wallet",
                        "address": "GbA2NZfpAnRVM2G2BG29qooqsYbdV5c2WVFymJ8MMir7"
                    },
                    "expiresAt": "2027-08-31T16:34:33.854Z",
                    "scopes": [
                        {
                            "type": "transfer",
                            "tokenLocator": "solana:sol",
                            "spendingLimit": { "amount": "1", "interval": 86400 }
                        }
                    ]
                }
            ]
        }
    }
    ```
  </Tab>

  <Tab title="Stellar">
    ```json theme={null}
    {
        "chainType": "stellar",
        "type": "smart",
        "config": {
            "adminSigner": {
                "type": "external-wallet",
                "address": "GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVSGZ"
            },
            "delegatedSigners": [
                {
                    "signer": {
                        "type": "external-wallet",
                        "address": "GDUCX62IF76FFIYRNI25LJDS2V43QEZDY4YNF2DJWVIASN5LKX76Q4TW"
                    },
                    "expiresAt": "2027-08-31T16:34:33.854Z",
                    "scopes": [
                        {
                            "type": "transfer",
                            "tokenLocator": "stellar:usdc",
                            "spendingLimit": { "amount": "100", "interval": 86400 }
                        }
                    ]
                }
            ]
        }
    }
    ```
  </Tab>
</Tabs>

## 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.

```json theme={null}
{
    "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.

```json theme={null}
{
    "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.

```json theme={null}
{
    "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.

```json theme={null}
{
    "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.

```json theme={null}
{
    "signer": "external-wallet:0x9999999999999999999999999999999999999999",
    "chain": "base-sepolia",
    "expiresAt": "2027-08-31T16:34:33.854Z"
}
```

## See Also

* [Add Signers to a Wallet](/wallets/guides/signers/add-signers): register a signer without scopes
* [Remove a Signer](/wallets/guides/signers/remove-signer): revoke a signer
* [Register delegated key API reference](/api-reference/wallets/register-delegated-key): full request and response schema
* [V1 Migration Guide](/wallets/guides/migrate-to-v1): SDK and REST API terminology mapping
* [Create wallet API reference](/api-reference/wallets/create-wallet): set scopes at wallet creation under `config.delegatedSigners`
