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 DENY blocks the request. A matching ALLOW bypasses 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 DENY records the activity as denied_by_policy. A matching ALLOW skips 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, optional description, and optional is_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:

  1. If any matching rule has effect: "DENY", the action is blocked.
  2. Otherwise, if any matching rule has effect: "ALLOW", the action proceeds and transaction approval is bypassed.
  3. 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, or guest.

  • Name
    context.hour
    Type
    integer
    Description

    UTC hour of evaluation, 0 through 23.

  • Name
    context.day_of_week
    Type
    string
    Description

    UTC weekday name, for example Monday or Saturday.

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 to field.

  • 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 transfer exposes to and value.


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/policy/schema

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

GET
/policy/schema
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" }
      ]
    }
  }
}

POST/workspaces/{workspace_id}/policies/rules/validate

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

POST
/workspaces/{workspace_id}/policies/rules/validate
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 }
}

POST/workspaces/{workspace_id}/policies

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

POST
/workspaces/{workspace_id}/policies
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, and offset.

  • 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. Returns pending_approval when gated by an approval group.

  • Name
    DELETE /workspaces/{workspace_id}/policies/{policy_id}
    Type
    delete
    Description

    Delete a policy. Returns pending_approval when the policy has an approval group; otherwise returns deleted.

  • 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"
  }
}

GET/workspaces/{workspace_id}/policies/{policy_id}/wallets

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

PUT
/workspaces/{workspace_id}/policies/{policy_id}/wallets
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, and effect. Optional body: description, is_enabled.

  • Name
    PUT /workspaces/{workspace_id}/policies/rules/{rule_id}
    Type
    update
    Description

    Update condition, effect, description, or is_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.create or web3.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. ALLOW or DENY.

  • 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.