Policies

Prepare a legacy typed grant or a stored unsigned budget operation

Changed on

With budget or approvalDiscovery, the experimental off-by-default wallet_budget_preparation_enabled path stores a priced unsigned owner operation and the exact intent/read/derived review. It requires a fresh signed approvalDiscovery, explicitly self-funds, and returns signing.kind owner_operation (personal_sign of raw userOpHash). It creates no usable policy or hold. Budget submit needs the separate off-by-default wallet_budget_signing_enabled flag and is limited to Sepolia. GET /wallets/{address}/policies/preparations/{id} recovers the same review. Missing/stale discovery returns 409 BUDGET_DISCOVERY_REQUIRED; incomplete discovery or failed reads return 503; 64 live unsigned records per owner returns 429. Only tested token deployments may be reset automatically: v1 Sepolia USDC. Other live allowances requiring reset return 409 BUDGET_RESET_UNSUPPORTED_TOKEN with token/spender; insufficient ETH returns 409 BUDGET_FEE_INSUFFICIENT_ETH with neededWei/availableWei. A quote above wallet_budget_max_fee_wei (default 0.005 ETH) returns 409 BUDGET_FEE_CEILING_EXCEEDED. Unsupported Permit2, NFT/operator or unaudited spending targets return 409 BUDGET_ROUTE_UNSUPPORTED. A live unknown validation authority returns BUDGET_AUTHORITY_UNKNOWN; remove it in the wallet and check again. Ava's session operations are refused with BUDGET_CHANGING while the wallet/chain hold is open; scheduled runs wait without consuming an execution. A known, unexpired legacy deferred signature returns 409 BUDGET_LEGACY_GRANT_PENDING with its latest outstanding deadline. The default legacy signing window lasts up to 30 days. Revoking a permission off chain does not invalidate its signature; migration must wait until that window closes. This route offers no nonce-cancellation remedy and an owner nonce change does not cancel a session-entity nonce.

Legacy path (no budget/discovery, non-budget wallet): Allocates the policy id, validation entity, and session signer, builds the exact installValidation calldata the signature will commit to, and returns the typed data for eth_signTypedData_v4. Stores nothing: a prepare that is never submitted leaves no state behind. The echoed fields must be passed back verbatim to :submit — the gateway recomputes everything from them, so tampering only produces a signature that no longer verifies.

When add is set and the runner has a usable grant, the merged grant starts from that grant. Actions, native recipients, and allowContractRecipient only grow. delegatedNativeRecipients is carried per recipient; when both grants name a delegate for the same recipient, the gateway records the addition's delegate. The owner signs the install call; that call's ETH row is not limited to the delegate. The acknowledgement is gateway state, checked again when the gateway sends. Each token cap is what is left on that grant (the on-chain remainder once the grant is applied, otherwise the stored cap), or what enabled automations still need when that is higher, plus the addition. A token with nothing left is omitted. A router action stays. A task whose target cannot be read does not fail prepare and does not add that unknown target.

When add is set and there is no usable grant, the merged grant is the addition plus every enabled task except ids in dropTaskIds. Acknowledgements for those running recipients come from the wallet's last stored grant, including one that was revoked. The chain read still applies, and this addition's delegate wins when both name one. A task whose token or recipient cannot be read from the stored workflow (a loop over a previous step) is 409 SESSION_POLICY_TARGET_UNRESOLVED. That is not an unsized amount. Pause the automation, or name its id in dropTaskIds. An unsized token amount on this path is 400 POLICIES_BAD_PERMISSIONS. Installing a native spend cap while a running payable cannot be sized is 409 SESSION_POLICY_NATIVE_UNSIZED, including when the runner has no usable grant. The addition or a sized running payable can be what installs that cap.

post/wallets/{address}/policies:prepare

Request

  • Base URL
    https://api.avaprotocol.org/api/v1
  • Auth
    HTTP bearer

Request body

chainIdinteger required

Numeric chain ID (e.g. 11155111 for Sepolia, 8453 for Base). On chain-aware trigger/node configs this is required and must be a configured chain; on query/filter params it is optional.

agentLabelstring required
justificationstring
nativeRecipientsEthereumAddress[]

EOAs this grant may send native ETH to (empty-calldata execute). Omit for Uniswap/ERC-20-only and for payable-write-only (nativeValueCap). A present empty array is 400. Max 20 (MaxNativeRecipients; a 20-row replace with four teardowns mines, measured on Sepolia).

allowContractRecipientboolean

When true, every native recipient may be a contract (any function on that address, ERC-20 uncapped). Default false: each recipient must have empty code, or be an acknowledged EIP-7702 delegation (delegatedNativeRecipients). Logged when true. An acknowledgement is not this flag.

expiresInSecondsinteger

Grant lifetime, relative (skew-proof). Becomes an absolute validUntil. When add is set, this is the new automation's horizon if add.validUntil is omitted. The signed expiry can be later, because the wallet keeps one expiry and it must cover every enabled automation. Values above 9223372036 overflow the duration conversion and are rejected.

basePolicyIdstring

Usable grant this prepare was merged against. Omit on a legacy prepare that sends the full permission set and does not compare. Send an empty string when the client believes there is no usable grant. A mismatch is 409 SESSION_POLICY_BASE_CHANGED.

dropTaskIdsstring[]

Enabled tasks this prepare may leave out of the merged grant. Honored only when add is set. Echo the same ids on submit. When the runner has a usable grant, a task whose target cannot be read does not fail prepare and does not add that target. When there is no usable grant, that task is 409 SESSION_POLICY_TARGET_UNRESOLVED until it is paused or named here.

Example request

{
  "approvalDiscovery": {
    "owner": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB",
    "wallet": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB",
    "chainId": 11155111,
    "candidates": [
      {
        "token": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB",
        "spender": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB"
      }
    ],
    "allowedContracts": [
      "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB"
    ]
  },
  "budget": {
    "erc20": [
      {
        "token": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB",
        "amount": "500000000"
      }
    ],
    "native": {
      "amount": "10000000000000000"
    },
    "removeActions": [
      {
        "target": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB"
      }
    ]
  },
  "chainId": 11155111,
  "allowedActions": [
    {
      "target": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB"
    }
  ],
  "erc20SpendCap": {
    "token": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB",
    "amount": "500000000"
  },
  "erc20SpendCaps": [
    {
      "token": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB",
      "amount": "500000000"
    }
  ],
  "nativeRecipients": [
    "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB"
  ],
  "delegatedNativeRecipients": [
    {
      "recipient": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB",
      "delegate": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB"
    }
  ],
  "nativeSpendCap": {
    "amount": "10000000000000000"
  },
  "add": {
    "allowedActions": [
      {
        "target": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB"
      }
    ],
    "erc20SpendCaps": [
      {
        "token": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB",
        "amount": "500000000"
      }
    ],
    "nativeRecipients": [
      "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB"
    ],
    "delegatedNativeRecipients": [
      {
        "recipient": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB",
        "delegate": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB"
      }
    ],
    "nativeSpendCap": {
      "amount": "10000000000000000"
    }
  }
}

Response

Payload to sign, plus the allocations submit must echo.

state'prepared' | 'submitting' | 'submitted' | 'active' | 'reverted' | 'rejected' | 'unsent' | 'superseded'

prepared expires after ten minutes; that window limits only the first relay. submitting, submitted and rejected remain recoverable without expiry and keep Ava runs paused. A bundler rejection is not final. active, reverted and superseded are settled. unsent is possible only before a submitting marker. Successful inclusion stays submitted until reset verification passes; activation and hold release are atomic.

reasonstring
transactionHashstring

Arbitrary-length hex-encoded byte string.

inclusionBlockinteger
finalReadBlockinteger
isCancelboolean

A successful cancel settles as active with isCancel true; it consumes the nonce and does not activate a new permission.

preparationIdstring

ULID identifier (26-char Crockford base32).

policyIdstring required

ULID identifier (26-char Crockford base32).

chainIdinteger required

Numeric chain ID (e.g. 11155111 for Sepolia, 8453 for Base). On chain-aware trigger/node configs this is required and must be a configured chain; on query/filter params it is optional.

entityIdinteger required

The validation entity allocated for this grant (provisional until submit).

sessionSignerstring required

Lowercase or checksummed hex EOA / contract address.

deadlineinteger required

Unix seconds; bounds signing → first use, NOT the grant lifetime.

validUntilinteger required

Absolute grant expiry, unix milliseconds. Echo verbatim to submit.

digeststring

The EIP-712 hash the typed data produces, for client-side verification.

typedDataobject

The exact eth_signTypedData_v4 payload for the owner's wallet.

basePolicyIdstring

Set when prepare merged an add. Empty means there was no usable grant. Echo changes.basePolicyId, including the empty string.

affectedTaskIdsstring[]

Enabled tasks this prepare left out because dropTaskIds named them. Echo those ids as submit's dropTaskIds.

nativeRecipientsEthereumAddress[]
allowContractRecipientboolean

Example response

{
  "preparationId": "01JG2FE5MDVKBPHEG0PEYSDKAC",
  "signing": {
    "preparationId": "01JG2FE5MDVKBPHEG0PEYSDKAC",
    "userOp": {
      "sender": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB",
      "factory": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB",
      "paymaster": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB"
    },
    "userOpHash": "0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
    "entryPoint": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB"
  },
  "budget": {
    "chainId": 11155111,
    "policyId": "01JG2FE5MDVKBPHEG0PEYSDKAC",
    "erc20": [
      {
        "token": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB"
      }
    ],
    "unmeteredPermissions": [
      {
        "target": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB",
        "token": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB"
      }
    ],
    "retirements": [
      {
        "policyId": "01JG2FE5MDVKBPHEG0PEYSDKAC",
        "erc20": [
          {
            "token": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB",
            "amount": "500000000"
          }
        ],
        "native": {
          "amount": "10000000000000000"
        }
      }
    ],
    "liveApprovals": [
      {
        "token": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB",
        "spender": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB"
      }
    ],
    "pendingChange": {
      "preparationId": "01JG2FE5MDVKBPHEG0PEYSDKAC",
      "policyId": "01JG2FE5MDVKBPHEG0PEYSDKAC"
    }
  },
  "approvalResets": [
    {
      "token": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB",
      "spender": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB"
    }
  ],
  "unmeteredPermissions": [
    {
      "target": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB",
      "token": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB"
    }
  ],
  "policyId": "01JG2FE5MDVKBPHEG0PEYSDKAC",
  "chainId": 11155111,
  "sessionSigner": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB",
  "changes": {
    "keptActions": [
      {
        "target": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB"
      }
    ],
    "addedActions": [
      {
        "target": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB"
      }
    ],
    "removedActions": [
      {
        "target": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB"
      }
    ],
    "capChanges": [
      {
        "token": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB"
      }
    ]
  },
  "allowedActions": [
    {
      "target": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB"
    }
  ],
  "erc20SpendCap": {
    "token": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB",
    "amount": "500000000"
  },
  "erc20SpendCaps": [
    {
      "token": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB",
      "amount": "500000000"
    }
  ],
  "nativeRecipients": [
    "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB"
  ],
  "delegatedNativeRecipients": [
    {
      "recipient": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB",
      "delegate": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB"
    }
  ],
  "nativeSpendCap": {
    "amount": "10000000000000000"
  }
}

Changes

    • breaking

      the response property became optional for the status

    • breaking

      the response property became optional for the status

    • info

      added the new optional request property

    • info

      added the new optional request property

    • info

      the request property became optional

    • info

      added the non-success response with the status

    • info

      added the non-success response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the new optional request property /

    • info

      added the new optional request property

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status

    • info

      added the optional property to the response with the status