Wallet policies

Wallet controls, default-allow rollout, and policy audit visibility.

Wallet policies constrain how a custody wallet can move funds. SDP starts from default allow, then applies the active wallet control profile and any payment policy fields you configure. Policies are managed via GET and PUT on /v1/payments/wallets/{walletId}/policies and enforced before wallet operations execute.

Use policies for treasury wallets, automated payout wallets, or any custody wallet where unbounded outbound flow would be a liability.

Reading a wallet's policies

curl https://api.solana.com/v1/payments/wallets/wal_abc123/policies \
  -H "Authorization: Bearer sk_test_..."
const response = await fetch(
  "https://api.solana.com/v1/payments/wallets/wal_abc123/policies",
  { headers: { Authorization: "Bearer sk_test_..." } }
);
const { data } = await response.json();
// data.policy.destinationAllowlist: string[]
// data.policy.maxTransferAmount?: string
// data.policy.maxDailyAmount?: string
// data.policy.controlProfile?.providerMappingStatus
// data.policy.audit?.recentEvaluations

The payload is wrapped in the standard data / meta envelope, with the policy under a policy key:

{
  "data": {
    "policy": {
      "walletId": "wal_abc123",
      "destinationAllowlist": ["7xKXz...9fGh", "5aBCd...2eFg"],
      "maxTransferAmount": "100.00",
      "maxDailyAmount": "1000.00",
      "defaultAction": "allow",
      "controlProfile": {
        "id": "wcp_abc123",
        "status": "active",
        "activeRevisionId": "wcpr_abc123",
        "revisionId": "wcpr_abc123",
        "revisionNumber": 3,
        "defaultAction": "allow",
        "rules": [
          {
            "id": "approval-required",
            "kind": "approval",
            "families": ["payment", "ramp"],
            "action": "approval_required",
            "name": "Approval checks"
          }
        ],
        "providerMappingStatus": "not_applicable",
        "createdAt": "2026-05-14T08:00:00Z",
        "updatedAt": "2026-05-14T10:15:00Z",
        "activatedAt": "2026-05-14T10:15:00Z"
      },
      "audit": {
        "recentEvaluations": [
          {
            "walletOperationId": "wop_abc123",
            "policyEvaluationId": "peval_abc123",
            "operationFamily": "payment",
            "operationType": "payment_transfer",
            "asset": "USDC",
            "amount": "100.00",
            "destination": "7xKXz...9fGh",
            "status": "pending_approval",
            "decision": "approval_required",
            "reasonCode": "wallet_policy_match",
            "reason": "Payment matched approval policy.",
            "requiresApproval": true,
            "approvalRequestId": "appr_abc123",
            "operationCreatedAt": "2026-05-14T10:20:00Z",
            "operationUpdatedAt": "2026-05-14T10:20:00Z",
            "evaluatedAt": "2026-05-14T10:20:00Z"
          }
        ]
      },
      "createdAt": "2026-05-14T08:00:00Z",
      "updatedAt": "2026-05-14T10:15:00Z"
    }
  },
  "meta": { "requestId": "req_...", "timestamp": "2026-05-18T00:00:00.000Z" }
}

Amount fields are UI-unit decimal strings ("100.00" means 100 tokens, regardless of the mint's decimals — matching the transfer-request convention). The configured value is a single threshold, but enforcement is per token: maxDailyAmount sums each token's outbound transfers separately within the day window, so a wallet with maxDailyAmount: "1000" can send up to 1000 USDC and 1000 SOL in the same UTC day — there is no aggregate cap across mints. If you need different per-token thresholds (e.g., 10,000 USDC daily but 50 SOL daily), partition into separate wallets.

Default allow and active controls

Wallet policy evaluation starts from default allow. If no wallet control profile exists, supported payment and ramp operations continue to run unless another product-specific check rejects them. When you activate a control profile, SDP evaluates its rules first and returns the resulting decision as one of allow, deny, approval_required, provider_approval_required, review, or not_evaluated.

The controlProfile object shows the active immutable revision. Treat revisionId and revisionNumber as audit references: a policy evaluation records which revision was active when the wallet operation was checked.

Policy evaluation and audit records

Each supported wallet operation records a wallet operation row before policy evaluation and a policy evaluation row after SDP decides what should happen. audit.recentEvaluations exposes the most recent decisions for the wallet so support and customer operators can review:

  • what operation was evaluated (walletOperationId, operationFamily, operationType)
  • what decision SDP made (decision, reasonCode, reason, requiresApproval)
  • whether an approval request was created (approvalRequestId)
  • when the operation was created, updated, and evaluated

Use these records to explain allow, deny, approval-required, and review outcomes. They are an operational audit summary, not the full raw provider payload.

Provider mapping status

providerMappingStatus describes whether an SDP policy revision has also been mapped into a custody or ramp provider's native policy system. Current payment wallet controls are SDP-enforced first. A value of not_applicable means there is no provider-native mapping for that revision. pending, partial, or failed mean SDP still evaluates the operation, but you should not assume the same rule has been fully synced to the provider.

Destination allowlist

The allowlist is a list of on-chain destination addresses (32-44 character base-58 Solana pubkeys). When set, POST /v1/payments/transfers rejects any destination not in the list.

  • Maximum entries: 500 addresses per wallet.
  • Each entry is a single on-chain address — no patterns, ranges, or address books.
  • Empty allowlist ([]) means no destination restriction. To enable enforcement, populate at least one entry.

The allowlist exists to bound risk on automated wallets (a hot wallet that should only ever pay a known set of counterparties) and to enforce treasury controls (only the corporate cold-storage address can drain the operating wallet).

Transfer limits

Two limits, both optional:

  • maxTransferAmount — per-transfer cap. Any single POST /v1/payments/transfers exceeding this amount is rejected. Compared against the request's amount directly, so it is naturally per-token.
  • maxDailyAmount — UTC-calendar-day cap, enforced per token. The day window resets at 00:00 UTC; the projected total is the sum of that wallet's outbound transfers for the same token in the current day (pending/processing/confirmed/finalized) plus the new request's amount. Different tokens are summed independently — see the per-token note under Reading a wallet's policies.

Set both for defense in depth: a per-transaction cap that catches obvious mistakes, plus a daily cap that bounds blast radius if many small transfers are submitted in a coordinated attack.

Updating policies

PUT /v1/payments/wallets/{walletId}/policies has full-replace semantics — the request body becomes the new policy state in its entirety. To remove the allowlist, send destinationAllowlist: []. To drop a transfer limit, omit the field on the next PUT.

curl -X PUT https://api.solana.com/v1/payments/wallets/wal_abc123/policies \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "destinationAllowlist": [
      "7xKXz...9fGh",
      "5aBCd...2eFg"
    ],
    "maxTransferAmount": "100.00",
    "maxDailyAmount": "1000.00"
  }'
await fetch(
  "https://api.solana.com/v1/payments/wallets/wal_abc123/policies",
  {
    method: "PUT",
    headers: {
      "Authorization": "Bearer sk_test_...",
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      destinationAllowlist: ["7xKXz...9fGh", "5aBCd...2eFg"],
      maxTransferAmount: "100.00",
      maxDailyAmount: "1000.00",
    }),
  }
);

Because PUT is full-replace, treat policy edits like ordinary form submissions: read the current state, present it for editing, and PUT the complete updated object. Don't construct partial patches.

Interaction with compliance

Wallet policies enforce structural constraints (where funds can go, how much, how fast). They do not replace compliance screening. If your organization has compliance screening enabled, screening happens on the destination address as a separate check, in addition to the allowlist match. A transfer can be rejected by either layer; both must pass to reach the network.

See the Compliance API reference for address-screening flows that complement policies.

Is this page helpful?