Skip to main content
This is a technical reference for engineers implementing permissions. For a high-level overview of how permissions work, see Permissions & Access Control.

Data Model

Permissions use a four-level hierarchy: Policy → Scopes → Permission Templates → Conditions.
Policy
└── Scope (user-facing consent group)
     └── Permission Template (the rule)
          ├── type         — what action (sign, transfer, smart contract call, smart contract deploy)
          ├── effect       — ALLOW or DENY
          ├── chainId      — which chain
          ├── smartContractAddress  — (optional) specific contract address
          ├── smartContractFunction — (optional) specific function
          └── Condition[]  — (optional) additional restrictions
               ├── resource    — what to inspect (value, address, etc.)
               ├── comparator  — how to compare (equals, less than, etc.)
               └── reference   — the value to compare against
A policy belongs to a single API key. It contains scopes, which group related rules for user consent. Each scope contains one or more permission templates that define the actual rules. Templates can optionally have conditions that add further constraints.

Policy

A policy defines the full set of actions an application may ever request from a user’s wallet. If something is not included in the policy, it cannot happen.
PropertyDetails
App-specificEach API key has its own policy
ImmutableChanges require creating a new policy version

Scopes

Policies are broken down into scopes, which are the user-facing consent items. Each scope appears as a consent checkbox during onboarding or login. Each scope has:
  • A name and description, shown to the user in plain language
  • A required flag: required scopes must be accepted to use the app; optional scopes can be declined
  • One or more permission templates, the actual rules behind this scope
Scopes can also be nested (parent-child hierarchy), allowing developers to organize complex permission sets into logical groups.

Permission Types

Each permission template specifies a type that determines which wallet actions it governs:
TypeDescriptionWhen It Applies
SIGN_MESSAGESign arbitrary messages (personal_sign, signTypedData)App requests a message signature
TRANSFERSend native tokens (ETH, SOL, etc.) to an addressTransaction has a to address with no contract calldata
CALL_CONTRACTInvoke a function on a deployed smart contractTransaction includes calldata targeting a contract
DEPLOY_CONTRACTDeploy a new smart contractTransaction has no to address (contract creation)
For CALL_CONTRACT permissions, developers can further restrict by smartContractAddress and smartContractFunction. This enables rules like “only allow calling the swap function on a specific DEX router contract.”

Effects: ALLOW vs DENY

Each permission template has an effect: either ALLOW or DENY. When a transaction is submitted, Para evaluates all matching permission templates:
  1. If any matching permission evaluates to DENY → the transaction is blocked
  2. If any matching permission evaluates to ALLOW (and none evaluate to DENY) → the transaction is allowed
  3. If no permissions match → the transaction is blocked (default-deny)
DENY always takes precedence over ALLOW. This means developers can create broad ALLOW rules and then add narrow DENY exceptions for specific cases.

Conditions

Conditions add constraints to permission templates. They enable going beyond “allow transfers” to “allow transfers under 1 ETH to specific addresses.” Each permission template can have zero or more conditions. All conditions on a single permission must evaluate to true for that permission’s effect to apply (logical AND). If any condition is false, the permission does not match and is skipped during evaluation.

Resources

The resource field specifies what part of the transaction to inspect:
ResourceDescriptionApplies To
VALUETransaction value in wei (as a string)TRANSFER, CALL_CONTRACT
TO_ADDRESSDestination address of the transactionTRANSFER, CALL_CONTRACT
MESSAGEThe message content being signedSIGN_MESSAGE
ARGUMENTSSmart contract function arguments (by index, e.g., the first argument)CALL_CONTRACT
The ARGUMENTS resource allows inspecting specific parameters of a smart contract function call. For example, in an ERC-20 transfer(address, uint256) call, the first argument is the recipient address and the second is the amount.

Comparators

The comparator field determines how the resource value is compared to the reference:
ComparatorDescription
EQUALSExact match
NOT_EQUALSDoes not match
GREATER_THANStrictly greater than
GREATER_THAN_OR_EQUALSGreater than or equal to
LESS_THANStrictly less than
LESS_THAN_OR_EQUALSLess than or equal to
CONTAINED_INValue is in a provided list
NOT_CONTAINED_INValue is not in a provided list
The reference field holds the value to compare against. It can be a string, number, or array (for CONTAINED_IN / NOT_CONTAINED_IN).

Condition Type

All conditions currently use the STATIC type, meaning they are evaluated against the transaction data at the time of the request.

Chain-Specific Scoping

Every permission template includes a chainId field. When a transaction is submitted, Para checks that the permission’s chain matches the transaction’s chain. This allows creating different rules for different chains. For example, allowing transfers on Ethereum mainnet but restricting them on other chains. An empty chainId ("") means the permission applies to all chains.

Current Scope

ScopeDetails
EVM condition evaluationThe detailed condition system (VALUE, TO_ADDRESS, ARGUMENTS) is implemented for EVM chains. Solana, Stellar, and Cosmos transactions are evaluated at the permission type level (allow/deny by type) but do not yet support fine-grained conditions
Static conditionsConditions are evaluated against transaction data at request time. All condition values are set at policy creation time and cannot be customized by end users

Full Policy Schema

Here is the complete JSON structure of a policy with two scopes:
{
  "scopes": [
    {
      "name": "Basic Wallet Access",
      "description": "Sign messages with your wallet",
      "required": true,
      "permissions": [
        {
          "type": "SIGN_MESSAGE",
          "effect": "ALLOW",
          "chainId": "",
          "conditions": []
        }
      ]
    },
    {
      "name": "Token Transfers",
      "description": "Send up to 1 ETH on Ethereum mainnet",
      "required": false,
      "permissions": [
        {
          "type": "TRANSFER",
          "effect": "ALLOW",
          "chainId": "1",
          "conditions": [
            {
              "type": "STATIC",
              "resource": "VALUE",
              "comparator": "LESS_THAN_OR_EQUALS",
              "reference": "1000000000000000000"
            }
          ]
        }
      ]
    }
  ]
}
The VALUE resource uses wei denomination. 1 ETH = 1,000,000,000,000,000,000 wei (10^18).

Examples

A simple permission that lets the app sign messages without restrictions.
{
  "type": "SIGN_MESSAGE",
  "effect": "ALLOW",
  "chainId": "",
  "conditions": []
}
An empty chainId means the permission applies to all chains. No conditions means no additional restrictions.
Restrict transfers to a maximum value on a specific chain.
{
  "type": "TRANSFER",
  "effect": "ALLOW",
  "chainId": "1",
  "conditions": [
    {
      "type": "STATIC",
      "resource": "VALUE",
      "comparator": "LESS_THAN_OR_EQUALS",
      "reference": "1000000000000000000"
    }
  ]
}
This permission only matches transactions on Ethereum mainnet (chain ID 1) where the value is at most 1 ETH.
Restrict interactions to a single function on a specific smart contract. This example allows calling the swap function on a DEX router, but only when the first argument (the token address) is in an approved list.
{
  "type": "CALL_CONTRACT",
  "effect": "ALLOW",
  "chainId": "1",
  "smartContractAddress": "0x7a250d5630B4cF539739dF2C5dAcb4c659F2488D",
  "smartContractFunction": "swap",
  "conditions": [
    {
      "type": "STATIC",
      "resource": "ARGUMENTS",
      "comparator": "CONTAINED_IN",
      "reference": [
        "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
        "0xdAC17F958D2ee523a2206206994597C13D831ec7"
      ]
    }
  ]
}
Use a DENY rule to block specific recipients while keeping a broad ALLOW rule for everything else. DENY takes precedence.
[
  {
    "type": "TRANSFER",
    "effect": "ALLOW",
    "chainId": "1",
    "conditions": []
  },
  {
    "type": "TRANSFER",
    "effect": "DENY",
    "chainId": "1",
    "conditions": [
      {
        "type": "STATIC",
        "resource": "TO_ADDRESS",
        "comparator": "EQUALS",
        "reference": "0x000000000000000000000000000000000000dEaD"
      }
    ]
  }
]
The first permission allows all transfers on Ethereum mainnet. The second blocks transfers to a specific address. Because DENY always wins, the blocked address cannot receive transfers even though the broad ALLOW rule would otherwise match.

Ready to Get Started?

Para Permissions Builder

Configure permissions policies in the Para Permissions Builder.