# Policies

Policies are **rule-based authorization controls** that govern which operations Openfort can perform on your behalf.
Each policy contains one or more rules that evaluate incoming requests against criteria you define — such as value limits, address allowlists, network restrictions, or calldata constraints.

**Common use cases:**

* **Transaction filtering** — block transfers to malicious or restricted addresses
* **Address allowlisting** — only permit transactions to known, trusted destinations
* **Value caps** — enforce per-transaction limits on native and token transfers
* **Contract call restrictions** — limit which smart contract functions can be called
* **Message signing controls** — prevent signing of arbitrary or fraudulent messages

## How policies work

When a request is received, the policy engine evaluates it:

1. **Policies are ordered by priority** — higher priority policies are evaluated first
2. **Rules within a policy use AND logic** — all criteria in a rule must match for it to apply
3. **First match wins** — the first rule that matches determines the outcome (`accept` or `reject`)
4. **Fail-closed** — if no rule matches, the operation is **rejected**

```text
Signing request
  │
  ├── Policy A (priority: 100)
  │     ├── Rule 1: accept if value ≤ 1 ETH AND address in allowlist → ✅ Match → Accept
  │     └── Rule 2: reject if value > 1 ETH                          → (skipped)
  │
  ├── Policy B (priority: 50)
  │     └── Rule 1: ...                                               → (skipped)
  │
  └── No match → ❌ Reject (fail-closed)
```

### Scopes

| Scope | Applies to | Use case |
| :--- | :--- | :--- |
| `project` | All backend wallets in the project, or all gas sponsorship requests | Organization-wide limits and allowlists |
| `account` | A specific backend wallet | Per-wallet restrictions for sensitive accounts |
| `transaction` | A specific gas sponsorship request (requires explicit `policyId`) | Strict per-request sponsorship control |

:::info
The `transaction` scope is used exclusively for [gas sponsorship policies](/docs/configuration/gas-sponsorship). When a `policyId` is passed to the paymaster, validation is strict — if the rules don't match, the transaction is rejected. Project-scoped gas sponsorship policies are auto-discovered and use soft validation.
:::

:::warning
Once you create your first policy, **all signing operations that don't match any rule will be rejected**. Start with permissive policies and tighten them over time.
:::

## Create your first policy

::::steps

### Prerequisites

Make sure you have the Node.js SDK installed and configured. See the [quickstart](/docs/products/server/setup) for setup instructions.

### Create a project-scoped policy

Create a policy that rejects high-value transactions across all backend wallets.

:::code-group

```ts [Ethereum]
import Openfort from '@openfort/openfort-node'

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

const policy = await openfort.policies.create({
  scope: 'project',
  description: 'Reject transactions above 1 ETH',
  rules: [
    {
      action: 'reject',
      operation: 'signEvmTransaction',
      criteria: [
        {
          type: 'ethValue',
          operator: '>',
          ethValue: '1000000000000000000', // 1 ETH in wei
        },
      ],
    },
  ],
})

console.log('Policy created:', policy.id)
```

```ts [Solana]
import Openfort from '@openfort/openfort-node'

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

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
        },
      ],
    },
  ],
})

console.log('Policy created:', policy.id)
```

:::

### Verify with pre-flight evaluation

Test your policy before making real signing requests using `openfort.policies.evaluate()`.

```ts
const result = await openfort.policies.evaluate({
  operation: 'signEvmTransaction',
  payload: {
    chainId: 1,
    to: '0x000000000000000000000000000000000000dEaD',
    value: '2000000000000000000', // 2 ETH — should be rejected
  },
})

console.log('Allowed:', result.allowed)             // false
console.log('Reason:', result.reason)               // "Rejected by policy rule"
console.log('Policy:', result.matchedPolicyId)      // "ply_..."
console.log('Rule:', result.matchedRuleId)          // "plr_..."
```

::::

## Common patterns

### Address allowlist

Only allow transactions to a set of known addresses on specific networks. Using `operator: 'in'` treats the list as an **allowlist** — any address not in the list is rejected.

```ts
const policy = await openfort.policies.create({
  scope: 'project',
  description: 'Only allow transfers to treasury and vault',
  rules: [
    {
      action: 'accept',
      operation: 'sendEvmTransaction',
      criteria: [
        {
          type: 'evmAddress',
          operator: 'in',
          addresses: [
            '0x000000000000000000000000000000000000dEaD', // Treasury
            '0x1234567890abcdef1234567890abcdef12345678', // Vault
          ],
        },
        {
          type: 'evmNetwork',
          operator: 'in',
          chainIds: [1, 137, 8453], // Ethereum, Polygon, Base
        },
      ],
    },
  ],
})
```

### Address denylist

Block transactions to known malicious addresses while allowing everything else. Using `operator: 'not in'` treats the list as a **denylist**.

```ts
const policy = await openfort.policies.create({
  scope: 'project',
  description: 'Block transfers to known bad addresses',
  rules: [
    {
      action: 'accept',
      operation: 'signEvmTransaction',
      criteria: [
        {
          type: 'evmAddress',
          operator: 'not in',
          addresses: [
            '0xBadAddress0000000000000000000000000000001',
            '0xBadAddress0000000000000000000000000000002',
          ],
        },
      ],
    },
  ],
})
```

### Multi-rule ordering strategies

When a policy has multiple rules, their **order matters** because evaluation stops at the first match. This lets you build layered authorization — for example, allowing small transactions to any address but requiring larger transactions to go to approved addresses.

```ts
const policy = await openfort.policies.create({
  scope: 'project',
  description: 'Tiered value limits with address restrictions',
  rules: [
    // Rule 1: Accept any transaction under 1 ETH
    {
      action: 'accept',
      operation: 'signEvmTransaction',
      criteria: [
        {
          type: 'ethValue',
          operator: '<=',
          ethValue: '1000000000000000000', // 1 ETH
        },
      ],
    },
    // Rule 2: Accept transactions up to 10 ETH, but only to allowlisted addresses
    {
      action: 'accept',
      operation: 'signEvmTransaction',
      criteria: [
        {
          type: 'ethValue',
          operator: '<=',
          ethValue: '10000000000000000000', // 10 ETH
        },
        {
          type: 'evmAddress',
          operator: 'in',
          addresses: ['0x000000000000000000000000000000000000dEaD'],
        },
      ],
    },
    // Anything above 10 ETH or to non-allowlisted addresses → no match → rejected
  ],
})
```

**How this evaluates:**

* A 0.5 ETH transaction to any address → matches Rule 1 → **accepted**
* A 5 ETH transaction to `0x...dEaD` → fails Rule 1 (over 1 ETH), matches Rule 2 → **accepted**
* A 5 ETH transaction to an unknown address → fails Rule 1, fails Rule 2 (not allowlisted) → **rejected**
* A 15 ETH transaction to `0x...dEaD` → fails both rules → **rejected**

### Restrict to specific contract calls

Only allow ERC-20 `transfer` calls by matching the transaction calldata against an ABI.

```ts
const policy = await openfort.policies.create({
  scope: 'project',
  description: 'Only allow ERC-20 transfer calls',
  rules: [
    {
      action: 'accept',
      operation: 'signEvmTransaction',
      criteria: [
        {
          type: 'evmData',
          operator: '==',
          abi: JSON.stringify([
            {
              type: 'function',
              name: 'transfer',
              inputs: [
                { name: 'to', type: 'address' },
                { name: 'amount', type: 'uint256' },
              ],
              outputs: [{ type: 'bool' }],
            },
          ]),
          functionName: 'transfer',
        },
      ],
    },
  ],
})
```

### Disable arbitrary hash signing

Reject all raw hash signing to prevent fraud. Since `signEvmHash` has no criteria, the rule matches all hash signing attempts unconditionally.

```ts
const policy = await openfort.policies.create({
  scope: 'project',
  description: 'Block all raw hash signing',
  rules: [
    {
      action: 'reject',
      operation: 'signEvmHash',
    },
  ],
})
```

### SPL token restrictions (Solana)

Combine mint address, value, and recipient criteria to restrict SPL token transfers.

:::code-group

```ts [USDC only]
// Only allow USDC transfers under 1000 USDC to approved recipients
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'],
        },
      ],
    },
  ],
})
```

```ts [SOL + USDC]
// Allow native SOL transfers and USDC transfers with separate limits
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: 'sendSolTransaction',
      criteria: [
        {
          type: 'solValue',
          operator: '<=',
          value: '10000000000', // 10 SOL
        },
      ],
    },
    // Rule 2: USDC transfers under 10,000 USDC
    {
      action: 'accept',
      operation: 'sendSolTransaction',
      criteria: [
        {
          type: 'mintAddress',
          operator: '==',
          addresses: ['EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v'],
        },
        {
          type: 'splValue',
          operator: '<=',
          value: '10000000000', // 10,000 USDC (6 decimals)
        },
      ],
    },
  ],
})
```

:::

### Account-scoped policy

Apply restrictions to a specific backend wallet rather than the entire project.

```ts
// Create a backend wallet
const account = await openfort.accounts.evm.backend.create()

// Create an account-scoped policy
const policy = await openfort.policies.create({
  scope: 'account',
  accountId: account.id,
  description: 'Account-level restrictions for this wallet',
  priority: 10,
  rules: [
    {
      action: 'accept',
      operation: 'signEvmTransaction',
      criteria: [
        {
          type: 'evmAddress',
          operator: 'in',
          addresses: ['0x000000000000000000000000000000000000dEaD'],
        },
        {
          type: 'ethValue',
          operator: '<=',
          ethValue: '500000000000000000', // 0.5 ETH
        },
      ],
    },
    {
      action: 'accept',
      operation: 'signEvmMessage',
      criteria: [
        {
          type: 'evmMessage',
          operator: 'match',
          pattern: '^Sign in to',
        },
      ],
    },
  ],
})
```

### Multi-rule cross-chain policy

A single policy can contain rules for both Ethereum and Solana operations.

```ts
const policy = await openfort.policies.create({
  scope: 'project',
  description: 'Cross-chain policy with Ethereum and Solana rules',
  priority: 100,
  rules: [
    // Ethereum: accept transactions under 1 ETH to known addresses
    {
      action: 'accept',
      operation: 'signEvmTransaction',
      criteria: [
        { type: 'ethValue', operator: '<=', ethValue: '1000000000000000000' },
        {
          type: 'evmAddress',
          operator: 'in',
          addresses: ['0x000000000000000000000000000000000000dEaD'],
        },
      ],
    },
    // Solana: accept SOL transfers under 5 SOL
    {
      action: 'accept',
      operation: 'signSolTransaction',
      criteria: [
        { type: 'solValue', operator: '<=', value: '5000000000' },
        {
          type: 'solAddress',
          operator: 'in',
          addresses: ['DtdSSG8ZJRZVv5Jx7K1MeWp7Zxcu19GD5wQRGRpQ9uMF'],
        },
      ],
    },
    // Reject all raw hash signing
    {
      action: 'reject',
      operation: 'signEvmHash',
    },
  ],
})
```

:::tip\[Runnable examples]
See the complete runnable examples for [EVM policies](https://github.com/openfort-xyz/openfort-node/tree/main/examples/evm/policies) and [Solana policies](https://github.com/openfort-xyz/openfort-node/tree/main/examples/solana/policies) in the Node SDK repository.
:::

## Managing policies

### List policies

```ts
// List all policies
const result = await openfort.policies.list({ limit: 10 })

console.log(`Found ${result.total} policies:`)
for (const policy of result.data) {
  console.log(`  ${policy.id} — ${policy.description}`)
}

// Filter by scope
const projectPolicies = await openfort.policies.list({ scope: ['project'] })
const accountPolicies = await openfort.policies.list({ scope: ['account'] })
```

### Update a policy

Updating a policy replaces all existing rules with the new set.

```ts
import type { UpdatePolicyBody } from '@openfort/openfort-node'

const updateBody: UpdatePolicyBody = {
  description: 'Lowered threshold to 0.5 ETH',
  rules: [
    {
      action: 'reject',
      operation: 'signEvmTransaction',
      criteria: [
        {
          type: 'ethValue',
          operator: '>',
          ethValue: '500000000000000000', // 0.5 ETH
        },
      ],
    },
  ],
}

const updated = await openfort.policies.update('ply_...', updateBody)
console.log('Updated:', updated.id)
```

### Delete a policy

```ts
const result = await openfort.policies.delete('ply_...')
console.log('Deleted:', result.deleted) // true
```

### Disable a policy

You can temporarily disable a policy without deleting it.

```ts
const disabled = await openfort.policies.update('ply_...', {
  enabled: false,
})
```

## Client-side validation

The SDK exports Zod schemas for client-side validation. Catch invalid payloads before they reach the API.

```ts
import {
  CreatePolicyBodySchema,
  UpdatePolicyBodySchema,
} from '@openfort/openfort-node'

try {
  CreatePolicyBodySchema.parse(policyBody)
} catch (error) {
  console.error('Invalid policy:', error.issues)
}
```

## Next steps

<HoverCardLayout>
  <HoverCardLink title="Rules reference" description="Complete reference for all operations, criteria types, operators, and pre-flight evaluation." href="/configuration/policies/rules-reference" icon={BookOpen} />

  <HoverCardLink title="Security" description="Learn about TEE-based signing, authentication, and wallet secret management." href="/products/server/security" icon={ShieldCheck} />
</HoverCardLayout>
