> ## Documentation Index
> Fetch the complete documentation index at: https://docs.getpara.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Treasury approvals

> Deny every unplanned action from a treasury wallet, check Safe payouts by recipient, amount and asset, and send each payout through staged approval.

export const PermissionsCards = ({basePath, pages, cols = 2, horizontal = false}) => {
  const ImageCard = Card;
  const hrefOverrides = {
    "/v3/react/guides/permissions": {
      "conditional-approvals": "/v3/react/guides/conditional-approvals"
    },
    "/v3/rest/permissions": {
      "backend": "/v3/rest/permissions-administration"
    }
  };
  const guardrailBases = ["/v3/rest/permissions", "/v3/server/guides/permissions"];
  const ownership = guardrailBases.includes(basePath) ? 'guardrail' : 'request';
  const overviewImages = {
    "/v3/rest/permissions": {
      "imgUrl": "/images/v3/cards/rest-guardrails.png",
      "horizontalImgUrl": "/images/v3/cards/rest-guardrails-horizontal.png"
    },
    "/v3/server/guides/permissions": {
      "imgUrl": "/images/v3/cards/sdk-server-guardrail-signing.png",
      "horizontalImgUrl": "/images/v3/cards/sdk-server-guardrail-signing-horizontal.png"
    },
    "/v3/react/guides/permissions": {
      "imgUrl": "/images/v3/cards/sdk-get-policy-consent-url.png",
      "horizontalImgUrl": "/images/v3/cards/sdk-get-policy-consent-url-horizontal.png"
    },
    "/v3/react-native/guides/permissions": {
      "imgUrl": "/images/v3/cards/sdk-get-policy-consent-url.png",
      "horizontalImgUrl": "/images/v3/cards/sdk-get-policy-consent-url-horizontal.png"
    }
  };
  const registry = {
    overview: {
      title: "Permissions overview",
      description: "Return to the start of the Permissions path.",
      ...overviewImages[basePath]
    },
    "developer-portal": {
      title: "Create policies in the Developer Portal",
      description: "Build a policy with the Portal forms and activate it on a beta key.",
      imgUrl: "/images/v3/cards/developer-portal-policy-form.png",
      horizontalImgUrl: "/images/v3/cards/developer-portal-policy-form-horizontal.png"
    },
    "json": {
      title: "Write policy JSON",
      description: "Move to JSON for policies the forms cannot express.",
      imgUrl: "/images/v3/cards/developer-portal-policy-json.png",
      horizontalImgUrl: "/images/v3/cards/developer-portal-policy-json-horizontal.png"
    },
    "testing": {
      title: "Test your policies",
      description: "Exercise every rule on beta before you ship.",
      imgUrl: "/images/v3/cards/concept-policy-testing.png",
      horizontalImgUrl: "/images/v3/cards/concept-policy-testing-horizontal.png"
    },
    "production": {
      title: "Go to production",
      description: "Request a production review and publish your policy.",
      imgUrl: "/images/v3/cards/concept-policy-publication.png",
      horizontalImgUrl: "/images/v3/cards/concept-policy-publication-horizontal.png"
    },
    "conditional-approvals": {
      title: "Conditional approvals",
      description: "Require people to approve a request before it signs.",
      imgUrl: "/images/v3/cards/portal-conditional-approval-case.png",
      horizontalImgUrl: "/images/v3/cards/portal-conditional-approval-case-horizontal.png"
    },
    "approval-signatures": {
      title: "Collect approval signatures",
      description: "Collect signatures from approvers for multisig workflows.",
      imgUrl: "/images/v3/cards/sdk-get-approval-signatures.png",
      horizontalImgUrl: "/images/v3/cards/sdk-get-approval-signatures-horizontal.png"
    },
    "adjustable-parameters": {
      title: "Adjustable parameters",
      description: "Let users set their own limits and managers adjust approval settings.",
      imgUrl: "/images/v3/cards/portal-user-policy-limits.png",
      horizontalImgUrl: "/images/v3/cards/portal-user-policy-limits-horizontal.png"
    },
    "lifecycle": {
      title: "Manage the policy lifecycle",
      description: "Version, activate, replace and retire policies.",
      imgUrl: "/images/v3/cards/developer-portal-policy-versions.png",
      horizontalImgUrl: "/images/v3/cards/developer-portal-policy-versions-horizontal.png"
    },
    "backend": {
      title: "Manage permissions from your backend",
      description: "Assign approvers and read policy state with your server secret.",
      imgUrl: "/images/v3/cards/rest-permissions-administration.png",
      horizontalImgUrl: "/images/v3/cards/rest-permissions-administration-horizontal.png"
    },
    templates: ownership === "guardrail" ? {
      title: "Guardrail templates",
      description: "Start from a tested Guardrail policy.",
      imgUrl: "/images/v3/cards/concept-guardrail-templates.png",
      horizontalImgUrl: "/images/v3/cards/concept-guardrail-templates-horizontal.png"
    } : {
      title: "Request templates",
      description: "Start from a tested Request policy.",
      imgUrl: "/images/v3/cards/concept-request-templates.png",
      horizontalImgUrl: "/images/v3/cards/concept-request-templates-horizontal.png"
    },
    "guardrail:templates/spending-caps": {
      title: "Spending caps",
      description: "Cap native and token transfers per transaction or per day.",
      imgUrl: "/images/v3/cards/concept-spending-limits.png",
      horizontalImgUrl: "/images/v3/cards/concept-spending-limits-horizontal.png"
    },
    "guardrail:templates/allowlists": {
      title: "Recipient and contract allowlists",
      description: "Limit transfers and contract calls to addresses you know.",
      imgUrl: "/images/v3/cards/concept-recipient-allowlists.png",
      horizontalImgUrl: "/images/v3/cards/concept-recipient-allowlists-horizontal.png"
    },
    "guardrail:templates/approval-tiers": {
      title: "Approval thresholds and tiers",
      description: "Hold large transfers for approval, or deny the largest.",
      imgUrl: "/images/v3/cards/concept-approval-tiers.png",
      horizontalImgUrl: "/images/v3/cards/concept-approval-tiers-horizontal.png"
    },
    "guardrail:templates/treasury": {
      title: "Treasury approvals",
      description: "Deny unplanned actions and approve wallet or Safe payouts in stages.",
      imgUrl: "/images/v3/cards/concept-treasury-approvals.png",
      horizontalImgUrl: "/images/v3/cards/concept-treasury-approvals-horizontal.png"
    },
    "guardrail:templates/message-signing": {
      title: "Message and raw signing",
      description: "Block or hold raw signatures, and block permits.",
      imgUrl: "/images/v3/cards/concept-signing-actions.png",
      horizontalImgUrl: "/images/v3/cards/concept-signing-actions-horizontal.png"
    },
    "request:templates/allowances": {
      title: "Transfer allowances",
      description: "Let small transfers and a daily budget sign without a prompt.",
      imgUrl: "/images/v3/cards/concept-spending-limits.png",
      horizontalImgUrl: "/images/v3/cards/concept-spending-limits-horizontal.png"
    },
    "request:templates/recipients-and-contracts": {
      title: "Known recipients and app contracts",
      description: "Cover transfers to known addresses and your contract's calls.",
      imgUrl: "/images/v3/cards/concept-recipient-allowlists.png",
      horizontalImgUrl: "/images/v3/cards/concept-recipient-allowlists-horizontal.png"
    },
    "request:templates/cosigner-approval": {
      title: "Co-signer approval",
      description: "Send large transfers to a co-signer for approval.",
      imgUrl: "/images/v3/cards/portal-conditional-approval-case.png",
      horizontalImgUrl: "/images/v3/cards/portal-conditional-approval-case-horizontal.png"
    },
    "request:templates/adjustable-limits": {
      title: "User-adjustable limits",
      description: "Let each user choose their own spending limits.",
      imgUrl: "/images/v3/cards/portal-user-policy-limits.png",
      horizontalImgUrl: "/images/v3/cards/portal-user-policy-limits-horizontal.png"
    },
    "request:templates/message-signing": {
      title: "Message signing",
      description: "Sign messages from users' wallets without a prompt.",
      imgUrl: "/images/v3/cards/concept-signing-actions.png",
      horizontalImgUrl: "/images/v3/cards/concept-signing-actions-horizontal.png"
    }
  };
  const entryFor = slug => registry[slug] || registry[`${ownership}:${slug}`];
  const hrefFor = slug => {
    if (slug === 'overview') return basePath;
    const overrides = hrefOverrides[basePath] || ({});
    return overrides[slug] || `${basePath}/${slug}`;
  };
  return <CardGroup cols={cols}>
      {pages.map(slug => {
    const entry = entryFor(slug);
    return <ImageCard key={slug} title={entry.title} description={entry.description} href={hrefFor(slug)} imgUrl={horizontal ? entry.horizontalImgUrl : entry.imgUrl} horizontal={horizontal} />;
  })}
    </CardGroup>;
};

export const Card = ({imgUrl, compactImgUrl, title, description, href, horizontal = false, newTab = false}) => {
  const [isHovered, setIsHovered] = useState(false);
  const handleClick = e => {
    e.preventDefault();
    if (newTab) {
      window.open(href, '_blank', 'noopener,noreferrer');
    } else {
      window.location.href = href;
    }
  };
  return <div className={`not-prose relative my-2 p-[1px] rounded-xl transition-all duration-300 ${isHovered && href ? 'bg-gradient-to-r from-[#FF4E00] to-[#874AE3]' : 'bg-gray-200'}`} onMouseEnter={() => setIsHovered(true)} onMouseLeave={() => setIsHovered(false)}>
      <a href={href} onClick={href ? handleClick : undefined} className={`not-prose flex ${horizontal ? 'flex-row' : 'flex-col'} font-normal h-full bg-white overflow-hidden w-full ${href ? 'cursor-pointer' : 'cursor-default'} rounded-[11px] no-underline`}>
        {imgUrl && <div className={`relative overflow-hidden flex-shrink-0 ${horizontal ? 'w-[30%] rounded-l-[11px]' : 'w-full'}`} onClick={e => e.stopPropagation()}>
            <picture style={{
    display: 'contents'
  }}>
              {compactImgUrl && <source media="(max-width: 639px)" srcSet={compactImgUrl} />}
              <img src={imgUrl} alt={title} className="w-full h-full object-cover pointer-events-none select-none" draggable="false" />
            </picture>
            <div className="absolute inset-0 pointer-events-none" />
          </div>}
        <div className={`flex-grow px-6 py-5 ${horizontal ? 'w-[70%]' : 'w-full'} flex flex-col ${horizontal && imgUrl ? 'justify-center' : 'justify-start'}`}>
          {title && <h2 className="font-semibold text-base text-gray-800 m-0">{title}</h2>}
          {description && <div className={`font-normal text-gray-500 re leading-6 ${horizontal || !imgUrl ? 'mt-0' : 'mt-1'}`}>
              <p className="m-0 text-xs">{description}</p>
            </div>}
        </div>
      </a>
    </div>;
};

A treasury wallet should sign only the payouts you plan for, and only after the right people agree. This template does both for an app-owned wallet: it denies every action you do not intend to sign, pays out only ETH and USDC on Base, and sends each payout through two approval stages. If the funds sit in a Safe that the wallet co-owns, use the [Safe version](#pay-out-from-a-safe) instead.

Both policies cover every app-owned EVM wallet on every network. If your backend also signs with wallets that are not treasury wallets, add the treasury wallet's ID to each selector as `walletId`. In the [Safe version](#pay-out-from-a-safe), add it in every case, so that only the treasury wallet draws on the Safe's daily caps.

<Info>
  Para turns on conditional approvals per API key, so your beta and production keys are enabled separately. Until it does, validating or saving a policy with an approval requirement fails with `permissions_v2_extension_unavailable`.
</Info>

## Deny by default and approve payouts in stages

The template is two policies. The Developer Portal forms cannot express either one, so publish them as JSON.

```json guardrail-treasury-payouts.json theme={null}
{
  "id": "base-treasury-payouts",
  "schemaVersion": "permissions.policy.v1",
  "version": "1",
  "selector": {
    "walletType": "EVM"
  },
  "rules": [
    {
      "id": "deny-unplanned-actions",
      "effect": "deny",
      "actions": [
        "approve_tokens",
        "transfer_nfts",
        "call_contract",
        "deploy_contract",
        "execute_safe_transaction"
      ],
      "when": { "kind": "compare", "fact": "para.wallet.hasUserOwner", "op": "eq", "value": false }
    },
    {
      "id": "deny-eth-off-base",
      "effect": "deny",
      "actions": ["transfer_native"],
      "when": { "kind": "compare", "fact": "request.value.asset", "op": "ne", "value": "eip155:8453:native" }
    },
    {
      "id": "deny-other-tokens",
      "effect": "deny",
      "actions": ["transfer_tokens"],
      "when": {
        "kind": "any",
        "of": [
          { "kind": "compare", "fact": "request.classification", "op": "ne", "value": "erc20_transfer" },
          {
            "kind": "compare",
            "fact": "request.token.asset",
            "op": "ne",
            "value": "eip155:8453:token:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"
          }
        ]
      }
    },
    {
      "id": "approve-eth-payouts",
      "effect": "require_approval",
      "actions": ["transfer_native"],
      "when": { "kind": "compare", "fact": "request.classification", "op": "eq", "value": "native_transfer" },
      "approvalRequirement": {
        "id": "eth-payout-review",
        "evidence": "authenticated_approval",
        "stages": [
          { "id": "operators", "requiredApprovals": 2, "eligibility": [{ "kind": "role", "name": "treasury-operator" }] },
          { "id": "finance", "requiredApprovals": 1, "eligibility": [{ "kind": "role", "name": "finance-lead" }] }
        ]
      }
    },
    {
      "id": "approve-usdc-payouts",
      "effect": "require_approval",
      "actions": ["transfer_tokens"],
      "when": {
        "kind": "all",
        "of": [
          { "kind": "compare", "fact": "request.classification", "op": "eq", "value": "erc20_transfer" },
          {
            "kind": "compare",
            "fact": "request.token.asset",
            "op": "eq",
            "value": "eip155:8453:token:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"
          }
        ]
      },
      "approvalRequirement": {
        "id": "usdc-payout-review",
        "evidence": "authenticated_approval",
        "stages": [
          { "id": "operators", "requiredApprovals": 2, "eligibility": [{ "kind": "role", "name": "treasury-operator" }] },
          { "id": "finance", "requiredApprovals": 1, "eligibility": [{ "kind": "role", "name": "finance-lead" }] }
        ]
      }
    }
  ],
  "metadata": {
    "productMode": "guardrail",
    "description": "Treasury pays out ETH and USDC on Base only, after operator and finance approval"
  }
}
```

```json guardrail-treasury-signatures.json theme={null}
{
  "id": "evm-treasury-signatures",
  "schemaVersion": "permissions.policy.v1",
  "version": "1",
  "selector": {
    "walletType": "EVM"
  },
  "rules": [
    {
      "id": "deny-signatures",
      "effect": "deny",
      "actions": ["sign_message", "sign_typed_data", "sign_authorization", "raw_signature"],
      "when": { "kind": "compare", "fact": "para.wallet.hasUserOwner", "op": "eq", "value": false }
    }
  ],
  "metadata": {
    "productMode": "guardrail",
    "description": "Treasury wallets sign no messages, typed data, authorizations or raw signatures"
  }
}
```

Each rule has one job:

| Policy | Rule | Effect | Covers |
| - | - | - | - |
| `base-treasury-payouts` | `deny-unplanned-actions` | Deny | Token approvals, NFT transfers, contract calls, deployments and Safe transactions |
| `base-treasury-payouts` | `deny-eth-off-base` | Deny | ETH transfers on any network except Base |
| `base-treasury-payouts` | `deny-other-tokens` | Deny | Every token transfer except a direct USDC `transfer` on Base |
| `base-treasury-payouts` | `approve-eth-payouts` | Require approval | Every ETH transfer on Base |
| `base-treasury-payouts` | `approve-usdc-payouts` | Require approval | Every USDC transfer on Base |
| `evm-treasury-signatures` | `deny-signatures` | Deny | Personal messages, typed data, authorizations and raw signatures |

Every rule needs a condition, even one meant to match everything. A Guardrail applies only to app-owned wallets, where `para.wallet.hasUserOwner` is always `false`, so both deny-all rules match every request for the actions they list.

Neither policy names a `chainId`. The wallet has the same address on every EVM network, so a policy scoped to Base would leave its transfers on other networks to Para's defaults, which sign them. Instead, `deny-eth-off-base` and `deny-other-tokens` compare each transfer's asset, which names its network, with ETH and USDC on Base.

`deny-signatures` blocks these requests on every network, Base included. It needs no `chainId`, because many of them carry no network or one other than Base. A personal message has none, and typed data and authorizations signed through the server SDK arrive as `sign_message` with none. Through REST, typed data carries the chain ID from its domain and an authorization carries its own, where chain ID `0` makes it valid on every network. The rule also lists `raw_signature`, because Para cannot read what a raw signature signs, so no payout rule applies to one:

<Note>
  While Permissions is on, raw signatures follow the same defaults as other signing requests: an app-owned wallet signs them, and a user-owned wallet sends them to the user for review. Rules that name the `raw_signature` action, and rules with `"actions": "all"`, can change that. Raw signatures are `sign-raw` requests, messages from Solana, Stellar and Sui wallets, and REST `sign-message` requests from a Cosmos wallet that do not pass `walletType: "EVM"`. The SDKs sign messages from Cosmos wallets as EVM `sign_message` requests. Raw signatures carry no network a selector can name, so only a policy without `chainId` governs them. A rule that names `raw_signature` can read only `para.wallet.*` facts. The Developer Portal forms cannot create one, so write it in policy JSON, with **Edit JSON** in the Developer Portal or with the [CLI](/v3/cli/permissions).
</Note>

## How payouts are approved

Both approval rules use the same two stages. Two people with the `treasury-operator` role approve first, then one person with the `finance-lead` role. The second stage opens only after the first completes.

A person counts once per case. Someone who approved as an operator cannot also approve as finance lead, even with both roles, so every payout needs three different people. If any eligible approver rejects, the case ends and the payout does not sign.

| Signing request | Result |
| - | - |
| Send ETH on Base | Needs approval from two operators, then a finance lead |
| Transfer USDC on Base | Needs approval from two operators, then a finance lead |
| Send ETH on another network | Denied by `deny-eth-off-base` |
| Transfer another ERC-20 token, any token on another network, or USDC with `transferFrom` | Denied by `deny-other-tokens` |
| Approve a token spender or call a contract, on any network | Denied by `deny-unplanned-actions` |
| Sign a message, typed data or an authorization, through the server SDK or REST | Denied by `deny-signatures` |
| Sign a raw payload with REST `sign-raw` | Denied by `deny-signatures` |

The payout comes from your backend, so Para does not know which person asked for it. When a team member requests a payout, pass their Para user ID as `initiatorUserId` on the signing request. Para leaves that person out of every stage, so nobody approves a payout they asked for. The user must be linked to your app, or the request fails with `user is not linked to this app`.

Once the policies are active, your backend assigns `treasury-operator` and `finance-lead` to Para users in an authorization scope and binds the treasury wallet to that scope. Para accepts only roles that an active policy declares.

<Note>
  A later change to those assignments or to the binding that would invalidate open cases fails with `409 APPROVAL_CASES_AFFECTED`, which lists the `affectedCaseIds`. Send the change again with `force: true` to apply it and invalidate those cases.
</Note>

### Finish or withdraw a payout

A payout that needs approval does not sign. Para opens a case and rejects the signing request with `POLICY_REVIEW_REQUIRED`. The case ID is `approvalCaseId`, at the top level of the REST response body or in `error.data` from the server SDK. When the case is `completed`, send the payout again:

* Through REST, use the same `Idempotency-Key` as the first attempt.
* Through the server SDK, pass the case ID as `resumeApprovalCaseId`.

Name the same `initiatorUserId` in every attempt. Para matches the new attempt to the approved case even if you rebuilt the transaction with a new nonce or new gas values. Each completed case covers one signature.

To withdraw a payout that is still waiting, cancel its case from your backend with `cancelPartnerApprovalCase(caseId)` in the REST SDK, which calls `POST /v1/permissions-v2/approval-cases/{caseId}/cancel`. The case ends as `cancelled` and the payout never signs. The user you named as `initiatorUserId` can also cancel it from their own session with `cancelApprovalCase`.

<Check>
  Test it on your Development key: assign the roles to three test users and bind the wallet. Sign a small ETH payout on Base with an `Idempotency-Key`, approve it with two operators and the finance lead, and send it again with the same key to sign. Then try a token approval and a personal message, which are both denied. To test on Base Sepolia instead, apply [Pay out on another network](#adjust-the-template) first.
</Check>

## Pay out from a Safe

If the treasury holds its funds in a Safe on Base and the app-owned wallet is one of its owners, the wallet pays out by co-signing Safe transactions. Para reads the payment inside each one, so a Guardrail can check the recipient, the amount and the asset, cap daily spend, and route larger payouts to more approvers.

```json guardrail-treasury-safe-payouts.json theme={null}
{
  "id": "evm-treasury-safe-payouts",
  "schemaVersion": "permissions.policy.v1",
  "version": "1",
  "selector": {
    "walletType": "EVM"
  },
  "rules": [
    {
      "id": "deny-direct-actions",
      "effect": "deny",
      "actions": ["transfer_native", "transfer_tokens", "approve_tokens", "transfer_nfts", "call_contract", "deploy_contract"],
      "when": { "kind": "compare", "fact": "para.wallet.hasUserOwner", "op": "eq", "value": false }
    },
    {
      "id": "deny-outer-value",
      "effect": "deny",
      "actions": ["execute_safe_transaction"],
      "when": { "kind": "compare", "fact": "request.value.baseUnits", "op": "gt", "value": "0" }
    },
    {
      "id": "deny-unknown-recipients",
      "effect": "deny",
      "actions": ["execute_safe_transaction", "sign_typed_data"],
      "when": {
        "kind": "not",
        "of": {
          "kind": "compare",
          "fact": "request.safe.to",
          "op": "in",
          "value": [
            "0x1111111111111111111111111111111111111111",
            "0x2222222222222222222222222222222222222222"
          ]
        }
      }
    },
    {
      "id": "deny-eth-off-base",
      "effect": "deny",
      "actions": ["execute_safe_transaction", "sign_typed_data"],
      "when": { "kind": "compare", "fact": "request.safe.value.asset", "op": "ne", "value": "eip155:8453:native" }
    },
    {
      "id": "deny-other-tokens",
      "effect": "deny",
      "actions": ["execute_safe_transaction", "sign_typed_data"],
      "when": {
        "kind": "compare",
        "fact": "request.safe.token.asset",
        "op": "ne",
        "value": "eip155:8453:token:0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"
      }
    },
    {
      "id": "cap-daily-eth",
      "effect": "deny",
      "actions": ["execute_safe_transaction", "sign_typed_data"],
      "when": { "kind": "aggregate", "fact": "safe.spend.native", "op": "gt", "value": "5000000000000000000", "window": "24h" }
    },
    {
      "id": "cap-daily-usdc",
      "effect": "deny",
      "actions": ["execute_safe_transaction", "sign_typed_data"],
      "when": {
        "kind": "aggregate",
        "fact": "safe.spend.erc20",
        "op": "gt",
        "value": "50000000000",
        "window": "24h",
        "asset": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"
      }
    },
    {
      "id": "approve-eth-payouts",
      "effect": "require_approval",
      "actions": ["execute_safe_transaction", "sign_typed_data"],
      "when": { "kind": "compare", "fact": "request.safe.value.baseUnits", "op": "gt", "value": "0" },
      "approvalRequirement": {
        "id": "safe-eth-payout-review",
        "evidence": "authenticated_approval",
        "amountFact": "request.safe.value.baseUnits",
        "stages": [
          { "id": "operators", "requiredApprovals": 2, "eligibility": [{ "kind": "role", "name": "treasury-operator" }] },
          {
            "id": "finance",
            "requiredApprovals": 1,
            "eligibility": [{ "kind": "role", "name": "finance-lead" }],
            "minimumAmount": "1000000000000000000"
          }
        ]
      }
    },
    {
      "id": "approve-usdc-payouts",
      "effect": "require_approval",
      "actions": ["execute_safe_transaction", "sign_typed_data"],
      "when": { "kind": "compare", "fact": "request.safe.token.value.baseUnits", "op": "gt", "value": "0" },
      "approvalRequirement": {
        "id": "safe-usdc-payout-review",
        "evidence": "authenticated_approval",
        "amountFact": "request.safe.token.value.baseUnits",
        "stages": [
          { "id": "operators", "requiredApprovals": 2, "eligibility": [{ "kind": "role", "name": "treasury-operator" }] },
          {
            "id": "finance",
            "requiredApprovals": 1,
            "eligibility": [{ "kind": "role", "name": "finance-lead" }],
            "minimumAmount": "10000000000"
          }
        ],
        "safeKnobs": [
          { "id": "usdc-finance-threshold", "kind": "minimum_amount", "stageId": "finance", "min": "1000000000", "max": "25000000000" }
        ]
      }
    }
  ],
  "metadata": {
    "productMode": "guardrail",
    "description": "Pay known vendors in ETH or USDC on Base from a Safe, with daily caps and staged approval"
  }
}
```

Use this policy in place of `base-treasury-payouts`, and remove `sign_typed_data` from the `actions` of `deny-signatures`. Otherwise `deny-signatures` blocks every Safe co-signature, because a deny outranks an approval.

| Rule | Effect | Covers |
| - | - | - |
| `deny-direct-actions` | Deny | Transfers, approvals, contract calls and deployments from the wallet itself, including an on-chain `approveHash` of a Safe transaction |
| `deny-outer-value` | Deny | An `execTransaction` call that attaches ETH from the owner wallet |
| `deny-unknown-recipients` | Deny | A Safe payment to anyone outside the vendor list, and every other Safe transaction or typed data request |
| `deny-eth-off-base` | Deny | An ETH payment on any network except Base |
| `deny-other-tokens` | Deny | A token payment in anything except USDC on Base |
| `cap-daily-eth` | Deny | ETH payments past 5 ETH per day from the Safe |
| `cap-daily-usdc` | Deny | USDC payments past 50,000 per day from the Safe |
| `approve-eth-payouts` | Require approval | Every ETH payment, with a finance lead added from 1 ETH |
| `approve-usdc-payouts` | Require approval | Every USDC payment, with a finance lead added from 10,000 USDC |

The policy has no `chainId`. A Safe co-signature carries the network from its domain, so leaving it out keeps payouts on other networks inside the policy, where `deny-eth-off-base` and `deny-other-tokens` stop them.

### Which Safe transactions are payouts

Para reads a Safe transaction as a payment only when it is a Call with `gasPrice` `0`, its inner call does not target the Safe itself, and that call either sends ETH with no calldata or makes one ERC-20 `transfer` or `transferFrom` without ETH. Everything else is opaque, including DelegateCalls, MultiSend batches, gas refunds, and owner or threshold changes. An opaque transaction has no recipient, so `deny-unknown-recipients` cannot evaluate and Para denies it. The same applies to typed data that is not a `SafeTx`, such as a `Permit`, which this treasury never needs to sign.

Each approval, network and token rule reads one payment family, ETH or token. A rule that requires both would fail on every payment, because Para treats the other family as missing. Para compares addresses without regard to case, so you can list vendors in lowercase or checksummed form.

<Warning>
  The wallet must co-sign through REST `POST /v1/wallets/{walletId}/sign-typed-data`, or execute through `sign-transaction` or the server SDK's `signTransaction`. A `SafeTx` co-signed through the server SDK has no Safe facts, and `deny-signatures` blocks it.

  The Para client and server SDKs, including their viem, ethers and wagmi integrations, sign EIP-712 typed data and EIP-7702 authorizations as a message over their hash, so Para evaluates those requests as `sign_message`. The `sign_typed_data` and `sign_authorization` actions, and the facts Para reads from typed data and authorizations, such as `request.typedData.primaryType`, apply only to the REST API's `sign-typed-data` and `sign-authorization` routes, which the REST SDK calls.
</Warning>

### How Safe payouts are approved and capped

Every payout needs two operators. A finance lead joins from 1 ETH or 10,000 USDC, because `amountFact` compares the payment amount with each stage's `minimumAmount`. The `safeKnobs` entry lets your backend move the USDC threshold for the finance stage between 1,000 and 25,000 USDC without publishing a new policy version. Without `max`, your backend could only lower it.

Approval covers one signing request. If the wallet co-signs a payout and later executes it, the execution opens a second case, so let another owner or a relayer execute payouts the wallet co-signed.

The daily caps count co-signatures of the same Safe transaction hash once per Safe, network and asset, however many of your wallets sign it. A day starts at 00:00 UTC. An ERC-20 `transferFrom` that pulls tokens from another address does not count, and Safe payments do not count toward the wallet's own spending limits. A payment counts once Para signs it, even if the Safe never executes it, and a payment made without one of your wallets signing through Para does not count. To see how much your wallets have co-signed or executed from the Safe today, read the policy's usage with the Safe's address as `safeAddress`; without it, the Safe meters are left out.

<Note>
  Executing a payout can count separately from its earlier co-signatures. See [Safe spend](/v3/references/permissions/facts#safe-spend) for when the execution receives credit against a counted co-signature.
</Note>

<Warning>
  Para does not check which wallets own the Safe. Every wallet the policy covers adds to the daily caps when it signs a payment from the Safe, even if it is not an owner. The caps also share their budget with every other policy on your API key that limits the same Safe, network and asset over a window of the same length. Add the treasury wallet's ID to the selector as `walletId` so that only its payments count.
</Warning>

| Signing request | Result |
| - | - |
| Co-sign a 500 USDC payment to a listed vendor | Needs approval from two operators |
| Co-sign a 20,000 USDC payment to a listed vendor | Needs approval from two operators, then a finance lead |
| Co-sign a payment to an address outside the vendor list | Denied by `deny-unknown-recipients` |
| Co-sign a USDC payment that would take the day's total past 50,000 | Denied by `cap-daily-usdc` |
| Co-sign a MultiSend batch or a DelegateCall | Denied, because Para cannot read the payment |
| Execute a payment and attach ETH from the owner wallet | Denied by `deny-outer-value` |
| Send USDC straight from the owner wallet | Denied by `deny-direct-actions` |
| Co-sign a `SafeTx` through the server SDK | Denied by `deny-signatures` |

<Check>
  Test it on your Development key with a test Safe that lists the treasury wallet as an owner. Through REST, co-sign a 500 USDC payment on Base to a listed vendor with an `Idempotency-Key`, approve it with two operators, and send it again with the same key to sign. Then co-sign a payment to an unlisted address, which is denied. To test on Base Sepolia instead, apply [Pay out on another network](#adjust-the-template) to the Safe policy first.
</Check>

## Adjust the template

Change these parts of the policies to fit your treasury.

<AccordionGroup>
  <Accordion title="Pay out on another network">
    Replace `8453` in each asset value with the network's chain ID, and the USDC address with the token's address on that network. On Base Sepolia, for example, use `eip155:84532:native` for ETH and `0x036cbd53842c5426634e7929541ec2318f3dcf7e` for USDC. Payouts on Base are then denied, because each policy pays out on one network.
  </Accordion>

  <Accordion title="Let small payouts sign without approval">
    Add an amount condition to each approval rule, such as `request.value.baseUnits` `gt` `"100000000000000000"` (0.1 ETH) for ETH or `request.token.value.baseUnits` `gt` `"100000000"` (100 USDC) for USDC. Payouts at or below the amount then sign without a case. In `approve-eth-payouts`, wrap the existing condition and the new one in an `all` group; in `approve-usdc-payouts`, add the new condition to the existing `all` group. In the Safe policy, raise the `gt` value of `approve-eth-payouts` or `approve-usdc-payouts` instead.
  </Accordion>

  <Accordion title="Choose approvers by attribute">
    Replace a role clause with an attribute clause, such as `{ "kind": "attribute", "name": "department", "operator": "eq", "value": "finance" }`. Your backend assigns attributes the same way it assigns roles.
  </Accordion>

  <Accordion title="Require every eligible approver">
    Set a stage's `requiredApprovals` to `"all"`. Para counts the people eligible for that stage when the case reaches it.
  </Accordion>
</AccordionGroup>

## Publish your treasury policy

Roles and wallet bindings live in each environment. Once payouts behave as expected on beta, request a production review, then assign the production roles and bind the production treasury wallet. If you added a `walletId`, the production policy needs the production wallet's ID.

<CardGroup cols={2}>
  <Card title="Approval requirements reference" description="Stages, requesters, adjustable settings and the limits on each." href="/v3/references/permissions/approval-requirements" imgUrl="/images/v3/cards/concept-approval-requirements.png" />

  <Card title="Safe payment facts" description="When Para can read a Safe payment, and how Safe spend is counted." href="/v3/references/permissions/facts#safe-payments" imgUrl="/images/v3/cards/concept-safe-payments.png" />
</CardGroup>

<PermissionsCards basePath="/v3/rest/permissions" pages={["conditional-approvals"]} cols={1} horizontal />

<PermissionsCards basePath="/v3/rest/permissions" pages={["backend", "production"]} cols={2} />


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.