{
  "$id": "https://glide.co/schemas/agent-banking/draft/agent-policy-envelope.json",
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "title": "AgentPolicyEnvelope",
  "description": "The 13-axis policy envelope evaluated by @glideco/policy-engine on every agent tool call. Source-of-truth evaluated in three places: (1) router @glideco/policy-engine pre-check, (2) on-chain multisig allowlist (derived), (3) Privy programmable signing policy (serialized). All three layers must agree or triple-enforcement is aspirational. Money-safety: ALL caps are minor-units integers; an absent cap field means 'no limit on this axis' but the engine ALWAYS evaluates step-up + counterparty + chain + geo + MCC + velocity gates regardless. The set of axes is closed at v1; new axes are additive minor releases.",
  "channel": "v1",
  "examples": [
    {
      "policy_id": "11111111-1111-4111-8111-111111111111",
      "vault_id": "22222222-2222-4222-8222-222222222222",
      "policy_version": 1,
      "amount_cap_cents_per_tx": 50000,
      "amount_cap_cents_per_day": 200000,
      "step_up_amount_cents": 100000,
      "counterparty_allowlist": [
        {
          "address": "0x71C7656EC7ab88b098defB751B7401B5f6d8976F",
          "chain": "base",
          "token": "USDC"
        }
      ],
      "velocity_max_txs_per_hour": 5,
      "velocity_max_txs_per_day": 20,
      "chain_allowlist": ["base", "sol"],
      "geo_allowlist": ["US", "GB"],
      "mcc_allowlist": [],
      "mcc_blocklist": ["7995"],
      "created_at": "2026-04-25T00:00:00.000Z",
      "updated_at": "2026-04-25T00:00:00.000Z"
    },
    {
      "policy_id": "ffffffff-ffff-ffff-ffff-ffffffffffff",
      "vault_id": "22222222-2222-4222-8222-222222222222",
      "policy_version": 7,
      "counterparty_allowlist": [],
      "chain_allowlist": ["sol"],
      "geo_allowlist": [],
      "mcc_allowlist": [],
      "mcc_blocklist": [],
      "created_at": "2026-04-25T00:00:00.000Z",
      "updated_at": "2026-04-25T01:00:00.000Z"
    }
  ],
  "schema": {
    "$schema": "https://json-schema.org/draft/2020-12/schema",
    "type": "object",
    "properties": {
      "policy_id": {
        "$ref": "_types.json#/$defs/uuidV4",
        "description": "Stable identifier for this policy across version bumps. (vault_id, policy_id) is unique; (vault_id, policy_id, policy_version) is the immutable revision."
      },
      "vault_id": {
        "$ref": "_types.json#/$defs/uuidV4",
        "description": "Vault this policy gates. Cross-vault policies are NOT supported at v1 — every vault has its own envelope."
      },
      "policy_version": {
        "$ref": "_types.json#/$defs/nonNegativeInt",
        "description": "Monotonic counter bumped on any mutation to this policy. The router caches keyed by (vault_id, policy_version); a mismatch between the grant's pinned version and the current envelope version triggers one retry then denies (PolicyStaleError)."
      },
      "amount_cap_cents_per_tx": {
        "$ref": "_types.json#/$defs/amountCents",
        "description": "Per-transaction hard cap in minor units (USD cents). Absent = no per-tx cap; the engine still evaluates other gates. Setting this to 0 effectively blocks all amount > 0; set policy_status = revoked at the row level for full disable."
      },
      "amount_cap_cents_per_day": {
        "$ref": "_types.json#/$defs/amountCents",
        "description": "Rolling 24-hour-window cap in minor units (USD cents). Window is sliding from now-24h to now (UTC), not calendar-day. Aggregates ALL outflows on this vault across rails."
      },
      "amount_cap_cents_lifetime": {
        "$ref": "_types.json#/$defs/amountCents",
        "description": "Lifetime cap (cumulative outflow under this policy) in minor units (USD cents). Reset on policy_id change, NOT on policy_version bump."
      },
      "step_up_amount_cents": {
        "$ref": "_types.json#/$defs/amountCents",
        "description": "Threshold above which the verdict flips from `allow` to `allow_with_step_up`. Compared against the proposed tx amount_cents. Step-up auth = fresh Privy WebAuthn / passkey; one step-up satisfies one tx, never cached. Note: step_up_amount_cents > amount_cap_cents_per_tx renders step-up unreachable (the per-tx cap denies first); the policy engine warns at write time but does not reject."
      },
      "counterparty_allowlist": {
        "type": "array",
        "default": [],
        "description": "Allowlist of (address, chain, token) tuples. EMPTY ARRAY = deny all counterparties (the engine fails closed). Address-only is unsafe because the same string can legitimately exist on multiple chains with different meaning (sanctions-cache-chain-key learning, F1).",
        "items": {
          "type": "object",
          "description": "Counterparty key. All three of (address, chain, token) are required to disambiguate.",
          "properties": {
            "address": {
              "type": "string",
              "minLength": 1,
              "maxLength": 128,
              "description": "Counterparty address. EVM addresses MUST be 0x-prefixed 20-byte hex; Solana addresses MUST be base58 (32-44 chars). Server-side normalizes (EVM → lowercased) before sanctions cache lookup. Loose schema-side typing keeps backward compat with seed-script values."
            },
            "chain": {
              "$ref": "_types.json#/$defs/chainId",
              "description": "Chain the address lives on. Determines which decimals + screening cache key to use."
            },
            "token": {
              "type": "string",
              "minLength": 1,
              "maxLength": 32,
              "description": "Token symbol (e.g. 'USDC') OR contract address. The router resolves to canonical contract per chain at evaluation time."
            }
          },
          "required": ["address", "chain", "token"],
          "additionalProperties": false
        }
      },
      "velocity_max_txs_per_hour": {
        "$ref": "_types.json#/$defs/positiveInt",
        "description": "Max transactions in any rolling 60-minute window. Sliding window, not calendar hour. Aggregated in router Redis layer (Privy TEE can't aggregate cross-tx state)."
      },
      "velocity_max_txs_per_day": {
        "$ref": "_types.json#/$defs/positiveInt",
        "description": "Max transactions in any rolling 24-hour window. Sliding window."
      },
      "velocity_multiple_of_baseline_threshold": {
        "type": "number",
        "exclusiveMinimum": 0,
        "maximum": 1000,
        "description": "Anomaly multiplier: if today's tx-count exceeds (baseline_28d_avg × this), flag as anomaly. Baseline is computed server-side from the past 28 days; integrators only set the multiplier."
      },
      "chain_allowlist": {
        "type": "array",
        "minItems": 1,
        "uniqueItems": true,
        "description": "Allowlist of chains this policy permits. MUST be non-empty — an empty chain allowlist would deny everything but is too easy to misconfigure; instead, set policy_status = revoked on the row.",
        "items": { "$ref": "_types.json#/$defs/chainId" }
      },
      "geo_allowlist": {
        "type": "array",
        "default": [],
        "uniqueItems": true,
        "description": "ISO 3166-1 alpha-2 country codes (uppercase) where the principal may transact from. Empty array = no geo gate (open). Geo is checked from server-detected IP + KYC residency; client headers are ignored.",
        "items": { "$ref": "_types.json#/$defs/iso3166Alpha2" }
      },
      "mcc_allowlist": {
        "type": "array",
        "default": [],
        "uniqueItems": true,
        "description": "ISO 18245 Merchant Category Codes (4 ASCII digits) the policy permits for card auth. Empty = open (any MCC allowed); use mcc_blocklist for a denylist posture. If both are set, blocklist wins.",
        "items": { "$ref": "_types.json#/$defs/mccCode" }
      },
      "mcc_blocklist": {
        "type": "array",
        "default": [],
        "uniqueItems": true,
        "description": "ISO 18245 Merchant Category Codes (4 ASCII digits) the policy refuses for card auth. Card-auth path only — wire/ACH paths ignore MCCs.",
        "items": { "$ref": "_types.json#/$defs/mccCode" }
      },
      "time_window_start": {
        "$ref": "_types.json#/$defs/isoDateTimeUtc",
        "description": "ABSOLUTE start instant (UTC) of the policy's effective window. Tx attempts before this fail-closed. Optional — absent means 'effective from created_at'. NOT a recurring time-of-day window; for hour-of-day gating use a per-skill timeWindow on a SkillManifest. The policy engine enforces time_window_start <= time_window_end at policy create/update time; JSON Schema cannot express the cross-field constraint."
      },
      "time_window_end": {
        "$ref": "_types.json#/$defs/isoDateTimeUtc",
        "description": "ABSOLUTE end instant (UTC) of the policy's effective window. Tx attempts after this fail-closed. Optional — absent means 'no end'. Must be >= time_window_start when both set (server-enforced)."
      },
      "created_at": {
        "$ref": "_types.json#/$defs/isoDateTimeUtc",
        "description": "When this policy row was first persisted. Server-stamped, immutable across version bumps."
      },
      "updated_at": {
        "$ref": "_types.json#/$defs/isoDateTimeUtc",
        "description": "When this policy row was last mutated. Server-stamped on every policy_version bump."
      }
    },
    "required": [
      "policy_id",
      "vault_id",
      "policy_version",
      "counterparty_allowlist",
      "chain_allowlist",
      "geo_allowlist",
      "mcc_allowlist",
      "mcc_blocklist",
      "created_at",
      "updated_at"
    ],
    "additionalProperties": false
  }
}
