Policies

Submit the owner's signature and persist the grant

Changed on

Recomputes the grant from the echoed prepare fields, verifies the signature recovers to the authenticated owner, re-checks the entity allocation inside the write, and stores the policy as pending. The install itself rides the first workflow operation on this wallet — nothing reaches the chain here.

This REPLACES the runner's previous grants. A wallet carries at most one usable grant: the send path resolves exactly one and refuses to execute when it finds more, so grants do not stack. Every other usable grant on this runner is revoked as part of this call, and the ones revoked come back in supersededPolicyIds. There is no flag to opt out, and clients do not need to revoke the previous grant first.

Replacement is scoped to the runner, not to a capability: submitting a grant for one capability revokes the runner's grant for any other.

The deferred batch that installs the new grant also uninstalls up to four prior validation entities (#717). The owner signs that batch once, at prepare. Replacing a grant does not leave the old entities installed until a separate uninstallValidation signature. DELETE of an already-applied grant is different: the controller cannot call uninstallValidation itself, so that response still carries onChainCleanup for the owner wallet.

When basePolicyId is sent, including an empty string, the runner's usable grant must still be that value or the call is 409 SESSION_POLICY_BASE_CHANGED. That code also covers a carried grant whose signed permissions no longer match a fresh merge: the remaining limit moved, the running set changed, or both. The grant id can be unchanged. Prepare again.

When a grant was carried, the stored grant is refused with 409 SESSION_POLICY_NOT_COVERING only when it covers less of an enabled task than the current grant did. Tasks listed in dropTaskIds are the ones this grant may leave uncovered. When there is no current grant, the stored grant must still cover every enabled task on this runner except those ids, or the call is 409 SESSION_POLICY_NOT_COVERING and nothing is stored. A missing prepare snapshot (restart, or the prepare was evicted) uses that same per-task check.

post/wallets/{address}/policies:submit

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.

policyIdstring required

ULID identifier (26-char Crockford base32).

entityIdinteger required
deadlineinteger required
validUntilinteger required

The ABSOLUTE expiry from prepare. It is baked into the signed calldata; recomputing it would change the digest.

agentLabelstring required
justificationstring
nativeRecipientsEthereumAddress[]
allowContractRecipientboolean
signaturestring required

The owner's 65-byte signature over the prepared digest.

basePolicyIdstring

Echo prepare's basePolicyId / changes.basePolicyId, including an empty string when prepare saw no usable grant. Omit only on a legacy submit that did not compare. A mismatch is 409 SESSION_POLICY_BASE_CHANGED.

dropTaskIdsstring[]

Enabled tasks this grant may leave uncovered. Echo prepare's affectedTaskIds. Naming a task does not fail submit. When a grant was carried, submit refuses with 409 SESSION_POLICY_NOT_COVERING only when the new grant covers less of an enabled task than the current grant did. A task the current grant already does not cover does not freeze the wallet. When there is no current grant, every other enabled task whose fund-moving steps are outside the grant is 409 SESSION_POLICY_NOT_COVERING, and a task whose target cannot be read is 409 SESSION_POLICY_TARGET_UNRESOLVED unless it is named here.

Example request

{
  "budget": {
    "erc20": [
      {
        "token": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB",
        "amount": "500000000"
      }
    ],
    "native": {
      "amount": "10000000000000000"
    },
    "removeActions": [
      {
        "target": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB"
      }
    ]
  },
  "chainId": 11155111,
  "policyId": "01JG2FE5MDVKBPHEG0PEYSDKAC",
  "allowedActions": [
    {
      "target": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB"
    }
  ],
  "erc20SpendCap": {
    "token": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB",
    "amount": "500000000"
  },
  "erc20SpendCaps": [
    {
      "token": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB",
      "amount": "500000000"
    }
  ],
  "nativeRecipients": [
    "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB"
  ],
  "delegatedNativeRecipients": [
    {
      "recipient": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB",
      "delegate": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB"
    }
  ],
  "nativeSpendCap": {
    "amount": "10000000000000000"
  }
}

Response

Grant stored; the gateway may now execute within it.

idstring required

ULID identifier (26-char Crockford base32).

runnerstring required

Lowercase or checksummed hex EOA / contract address.

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.

status'pending' | 'active' | 'revoked' required

pending = signed and stored, install not yet on-chain (revocable for free). active = install applied. revoked = grants nothing.

entityIdinteger required
sessionSignerstring required

Lowercase or checksummed hex EOA / contract address.

agentLabelstring required
justificationstring
sizing'workflow' | 'budget'
budgetStartedAtinteger
nativeRecipientsEthereumAddress[]
allowContractRecipientboolean
validUntilinteger required

Unix milliseconds.

createdAtinteger required

Unix milliseconds.

supersededPolicyIdsUlid[] required

Grants revoked to keep this runner's authority a singleton — the previous grants this submit replaced. Their status now reads revoked; the array distinguishes a replacement the gateway performed from one the user asked for via DELETE .../policies/{policyId}.

Empty on a first grant. Non-empty means the user's earlier permission is gone, which is worth reflecting in the UI.

affectedTaskIdsstring[]

Enabled tasks this grant leaves uncovered, which the request named in dropTaskIds. Empty when every enabled task on the runner is still covered.

Example response

{
  "id": "01JG2FE5MDVKBPHEG0PEYSDKAC",
  "runner": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB",
  "chainId": 11155111,
  "sessionSigner": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB",
  "budgetStartAmounts": [
    {
      "token": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB",
      "amount": "500000000"
    }
  ],
  "budgetStartNative": {
    "amount": "10000000000000000"
  },
  "allowedActions": [
    {
      "target": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB"
    }
  ],
  "erc20SpendCap": {
    "token": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB",
    "amount": "500000000"
  },
  "erc20SpendCaps": [
    {
      "token": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB",
      "amount": "500000000"
    }
  ],
  "nativeRecipients": [
    "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB"
  ],
  "delegatedNativeRecipients": [
    {
      "recipient": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB",
      "delegate": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB"
    }
  ],
  "nativeSpendCap": {
    "amount": "10000000000000000"
  },
  "onChainCleanup": {
    "target": "0x82F2Dd9a552a69f2ceD7Ff2D05c43aB8430158FB"
  },
  "supersededPolicyIds": [
    "01JG2FE5MDVKBPHEG0PEYSDKAC"
  ]
}

Changes

    • 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

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