How the engine works
The policy engine evaluates every operation against all applicable policies and outputs one of three decisions: allow, require_approval, or deny.Evaluation order (within a policy)
- Check
deny_if— if hit, deny immediately - Check
review_if/always_review— if hit, pause for owner approval - Otherwise, allow
Default-deny semantics
Pact-level policies use fail-closed semantics: if the operation does not match any policy’swhen conditions, it is automatically denied. Every operation the agent needs to perform must be explicitly covered by a policy.
Final decision
When multiple policies apply:- Any deny → deny
- No deny, but any review triggered → require_approval
- Otherwise → allow
Policy structure
Each policy in thepolicies array has this shape:
| Field | Required | Description |
|---|---|---|
name | Yes | Human-readable label for this policy |
type | Yes | Operation type: transfer, contract_call, or message_sign |
rules.effect | Yes | Always "allow" for pact-level policies |
rules.when | Yes (unless always_review: true) | Allowlist conditions — which chains, tokens, or contracts are permitted |
rules.deny_if | No | Hard-block conditions — operations that exceed these limits are denied |
rules.review_if | No | Soft-block conditions — operations exceeding these thresholds pause for owner approval |
rules.always_review | No | When true, every matched operation requires owner approval |
deny_iftakes priority overreview_if/always_reviewdenyeffect policies cannot havereview_iforalways_reviewallow+review_ifrequires a non-emptywhenallowrequires either a non-emptywhenoralways_review: true
Transfer policies
Use"type": "transfer" to control token transfers.
when — allowlist conditions
All fields are AND conditions. All specified fields must match.
| Field | Type | Description |
|---|---|---|
chain_in | string[] | Restrict to specific chains, e.g. ["BASE_ETH", "ETH"] |
token_in | ChainTokenRef[] | Restrict to specific tokens — [{"chain_id": "BASE_ETH", "token_id": "BASE_USDC"}] |
destination_address_in | ChainAddressRef[] | Restrict to specific destination addresses — [{"chain_id": "BASE_ETH", "address": "0x..."}] |
deny_if — hard-block limits
| Field | Type | Description |
|---|---|---|
amount_gt | string (decimal) | Deny if a single transfer’s token amount exceeds this |
amount_usd_gt | string (decimal) | Deny if a single transfer’s USD value exceeds this |
usage_limits.rolling_1h | object | Rolling 1-hour window limits |
usage_limits.rolling_24h | object | Rolling 24-hour window limits |
usage_limits.rolling_7d | object | Rolling 7-day window limits |
usage_limits.rolling_30d | object | Rolling 30-day window limits |
amount_gt, amount_usd_gt, tx_count_gt.
review_if — approval thresholds
Supports the same fields as when (chain_in, token_in, destination_address_in), plus:
| Field | Type | Description |
|---|---|---|
amount_gt | string (decimal) | Require approval if token amount exceeds this |
amount_usd_gt | string (decimal) | Require approval if USD value exceeds this |
Examples
Contract call policies
Use"type": "contract_call" to control smart contract interactions. EVM and Solana use different target fields.
when — allowlist conditions (EVM)
| Field | Type | Description |
|---|---|---|
chain_in | string[] | Restrict to specific chains |
target_in | ContractTargetRef[] | Allowlist by contract address and optionally function selector — [{"chain_id": "SETH", "contract_addr": "0x...", "function_id": "0x38ed1739"}]. Omit function_id to allow all functions on the contract. |
params_match | ParamMatchRule[] | Match on decoded calldata parameters (EVM only — requires function_abis) |
when — allowlist conditions (Solana)
| Field | Type | Description |
|---|---|---|
chain_in | string[] | Restrict to specific chains |
program_in | ProgramRef[] | Allow if the transaction involves any of the listed programs |
program_all_in | ProgramRef[] | Allow only if the transaction involves all of the listed programs |
params_match rule
Used to match on decoded function parameters. Requires function_abis to be set.
| Field | Type | Description |
|---|---|---|
param_name | string | Parameter name as defined in the ABI |
op | string | eq, neq, in, not_in, gt, gte, lt, lte |
value | any | Value to compare against |
params_match is only supported on EVM chains. Multiple rules are AND conditions.
function_abis
Required when using params_match. Provide the ABI fragment for each function selector you reference:
deny_if — hard-block limits
| Field | Type | Description |
|---|---|---|
amount_gt | string | Deny if the operation’s native value exceeds this |
amount_usd_gt | string | Deny if the USD value exceeds this |
usage_limits | object | Same rolling window structure as transfer (rolling_1h, rolling_24h, rolling_7d, rolling_30d), each supporting amount_gt, amount_usd_gt, tx_count_gt |
review_if — approval thresholds
Supports the same fields as when (chain_in, target_in, program_in, params_match), plus amount_gt and amount_usd_gt.
Examples
Message sign policies
Use"type": "message_sign" to control EIP-712 typed-data signing. There are no amount_gt / amount_usd_gt fields — rate limits use request_count_gt instead.
when — allowlist conditions
| Field | Type | Description |
|---|---|---|
chain_in | string[] | Restrict to specific chains |
primary_type_in | string[] | Match on the EIP-712 primaryType field, e.g. ["PermitSingle", "PermitBatch"] |
source_address_in | ChainAddressRef[] | Restrict to specific signing addresses. EVM: case-insensitive. Solana: case-sensitive. |
domain_match | MatchRule[] | Match on EIP-712 domain fields |
message_match | MatchRule[] | Match on EIP-712 message fields |
when fields are AND conditions.
Path syntax for domain_match and message_match
Use dot notation to address nested fields:
| Syntax | Meaning | Example |
|---|---|---|
. | Nested field | details.spender |
[N] | Array index (0-based) | path[0].tokenIn |
* | All array elements | items.*.token |
.length | Array length | transfers.length |
items.*.token:
eq,in,gt,gte,lt,lte: matches if any element satisfies the conditionneq,not_in: matches only if all elements satisfy the condition
eq, neq, in, not_in, gt, gte, lt, lte
deny_if — rate limits
| Field | Type | Description |
|---|---|---|
usage_limits.rolling_1h.request_count_gt | integer | Deny if signing requests in the past hour exceed this |
usage_limits.rolling_24h.request_count_gt | integer | Deny if signing requests in the past 24h exceed this |
usage_limits.rolling_7d.request_count_gt | integer | Deny if signing requests in the past 7 days exceed this |
usage_limits.rolling_30d.request_count_gt | integer | Deny if signing requests in the past 30 days exceed this |
review_if — approval thresholds
Supports the same fields as when (chain_in, primary_type_in, source_address_in, domain_match, message_match).
Chain ID validation
For EVM chains, the engine checks that the requestchain_id matches domain.chainId in the typed data. A mismatch results in an immediate deny with reason eip712_domain_chain_id_mismatch.
Examples
Completion conditions
Completion conditions determine when a pact automatically ends. At least one is required. The pact completes when any condition is satisfied, after which access is revoked immediately.| Type | Threshold | Description |
|---|---|---|
tx_count | string (integer) | Complete after N successful transactions — e.g. "1" for a one-time operation |
amount_spent | string (decimal) | Complete after cumulative token amount reaches this — e.g. "1000" for 1000 USDC |
amount_spent_usd | string (decimal) | Complete after cumulative USD spend reaches this — e.g. "500" |
time_elapsed | string (seconds) | Complete after N seconds from pact activation — e.g. "2592000" for 30 days |
Amount units
amount_gt, amount_spent, and related fields use the token’s transfer unit — the same unit passed when submitting a transfer:
"1.5"for USDC means 1.5 USDC (not 1,500,000 micro-USDC)"0.01"for ETH means 0.01 ETH (not wei)
USD-based conditions (
amount_usd_gt, amount_spent_usd) only apply to tokens with available price data. For tokens without price data, use token-denominated limits (amount_gt, amount_spent) instead.