---
title: "Submit the owner's signature and persist the grant"
method: POST
path: "/wallets/{address}/policies:submit"
tags: ["Policies"]
---

# Submit the owner's signature and persist the grant

`POST /wallets/{address}/policies:submit`

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.

Off-chain only. The superseded grants' validation entities and ERC-20
spend caps stay installed on the account until the owner signs
`uninstallValidation` — replacing a grant does not reduce what the
account could authorize on chain, only what this gateway will use.

## Request body

- SubmitPolicyRequest
  - `chainId` integer, 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.
  - `policyId` string, required — ULID identifier (26-char Crockford base32).
  - `entityId` integer, required
  - `deadline` integer, required
  - `validUntil` integer, required — The ABSOLUTE expiry from prepare. It is baked into the signed calldata; recomputing it would change the digest.
  - `agentLabel` string, required
  - `justification` string
  - `allowedActions` AllowedAction[], required
    - `target` string, required — Lowercase or checksummed hex EOA / contract address.
    - `selectors` string[], required — 4-byte function selectors permitted on the target.
  - `erc20SpendCap` Erc20SpendCap, required — Cumulative ERC-20 spend cap, enforced on-chain at execution. The token must appear as an `allowedActions` target.
    - `token` string, required — Lowercase or checksummed hex EOA / contract address.
    - `amount` string, required — Total cap in the token's smallest unit (decimal string, no reset).
  - `signature` string, required — The owner's 65-byte signature over the prepared digest.

## Response `201`

Grant stored; the gateway may now execute within it.

- SubmitPolicyResponse
  - `id` string, required — ULID identifier (26-char Crockford base32).
  - `runner` string, required — Lowercase or checksummed hex EOA / contract address.
  - `chainId` integer, 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.
  - `entityId` integer, required
  - `sessionSigner` string, required — Lowercase or checksummed hex EOA / contract address.
  - `agentLabel` string, required
  - `justification` string
  - `allowedActions` AllowedAction[]
    - `target` string, required — Lowercase or checksummed hex EOA / contract address.
    - `selectors` string[], required — 4-byte function selectors permitted on the target.
  - `erc20SpendCap` Erc20SpendCap — Cumulative ERC-20 spend cap, enforced on-chain at execution. The token must appear as an `allowedActions` target.
    - `token` string, required — Lowercase or checksummed hex EOA / contract address.
    - `amount` string, required — Total cap in the token's smallest unit (decimal string, no reset).
  - `validUntil` integer, required — Unix milliseconds.
  - `createdAt` integer, required — Unix milliseconds.
  - `onChainCleanup` OnChainRevokeCleanup — Owner-executable call that clears an applied grant's validation entity and hooks from the runner. Production grants are policied: the gateway controller cannot self-uninstall (allowlist blocks uninstallValidation). The owner sends this as a plain transaction to `target` (or a UserOp validated by the owner fallback). Derived from the retained `Grant.InstallCall`, never from live permission structs.
    - `entityId` integer, required — Validation entity this cleanup removes.
    - `target` string, required — Lowercase or checksummed hex EOA / contract address.
    - `callData` string, required — Arbitrary-length hex-encoded byte string.
    - `chainId` integer, required — Chain the runner lives on.
  - `supersededPolicyIds` Ulid[], 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.

## Other responses

- `400` — Request validation failed.
- `401` — Missing or invalid bearer token.
- `403` — Authenticated but not permitted.
- `404` — Resource not found.
- `409` — The validation entity was taken by another grant while this one was being signed. Prepare again.

---

[API](https://skmtc.net/avaprotocol/apis/ava-protocol-avs-api.md) · [All operations](https://skmtc.net/avaprotocol/apis/ava-protocol-avs-api/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/avaprotocol/ava-protocol-avs-api/versions/231d563f2c9f/schema)
