| Internet-Draft | Agent Payment Verdicts | October 2026 |
| Dias | Expires 12 April 2027 | [Page] |
When a software agent initiates a payment on behalf of an organization, the party that settles the payment usually cannot see the policy under which it was permitted. This document describes a signed authorization verdict: a JSON Web Token, signed with ES256, that records the outcome of evaluating one payment request, including its amount, currency, and recipient, against a spending policy that the organization configured. A party holding the issuer's published public key can verify a verdict without contacting the issuer.¶
The document describes the claim set, key publication and rotation, and a verification procedure, and states what a verdict does not assert. It describes the format as one implementation issues it, including the points where it departs from JSON Web Token Best Current Practices (RFC 8725). It is offered as input to IETF discussion of authorization records for agent-initiated actions and is not a proposal for standardization.¶
This note is to be removed before publishing as an RFC.¶
Comments are welcome by email to the author. Whether signed authorization decisions are in scope for the work proposed for a potential AUDIT BoF can be discussed on the audit@ietf.org list [AUDIT].¶
This Internet-Draft is submitted in full conformance with the provisions of BCP 78 and BCP 79.¶
Internet-Drafts are working documents of the Internet Engineering Task Force (IETF). Note that other groups may also distribute working documents as Internet-Drafts. The list of current Internet-Drafts is at https://datatracker.ietf.org/drafts/current/.¶
Internet-Drafts are draft documents valid for a maximum of six months and may be updated, replaced, or obsoleted by other documents at any time. It is inappropriate to use Internet-Drafts as reference material or to cite them other than as "work in progress."¶
This Internet-Draft will expire on 12 April 2027.¶
Copyright (c) 2026 IETF Trust and the persons identified as the document authors. All rights reserved.¶
This document is subject to BCP 78 and the IETF Trust's Legal Provisions Relating to IETF Documents (https://trustee.ietf.org/license-info) in effect on the date of publication of this document. Please review these documents carefully, as they describe your rights and restrictions with respect to this document. Code Components extracted from this document must include Revised BSD License text as described in Section 4.e of the Trust Legal Provisions and are provided without warranty as described in the Revised BSD License.¶
Three properties of an agent-initiated payment can be checked separately: which agent initiated it, whether it was within the authority the agent was given, and whether value moved. The first and third are the subject of active protocol work (Section 2). The second is usually decided inside the operator's own systems or a provider's, and the decision does not travel with the payment.¶
This document describes a verdict: a signed statement, valid for a short time, that names the request it was made about, the mandate it was checked against, if any, and the outcome of the check. A party asked to settle the payment can check a verdict without contacting the issuer, using the issuer's published keys, its own record of the payment, a record of the verdicts it has already accepted, and, where the payment draws on a particular operator's funds, the mandate identifiers and amount unit agreed with that operator (Section 6). Refusals produced by policy evaluation are signed in the same way as approvals; an operator's refusal of a request held for review produces no verdict (Section 4.5).¶
This document is descriptive. It records the format that one implementation issues (Section 9), including the points where the format departs from [RFC8725] and other properties the author considers weaknesses (Section 10, Appendix B). It is offered as input to IETF discussion of authorization records for agent actions, such as the discussion on the audit mailing list [AUDIT] of a potential AUDIT BoF and working group charter; in particular, Section 6.1 and Section 7 state, for one deployed format, what a successful check establishes and what it does not. The author does not ask for this format to be adopted; a standard format in this area would need to resolve the issues in Appendix B and might not be compatible with this one.¶
The author is the founder of Saifuro LLC, which operates the implementation described here, and has a commercial interest in this area.¶
In scope are the verdict format (Section 4), the publication of verification keys (Section 5), and the procedure a relying party follows before it relies on a verdict (Section 6).¶
Out of scope are: the language in which spending policy is expressed; how an agent authenticates to the issuer; user consent; how a payment is executed or settled; how a verdict is carried inside a particular payment protocol; and the identity of an agent beyond an identifier assigned by its operator. The issuer described here does not hold or move funds, and a verdict is not a payment instrument.¶
The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and "OPTIONAL" in this document are to be interpreted as described in BCP 14 [RFC2119] [RFC8174] when, and only when, they appear in all capitals, as shown here.¶
In this document, BCP 14 key words state what a relying party has to do to rely safely on verdicts in this format. In Table 2, and where Section 4.3 says that members are REQUIRED, they state what a verdict contains: a verifier rejects a token in which a required claim or member is absent (Section 6, steps 4 and 6). Section 5.5, Section 10.4, and Section 11 also address operators and others that carry or keep verdicts. The key words do not define requirements for other issuers or formats.¶
Names of claims, members of request, and header parameters that this format or its event tokens (Section 10.14) use are written without quotation marks (iss, request.amount, kid); values, and names that the format does not use, are written in quotation marks ("ES256", "allow", "aud").¶
Agent Issuer Relying Party
| | |
| (1) request | |
|------------------>| |
| | (2) evaluate against |
| | the mandates |
| (3) verdict (JWS) | |
|<------------------| |
| | |
| (4) payment, verdict attached |
|------------------------------------------->|
| | (5) JWK Set (cached) |
| |<-----------------------|
| |----------------------->|
| | | (6) verify,
| | | compare with
| | | payment record
The relying party needs no account with the issuer and sends it no request about individual verdicts: from the issuer it needs only the published JWK Set. It also needs its own payment record, its store of accepted nonces, and, for step 8 of Section 6, the mandate identifiers and the amount unit it has agreed with the operator.¶
Table 1 collects the values that this document refers to: those of the deployed implementation, and those this document sets for verifiers.¶
| Value | Setting |
|---|---|
| Verdict lifetime (exp - iat) | 300 seconds |
| Clock skew allowed to verifiers | at most 60 seconds |
| Review expiry | 24 hours |
| Frequency rule | 10 decisions per agent in 60 seconds |
| Window of the rolling limit | 30 days |
| JWK Set cache lifetime (max-age) | 300 seconds |
| Refetches on unknown kid | at most one per 30 seconds (recommended) |
| Pre-publication of a new key | at least 1 hour |
| Retention of a retired key | at least 25 hours after its last token |
| Redelivery of event tokens | last retry due 24 hours after creation |
| Identifier prefixes | "dec_", "mnd_", "n_", "agt_", "evt_" |
| Nonce | 64 random bits |
| Amount | greater than 0, less than 10^15, at most 6 fractional digits |
| Idempotency-Key retention | 24 hours |
| API request body limit | 64 KB |
| Length of recipient; of category and currency | at most 1,024 characters; at most 128 characters each |
A verdict is a JSON Web Token (JWT) [RFC7519] in JWS Compact Serialization [RFC7515]: three base64url segments without padding, separated by periods.¶
The issuer emits exactly three header parameters: alg, with the value "ES256" (ECDSA using P-256 and SHA-256, Section 3.4 of [RFC7518]); typ, with the value "JWT"; and kid, naming the signing key in the issuer's JWK Set (Section 5). It does not emit "crit", "jku", "jwk", "x5u", "x5c", "x5t", "cty", or any other header parameter. The typ value does not distinguish a verdict from other tokens signed with the same key (Section 10.6).¶
The signature is the 64-byte concatenation of R and S, each 32 bytes, big-endian, as Section 3.4 of [RFC7518] requires, not an ASN.1 DER encoding. The issuer uses randomized ECDSA (Section 10.5) and does not normalize S (Section 10.8).¶
The payload is a JSON object [RFC8259] encoded in UTF-8 [RFC3629]. The issuer writes it without insignificant whitespace and in the member order of Table 2. Verifiers MUST NOT depend on member order, whitespace, or string escaping.¶
| Claim | Type | Presence | Meaning |
|---|---|---|---|
| iss | string | REQUIRED | Issuer identifier. |
| jti | string | REQUIRED | Identifier the issuer assigns to the decision. Unique per verdict. |
| iat | integer | REQUIRED | Time of the decision; for an "allow" that approves a review, the time of the approval. |
| exp | integer | REQUIRED | Expiry, on every verdict whatever its code. |
| verdict | string | REQUIRED | Verdict code (Section 4.5). |
| mandate | string | see Section 4.4 | Identifier of the mandate whose evaluation produced the verdict. |
| policy_version | string | REQUIRED | Per-operator label; opaque to verifiers. |
| nonce | string | REQUIRED | Random value generated for each verdict; the replay key (Section 6, step 12). |
| request | object | REQUIRED | The payment request that was evaluated (Section 4.3). |
| supersedes | string | see Section 4.4 | The jti of the "review" verdict that this "allow" resolves. |
In Table 2 and in step 6 of Section 6, "integer" means a JSON number whose value is an integer; the issuer writes such values with no fraction or exponent part. This narrows NumericDate (Section 2 of [RFC7519]), which also allows non-integer values.¶
In the deployed implementation, policy_version is assigned once per operator and is not updated when mandates change; despite its name, it does not identify the rules that were applied. Because mandates are not edited, the mandate claim identifies the limits that were evaluated. Verifiers MUST treat policy_version as an opaque string. The nonce is 64 random bits in the deployed implementation; jti is enforced unique by the issuer.¶
The claims iss, jti, iat, and exp are used with their registered meanings (Section 4.1 of [RFC7519]). The claim nonce is registered in the IANA "JSON Web Token Claims" registry by [OIDC-CORE]; Section 12.7.1 of [RFC9449] updated the registration to state that it may also be used for nonce values in other applications of JWTs, and it is used here in that sense, as a non-repeating value used to detect replay. Unlike the nonce of [OIDC-CORE] and [RFC9449], which is supplied by the party that later checks the token, this nonce is chosen by the issuer, so it gives the relying party no assurance of freshness beyond exp. The claims verdict, mandate, policy_version, request, and supersedes are Private Claim Names (Section 4.3 of [RFC7519]) and can collide with other uses of the same names (Appendix B).¶
Apart from the "sandbox-" prefix of kid (step 3 of Section 6) and the "evt_" prefix of jti (step 6), verifiers SHOULD treat identifiers as opaque strings.¶
Verifiers MUST ignore private claims, and members of request, that they do not understand (Section 8). Registered claims that verdicts do not carry are processed as [RFC7519] specifies: a verdict carrying "aud" is rejected unless the verifier identifies itself with one of its values (Section 4.1.3 of [RFC7519]), and one carrying "nbf" is rejected before that time (Section 4.1.5 of [RFC7519]).¶
| Member | Type | Meaning |
|---|---|---|
| agent | string | Identifier of the agent, unique among one operator's agents in one environment. |
| amount | number | Amount evaluated (Section 4.6). |
| currency | string | Currency or asset code, exactly as the request stated it. When the mandate claim is present, it equals that mandate's currency. |
| recipient | string | Payee identifier, exactly as sent in the request. |
| category | string | Spending category, exactly as sent in the request. |
All five members are REQUIRED.¶
The issuer matches currency against the mandate's currency by exact, case-sensitive comparison, and does not normalize the value or restrict it to a list of codes. The issuer's documentation uses uppercase ISO 4217 codes such as "USD", and "USDC" for an asset that has no ISO 4217 code. Other specifications use other forms, for example lowercase codes and integer minor units in [ACP]; a relying party that holds its payment record in another form converts the currency code to this form, and the amount to the unit agreed with the operator (Section 4.6; Section 6, step 8), before steps 9 and 10 of Section 6; that conversion is part of its own code.¶
The format of recipient is agreed between the operator and the relying party; examples are "vendor:acme_saas" and a wallet address. The issuer does not check it against the identity of any party (Section 10.3).¶
In the deployed implementation's test environment, creating an escrow is also evaluated. Such a verdict carries the escrow's seller as recipient and the fixed category "escrow", and the evaluation skips the category check.¶
| Verdict code | mandate | supersedes |
|---|---|---|
| allow, from an evaluation | present | absent |
| allow, from approving a review | present | present |
| review | present | absent |
| deny_limit, deny_velocity, deny_category, deny_expired, deny_revoked | present | absent |
| deny_no_mandate | absent | absent |
| Code | Meaning |
|---|---|
| allow | The request passed every check in this table, or it was held for review and the operator approved it (Section 10.12). The only code on which a relying party may settle. |
| review | The amount is above the mandate's review threshold, and the operator must approve or deny the request. Not an approval. |
| deny_limit | The amount exceeds the per-transaction limit, or, together with the amounts allowed under the same mandate in the previous 30 days, the rolling 30-day limit. |
| deny_velocity | The agent already had 10 or more decisions in the previous 60 seconds. |
| deny_category | The category is not one of the mandate's allowed categories. |
| deny_expired | The mandate is past its expiry date. |
| deny_revoked | The mandate has been revoked. |
| deny_no_mandate | The agent is not active, holds no mandate, or holds no mandate in the requested currency. |
For each of the agent's mandates in the requested currency, the checks run in this order: revoked, expired, category, per-transaction limit, rolling 30-day limit, frequency rule, review threshold. When the agent holds several such mandates, they are evaluated in order of creation, and the verdict is the first "allow", otherwise the first "review", otherwise the result for the oldest mandate. These checks are the whole of the evaluation; in particular, a mandate does not restrict the recipient (Section 10.3).¶
A relying party MUST treat every code other than "allow", including codes this document does not define, as "do not settle".¶
When the operator approves a review, the issuer issues a new verdict with the code "allow", its own nonce and expiry, and a supersedes claim naming the "review" verdict (Section 10.12). The "review" verdict is not modified. A review that the operator denies, or that expires, produces no verdict.¶
An operator records, per environment, a mode of "observe" or "enforce", which tells its own systems whether to block payments that are not allowed; the issuer blocks nothing in either mode. In the deployed implementation, a production environment is in observe mode until the operator changes it. The issuer evaluates, records, and signs the same verdicts in either mode, and the mode is not in the token, so a signed refusal does not show that a payment was blocked.¶
request.amount is a JSON number in the same units as the mandate's limits. The issuer does not interpret or convert units; its documentation uses major units, so that 340 with currency "USD" means 340 US dollars, not 340 cents. The amount is greater than zero and less than 10^15, and has at most six fractional digits. The issuer writes it in minimal fixed-point form: an integral amount has no decimal point (340, never 340.00), other amounts have no trailing zeros (340.5, never 340.50), and the issuer never uses exponent notation or leading zeros other than the single 0 before the decimal point of an amount below 1 (0.5).¶
Amounts can therefore carry up to 21 significant digits, more than an IEEE 754 binary64 number holds. This departs from Section 2.2 of [RFC7493], which says that I-JSON messages should not include such numbers and recommends JSON strings for them. Many JSON parsers, including those inside common JWT libraries, read every number as binary64, and from 2^33 (about 8.6 x 10^9) upward two amounts that differ in the sixth fractional digit can map to the same binary64 value; 8589934592.000001 and 8589934592.000002 do. A verifier therefore compares amounts by decimal value (Section 6, step 9).¶
Nothing in the token states the unit. An operator that configures a mandate in minor units obtains verdicts whose amounts are in minor units, and a relying party that compares them with a payment record in major units (Section 6, step 9) would match a verdict evaluated for 340 cents against a payment of 340 US dollars. Step 8 of Section 6 therefore includes agreeing the unit with the operator.¶
The issuer keeps information about each decision that is not in the token and is therefore not signed. The decision record it returns to the operator contains the enforcement mode (Section 4.5), a human-readable reason, operator metadata such as order references, and, for a "review" verdict, the review identifier and the principal it waits for. The issuer also stores, but neither signs nor returns in the decision record, the payment protocol named in the request. A verdict is therefore not bound to an order, and a verdict evaluated for one payment protocol can be presented on another.¶
The token has no "aud", "sub", "nbf", or "cnf" claim, and no claim that identifies the operator (Section 10.2, Section 10.4, Section 10.7).¶
The issuer publishes its public keys as a JWK Set (Section 5 of [RFC7517]). Each key has "kty" "EC", "crv" "P-256", "x", "y", "use" "sig", "alg" "ES256", and a "kid". The deployed implementation serves the set over HTTPS with "Cache-Control: public, max-age=300" [RFC9111] and "Access-Control-Allow-Origin: *", with the media type "application/json" rather than "application/jwk-set+json" (Section 8.5 of [RFC7517]).¶
The deployed implementation publishes its JWK Set at the path "/.well-known/jwks.json" under its issuer identifier, https://verdicts.saifuro.com (Section 9), and keeps an older location that permanently redirects to it. The suffix "jwks.json" is widely used but is not registered in the Well-Known URIs registry; serving the set under it departs from Section 3 of [RFC8615], which requires registration, and this document does not register it (Appendix B).¶
A relying party MUST take the list of issuers it accepts, and the JWK Set location for each, from its own configuration, and MUST fetch each set over HTTPS with certificate validation. It MUST NOT fetch keys from a location derived from a token. Otherwise any party can mint verdicts that verify against keys it publishes itself. A verifier SHOULD NOT follow HTTP redirects when fetching a JWK Set, since a redirect moves the source of its keys to a location that is not in its configuration, and SHOULD bound the size of the response and the time it waits for it.¶
The deployed implementation operates a test environment and a production environment. Each has its own signing key, and both keys are published in the same JWK Set. The kid of a test key begins with "sandbox-"; the kid of a production key does not. The issuer identifier, the claim set, and the token shape are otherwise identical. Anyone with access to the test environment can obtain a correctly signed test "allow" for any recipient and amount, so a verifier rejects tokens whose kid begins with "sandbox-" unless it is used only for testing (Section 6, step 3; Section 10.6).¶
Verifiers SHOULD cache each JWK Set and refresh it when it is older than the max-age it was served with. If a refresh fails, a verifier MAY keep using the last copy it fetched successfully, SHOULD raise an alert, and SHOULD stop accepting verdicts once that copy is older than a bound it chooses: the longer it uses a stale copy, the longer a key the issuer has removed stays trusted (Section 10.10).¶
The kid is untrusted input, used only as a lookup key, never in a file path, query, or URL (Section 3.10 of [RFC8725]). On a kid that is absent from the cached set, a verifier fetches the set again once and looks again. Because anyone can send a token with any kid, these refetches MUST be rate-limited; an interval of at least 30 seconds between refetches triggered by unknown kids is RECOMMENDED, and scheduled refreshes SHOULD NOT count against that limit. Because the refetch is rate-limited, a stream of tokens with made-up kids can delay acceptance of a verdict signed with a key the verifier has not yet fetched; pre-publication (Section 5.5) avoids that for verifiers that refresh on schedule.¶
Pinning a single key in code is NOT RECOMMENDED: an integration that does so rejects valid verdicts after the next rotation.¶
The issuer's rotation procedure extends the approach of Section 10.1.1 of [OIDC-CORE], in which a verifier refetches the set on an unfamiliar kid, with a period of pre-publication:¶
The deployed implementation has not yet rotated a key (Section 9), so this procedure has not been exercised.¶
The set carries only keys that are about to be used, in use, or recently retired. A party that keeps verdicts as records, for audit or disputes, SHOULD store a copy of the JWK Set with them, so that the signatures can be checked after the key has been removed (Section 6.2).¶
The inputs to verification are the token; the configured list of accepted issuers with the JWK Set of each; the current time from a synchronized clock; the payment record; a store of nonces from verdicts already accepted; whether the verifier is used only for testing; where step 8 requires them, the mandate identifiers agreed with the operator and the unit its mandates use; and, for the check of Section 10.12, a store of the supersedes values of verdicts already accepted.¶
A relying party MUST apply every step below before it relies on a verdict, and MUST reject the token if any step fails. It MAY perform the steps in a different order, provided that it verifies the signature (step 5) before it relies on any claim, and records the nonce (step 12) only after every other step has passed. Every check MUST use values taken from the payload bytes whose signature step 5 verified. A verifier that parses those bytes more than once, for example with a JWT library for steps 4 and 7 and with a decimal-preserving parser for step 9, MUST reject a header or payload that contains duplicate member names at any depth, so that the parses cannot disagree about which member they read.¶
JWT libraries perform most of steps 1, 2, 4, and 5, and the exp and "nbf" checks of step 7, when they are configured with the algorithm list ["ES256"], the accepted issuer, and a clock tolerance of at most 60 seconds. Some libraries, such as jose, check "aud" only when an audience is configured. Implementers need to check what their library leaves out: a library that is handed a key object cannot check that key's "alg" and "use" members or whether its kid appears twice in the set, and some libraries accept padded or whitespace-broken segments. Steps 3, 6, 8, 9, 10, 11, and 12 are the relying party's own code, and so is the iat check of step 7 where the library does not perform it.¶
For each property of the payment record that the relying party relies on, verification has one of three outcomes:¶
A contradicted property causes rejection, by steps 9 to 11. An unsupported property is neither evidence of a problem nor evidence of correctness: the relying party has to establish it independently. In this format, amount, currency, and recipient can be matched or contradicted. The members request.agent and request.category are signed, but the payment record has no corresponding property, so they can be neither matched nor contradicted. The operator behind the verdict is unsupported unless the relying party maps the mandate claim to an operator through identifiers agreed out of band (Section 10.2); the order, the payment protocol, and the identity of an intermediary relying party are always unsupported. A relying party SHOULD record which properties it took from its own records rather than from the signature, so that a later audit can tell the two apart.¶
A party that checks a verdict as a record rather than to settle a payment, such as an auditor examining a refusal, applies steps 1, 2, 4, 5, and 6 using a copy of the JWK Set that contains the key (Section 5.5), and step 3 if it needs to distinguish test verdicts. It evaluates step 7 against the time at which the verdict was received if that time was recorded, and otherwise does not apply step 7; it does not apply steps 8 to 12. It does not refetch a key set for a stored verdict: a kid missing from the stored copy is a failure. Verdicts that the deployed implementation issued before 09:12 UTC on 9 October 2026 may lack request.currency, may contain Unicode noncharacters, and may be up to about 94,000 characters long; a party checking one of them as a record applies step 6 without the currency requirement, and step 1 with a size bound that admits them. Unless the party obtained the copy of the JWK Set itself, a successful check shows only that the verdict matches the key set that the record keeper stored (Section 10.1).¶
A verdict that passes steps 1 to 6 of Section 6 is a statement, signed by the holder of the signing key, that it evaluated a payment request with the stated agent, amount, currency, recipient, and category against the named mandate (none, for "deny_no_mandate") and reached the stated verdict code. For a verdict without supersedes, iat is the time of that evaluation; for an "allow" that carries supersedes, the evaluation took place when the superseded "review" verdict was issued, some of its checks were repeated at approval, and iat is the time of the operator's approval (Section 10.12). The signature shows that the statement was made, not that it is correct (Section 10.1). In the terms used on the audit mailing list [AUDIT], a verdict is an attestation, not a record that a relying party can recompute: neither the mandate's limits nor the history behind the rolling 30-day limit is in it. A verdict does not show:¶
The token carries no version number, and the format has no mechanism for signalling a change. Verifiers detect features by the presence of claims. An issuer can add private claims and members of request, which verifiers ignore if they do not understand them (Section 4.2), and verdict codes, which verifiers treat as "do not settle" (Section 4.5).¶
Because verifiers ignore private claims they do not understand, a new private claim that narrows when a verdict may be relied on constrains only verifiers updated to check it; until every relying party checks it, a verdict carrying it is accepted where it was not meant to be. Adding "aud" is incompatible in the other direction: verifiers that apply Section 4.1.3 of [RFC7519] without a configured audience reject every verdict that carries it. Adding either kind of claim is therefore an incompatible change, as are removing a claim and changing a claim's type or meaning. Explicit typing alone would not make such a version fail at verifiers built to this document, which do not check typ (Section 6, step 2); a new issuer identifier would (step 4). The deployed implementation announces changes that verifiers have to act on in its documentation [SAIFURO-VERIFY], with the date from which they apply.¶
This section is to be removed before publishing as an RFC.¶
This section records the status of known implementations of the format described in this document at the time of posting of this Internet-Draft, and is based on a proposal described in [RFC7942]. The description of implementations in this section is intended to assist the IETF in its decision processes in progressing drafts to RFCs. Please note that the listing of any individual implementation here does not imply endorsement by the IETF. Furthermore, no effort has been spent to verify the information presented here that was supplied by IETF contributors. This is not intended as, and must not be construed to be, a catalog of available implementations or their features. Readers are advised to note that other implementations may exist.¶
According to [RFC7942], "this will allow reviewers and working groups to assign due consideration to documents that have the benefit of running code, which may serve as evidence of valuable experimentation and feedback that have made the implemented protocols more mature. It is up to the individual working groups to use this information as they see fit".¶
Note to the RFC Editor: please also remove the reference to [RFC7942].¶
The security considerations of [RFC7515], [RFC7519], and [RFC8725] apply. [I-D.ietf-oauth-rfc8725bis], which would obsolete [RFC8725], states that a typ value of "JWT" is not effective explicit typing (Section 3.11 of [I-D.ietf-oauth-rfc8725bis]) and extends the audience requirement to issuers that may issue JWTs for more than one relying party in the future (Section 3.9 of [I-D.ietf-oauth-rfc8725bis]); both widen the departures described in Section 10.6 and Section 10.7.¶
A verdict proves that the issuer made a statement, not that the statement is correct. An issuer that is compromised, misconfigured, or acting in bad faith can sign approvals for requests outside any mandate. How a relying party decides to accept an issuer is outside the scope of this document.¶
The JWK Set is not signed. Whoever can change what is served at its configured location, including through control of the domain, its DNS, certificate issuance, or a TLS-terminating intermediary such as a content delivery network in front of the server, can add a key and mint verdicts that every verifier accepts. Protecting that endpoint is equivalent to protecting the signing keys. A copy of the JWK Set kept with stored verdicts (Section 5.5) is the relying party's own record; it is not evidence to a third party that a key belonged to the issuer, because the deployed implementation neither signs its JWK Set nor publishes a history of retired keys.¶
A verdict is evidence about one operator's policy, not about the payer. The token names no operator, and any operator that the issuer serves can configure a mandate that allows any recipient and amount, and then obtain a valid "allow" under it. A relying party that needs to know which operator stands behind a payment agrees the mandate identifiers with that operator out of band and accepts only verdicts whose mandate claim is one of them (Section 6, step 8). In the deployed implementation, mandate identifiers are assigned by the issuer and are not reused.¶
The policy_version claim is the same on every verdict of one operator and is shared only with operators whose accounts were created on the same day. It links verdicts to an operator without reliably identifying one, and relying parties MUST NOT use it as an operator identifier.¶
The member request.agent is unique only among one operator's agents in one environment, and in the deployed implementation it is "agt_" followed by a name the operator chooses, so any operator can obtain verdicts that name the same agent identifier as another operator. Relying parties MUST NOT use request.agent to identify an operator or a payer; step 8 of Section 6 uses the mandate claim for that.¶
Mandates in the deployed implementation do not restrict recipients. An agent that is compromised or misled, or anyone holding its credentials, can obtain an "allow" for a recipient of its choosing within the mandate's limits, and a relying party controlled by that recipient finds that every step of Section 6 passes. Verdicts therefore do not defend against payee substitution; an operator that needs that defence applies its own controls to the recipients its agents request. Restricting recipients in mandates is an open issue (Appendix B).¶
The issuer accepts as recipient (up to 1,024 characters) or category (up to 128) any non-empty string of Unicode characters other than noncharacters, including characters that render invisibly, reorder text, or resemble other characters, and an operator approving a review is shown the strings as the agent supplied them. An approval can therefore be obtained for a recipient that looks like another. Step 11 of Section 6 does not help: it compares exactly, and the recipient that benefits is the one the token names.¶
A verdict is a bearer token. It is not bound to a key held by the agent or the operator, it has no "cnf" claim [RFC7800], and nothing in it identifies the party presenting it, the payer, or the order. Anyone who obtains an unused "allow" before it expires, such as the agent's runtime, an intermediary between the agent and the relying party, anyone who can read a log that recorded it, or a receiver of an event token that carries it (Section 10.14), can present it first, with a payment of the same amount and currency to the same recipient; the legitimate presentation then fails step 12 of Section 6. A relying party that releases goods, services, or funds on a verdict has to bind the order to the payer by its own means. Verdicts SHOULD be carried only over authenticated, confidential channels. Agents, operators, and relying parties SHOULD NOT log unexpired "allow" verdicts in full; jti or nonce identifies a verdict in a log without making it presentable. Proof of possession is an open issue (Appendix B).¶
Only ES256 is used and accepted. Verifiers pin it in configuration and reject "none", every HMAC algorithm, including when a public key is offered as the HMAC secret, and any spelling that differs in case (Section 4.1.1 of [RFC7515], Section 2.1 of [RFC8725], Section 3.1 of [RFC8725]). Every key in the set is published with "alg" "ES256".¶
Section 3.2 of [RFC8725] says that JWT libraries should implement ECDSA using the deterministic approach of [RFC6979]. The deployed issuer does not: it signs through OpenSSL 3.0, which derives the per-signature value from fresh random bytes mixed with the private key and the message digest. Its signatures are therefore not deterministic and do not reproduce the test vectors of [RFC6979]; the mixing is intended to keep a failure of the random number generator from exposing the key. Verifiers are unaffected: deterministic and randomized signatures verify identically.¶
Verdicts and event tokens share the key, the issuer identifier, and the typ value "JWT", so Section 2.8 of [RFC8725] requires a mitigation. This format uses the second strategy listed in Section 3.12 of [RFC8725], different required claims: a verdict requires exp, verdict, nonce, and request, which event tokens lack, and step 6 of Section 6 rejects tokens that carry a type claim or an "evt_" jti. That step is the mutually exclusive validation rule that Section 3.12 of [RFC8725] requires for verdict verifiers; it rests on claims rather than on the header. Event receivers that require type and data, which verdicts lack, complete the mutual exclusion.¶
The format does not use explicit typing, which Section 3.11 of [RFC8725] recommends for new kinds of JWT. With explicit typing, the distinction would rest on the header rather than on each verifier implementing step 6. Adopting it is an open issue (Appendix B).¶
Test and production verdicts are the same kind of JWT from different environments, and differ only in the signing key. The only rule that excludes test verdicts is step 3 of Section 6, which reads a prefix of kid. JOSE leaves the structure of kid unspecified (Section 4.1.4 of [RFC7515]), so no generic JWT library applies this rule, and a relying party that omits step 3 accepts test verdicts. A verifier used only for testing does not reject production verdicts. A separate issuer identifier and JWK Set for test verdicts is an open issue (Appendix B).¶
Verdicts have no "aud" claim. Section 3.9 of [RFC8725] requires one when an issuer issues JWTs intended for more than one relying party, which is the case here; this format does not meet that requirement. The same section requires a relying party to reject a JWT that has no audience value; a relying party that follows Section 6 accepts verdicts without one, and so departs from that requirement as well. The member request.recipient is not an audience in the sense of Section 4.1.3 of [RFC7519]: it names the payee, which the agent chooses and the issuer does not check. When the relying party is the payee, step 11 of Section 6 rejects a verdict presented for any other payee. When the relying party is an intermediary acting for the payee, such as a payment service provider or an escrow release process, nothing in the token names it, and two intermediaries settling for the same payee run separate nonce stores and could each accept the same verdict within its lifetime. A relying party SHOULD make sure that the identifier it compares against is specific to it and not shared with other parties. Adding an audience claim is an open issue (Appendix B).¶
A verdict is valid for 300 seconds, and a relying party accepts its nonce only once (Section 6, step 12); relying parties that do not share a nonce store can each accept it (Section 10.7).¶
ECDSA signatures are malleable: if (R, S) is a valid signature, so is (R, n - S), where n is the order of the P-256 group. A verifier whose base64url decoder ignores unused trailing bits also accepts several spellings of the same signature segment. The same signed claims can therefore appear in more than one token string, and verifiers MUST NOT use the token string, or a hash of it, as the replay key.¶
A verifier that recorded the nonce before comparing the verdict with its payment record would let anyone with a copy of a verdict burn it by presenting a mismatched payment; this is why step 12 comes last.¶
The deployed implementation supports idempotent retries: an operator that retries an authorization request with the same Idempotency-Key, the same request body, and the same API credential within 24 hours receives the stored response, with the identical token and nonce, so a retry does not produce a second approval. A retry after 24 hours is a new evaluation with a new nonce, and each "allow" counts toward the mandate's rolling 30-day limit.¶
With a 300-second lifetime and a 60-second skew allowance, a verifier accepts a verdict until its own clock reads iat + 360, and rejects an iat more than 60 seconds ahead of its clock. Step 7 of Section 6 assumes a synchronized clock.¶
There is no per-token revocation. The short lifetime of a verdict bounds the use of a stolen verdict, and removal from the JWK Set is the response to a stolen key. A verifier that refreshes on schedule stops trusting a removed key within the max-age of the set; one that pinned the key, or that keeps using a stale copy, keeps trusting it for as long as it does so. A party holding a compromised signing key can mint verdicts with current timestamps until the key is removed, and after a compromise, verdicts signed with that key, including those kept as records, can no longer be told apart from forgeries. How the issuer protects its signing keys is outside the scope of this document.¶
A verifier that cannot obtain a key for a verdict MUST reject the verdict rather than accept it unverified.¶
JSON leaves numeric range and precision to implementations, and Section 6 of [RFC8259] notes that good interoperability can be achieved by implementations that expect no more precision or range than binary64 provides; this is why amounts are compared by decimal value (Section 4.6; Section 6, step 9). Step 1 of Section 6 bounds the size of tokens; the deployed implementation limits recipient to 1,024 characters and category and currency to 128 characters each, so the verdicts it has issued since 09:12 UTC on 9 October 2026 are shorter than 11,000 characters. Its earlier verdicts could reach about 94,000 characters. Event tokens (Section 10.14) are not bounded in this way: an event token that carries a decision also carries that decision's verdict and is longer than it.¶
An "allow" that carries supersedes records an operator's approval of a request held for review. The format does not state whether the mandate was evaluated again at approval time, and nothing in the token prevents an issuer from issuing more than one "allow" that supersedes the same "review" verdict; such verdicts have different nonces, so the replay check in Section 6 does not detect them. A relying party that must not settle against a revoked or expired mandate needs to learn of revocations from the operator. A relying party SHOULD record the supersedes value of each "allow" it accepts, keep it at least for the review expiry plus the verdict lifetime and skew allowance (24 hours and 360 seconds in the deployed implementation), and reject a second "allow" that supersedes the same verdict.¶
In the deployed implementation, approving a review repeats three checks whose outcome can have changed since the review: that the agent is active, that the mandate is neither revoked nor expired, and that the amount, added to every "allow" issued under the mandate in the 30 days before the approval, is within the rolling 30-day limit. It does not repeat the frequency rule; the category and the per-transaction limit cannot have changed, because mandates are not edited. An approval that fails these checks is refused and issues no verdict, and a review yields at most one "allow". Any holder of an operator-wide credential can approve a review; the principal named on the mandate is recorded but not authenticated.¶
A verdict records the outcome of one evaluation. Whether an issuer enforces aggregate limits, such as the rolling 30-day limit and the frequency rule, across concurrent evaluations is a property of the issuer and is not visible in the token. The deployed implementation reads the limits and records each decision in a single database transaction.¶
The deployed implementation notifies operators of events, such as a pending or resolved review, by webhook. Each event is a JWT with the same header, signed with the same key as the verdicts of its environment, under the same iss. Its claims are iss, jti (beginning "evt_"), iat, type, and data; it has no exp and no audience, and the issuer adds no claim that names the operator; only a "ping" event names the endpoint, by the webhook identifier in its data. A "decision.resolved" event carries, inside data, either the decision that approves a review, whose verdict member is "allow", together with that decision's verdict token, or the review object of a review that was denied or expired. A failed delivery is retried with the identical token; the last retry falls due 24 hours after the event is created.¶
Because all operators' events in one environment are signed with the same key under the same iss, a genuine event issued to one operator also verifies at another operator's endpoint, at any later time: any operator served by the issuer can forward a "decision.resolved" event from its own endpoint, carrying an "allow" for a recipient it chose, to another operator's endpoint. Signature checks and deduplication on jti do not stop this. A receiver therefore confirms, through its own authenticated access to the issuer or against identifiers from its own responses, that an object named in an event is its own before acting on it, and bounds how old an event it accepts. A verdict verifier rejects event tokens by step 6 of Section 6 and MUST NOT accept one as a verdict.¶
Verdicts are signed, not encrypted. Anyone who sees a token can read the agent identifier, which in the deployed implementation contains a name the operator chose for the agent; the amount, currency, recipient, and category; the mandate identifier; the time of the decision; and policy_version, which in the deployed implementation contains the date on which the operator's account was created.¶
A token does not contain the operator's name or identifier, the end user's identity, payment credentials, the operator's metadata, or the reason text. The agent and mandate identifiers are stable across verdicts, and policy_version is the same for all of an operator's agents, so relying parties that compare the tokens they received can link payments made by the same agent, under the same mandate, or by different agents of the same operator.¶
The token travels through the agent and any intermediary between the agent and the relying party. Operators SHOULD NOT put personal data in category, or in recipient beyond what identifies the payee; when the payee is a natural person, the recipient identifier is personal data and is visible to every party that handles the token. Relying parties SHOULD protect retained verdicts as they protect the payment records to which the verdicts relate.¶
This document has no IANA actions.¶
A later version might request registration of the private claim names in Section 4.2, of a media type for explicit typing, and of a well-known URI suffix for the JWK Set (Appendix B).¶
This appendix contains a verdict signed with an example key. The private key is published here so that anyone can reproduce and extend the example. It is not a key of any issuer, a token signed with it proves nothing, and it MUST NOT be added to a verifier that protects real payments. The issuer identifier is a reserved example domain [RFC2606].¶
{
"kty": "EC",
"crv": "P-256",
"x": "50tYIab3ytEcPnb_fuZCPc4cM87aNhpYhOYb9LmSseA",
"y": "iZho4NBfqRZnM7AgedlHkvEMRU030NPOYGoTv7s2Ls0",
"d": "34KI9Rx04_gEp_Ljm4-dGFq7O75vEFednnvRwcDdvUc",
"use": "sig",
"alg": "ES256",
"kid": "example-1"
}
¶
The token, with line breaks for display purposes only:¶
eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImV4YW1wbGUtMSJ9.eyJ pc3MiOiJodHRwczovL2lzc3Vlci5leGFtcGxlIiwianRpIjoiZGVjXzViMmU5YzQ xIiwiaWF0IjoxNzkxMzc0NDAwLCJleHAiOjE3OTEzNzQ3MDAsInZlcmRpY3QiOiJ hbGxvdyIsIm1hbmRhdGUiOiJtbmRfN2YzYTBkMTIiLCJwb2xpY3lfdmVyc2lvbiI 6InBvbF8yMDI2LTA5LTAyLjEiLCJub25jZSI6Im5fOWQwMmM2ZTQxYTdmM2I4NSI sInJlcXVlc3QiOnsiYWdlbnQiOiJhZ3RfcHJvY3VyZW1lbnRfMDEiLCJhbW91bnQ iOjM0MCwiY3VycmVuY3kiOiJVU0QiLCJyZWNpcGllbnQiOiJ2ZW5kb3I6YWNtZV9 zYWFzIiwiY2F0ZWdvcnkiOiJzYWFzIn19.5NLM85wfRPdBIBaW_fY_gmgfml5w-0 KZlVbDj0YUVxa03wiYmqXamfLynH_S_DUkk2j_HTAlorcS3tHdtNvwYA¶
Decoded JOSE Header:¶
{"alg":"ES256","typ":"JWT","kid":"example-1"}
¶
Decoded claims, with whitespace added:¶
{
"iss": "https://issuer.example",
"jti": "dec_5b2e9c41",
"iat": 1791374400,
"exp": 1791374700,
"verdict": "allow",
"mandate": "mnd_7f3a0d12",
"policy_version": "pol_2026-09-02.1",
"nonce": "n_9d02c6e41a7f3b85",
"request": {
"agent": "agt_procurement_01",
"amount": 340,
"currency": "USD",
"recipient": "vendor:acme_saas",
"category": "saas"
}
}
¶
The verdict was issued at 2026-10-07T12:00:00Z and expired at 12:05:00Z. A verifier configured with the example key, the issuer identifier https://issuer.example, and a payment record of 340.00 USD to "vendor:acme_saas", with its clock pinned to 2026-10-07T12:02:00Z (Unix time 1791374520), accepts it; for step 8, the mandate identifier agreed with the operator is "mnd_7f3a0d12" and the agreed unit is major units (US dollars). With the clock unpinned, it rejects the token at step 7 of Section 6, as intended. A production verifier never pins its clock.¶
The S component of the example signature is in the upper half of the group order, so a verifier that enforces low-S signatures, contrary to step 5, rejects this example.¶
The author thanks Nicholas Templeman, whose messages on the audit mailing list [AUDIT] set out the distinction between an unsupported association and a contradicted one, which Section 6.1 adopts, and the binding problem that Section 10.7 describes.¶
The author also thanks Nancy Sahu and Gareth Wong, whose proposal on the same list, that the charter state, for each verification mechanism, what a successful check establishes, under what assumptions, and whether an omitted record would be detectable, poses the question that Section 7 answers for this format.¶
The text of this document was drafted with an AI system (Claude, by Anthropic) from the implementation's source code and documentation and from the cited specifications, and was checked against them in further AI-assisted reviews. The author has reviewed the whole document and is responsible for its content.¶