walletId. In the Safe version, add it in every case, so that only the treasury wallet draws on the Safe’s daily caps.
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.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.guardrail-treasury-payouts.json
guardrail-treasury-signatures.json
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:
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.How payouts are approved
Both approval rules use the same two stages. Two people with thetreasury-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.
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.
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.Finish or withdraw a payout
A payout that needs approval does not sign. Para opens a case and rejects the signing request withPOLICY_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-Keyas the first attempt. - Through the server SDK, pass the case ID as
resumeApprovalCaseId.
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.
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 first.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.guardrail-treasury-safe-payouts.json
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.
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 withgasPrice 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.
How Safe payouts are approved and capped
Every payout needs two operators. A finance lead joins from 1 ETH or 10,000 USDC, becauseamountFact 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.
Executing a payout can count separately from its earlier co-signatures. See Safe spend for when the execution receives credit against a counted co-signature.
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 to the Safe policy first.Adjust the template
Change these parts of the policies to fit your treasury.Pay out on another network
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.Let small payouts sign without approval
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.Choose approvers by attribute
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.Require every eligible approver
Require every eligible approver
Set a stage’s
requiredApprovals to "all". Para counts the people eligible for that stage when the case reaches it.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 awalletId, the production policy needs the production wallet’s ID.