Dry Run a Policy
Dry run previews whether a policy allows or denies a transaction before you save a policy update or submit a signing request. Use it to check draft rules, test stateful limits, or evaluate the policy already attached to a key.
The request uses the signing flow's policy engine. It produces no transaction signature, saves no policy or state changes, and leaves the key's authorization epoch unchanged.
Process Overview
- Choose Policy: Use an existing Key ID and supply a draft policy, or select the policy already attached to that key.
- Prepare Transaction: Include one transaction and, for a stateful draft, the controllers and starting state to test.
- Submit Authenticated Request: Send the request to your Wallet Backend using the same authentication flow as other policy-management requests.
- Review Result: Inspect the decision and any reasons or predicted state changes. Revise the draft or proceed with the signing or policy-update flow.
Policy Source
The policy field selects what to evaluate:
| Mode | policy value | State used for evaluation |
|---|---|---|
| Draft | Policy as a JSON string | Controllers and initial entries supplied in the request. |
| Stored | Empty string ("") | The key's stored controllers and entries, copied into memory. |
Use draft mode to test policy and controller changes before saving them. Use stored mode to preview the current policy. Stored mode requires an existing policy; omit state_controllers and initial_state_entries, or leave both as "[]".
Dry Run Policy in Silent Network
Example
This draft allows the EIP-191 message "Sign me" for issuer alice. Authenticate as that issuer to test the allow case.
POST /v2/rest/dryRunPolicy accepts the following request:
curl -X POST "http://<YOUR_WALLET_BACKEND_ENDPOINT>/v2/rest/dryRunPolicy" \
-H "Content-Type: application/json" \
-d '{
"payload": {
"key_id": "YOUR_KEY_ID_HERE",
"message": "{\"tx-1\":{\"requestType\":\"EIP191\",\"signingMessage\":\"Sign me\"}}",
"signAlg": "secp256k1",
"policy": "{\"version\":\"1.0\",\"epoch\":1,\"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": { ... }
}'
To test the stored policy instead, use the same request with "policy": "".
Send message, policy, state_controllers, and initial_state_entries as JSON strings. Replace the placeholders with your Key ID, backend endpoint, and authentication signatures.
Request Fields
| Field | Description |
|---|---|
key_id | Key ID used for authentication and, in stored mode, loading the policy. |
message | Map containing exactly one transaction ID and its signing payload, sent as a JSON string. |
signAlg | Signature algorithm for the transaction, such as "secp256k1". |
policy | Draft policy as a JSON string, or "" to evaluate the stored policy. |
state_controllers | Optional array of draft controllers, sent as a JSON string. Defaults to "[]"; used only in draft mode. |
initial_state_entries | Optional array of starting state entries, sent as a JSON string. Defaults to "[]"; used only in draft mode. |
evaluation_count | Number of times to simulate the same transaction. Defaults to 1. |
evaluation_count accepts 1–100 by default. Operators can set a different positive maximum with MAX_DRY_RUN_EVALUATION_COUNT.
Response
An allowed evaluation returns:
{
"policy_valid": true,
"stateful": false,
"evaluation_count": 1,
"results": [
{
"evaluation_index": 0,
"action": "allow"
}
]
}
An allow or deny is a completed evaluation. A deny means the policy would reject the transaction; it does not mean the API request failed.
| Field | Description |
|---|---|
policy_valid | true when the policy parsed and validated. An invalid policy returns an API error instead. |
stateful | Whether the policy evaluates accumulated state. |
evaluation_count | Number of evaluations performed. |
results | Evaluation results in order. |
results[*].evaluation_index | Evaluation number, starting at 0. |
results[*].action | "allow" or "deny" for that evaluation. |
results[*].reasons | Explanations of the decision, when available. Use them to understand why a transaction was denied. |
results[*].state_changes | Predicted state updates from an allowed stateful evaluation. These are not saved. |
A later signing request evaluates the policy again using the state and approvals available at that time.
Stateful Dry Run
Use stateful dry run to test limits across repeated requests, such as a daily spend cap or a maximum transaction count. Each allowed evaluation updates the simulated state before the next evaluation; a denied evaluation leaves it unchanged.
In stored mode, this starts from the key's stored state. In draft mode, supply the controllers referenced by condition.state.id and, optionally, starting state entries. For the underlying concepts, see Stateful Policy.
Example: Test a Message Count Limit
To allow "Sign me" at most twice per 24-hour window, add the stateful condition under payload.policy.rules[].conditions.
These examples show the expanded request structure, with "..." for unrelated values. Convert policy and state_controllers to JSON strings before sending the request.
{ // dry-run request payload
"payload": {
"key_id": "...",
"message": "...",
"signAlg": "...",
"policy": {
// "...",
"rules": [
{
// "...",
"conditions": [
{
"transaction_type": "eip191",
"transaction_attr": "message",
"operator": "lte",
"value": 2,
"state": {
"id": "draft-message-count",
"exclude_current": false
}
}
]
}
]
},
"state_controllers": "...",
"initial_state_entries": "...",
"evaluation_count": "..."
},
"userSignatures": "..."
}
Here, method: "count" makes the condition compare the accumulated count with 2; exclude_current: false includes the request being evaluated.
Define the matching controller under payload.state_controllers:
{
"payload": {
// "...",
"state_controllers": [
{
"id": "draft-message-count",
"key_id": "YOUR_KEY_ID_HERE",
"description": "At most two evaluations per message in 24 hours",
"method": "count",
"window_config": {
"type": "rolling",
"interval": "day",
"count": 1
},
"partition_by": [
{
"transaction_type": "eip191",
"transaction_attr": "message"
}
]
}
],
},
"userSignatures": "..."
}
The controller ID must match state.id, and its key_id must match the request. partition_by tracks each message separately. Controllers support sum for accumulated amounts or count for request counts, with rolling windows.
To create a controller for use outside the dry run, see Create a State Controller.
Leave initial_state_entries as "[]" and set evaluation_count to 3. Starting from an empty count, all three evaluations within the same window produce:
evaluation_index | Decision | Simulated count after evaluation |
|---|---|---|
0 | allow | 1 |
1 | allow | 2 |
2 | deny | 2 |
The third evaluation would raise the count to 3, so it is denied without changing the simulated state. No stored entries are created or updated.
Start from Existing Simulation Results
To continue a draft simulation, convert the previous result's state_changes to a JSON string and send it as initial_state_entries in the next request.
Set window_start (UTC) to test window expiry; otherwise, the window starts at each operator's current time. This field is not returned in the response.
Validation and Limits
Invalid requests return an API error rather than an evaluation result. Common causes include:
- Invalid policy JSON, an unsupported transaction payload, or a message map containing more or fewer than one transaction.
- An
evaluation_countoutside the configured range. - Stored mode with no policy attached to the key, or with draft controllers or initial entries supplied.
- A referenced controller that is missing, a draft controller with the wrong Key ID, or duplicate draft controller IDs.
- Initial entries with a missing controller, duplicate
(controller_id, partition_key)pairs, or invalid numeric values.
Dry run upcoming:
- External checks: If a matched rule requires external provider decisions, the dry run returns an API error. This requires mocked provider decisions.
- Approval quorums: Requires simulate a full multi-approver quorum.