> **Can't find what you're looking for?** Use `search_docs` on the docs MCP server at `https://www.openfort.io/api/mcp` to find what you need.
>
> **Have feedback?** Use `submit_feedback` on the same MCP server.

# Solana policy rules

This page covers every Solana operation and criterion type available for policies.
For shared concepts like evaluation logic and allowlist vs denylist patterns, see the [rules reference](https://www.openfort.io/docs/configuration/policies/rules-reference).

## Operations

### Backend wallet operations

| Operation | Description | Available criteria |
| :--- | :--- | :--- |
| `signSolTransaction` | Sign a Solana transaction | `solAddress`, `solValue`, `splAddress`, `splValue`, `mintAddress`, `solData`, `programId`, `solNetwork` |
| `sendSolTransaction` | Sign and broadcast a Solana transaction | `solAddress`, `solValue`, `splAddress`, `splValue`, `mintAddress`, `solData`, `programId`, `solNetwork` |
| `signSolMessage` | Sign an off-chain Solana message | `solMessage` |

The backend wallet `/sign` endpoint picks the operation from the bytes it receives: compiled transaction message bytes (what the SDK sends) and full wire-format transactions are `signSolTransaction`; anything else is `signSolMessage`. For a wire-format transaction only the message bytes are signed.

`accounts.solana.backend.sendTransaction()` and `transfer()` sign through `/sign`, so they are evaluated as `signSolTransaction`. No signing path evaluates `sendSolTransaction`; rules on it only take effect in [`policies.evaluate()`](https://www.openfort.io/docs/configuration/policies/rules-reference#pre-flight-evaluation).

### gas sponsorship operations

| Operation | Description | Available criteria |
| :--- | :--- | :--- |
| `sponsorSolTransaction` | Sponsor a Solana transaction via the [paymaster](https://www.openfort.io/docs/products/infrastructure/paymaster/solana) | `solAddress`, `solValue`, `splAddress`, `splValue`, `mintAddress`, `solData`, `programId`, `solNetwork` |

A rule may only use the criteria listed for its operation. Creating or updating a policy with any other combination fails with a validation error, because such a rule could never match.

## Criteria

:::warning
**Criteria are evaluated fail-closed against the decoded transaction.** Every criterion in a
rule must match (AND logic), and a criterion whose field is absent from the transaction does
**not** pass — it fails. The most common cause of an unexpected rejection is a criterion that
reads a field the transaction never populates, such as `solValue` on a transaction that
contains no System Program transfer. Only constrain fields your transactions actually carry.
:::

### `solAddress`

Check the recipients of the transaction's System Program transfers against a Solana address list. Every recipient must pass. A transaction with no System Program transfer has no recipients, so the criterion rejects it.

:::info
Solana addresses are stored as [Base58](https://solana.com/docs/more/exchange#basic-validation) strings. Ensure you use the standard Base58 encoding when supplying addresses.
:::

| Field | Type | Description |
| :--- | :--- | :--- |
| `type` | `'solAddress'` | Criterion type |
| `operator` | `'in'` | `'not in'` | Set membership operator |
| `addresses` | string\[] | Solana addresses (Base58) |

```ts
{
  type: 'solAddress',
  operator: 'in',
  addresses: ['DtdSSG8ZJRZVv5Jx7K1MeWp7Zxcu19GD5wQRGRpQ9uMF'],
}
```

### `solValue`

Compare the SOL transfer amount in lamports.

| Field | Type | Description |
| :--- | :--- | :--- |
| `type` | `'solValue'` | Criterion type |
| `operator` | `'<='` | `'>='` | Comparison operator |
| `value` | string | Amount in lamports (numeric string) |

```ts
{
  type: 'solValue',
  operator: '<=',
  value: '5000000000', // 5 SOL
}
```

`solValue` is read from a **System Program transfer** instruction. A transaction with no System
Program transfer — an SPL token transfer, for example — carries no `solValue`, so a `solValue`
criterion rejects it. Use [`splValue`](#splvalue) to bound SPL token amounts.

### `splAddress`

Check SPL token transfer recipient addresses.

| Field | Type | Description |
| :--- | :--- | :--- |
| `type` | `'splAddress'` | Criterion type |
| `operator` | `'in'` | `'not in'` | Set membership operator |
| `addresses` | string\[] | Recipient token account addresses (Base58) |

The recipient is the destination **token account** of the `Transfer` or `TransferChecked` instruction (for example the recipient's associated token account), not the owner wallet address.

```ts
{
  type: 'splAddress',
  operator: 'in',
  addresses: ['DtdSSG8ZJRZVv5Jx7K1MeWp7Zxcu19GD5wQRGRpQ9uMF'],
}
```

### `splValue`

Compare SPL token transfer amount.

| Field | Type | Description |
| :--- | :--- | :--- |
| `type` | `'splValue'` | Criterion type |
| `operator` | `'<='` | `'>='` | Comparison operator |
| `value` | string | Token amount (numeric string, in smallest unit) |

```ts
{
  type: 'splValue',
  operator: '<=',
  value: '1000000000', // 1000 USDC (6 decimals)
}
```

`splValue` is read from an SPL Token `Transfer` or `TransferChecked` instruction (Token or
Token-2022). A transaction containing neither carries no `splValue`, so the criterion rejects it.

### `mintAddress`

Check the SPL token mint address. The mint is read from an SPL Token `TransferChecked` instruction only; a plain `Transfer` instruction carries no mint, so the criterion rejects it.

| Field | Type | Description |
| :--- | :--- | :--- |
| `type` | `'mintAddress'` | Criterion type |
| `operator` | `'=='` | `'in'` | Match operator |
| `addresses` | string\[] | Mint addresses (Base58) |

```ts
{
  type: 'mintAddress',
  operator: '==',
  addresses: ['EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'], // USDC
}
```

### `solData`

Validate Solana instruction data against an Anchor IDL.

| Field | Type | Description |
| :--- | :--- | :--- |
| `type` | `'solData'` | Criterion type |
| `operator` | `'=='` | `'in'` | `'not in'` | `'<='` | `'>='` | `'match'` | Comparison operator |
| `idl` | string | Anchor IDL JSON string (v0.30+) |
| `instructionName` | string | Instruction name to match |
| `args` | object | Argument constraints (optional) |

The `solData` criterion decodes Solana instruction data using an [Anchor IDL](https://www.anchor-lang.com/) (v0.30+) and validates instruction parameters, similar to how the EVM `evmData` criterion uses a contract ABI.

**Supported argument types:** `bool`, `string`, `pubkey`, `u8`, `u16`, `u32`, `u64`, `u128`, `i8`, `i16`, `i32`, `i64`, `i128`, `f32`, `f64`

:::warning
User-defined types, arrays, vectors, and optionals are **not supported** in `solData` argument validation.
:::

The IDL must include `instructions` with an 8-byte `discriminator`, a `name`, and `args` with `name` and `type` for each parameter:

```ts
{
  type: 'solData',
  operator: '<=',
  idl: JSON.stringify({
    instructions: [
      {
        name: 'transfer',
        discriminator: [163, 52, 200, 231, 140, 3, 69, 186], // 8-byte discriminator
        args: [
          { name: 'amount', type: 'u64' },
        ],
      },
    ],
  }),
  instructionName: 'transfer',
  args: {
    amount: '5000000000', // 5 SOL in lamports
  },
}
```

The `args` field constrains decoded instruction arguments by name or positional key (`_0`, `_1`, ...). If `args` is omitted, matching the instruction name is sufficient.

:::warning
`solData` reads the `instructionData` field, which only [`policies.evaluate()`](https://www.openfort.io/docs/configuration/policies/rules-reference#pre-flight-evaluation) accepts. Transactions decoded at `/sign` or for sponsorship do not populate it, so a `solData` criterion rejects them.
:::

### `programId`

Check which Solana programs are involved in the transaction.

| Field | Type | Description |
| :--- | :--- | :--- |
| `type` | `'programId'` | Criterion type |
| `operator` | `'in'` | `'not in'` | Set membership operator |
| `programIds` | string\[] | Program IDs (Base58) |

```ts
{
  type: 'programId',
  operator: 'in',
  programIds: [
    'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', // Token Program
    '11111111111111111111111111111111',               // System Program
  ],
}
```

**`in` is all-of, not any-of.** The criterion passes only when **every** program the transaction
invokes appears in `programIds`. A single unlisted program rejects the whole transaction.
`not in` is the mirror image: it passes only when no invoked program appears in the list.

Programs are collected from the transaction's **top-level instructions only**. Programs reached
through cross-program invocations (CPIs) are invisible to this criterion — constrain the
top-level program you actually call, not the ones it calls internally.

:::warning
Compute-budget instructions count. `SetComputeUnitLimit` and `SetComputeUnitPrice` are top-level
instructions of the ComputeBudget program, and most Solana clients — including the
[gasless transfer example](https://www.openfort.io/docs/products/infrastructure/paymaster/solana) — add them
automatically. If your transactions set a compute budget, include the ComputeBudget program in
the list:

```ts
programIds: [
  'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', // Token Program
  'ComputeBudget111111111111111111111111111111', // ComputeBudget Program
]
```
:::

### `solNetwork`

Restrict to specific Solana networks. Available for all Solana transaction operations. Backend wallets carry no
cluster context, so on `signSolTransaction` from `/sign` the network is always `mainnet-beta`.

| Field | Type | Description |
| :--- | :--- | :--- |
| `type` | `'solNetwork'` | Criterion type |
| `operator` | `'in'` | `'not in'` | Set membership operator |
| `networks` | string\[] | `'mainnet-beta'` or `'devnet'` |

```ts
{
  type: 'solNetwork',
  operator: 'in',
  networks: ['mainnet-beta'],
}
```

### `solMessage`

Match an off-chain Solana message against a regex pattern. Only available for `signSolMessage`.

| Field | Type | Description |
| :--- | :--- | :--- |
| `type` | `'solMessage'` | Criterion type |
| `operator` | `'match'` | Regex match operator |
| `pattern` | string | JavaScript regular expression (no flags, up to 500 characters). Patterns at risk of catastrophic backtracking are rejected. |

```ts
{
  type: 'solMessage',
  operator: 'match',
  pattern: '^Sign in to MyApp:',
}
```

## Examples

### Address allowlist

Only accept SOL transfers to known addresses.

```ts
const policy = await openfort.policies.create({
  scope: 'project',
  description: 'Only allow transfers to treasury',
  rules: [
    {
      action: 'accept',
      operation: 'signSolTransaction',
      criteria: [
        {
          type: 'solAddress',
          operator: 'in',
          addresses: ['DtdSSG8ZJRZVv5Jx7K1MeWp7Zxcu19GD5wQRGRpQ9uMF'],
        },
      ],
    },
  ],
})
```

### Account-scoped allowlist

Restrict one backend wallet instead of the whole project. Account-scoped policies apply to Solana wallets exactly as they do to EVM wallets: pass `scope: 'account'` and the wallet's `accountId`. Project-scoped and account-scoped policies are evaluated as one list ordered by priority, so give this policy a higher priority than any project-wide policy it must override.

```ts
const account = await openfort.accounts.solana.backend.create()

const policy = await openfort.policies.create({
  scope: 'account',
  accountId: account.id,
  description: 'Treasury-only transfers for this wallet',
  priority: 10,
  rules: [
    {
      action: 'accept',
      operation: 'signSolTransaction',
      criteria: [
        {
          type: 'solAddress',
          operator: 'in',
          addresses: ['DtdSSG8ZJRZVv5Jx7K1MeWp7Zxcu19GD5wQRGRpQ9uMF'],
        },
      ],
    },
  ],
})
```

A transfer from this wallet to any other address is rejected at `/sign` time, while other wallets in the project are unaffected.

### SOL transfer limit

Reject SOL transfers above a threshold.

```ts
const policy = await openfort.policies.create({
  scope: 'project',
  description: 'Reject SOL transfers above 10 SOL',
  rules: [
    {
      action: 'reject',
      operation: 'signSolTransaction',
      criteria: [
        {
          type: 'solValue',
          operator: '>=',
          value: '10000000000', // 10 SOL in lamports
        },
      ],
    },
  ],
})
```

### Multi-mint SPL allowlist

Only allow transfers of USDC or USDT by using `operator: 'in'` with multiple mint addresses.

```ts
const policy = await openfort.policies.create({
  scope: 'project',
  description: 'Only allow USDC and USDT transfers',
  rules: [
    {
      action: 'accept',
      operation: 'signSolTransaction',
      criteria: [
        {
          type: 'mintAddress',
          operator: 'in',
          addresses: [
            'EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v', // USDC
            'Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB',  // USDT
          ],
        },
      ],
    },
  ],
})
```

### SPL token restriction (USDC)

Only allow USDC transfers under 1000 USDC to approved recipients.

```ts
const policy = await openfort.policies.create({
  scope: 'project',
  description: 'Restrict USDC transfers',
  rules: [
    {
      action: 'accept',
      operation: 'signSolTransaction',
      criteria: [
        {
          type: 'mintAddress',
          operator: '==',
          addresses: ['EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'], // USDC
        },
        {
          type: 'splValue',
          operator: '<=',
          value: '1000000000', // 1000 USDC (6 decimals)
        },
        {
          type: 'splAddress',
          operator: 'in',
          addresses: ['DtdSSG8ZJRZVv5Jx7K1MeWp7Zxcu19GD5wQRGRpQ9uMF'], // recipient's token account
        },
      ],
    },
  ],
})
```

### Program ID allowlist

Restrict transactions to only interact with known programs.

```ts
const policy = await openfort.policies.create({
  scope: 'project',
  description: 'Only allow System, Token and ComputeBudget programs',
  rules: [
    {
      action: 'accept',
      operation: 'signSolTransaction',
      criteria: [
        {
          type: 'programId',
          operator: 'in',
          programIds: [
            'TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA', // Token Program
            '11111111111111111111111111111111',               // System Program
            'ComputeBudget111111111111111111111111111111',    // ComputeBudget Program
          ],
        },
      ],
    },
  ],
})
```

### Network restriction

Only sponsor transactions on mainnet. Link the policy to a [gas sponsorship](https://www.openfort.io/docs/configuration/gas-sponsorship#sponsor-gas-on-solana); sponsorship evaluates `solNetwork` against the cluster of the request. On `signSolTransaction` the network is always `mainnet-beta`, so the same criterion always matches there.

```ts
const policy = await openfort.policies.create({
  scope: 'project',
  description: 'Sponsor mainnet only',
  rules: [
    {
      action: 'accept',
      operation: 'sponsorSolTransaction',
      criteria: [
        {
          type: 'solNetwork',
          operator: 'in',
          networks: ['mainnet-beta'],
        },
      ],
    },
  ],
})
```

### Message signing restriction

Only allow messages matching a specific prefix.

```ts
const policy = await openfort.policies.create({
  scope: 'project',
  description: 'Only allow sign-in messages',
  rules: [
    {
      action: 'accept',
      operation: 'signSolMessage',
      criteria: [
        {
          type: 'solMessage',
          operator: 'match',
          pattern: '^Sign in to MyApp:',
        },
      ],
    },
  ],
})
```

### Instruction data validation (solData)

Validate Solana instruction parameters using an Anchor IDL. This example restricts a custom program's `transfer` instruction to amounts under 5 SOL. As noted in [`solData`](#soldata), the rule only matches in [`policies.evaluate()`](https://www.openfort.io/docs/configuration/policies/rules-reference#pre-flight-evaluation) calls that pass `instructionData`; it never matches at `/sign` or for sponsorship.

```ts
const policy = await openfort.policies.create({
  scope: 'project',
  description: 'Limit custom program transfers to 5 SOL',
  rules: [
    {
      action: 'accept',
      operation: 'signSolTransaction',
      criteria: [
        {
          type: 'solData',
          operator: '<=',
          idl: JSON.stringify({
            instructions: [
              {
                name: 'transfer',
                discriminator: [163, 52, 200, 231, 140, 3, 69, 186],
                args: [
                  { name: 'amount', type: 'u64' },
                ],
              },
            ],
          }),
          instructionName: 'transfer',
          args: {
            amount: '5000000000', // 5 SOL in lamports
          },
        },
      ],
    },
  ],
})
```

### Combined: SOL and USDC limits

Allow native SOL transfers and USDC transfers with separate limits.

```ts
const policy = await openfort.policies.create({
  scope: 'project',
  description: 'SOL and USDC transfer limits',
  rules: [
    // Rule 1: SOL transfers under 10 SOL
    {
      action: 'accept',
      operation: 'signSolTransaction',
      criteria: [
        {
          type: 'solValue',
          operator: '<=',
          value: '10000000000', // 10 SOL
        },
      ],
    },
    // Rule 2: USDC transfers under 10,000 USDC
    {
      action: 'accept',
      operation: 'signSolTransaction',
      criteria: [
        {
          type: 'mintAddress',
          operator: '==',
          addresses: ['EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'],
        },
        {
          type: 'splValue',
          operator: '<=',
          value: '10000000000', // 10,000 USDC (6 decimals)
        },
      ],
    },
  ],
})
```

### Complete implementation

End-to-end example: create a policy, sign a message that passes, then attempt one that gets rejected.

```ts
import Openfort from '@openfort/openfort-node'

const openfort = new Openfort(process.env.OPENFORT_SECRET_KEY!, {
  walletSecret: process.env.OPENFORT_WALLET_SECRET,
})

// 1. Create a Solana backend wallet
const account = await openfort.accounts.solana.backend.create()
console.log('Wallet:', account.address)

// 2. Create a policy that only allows sign-in messages
const policy = await openfort.policies.create({
  scope: 'project',
  description: 'Only allow sign-in messages',
  rules: [
    {
      action: 'accept',
      operation: 'signSolMessage',
      criteria: [
        {
          type: 'solMessage',
          operator: 'match',
          pattern: '^Sign in to MyApp:',
        },
      ],
    },
  ],
})
console.log('Policy created:', policy.id)

// 3. Sign an allowed message — should succeed
const signature = await account.signMessage({
  message: 'Sign in to MyApp: session_abc123',
})
console.log('✅ Signed:', signature)

// 4. Attempt a disallowed message — should be rejected
try {
  await account.signMessage({
    message: 'Send 1000 SOL to attacker',
  })
} catch (error) {
  console.log('❌ Rejected by policy:', error.message)
}
```

## Token decimals

| Token | Decimals | 1 unit in smallest denomination |
| :--- | :--- | :--- |
| SOL | 9 | 1 SOL = 1,000,000,000 lamports |
| USDC | 6 | 1 USDC = 1,000,000 |
| USDT | 6 | 1 USDT = 1,000,000 |

## Common addresses

**Token mint addresses:**

| Token | Mint address |
| :--- | :--- |
| USDC | `EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v` |
| USDT | `Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB` |
| Wrapped SOL | `So11111111111111111111111111111111111111112` |

**Program IDs:**

| Program | Address |
| :--- | :--- |
| System Program | `11111111111111111111111111111111` |
| Token Program | `TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA` |
| Associated Token Program | `ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL` |
