# Policy rules reference

This page covers the shared concepts behind policy rules — structure, evaluation logic, pre-flight testing, and limits.
For chain-specific operations and criteria, see the dedicated pages:

<HoverCardLayout>
  <HoverCardLink title="Ethereum rules" description="Operations and criteria for Ethereum transaction signing, message signing, and typed data." href="/configuration/policies/ethereum-rules" icon={Cpu} />

  <HoverCardLink title="Solana rules" description="Operations and criteria for Solana transaction signing and message signing." href="/configuration/policies/solana-rules" icon={Sun} />
</HoverCardLayout>

## Policy structure

Each policy contains an array of **rules**. Each rule specifies:

| Field | Type | Description |
| :--- | :--- | :--- |
| `action` | `'accept'` | `'reject'` | What to do when the rule matches |
| `operation` | string | The signing operation this rule applies to |
| `criteria` | array | Conditions that must **all** be true for the rule to match (AND logic) |

A rule without `criteria` matches **all** requests for that operation.

## Address formats

:::warning
**EVM addresses** are stored and matched as [EIP-55 checksummed](https://eips.ethereum.org/EIPS/eip-55) strings (mixed-case hex, e.g. `0xAb5801a7D398351b8bE11C439e05C5B3259aeC9B`). When filtering or comparing addresses, always use the checksummed form — a lowercase or incorrectly cased address will not match.

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

## Allowlist vs denylist

Most criteria that accept an `addresses` or `programIds` array support both `'in'` and `'not in'` operators, enabling two patterns:

| Approach | Operator | Behavior |
| :--- | :--- | :--- |
| **Allowlist** | `'in'` | Only accept if the value **is** in the set |
| **Denylist** | `'not in'` | Accept as long as the value is **not** in the set |

:::tip
Start with a denylist if you want a permissive policy that blocks only known-bad actors. Use an allowlist when you need strict control over which destinations are allowed.
:::

## Pre-flight evaluation

Use `openfort.policies.evaluate()` to test how your policies would handle a signing request without actually performing the operation.

### Ethereum evaluation

```ts
const result = await openfort.policies.evaluate({
  operation: 'signEvmTransaction',
  payload: {
    chainId: 1,
    to: '0x000000000000000000000000000000000000dEaD',
    value: '500000000000000000', // 0.5 ETH
    data: '0x',                  // Optional: calldata
  },
})

console.log('Allowed:', result.allowed)             // true or false
console.log('Reason:', result.reason)               // Human-readable explanation
console.log('Policy:', result.matchedPolicyId)      // "ply_..." (if matched)
console.log('Rule:', result.matchedRuleId)          // "plr_..." (if matched)
```

**Request fields:**

| Field | Type | Description |
| :--- | :--- | :--- |
| `chainId` | number | EVM chain ID |
| `to` | string | Recipient address |
| `value` | string | Transaction value in wei |
| `data` | string | Transaction calldata (optional) |
| `verifyingContract` | string | EIP-712 verifying contract (for typed data) |
| `domain` | object | EIP-712 domain object (for typed data) |
| `message` | object | EIP-712 message object (for typed data) |
| `primaryType` | string | Primary type for EIP-712 typed data |
| `evmMessage` | string | Message to sign (for personal sign) |

### Solana evaluation

```ts
const result = await openfort.policies.evaluate({
  operation: 'signSolTransaction',
  payload: {
    network: 'mainnet-beta',
    recipients: ['DtdSSG8ZJRZVv5Jx7K1MeWp7Zxcu19GD5wQRGRpQ9uMF'],
    solValue: '1000000000', // 1 SOL
  },
})

console.log('Allowed:', result.allowed)
console.log('Reason:', result.reason)
```

| Field | Type | Description |
| :--- | :--- | :--- |
| `network` | string | Solana network (`mainnet-beta`, `devnet`, `testnet`) |
| `recipients` | string\[] | Recipient addresses |
| `solValue` | string | SOL amount in lamports |
| `splValue` | string | SPL token amount |
| `mintAddress` | string | SPL token mint address |
| `programIds` | string\[] | Program IDs involved |

### Response fields

| Field | Type | Description |
| :--- | :--- | :--- |
| `allowed` | boolean | Whether the operation is permitted |
| `reason` | string | Explanation of the decision |
| `operation` | string | The operation that was evaluated |
| `matchedPolicyId` | string | ID of the policy that matched (`ply_...`) |
| `matchedRuleId` | string | ID of the rule that matched (`plr_...`) |
| `accountId` | string | The account ID evaluated (if applicable) |

## Evaluation algorithm

The policy engine follows these steps when evaluating a signing request:

1. **Collect all enabled policies** that match the request scope (project-level policies + any account-level policies for the signing wallet)
2. **Sort by priority** — higher priority numbers are evaluated first
3. **For each policy**, iterate through its rules in order:
   * If the rule's `operation` matches the request type, check all `criteria`
   * All criteria must pass (AND logic) for the rule to match
   * If the rule matches, return its `action` (`accept` or `reject`) — **first match wins**
4. **If no rule matched** across all policies, the request is **rejected** (fail-closed)

### Walkthrough example

Consider a policy with two rules:

| Rule | Action | Criteria |
| :--- | :--- | :--- |
| Rule 1 | `accept` | `ethValue <= 1 ETH` |
| Rule 2 | `accept` | `ethValue <= 10 ETH` AND `evmAddress in [0x...dEaD]` |

**Transaction A** — 0.5 ETH to any address:

* Rule 1: 0.5 ETH ≤ 1 ETH → **match** → accepted. Evaluation stops.

**Transaction B** — 5 ETH to `0x...dEaD`:

* Rule 1: 5 ETH ≤ 1 ETH → no match.
* Rule 2: 5 ETH ≤ 10 ETH AND address in list → **match** → accepted.

**Transaction C** — 5 ETH to unknown address:

* Rule 1: 5 ETH ≤ 1 ETH → no match.
* Rule 2: address not in list → no match.
* No rules matched → **rejected** (fail-closed).

**Transaction D** — 15 ETH to `0x...dEaD`:

* Rule 1: no match. Rule 2: 15 ETH > 10 ETH → no match.
* No rules matched → **rejected**.

:::warning
The fail-closed design means that once you create your first policy, any signing operation that doesn't match an explicit `accept` rule will be rejected. Plan your policies carefully before enabling them in production.
:::

## Reference tables

### Token decimals

| Token | Decimals | 1 unit in smallest denomination |
| :--- | :--- | :--- |
| ETH | 18 | 1 ETH = 1,000,000,000,000,000,000 wei |
| SOL | 9 | 1 SOL = 1,000,000,000 lamports |
| USDC (EVM & Solana) | 6 | 1 USDC = 1,000,000 |
| USDT (EVM & Solana) | 6 | 1 USDT = 1,000,000 |

## Limits

| Resource | Limit |
| :--- | :--- |
| Rules per policy | 50 |
| Criteria per rule | 20 |
| Addresses per criterion | 100 |
| Chain IDs per criterion | 50 |
| Program IDs per criterion | 100 |
| Networks per criterion | 10 |
| Regex pattern length | 500 characters |
| ABI JSON length | 100 KB |
| IDL JSON length | 100 KB |
