Policies
Fystack policies are programmable guardrails for sensitive wallet actions. A policy targets a workspace, optionally narrows to specific wallets, and contains rules that evaluate an expression against a structured action payload.
Rules currently run for two actions:
- Name
- withdrawal.create
- Type
- trigger_action
- Description
Evaluated before a withdrawal is created. A
DENYblocks the request. A matchingALLOWbypasses transaction approval and lets the withdrawal move to signing.
- Name
- web3.contract_call
- Type
- trigger_action
- Description
Evaluated before EVM contract calls and ERC-20 token transfers are sent to signing. A
DENYrecords the activity asdenied_by_policy. A matchingALLOWskips transaction approval and proceeds to signing.
The policy schema is generated from the backend action schema in Apex. Use Get the policy schema when building UI builders or validating field availability dynamically.
Policy structure
A policy is the container. A rule is the executable unit.
{
"name": "Large withdrawal guard",
"approval_group_id": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
"wallet_ids": [],
"rules": [
{
"trigger_action": "withdrawal.create",
"condition": "withdrawal.value_usd > 10000 && !withdrawal.is_whitelisted",
"effect": "DENY"
}
]
}
- Name
- approval_group_id
- Type
- string
- Description
The approval group that reviews policy creation, policy edits, policy deletion, and rule changes.
- Name
- wallet_ids
- Type
- string[]
- Description
Wallets this policy applies to. An empty array means every wallet in the workspace.
- Name
- rules
- Type
- array
- Description
Up to 50 rules. Each rule has a
trigger_action,condition,effect, optionaldescription, and optionalis_enabled.
Policy statuses
- Name
- pending
- Type
- status
- Description
Created but not approved yet. Pending policies are not evaluated.
- Name
- active
- Type
- status
- Description
Approved and live. Only active policies are loaded by the evaluator.
- Name
- inactive
- Type
- status
- Description
Disabled and not evaluated.
- Name
- rejected
- Type
- status
- Description
Rejected during approval and not evaluated.
Policy evaluation
For each guarded request, Fystack loads active policies for the workspace, compiles enabled rules, filters matches by action and wallet targeting, then resolves the result:
- If any matching rule has
effect: "DENY", the action is blocked. - Otherwise, if any matching rule has
effect: "ALLOW", the action proceeds and transaction approval is bypassed. - Otherwise, no policy rule matched. The action continues through the normal approval flow.
DENY always wins over ALLOW. Within the winning effect, the first match in declaration order is used for attribution, so the API can surface a stable rule_id and policy reason.
Evaluation is fail-closed. Cache, database, compile, or runtime evaluation failures deny the action. Each evaluation has a 100 ms timeout.
Scope and caching
Policies are scoped by workspace. Apex caches a compiled policy bundle per workspace and invalidates it after policy approvals, rejections, and wallet targeting changes. Wallet filtering happens during evaluation, so one workspace bundle can serve all wallets in that workspace.
Conditions
condition is an expression that must return a boolean. Reference fields by section and field name, for example principal.role, withdrawal.value_usd, or resource.method_selector.
Use normal boolean and comparison operators:
&& || ! == != > >= < <=
Examples:
- Name
- Deny large unwhitelisted withdrawals
- Type
- DENY
- Description
withdrawal.value_usd > 10000 && !withdrawal.is_whitelisted
- Name
- Bypass approval for small admin withdrawals
- Type
- ALLOW
- Description
principal.role == "admin" && withdrawal.value_usd <= 500
- Name
- Deny weekend high-value withdrawals
- Type
- DENY
- Description
(context.day_of_week == "Saturday" || context.day_of_week == "Sunday") && withdrawal.value_usd > 25000
- Name
- Deny high-gas ERC-20 transfers
- Type
- DENY
- Description
resource.method_selector == "0xa9059cbb" && resource.gas_limit > 100000
resource.decoded_args is dynamic. Guard dynamic fields with has(...)
before reading them, for example has(resource.decoded_args.to) && resource.decoded_args.to == "0x000000000000000000000000000000000000dead".
Action schema
The action schema defines the fields available inside a condition.
Common fields
Both supported actions expose:
- Name
- principal.user_email
- Type
- string
- Description
Email of the user initiating the action, when available.
- Name
- principal.role
- Type
- string
- Description
Workspace role. One of
owner,admin,signer,proposer,viewer, orguest.
- Name
- context.hour
- Type
- integer
- Description
UTC hour of evaluation,
0through23.
- Name
- context.day_of_week
- Type
- string
- Description
UTC weekday name, for example
MondayorSaturday.
withdrawal.create fields
- Name
- withdrawal.amount
- Type
- number
- Description
Requested asset amount.
- Name
- withdrawal.value_usd
- Type
- number
- Description
Requested withdrawal value in USD.
- Name
- withdrawal.asset_symbol
- Type
- string
- Description
Asset symbol.
- Name
- withdrawal.asset_is_native
- Type
- boolean
- Description
Whether the asset is the network native asset.
- Name
- withdrawal.destination_address
- Type
- string
- Description
Recipient address.
- Name
- withdrawal.network_code
- Type
- string
- Description
Internal network code.
- Name
- withdrawal.is_whitelisted
- Type
- boolean
- Description
Whether the destination address is present in the workspace address book.
- Name
- withdrawal.address_age_sec
- Type
- number
- Description
Age of the address book record in seconds. Use this to guard newly added recipients.
- Name
- wallet.outflow_1h / outflow_6h / outflow_24h
- Type
- number
- Description
Realized USD outflow for this wallet over the last 1, 6, or 24 hours.
- Name
- wallet.withdrawal_count_1h
- Type
- integer
- Description
Withdrawal attempts from this wallet in the last hour.
- Name
- workspace.outflow_1h / outflow_6h / outflow_24h
- Type
- number
- Description
Realized USD outflow for the workspace over the last 1, 6, or 24 hours.
- Name
- workspace.withdrawal_count_1h
- Type
- integer
- Description
Workspace withdrawal attempts in the last hour.
- Name
- workspace.network_outflow_1h / network_outflow_6h / network_outflow_24h
- Type
- number
- Description
Realized USD outflow for the current network over the last 1, 6, or 24 hours.
- Name
- workspace.network_withdrawal_count_1h
- Type
- integer
- Description
Workspace withdrawal attempts on the current network in the last hour.
Velocity fields count realized outflow only after a withdrawal has reached an executed or successful terminal state.
web3.contract_call fields
- Name
- resource.network_code
- Type
- string
- Description
Internal network code.
- Name
- resource.contract_address
- Type
- string
- Description
Lowercase contract address from the raw transaction
tofield.
- Name
- resource.method_selector
- Type
- string
- Description
Lowercase 4-byte selector, for example
0xa9059cbb.
- Name
- resource.method_name
- Type
- string
- Description
Method name from request metadata when provided.
- Name
- resource.value_wei
- Type
- string
- Description
Native value sent with the call, represented as a string.
- Name
- resource.gas_limit
- Type
- integer
- Description
Gas limit from the raw transaction.
- Name
- resource.decoded_args
- Type
- map
- Description
Decoded arguments when Fystack recognizes the call. ERC-20
transferexposestoandvalue.
Approval lifecycle
Create, update, and delete policy operations are approval-gated. The mutation creates a policy approval request and returns status: "pending_approval" with an approval_request_id. The policy change is applied only after the approval group reaches threshold.
Rule create, update, and delete operations follow the same model when the parent policy has an approval group. The pending revision stores field-level diffs for audit and review.
Updating wallet targeting through /wallets applies immediately after validation and cache invalidation. Use the policy update endpoint when you want wallet targeting changes reviewed as part of a larger policy revision.
Get the policy schema
Returns fields a condition may reference, grouped by trigger_action and
section. Use this endpoint for condition builders and client-side previews.
Request
curl -X GET https://api.fystack.io/api/v1/policy/schema \
-H "ACCESS-API-KEY: your_api_key" \
-H "ACCESS-TIMESTAMP: 1667836889" \
-H "ACCESS-SIGN: YourBase64EncodedSignature=="
Response
{
"success": true,
"message": "success",
"code": 0,
"data": {
"withdrawal.create": {
"principal": [
{ "name": "user_email", "type": "string" },
{ "name": "role", "type": "string", "enum": ["owner", "admin", "signer", "proposer", "viewer", "guest"] }
],
"withdrawal": [
{ "name": "amount", "type": "number" },
{ "name": "value_usd", "type": "number" },
{ "name": "asset_symbol", "type": "string" },
{ "name": "asset_is_native", "type": "boolean" },
{ "name": "destination_address", "type": "string" },
{ "name": "network_code", "type": "string" },
{ "name": "is_whitelisted", "type": "boolean" },
{ "name": "address_age_sec", "type": "number" }
],
"wallet": [
{ "name": "outflow_1h", "type": "number" },
{ "name": "outflow_6h", "type": "number" },
{ "name": "outflow_24h", "type": "number" },
{ "name": "withdrawal_count_1h", "type": "integer" }
],
"workspace": [
{ "name": "outflow_1h", "type": "number" },
{ "name": "outflow_6h", "type": "number" },
{ "name": "outflow_24h", "type": "number" },
{ "name": "withdrawal_count_1h", "type": "integer" },
{ "name": "network_outflow_1h", "type": "number" },
{ "name": "network_outflow_6h", "type": "number" },
{ "name": "network_outflow_24h", "type": "number" },
{ "name": "network_withdrawal_count_1h", "type": "integer" }
],
"context": [
{ "name": "hour", "type": "integer" },
{ "name": "day_of_week", "type": "string" }
]
},
"web3.contract_call": {
"principal": [
{ "name": "user_email", "type": "string" },
{ "name": "role", "type": "string", "enum": ["owner", "admin", "signer", "proposer", "viewer", "guest"] }
],
"resource": [
{ "name": "network_code", "type": "string" },
{ "name": "contract_address", "type": "string" },
{ "name": "method_selector", "type": "string" },
{ "name": "method_name", "type": "string" },
{ "name": "value_wei", "type": "string" },
{ "name": "gas_limit", "type": "integer" },
{ "name": "decoded_args", "type": "map" }
],
"context": [
{ "name": "hour", "type": "integer" },
{ "name": "day_of_week", "type": "string" }
]
}
}
}
Validate a rule
Compile-check a rule without saving it. Validation checks the trigger action, effect, boolean condition, and whether referenced fields exist for that action schema.
Request
curl -X POST https://api.fystack.io/api/v1/workspaces/{workspace_id}/policies/rules/validate \
-H "ACCESS-API-KEY: your_api_key" \
-H "ACCESS-TIMESTAMP: 1667836889" \
-H "ACCESS-SIGN: YourBase64EncodedSignature==" \
-H "Content-Type: application/json" \
-d '{
"trigger_action": "withdrawal.create",
"condition": "withdrawal.value_usd > 10000 && !withdrawal.is_whitelisted",
"effect": "DENY"
}'
Response
{
"success": true,
"message": "success",
"code": 0,
"data": { "valid": true }
}
Create a policy
Creates a pending policy revision and approval request. Inline rules are validated before the policy is created.
Body parameters
- Name
- name
- Type
- string
- Description
Required. 3 to 100 characters.
- Name
- description
- Type
- string
- Description
Optional. Up to 500 characters.
- Name
- approval_group_id
- Type
- string
- Description
Required UUID of the approval group that controls this policy.
- Name
- wallet_ids
- Type
- string[]
- Description
Optional. Up to 500 wallet UUIDs. Empty or omitted applies to all wallets.
- Name
- rules
- Type
- array
- Description
Optional. Up to 50 rules.
Request
curl -X POST https://api.fystack.io/api/v1/workspaces/{workspace_id}/policies \
-H "ACCESS-API-KEY: your_api_key" \
-H "ACCESS-TIMESTAMP: 1667836889" \
-H "ACCESS-SIGN: YourBase64EncodedSignature==" \
-H "Content-Type: application/json" \
-d '{
"name": "Large withdrawal guard",
"approval_group_id": "a1b2c3d4-e5f6-7890-1234-567890abcdef",
"wallet_ids": [],
"rules": [
{
"trigger_action": "withdrawal.create",
"condition": "withdrawal.value_usd > 10000 && !withdrawal.is_whitelisted",
"effect": "DENY",
"description": "Block large withdrawals to unlisted recipients"
}
]
}'
Response
{
"success": true,
"message": "success",
"code": 0,
"data": {
"status": "pending_approval",
"approval_request_id": "b7e2a1c4-9d8f-4a3b-8c1d-2e3f4a5b6c7d",
"policy": {
"id": "9c8b7a6d-5e4f-3210-9a8b-7c6d5e4f3210",
"name": "Large withdrawal guard",
"status": "pending",
"version": 1,
"wallet_ids": []
}
}
}
Manage policies
- Name
- GET /workspaces/{workspace_id}/policies
- Type
- list
- Description
List policies in a workspace. Supports
status,limit, andoffset.
- Name
- GET /workspaces/{workspace_id}/policies/{policy_id}
- Type
- read
- Description
Get one policy, including its approval group, rules, and latest pending approval request when one exists.
- Name
- PUT /workspaces/{workspace_id}/policies/{policy_id}
- Type
- update
- Description
Update policy metadata, approval group, wallet targeting, or replace the complete rule set. Omitted fields are unchanged.
rules: []clears all rules. Returnspending_approvalwhen gated by an approval group.
- Name
- DELETE /workspaces/{workspace_id}/policies/{policy_id}
- Type
- delete
- Description
Delete a policy. Returns
pending_approvalwhen the policy has an approval group; otherwise returnsdeleted.
- Name
- GET /workspaces/{workspace_id}/policies/{policy_id}/history
- Type
- audit
- Description
Return policy revisions, newest first, with field-level
changes.
Update a policy
curl -X PUT https://api.fystack.io/api/v1/workspaces/{workspace_id}/policies/{policy_id} \
-H "ACCESS-API-KEY: your_api_key" \
-H "ACCESS-TIMESTAMP: 1667836889" \
-H "ACCESS-SIGN: YourBase64EncodedSignature==" \
-H "Content-Type: application/json" \
-d '{
"description": "Updated guard for large outflows",
"wallet_ids": ["e0851607-417a-475a-b399-5f3aa262b81d"]
}'
{
"success": true,
"message": "success",
"code": 0,
"data": {
"status": "pending_approval",
"approval_request_id": "b7e2a1c4-9d8f-4a3b-8c1d-2e3f4a5b6c7d"
}
}
Wallet targeting
Wallet targeting controls where a policy applies. An empty wallet_ids
list means workspace-wide.
Use PUT /workspaces/{workspace_id}/policies/{policy_id}/wallets to
replace targeting immediately.
Request
curl -X PUT https://api.fystack.io/api/v1/workspaces/{workspace_id}/policies/{policy_id}/wallets \
-H "ACCESS-API-KEY: your_api_key" \
-H "ACCESS-TIMESTAMP: 1667836889" \
-H "ACCESS-SIGN: YourBase64EncodedSignature==" \
-H "Content-Type: application/json" \
-d '{ "wallet_ids": ["e0851607-417a-475a-b399-5f3aa262b81d"] }'
Manage rules
Rules can be managed independently after a policy exists.
- Name
- POST /workspaces/{workspace_id}/policies/{policy_id}/rules
- Type
- create
- Description
Add one rule. Required body:
trigger_action,condition, andeffect. Optional body:description,is_enabled.
- Name
- PUT /workspaces/{workspace_id}/policies/rules/{rule_id}
- Type
- update
- Description
Update
condition,effect,description, oris_enabled. The trigger action is immutable; delete and recreate the rule to change it.
- Name
- DELETE /workspaces/{workspace_id}/policies/rules/{rule_id}
- Type
- delete
- Description
Delete a rule from its parent policy.
Rule body
- Name
- trigger_action
- Type
- string
- Description
Required on create.
withdrawal.createorweb3.contract_call.
- Name
- condition
- Type
- string
- Description
Required on create. 1 to 4000 characters. Must compile to a boolean.
- Name
- effect
- Type
- string
- Description
Required on create.
ALLOWorDENY.
- Name
- description
- Type
- string
- Description
Optional. Up to 500 characters. Used as the decision message when the rule matches.
- Name
- is_enabled
- Type
- boolean
- Description
Optional. Defaults to
true. Disabled rules are ignored by the evaluator.
Create a rule
curl -X POST https://api.fystack.io/api/v1/workspaces/{workspace_id}/policies/{policy_id}/rules \
-H "ACCESS-API-KEY: your_api_key" \
-H "ACCESS-TIMESTAMP: 1667836889" \
-H "ACCESS-SIGN: YourBase64EncodedSignature==" \
-H "Content-Type: application/json" \
-d '{
"trigger_action": "web3.contract_call",
"condition": "resource.method_selector == \"0xa9059cbb\" && resource.gas_limit > 100000",
"effect": "DENY",
"description": "Block expensive ERC-20 transfers"
}'
{
"success": true,
"message": "success",
"code": 0,
"data": {
"status": "pending_approval",
"approval_request_id": "c9f42f0a-1d89-41d8-8d22-51ae7eb25b13"
}
}
Design patterns
Use DENY rules for hard controls: sanctions lists, unknown recipients, new address book entries, outflow velocity, or high-risk contract methods.
Use ALLOW rules narrowly. A matching ALLOW skips approval, so conditions should be precise: low USD value, trusted role, known wallet target, known network, and known recipient or contract.
Keep policy sets small and explicit. Broad workspace-wide policies are useful for baseline controls; wallet-scoped policies are better when a hot wallet, treasury wallet, or customer wallet has different risk thresholds.