Internet-Draft The Pulse Protocol September 2026
Petkov & Panitch Expires 21 March 2027 [Page]
Workgroup:
Individual Submission
Internet-Draft:
draft-effortel-pulse-00
Published:
Intended Status:
Standards Track
Expires:
Authors:
M. Petkov
Effortel Technologies SA
A. Panitch
Effortel Technologies SA

Pulse: Real-Time Online Charging for AI Services

Abstract

This document specifies Pulse, version 1.1 — the AI online charging protocol: the interface between an AI Gateway (the client) and the Charging Server (the server) by which AI service usage is authorised in real time, supervised under a granted budget, and settled. The spend guarantee is precisely stated: settled spend never exceeds the reported meter or the plan's ceiling, and serving exposure is bounded by the granted pool plus the completion of Calls already in flight when a stop lands. The protocol consists of two request/response operations over HTTPS with JSON bodies: Authorise and Report. It follows the reserve-then-settle discipline of telecom online charging (cf. Diameter Credit-Control, RFC 4006) applied to AI workloads.

Status of This Memo

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 21 March 2027.

Table of Contents

1. Introduction

1.1. Motivation

Providers and resellers of mass-market AI services cannot today guarantee that an individual subscriber will not overspend, nor apply a hard per-subscriber budget. Pulse provides that guarantee the way telecom charging does: no chargeable work proceeds without a prior authorisation carrying a budget, and all consumption is reported back and settled against it.

1.2. Relationship to Diameter Credit-Control

Pulse is not a Diameter application, but deliberately reuses proven Diameter Credit-Control concepts from [RFC4006]: session-based credit control with granted quota (here: the grant), reporting of used units, and credit pooling — a single abstract unit pool with per-unit-type multipliers (Section 6.3), mirroring the Diameter G-S-U-Pool-Reference mechanism. [RFC8506], which obsoletes [RFC4006], is the current specification. The name reflects the protocol's own rhythm — supervision happens on a regular pulse of reports (heartbeat reports, reporting cadence, settlement cycles).

1.3. Scope

This memo specifies the charging plane only: the two operations, their semantics, timing, idempotency, errors, and security requirements. The management plane (tenant onboarding, plan authoring, subscriber provisioning, credential administration, reporting) is a separate surface and is out of scope here.

1.4. Version-1 scope and deferred items (named)

Version 1 targets bounded synchronous and streaming Actions operated against one logical charging domain per tenant; an Action MUST be operated against the domain that opened it, and HTTP redirection (3xx) is not used. Quiet periods inside an Action are covered by heartbeat reports (Section 7.1). The following are deliberately deferred, not accidentally missing — each is named so an implementer never mistakes silence for support:

  • Batch / asynchronous work (a result returning hours later) has no contract in version 1.

  • Cross-region failover semantics (retrying against a different charging domain).

  • Pre-grants / bulk authorisation for latency amortisation (Section 13.5 states version 1's posture).

  • Multi-Action batch reporting / multiplexed transport (Section 13.5).

  • An interoperable call-signature profile (Section 7.2 — the field is reserved).

  • The unit-vocabulary catalogue contract (distribution of Action-Type unit definitions is a management-plane artefact; version 1 assumes out-of-band onboarding).

  • Serving-system provenance (which model or system actually served an Action — a self-attested, evidence-only telemetry block on reports). No version-1 contract; its design belongs with the reference-model phase, alongside the evidence-grade question any self-attested provenance raises.

  • A single owner's cap across several of its own operators — one company operating multiple tenants under a jurisdiction-mandated joint bound (the shape of, e.g., German gambling regulation). In version 1 a relation-scoped bound ranges within one tenant (Section 12.1); a cross-tenant bound has no contract.

2. Conventions and Terminology

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.

Table 1
Term Meaning
Charging Server ("the server") The OCS instance implementing this protocol.
Gateway ("the client") The authenticated system in the AI usage path that requests authorisation and reports consumption.
Tenant The commercial customer of the platform; every credential is bound to one or more tenants.
Subscriber An end user provisioned under a tenant, identified by accountRef.
Action The chargeable unit of work (e.g. one agent task, one conversation exchange), identified by a client-asserted actionId. An Action contains one or more Calls.
Call One inference invocation within an Action, identified by callId.
Grant The server's authorisation to consume up to a stated quantity of pooled units before reporting back, valid until expiresAt. Exactly one grant is current per live Action; grants carry a strictly increasing generation number (grantSeq, +1 per mint — the ending itself mints the final generation, Section 3.5), and a client discards any grant older than one it has already seen.
Envelope The always-fresh part of a response — serverTime and maxSchemaVersion on every response (error envelopes included); actionLive, currentGrantSeq, currentGrantId, and per-attempt unattributable on every 200 that identifies an opened Action (defined-absent on refusals of unopened Actions and on error envelopes). Computed at each processing, replays included; the client MUST obey the envelope over a replayed body. actionLive is monotonic — once false, never true again — and scoped precisely: false forbids lifecycle serving under any grant; a flat Action's work is authorised by its settled GRANT itself (Section 3.3), which is a record, not a serving grant subject to the stale-discard rule.
Cycle One Report exchange: the server settles the reported consumption and, if the Action continues, issues the next grant.
Action Type The declared, catalogue-defined kind of Action; it selects the price entry (the "law") the Action is rated under.
Ending The terminal transition of an Action: CLOSE (client-declared final report), STOP (server-directed stop), or LAPSE (grant validity lapsed without a report).
Late window The period after an ending — until max(endedAt + 24 h, expiresAt + 1 h) (Section 3.5, Section 7.6) — during which the server retains the Action's terminal state, honours retries, and can still settle late-arriving consumption.
Sponsor cap An aggregate spend bound a sponsoring party declares across a whole population of subscribers (e.g. a reseller capping the total cost of a bundled AI benefit). Enforced at authorisation, attributed to the sponsor in refusals (Section 12.1).
Capacity pool A tenant-declared provisioned serving rate that Actions of given types draw on. Finite and perishable: what is not admitted now is gone. Enforced at admission (Section 12.2).
Meter The server's stored per-call cumulative high-water totals for an Action — the authoritative record of reported consumption. Settlement never exceeds the meter.
Ceiling / headroom The ceiling is the maximum total charge an Action (or Action family, Section 9) may accumulate under its plan; headroom is what remains of it.
Minimum rate The plan-declared serve rate below which degradation (Section 12.3) is not attempted and the honest answer is NO_CAPACITY.
Integration profile The tested record of what a given gateway integration actually does (authorise-before-serving, stop obedience, report promptness, …), established by the conformance suite (Section 12.4).

All normative statements about server behaviour ("the server MUST …") are guarantees a client may rely on; statements about client behaviour are conformance requirements for gateways.

3. Protocol Overview

3.1. Message flow

Gateway                                   Charging Server
   |                                             |
   |--- POST /ocs/ai/v1/authorizations --------->|   Authorise: may
   |<-- GRANT {grantId, grantedUnits,            |   this Action
   |           reportAt, expiresAt, unitWeights} |   proceed, and on
   |                                             |   what budget?
   |    (gateway serves AI calls, metering       |
   |     consumption against the granted pool)   |
   |                                             |
   |--- POST /ocs/ai/v1/grants/{id}/reports ---->|   Report: usage
   |<-- CONTINUE {next grant}   or               |   so far. Settle
   |    STOP {stopReason}       or               |   and re-grant,
   |    SETTLED                                  |   stop, or settle
   |                                             |   finally.
   |    ... repeat cycles until an ending ...    |

An Action opens with exactly one successful Authorise and ends exactly once (CLOSE, STOP, or LAPSE). Between opening and ending, the gateway reports at the server-stated thresholds and the server answers each report with the next directive.

3.2. The two-message contract

The protocol is deliberately two messages and no more:

  1. Authorise — the client asks; the server answers GRANT or REFUSE. A refusal means no chargeable work may proceed; a grant means work up to the granted pool may proceed.

  2. Report — the client reports cumulative consumption; the server settles and answers CONTINUE (with the next grant), STOP (a stop obligation: the client MUST cease serving the Action's traffic promptly), or SETTLED (final settlement acknowledged).

There is no server-initiated push channel in version 1. The stop obligation is delivered on the response to a report; grant lapse (expiresAt) bounds how long an unreported Action can run.

3.3. Flat-priced Actions

An Action whose type is flat-priced settles at open: the Authorise response carries decision: GRANT with settled: true and a grantId for correlation, and no reports are expected. A flat Action ends at authorisation — its ending is CLOSE, its endedAt is the response's serverTime, and its late window anchors there. Its envelope therefore carries actionLive: false with terminal generation 2 — which is correct and does not countermand the work: the settled GRANT itself is the complete authorisation for the flat Action's work (Section 2, Envelope). The opening settlement carries a receipt of settlementId alone (no units existed to accept). A Report addressed to its grantId never settles — an identity match replays; anything else receives the terminal directive, with unattributable: true if units were carried (Section 3.5). The report-borne controls (call limits, runtime, signatures, inflation) do not apply to flat Actions by construction — their controls are the authorisation-time checks (Section 6.2). All other price forms follow the report lifecycle.

3.4. Division of responsibility

  • The client admits Calls; the server settles money. The client MUST NOT knowingly serve consumption beyond the granted pool (checking each next Call against the remaining pool, which it can compute from grantedUnits and unitWeights, is the expected mechanism) and MUST report when the reportAt threshold is crossed. The server clamps every settlement at the reported meter and at the Action's ceiling; overshoot exposure is bounded to one final report. The permitted tail: consumption whose serving began before the report was dispatched may complete under the old grant and is settled on the next cycle.

  • The server's clock is authoritative for lapse boundaries and time-band selection. A client MUST NOT assume its own clock decides either.

  • One in-flight Report per Action. The client MUST serialise its Reports: at most one Report (including its retries) is outstanding per Action at any time, and while a Report is outstanding the client MUST NOT admit new Calls — work already begun (the tail, first bullet above) completes. The server's convergence rules (Section 3.5) remain the safety net for the distributed-gateway case where two workers overlap despite this rule.

3.5. The Action state machine (normative)

This section is the authority for every exchange; the prose of Section 6 and Section 7 restates it and never overrides it.

The gates — evaluated before the table, in this order, each state-neutral (Section 13.1) and carrying no Action-state envelope: transport (wrong method → 405, wrong content type → 415) → authentication (401) → gateway ownership (403 — including a parentActionId owned by another gateway, Section 9) → rate limiting (429) → parse and validation (400 — Section 4.3, Section 4.4, Section 5) → identity resolution (404 — an unknown actionId or grantId, or a path grantId that does not belong to the body's Action). Only a request that passes every gate reaches the table.

Row evaluation: within a state, rows are evaluated in the listed order and the first matching row applies — the table is a function from (state, input) to one outcome.

States: NONE (the actionId is unclaimed) · LIVE(Gn) (open; one current grant, generation n) · ENDED(e) (ended as e ∈ {CLOSE, STOP, LAPSE}; recognised) · FORGOTTEN. A flat-priced Action passes through LIVE for zero time: a successful flat Authorise opens and ends it (ENDED(CLOSE)) in one transition. Generations: every stored outcome mints a generation (grantSeq/outcome generation, strictly +1) — cycles, the terminal transition, and late adjustments alike — so envelope ordering (Section 6.3) and receipt ordering (Section 7.3) are total, and terminal state is sticky (actionLive never returns to true).

In state NONE (a Report cannot reach NONE — the identity gate returns 404):

Table 2
# Input Outcome Next state
1 Authorise, admissible GRANT (lifecycle: G1; flat: settled + endedAt) LIVE(G1); flat: ENDED(CLOSE)
2 Authorise, refused 200 REFUSE + reason (no identity retained) NONE

In state LIVE(Gn):

Table 3
# Input Outcome Next state
1 Authorise, identity = opening identity re-presentation of Gn + its baseline receipt LIVE(Gn)
2 Authorise, other identity 200 REFUSE / DUPLICATE_ACTION LIVE(Gn)
3 Report whose identity matches a retained outcome that stored outcome replayed + fresh envelope LIVE(Gn)
4 Report, final: true (addressed to any grant of this Action) the closing cycle against the current state: merge → settle the full unsettled meter delta → SETTLED + receipt (a close mints no serving grant; the outcome mints its generation) ENDED(CLOSE)
5 Report addressed to the current grant Gn (calls[] MAY be empty — a heartbeat) the fresh cycle (the primary operation): merge → settle the full unsettled meter delta → evaluate ending conditions in precedence order (Section 7.4): none hold → CONTINUE(Gn+1); one holds → STOP(reason) + receipt LIVE(Gn+1) · ENDED(STOP)
6 Report addressed to a superseded grant (non-final, no identity match) meters max-merge; then ending conditions are evaluated (one raised by the merge ends the Action per precedence, settling at the meter → STOP path); none hold → CURRENT: the standing grant Gn re-presented + baselineCumulative (Gn's cycle baseline) + meterCumulative (the post-merge meter). No settlement, no mint. LIVE(Gn) · ENDED(STOP)
7 Server event: grace-end without a report (now ≥ expiresAt + 5 s), parent cascade, administrative stop settle at the current meter, then the terminal transition (mints) ENDED(LAPSE or STOP)

In state ENDED(e) (while recognised):

Table 4
# Input Outcome Next state
1 Authorise, identity = opening identity terminal acknowledgement (decision: GRANT, settled where applicable, ending, endedAt, final receipt) ENDED(e)
2 Authorise, other identity 200 REFUSE / DUPLICATE_ACTION ENDED(e)
3 Report whose identity matches a retained outcome that stored outcome replayed verbatim + fresh envelope ENDED(e)
4 Report to a flat Action terminal directive (SETTLED) — never settles; unattributable: true if units were carried (recorded, never charged) ENDED(e)
5 Report, merge adds new consumption, settleable late adjustment: terminal directive + adjustment: true + new settlementId + receipt (mints its outcome generation) ENDED(e)
6 Report, merge adds new consumption, not settleable terminal directive + unattributable: true, no settlementId; the unsettled range marks the Action — every later zero-merge response also carries unattributable: true until it settles or recognition lapses ENDED(e)
7 Report, merge adds zero terminal directive + current receipt (no adjustment; sticky unattributable where marked) ENDED(e)

The terminal directive applies to every ENDED-state response except row 3's verbatim replays, and is a pure function of the ending: CLOSE → SETTLED · STOP → STOP with the original stopReason (and limitOwner where it applied) · LAPSE → STOP (LAPSED) — always with endedAt. The ENDED envelope: currentGrantSeq = the latest outcome generation; currentGrantId = the last issued grant's id (the address late reports use). A flat Action: generation 2 and the settled grant's id.

Arbitration: processing is serialised; the applicable rules are those of the state at processing time. The "final beats stop" precedence (Section 7.5) applies to conditions evaluated within one report's processing — never to transitions that already committed: a final report processed after an ending is late handling under the terminal directive.

Retention (uniform): the terminal outcome plus the last 4 non-terminal outcomes (cycles and late adjustments share the ring), each carrying its generation. A retry whose outcome has aged out takes its state's no-match row — safe by construction (Section 7.2's absorption), though its original settlementId is then no longer re-presentable (Section 7.6). Recognition (duplicate detection and the terminal directive) is unconditional until max(endedAt + 24 h, expiresAt + 1 h) — an unseen asynchronous stop can never outlive the window of the grant that hid it.

When recognition ends: ENDED → FORGOTTEN — the identity is discarded. Reports → 404, unattributable (Section 7.6); an Authorise on the id is evaluated fresh (this is the documented reopen residual of Section 6.5).

4. Transport and Encoding

4.1. Endpoints

Table 5
Operation Method and path
Authorise POST /ocs/ai/v1/authorizations
Report POST /ocs/ai/v1/grants/{grantId}/reports

Requests and responses are application/json bodies, in the JSON format of [RFC8259], carried over HTTP [RFC9110] on TLS. TLS 1.2 [RFC5246] or later is REQUIRED (TLS 1.3 [RFC8446] RECOMMENDED; configuration per [RFC9325]); plaintext HTTP MUST be refused. A non-POST method on these paths → 405; a non-JSON content type → 415. These two paths are the only public charging surface.

4.2. Authentication

Every request MUST carry a per-gateway bearer API key (Authorization: Bearer …). Keys are issued per gateway, support dual-active rotation, and are revocable with effect within seconds. Mutual TLS is a deployment option. OAuth2 client-credentials is planned for a later protocol version and is not part of version 1.

4.3. Encoding rules and JSON types (normative)

Every field has exactly one JSON type; a value of the wrong type is malformed (MALFORMED_REQUEST / MALFORMED_REPORT):

  • Decimal quantities — unit quantities, estimate values, grantedUnits, reportAt values, unitWeights, quantumUnits, minRate, and every other decimal — are JSON strings in canonical decimal rendering: plain notation, no exponent, no trailing zeros, no trailing decimal point, no leading zeros (a bare 0 integer part is the only zero prefix), no leading +, no -0, non-negative unless stated otherwise, scale ≤ 6. Exactly one spelling of every value is valid; any other spelling is malformed — there is no equivalence class of spellings anywhere in this protocol. (JSON numbers are IEEE-754 doubles and cannot carry scale-6 decimals faithfully; single-spelling strings also make canonicalization exact, Section 6.5.) Money never appears on this wire (Section 15.7).

  • Identifiers (actionId, callId, grantId, currentGrantId, parentActionId, tenantRef, accountRef, actionType, scope, timeBandId, settlementId): JSON strings, ≤ 128 characters from [A-Za-z0-9._:-].

  • Display labels (limitOwner): UTF-8 JSON strings, ≤ 64 characters of printable text — no control or bidirectional-control characters; clients render them as plain text, never as markup. Presentation text declared with the relation, not an identifier.

  • Unit-type names (map keys in estimate, units, unitWeights, receipt maps): strings matching [a-z0-9_]{1,64}.

  • Timestamps (expiresAt, serverTime, grantIssuedAt, endedAt, gatewayTime): JSON strings in the [RFC3339] profile YYYY-MM-DDThh:mm:ss.sssZ — uppercase T and Z, exactly three fractional digits, years 1970–9999, no leap seconds.

  • Counts and versions (schemaVersion, maxSchemaVersion, grantSeq, currentGrantSeq, lawVersion, retryAfter, maxCallsPerAction, maxRuntimeSec, maxConcurrentActions, enforcementWindowSec): JSON integers ≥ 0 and < 2⁵³.

  • Numeric domains (closed under the arithmetic): request quantities lie in [0, 10^9] and weights in [0, 10^6], scale ≤ 6; derived pooled quantities (grantedUnits, reportAt, quantumUnits, cyclePooledUnits, baselineCumulative/meterCumulative pooled values) have their own domain [0, 10¹⁷), scale ≤ 6 — closed under 16 unit types × 10⁹ × 10⁶. Response maps are bounded by the 16-unit-type limit; acceptedCalls by the 256-entry limit. The 10⁹ per-unit-per-Call cap is a hard operational bound: a gateway MUST terminate or segment work before any Call's cumulative value reaches it — it is not merely a parser limit.

  • Flags (final, settled, retryable, rateReduced, adjustment, clamped, actionLive, unattributable, signaturesRequired): JSON booleans.

  • Maps: JSON objects whose keys are unit-type names and whose values are decimal strings as above.

  • Duplicate JSON keys anywhere in a body are malformed.

4.4. Limits (normative)

Table 6
Limit Value On violation
Request body size ≤ 256 KB 400 LIMIT_EXCEEDED
calls[] entries per report ≤ 256 400 LIMIT_EXCEEDED
Unit-map entries per Call ≤ 16 400 LIMIT_EXCEEDED
Per-call unit quantity ≤ 10⁹ 400 LIMIT_EXCEEDED
Cumulative per-unit-type total per Action ≤ 10⁹ 400 LIMIT_EXCEEDED

5. Schema Versioning

Every request MUST carry schemaVersion (integer ≥ 1); every response echoes it and additionally carries maxSchemaVersion. Two version layers with distinct roles: the URL path (/v1/) changes only for incompatible protocol generations; schemaVersion evolves compatibly within a generation, and a server MUST support every schema version from 1 through its maxSchemaVersion within the path generation it serves.

Protocol versions are cited major.minor. The major is the path generation (/v1/); the minor identifies one compatible addition set within it, and each minor version binds to exactly one wire schemaVersion. The wire carries only the integer — the dotted form is the human citation, and "version 1" unqualified means the generation.

Table 7
Protocol version Wire schemaVersion Surface
1.0 1 The base contract (the draft-effortel-circ-05/-06 surface).
1.1 2 Adds the GROUP_VELOCITY refusal reason, the refusal fields automatedDecision and reviewContact (Section 6.4), and the per-call startedAt evidence field (Section 7.2).

Fields versus values under version skew. A new field never crosses a version boundary: a client MUST NOT send a field beyond its declared schemaVersion (strict parse rejects it), and a server sends only the fields of the echoed version — a version-1.0 client therefore never sees automatedDecision or reviewContact. A new value in an existing closed set MAY reach an older client: the tolerance fallbacks above exist for exactly this skew, and the server always refuses with the true reason rather than mislabelling it under an older value — a version-1.0 client refused for a group-velocity bound receives refusalReason: GROUP_VELOCITY and treats it as the generic refusal the fallback prescribes.

6. The Authorise Operation

POST /ocs/ai/v1/authorizations

6.1. Request

Table 8
Field Requirement Meaning
schemaVersion REQUIRED See Section 5.
actionId REQUIRED Client-asserted Action identity. SHOULD be asserted by the application originating the work; a gateway-fabricated id is permitted but downgrades attribution quality (known per contract). Never legally reusable (Section 6.5).
tenantRef REQUIRED The tenant under which the subscriber is provisioned. MUST be within the credential's bound tenant set.
accountRef REQUIRED The subscriber identity under that tenant.
actionType REQUIRED The declared Action Type. Undeclared/unknown types are refused (NO_LAW) — the plan's vocabulary is a whitelist; there is no default.
estimate RECOMMENDED Expected consumption (units per unit type). Validated like a report's units: keys MUST be unit types declared by the Action Type, ≤ 16 entries (else MALFORMED_REQUEST). Used to size the opening reservation; a good estimate reduces both refusal risk and over-reservation.
scope OPTIONAL An opaque funding-scope label. Funding buckets provisioned with a matching scope become eligible; exclusive-scope buckets fund only matching Actions and never fall through to general funds. Values are plan-defined.
parentActionId OPTIONAL Declares this Action a child of an open parent Action of the same subscriber (Section 9).
gatewayTime OPTIONAL The gateway's clock at the first attempt — preserved verbatim on retries (it is part of the request identity). Recorded as evidence, never enforced against (Section 15.6).

6.2. Processing

The server resolves the subscriber, verifies tenancy (Section 15), selects the price entry for the declared Action Type from the subscriber's plan (pinning the plan version — the Action is rated under that law for its entire life), resolves the applicable time band if the type is banded (Section 10). It then verifies budget headroom (the opening reservation covers the estimated charge, any contractual minimum, and rounding), verifies the shared bounds (any sponsor cap and the applicable capacity pool, Section 12), applies the trust limits (Section 15.4), and answers.

6.3. Response — GRANT

Table 9
Field Present Meaning
decision always GRANT or REFUSE.
grantId every grant Correlates the lifecycle; on a flat-priced Action it correlates the settled Action.
grantSeq every grant Monotonic generation number per Action. A client MUST discard any grant whose grantSeq is lower than one it has already seen for the Action.
grantIssuedAt every lifecycle grant The grant's issue instant — replay-stable; the base of the capacity-rate window (Section 12.2).
grantedUnits lifecycle grants The granted pool, in abstract pooled units.
reportAt lifecycle grants The consumption threshold at which the client MUST report.
expiresAt lifecycle grants Grant validity instant (Section 8).
unitWeights every lifecycle grant (all price forms, including free) Pool multipliers per unit type — the complete map, constant for the Action's whole life (pinned with the law): consuming quantity q of unit type u draws q × weight(u) from the pool. Weights are positive decimals, scale ≤ 6; each product is rounded HALF_UP at scale 6, and products are summed exactly. Ratios, not prices.
quantumUnits quantised Action Types The block size in pooled units: consumption is charged in whole blocks; the client SHOULD expect pool draw-down in block increments.
timeBandId banded Action Types (including flat) The time band fixed at opening (Section 10).
serverTime every response — envelope, always fresh, never replayed The server clock at this processing — the authoritative time reference (Section 8).
actionLive, currentGrantSeq, currentGrantId every Action-identifying 200 — envelope, always fresh The live truth: whether the Action is open (monotonic — once false, never true), the current generation, and the current grant's opaque address (currentGrantId — the recovery handle: a restarted client reports against it). On any response, replays included, the client MUST obey the envelope over the stored body: actionLive: false means stop; currentGrantSeq above a replayed grant's grantSeq means that grant is stale.
lawVersion every lifecycle grant The pinned plan/price-book version (integer) this Action rates under — for client-side correlation with the tenant's published catalogue; opaque otherwise.
limits every lifecycle grant The enforceable caps: maxCallsPerAction (int, always), maxRuntimeSec (int, always — plan value or platform default; measured from the Action's opening grantIssuedAt, exceeded when now > open + maxRuntimeSec; also bounds heartbeat-only renewal), maxConcurrentActions (int, always — subscriber-level), signaturesRequired (bool, where configured), minRate (decimal string, pooled units per second, where a capacity pool applies), enforcementWindowSec (int, where a capacity pool applies — the window over which consumed rate is accounted, Section 12.2). Disclosed so a gateway can plan work instead of discovering limits through stops.
rateReduced when capacity pressure reduced this grant (opening or continuation) true, with reduceCause: "CAPACITY" (Section 12.3).
settled flat-priced Actions true: charged at open; no reports, no validity.

Grant invariants (normative): every issued grant satisfies grantedUnits > 0, 0 < reportAt ≤ grantedUnits, quantumUnits > 0 where present, expiresAt > grantIssuedAt, and reachability: reportAt MUST be attainable within the Action's cumulative caps under the grant's weights. A response violating them is a server defect; the client treats it as a 5xx-equivalent and retries the identical request.

6.3.1. Client obligations under a grant

The client MAY serve Calls until the earliest of: pool exhaustion, the cycle delta reaching reportAt (≥, Section 7.7), or expiresAt. It MUST report at reportAt; it MUST NOT knowingly serve consumption beyond the granted pool other than the tail defined in Section 3.4.

6.4. Response — REFUSE

refusalReason registry (version 1):

Table 10
Reason Meaning
NO_HEADROOM The opening reservation cannot be fully funded. Partial funding is never granted.
CEILING_PREEMPT The Action's ceiling (or its family's, Section 9) leaves no room.
ACCOUNT_VELOCITY Account-level velocity limit engaged.
BALANCE_VELOCITY Balance-level velocity limit engaged.
GROUP_VELOCITY Group-level velocity limit engaged: a rate bound across a declared group of related accounts. Version 1.1 (Section 5).
MAX_CONCURRENT_ACTIONS The subscriber's concurrent-Action cap is reached.
PARENT_STOPPED The declared parent Action is not open.
NO_LAW No applicable price entry: unknown Action Type, no plan, or unknown identities on a declared tenant.
DRAINING The service is draining (an operator-controlled, temporary state): new Actions are refused. Always retryable, with retryAfter (Section 11).
BLOCKED_TARIFF The plan blocks this Action Type for this subscriber.
SERVICE_BARRED The subscriber's state bars service.
DUPLICATE_ACTION The actionId belongs to an opened Action and the request identity differs (Section 6.5).
EXPOSURE_CAP A sponsor's aggregate bound across its population is reached; limitOwner names the sponsor (Section 12.1).
NO_CAPACITY The applicable capacity pool cannot admit the Action at or above the declared minimum rate (Section 12.2). Always retryable, with retryAfter.
PREEMPTED A capacity commitment was reclaimed under the declared pressure policy before the grant could stand (Section 12.2). Always retryable.
CLASS_BLOCKED Reserved; never emitted in version 1.

Every refusal additionally carries:

Table 11
Field Present Meaning
retryable always on refuse Whether the identical request MAY succeed later with nothing changed but time. Version-1 retryable reasons — exactly seven: the transient-scarcity six (NO_CAPACITY, PREEMPTED, ACCOUNT_VELOCITY, BALANCE_VELOCITY, GROUP_VELOCITY, MAX_CONCURRENT_ACTIONS) plus DRAINING. Every other reason is retryable: false.
retryAfter REQUIRED when retryable: true Seconds to wait before retrying the identical request.
grade always on refuse The evidence behind the reason: PROVEN (the server's own arithmetic on its own records), ATTESTED (an operator process decided), or BOUNDED (true within a stated window, no stronger). A client MUST NOT present more certainty to the end user than the grade holds.
limitOwner on a party-scoped bound Names whose limit was reached (e.g. the sponsor's), so an end user is never shown a generic "insufficient funds" for someone else's cap.
automatedDecision always on refuse, from version 1.1 (Section 5) true in version 1: the refusal was decided by machine, with no human in the loop (Section 15.7). Carried on the decision itself so the notice travels with the refusal it describes.
reviewContact when the tenant configures one, from version 1.1 (Section 5) A human-review channel — a URI or plain-text contact — through which the refused party can have the decision re-examined by a person. Display text under the limitOwner presentation rules (printable characters only, rendered as plain text, never markup); tenant-configured and echoed by the server. A client SHOULD present it alongside the refusal it accompanies.

Version-1 grades: NO_CAPACITY and PREEMPTED are BOUNDED; SERVICE_BARRED and DRAINING are ATTESTED; every other reason is PROVEN.

A refusal is a complete outcome: no budget is held, nothing is charged, and the client MUST NOT serve the work. A refusal retains no identity: no Action opens, the actionId remains unclaimed, and any later Authorise carrying it — identical or different — is evaluated fresh. DUPLICATE_ACTION applies only against Actions that actually opened. Concurrent identical Authorises are serialised per subscriber; if one opens the Action, the other lands on live-Action replay (Section 6.5).

6.5. Idempotency, request identity, and identifier scopes

Request identity is SHA-256 over the JSON Canonicalization Scheme (JCS) [RFC8785] canonicalization of the strict-parsed request body (duplicate or unknown members are rejected before hashing, Section 5); for Reports, the path grantId is appended to the canonical bytes before hashing. Because every decimal travels as a canonical string (Section 4.3), JCS needs no number-rendering caveats. Two requests with equal canonical form are the same request. Test vectors accompany the conformance suite.

Identifier scopes: actionId is unique within its (tenant, account) pair — two tenants can never collide; callId is unique within its Action; grantId is server-issued, opaque, and unique within the charging domain. Server recognition retention is the late window (Section 7.6).

Authorise always answers in the Authorise shape — a replay re-presents facts, it never returns a stored Report body:

  • Retry of a live Action (same actionId, matching identity) → decision: GRANT re-presenting the current standing grant (current grantSeq and parameters) plus its cycle-baseline receipt (acceptedCumulative at the settlement that issued it), so a restarted gateway recovers both the address and the remaining pool — with a fresh envelope.

  • Retry of an ended Action within the late window (matching identity) → an Authorise-shaped terminal acknowledgement: decision: GRANT, settled where applicable, the ending, endedAt, and the final receipt; a flat-priced Action's retry re-receives GRANT/settled: true, never a refusal.

  • Same actionId as an opened Action, different identity → 200 REFUSE / DUPLICATE_ACTION (a charging refusal like any other — identity conflicts are not HTTP errors).

  • An actionId that opened an Action is never legally reusable, including after the Action ends. Beyond the late window the server no longer guarantees replay; a client MUST NOT rely on replay after it.

Clients MUST serialise a retry as the identical logical request: JCS removes JSON key-order differences, and nothing else — there is no equivalence class of spellings (Section 4.3 admits exactly one spelling of every value; any other is malformed before identity is ever computed). Any byte difference that survives canonicalization is a different request.

7. The Report Operation

POST /ocs/ai/v1/grants/{grantId}/reports

7.1. Request

Table 12
Field Requirement Meaning
schemaVersion REQUIRED See Section 5.
actionId REQUIRED The Action being reported.
tenantRef, accountRef REQUIRED Identity resolution; MUST match the Action's.
calls[] REQUIRED Per-call cumulative totals (Section 7.2). MAY be empty: an empty calls[] with final: false is a heartbeat — an ordinary fresh cycle (Section 3.5) that settles whatever unsettled meter delta exists (normally zero) and renews the grant — the lease-renewal mechanism for quiet periods; it passes through the same stop-condition precedence as any cycle.
final REQUIRED true declares this the closing report (the CLOSE ending).
gatewayTime OPTIONAL As in Section 6.1 — recorded, never enforced against (Section 15.6).

7.2. Cumulative semantics (normative)

Each calls[] entry is {callId, units, startedAt?, signature?} where units is a map of unit type → cumulative total for that Call since the Call began — not a delta; values are canonical decimal strings (Section 4.3). calls[] is a keyed patch, not a snapshot: only Calls with changed totals need appear; an omitted callId reads as unchanged; a callId never seen before initialises all its declared units at zero baseline; two entries with the same callId in one request are malformed. The calls-per-Action limit counts distinct callIds over the Action's life. Unit keys MUST be among those declared by the pinned Action Type (undeclared keys are malformed); declared keys absent from an entry read as unchanged. signature is reserved in version 1: an opaque string, recorded verbatim. Its verification profile is bilateral and out of band, and SIGNATURES_REQUIRED (Section 7.4) applies only where such a profile is agreed. startedAt (version 1.1, Section 5) is OPTIONAL evidence: the instant the Call began at the serving system (timestamp, Section 4.3). Like gatewayTime it is recorded and never enforced against (Section 15.6), and it can never re-price anything — the Action's time band is pinned at open (Section 10), so no client-reported timestamp changes a charge. Its value is that the moment consumption occurred, not merely the moment it was reported, survives into the audit record. It SHOULD carry the same value on every entry for a given callId; the server retains it under the audit retention rules (Section 15.7).

Monotonicity and out-of-order delivery: a reported value lower than the server's stored high-water for that Call and unit is absorbed: the meter is the per-value maximum, so a stale, reordered, or replayed report charges zero and is never an error — this is what makes the arithmetic immune to retries, duplicates, and delivery order. (Within one request there is no shrinking case to define: duplicate callId entries are malformed outright.)

Cumulative reporting is what makes the protocol retry-safe: a replayed report or replayed call entry moves the cumulative meter by zero and therefore charges zero. The server settles each cycle as the increment of the rated cumulative consumption under the pinned law — never rating the same consumption twice, for any price form.

Pool arithmetic (normative — one cumulative pipeline, cadence-independent): everything is computed on Action-cumulative quantities, and every charge or threshold is a delta of cumulatives — never a rounded delta. Per unit type u: cumulativePooled_u = HALF_UP₆(weight_u × cumulativeTotal_u); the Action's pooled total is the exact sum over unit types. For quantised forms, blocks apply to that cumulative pooled total — blocksCharged = ceil(cumulativePooled / quantumUnits) — and each cycle charges the newly completed blocks (the cumulative-rated rule). reportAt and pool exhaustion compare raw (unquantised) pooled-total deltas against the cycle baseline. Because rounding and quantisation are applied only to cumulative values, report segmentation provably cannot change any charge.

7.3. Response

Table 13
Directive Meaning
CONTINUE Settlement done; the next grant is included (grantId, grantedUnits, reportAt, expiresAt, unitWeights, serverTime, and the other grant fields of Section 6.3). The tail (Section 3.4) is served under the old grant and settled on the next cycle. Under capacity pressure the next grant MAY be reduced (Section 12.3), flagged rateReduced: true.
STOP Settlement done; stop obligation — the client MUST promptly cease serving the Action's traffic. stopReason per Section 7.4; endedAt states the ending instant (the late window's start). Consumption already in flight is reportable afterwards (Section 7.6 / Section 8).
SETTLED Final settlement acknowledged. Returned for a final: true close and for every non-replay terminal response of a CLOSE-ended Action (the terminal directive is a function of the ending, Section 3.5: STOP-ended Actions answer STOP, LAPSE-ended answer STOP (LAPSED)); late adjustments are flagged adjustment: true. Carries endedAt.
CURRENT No settlement occurred (Section 3.5 LIVE row 6): the response to a stale, non-final, unrecognised report — the standing grant re-presented (its full Section 6.3 grant fields) plus baselineCumulative (that grant's cycle baseline) and meterCumulative (the post-merge meter). The client resynchronises: pool draw-down = weighted(meterCumulativebaselineCumulative).

This distributed-worker convergence flow is illustrative; it does not relax the one-in-flight requirement of Section 3.4. The CURRENT branch shown is the no-ending-condition branch of Section 3.5 LIVE row 6:

Gateway worker A   Charging Server      Gateway worker B
    |                     |                     |
    |--- Report on G1 --->|                     |
    |<-- CONTINUE G2 -----|                     |
    |                     |<-- stale non-final -|
    |                     |    G1; no retained- |
    |                     |    id match         |
    |                     | max-merge meter;    |
    |                     | evaluate ending     |
    |                     | conditions          |
    |                     |--- CURRENT G2 + --->|
    |                     |    baseline +       |
    |                     |    post-merge meter |
    |                     |    no settlement,   |
    |                     |    no mint          |
    |                     |                     | B resynchronizes

Cycle baseline (normative): grantedUnits and reportAt are cycle-scoped, and the baseline is a meter state, not an instant: a cycle's consumption is the weighted pooled delta (Section 7.2 arithmetic) between the accepted meter at the settlement that issued the grant — the acceptedCumulative of that response; zero for the first grant — and the current meter. Both sides can compute it exactly: the client from its last receipt, the server from its stored meter. Report values remain Action-cumulative (Section 7.2). Tail consumption lands in whichever cycle's report first carries it — the cumulative arithmetic makes double-counting impossible.

Settlement receipt: every settling response (CONTINUE, STOP, SETTLED) carries settlementId (stable per settlement; a replay returns the same id), acceptedCumulative (the server's post-merge Action totals — a complete map over the declared unit types), acceptedCalls (an array of {callId, units} entries — the per-call accepted cumulative values for exactly the Calls present in this request, ≤ 256 entries), and cyclePooledUnits (the raw pooled delta settled this cycle, pre-quantisation), plus where applicable clamped: true (limited by the meter or a ceiling) and adjustment: true (a late settlement). A client holding several receipts trusts the one from the highest-generation outcome. Receipts are denominated in units only — no balance, price, or currency ever crosses this wire.

Response replay and convergence: governed entirely by the state machine (Section 3.5) — the server serialises all of one Action's operations; only a fresh Report on the current grant runs a cycle and mints; identity-matched retries replay their stored outcome under a fresh envelope; non-matching reports converge (a final: true close is always honoured against the current state; non-final ones max-merge and answer CURRENT); retention is the terminal outcome plus the last 4 non-terminal outcomes — an aged retry converges safely, though its original settlementId is then no longer re-presentable (Section 7.6). A lost CONTINUE is therefore always recoverable: retry the identical request (replay), or report against currentGrantId from any response's envelope. acceptedCalls appears on Report responses only.

The lost-response path below illustrates the request identity and replay rules of Section 6.5 and this section. R includes the Report body and its path grantId; S is the stable settlement receipt. The retry remains the same logical Report for Section 3.4:

Gateway                          Charging Server
   |                                    |
   |--- Report R against current Gn --->|
   |                                    | merge, settle, and
   |                                    | atomically store
   |                                    | CONTINUE G(n+1) + S
   |<-- CONTINUE G(n+1) + receipt S --X |  response lost
   |                                    |
   |--- identical retry of R ---------->|
   |                                    | retained identity
   |                                    | matches
   |<-- stored CONTINUE + receipt S ----|  body replay; fresh
   |                                    |  envelope; no
   |                                    |  settlement, no mint

7.4. stopReason registry (version 1)

Table 14
Reason Meaning
CEILING_REACHED The Action (or family) ceiling is exhausted.
NO_HEADROOM Funds cannot cover a further grant.
CALL_LIMIT The calls-per-Action limit was reached (always enforced; the settlement of the limit-crossing report is completed first).
MAX_RUNTIME The Action exceeded its maximum runtime.
INFLATION A consumption-inflation trust wire tripped.
SIGNATURES_REQUIRED The Action Type requires call signatures and they were absent.
PARENT_STOPPED The parent Action ended; children are stopped (Section 9).
ADMIN_FORCED Operator intervention.
DRAINING The service is draining: settlement completed, no re-grants are issued.
NO_CAPACITY Capacity fell below the declared minimum serve rate; continuing usefully is impossible, so the server stops honestly rather than degrading past use (Section 12.3).
EXPOSURE_CAP The sponsor's aggregate bound was exhausted mid-Action; limitOwner names the sponsor (Section 12.1).
LAPSED The grant's validity lapsed — returned for the grace-window close (Section 7.5), and as the terminal directive of every LAPSE-ended Action's later responses (Section 3.5).

Precedence (normative): final: true beats every server stop condition → SETTLED (Section 7.5), draining included. For a non-final report on which several stop conditions hold at once, the reported stopReason is the first that applies in this order: ADMIN_FORCED · DRAINING · PARENT_STOPPED · LAPSED · CEILING_REACHED · EXPOSURE_CAP · NO_HEADROOM · CALL_LIMIT · MAX_RUNTIME · INFLATION · SIGNATURES_REQUIRED · NO_CAPACITY — and the stored terminal outcome records that same reason, so replays agree.

7.5. Lapse boundary (normative)

With now being the server clock at processing:

  • now < expiresAt → normal processing; CONTINUE is possible.

  • expiresAt ≤ now < expiresAt + 5 s → the report is accepted as the closing report: consumption is settled and the directive is SETTLED when final: true, else STOP (LAPSED) — never CONTINUE.

  • now ≥ expiresAt + 5 s → the Action is ended as LAPSE (if it wasn't already — the transition instant is the same either way, Section 3.5) and the report is processed as a late report (Section 7.6).

The following diagram illustrates those branches; Section 3.5 and the rules above remain authoritative, including the stored-outcome replay exception:

Gateway                      Charging Server (`now`)
   |                                    |
   |--- Report ------------------------>|
   |                                    +-- now < expiresAt
   |                                    |   normal processing;
   |                                    |   CONTINUE is possible
   |                                    |
   |                                    +-- expiresAt <= now
   |                                    |   < expiresAt + 5 s
   |                                    |   final=true: SETTLED
   |                                    |   final=false:
   |                                    |   STOP (LAPSED)
   |                                    |
   |                                    `-- now >= expiresAt + 5 s
   |                                        ENDED(LAPSE), with
   |                                        endedAt=expiresAt+5 s;
   |<-- response under selected branch --    late handling
   |                                        (section 7.6)

Precedence: on a final: true report, the client's close wins over any concurrent server stop condition — the response is SETTLED (the stop obligation is moot once the client has ended the Action). A late report can never revive a grant. A client that wants to continue past expiresAt MUST have reported before it.

The LAPSE instant (normative): an Action that ends as LAPSE has endedAt = expiresAt + 5 s — by definition, whether the ending is physically performed by a timer, a sweep, or the first post-grace report. Both sides can compute the late window's anchor from the grant alone. CLOSE and STOP endings record their processing instant as endedAt.

7.6. Late reports and the late window

While recognised — until max(endedAt + 24 h, expiresAt + 1 h) (Section 3.5; endedAt is carried on every terminal response) — the server:

  • MUST retain recognition: replay of stored responses to recognised retries (matching request identity) and duplicate-actionId detection are unconditional for the full window — this guarantee does not degrade.

  • Settles genuinely new consumption reported late: the cumulative meters are merged upward, and the increment of rated consumption — floored at zero, clamped by the remaining ceiling — is charged under the Action's pinned law and returned with the ending's terminal directive (Section 3.5), adjustment: true, and a receipt (Section 7.3). Contractual minimums are never applied a second time. Repeated late reports charge each unit at most once. Late-settlement capability (unlike recognition) is best-effort under extreme retention pressure — an exceptional, server-metered condition; a late report the server recognises but can no longer settle is answered with the stored terminal outcome plus unattributable: true and no new settlementId — the response says plainly that the new units were not settled (they are recorded unattributable). A non-matching terminal report whose merge adds zero receives the terminal outcome with the current receipt and no adjustment flag.

Beyond the late window, reports against unknown identities receive 404 and the traffic is recorded as unattributable — recorded, never charged.

7.7. Reporting cadence requirements

A client MUST report when the cycle's pooled consumption delta reaches reportAt (comparator: ), and SHOULD report promptly on its own initiative when an Action ends client-side (with final: true). Batching beyond reportAt increases the client's overshoot exposure, which the protocol bounds to one final report but does not eliminate.

On receiving STOP, the client MUST stop admitting new Calls immediately, SHOULD cancel cancellable in-flight work, and MUST flush the final meter — dispatching the first flush report within 30 seconds (a hard per-event deadline, distinct from the statistical promptness profile of Section 12.4); when more than 256 Calls changed, the flush spans serialised 256-call chunks continued back-to-back. The flush obligation fires once per ending: a STOP directive received in response to the flush itself, a replay, or a late response does not re-trigger it. Uncancellable work may complete, and its consumption reports late (Section 7.6).

8. Timing Summary

Table 15
Instant Source Effect
reportAt grant Consumption threshold: the client MUST report on crossing it.
expiresAt grant Grant validity end. Reports within a 5 s grace settle as closing; beyond it the Action ends as LAPSE.
max(endedAt + 24 h, expiresAt + 1 h) terminal response + last grant Late-window end: recognition unconditional until it; late consumption settleable within it (Section 7.6).

All boundaries are evaluated on the server clock. Every response carries serverTime (Section 6.3), the authoritative reference: the client SHOULD track the offset between serverTime and its own clock plus the measured round-trip, and dispatch its pre-lapse report with at least that margin in hand — a report sent "before the lapse" by the client's clock but processed after it lands in the closing or late branch.

8.1. Protocol constants and configurables

This table is a descriptive index to the defining prose, not a second source of rules. Where a value is a recommendation or a configured value, the table says so; where two statements are not fully harmonised, it preserves both without choosing between them. "Deployment-configurable" includes plan-, catalogue-, integration-, client-, and server-policy values.

Fixed in protocol.

Table 16
Name Value Defining section
Protocol generation Version 1 (/v1/) Section 1.4, Section 4.1, Section 5
Charging operation count Exactly two: Authorise and Report Section 3.2
Authorise endpoint POST /ocs/ai/v1/authorizations Section 4.1
Report endpoint POST /ocs/ai/v1/grants/{grantId}/reports Section 4.1
Media and transport application/json over HTTPS; plaintext refused Section 4.1
Request authentication A per-gateway bearer API key in every request; OAuth2 is not in version 1 Section 4.2
Redirect posture HTTP 3xx is not used Section 1.4
Charging-domain topology One logical charging domain per tenant in version 1 Section 1.4
Action lifecycle cardinality One successful Authorise; one ending Section 3.1
Current grant cardinality Exactly one per live Action Section 2
Outstanding Report cardinality At most one logical Report per Action Section 3.4
Opening and mint generations Lifecycle opening is G1; each mint is exactly +1; a flat terminal envelope is generation 2 Section 2, Section 3.3, Section 3.5
Outcome retention ring Terminal outcome plus the last 4 non-terminal outcomes; cycles and late adjustments share the ring Section 3.5, Section 7.3
Lapse grace 5 s Section 3.5, Section 7.5, Section 8
LAPSE instant endedAt = expiresAt + 5 s Section 7.5
Recognition / late window Until max(endedAt + 24 h, expiresAt + 1 h) Section 2, Section 3.5, Section 7.6, Section 8
Decimal scale and rendering Canonical decimal JSON string; scale at most 6; the lexical exclusions are in Section 4.3 Section 4.3
Request-quantity domain [0, 10^9], scale at most 6 Section 4.3
Per-call and Action/unit parser caps Each is at most 10^9 Section 4.4
Per-call operational segmentation point Terminate or segment before a Call's cumulative value reaches 10^9 Section 4.3
Weight domain [0, 10^6], scale at most 6; issued weights are positive Section 4.3, Section 6.3
Derived pooled domain [0, 10^17), scale at most 6, for the fields named in Section 4.3 Section 4.3
Identifier syntax At most 128 characters from [A-Za-z0-9._:-] Section 4.3
Display-label syntax At most 64 printable UTF-8 characters; no control or bidirectional-control characters Section 4.3
Unit-type syntax [a-z0-9_]{1,64} Section 4.3
Timestamp profile YYYY-MM-DDThh:mm:ss.sssZ; 3 fractional digits; years 1970-9999; no leap seconds Section 4.3
Count/version integer domain JSON integer 0 <= n < 2^53; schemaVersion >= 1 Section 4.3, Section 5
Compatible schema support Every version from 1 through maxSchemaVersion Section 5
Request-body limit At most 256 KB; the memo does not define the KB base Section 4.4
Report patch size At most 256 calls[] entries Section 4.4
Unit-map size At most 16 entries per Call; estimate and response maps share the 16-unit bound Section 4.3, Section 4.4, Section 6.1
Receipt call count acceptedCalls has at most 256 entries and covers exactly the Calls in the settling Report Section 4.3, Section 7.3
Grant inequalities grantedUnits > 0; 0 < reportAt <= grantedUnits; quantumUnits > 0 where present; expiresAt > grantIssuedAt Section 6.3
Pooled product rounding HALF_UP at scale 6 per cumulative unit-type product; exact sum Section 6.3, Section 7.2
Quantised rating ceil(cumulativePooled / quantumUnits); charge block deltas Section 7.2
Threshold and exhaustion basis Raw, unquantised pooled-total delta from the cycle baseline Section 7.2
Report threshold comparator Dispatch when the cycle delta is >= reportAt Section 7.7
Threshold / closing cadence Section 7.7 requires threshold dispatch and recommends a prompt client close; Section 12.4 says both are due immediately outside the profile bound Section 7.7, Section 12.4
STOP flush First chunk within 30 s, once per ending; chunks of at most 256 changed Calls back-to-back Section 7.7, Section 14
Retry backoff recommendation Initial 1 s, factor 2, full jitter, cap 30 s (recommendation) Section 13.3
Client attempt deadline recommendation 2 s per attempt (recommendation) Section 13.5
TLS versions TLS 1.2 or later required; TLS 1.3 recommended; [RFC9325] configuration Section 4.1, Section 15.1
Request-identity algorithm SHA-256 over JCS [RFC8785]; append path grantId for Report Section 6.5
Explicit HTTP status mappings 200, 400, 401, 403, 404, 405, 415, 429, and 503 as mapped Section 3.5, Section 4.1, Section 13.1
Additional server-error class INTERNAL is status-mirroring; retry guidance covers 5xx generally, whose exact statuses are not enumerated Section 13.1, Section 13.3
HTTP retry header Retry-After on 429 and 503 Section 13.1
HTTP connection posture HTTP/2 connection reuse is assumed (operational assumption) Section 13.5
Current protocol version 1.1 — generation 1, wire schemaVersion 2; each minor binds to one schemaVersion Section 5
Retryable refusal set Exactly seven reasons from version 1.1 (six in 1.0), enumerated in Section 6.4 Section 5, Section 6.4
Closed per-version value sets Decision, refusal reason, stop reason, error code, ending, directive, grade, and reduce cause; initial values in Section 17; additions bind to minor versions per Section 5's version table Section 5, Section 17
API-key rotation and revocation posture Dual-active rotation; revocation effective "within seconds" Section 4.2, Section 15.1

Deployment-configurable.

Table 17
Name Value Defining section
Refusal human-review channel (reviewContact) Tenant-configured; absent unless configured Section 6.4, Section 15.7
Sponsor-cap accounting window Part of the sponsor's declaration (credits per period); boundary mechanics deployment-profile in version 1 Section 12.1
maxSchemaVersion Highest compatible schema supported; 1 <= n < 2^53 Section 4.3, Section 5
grantedUnits Server-selected per cycle within the pooled domain and grant inequalities Section 4.3, Section 6.3, Section 12.2 to Section 12.3
reportAt Server-selected per cycle; 0 < reportAt <= grantedUnits Section 6.3, Section 7.7
Grant validity / expiresAt Server-selected instant after grantIssuedAt; no default duration stated Section 6.3, Section 8, Section 12.3
Action-Type vocabulary Catalogue-defined, whitelisted, and distributed out of band Section 1.4, Section 6.1
unitWeights Positive plan/law ratios, at most 10^6, pinned for the Action Section 4.3, Section 6.3
Price form and lawVersion Catalogue/plan choice; the selected law version is pinned Section 3.3, Section 6.2 to Section 6.3
Quantisation / quantumUnits Action-Type choice; positive pooled decimal where used Section 6.3, Section 7.2
Time-band schedule / timeBandId Tenant-published full-week schedule; band selected once at opening Section 6.3, Section 10
Action or family ceiling Plan-defined internal ceiling; no monetary value appears on the wire Section 2, Section 6.2, Section 9, Section 15.7
Contractual minimum Plan-defined; applied as described at open and not a second time late Section 6.2, Section 7.6
Funding scope and buckets Plan-defined scope, eligibility, and exclusivity Section 6.1
limits.maxCallsPerAction Always disclosed; integer in the count domain; counts distinct Calls over the Action Section 4.3, Section 6.3, Section 7.2
limits.maxRuntimeSec Section 6.3 says always disclosed as a plan value or platform default and exceeded when now > open + value; Section 15.4 calls maximum runtime optional per deployment/plan Section 4.3, Section 6.3, Section 15.4
limits.maxConcurrentActions Always disclosed subscriber-level integer cap Section 4.3, Section 6.3, Section 15.4
limits.signaturesRequired Boolean where a bilateral signature profile is configured Section 6.3, Section 7.2, Section 15.4
Call-signature profile Bilateral and out of band; no field-specific size cap is stated Section 1.4, Section 7.2
limits.minRate Capacity-only pooled units per second; exact upper numeric domain is not stated Section 4.3, Section 6.3, Section 12.2 to Section 12.3
limits.enforcementWindowSec Capacity-only integer seconds, greater than 0; exact algorithm provisional Section 4.3, Section 6.3, Section 12.2
Sponsor cap and owner label Sponsor-defined aggregate internal bound and presentation-safe label Section 12.1, Section 15.7
Capacity pool Tenant-defined provisioned rate and named Action-Type membership Section 12.2
Capacity pressure / reclamation policy Deployment-declared; no numeric default stated Section 6.4, Section 12.2 to Section 12.3
Integration promptness bound Agreed per integration; provisional version-1 default is 30 s at p95 Section 12.4
Refusal-body retryAfter Server-selected integer seconds, required when retryable: true Section 4.3, Section 6.4
Error-envelope retry members retryable and retryAfter are independently optional; no status coupling or numeric default is stated Section 13.1
HTTP Retry-After value Server-selected on 429 and 503; Pulse states no minimum or maximum Section 13.1
Client pre-lapse margin Measured server-clock offset plus round-trip margin; no fixed value Section 8
Velocity limits Optional account/balance controls; thresholds and windows not stated Section 6.4, Section 15.4
Consumption-inflation wire Optional control; threshold/window not stated Section 7.4, Section 15.4
Per-credential API rate limit Token-bucket rate/capacity; no numeric default stated Section 13.1, Section 15.5
Draining state Operator-controlled temporary state; delay returned in retryAfter Section 6.4, Section 11
Mutual TLS Optional deployment choice Section 4.2
Credential tenant set One or more bound tenants; exact cardinality not stated Section 2, Section 15.2
API-key material and exact revocation SLA Deployment credential configuration; no exact seconds bound is stated Section 4.2, Section 15.1
Persistent clock-skew tolerance Integration trust threshold; no value stated Section 15.6
Audit, gatewayTime, and startedAt retention Operator retention policy, apart from the fixed late window Section 15.7

No row defines an Action boundary: the protocol does not define what constitutes one Action — the actionId is asserted by the application originating the work (Section 6.1). No row exposes a monetary amount (Section 15.7).

9. Child Actions

Where enabled, an Authorise MAY declare parentActionId, nesting the new Action under an open parent of the same subscriber, opened by a credential of the same owning gateway — a parentActionId referencing another gateway's Action is refused 403 before any charging evaluation (Section 15.3). Children share the parent's family ceiling: sibling headroom reflects every family settlement as it happens. Any parent ending cascades: open children are stopped (PARENT_STOPPED) before the parent concludes.

Cascade timing (normative): children are stopped server-side at the parent's ending instant — the server serialises all of one subscriber's operations, so a concurrent parent ending and child report have a definite order. A stopped child's grant is no longer eligible for CONTINUE; the child gateway learns at its next exchange, whose response is STOP (PARENT_STOPPED) after settling the reported consumption. Consumption already in flight when the cascade lands remains reportable and settleable under the late rules; the notification gap is bounded by the child's grant validity, exactly like any other silent period. Late child consumption continues to settle against the family ceiling during the late window.

The following CLOSE example illustrates the cascade for any parent ending. It also shows that version 1 has no server push: the child gateway learns the stop at its next exchange.

Parent gateway     Charging Server                  Child gateway
    |                     |                               |
    |--- final Report --->|                               |
    |                     | serialize subscriber state    |
    |                     | stop open child at parent's   |
    |                     | ending instant: PARENT_STOPPED|
    |                     | conclude parent               |
    |<-- SETTLED ---------|                               |
    |                     |       (no server push)        |
    |                     |<----- child's next Report ----|
    |                     | merge, settle reported units  |
    |                     |--- STOP (PARENT_STOPPED) ---->|

10. Time Bands

A banded Action Type prices differently by time of day/week, per the tenant's published schedule. Normative rules:

11. Draining

When the service is draining (operator-controlled, temporary): new Actions are refused DRAINING (retryable, with retryAfter); replays of live Actions are still answered; every non-final report on a live Action settles normally and answers STOP (DRAINING) — no re-grants are issued — while a final: true report settles as SETTLED per Section 7.5's precedence. Terminal semantics (Section 7.6) remain in force until no live state remains.

12. Shared Bounds and Fair Usage

The bounds in Section 6 to Section 7 are per subscriber. Two further bounds range over populations, and both surface on the wire only through the vocabulary above — no new operations, no new fields beyond the refusal metadata of Section 6.4.

12.1. Sponsor caps

A sponsoring party (typically a reseller funding an AI benefit) MAY declare an aggregate spend bound across a population of subscribers. It is enforced at authorisation and at each cycle like any other headroom check. Its refusals and stops are attributed: EXPOSURE_CAP always carries limitOwner, and a client MUST surface the distinction to the end user — from a single account's viewpoint the sponsor's cap and the user's own limit are otherwise indistinguishable, and telling a user their own money ran out when the sponsor's did is untrue.

The cap's accounting basis is exposure — credits committed to open grants plus credits settled, per sponsor relation; an open grant is spend the sponsor is already standing behind, so a cap that counted settled spend alone could be overshot by in-flight work. The bound is declared per period: the accounting window (a calendar month, a rolling interval) is part of the sponsor's declaration, not a protocol constant, and is disclosed to the sponsor and its members out of band — the wire carries only the refusals and stops the cap produces. Exact period-boundary mechanics (the reset instant, the treatment of exposure spanning the boundary) are deployment-profile in version 1, pending the same trace validation as Section 12.2 to Section 12.4.

12.2. Capacity pools

A tenant MAY declare provisioned serving capacity — a rate — that named Action Types draw on. Admission then answers two questions: may this be paid for, and can it be served. A grant is a rate authorisation: its rate is grantedUnits over the interval from grantIssuedAt to expiresAt — so capacity enforcement changes grant sizing, not the message set. When a pool cannot admit at or above the declared minimum rate, authorisation refuses NO_CAPACITY — always retryable with retryAfter — graded BOUNDED, because capacity keeps no ledger. Capacity refusals say something about the service's provisioned room, never about the customer's money; clients MUST NOT render them as balance problems.

Enforcement is server-side and window-aware: when sizing each grant, the server accounts the Action's consumed rate over the enforcement window (enforcementWindowSec > 0, disclosed in limits) — so reporting early and cycling fast confers no rate advantage (the window remembers, and smaller grants follow). The client has no pacing obligation; pacing to the grant rate is optional smoothness, not conformance. Shared bounds are linearizable: admissions against a capacity pool — and against a sponsor cap (Section 12.1) — are evaluated as if serially against the aggregate; two concurrent admissions can never both observe and consume the same remaining headroom. (Provisional: the window's exact algorithm and reproducibility are completed by the capacity conformance traces — see Status.)

12.3. Degradation before refusal (fair usage)

Under capacity pressure the server degrades before it refuses: a grant (opening or continuation) MAY be reduced — always by lowering grantedUnits, never by shortening the validity window (a shorter window with the same units would raise the implied rate) — and never below the declared minimum rate. Such a response carries rateReduced: true and reduceCause: "CAPACITY"; the client can compute the allowed rate exactly from grantedUnits, grantIssuedAt, and expiresAt. Three guarantees travel with it:

  • The price never changes with the rate. A slowdown is never a surcharge.

  • The cause is always capacity. Commercial throttling has no code path in this protocol — a margin decision is a tariff term to be declared in the plan, never an operational throttle.

  • It is disclosed. The reduction is flagged on the wire and recorded per party, so "what was reduced, when, and why" is answerable per customer, not only per pool.

Degradation lands at exchange boundaries only — there is no push channel — which is one reason validity windows are short. Below the minimum rate the server answers honestly instead: STOP (NO_CAPACITY) at a report boundary, refusal at authorisation.

12.4. Report promptness

Rate enforcement is defeated by stale reports: a gateway batching every thirty seconds defeats a per-second pool while passing every other check. Promptness — measured as the elapsed time from a unit of consumption occurring to dispatch of the report that first carries it, evaluated at the 95th percentile — is a tested dimension of an integration's profile (alongside authorise-before-serving and stop obedience): a bound the conformance suite measures, never a documentation claim. The bound is agreed per integration at onboarding; the version-1 default profile bound is 30 s at p95 (provisional — subject to ratification with the rate-enforcement window design; integrations should treat the mechanism as fixed and the number as negotiable). Threshold-triggered reports (Section 7.7) and the closing report are due immediately regardless of the profile bound.

13. Error Handling

13.1. HTTP status mapping

Table 18
Status Meaning
200 Charging outcome — including refusals and stop obligations; inspect decision/directive.
400 Malformed or out-of-limit request; machine errorCode included (Section 13.2).
401 Missing/invalid credential.
403 Credential valid but not authorised: tenant not in the bound set, the Action belongs to another gateway, or a parentActionId references another gateway's Action (Section 9, Section 15.3).
404 Unknown or mismatched identity: an actionId/grantId the server does not recognise (never opened, or past the late window, Section 7.6), a grantId that does not belong to the body's actionId, or tenantRef/accountRef that do not match the Action's — traffic is unattributable.
405 / 415 Wrong method / wrong content type (Section 4.1).
429 Per-credential rate limit exceeded (Section 15.5). MUST carry Retry-After. When rate-limited, the client prioritises mandatory reports (threshold crossings, STOP flushes, pre-lapse) over new Authorises.
503 + Retry-After The server's own condition — transient contention or overload — on its own channel. Overload is never expressed as a refusal reason: no decision about the caller was reached, nothing is charged, and a shed request is not counted as a refusal. The client SHOULD retry after the indicated delay with the identical request.

Charging refusals are not HTTP errors: a REFUSE or STOP arrives as 200 — including DUPLICATE_ACTION (an identity conflict is a charging refusal, not an HTTP error). Clients MUST NOT treat 200 as permission without inspecting the body.

Error envelope: every non-200 response carries a JSON body {errorCode, message, serverTime, maxSchemaVersion, retryable?, retryAfter?}errorCode from Section 13.2 for the 400 family or a status-mirroring code otherwise (UNAUTHORIZED, FORBIDDEN, NOT_FOUND, METHOD_NOT_ALLOWED, UNSUPPORTED_MEDIA, RATE_LIMITED, UNAVAILABLE, INTERNAL); schemaVersion is echoed when it was parseable (Section 5). Error envelopes carry no Action-state fields (Section 2, Envelope).

Atomicity (normative): request processing is all-or-nothing. Full validation precedes any state mutation, and every non-200 response is state-neutral — nothing merged, counted, or settled. Settlement and the stored exchange outcome persist atomically; a crash between processing and durable storage means the request was not processed, and the identical retry is safe.

13.2. errorCode registry (400 family, version 1)

Table 19
Code Meaning
UNSUPPORTED_SCHEMA Unknown schemaVersion, or fields beyond the declared version.
LIMIT_EXCEEDED A Section 4.4 limit violated.
MALFORMED_REPORT Bad unit value, undeclared unit key, duplicate callId entries, duplicate JSON keys.
MALFORMED_REQUEST Any other syntactic/semantic malformation.

13.3. Client retry guidance

Idempotency (Section 6.5, Section 7.2) makes retries safe: on timeout or 5xx, the client SHOULD retry the identical request with bounded, jittered exponential backoff (RECOMMENDED: initial 1 s, factor 2, full jitter, cap 30 s). The serialisation unit of Section 3.4 is the logical Report: identical retries are the same unit, and overlapping identical attempts (a retry racing its timed-out original) are permitted — replay makes them harmless. The admission freeze ends on a charging outcome (any 200); state-neutral errors (4xx/429/503) leave it in place while retries continue, until an outcome arrives or the grant expires. The client MUST NOT mint a fresh actionId to retry an Action whose authorisation outcome is unknown — that risks double authorisation; retry the original request instead, or abandon the actionId permanently. Retrying a Report remains useful up to the late window; retrying an Authorise past the point the work would still be served is pointless — abandon it; if the work is re-attempted later, that is a new Action.

13.4. Charging-plane unavailability — the client state machine

When the server is slow or unreachable, the existing mechanisms compose into a complete, bounded posture; a conforming gateway follows it:

  1. Mid-Action: per Section 3.4's single-writer rule, once the report is dispatched no new Calls are admitted until a response arrives — retrying included. Work already begun (the tail) completes within the remaining pool of the last received grant; consumption from uncancellable streams is reported when connectivity returns (late, if need be — the late window exists for this).

  2. At pool exhaustion with no response: stop admitting new Calls. Serving beyond the pool is non-conformant regardless of the outage.

  3. At expiresAt with no successful report: the gateway MUST stop serving the Action — validity is the hard bound, and the server will settle at the reported meter via the lapse ending. Consumption reported late still settles while late-settlement capability holds (Section 7.6). Its exceptional degradation is the one bounded exception, and it is flagged unattributable, never silent.

  4. New Actions: when Authorise is unreachable, a conforming gateway does not serve — fail-closed is the only conformance posture this protocol defines. (Informative: a tenant may contractually accept unauthorized serving during outages; such traffic is outside this protocol — never authorised, never reported, never settled here — and is accounted between the parties as declared loss. Pulse neither forbids the business arrangement nor provides a wire path for it.)

  5. Recovery: retry identical requests (Section 6.5/Section 7.3 replay makes every lost response recoverable), or report against the envelope's currentGrantId; reconcile via receipts (settlementId, acceptedCumulative, acceptedCalls).

This post-grace, late-settleable example illustrates the end-to-end posture. The gateway's local hard bounds remain the received grant's remaining pool and expiresAt; new Authorises fail closed during the outage. Other recovery branches remain as specified above.

Gateway                          Charging Server
   |                                    |
   |  outage begins; a grant is live    |
   |  serve only within remaining pool  |
   |--- Report R at reportAt --------X  |  unreachable
   |  no new Calls; the tail finishes   |
   |--- identical retries of R ------X  |
   |  at expiresAt: stop serving        |
   |                                    |
   |  connectivity recovers at          |
   |  now >= expiresAt + 5 s            |
   |--- identical retry of R ---------->|
   |                                    |  LAPSE and late
   |                                    |  handling
   |<-- STOP (LAPSED) + receipt --------|
   |                                    |
   |--- final-meter flush ------------->|  within 30 s of
   |    (tail included)                 |  STOP
   |<-- STOP (LAPSED) + receipt --------|

The exposure of the outage posture is: the remaining granted pool, plus the consumption of Calls already in flight when the outage began — each such Call bounded by the call-level caps (Section 4.4 magnitudes; limits.maxRuntimeSec) and settling late. Uncancellable in-flight work is the one component the pool does not cap; the protocol bounds it per Call and prices it under the pinned law, never silently.

13.5. Operational expectations (informative)

Authorise and Report sit on the serving hot path; the server is designed to answer from hot state and the RECOMMENDED client deadline is 2 s per attempt. For latency-critical cheap actions, the flat price form (Section 3.3) is the minimal-overhead shape (one exchange, no lifecycle). Control-plane request rate scales with Actions × cycles; the server controls the cycle rate via reportAt/grantedUnits sizing, and HTTP/2 connection reuse is assumed. Pre-grants, bulk authorisation, and multi-Action batch reporting are deferred, named (Section 1.4).

14. Conformance Requirements Summary (client)

  1. Send canonical, versioned, within-limits bodies (Section 4, Section 5).

  2. Never serve lifecycle work without a live grant (a flat Action's work is authorised by its settled GRANT itself, Section 3.3); honour stop obligations promptly (Section 3.2).

  3. Report cumulatively, never by delta; never shrink a cumulative (Section 7.2).

  4. Report on reportAt crossing (MUST) and SHOULD report promptly at client-side endings with final: true (Section 7.7).

  5. Retry with canonically identical requests; never reuse an actionId (Section 6.5, Section 13.3).

  6. Tolerate unknown reason/code strings (Section 5).

  7. Never knowingly exceed the granted pool; admit Calls against it using unitWeights (Section 3.4), and follow the Section 13.4 posture when the charging plane is unreachable.

  8. Treat the server clock as authoritative for lapse and bands (Section 3.4, Section 10).

  9. Report promptly — within the integration's tested promptness bound (Section 12.4); batched, stale reports defeat rate enforcement and downgrade the integration's profile.

  10. Render refusals honestly: no more certainty than grade holds, capacity refusals never as balance problems, and attributed limits (limitOwner) never as the user's own (Section 6.4, Section 12).

  11. Obey the envelope over any stored body: discard grants with a stale grantSeq, treat actionLive: false as a stop, and converge on currentGrantSeq (Section 2, Section 6.3, Section 7.3).

  12. On STOP: admit nothing new, cancel what is cancellable, and flush the final meter — first chunk within 30 seconds, once per ending (Section 7.7).

  13. One in-flight Report per Action, and no new Call admissions while one is outstanding (Section 3.4).

15. Security Considerations

15.1. Transport and credentials

TLS 1.2+ is REQUIRED (1.3 RECOMMENDED; [RFC9325] configuration). API keys are secrets: gateways MUST store them in a secrets facility, MUST NOT place them in URLs, and MUST redact them from logs and traces; the server never echoes a key and audits by credential id only. Dual-active rotation rolls keys without downtime. Compromise of a key is bounded by revocation (effective within seconds), tenant binding, gateway ownership, and the per-credential rate limit. Revocation and outstanding grants: a revoked credential's requests fail 401 from the revocation instant; Actions its gateway opened continue only to their grants' lapse, ending and settling at their reported meters — the residual exposure is the remaining validities. Because ownership is gateway-level, a replacement credential of the same gateway MAY report and close them; nothing strands.

15.2. Tenant binding

Every request is authorised as: credential enabled → tenantRef within the credential's bound set (else 403) → subscriber belongs to that tenant (else NO_LAW). A credential can never operate on another tenant's subscribers.

15.3. Gateway ownership of Actions

Every Action records the opening credential's gateway. Reports and Authorise-replays are accepted only from credentials of that gateway (else 403) — a sibling gateway of the same tenant cannot operate another gateway's Actions, and a child Action MUST be opened by the parent's owning gateway (Section 9; checked before any charging evaluation). Identifiers are correlators, not secrets; authority derives from credential + binding + ownership, so sequential or guessable ids confer no authority. Acknowledged residual: the 403-vs-404 distinction lets an authenticated gateway of the same tenant probe whether an identifier exists — existence only, never content or authority; accepted and documented.

15.4. Abuse limits

Always enforced per subscriber: a concurrent-Actions cap and a calls-per-Action limit. Optionally (per deployment/plan): velocity limits, consumption-inflation wires, maximum runtime, and required call signatures. These stop Actions; only operator action bars a subscriber.

15.5. API-level protection

A per-credential request rate limit (token bucket; 429 on excess) protects the platform independently of charging-level velocity. Every request is audit-recorded by credential id; the key itself never appears in records.

15.6. Clock authority and posture

Band selection and lapse are computed exclusively from the server clock (Section 3.4), removing client-asserted-time gaming. The gateway MAY report its own clock (gatewayTime) and per-call start instants (startedAt, Section 7.2); both are recorded as evidence and never enforced against — a wrong clock is a fact about the gateway's estate, not a verdict about the customer whose call it carries. Persistent skew beyond tolerance is a trust signal on the integration's profile, never a refusal.

15.7. Trust model and privacy

The gateway is a trusted metering point. Its reports are attestations of consumption, not proofs: the protocol bounds what a dishonest or compromised gateway can cost (grants, ceilings, caps, tenant binding, gateway ownership, revocation, rate limits) but cannot verify token counts it did not observe. The residual — systematic under- or mis-reporting — is addressed outside the request path: integration grading by the conformance suite, and slow-clock observation of the executor. Stated plainly so no one mistakes the boundary: a tenant's spend guarantee holds against its subscribers; the operator's protection against a dishonest metering point is detection and contract, not cryptography, in version 1.

Every version-1 decision is automated. No human participates in the request path — for a grant, a refusal, or a stop. A refusal says so on the wire (automatedDecision, Section 6.4) and MAY carry a tenant-configured human-review channel (reviewContact), so a subscriber facing an adverse automated decision has a route to a person; the notice travels with the decision it describes rather than living only in documentation.

No money crosses outward. Grants and receipts are denominated in work units only; a gateway never learns a balance, a price, or a currency. This is deliberate — a commercial and privacy boundary, not an omission.

Privacy. accountRef SHOULD be a pseudonymous, tenant-scoped identifier (never a raw MSISDN, e-mail, or name); the server treats it as an opaque key. limitOwner is a presentation-safe display label declared by the sponsor with its relation — never a legal identity the tenant did not choose to show. gatewayTime, startedAt, and per-request audit records are retained under the operator's retention policy; the protocol itself mandates retention only for the late window. Refusal metadata deliberately reveals coarse state (which kind of limit bound); tenants choosing per-subscriber secrecy beyond that boundary configure presentation, not the wire.

16. Implementation Status

This section records the implementation status of the protocol described in this document, as encouraged by [RFC7942]. It is meant to be removed before publication as an RFC.

At the time of writing, one server-side implementation of this protocol exists, built into a commercial telecom online charging system, together with a conformance suite exercising the state machine of Section 3.5, the cumulative reporting rules of Section 7.2, and the lapse boundary of Section 7.5. No independent interoperating implementation exists. Readers should treat the capacity-enforcement semantics of Section 12.2 to Section 12.4 as provisional on that basis.

17. IANA Considerations

This document establishes the registries below. For each registry, the placeholder registration policy mirrors Section 5: the registries are owned by this document, and new values are allocated only by a new schema version of this specification — a Standards-Action-style posture in the [RFC8126] taxonomy, not Specification Required. The cited sections, not this summary, define the initial values and their meanings.

Table 20
Registry Initial contents Defining section
Pulse Decision GRANT, REFUSE Section 6.3, Section 6.4
Pulse Refusal Reason NO_HEADROOM, CEILING_PREEMPT, ACCOUNT_VELOCITY, BALANCE_VELOCITY, GROUP_VELOCITY (version 1.1), MAX_CONCURRENT_ACTIONS, PARENT_STOPPED, NO_LAW, DRAINING, BLOCKED_TARIFF, SERVICE_BARRED, DUPLICATE_ACTION, EXPOSURE_CAP, NO_CAPACITY, PREEMPTED, CLASS_BLOCKED (reserved in version 1) Section 6.4
Pulse Stop Reason CEILING_REACHED, NO_HEADROOM, CALL_LIMIT, MAX_RUNTIME, INFLATION, SIGNATURES_REQUIRED, PARENT_STOPPED, ADMIN_FORCED, DRAINING, NO_CAPACITY, EXPOSURE_CAP, LAPSED Section 7.4
Pulse Error Code UNSUPPORTED_SCHEMA, LIMIT_EXCEEDED, MALFORMED_REPORT, MALFORMED_REQUEST, UNAUTHORIZED, FORBIDDEN, NOT_FOUND, METHOD_NOT_ALLOWED, UNSUPPORTED_MEDIA, RATE_LIMITED, UNAVAILABLE, INTERNAL Section 13.1, Section 13.2
Pulse Ending CLOSE, STOP, LAPSE Section 2, Section 3.5
Pulse Directive CONTINUE, CURRENT, SETTLED, STOP Section 7.3
Pulse Grade PROVEN, ATTESTED, BOUNDED Section 6.4
Pulse Reduce Cause CAPACITY Section 12.3

Formal IANA text, including final registry names and the formal [RFC8126] policy designations, is completed at publication.

18. References

18.1. Normative References

[RFC2119]
Bradner, S., "Key words for use in RFCs to Indicate Requirement Levels", BCP 14, RFC 2119, DOI 10.17487/RFC2119, , <https://www.rfc-editor.org/rfc/rfc2119>.
[RFC3339]
Klyne, G. and C. Newman, "Date and Time on the Internet: Timestamps", RFC 3339, DOI 10.17487/RFC3339, , <https://www.rfc-editor.org/rfc/rfc3339>.
[RFC5246]
Dierks, T. and E. Rescorla, "The Transport Layer Security (TLS) Protocol Version 1.2", RFC 5246, DOI 10.17487/RFC5246, , <https://www.rfc-editor.org/rfc/rfc5246>.
[RFC8174]
Leiba, B., "Ambiguity of Uppercase vs Lowercase in RFC 2119 Key Words", BCP 14, RFC 8174, DOI 10.17487/RFC8174, , <https://www.rfc-editor.org/rfc/rfc8174>.
[RFC8259]
Bray, T., Ed., "The JavaScript Object Notation (JSON) Data Interchange Format", STD 90, RFC 8259, DOI 10.17487/RFC8259, , <https://www.rfc-editor.org/rfc/rfc8259>.
[RFC8446]
Rescorla, E., "The Transport Layer Security (TLS) Protocol Version 1.3", RFC 8446, DOI 10.17487/RFC8446, , <https://www.rfc-editor.org/rfc/rfc8446>.
[RFC8610]
Birkholz, H., Vigano, C., and C. Bormann, "Concise Data Definition Language (CDDL): A Notational Convention to Express Concise Binary Object Representation (CBOR) and JSON Data Structures", RFC 8610, DOI 10.17487/RFC8610, , <https://www.rfc-editor.org/rfc/rfc8610>.
[RFC8785]
Rundgren, A., Jordan, B., and S. Erdtman, "JSON Canonicalization Scheme (JCS)", RFC 8785, DOI 10.17487/RFC8785, , <https://www.rfc-editor.org/rfc/rfc8785>.
[RFC9110]
Fielding, R., Ed., Nottingham, M., Ed., and J. Reschke, Ed., "HTTP Semantics", STD 97, RFC 9110, DOI 10.17487/RFC9110, , <https://www.rfc-editor.org/rfc/rfc9110>.
[RFC9325]
Sheffer, Y., Saint-Andre, P., and T. Fossati, "Recommendations for Secure Use of Transport Layer Security (TLS) and Datagram Transport Layer Security (DTLS)", BCP 195, RFC 9325, DOI 10.17487/RFC9325, , <https://www.rfc-editor.org/rfc/rfc9325>.

18.2. Informative References

[RFC4006]
Hakala, H., Mattila, L., Koskinen, J., Stura, M., and J. Loughney, "Diameter Credit-Control Application", RFC 4006, DOI 10.17487/RFC4006, , <https://www.rfc-editor.org/rfc/rfc4006>.
[RFC7942]
Sheffer, Y. and A. Farrel, "Improving Awareness of Running Code: The Implementation Status Section", BCP 205, RFC 7942, DOI 10.17487/RFC7942, , <https://www.rfc-editor.org/rfc/rfc7942>.
[RFC8126]
Cotton, M., Leiba, B., and T. Narten, "Guidelines for Writing an IANA Considerations Section in RFCs", BCP 26, RFC 8126, DOI 10.17487/RFC8126, , <https://www.rfc-editor.org/rfc/rfc8126>.
[RFC8506]
Bertz, L., Ed., Dolson, D., Ed., and Y. Lifshitz, Ed., "Diameter Credit-Control Application", RFC 8506, DOI 10.17487/RFC8506, , <https://www.rfc-editor.org/rfc/rfc8506>.

Appendix A. Examples (illustrative, not normative; abbreviated)

Except for A.7, the 200 responses below omit, for brevity only: serverTime, actionLive, currentGrantSeq, currentGrantId, unattributable where applicable, grantIssuedAt, lawVersion, limits, and settlement receipts where a settling response requires one — all present in real responses under Section 2/Section 6.3/Section 7.3. A refusal of an unopened Action (A.5) and an error envelope (A.7) carry no Action-state fields; A.7 intentionally shows serverTime. maxSchemaVersion is shown throughout. Every field shown conforms to the Section 4.3 type rules, except A.7's deliberately malformed estimate — a JSON number where Section 4.3 requires a canonical decimal string, the defect that example exists to show.

A.1. Authorise → grant

POST /ocs/ai/v1/authorizations
{
  "schemaVersion": 1,
  "actionId": "app-7f3e:conv-2091:task-4",
  "tenantRef": "4102",
  "accountRef": "subscriber-889201",
  "actionType": "chat.frontier",
  "estimate": { "tokens_in": "12000", "tokens_out": "4000" }
}

200
{
  "schemaVersion": 1, "maxSchemaVersion": 1,
  "decision": "GRANT",
  "grantId": "g-000117", "grantSeq": 1,
  "grantedUnits": "50000",
  "reportAt": "40000",
  "expiresAt": "2026-09-01T10:15:00.000Z",
  "unitWeights": { "tokens_in": "1", "tokens_out": "4" },
  "timeBandId": "offpeak"
}

A.2. Report → continue

POST /ocs/ai/v1/grants/g-000117/reports
{
  "schemaVersion": 1,
  "actionId": "app-7f3e:conv-2091:task-4",
  "tenantRef": "4102", "accountRef": "subscriber-889201",
  "final": false,
  "calls": [
    { "callId": "c1",
      "units": { "tokens_in": "9100", "tokens_out": "7800" } }
  ]
}

200
{
  "schemaVersion": 1, "maxSchemaVersion": 1,
  "directive": "CONTINUE",
  "grantId": "g-000118", "grantSeq": 2,
  "grantedUnits": "50000",
  "reportAt": "40000",
  "expiresAt": "2026-09-01T10:30:00.000Z",
  "unitWeights": { "tokens_in": "1", "tokens_out": "4" },
  "timeBandId": "offpeak"
}

A.3. Final report → settled

POST /ocs/ai/v1/grants/g-000118/reports
{
  "schemaVersion": 1,
  "actionId": "app-7f3e:conv-2091:task-4",
  "tenantRef": "4102", "accountRef": "subscriber-889201",
  "final": true,
  "calls": [ { "callId": "c1",
               "units": { "tokens_in": "9100",
                          "tokens_out": "9950" } } ]
}

200
{ "schemaVersion": 1, "maxSchemaVersion": 1,
  "directive": "SETTLED",
  "endedAt": "2026-09-01T10:19:42.114Z" }

A.4. Stop obligation

200
{ "schemaVersion": 1, "maxSchemaVersion": 1,
  "directive": "STOP", "stopReason": "CEILING_REACHED",
  "endedAt": "2026-09-01T10:12:03.551Z" }

A.5. Authorise → refuse metadata

The two abbreviated outcomes below show the distinct conditional metadata rules of Section 6.4: retryAfter accompanies a retryable refusal, while the sponsor's party-scoped bound carries limitOwner. Both carry the automated-decision notice; the second additionally shows a tenant-configured human-review channel. This is a version-1.1 exchange (schemaVersion: 2, Section 5) — the disclosure fields belong to schema version 2; the other examples show version-1.0 exchanges. They are alternatives for separate Authorise attempts, not two responses to one request.

POST /ocs/ai/v1/authorizations
{
  "schemaVersion": 2,
  "actionId": "app-7f3e:capacity-check",
  "tenantRef": "4102",
  "accountRef": "subscriber-889201",
  "actionType": "chat.frontier"
}

200 (capacity refusal)
{
  "schemaVersion": 2, "maxSchemaVersion": 2,
  "decision": "REFUSE", "refusalReason": "NO_CAPACITY",
  "retryable": true, "retryAfter": 5, "grade": "BOUNDED",
  "automatedDecision": true
}

200 (sponsor-cap refusal)
{
  "schemaVersion": 2, "maxSchemaVersion": 2,
  "decision": "REFUSE", "refusalReason": "EXPOSURE_CAP",
  "retryable": false, "grade": "PROVEN",
  "limitOwner": "Example AI Benefit Sponsor",
  "automatedDecision": true,
  "reviewContact": "support.example-tenant.com/spend-review"
}

A.6. Stale report → CURRENT

This exchange happens between A.2 and A.3, while the Action is still LIVE — after A.3's close, the same stale body would instead draw the terminal directive (Section 3.5, ENDED rows). This non-final body differs from the retained report for g-000117, so it is not an identity replay. Both values are below the stored high-water after A.2; the max-merge adds zero, no ending condition holds, and the standing grant is re-presented (Section 3.5, Section 7.3). The map rendering follows Section 7.3's weighted-difference description; Appendix B, schema note 5, records the conflicting domain wording and keeps this representation provisional.

POST /ocs/ai/v1/grants/g-000117/reports
{
  "schemaVersion": 1,
  "actionId": "app-7f3e:conv-2091:task-4",
  "tenantRef": "4102", "accountRef": "subscriber-889201",
  "final": false,
  "calls": [
    { "callId": "c1",
      "units": { "tokens_in": "9000", "tokens_out": "7700" } }
  ]
}

200
{
  "schemaVersion": 1, "maxSchemaVersion": 1,
  "directive": "CURRENT",
  "grantId": "g-000118", "grantSeq": 2,
  "grantedUnits": "50000", "reportAt": "40000",
  "expiresAt": "2026-09-01T10:30:00.000Z",
  "unitWeights": { "tokens_in": "1", "tokens_out": "4" },
  "timeBandId": "offpeak",
  "baselineCumulative": { "tokens_in": "9100",
    "tokens_out": "7800" },
  "meterCumulative": { "tokens_in": "9100", "tokens_out": "7800" }
}

A.7. Error envelope

Here an Authorise estimate uses a JSON number instead of the canonical decimal string required by Section 4.3, so the request fails state-neutrally with the Section 13.1 envelope.

POST /ocs/ai/v1/authorizations
{
  "schemaVersion": 1,
  "actionId": "app-7f3e:conv-2091:task-bad",
  "tenantRef": "4102",
  "accountRef": "subscriber-889201",
  "actionType": "chat.frontier",
  "estimate": { "tokens_in": 12000 }
}

400
{
  "schemaVersion": 1,
  "errorCode": "MALFORMED_REQUEST",
  "message": "estimate values must be canonical decimal strings",
  "serverTime": "2026-09-01T10:20:11.006Z",
  "maxSchemaVersion": 1
}

Appendix B. Message schemas (CDDL, provisional)

The schemas use CDDL [RFC8610] to describe the JSON bodies. They do not define an additional wire format. Settlement-receipt members are top-level response members; there is no nested receipt object. Regular expressions encode the principal lexical restrictions; comments retain the calendar and character-class details not usefully repeated in the message shapes. The schemas describe the current protocol version (1.1, schemaVersion 2); members and values whose comment says version 1.1 belong to schema version 2 (Section 5) — a schema-version-1 exchange omits those fields, while the new value may still arrive under Section 5's tolerance rule.

; ---------- Lexical and numeric types (section 4.3) ----------

schema-version = 1..9007199254740991
count = 0..9007199254740991
generation = 1..9007199254740991
positive-count = 1..9007199254740991

; At most 128 characters from [A-Za-z0-9._:-].
identifier = tstr .regexp "[A-Za-z0-9._:-]{0,128}"

; [a-z0-9_]{1,64}.
unit-type = tstr .regexp "[a-z0-9_]{1,64}"

; The expression fixes the wire layout. Year range and calendar
; validity remain
; subject to section 4.3.
timestamp = tstr .regexp
  "[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}\\.[0-9]{3}Z"

; At most 64 printable UTF-8 characters, excluding control and bidi
; controls.
display-label = tstr

; Each decimal below is a canonical decimal JSON string with scale at
; most 6.
request-decimal = tstr .regexp
  "1000000000|(0|[1-9][0-9]{0,8})(\\.[0-9]{0,5}[1-9])?"
pooled-decimal = tstr .regexp
  "(0|[1-9][0-9]{0,16})(\\.[0-9]{0,5}[1-9])?"
positive-pooled-decimal = tstr .regexp
  "0\\.[0-9]{0,5}[1-9]|[1-9][0-9]{0,16}(\\.[0-9]{0,5}[1-9])?"
positive-weight = tstr .regexp
  "1000000|0\\.[0-9]{0,5}[1-9]|[1-9][0-9]{0,5}(\\.[0-9]{0,5}[1-9])?"
canonical-decimal = tstr .regexp
  "(0|[1-9][0-9]*)(\\.[0-9]{0,5}[1-9])?"

request-unit-map = { *16 unit-type => request-decimal }
cumulative-unit-map = { *16 unit-type => request-decimal }
weight-map = { *16 unit-type => positive-weight }

; The prose leaves this as a representation ambiguity (schema note
; 5), but
; both members of one response have the same representation.
current-meter-fields =
  (
    baselineCumulative: cumulative-unit-map,
    meterCumulative: cumulative-unit-map
  )
  //
  (
    baselineCumulative: pooled-decimal,
    meterCumulative: pooled-decimal
  )

; ---------- Closed value sets (sections 5, 6.4, 7.4) ----------

ending = "CLOSE" / "STOP" / "LAPSE"
directive = "CONTINUE" / "CURRENT" / "SETTLED" / "STOP"
grade = "PROVEN" / "ATTESTED" / "BOUNDED"
reduce-cause = "CAPACITY"

error-code =
    "UNSUPPORTED_SCHEMA"
  / "LIMIT_EXCEEDED"
  / "MALFORMED_REPORT"
  / "MALFORMED_REQUEST"
  / "UNAUTHORIZED"
  / "FORBIDDEN"
  / "NOT_FOUND"
  / "METHOD_NOT_ALLOWED"
  / "UNSUPPORTED_MEDIA"
  / "RATE_LIMITED"
  / "UNAVAILABLE"
  / "INTERNAL"

stop-reason-without-owner =
    "CEILING_REACHED"
  / "NO_HEADROOM"
  / "CALL_LIMIT"
  / "MAX_RUNTIME"
  / "INFLATION"
  / "SIGNATURES_REQUIRED"
  / "PARENT_STOPPED"
  / "ADMIN_FORCED"
  / "DRAINING"
  / "NO_CAPACITY"
  / "LAPSED"

stop-fields =
  (
    stopReason: "EXPOSURE_CAP",
    limitOwner: display-label
  )
  //
  (
    stopReason: stop-reason-without-owner
  )

; ---------- Shared response envelope (sections 2, 5, 6.3) ----------

versioned-success = (
  schemaVersion: schema-version,
  maxSchemaVersion: schema-version,
  serverTime: timestamp
)

action-envelope = (
  actionLive: bool,
  currentGrantSeq: generation,
  currentGrantId: identifier,
  unattributable: bool
)

live-action-envelope = (
  actionLive: true,
  currentGrantSeq: generation,
  currentGrantId: identifier,
  unattributable: bool
)

ended-action-envelope = (
  actionLive: false,
  currentGrantSeq: generation,
  currentGrantId: identifier,
  unattributable: bool
)

ended-unattributable-envelope = (
  actionLive: false,
  currentGrantSeq: generation,
  currentGrantId: identifier,
  unattributable: true
)

; serverTime and maxSchemaVersion occur on every response. The four
; Action
; fields occur on every 200 that identifies an opened Action. They
; are absent
; from error envelopes and refusals of unopened Actions.

; ---------- Shared lifecycle-grant fields (section 6.3) ----------

limits = {
  maxCallsPerAction: count,
  maxRuntimeSec: count,
  maxConcurrentActions: count,
  ? signaturesRequired: bool,
  ? capacity-limit-fields
}

capacity-limit-fields = (
  minRate: canonical-decimal,
  enforcementWindowSec: positive-count
)

reduced-grant-fields = (
  rateReduced: true,
  reduceCause: reduce-cause
)

lifecycle-grant-fields = (
  grantId: identifier,
  grantSeq: generation,
  grantIssuedAt: timestamp,
  grantedUnits: positive-pooled-decimal,
  reportAt: positive-pooled-decimal,
  expiresAt: timestamp,
  unitWeights: weight-map,
  ? quantumUnits: positive-pooled-decimal,
  ? timeBandId: identifier,
  lawVersion: count,
  limits: limits,
  ? reduced-grant-fields
)

; ---------- Settlement receipts (sections 3.3, 6.5, 7.3) ----------

accepted-call = {
  callId: identifier,
  units: request-unit-map
}

receipt-core = (
  settlementId: identifier,
  acceptedCumulative: cumulative-unit-map,
  cyclePooledUnits: pooled-decimal,
  ? clamped: true
)

; Required on a settling Report response. acceptedCalls has exactly
; the Calls
; present in that Report request.
fresh-report-receipt = (
  receipt-core,
  acceptedCalls: [*256 accepted-call]
)

late-report-receipt = (
  receipt-core,
  acceptedCalls: [*256 accepted-call],
  adjustment: true
)

terminal-settlement-receipt =
  fresh-report-receipt // late-report-receipt

; Used for the "current receipt" on a zero-merge terminal response.
terminal-current-receipt = (
  receipt-core,
  ? acceptedCalls: [*256 accepted-call]
)

; acceptedCalls appears on Report responses only.
authorise-settlement-receipt = (
  receipt-core,
  ? adjustment: true
)

; The accepted baseline is the only member explicitly named for a
; live
; Authorise re-presentation; the remaining receipt members are
; provisional.
authorise-baseline-receipt = (
  acceptedCumulative: cumulative-unit-map,
  ? settlementId: identifier,
  ? cyclePooledUnits: pooled-decimal,
  ? clamped: true
)

; A flat opening has no units to accept.
flat-opening-receipt = (
  settlementId: identifier
)

; ---------- Authorise request (section 6.1) ----------

authorise-request = {
  schemaVersion: schema-version,
  actionId: identifier,
  tenantRef: identifier,
  accountRef: identifier,
  actionType: identifier,
  ? estimate: request-unit-map,
  ? scope: identifier,
  ? parentActionId: identifier,
  ? gatewayTime: timestamp
}

; ---------- Authorise GRANT responses (sections 3.3, 6.3, 6.5)
; ----------

authorise-grant-response =
    authorise-live-grant-response
  / authorise-flat-grant-response
  / authorise-terminal-grant-response

authorise-live-grant-response = {
  versioned-success,
  live-action-envelope,
  decision: "GRANT",
  lifecycle-grant-fields,
  ? authorise-baseline-receipt
}

authorise-flat-grant-response = {
  versioned-success,
  ended-action-envelope,
  decision: "GRANT",
  grantId: identifier,
  grantSeq: generation,
  ? timeBandId: identifier,
  settled: true,
  endedAt: timestamp,
  ? ending: "CLOSE",
  flat-opening-receipt
}

last-grant-reference = (
  grantId: identifier,
  grantSeq: generation
)

authorise-terminal-grant-response = {
  versioned-success,
  ended-action-envelope,
  decision: "GRANT",
  ? last-grant-reference,
  ? settled: true,
  ending: ending,
  endedAt: timestamp,
  authorise-settlement-receipt
}

; ---------- Authorise REFUSE response (section 6.4) ----------

unopened-refusal-fields =
  (
    refusalReason: "NO_CAPACITY" / "PREEMPTED",
    retryable: true,
    retryAfter: count,
    grade: "BOUNDED",
    ? limitOwner: display-label
  )
  //
  (
    refusalReason: "DRAINING",
    retryable: true,
    retryAfter: count,
    grade: "ATTESTED"
  )
  //
  (
    refusalReason:
        "ACCOUNT_VELOCITY"
      / "BALANCE_VELOCITY"
      / "GROUP_VELOCITY"        ; v1.1 (schemaVersion 2)
      / "MAX_CONCURRENT_ACTIONS",
    retryable: true,
    retryAfter: count,
    grade: "PROVEN",
    ? limitOwner: display-label
  )
  //
  (
    refusalReason: "SERVICE_BARRED",
    retryable: false,
    ? retryAfter: count,
    grade: "ATTESTED"
  )
  //
  (
    refusalReason: "EXPOSURE_CAP",
    retryable: false,
    ? retryAfter: count,
    grade: "PROVEN",
    limitOwner: display-label
  )
  //
  (
    refusalReason:
        "NO_HEADROOM"
      / "CEILING_PREEMPT",
    retryable: false,
    ? retryAfter: count,
    grade: "PROVEN",
    ? limitOwner: display-label
  )
  //
  (
    refusalReason: "PARENT_STOPPED" / "NO_LAW" / "BLOCKED_TARIFF",
    retryable: false,
    ? retryAfter: count,
    grade: "PROVEN"
  )

duplicate-refusal-fields = (
  refusalReason: "DUPLICATE_ACTION",
  retryable: false,
  ? retryAfter: count,
  grade: "PROVEN"
)

authorise-refuse-response =
    authorise-unopened-refuse-response
  / authorise-duplicate-refuse-response

authorise-unopened-refuse-response = {
  versioned-success,
  decision: "REFUSE",
  automatedDecision: true,      ; v1.1 (schemaVersion 2)
  ? reviewContact: display-label, ; v1.1 (schemaVersion 2)
  unopened-refusal-fields
}

authorise-duplicate-refuse-response = {
  versioned-success,
  decision: "REFUSE",
  automatedDecision: true,      ; v1.1 (schemaVersion 2)
  ? reviewContact: display-label, ; v1.1 (schemaVersion 2)
  duplicate-refusal-fields,
  ? action-envelope
}

; CLASS_BLOCKED is reserved and never emitted in version 1.

; ---------- Report request (sections 7.1, 7.2) ----------

report-call = {
  callId: identifier,
  units: request-unit-map,
  ? startedAt: timestamp,       ; v1.1 (schemaVersion 2)
  ? signature: tstr
}

report-request = {
  schemaVersion: schema-version,
  actionId: identifier,
  tenantRef: identifier,
  accountRef: identifier,
  calls: [*256 report-call],
  final: bool,
  ? gatewayTime: timestamp
}

; The path grantId participates in request identity but is not a body
; member.

; ---------- Report responses (sections 3.5, 7.3, 7.6) ----------

report-response =
    report-continue-response
  / report-current-response
  / report-stop-response
  / report-settled-response

; actionLive can be false when a retained CONTINUE outcome is
; replayed after
; the Action has ended; the fresh envelope overrides the stored
; grant.
report-continue-response = {
  versioned-success,
  action-envelope,
  directive: "CONTINUE",
  lifecycle-grant-fields,
  fresh-report-receipt
}

report-current-response = {
  versioned-success,
  live-action-envelope,
  directive: "CURRENT",
  lifecycle-grant-fields,
  current-meter-fields
}

report-stop-response =
    report-stop-settlement-response
  / report-stop-current-response
  / report-stop-unattributable-response

report-stop-settlement-response = {
  versioned-success,
  ended-action-envelope,
  directive: "STOP",
  stop-fields,
  endedAt: timestamp,
  terminal-settlement-receipt
}

report-stop-current-response = {
  versioned-success,
  ended-action-envelope,
  directive: "STOP",
  stop-fields,
  endedAt: timestamp,
  terminal-current-receipt
}

report-stop-unattributable-response = {
  versioned-success,
  ended-unattributable-envelope,
  directive: "STOP",
  stop-fields,
  endedAt: timestamp,
  ? terminal-current-receipt
}

report-settled-response =
    report-settled-settlement-response
  / report-settled-current-response
  / report-flat-settled-response
  / report-settled-unattributable-response

report-settled-settlement-response = {
  versioned-success,
  ended-action-envelope,
  directive: "SETTLED",
  endedAt: timestamp,
  terminal-settlement-receipt
}

report-settled-current-response = {
  versioned-success,
  ended-action-envelope,
  directive: "SETTLED",
  endedAt: timestamp,
  terminal-current-receipt
}

report-flat-settled-response = {
  versioned-success,
  ended-action-envelope,
  directive: "SETTLED",
  endedAt: timestamp,
  ? settlementId: identifier
}

report-settled-unattributable-response = {
  versioned-success,
  ended-unattributable-envelope,
  directive: "SETTLED",
  endedAt: timestamp,
  ? terminal-current-receipt
}

; A retained terminal replay has the same stored directive body and
; receipt
; as the original response, but versioned-success and action-envelope
; are
; recomputed. A replayed CONTINUE can therefore carry actionLive:
; false.
terminal-stored-outcome-replay =
    report-continue-response
  / report-stop-response
  / report-settled-response

; ---------- Error envelope (sections 5, 13.1, 13.2) ----------

error-envelope = {
  ? schemaVersion: count,
  errorCode: error-code,
  message: any,
  serverTime: timestamp,
  maxSchemaVersion: schema-version,
  ? retryable: bool,
  ? retryAfter: count
}

; schemaVersion is present only when parseable. Error envelopes never
; carry
; actionLive, currentGrantSeq, currentGrantId, or unattributable.

B.1. Schema notes — ambiguities found

  1. Section 6.5 requires a live Authorise replay to include the standing grant's "cycle-baseline receipt", but explicitly identifies only acceptedCumulative. Section 7.3 defines the remaining receipt fields for settling Report responses, while the first grant has no preceding settlement. The schema therefore makes the baseline receipt optional and makes its other members optional.

  2. An Authorise terminal acknowledgement explicitly contains decision, ending, endedAt, and the final receipt, with settled "where applicable". It does not say whether grantId and grantSeq are repeated, or enumerate the applicability of settled. The schema makes the last-grant reference and settled optional.

  3. The initial flat-GRANT state-machine row names settled and endedAt, while an ended-Action Authorise replay names ending. It is not explicit whether the initial flat response also carries ending: "CLOSE"; the schema makes it optional.

  4. The shared-envelope definition requires Action-state fields on every 200 that identifies an opened Action and excludes them on refusals of unopened Actions. It is unclear whether REFUSE / DUPLICATE_ACTION counts as identifying the already-opened Action. The unopened REFUSE shape excludes the envelope; only the duplicate-Action branch optionally permits the complete envelope.

  5. Sections 3.5 and 7.3 describe baselineCumulative and meterCumulative as meter states whose difference is weighted. Section 4.3 groups their "pooled values" with derived quantities in [0, 10^17). The examples provisionally use per-unit maps because the weighted-difference operation needs them; current-meter-fields permits either a map/map or pooled-scalar/pooled-scalar representation because the exact JSON shape and value domain require adjudication.

  6. A zero-merge terminal response carries the "current receipt", but Section 7.3 defines acceptedCalls relative to the Calls present in the request that caused a settlement. It is unclear what acceptedCalls contains for a non-settling zero-merge request, so it is optional in terminal-current-receipt.

  7. For recognised new consumption that can no longer be settled, Section 3.5 says there is no settlementId, while Section 7.6 says there is no new settlementId and also refers to the stored terminal outcome. It is unclear whether an old current receipt remains present; the unattributable terminal shapes therefore permit it optionally.

  8. A Report to a flat Action receives SETTLED and never settles. The prose does not say whether the response re-presents the flat opening receipt's settlementId; the flat Report shape therefore makes it optional.

  9. retryAfter is required when an Authorise refusal has retryable: true, but its presence is not expressly forbidden or defined when retryable: false. The false variants therefore model it as optional. The error envelope separately leaves retryable and retryAfter independently optional and does not couple them to an HTTP status.

  10. The JSON type, length, and character domain of error-envelope message are not stated in Section 4.3 or Section 13.1. The provisional schema uses any; if a JSON string is intended, an authoritative field rule is needed before regeneration.

  11. Section 7.3 does not state whether each acceptedCalls[].units map is complete over the declared unit types or contains only the keys relevant to that Call or request. The schema permits any within-limit unit map.

  12. The exact trigger for clamped: true is not fully mechanical: Section 7.3 says "limited by the meter or a ceiling", although every settlement is bounded by the meter. The schema preserves the stated optional presence and does not infer a trigger.

  13. minRate is a pooled-units-per-second decimal, but Section 4.3 does not unambiguously assign it the request, weight, or derived-pooled upper domain. The schema enforces only the canonical decimal representation pending regeneration.

  14. The 256 KB request limit does not say whether KB is decimal or binary, so CDDL cannot express an exact byte .size bound without choosing a meaning.

  15. Section 7.7 requires a Report that flushes the final meter after STOP, but does not state the value of that Report's required final field. The request schema accepts either Boolean, and the outage diagram does not choose one.

  16. Section 6.4 requires limitOwner for a party-scoped bound but does not enumerate every refusal reason that can be party-scoped. Section 12.1 makes it unconditionally present for EXPOSURE_CAP. The schema requires it there, permits it on the bound-like refusal shapes, and excludes it from conditions that the prose does not describe as bounds.

Appendix C. Worked arithmetic (illustrative)

This appendix follows one quantised lifecycle from Authorise through CLOSE. It illustrates the cumulative pipeline of Section 7.2; that section remains authoritative. All quantities are work units or pooled units — never money.

C.1. Authorise and opening grant

The Action Type declares two unit types with distinct weights and a pooled quantum of 10:

Table 21
Unit type Weight
tokens_in 0.333333
tokens_out 2.5

The gateway opens the Action with both types in its estimate:

POST /ocs/ai/v1/authorizations
{
  "schemaVersion": 1,
  "actionId": "arith-action-1",
  "tenantRef": "4102",
  "accountRef": "subscriber-arith",
  "actionType": "chat.quantised",
  "estimate": { "tokens_in": "18", "tokens_out": "10.1" }
}

The full opening GRANT is:

200
{
  "schemaVersion": 1,
  "maxSchemaVersion": 1,
  "decision": "GRANT",
  "grantId": "g-arith-1",
  "grantSeq": 1,
  "grantIssuedAt": "2026-09-01T10:00:00.000Z",
  "grantedUnits": "20",
  "reportAt": "15",
  "expiresAt": "2026-09-01T10:05:00.000Z",
  "unitWeights": { "tokens_in": "0.333333", "tokens_out": "2.5" },
  "quantumUnits": "10",
  "serverTime": "2026-09-01T10:00:00.000Z",
  "actionLive": true,
  "currentGrantSeq": 1,
  "currentGrantId": "g-arith-1",
  "unattributable": false,
  "lawVersion": 1,
  "limits": {
    "maxCallsPerAction": 10,
    "maxRuntimeSec": 600,
    "maxConcurrentActions": 4
  }
}

The first cycle baseline is the zero meter (Section 7.3): M0 = {tokens_in: 0, tokens_out: 0}. Applying Section 7.2 gives pooled total P0 = 0 and block count B0 = ceil(P0 / 10) = ceil(0 / 10) = 0.

For each accepted meter Mk, the calculations below use the Section 7.2 formulas:

P_u(k) = HALF_UP_6(weight_u * cumulativeTotal_u(k))
P(k)   = exact sum of P_u(k) over unit types
D(k)   = P(k) - P(cycle baseline)     ; raw cycle pool consumption
B(k)   = ceil(P(k) / quantumUnits)
DB(k)  = B(k) - B(previous settlement) ; newly charged block delta

D(k), not DB(k) * quantumUnits, is compared with reportAt and the grant pool (Section 7.2). cyclePooledUnits carries that raw pre-quantisation delta (Section 7.3).

C.2. Cycle 1 — threshold crossing, settlement, and re-grant

At 2026-09-01T10:01:00.000Z, the gateway reports one Call cumulatively against G1:

{
  "schemaVersion": 1,
  "actionId": "arith-action-1",
  "tenantRef": "4102",
  "accountRef": "subscriber-arith",
  "calls": [
    { "callId": "c1",
      "units": { "tokens_in": "10", "tokens_out": "4.666669" } }
  ],
  "final": false
}

The cumulative products and exact sum are:

P_in(1)  = HALF_UP_6(0.333333 * 10)
         = HALF_UP_6(3.333330) = 3.33333                (section 7.2)
P_out(1) = HALF_UP_6(2.5 * 4.666669)
         = HALF_UP_6(11.6666725) = 11.666673            (section 7.2)
P(1)     = 3.33333 + 11.666673 = 15.000003             (section 7.2)
D(1)     = P(1) - P0 = 15.000003 - 0 = 15.000003       (section 7.2)

Thus D(1) = 15.000003 >= reportAt 15 and D(1) <= grantedUnits 20. The crossing runs a settling cycle and produces CONTINUE with G2. Quantised rating is cumulative:

B(1)  = ceil(15.000003 / 10) = 2                        (section 7.2)
DB(1) = B(1) - B0 = 2 - 0 = 2 newly charged blocks     (section 7.2)

Those two blocks represent 20 pooled units in the quantised rating. No such derived block field is added to the wire. The receipt members in the top-level response are exactly:

{
  "settlementId": "s-arith-1",
  "acceptedCumulative": { "tokens_in": "10",
    "tokens_out": "4.666669" },
  "acceptedCalls": [
    { "callId": "c1",
      "units": { "tokens_in": "10", "tokens_out": "4.666669" } }
  ],
  "cyclePooledUnits": "15.000003"
}

Neither clamped nor adjustment is present. CONTINUE mints G2 with grantId: "g-arith-2", grantSeq: 2, grantIssuedAt: "2026-09-01T10:01:00.000Z", grantedUnits: "12", reportAt: "9", and expiresAt: "2026-09-01T10:06:00.000Z"; its pinned weights, quantum, law, and limits are unchanged. Its fresh envelope has actionLive: true, currentGrantSeq: 2, currentGrantId: "g-arith-2", and unattributable: false. The cycle baseline moves to M1 = {tokens_in: 10, tokens_out: 4.666669}, whose pooled value is P(1) (Section 7.3).

C.3. Stale, out-of-order report — zero merge and CURRENT

At 2026-09-01T10:01:10.000Z, a stale worker sends a different non-final request against superseded G1 with c1 = {tokens_in: 9, tokens_out: 4.5}. It is not the retained Cycle 1 identity. Per Section 7.2, the max-merge is:

max({10, 4.666669}, {9, 4.5}) = {10, 4.666669}; meter increase = 0

No ending condition holds. Under Section 3.5 LIVE row 6 and Section 7.3, the response is CURRENT. The full standing G2 grant is re-presented; the selected convergence fields are below. As in A.6, the per-unit map rendering is provisional under Appendix B, schema note 5.

{
  "directive": "CURRENT",
  "grantId": "g-arith-2",
  "grantSeq": 2,
  "baselineCumulative": { "tokens_in": "10",
    "tokens_out": "4.666669" },
  "meterCumulative": { "tokens_in": "10", "tokens_out": "4.666669" },
  "actionLive": true,
  "currentGrantSeq": 2,
  "currentGrantId": "g-arith-2",
  "unattributable": false
}

There is no settlement, receipt, charge, or mint; the current generation remains 2.

C.4. Cycle 2 — the baseline moves again

At 2026-09-01T10:02:00.000Z, the gateway reports against G2 with the cumulative meter M2 = {tokens_in: 15, tokens_out: 7.8}:

P_in(2)  = HALF_UP_6(0.333333 * 15) = 4.999995          (section 7.2)
P_out(2) = HALF_UP_6(2.5 * 7.8) = 19.5                 (section 7.2)
P(2)     = 4.999995 + 19.5 = 24.499995                 (section 7.2)
D(2)     = P(2) - P(1)
         = 24.499995 - 15.000003 = 9.499992             (section 7.2)

D(2) = 9.499992 >= reportAt 9 and D(2) <= grantedUnits 12, so this is another settlement and re-grant. The cumulative block calculation is:

B(2)  = ceil(24.499995 / 10) = 3                        (section 7.2)
DB(2) = B(2) - B(1) = 3 - 2 = 1 newly charged block    (section 7.2)

The exact top-level receipt members are:

{
  "settlementId": "s-arith-2",
  "acceptedCumulative": { "tokens_in": "15", "tokens_out": "7.8" },
  "acceptedCalls": [
    { "callId": "c1",
      "units": { "tokens_in": "15", "tokens_out": "7.8" } }
  ],
  "cyclePooledUnits": "9.499992"
}

Again, clamped and adjustment are absent. CONTINUE mints G3 with grantId: "g-arith-3", grantSeq: 3, grantIssuedAt: "2026-09-01T10:02:00.000Z", grantedUnits: "10", reportAt: "8", and expiresAt: "2026-09-01T10:07:00.000Z". Its envelope carries current generation 3 and current id g-arith-3. The new baseline is M2 = {tokens_in: 15, tokens_out: 7.8} with pooled value P(2) (Section 7.3).

C.5. Final report — CLOSE and terminal settlement

At 2026-09-01T10:02:30.000Z, the gateway sends final: true against G3 with cumulative meter M3 = {tokens_in: 18, tokens_out: 10.1}. The close is allowed before reportAt; it settles the full remaining meter delta (Section 3.5, Section 7.3):

P_in(3)  = HALF_UP_6(0.333333 * 18) = 5.999994          (section 7.2)
P_out(3) = HALF_UP_6(2.5 * 10.1) = 25.25               (section 7.2)
P(3)     = 5.999994 + 25.25 = 31.249994                (section 7.2)
D(3)     = P(3) - P(2)
         = 31.249994 - 24.499995 = 6.749999             (section 7.2)
B(3)     = ceil(31.249994 / 10) = 4                     (section 7.2)
DB(3)    = B(3) - B(2) = 4 - 3 = 1 newly charged block (section 7.2)

D(3) = 6.749999 <= grantedUnits 10. The exact terminal receipt members are:

{
  "settlementId": "s-arith-3",
  "acceptedCumulative": { "tokens_in": "18", "tokens_out": "10.1" },
  "acceptedCalls": [
    { "callId": "c1",
      "units": { "tokens_in": "18", "tokens_out": "10.1" } }
  ],
  "cyclePooledUnits": "6.749999"
}

Neither optional flag is present. The response directive is SETTLED; the Action's ending is CLOSE and endedAt is 2026-09-01T10:02:30.000Z. The closing outcome mints generation 4 without minting a serving grant. The terminal envelope is therefore actionLive: false, currentGrantSeq: 4, currentGrantId: "g-arith-3", and unattributable: false (Section 3.5).

As a final check, the raw receipt deltas sum without cadence loss:

15.000003 + 9.499992 + 6.749999 = 31.249994 = P(3) - P0

The charged block deltas also telescope:

2 + 1 + 1 = 4 = ceil(31.249994 / 10)

The stale report contributes zero to both checks. This is the cadence-independence property of the cumulative pipeline in Section 7.2.

Appendix D. Vocabulary mapping (informative)

The implementing charging stack and its surrounding documents use an established telecom vocabulary; Pulse deliberately uses short, wire-suited, vendor-neutral names. The table translates between the two. The wire vocabulary is the protocol's; the left column is one implementer's, recorded here so that a reader of either vocabulary can read the other.

Each D.1 definition is the opening of the framework's own definition box in its current edition, pasted rather than paraphrased; the objects are identical in v3.1, the edition this document cites, except row 17 (Relation: v3.1's box reads "EXPOSED or EXECUTES"; HOLDS and RELIES arrived at v4.0). "No wire concept" is a deliberate entry, not a gap: the concept lives entirely server-side, and the gateway sees only its consequences.

Framework terms not listed here — every Event the engine writes, the Class, the Draw-down Order, the Night, the Bill, the Commercial Group, the Governance Floor, the Platform, the record kinds — are server-side objects with no wire counterpart, and their absence is deliberate. Their one-line dispositions, together with every term this document and its implementation use, are carried in the Pulse vocabulary register: a companion document owned with this specification, seeded from the framework's register of 6 September 2026 and checked against every draft by script, so that silence here is declared once rather than mistaken for support (Section 1.4).

D.1. The framework's terms

Table 22
# Framework term — the opening of its definition box This specification
1 ActionOne customer-visible unit of work with one price question: a question answered, a picture generated, a research task completed. Action (shared term)
2 BalanceMoney with rules: an amount, a priority in the draw-down order, optionally a Scope and an Expiry, and a declared behaviour at its limit. No wire concept — the server holds it; the gateway sees refusals and stops. scope names a Balance's Scope (Section 6.1)
3 CeilingThe most one Action may charge — "up to" as an enforced number. Ceiling / headroom (shared term, Section 2); it reaches the wire only as CEILING_REACHED (stop) and CEILING_PREEMPT (refusal). limits is a different object (the disclosed protective bounds, Section 6.3), and limitOwner belongs to the sponsor cap (Section 12.1)
4 CountableA number computed from records that answers one operational question, on a stated cadence and at a stated grain. No wire concept — the meter's unit is the framework's counted unit: units and the unit types (Section 7.2)
5 CycleOne Quota's span: grant, consume, settle. A reserved Action is a chain of Cycles. Cycle (shared term); the cycle Report rides it (Section 7.3)
6 ExpiryA date after which a Balance's unreserved credits are written off. Reserved credits are spared until their Actions settle. No wire concept. Through draft-03 this document's ending for a lapsed grant was named EXPIRY and the two were false friends; from draft-04 that ending is LAPSE (D.2)
7 FacilityOne regulatory regime, as fine-grained as reality requires — Belgium; Kenya; a single US state where its rules differ. No wire concepttenantRef identifies the Tenant, which a Facility contains (D.2)
8 Grant — no definition box: the second step of the loop Authorise · Grant · Supervise · Settle, the act of issuing a Quota decision: GRANT (Section 6.3) — the act. The object the framework calls a Quota is this document's Grant (row 16)
9 Metering PointThe component that executes Inference Calls, counts consumption and obeys the Quota. In the AI ecosystem the role is played by the gateway. The Gateway (Section 2) — Section 15.7's attesting party
10 Node — no definition box; the reference engine's class Node: one path from the Platform through the Facility chain to a Tenant tenantRef, and accountRef for the position below — not scope
11 Packet (Substantiation Packet) — Everything needed to defend one Charge to a regulator or a court, in five limbs. Partial: the per-settlement receipt (Section 7.3) is the wire-visible record; the Packet itself is off-wire
12 PartyA legal or natural person with standing in at least one relation. The Framework records that a party holds a relation, or changed; it never verifies who they are. The roles a party takes on this wire: sponsor (limitOwner), subscriber (accountRef), gateway (the credential), tenant (Section 2); limitOwner names the exposed one (Section 12.1)
13 PeriodThe Bill's integration window, opened and closed on the Facility's calendar. The sponsor's declared accounting window (Section 12.1) — narrower; the Bill's Period has no wire term
14 Price BookA Tenant's complete set of Action Types with their price forms and elements, held as a versioned object with an identity. No wire object — lawVersion is "the pinned plan/price-book version" (Section 6.3); the book itself is the tenant's published catalogue, server-side
15 Profile (Metering Point Profile) — The record of what an integration actually does, per route … Established by running the conformance suite against it, never by reading its documentation. Integration profile (Section 2, Section 12.4) — a related object under a different name; not this document's deployment profile — the false friend below
16 QuotaThe Reservation's outside face: the same amount expressed to the Metering Point in work units, carrying a report threshold and a Validity. Grant (grantedUnits, reportAt, expiresAt) — the RFC 4006 granted-quota concept (Section 1.2)
17 RelationA dated, typed, scoped link between a Party and work: EXPOSED or EXECUTES. (v3.1's box; the current edition adds the record relation, HOLDS and RELIES) No wire concept — the wire sees the sponsor relation only through limitOwner; the exposure relation's aggregate is EXPOSURE_CAP's basis (Section 12.1)
18 ReservationCredits set aside on a Balance for one Action, under the Solvency Rule. The promise held against money. An open grant (pool exposure) — "spend the sponsor is already standing behind" (Section 12.1)
19 Action TypeThe Price Book row an Action instantiates: its price form, its elements, its counted unit, its thresholds. Action Type (actionType; Section 2, Section 6.1)
20 Effective LawThe single package of properties an Action receives at open: the path from Platform through its Facility chain to its Tenant, resolved top-down. The law pinned at open (Section 2, Section 6.2); lawVersion identifies its version
21 TenantWhose money and commercial law. A Tenant owns its Price Book, its Balances and one settlement relationship, and attaches to exactly one Facility. Tenant (tenantRef; Section 2, Section 6.1)
22 WeightsPer-type conversion from counted units to billing units … Part of the price, pinned at open. unitWeights (Section 6.3) — the complete map, pinned with the law
23 stop_reason (the Section 9 field) — Why the Action must end now: the Trip-wire, the cascade … directive: STOP with stopReason (Section 7.3, Section 7.4) — the stop obligation

The sponsor-cap concept, by its framework name: what EXPOSURE_CAP enforces is the framework's Solvency Rule at a second scope"a bound on exposure ranges over a relation, not over any one Balance." Where the engine enforces open grants ≤ the Balance, it also enforces commitments across the relation ≤ the exposure bound; Section 12.1 states the same basis (committed + settled, per relation) explicitly.

Two cross-vocabulary notes: the framework's fourth velocity level, RELATION_VELOCITY (Actions started and credits charged per minute, counted across a declared relation), is this document's GROUP_VELOCITY — the same object, counted as the framework defines it, in both Actions started and credits charged. And the framework closes its refusal vocabulary (fourteen reasons) but enumerates no closed set of stop reasons — its reference engine's stop codes are the nearest list; this document closes both registries per version (Section 5, Section 7.4, Section 13.2) — a protocol decision, not a divergence.

D.2. Additional implementation and telecom terms

Table 23
Implementation / telecom term This specification
Facility / region (tenancy detail below the operator) No wire concept — subscriber-level detail below the tenant boundary
Subscriber (MSISDN) Account (accountRef — pseudonymous on the wire, Section 15.7)
Charging session Action
Decision-tree classification Action Type (Actions arrive typed; classification collapses into a catalogue lookup)
Interim usage report (opIntermediate) Cycle Report (final: false)
Final usage report (opFinal) Closing Report (final: true, the CLOSE ending)
CDR (the concluded-session record) The concluded Action record (emitted at ending; not a wire object)
CODR (per-settlement record) The cycle settlement and its receipt
Time band Time band (shared term — Section 10 adopted it from the implementation)

False friends — same word, different meaning:

  • Profile. Three meanings collide. In the charging stack, the subscription profile is the subscriber's stored charging state. In the framework, a Profile is the Metering Point's tested integration record — a test result, not stored state. In this specification, profile appears only as deployment profile — a named set of deployment choices (Section 12, Section 13.5). None of the three is any of the others.

  • Expiry (resolved at draft-04). In the framework, Expiry is a date after which a Balance's unreserved credits are written off — money is destroyed, and reserved credits are spared until their Actions settle; the implementation's counter expiry is the same event. Through draft-03 this document named the ending of a lapsed grant EXPIRY, and the two were false friends. From draft-04 that ending is LAPSE and its stop reason LAPSED (the expiresAt field keeps its name): a lapse settles at the reported meter (Section 7.5) and writes nothing off; an Expiry is a balance event with no wire term. The word expiry is no longer used for the lapse anywhere in this document.

  • Envelope. Here, the Envelope is the always-fresh part of a response (Section 2). In the framework, an envelope is a money partition — the authorisation floor. Neither is the other.

  • Minimum. The framework's Minimum is a price form: a per-Action floor on the Charge. Its counterpart here is the contractual minimum reserved at open (Section 6.2) and never applied a second time (Section 7.6) — no wire field. limits (Section 6.3) are the disclosed protective bounds, and limits.minRate (Section 12.2) is a capacity rate: three objects, one word apart.

  • Facility. In the framework, a Facility is a regulatory regime that contains Tenants. In the implementation, a facility is a node of the operator's tenancy tree below the tenant (the root facility is the tenant). Neither reaches the wire; tenantRef is the Tenant.

One field, two uses — signature. On this wire, signature is a reserved, opaque per-call attestation (Section 7.2), enforced only where a bilateral profile is agreed (SIGNATURES_REQUIRED, Section 7.4). The framework's reference engine uses a per-call signature as a fingerprint for repeat detection (its Inflation wire), and the implementation does the same server-side with the value it receives. Same field, two purposes — recorded here so the wire contract and the server-side use are never conflated.

Appendix E. Change history (informative)

This is the first Internet-Draft of this specification. The protocol was developed privately through several revisions during 2026, under an earlier working name, and was reviewed against an independent implementer's assessment before this submission. Later versions record their changes in this appendix.

Authors' Addresses

Martin Petkov
Effortel Technologies SA
Arkadi Panitch
Effortel Technologies SA