Dry Run a Policy
Test a draft or stored policy against a single transaction before saving a policy update or starting a real signing flow.
POST /v2/rest/dryRunPolicy evaluates a transaction with the same policy engine used by signing requests, but it does not produce a signature and does not mutate policy or state data.
Use dry run when you need to:
- Validate a draft policy JSON document before calling
updatePolicy. - Preview whether a transaction would be allowed or denied by the draft/stored policy for a key.
- Test draft state controllers before creating them.
- Simulate repeated stateful evaluations without writing state entries.
How it fits
Dry run follows the same authenticated initiator path as policy update and state-controller creation. A non-empty policy evaluates a draft policy from the request. An empty policy loads the stored policy for key_id, reject when policy is empty and (state_controllers or initial_state_entries) contains draft data.
In both modes, persistence is read-only: draft inputs are never saved, and stored policy state may be read but is never mutated.
Request
Draft policy:
curl -sS -X POST "$WALLET_PROVIDER_BE_URL/v2/rest/dryRunPolicy" \
-H "Content-Type: application/json" \
-d '{
"payload": {
"key_id": "YOUR_KEY_ID",
"message": "{\"tx-1\":{\"requestType\":\"EIP191\",\"signingMessage\":\"Sign me\"}}",
"signAlg": "secp256k1",
"policy": "{\"version\":\"1.0\",\"description\":\"Allow one message\",\"rules\":[{\"description\":\"Allow exact EIP-191 message\",\"issuer\":[{\"type\":\"UserId\",\"id\":\"alice\"}],\"action\":\"allow\",\"chain_type\":\"ethereum\",\"conditions\":[{\"transaction_type\":\"eip191\",\"transaction_attr\":\"message\",\"operator\":\"eq\",\"value\":\"Sign me\"}]}]}"
},
"userSignatures": { ... }
}'
Stored policy:
curl -sS -X POST "$WALLET_PROVIDER_BE_URL/v2/rest/dryRunPolicy" \
-H "Content-Type: application/json" \
-d '{
"payload": {
"key_id": "YOUR_KEY_ID",
"message": "{\"tx-1\":{\"requestType\":\"EIP191\",\"signingMessage\":\"Sign me\"}}",
"signAlg": "secp256k1",
"policy": ""
},
"userSignatures": { ... }
}'
type DryRunPolicyPostRequest = {
payload: DryRunPolicyRequest;
userSignatures?: Record<string, UserAuthentication>;
};
type DryRunPolicyRequest = {
key_id: string;
message: string;
signAlg: string;
policy: string;
state_controllers?: string;
initial_state_entries?: string;
evaluation_count?: number;
};
| Field | Description |
|---|---|
key_id | Key ID whose auth context is used for the dry run. When policy is empty, the stored policy for this key is loaded. |
message | Stringified single-entry map of transaction ID to DSGTransaction. The request must contain exactly one transaction. |
signAlg | Signature algorithm used to parse message into a policy-engine transaction. |
policy | Policy JSON string evaluated only for this dry run. Send an empty string to load the stored policy for key_id. |
state_controllers | Optional JSON string containing DryRunStateController[]. Defaults to "[]". Used only with a supplied draft policy. |
initial_state_entries | Optional JSON string containing PolicyStateEntry[]. Defaults to "[]". Used only with a supplied draft policy. |
evaluation_count | Optional number of repeated evaluations to simulate for the same transaction. Defaults to 1. |
evaluation_count must be between 1 and 100. This is configurable with MAX_DRY_RUN_EVALUATION_COUNT.
Policy source
policy value | What is evaluated | Stateful data source |
|---|---|---|
| Non-empty string | Supplied draft policy JSON | Request state_controllers and initial_state_entries |
| Empty string | Stored policy for key_id | Stored controllers and entries for key_id, loaded into memory |
Use draft mode before saving policy or state-controller changes. Use stored mode to preview how the current key policy would evaluate a transaction before sending a real sign request.
Response
Allowed evaluation:
{
"policy_valid": true,
"stateful": false,
"evaluation_count": 1,
"results": [
{
"evaluation_index": 0,
"action": "allow"
}
]
}
Denied evaluation:
{
"policy_valid": true,
"stateful": false,
"evaluation_count": 1,
"results": [
{
"evaluation_index": 0,
"action": "deny",
"reasons": [
"No allow rule matched transaction type eip191 for issuer UserId:alice",
"Condition message eq \"Sign me\" was not satisfied"
]
}
]
}
Policy denial is a successful dry-run response. Inspect results[*].action to see whether the policy would allow or deny the transaction, and inspect results[*].reasons to understand which rule or condition prevented approval.
Request validation failures do not use the dry-run evaluation response shape. They fail before policy evaluation and return an API error message for the first validation failure, see validation section.
type DryRunPolicyResponse = {
policy_valid: boolean;
stateful: boolean;
evaluation_count: number;
results: DryRunPolicyEvaluationResult[];
};
type DryRunPolicyEvaluationResult = {
evaluation_index: number;
action: "allow" | "deny";
reasons?: string[];
state_changes?: PolicyStateEntry[];
};
| Field | Description |
|---|---|
policy_valid | Whether the evaluated policy parsed and validated successfully. |
stateful | Whether the evaluated policy uses stateful evaluation. |
evaluation_count | Number of evaluations requested and returned. |
results | Ordered dry-run evaluation results. |
evaluation_index | Zero-based simulation index. |
action | Policy decision for this evaluation. A policy denial returns deny; it is not a transport failure. |
results[*].reasons | Denial reasons for a specific evaluation, when available. |
state_changes | Predicted state updates for an allowed stateful evaluation. These entries are not persisted. |
The response does not include key_id; the caller already supplied it. It also does not include evaluated_at, because each participant uses its own local evaluation time.
Stateful dry run
For a draft stateful policy, include draft state controllers and optional initial state entries in the request. The policy references the draft controller through condition.state.id, and the controller ID is defined by the client.
For a stored stateful policy, send policy: "". The service loads the stored policy, stored controllers, and stored entries for the key into an in-memory state store.
{
"transaction_type": "erc20",
"transaction_attr": "value",
"operator": "lte",
"value": 1000,
"state": {
"id": "draft-controller-1",
"exclude_current": false
}
}
type DryRunStateController = {
id: string;
key_id: string;
description: string;
method: "sum" | "count";
window_config: WindowConfig;
partition_by: PartitionField[];
};
| Field | Description |
|---|---|
id | Client-generated draft controller ID referenced by condition.state.id in the dry-run policy. |
key_id | Must match the request key_id. |
description | Human-readable controller description. |
method | Aggregation method used by the referenced state condition. |
window_config | Rolling or fixed-window configuration. |
partition_by | Fields used to derive partition keys for state entries. |
curl -sS -X POST "$WALLET_PROVIDER_BE_URL/v2/rest/dryRunPolicy" \
-H "Content-Type: application/json" \
-d '{
"payload": {
"key_id": "YOUR_KEY_ID",
"message": "{\"tx-1\":{\"requestType\":\"EIP1559\",\"signingMessage\":\"...\"}}",
"signAlg": "secp256k1",
"policy": "{...}",
"state_controllers": "[{\"id\":\"draft-controller-1\",\"key_id\":\"YOUR_KEY_ID\",\"description\":\"Daily limit\",\"method\":\"sum\",\"window_config\":{\"type\":\"fixed\",\"interval\":\"day\"},\"partition_by\":[{\"transaction_type\":\"erc20\",\"transaction_attr\":\"to\"}]}]",
"initial_state_entries": "[]",
"evaluation_count": 3
},
"userSignatures": { ... }
}'
For repeated stateful dry run, each allowed evaluation applies its predicted state changes to the in-memory store before the next evaluation. This simulates repeated use of the same transaction without touching persistent storage.
Validation
The request is rejected before evaluation when:
evaluation_countis0or greater thanMAX_DRY_RUN_EVALUATION_COUNT.policyis non-empty and contains invalid JSON.policyis empty andkey_idhas no stored policy or whenpolicyis empty and (state_controllersorinitial_state_entries) contains draft data.- A draft policy references
condition.state.idbut no matching draft controller is supplied instate_controllers. - A stored policy references a controller that cannot be loaded from stored state.
- A draft controller has a
key_iddifferent from the requestkey_id. - Two draft controllers use the same
id. - An initial state entry references a missing controller.
- An initial state entry duplicates the same
(controller_id, partition_key). - An initial state entry contains an invalid numeric value.
A policy denial is different from these validation errors.
Environment
| Variable | Default | Description |
|---|---|---|
MAX_DRY_RUN_EVALUATION_COUNT | 100 | Maximum allowed evaluation_count. Must be greater than 0. |