Skip to content

PolicyGrant

Artifact Type: MPCP:PolicyGrant

Part of the Machine Payment Control Protocol (MPCP).


Purpose

A PolicyGrant represents the result of a policy evaluation performed before a machine is allowed to initiate payment activity.

The PolicyGrant defines the initial permission envelope for a session or payment scope. It constrains which rails, assets, and spending limits may later be authorized via the SignedBudgetAuthorization (SBA).

PolicyGrant is typically produced by a policy engine during an entry phase when a machine, vehicle, or agent attempts to access a service.

The PolicyGrant is a signed artifact. It is signed by the policy authority; verifiers use issuer and issuerKeyId to resolve the policy authority public key for signature verification.


Problem

In autonomous payment environments (vehicles, robots, AI agents), a system must determine whether payment activity is allowed before any transaction is authorized.

Without a formal grant artifact:

  • agents may bypass policy
  • payment decisions may not be traceable to policy evaluation
  • downstream authorizations cannot prove they were derived from policy

PolicyGrant solves this by creating a verifiable policy snapshot that later artifacts must reference.


Policy Lifecycle Context

Policy evaluation follows this chain:

PolicyGrant
     ↓
SignedBudgetAuthorization (SBA)
     ↓
Trust Gateway → XRPL Settlement

PolicyGrant establishes the upper policy boundary that subsequent artifacts must respect.

For example:

  • allowed rails
  • allowed assets
  • spending caps
  • policy expiration

Downstream artifacts must be subsets of the PolicyGrant constraints.


MPCP conformance (mandatory XRPL)

MPCP v1.0 defines a single normative settlement rail: XRPL. A PolicyGrant is conforming only if all of the following hold:

  1. allowedRails is exactly ["xrpl"] (no additional or alternate rail identifiers).
  2. authorizedGateway is present — the PA binds settlement to one Trust Gateway XRPL address.
  3. velocityLimit is present — the PA sets a gateway-enforceable cap on settlement frequency (see Velocity limit enforcement).
  4. revocationEndpoint MUST NOT be present — use activeGrantCredentialIssuer and XLS-70 Credentials for revocation (see Revocation).

Verifiers and Trust Gateways MUST reject non-conforming PolicyGrants at settlement. Fields such as gatewayCredentialIssuer / gatewayCredentialType are optional strengtheners for on-chain gateway authorization.


Merchant privacy and grant presentation (PolicyGrant exposure)

PolicyGrants can carry sensitive or linkable data (subjectId, allowedPurposes, credential issuer addresses, destination policy, etc.). Operators SHOULD minimize what each merchant receives.

Deployments MAY use gateway-only PolicyGrant presentation:

  1. Trust Gateway (settlement path): MUST receive the full, PA-signed PolicyGrant and the SBA for every online settlement. The gateway runs the full verification chain (PolicyGrant signature, policyHash consistency, SBA ⊆ grant constraints, velocity, credentials, etc.).
  2. Merchants and other verifiers: MAY receive only the SBA (plus payment quote metadata). The SBA carries grantId and policyHash — the minimum grant linkage in the SBA payload. No other PolicyGrant fields are required on the wire to the merchant for this mode.
  3. Assurance trade-off: A verifier without the PolicyGrant cannot verify the PA signature on the grant or cryptographically confirm that SBA constraints are subsets of hidden grant fields. It CAN still verify the SBA signature (session authority), expiresAt, payment envelope fields on the SBA, and treat policyHash as an opaque commitment to the policy the PA published. Strong policy enforcement for fields not present on the SBA relies on the Trust Gateway (and on operators not issuing over-broad SBAs).
  4. Offline merchants: Without a PolicyGrant, local verification cannot read PA-signed offlineMaxSinglePayment from the grant. Operators SHOULD either (a) include necessary caps in Trust Bundle or merchant configuration, (b) issue SBAs whose maxAmountMinor is already within the intended offline cap, or (c) use a PA-signed redacted excerpt artifact (separate profile) if offline verifiers must enforce grant caps without seeing the full grant.

Full-chain presentation (default)

When the merchant receives the full PolicyGrant and SBA, verifiers MUST verify both signatures and subset constraints as described in Verification.

See also SignedBudgetAuthorization — Policy linkage fields.


Structure

PolicyGrant (payload)

Field Type Required Description
version string yes MPCP semantic version (e.g. "1.0")
grantId string yes Unique identifier for the grant
policyHash string yes SHA-256 hash of the canonical policy document from which this grant was issued. Computed as SHA256("MPCP:Policy:<version>:" \|\| canonicalJson(policyDocument)). Downstream SBA artifacts MUST carry the same value.
subjectId string yes Identifier of the entity receiving the grant (vehicle, agent, wallet, etc.)
operatorId string optional Service operator identifier
scope string yes Scope of the grant (SESSION, VEHICLE, FLEET, etc.)
allowedRails Rail[] yes Conforming grants: MUST be exactly ["xrpl"]. MPCP v1 does not define multi-rail settlement.
allowedAssets Asset[] conditional Allowed assets for on-chain rails
maxSpend object optional Policy-layer spending caps (e.g. per-tx / per-session in policy or fiat minor units). Human-readable and tooling; not the Trust Gateway on-chain ceiling. See budgetMinor and maxSpend.
expiresAt string yes ISO 8601 expiration timestamp
requireApproval boolean optional Indicates that further approval is required before payment
reasons string[] optional Policy evaluation reasons
issuer string yes Identifier for the policy authority (e.g. DID, domain, or registry ID). Verifiers use this to resolve the signing key.
issuerKeyId string yes Identifies the specific key used to sign (for deployments with multiple keys per issuer).
signature string yes Cryptographic signature over the canonical JSON of the grant payload (all fields except signature).
revocationEndpoint string deprecated MUST NOT appear on conforming PolicyGrants. Reserved for reading historic artifacts only; MPCP v1 revocation is XRPL Credentials only. See Revocation below.
activeGrantCredentialIssuer string optional XRPL address of the PA (or delegate) that issues the active grant XLS-70 Credential. When present, online verifiers (including the Trust Gateway) MUST treat the grant as revoked if that credential does not exist on the subject's XRPL account. Conforming grants SHOULD set this field so revocation is ledger-native. See Revocation below.
allowedPurposes string[] optional Merchant category allowlist (e.g. ["travel:hotel", "travel:flight"]). PA-signed. The agent MUST check purpose before issuing each SBA. The Trust Gateway SHOULD enforce purpose when the settlement request includes a purpose field. See Purpose Enforcement section below.
anchorRef string optional Pointer to an on-chain record of the policy document. Format: "hcs:{topicId}:{sequenceNumber}" (Hedera HCS). The historical xrpl:nft:{tokenId} pattern is deprecated and MUST NOT be used in new deployments; use HCS (or off-chain custody) for policy hash audit and activeGrantCredentialIssuer for XRPL grant revocation. See Policy Document Anchoring below.
budgetMinor string optional PA-signed on-chain budget ceiling in the settlement asset’s atomic units (e.g. XRP drops). The Trust Gateway MUST enforce this as the hard escrow / cumulative ceiling. See budgetMinor and maxSpend.
budgetCurrency string optional Currency of budgetMinor (e.g. "XRP").
budgetEscrowRef string optional URI reference to the on-chain budget escrow that pre-reserves the full budgetMinor. Format: "{rail}:{mechanism}:{identifier}" (e.g. "xrpl:escrow:{account}:{sequence}"). PA-signed. See Rails.
authorizedGateway string yes XRPL classic address of the only Trust Gateway authorized to submit XRPL payments for this grant. The Trust Gateway MUST reject settlement if its own XRPL address does not match. PA-signed. Required for MPCP v1 conformance (see MPCP conformance).
gatewayCredentialIssuer string optional XRPL address of the credential issuer for gateway authorization (XLS-70). When present together with gatewayCredentialType, the Trust Gateway MUST verify on-chain that its settlement account holds a valid, non-expired credential from this issuer and type before submitting each payment; on compromise, the PA revokes by CredentialDelete.
gatewayCredentialType string optional Hex-encoded credential type the gateway account must hold (e.g. hex("mpcp:authorized-gateway")). Used with gatewayCredentialIssuer.
velocityLimit object yes PA-signed velocity cap for settlements. Required for MPCP v1 conformance. Fields: maxPayments (integer ≥ 1) and windowSeconds (integer ≥ 1). See Velocity limit enforcement.
destinationAllowlist string[] optional PA-signed allowlist of permitted payment destination addresses (e.g. XRPL r-addresses). When present, the Trust Gateway MUST verify that the payment destination is in this list before settling. SBA destinationAllowlist MUST be a subset. See Destination Enforcement section below.
merchantCredentialIssuer string optional XRPL address of the credential issuer for approved merchants. Used with XLS-70 on-chain Credentials for dynamic destination enforcement. See Destination Enforcement section below.
merchantCredentialType string optional Hex-encoded credential type that approved merchants must hold (e.g. hex("mpcp:approved-merchant")). Used with merchantCredentialIssuer.
subjectCredentialIssuer string optional XRPL address of the credential issuer that attests the subject's identity. When present, the gateway SHOULD verify on-chain that subjectId's XRPL account holds a valid credential from this issuer. See Subject Attestation section below.
subjectCredentialType string optional Hex-encoded credential type the subject must hold (e.g. hex("mpcp:fleet-agent")). Used with subjectCredentialIssuer.
offlineMaxSinglePayment string optional PA-signed per-transaction cap (in offlineMaxSinglePaymentCurrency minor units) for offline merchant acceptance. Offline merchants MUST reject SBAs whose maxAmountMinor exceeds this value. Cumulative offline exposure across merchants is only bounded when offlineMaxCumulativePayment is present; see Offline cumulative exposure below.
offlineMaxSinglePaymentCurrency string optional Currency of offlineMaxSinglePayment (e.g. "XRP").
offlineMaxCumulativePayment string optional PA-signed maximum total amount (in offlineMaxCumulativePaymentCurrency minor units) that offline verifiers MAY cumulatively accept for this grant across all offline transactions at this verifier. Merchants SHOULD sum accepted amounts per grantId and reject when the next acceptance would exceed this cap. See Offline cumulative exposure below.
offlineMaxCumulativePaymentCurrency string optional Currency of offlineMaxCumulativePayment (e.g. "XRP"). If absent while offlineMaxCumulativePayment is present, implementations SHOULD use offlineMaxSinglePaymentCurrency.

Example

{
  "version": "1.0",
  "grantId": "grant_7ab3",
  "policyHash": "9f3a0d...",
  "subjectId": "vehicle_1284",
  "operatorId": "operator_12",
  "scope": "SESSION",
  "allowedRails": ["xrpl"],
  "allowedAssets": [{ "kind": "IOU", "currency": "RLUSD", "issuer": "rIssuer..." }],
  "allowedPurposes": ["transport:toll", "transport:charging", "transport:parking"],
  "authorizedGateway": "rTrustGatewayMain...",
  "gatewayCredentialIssuer": "rPolicyAuthorityXrpl...",
  "gatewayCredentialType": "6D7063703A617574686F72697A65642D67617465776179",
  "velocityLimit": { "maxPayments": 120, "windowSeconds": 3600 },
  "destinationAllowlist": ["rChargingStation1", "rTollBooth42"],
  "merchantCredentialIssuer": "rPAMerchantRegistry",
  "merchantCredentialType": "6D7063703A617070726F7665642D6D65726368616E74",
  "activeGrantCredentialIssuer": "rPolicyAuthorityXrpl...",
  "offlineMaxSinglePayment": "5000000",
  "offlineMaxSinglePaymentCurrency": "XRP",
  "offlineMaxCumulativePayment": "15000000",
  "offlineMaxCumulativePaymentCurrency": "XRP",
  "maxSpend": {
    "perTxMinor": "5000",
    "perSessionMinor": "20000"
  },
  "expiresAt": "2026-03-08T14:00:00Z",
  "requireApproval": false,
  "reasons": ["OK"],
  "issuer": "did:web:operator.example.com",
  "issuerKeyId": "policy-auth-key-1",
  "signature": "..."
}

Policy Intersection Model

PolicyGrant may represent the intersection of multiple policy sources.

Example policy layers:

  • operator policy
  • fleet policy
  • user policy
  • regulatory policy

The effective policy becomes:

effectivePolicy =
    operatorPolicy
 ∩  fleetPolicy
 ∩  userPolicy
 ∩  regulatoryPolicy

The PolicyGrant stores the resulting effective constraints.


budgetMinor and maxSpend (precedence)

PolicyGrant may carry both maxSpend (optional) and budgetMinor (optional but required for meaningful Trust Gateway budget enforcement on XRPL with escrow). They serve different roles:

Field Role Enforced by
budgetMinor Authoritative on-chain ceiling for the grant, in atomic settlement units (drops, etc.), PA-signed Trust Gateway (durable cumulative spend + escrow)
maxSpend Policy-layer caps (e.g. perTxMinor, perSessionMinor) aligned with the policy document and operator UX Session authority / agent guidance; documented intent; not a substitute for budgetMinor at the gateway

Precedence:

  1. Settlement: The Trust Gateway MUST use budgetMinor (and escrow) as the hard spending ceiling. It MUST NOT derive the on-chain ceiling from maxSpend alone.
  2. Consistency: When both are present, the PA SHOULD ensure budgetMinor, after any unit conversion to the settlement asset, does not exceed the effective cap implied by intersected policy (including fleet policy — see FleetPolicyAuthorization). The session authority SHOULD issue SBAs whose maxAmountMinor stays within both the SBA scope envelope and the remaining headroom under budgetMinor.
  3. Subset checks: SBA.allowedRails / SBA.allowedAssets MUST remain subsets of the PolicyGrant. If maxSpend defines comparable numeric limits in the same units as SBA.maxAmountMinor, implementations SHOULD verify the SBA does not violate those limits; otherwise treat maxSpend as informational relative to the policy document behind policyHash.

If budgetMinor is omitted, gateway-level cumulative enforcement against a PA-signed escrow budget is undefined — conforming XRPL deployments SHOULD always set budgetMinor.


Relationship to SBA

A SignedBudgetAuthorization must always be a subset of the PolicyGrant.

For example:

SBA.allowedRails ⊆ PolicyGrant.allowedRails
SBA.allowedAssets ⊆ PolicyGrant.allowedAssets

The SBA MUST reference the same policyHash as the PolicyGrant. Cumulative spend against PolicyGrant.budgetMinor MUST be enforced by the Trust Gateway; SBA.maxAmountMinor is the per-payment envelope within that grant (and within session authority tracking). When maxSpend is present and comparable, the SBA MUST also respect those policy-layer caps.


Expiration

PolicyGrant defines the maximum validity window for downstream artifacts.

Implementations MUST ensure:

  • SBA expiration does not exceed PolicyGrant expiration

Policy Hashing

policyHash is the SHA-256 hash of the canonical policy document from which the PolicyGrant was issued.

The policy document is the structured representation of the rules evaluated to produce the grant (operator policy, fleet policy, regulatory constraints, etc.). It MUST be serialized using MPCP canonical JSON before hashing.

Computation:

policyHash = SHA256("MPCP:Policy:<version>:" || canonicalJson(policyDocument))

Example:

policyHash = SHA256("MPCP:Policy:1.0:" || '{"allowedAssets":[...],"allowedRails":[...],...}')

The policyHash is not a hash of the PolicyGrant artifact itself — it is a hash of the source policy that the grant was derived from.

Downstream SBA artifacts MUST carry the same policyHash. During settlement verification, the Trust Gateway checks that PolicyGrant.policyHash and SBA.policyHash are equal, confirming the entire authorization chain derives from the same policy snapshot.


Signing Requirements

PolicyGrant MUST be cryptographically signed by the policy authority. Verifiers that have a public key configured MUST verify the signature and MUST reject unsigned grants.

Domain Hash

The signed payload uses domain-separated hashing:

hash = SHA256("MPCP:PolicyGrant:1.0:" || canonicalJson(grantPayload))
signature = sign(hash, policyAuthorityPrivateKey)

where grantPayload is all grant fields except signature, serialized as canonical JSON.

Reference Implementation — Environment Variables

The reference implementation (policyGrant.ts) exposes signing and verification via environment variables:

Variable Purpose
MPCP_POLICY_GRANT_SIGNING_PRIVATE_KEY_PEM Private key for signing PolicyGrants
MPCP_POLICY_GRANT_SIGNING_PUBLIC_KEY_PEM Public key for verifying PolicyGrant signatures. When set, unsigned grants are rejected.
MPCP_POLICY_GRANT_SIGNING_KEY_ID Key identifier (default: mpcp-policy-grant-signing-key-1)

Functions: - createSignedPolicyGrant(grant) — signs the grant, returns SignedPolicyGrant - verifyPolicyGrantSignature(envelope) — verifies the signature

Enforcement

If MPCP_POLICY_GRANT_SIGNING_PUBLIC_KEY_PEM is set: - Grants without issuerKeyId and signature are rejected with invalid_policy_grant_signature - Grants with an invalid signature are rejected with invalid_policy_grant_signature

If the env var is not set, signature verification is skipped (backward-compatible mode for environments using pre-validated grants).

If signature verification fails, the verifier MUST reject the grant.

Key Resolution

Verifiers resolve the public key (as JWK) using issuer and issuerKeyId via the HTTPS well-known endpoint or pre-configured keys. See Key Resolution.


Policy Document Anchoring

The anchorRef field is an optional pointer to an on-chain record of the policy document that produced this grant. The conforming format is:

"hcs:{topicId}:{sequenceNumber}"   — Hedera Consensus Service message

Verifier behavior: The MPCP verifier passes anchorRef through without enforcement. It is informational metadata used by auditors, merchants, and dispute resolution tooling.

See Policy Anchoring for the full anchoring specification, including HCS message format and environment variables. XRPL grant revocation uses XLS-70 Credentials (activeGrantCredentialIssuer), not NFToken burn.


Velocity limit enforcement

The velocityLimit object limits how many successful XRPL settlements the Trust Gateway may execute for this grant within a sliding wall-clock window. It mitigates rapid-fire draining of budgetMinor by a compromised agent before operators can react.

Shape

Field Type Description
maxPayments integer Maximum settlements allowed in each window. MUST be ≥ 1.
windowSeconds integer Window length in seconds. MUST be ≥ 1.

Gateway behaviour (MUST)

Before submitting a payment that would settle against this grant, the Trust Gateway MUST:

  1. Count how many settlements it has already completed for this grantId within the preceding windowSeconds (inclusive of the current verification instant), using a sliding window in wall-clock time.
  2. If that count is already ≥ maxPaymentsreject with VELOCITY_LIMIT_EXCEEDED.
  3. Otherwise, if the payment is submitted successfully, include the settlement in the count for future windows.

Implementations MAY use a fixed clock window (e.g. calendar hour) if behaviour is documented and is no looser than the sliding-window semantics above.

Error code

Code Meaning
VELOCITY_LIMIT_EXCEEDED Settlement would exceed PolicyGrant.velocityLimit for this grantId

Gateway credential binding (optional)

When both gatewayCredentialIssuer and gatewayCredentialType are present, the Trust Gateway MUST verify on-chain — before each XRPL Payment — that its own XRPL account (the same address as authorizedGateway) holds a valid, non-expired XLS-70 Credential with:

  • Issuer = gatewayCredentialIssuer
  • CredentialType = gatewayCredentialType (e.g. hex("mpcp:authorized-gateway"))

If the credential is missing or expired → reject settlement (implementation SHOULD use GATEWAY_NOT_CREDENTIALED). On gateway compromise, the PA revokes by CredentialDelete without reissuing the PolicyGrant.


Revocation

MPCP v1 defines XRPL Credential-based revocation via activeGrantCredentialIssuer. The field revocationEndpoint is deprecated and MUST NOT appear on conforming PolicyGrants.

When activeGrantCredentialIssuer is present, the PA (or a delegate controlling that XRPL account) MUST issue an XLS-70 CredentialCreate for each active grant, and the grant subject (agent / vehicle wallet) MUST CredentialAccept it on-ledger.

Credential binding:

Field Value
Issuer PolicyGrant.activeGrantCredentialIssuer (PA XRPL address)
Subject The grant subject's XRPL account — the same on-chain identity that signs or anchors spending for this grant (typically the address in subjectId when expressed as an XRPL r-address, or as defined by the deployment profile)
CredentialType Hex-encoded UTF-8 string: mpcp:active-grant: concatenated with the literal grantId (same characters as in the PolicyGrant). Example: hexEncode(UTF8("mpcp:active-grant:" + grantId))

Revocation: The PA submits CredentialDelete for that credential. After ledger finality, the grant MUST be treated as revoked — no HTTP endpoint is consulted.

Verifier behaviour (MUST when field is present): Before accepting a payment or settlement, online verifiers MUST query the XRPL ledger. If no matching non-expired credential exists for (Subject, Issuer, CredentialType) → reject with ACTIVE_GRANT_CREDENTIAL_MISSING.

The Trust Gateway performs this check in MPCP verification Step 1.

Advantages: On-chain finality (~4s), no hosted revocation HTTP service, composable with other XLS-70 tooling.

Offline verifiers: Cannot query the ledger; they remain subject to the usual offline revocation limitations (TTL cache, bundle refresh, expiresAt).

Deprecated: revocationEndpoint (HTTP)

Historic artifacts MAY carry revocationEndpoint for backward compatibility with pre-v1 implementations. Conforming PolicyGrants MUST NOT include this field. Verifiers processing legacy grants MAY still call HTTP revocation when no activeGrantCredentialIssuer is present, but such grants are outside MPCP v1 conformance.

Choosing a mechanism

New grants MUST omit revocationEndpoint and SHOULD set activeGrantCredentialIssuer for on-chain revocation.

If both fields appear on one artifact (e.g. during migration), verifiers that support Credentials MUST apply the on-chain check when activeGrantCredentialIssuer is present; HTTP is optional redundancy for legacy consumers only, not a substitute for a missing credential on conforming deployments.

Deprecated: XRPL NFToken burn

Revocation via burning an NFToken referenced in anchorRef (xrpl:nft:{tokenId}) is deprecated and MUST NOT be specified for new grants. Use activeGrantCredentialIssuer and CredentialDelete instead. See Policy Anchoring.


Purpose Enforcement

Overview

allowedPurposes defines the merchant categories a grant permits (e.g. ["transport:toll", "transport:charging"]). It is PA-signed and tamper-proof — a compromised agent cannot modify the list.

Purpose enforcement operates at two levels:

Level Actor Enforcement Trust
Agent-side Agent / Vehicle Wallet MUST check before issuing each SBA Untrusted if agent is compromised
Gateway-side Trust Gateway SHOULD check before submitting settlement Trusted — agent cannot bypass

Agent-Side Enforcement (MUST)

Before issuing an SBA, the agent MUST check whether the merchant category (purpose) is in the allowedPurposes list. If not, the agent MUST refuse to issue an SBA — no payment proceeds.

const purposeAllowed = grant.allowedPurposes?.includes(merchantCategory) ?? true;
if (!purposeAllowed) {
  // refuse to issue SBA — purpose not permitted by grant
}

Gateway-Side Enforcement (SHOULD)

When the PolicyGrant contains allowedPurposes, the Trust Gateway SHOULD enforce purpose compliance before submitting each settlement transaction.

The settlement request context (the request from the agent or proxy to the gateway) SHOULD include a purpose field declaring the merchant category for the payment. When present, the gateway checks:

if (policyGrant.allowedPurposes is present)
    and (settlementRequest.purpose is present):
  if settlementRequest.purpose ∉ policyGrant.allowedPurposes:
    → reject with PURPOSE_NOT_ALLOWED

If the settlement request does not include a purpose field and the grant has allowedPurposes, the gateway MAY reject the request or MAY accept it (backward-compatible mode). Implementations transitioning to gateway-side enforcement SHOULD log a warning when purpose is absent.

Why Gateway Enforcement Is Needed

allowedPurposes was originally specified as agent-enforced only. This is insufficient because a compromised agent (prompt injection, model manipulation, software vulnerability) can skip its own purpose check, call createSba() for any merchant category, and the gateway — without purpose enforcement — would sign and submit the payment.

The Trust Gateway is the trust boundary: it holds the settlement keys and the agent cannot bypass it. The gateway already enforces budget ceiling, SBA validity, and authorizedGateway from the PA-signed grant. Adding purpose enforcement closes the last policy bypass vector available to a compromised agent.

Trust Model After Purpose Enforcement

Enforcement point What it checks Trusted?
Agent (first line) Purpose, budget, revocation No — can be bypassed if compromised
Trust Gateway SBA validity, budget ceiling, gateway auth, purpose Yes — holds keys, agent cannot bypass
Merchant SBA + PolicyGrant signatures (offline), on-chain settlement (online) Yes — independent party

A compromised agent that skips purpose checking is still bounded by the gateway: the gateway refuses to submit the payment if the declared purpose is not in the PA-signed allowedPurposes. No XRPL transaction is made.

Error Code

Code Meaning
PURPOSE_NOT_ALLOWED Settlement request purpose is not in PolicyGrant.allowedPurposes

Destination Enforcement

Overview

A compromised agent could populate the SBA destinationAllowlist with an attacker-controlled address, directing funds away from legitimate merchants. Because the SBA is agent-signed, the agent controls this field.

MPCP addresses this with two complementary mechanisms, both PA-signed and tamper-proof:

  1. Static allowlistdestinationAllowlist on the PolicyGrant
  2. On-chain credential registrymerchantCredentialIssuer + merchantCredentialType fields referencing XRPL Credentials (XLS-70)

Either or both may be present. When both are present, a destination MUST satisfy at least one mechanism.

Mechanism 1: PA-Signed Static Allowlist

The destinationAllowlist field on the PolicyGrant is an array of permitted payment destination addresses (e.g. XRPL r-addresses), signed by the PA.

Agent responsibility: When issuing an SBA, the agent MUST set SBA.destinationAllowlist ⊆ PolicyGrant.destinationAllowlist. An SBA that includes a destination not in the PA-signed list is invalid.

Gateway enforcement (MUST): Before submitting settlement, the Trust Gateway MUST verify:

if PolicyGrant.destinationAllowlist is present:
  payment.destination ∈ PolicyGrant.destinationAllowlist
  → reject with DESTINATION_NOT_ALLOWED on mismatch

When to use: Deployments where the set of approved merchants is known at grant issuance and does not change during the grant's lifetime. Suitable for fleet deployments with a fixed set of infrastructure providers.

Mechanism 2: XRPL Credential Merchant Registry

For deployments where the set of approved merchants changes dynamically (merchants onboard or offboard during a grant's lifetime), the PA can use XRPL Credentials (XLS-70) to maintain a live, on-chain merchant registry.

Setup:

  1. PA issues CredentialCreate to each approved merchant's XRPL account:
  2. Issuer = PA's XRPL address
  3. Subject = merchant's XRPL address
  4. CredentialType = hex-encoded type string (e.g. hex("mpcp:approved-merchant"))
  5. Optional Expiration for time-bounded approval
  6. Merchant calls CredentialAccept to activate the credential on-ledger
  7. PolicyGrant carries:
  8. merchantCredentialIssuer: PA's XRPL address
  9. merchantCredentialType: the hex-encoded credential type

Gateway enforcement (MUST when fields are present): Before submitting settlement, the Trust Gateway MUST verify on-chain that the payment destination account holds a valid, non-expired credential matching both merchantCredentialIssuer and merchantCredentialType:

if PolicyGrant.merchantCredentialIssuer is present:
  credential = lookupCredential(
    subject:  payment.destination,
    issuer:   PolicyGrant.merchantCredentialIssuer,
    type:     PolicyGrant.merchantCredentialType
  )
  if credential does not exist or is expired:
    → reject with DESTINATION_NOT_CREDENTIALED

Advantages over static allowlist:

  • PA can add new merchants by issuing credentials without reissuing PolicyGrants
  • PA can remove merchants by deleting credentials — takes effect immediately on-chain
  • Merchant approval is publicly verifiable by any party
  • No grant reissuance needed when the merchant set changes

When to use: Deployments where merchants are onboarded dynamically, multi-operator ecosystems, or when the PA wants centralized on-chain control over merchant approval.

Combining Both Mechanisms

When both destinationAllowlist and merchantCredentialIssuer are present on a PolicyGrant, a destination is approved if it satisfies either mechanism:

approved = (destination ∈ PolicyGrant.destinationAllowlist)
        OR (destination holds matching credential on-chain)

This allows deployments to use the static list as a baseline and the credential registry for dynamic additions.

Error Codes

Code Meaning
DESTINATION_NOT_ALLOWED Payment destination not in PolicyGrant.destinationAllowlist and no credential match
DESTINATION_NOT_CREDENTIALED merchantCredentialIssuer is set but destination does not hold a matching credential

Subject Attestation

Overview

The subjectId field identifies the entity receiving the grant (vehicle, agent, wallet). By default, subjectId is self-reported and informational only — it cannot be cryptographically verified. This means a compromised agent sharing a signing key with other agents can issue SBAs that appear to come from any agent in the fleet.

Problem: When multiple agents share the same SBA signing key, revoking a compromised agent's grant affects all agents using that key. The fleet operator cannot isolate the compromised agent without disrupting the entire fleet.

Per-Agent Signing Keys (SHOULD)

Each agent or vehicle wallet SHOULD have a unique SBA signing key. This ensures:

  • Revoking one agent's grant does not affect other agents
  • SBAs can be attributed to a specific agent for audit purposes
  • Compromised agents can be isolated without fleet-wide disruption

Fleet operators SHOULD register each agent's public key in the Trust Bundle or JWKS endpoint under a unique kid that includes the agent identity (e.g. "sba-key:vehicle-1284").

XRPL Credential-Based Subject Attestation (SHOULD for XRPL deployments)

For XRPL deployments, the fleet operator or PA SHOULD issue an on-chain credential to each agent's XRPL account using XLS-70 Credentials:

  • Issuer = Fleet operator's or PA's XRPL address
  • Subject = Agent's XRPL account
  • CredentialType = hex-encoded type (e.g. hex("mpcp:fleet-agent") or hex("mpcp:authorized-agent"))

The PolicyGrant binds to the specific agent via:

  • subjectCredentialIssuer — the XRPL address of the credential issuer
  • subjectCredentialType — the hex-encoded credential type

Canonical subject identifier (XRPL): Deployments using credential-based attestation SHOULD encode subjectId as did:xrpl:{network}:{rAddress} (classic address r... on the intended ledger, with {network} disambiguating mainnet vs testnet or other deployments). This makes the grant subject align with a verifiable on-chain identity. When subjectId uses another format, implementations MUST still use a single unambiguous mapping from grant subject to the XRPL account that holds the credential.

Binding actorId to the credential Subject: When subjectCredentialIssuer is present, the SBA's authorization.actorId MUST equal the XRPL classic address of the on-chain credential Subject (the account that holds the credential). This prevents a compromised agent that shares no unique signing key from impersonating another fleet agent's actorId while presenting a different XRPL settlement identity: the gateway correlates the attested account with the self-reported actor field.

When PolicyGrant.subjectId uses the did:xrpl:{network}:{rAddress} form, authorization.actorId MUST equal that {rAddress}; the gateway MUST reject with SUBJECT_ACTOR_MISMATCH if the grant binds a DID subject and the SBA's actorId does not match the DID's address component.

Gateway enforcement (SHOULD): Before accepting SBAs from an agent, the gateway SHOULD verify on-chain that the account SBA.authorization.actorId holds a valid, non-expired credential matching the grant's subjectCredentialIssuer and subjectCredentialType:

if PolicyGrant.subjectCredentialIssuer is present:
    if PolicyGrant.subjectId matches did:xrpl:*:{rAddress}:
        if SBA.authorization.actorId ≠ rAddress:
            → reject with SUBJECT_ACTOR_MISMATCH
    credential = lookupCredential(
        subject: SBA.authorization.actorId,
        issuer:  PolicyGrant.subjectCredentialIssuer,
        type:    PolicyGrant.subjectCredentialType
    )
    if credential does not exist or is expired:
        → reject with SUBJECT_NOT_ATTESTED

Isolation on compromise: When a single agent is compromised, the fleet operator:

  1. Deletes the agent's on-chain credential via CredentialDelete
  2. The gateway rejects further SBAs from that agent (credential check fails)
  3. Other agents' credentials are unaffected — they continue operating normally
  4. No grant reissuance needed for uncompromised agents

Error codes

Code Meaning
SUBJECT_NOT_ATTESTED subjectCredentialIssuer is set but the credential Subject account does not hold a matching on-chain credential
SUBJECT_ACTOR_MISMATCH subjectCredentialIssuer is set and either SBA.authorization.actorId does not equal the credential Subject account, or subjectId is did:xrpl:…:{rAddress} and actorId{rAddress}

Offline cumulative exposure

Risk

Without a cumulative offline cap, an agent can present SBAs to many offline merchants in sequence. Each merchant enforces only offlineMaxSinglePayment per transaction. The total offline spend can therefore exceed budgetMinor until the gateway reconciles on-chain.

Mitigation: offlineMaxCumulativePayment

When the PA includes offlineMaxCumulativePayment, offline verifiers that enforce this field MUST maintain a running total of amounts already accepted for each grantId (in the grant's offline minor units) and MUST reject a new SBA if accepting it would make the cumulative total exceed offlineMaxCumulativePayment.

Currency for the cumulative cap is offlineMaxCumulativePaymentCurrency, or offlineMaxSinglePaymentCurrency if the former is absent.

Operators SHOULD set offlineMaxCumulativePaymentbudgetMinor (or lower, to reflect risk tolerance). Operators SHOULD size offlineMaxSinglePayment with the expected number of offline touchpoints per grant in mind — a small per-tx cap with many merchants can still produce large aggregate exposure if no cumulative field is used.

Per-verifier scope

The cumulative total is tracked per offline verifier (per device). Distinct merchants do not share state; a fleet-wide cumulative bound requires synchronized infrastructure outside the scope of this specification. The PA SHOULD set offlineMaxCumulativePayment assuming worst-case fan-out across independent verifiers when modeling exposure.

Error code

Code Meaning
OFFLINE_CUMULATIVE_EXCEEDED Acceptance would exceed PolicyGrant.offlineMaxCumulativePayment

Summary

PolicyGrant establishes the policy boundary for machine payments.

It ensures that downstream SBA artifacts are always derived from a validated policy evaluation.


See Also