Pulse: Real-Time Online Charging for AI Services
draft-effortel-pulse-00
This document is an Internet-Draft (I-D).
Anyone may submit an I-D to the IETF.
This I-D is not endorsed by the IETF and has no formal standing in the
IETF standards process.
| Document | Type | Active Internet-Draft (individual) | |
|---|---|---|---|
| Authors | Martin Petkov , Arkadi Panitch | ||
| Last updated | 2026-09-17 | ||
| RFC stream | (None) | ||
| Intended RFC status | (None) | ||
| Formats | |||
| Stream | Stream state | (No stream defined) | |
| Consensus boilerplate | Unknown | ||
| RFC Editor Note | (None) | ||
| IESG | IESG state | I-D Exists | |
| Telechat date | (None) | ||
| Responsible AD | (None) | ||
| Send notices to | (None) |
draft-effortel-pulse-00
Individual Submission M. Petkov
Internet-Draft A. Panitch
Intended status: Standards Track Effortel Technologies SA
Expires: 21 March 2027 17 September 2026
Pulse: Real-Time Online Charging for AI Services
draft-effortel-pulse-00
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.
Copyright Notice
Copyright (c) 2026 IETF Trust and the persons identified as the
document authors. All rights reserved.
Petkov & Panitch Expires 21 March 2027 [Page 1]
Internet-Draft The Pulse Protocol September 2026
This document is subject to BCP 78 and the IETF Trust's Legal
Provisions Relating to IETF Documents (https://trustee.ietf.org/
license-info) in effect on the date of publication of this document.
Please review these documents carefully, as they describe your rights
and restrictions with respect to this document. Code Components
extracted from this document must include Revised BSD License text as
described in Section 4.e of the Trust Legal Provisions and are
provided without warranty as described in the Revised BSD License.
Table of Contents
1. Introduction . . . . . . . . . . . . . . . . . . . . . . . . 4
1.1. Motivation . . . . . . . . . . . . . . . . . . . . . . . 4
1.2. Relationship to Diameter Credit-Control . . . . . . . . . 4
1.3. Scope . . . . . . . . . . . . . . . . . . . . . . . . . . 4
1.4. Version-1 scope and deferred items (named) . . . . . . . 4
2. Conventions and Terminology . . . . . . . . . . . . . . . . . 5
3. Protocol Overview . . . . . . . . . . . . . . . . . . . . . . 7
3.1. Message flow . . . . . . . . . . . . . . . . . . . . . . 8
3.2. The two-message contract . . . . . . . . . . . . . . . . 8
3.3. Flat-priced Actions . . . . . . . . . . . . . . . . . . . 8
3.4. Division of responsibility . . . . . . . . . . . . . . . 9
3.5. The Action state machine (normative) . . . . . . . . . . 9
4. Transport and Encoding . . . . . . . . . . . . . . . . . . . 14
4.1. Endpoints . . . . . . . . . . . . . . . . . . . . . . . . 14
4.2. Authentication . . . . . . . . . . . . . . . . . . . . . 15
4.3. Encoding rules and JSON types (normative) . . . . . . . . 15
4.4. Limits (normative) . . . . . . . . . . . . . . . . . . . 16
5. Schema Versioning . . . . . . . . . . . . . . . . . . . . . . 17
6. The Authorise Operation . . . . . . . . . . . . . . . . . . . 18
6.1. Request . . . . . . . . . . . . . . . . . . . . . . . . . 19
6.2. Processing . . . . . . . . . . . . . . . . . . . . . . . 20
6.3. Response — GRANT . . . . . . . . . . . . . . . . . . . . 20
6.3.1. Client obligations under a grant . . . . . . . . . . 23
6.4. Response — REFUSE . . . . . . . . . . . . . . . . . . . . 24
6.5. Idempotency, request identity, and identifier scopes . . 27
7. The Report Operation . . . . . . . . . . . . . . . . . . . . 28
7.1. Request . . . . . . . . . . . . . . . . . . . . . . . . . 28
7.2. Cumulative semantics (normative) . . . . . . . . . . . . 29
7.3. Response . . . . . . . . . . . . . . . . . . . . . . . . 30
7.4. stopReason registry (version 1) . . . . . . . . . . . . . 32
7.5. Lapse boundary (normative) . . . . . . . . . . . . . . . 34
7.6. Late reports and the late window . . . . . . . . . . . . 35
7.7. Reporting cadence requirements . . . . . . . . . . . . . 35
8. Timing Summary . . . . . . . . . . . . . . . . . . . . . . . 36
8.1. Protocol constants and configurables . . . . . . . . . . 36
9. Child Actions . . . . . . . . . . . . . . . . . . . . . . . . 46
10. Time Bands . . . . . . . . . . . . . . . . . . . . . . . . . 47
Petkov & Panitch Expires 21 March 2027 [Page 2]
Internet-Draft The Pulse Protocol September 2026
11. Draining . . . . . . . . . . . . . . . . . . . . . . . . . . 48
12. Shared Bounds and Fair Usage . . . . . . . . . . . . . . . . 48
12.1. Sponsor caps . . . . . . . . . . . . . . . . . . . . . . 48
12.2. Capacity pools . . . . . . . . . . . . . . . . . . . . . 49
12.3. Degradation before refusal (fair usage) . . . . . . . . 49
12.4. Report promptness . . . . . . . . . . . . . . . . . . . 50
13. Error Handling . . . . . . . . . . . . . . . . . . . . . . . 50
13.1. HTTP status mapping . . . . . . . . . . . . . . . . . . 50
13.2. errorCode registry (400 family, version 1) . . . . . . . 52
13.3. Client retry guidance . . . . . . . . . . . . . . . . . 52
13.4. Charging-plane unavailability — the client state
machine . . . . . . . . . . . . . . . . . . . . . . . . 52
13.5. Operational expectations (informative) . . . . . . . . . 54
14. Conformance Requirements Summary (client) . . . . . . . . . . 54
15. Security Considerations . . . . . . . . . . . . . . . . . . . 55
15.1. Transport and credentials . . . . . . . . . . . . . . . 55
15.2. Tenant binding . . . . . . . . . . . . . . . . . . . . . 56
15.3. Gateway ownership of Actions . . . . . . . . . . . . . . 56
15.4. Abuse limits . . . . . . . . . . . . . . . . . . . . . . 56
15.5. API-level protection . . . . . . . . . . . . . . . . . . 56
15.6. Clock authority and posture . . . . . . . . . . . . . . 57
15.7. Trust model and privacy . . . . . . . . . . . . . . . . 57
16. Implementation Status . . . . . . . . . . . . . . . . . . . . 58
17. IANA Considerations . . . . . . . . . . . . . . . . . . . . . 58
18. References . . . . . . . . . . . . . . . . . . . . . . . . . 60
18.1. Normative References . . . . . . . . . . . . . . . . . . 60
18.2. Informative References . . . . . . . . . . . . . . . . . 61
Appendix A. Examples (illustrative, not normative;
abbreviated) . . . . . . . . . . . . . . . . . . . . . . 61
A.1. Authorise → grant . . . . . . . . . . . . . . . . . . . . 62
A.2. Report → continue . . . . . . . . . . . . . . . . . . . . 62
A.3. Final report → settled . . . . . . . . . . . . . . . . . 63
A.4. Stop obligation . . . . . . . . . . . . . . . . . . . . . 63
A.5. Authorise → refuse metadata . . . . . . . . . . . . . . . 64
A.6. Stale report → CURRENT . . . . . . . . . . . . . . . . . 64
A.7. Error envelope . . . . . . . . . . . . . . . . . . . . . 65
Appendix B. Message schemas (CDDL, provisional) . . . . . . . . 66
B.1. Schema notes — ambiguities found . . . . . . . . . . . . 78
Appendix C. Worked arithmetic (illustrative) . . . . . . . . . . 80
C.1. Authorise and opening grant . . . . . . . . . . . . . . . 80
C.2. Cycle 1 — threshold crossing, settlement, and re-grant . 81
C.3. Stale, out-of-order report — zero merge and CURRENT . . . 83
C.4. Cycle 2 — the baseline moves again . . . . . . . . . . . 83
C.5. Final report — CLOSE and terminal settlement . . . . . . 84
Appendix D. Vocabulary mapping (informative) . . . . . . . . . . 85
D.1. The framework's terms . . . . . . . . . . . . . . . . . . 86
D.2. Additional implementation and telecom terms . . . . . . . 90
Appendix E. Change history (informative) . . . . . . . . . . . . 92
Petkov & Panitch Expires 21 March 2027 [Page 3]
Internet-Draft The Pulse Protocol September 2026
Authors' Addresses . . . . . . . . . . . . . . . . . . . . . . . 92
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).
Petkov & Panitch Expires 21 March 2027 [Page 4]
Internet-Draft The Pulse Protocol September 2026
* *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.
+==============+===================================================+
| Term | Meaning |
+==============+===================================================+
| *Charging | The OCS instance implementing this protocol. |
| Server* | |
| ("the | |
| server") | |
+--------------+---------------------------------------------------+
| *Gateway* | The authenticated system in the AI usage path |
| ("the | that requests authorisation and reports |
| client") | consumption. |
+--------------+---------------------------------------------------+
| *Tenant* | The commercial customer of the platform; every |
| | credential is bound to one or more tenants. |
+--------------+---------------------------------------------------+
Petkov & Panitch Expires 21 March 2027 [Page 5]
Internet-Draft The Pulse Protocol September 2026
| *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 | The declared, catalogue-defined kind of Action; |
| Type* | 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 |
Petkov & Panitch Expires 21 March 2027 [Page 6]
Internet-Draft The Pulse Protocol September 2026
| | without a report). |
+--------------+---------------------------------------------------+
| *Late | The period after an ending — until max(endedAt + |
| window* | 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 | An aggregate spend bound a sponsoring party |
| cap* | 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 | A tenant-declared provisioned serving rate that |
| pool* | 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 / | The ceiling is the maximum total charge an Action |
| headroom* | (or Action family, Section 9) may accumulate |
| | under its plan; headroom is what remains of it. |
+--------------+---------------------------------------------------+
| *Minimum | The plan-declared serve rate below which |
| rate* | degradation (Section 12.3) is not attempted and |
| | the honest answer is NO_CAPACITY. |
+--------------+---------------------------------------------------+
| *Integration | The tested record of what a given gateway |
| profile* | integration actually does (authorise-before- |
| | serving, stop obedience, report promptness, …), |
| | established by the conformance suite |
| | (Section 12.4). |
+--------------+---------------------------------------------------+
Table 1
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
Petkov & Panitch Expires 21 March 2027 [Page 7]
Internet-Draft The Pulse Protocol September 2026
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 —
Petkov & Panitch Expires 21 March 2027 [Page 8]
Internet-Draft The Pulse Protocol September 2026
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.
Petkov & Panitch Expires 21 March 2027 [Page 9]
Internet-Draft The Pulse Protocol September 2026
*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):
+===+============+==========================+=================+
| # | Input | Outcome | Next state |
+===+============+==========================+=================+
| 1 | Authorise, | GRANT (lifecycle: G1; | LIVE(G1); flat: |
| | admissible | flat: settled + endedAt) | ENDED(CLOSE) |
+---+------------+--------------------------+-----------------+
| 2 | Authorise, | 200 REFUSE + reason (no | NONE |
| | refused | identity retained) | |
+---+------------+--------------------------+-----------------+
Table 2
*In state LIVE(Gn):*
+===+================+===============================+==============+
| # | Input | Outcome | Next state |
+===+================+===============================+==============+
| 1 | Authorise, | re-presentation of Gn + | LIVE(Gn) |
| | identity = | its baseline receipt | |
| | opening | | |
Petkov & Panitch Expires 21 March 2027 [Page 10]
Internet-Draft The Pulse Protocol September 2026
| | identity | | |
+---+----------------+-------------------------------+--------------+
| 2 | Authorise, | 200 REFUSE / | LIVE(Gn) |
| | other identity | DUPLICATE_ACTION | |
+---+----------------+-------------------------------+--------------+
| 3 | Report whose | that stored outcome | LIVE(Gn) |
| | identity | replayed + fresh | |
| | matches a | envelope | |
| | retained | | |
| | outcome | | |
+---+----------------+-------------------------------+--------------+
| 4 | Report, final: | the *closing cycle | ENDED(CLOSE) |
| | true | against the current | |
| | (addressed to | state*: merge → settle | |
| | any grant of | the full unsettled | |
| | this Action) | meter delta → SETTLED + | |
| | | receipt (a close mints | |
| | | no serving grant; the | |
| | | outcome mints its | |
| | | generation) | |
+---+----------------+-------------------------------+--------------+
| 5 | Report | the *fresh cycle* (the | LIVE(Gn+1) · |
| | addressed to | primary operation): | ENDED(STOP) |
| | the current | merge → *settle the | |
| | grant Gn | full unsettled meter | |
| | (calls[] MAY | delta* → evaluate | |
| | be empty — a | ending conditions in | |
| | heartbeat) | precedence order | |
| | | (Section 7.4): none | |
| | | hold → CONTINUE(Gn+1); | |
| | | one holds → | |
| | | STOP(reason) + receipt | |
+---+----------------+-------------------------------+--------------+
| 6 | Report | meters max-merge; then | LIVE(Gn) · |
| | addressed to a | ending conditions are | ENDED(STOP) |
| | superseded | evaluated (one raised | |
| | grant (non- | by the merge ends the | |
| | final, no | Action per precedence, | |
| | identity | settling at the meter → | |
| | match) | 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. | |
+---+----------------+-------------------------------+--------------+
Petkov & Panitch Expires 21 March 2027 [Page 11]
Internet-Draft The Pulse Protocol September 2026
| 7 | Server event: | *settle at the current | ENDED(LAPSE |
| | grace-end | meter*, then the | or STOP) |
| | without a | terminal transition | |
| | report (now ≥ | (mints) | |
| | expiresAt + 5 | | |
| | s), parent | | |
| | cascade, | | |
| | administrative | | |
| | stop | | |
+---+----------------+-------------------------------+--------------+
Table 3
*In state ENDED(e) (while recognised):*
Petkov & Panitch Expires 21 March 2027 [Page 12]
Internet-Draft The Pulse Protocol September 2026
+===+==============+===================================+==========+
| # | Input | Outcome | Next |
| | | | state |
+===+==============+===================================+==========+
| 1 | Authorise, | terminal acknowledgement | ENDED(e) |
| | identity = | (decision: GRANT, settled where | |
| | opening | applicable, ending, endedAt, | |
| | identity | final receipt) | |
+---+--------------+-----------------------------------+----------+
| 2 | Authorise, | 200 REFUSE / DUPLICATE_ACTION | ENDED(e) |
| | other | | |
| | identity | | |
+---+--------------+-----------------------------------+----------+
| 3 | Report whose | that stored outcome replayed | ENDED(e) |
| | identity | *verbatim* + fresh envelope | |
| | matches a | | |
| | retained | | |
| | outcome | | |
+---+--------------+-----------------------------------+----------+
| 4 | Report to a | terminal directive (SETTLED) — | ENDED(e) |
| | *flat* | never settles; unattributable: | |
| | Action | true if units were carried | |
| | | (recorded, never charged) | |
+---+--------------+-----------------------------------+----------+
| 5 | Report, | late adjustment: terminal | ENDED(e) |
| | merge adds | directive + adjustment: true + | |
| | new | new settlementId + receipt (mints | |
| | consumption, | its outcome generation) | |
| | settleable | | |
+---+--------------+-----------------------------------+----------+
| 6 | Report, | terminal directive + | ENDED(e) |
| | merge adds | unattributable: true, no | |
| | new | settlementId; the unsettled range | |
| | consumption, | *marks the Action* — every later | |
| | not | zero-merge response also carries | |
| | settleable | unattributable: true until it | |
| | | settles or recognition lapses | |
+---+--------------+-----------------------------------+----------+
| 7 | Report, | terminal directive + current | ENDED(e) |
| | merge adds | receipt (no adjustment; sticky | |
| | zero | unattributable where marked) | |
+---+--------------+-----------------------------------+----------+
Table 4
*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
Petkov & Panitch Expires 21 March 2027 [Page 13]
Internet-Draft The Pulse Protocol September 2026
(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
+===========+==========================================+
| Operation | Method and path |
+===========+==========================================+
| Authorise | POST /ocs/ai/v1/authorizations |
+-----------+------------------------------------------+
| Report | POST /ocs/ai/v1/grants/{grantId}/reports |
+-----------+------------------------------------------+
Table 5
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.
Petkov & Panitch Expires 21 March 2027 [Page 14]
Internet-Draft The Pulse Protocol September 2026
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.
Petkov & Panitch Expires 21 March 2027 [Page 15]
Internet-Draft The Pulse Protocol September 2026
* *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)
+============================+==========+====================+
| 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 | ≤ 10⁹ | 400 LIMIT_EXCEEDED |
| total per Action | | |
+----------------------------+----------+--------------------+
Table 6
Petkov & Panitch Expires 21 March 2027 [Page 16]
Internet-Draft The Pulse Protocol September 2026
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.
* Fields belong to schema versions. A client MUST NOT send a field
beyond the version it declares; the server rejects fields not in
the declared version (strict parse per version — unknown members
are rejected, never ignored).
* An unknown schemaVersion → 400 UNSUPPORTED_SCHEMA. If
schemaVersion itself is missing or unparseable, the error response
carries maxSchemaVersion but cannot echo a version (the one
exception to echoing).
* The server-side value sets (refusalReason, stopReason, errorCode,
decision, directive, ending, grade, reduceCause) are closed per
version, but clients MUST tolerate unknown strings in these fields
— treating an unknown refusalReason as a refusal, an unknown
stopReason as a stop obligation, an unknown errorCode as the error
class its HTTP status indicates, an unknown directive as a stop
obligation, an unknown decision as a refusal, an unknown ending as
STOP, an unknown grade as BOUNDED, and an unknown reduceCause as
CAPACITY.
* *Responses are version-bounded too:* a server sends only fields
belonging to the echoed schemaVersion; a client reads responses
tolerantly, acting on the fields of its declared version and
ignoring unknown members (response reading is tolerant; request
parsing stays strict).
* The reason/code registries are owned by this document; new values
are allocated only by a new schema version of this specification
(no private-use values in version 1). While the series is
unpublished, a draft MAY rename a registry value, recording the
rename in its change history; once published, a renamed value is a
new value. The registries and their allocation policy are stated
in Section 17.
Petkov & Panitch Expires 21 March 2027 [Page 17]
Internet-Draft The Pulse Protocol September 2026
*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.
+==========+===============+===============================+
| Protocol | Wire | Surface |
| version | schemaVersion | |
+==========+===============+===============================+
| 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). |
+----------+---------------+-------------------------------+
Table 7
*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
Petkov & Panitch Expires 21 March 2027 [Page 18]
Internet-Draft The Pulse Protocol September 2026
6.1. Request
+================+=============+===================================+
| 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. |
+----------------+-------------+-----------------------------------+
Petkov & Panitch Expires 21 March 2027 [Page 19]
Internet-Draft The Pulse Protocol September 2026
| 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). |
+----------------+-------------+-----------------------------------+
Table 8
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
+==================+====================+===========================+
| 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 | The grant's issue |
Petkov & Panitch Expires 21 March 2027 [Page 20]
Internet-Draft The Pulse Protocol September 2026
| | grant | 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 | Pool multipliers per |
| | grant (all price | unit type — the |
| | forms, including | *complete* map, |
| | free) | 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 | The block size in |
| | Types | pooled units: |
| | | consumption is charged |
| | | in whole blocks; the |
| | | client SHOULD expect |
| | | pool draw-down in block |
| | | increments. |
+------------------+--------------------+---------------------------+
| timeBandId | banded Action | The time band fixed at |
| | Types (including | opening (Section 10). |
| | flat) | |
+------------------+--------------------+---------------------------+
| serverTime | every response — | The server clock at |
| | *envelope*, always | this processing — the |
| | fresh, never | authoritative time |
| | replayed | reference (Section 8). |
Petkov & Panitch Expires 21 March 2027 [Page 21]
Internet-Draft The Pulse Protocol September 2026
+------------------+--------------------+---------------------------+
| actionLive, | every Action- | The live truth: whether |
| currentGrantSeq, | identifying 200 — | the Action is open |
| currentGrantId | *envelope*, always | (monotonic — once |
| | fresh | 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 | The pinned plan/price- |
| | grant | book version (integer) |
| | | this Action rates under |
| | | — for client-side |
| | | correlation with the |
| | | tenant's published |
| | | catalogue; opaque |
| | | otherwise. |
+------------------+--------------------+---------------------------+
| limits | every lifecycle | The enforceable caps: |
| | grant | 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 |
Petkov & Panitch Expires 21 March 2027 [Page 22]
Internet-Draft The Pulse Protocol September 2026
| | | (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 | true, with reduceCause: |
| | pressure reduced | "CAPACITY" |
| | this grant | (Section 12.3). |
| | (opening or | |
| | continuation) | |
+------------------+--------------------+---------------------------+
| settled | flat-priced | true: charged at open; |
| | Actions | no reports, no |
| | | validity. |
+------------------+--------------------+---------------------------+
Table 9
*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.
Petkov & Panitch Expires 21 March 2027 [Page 23]
Internet-Draft The Pulse Protocol September 2026
6.4. Response — REFUSE
refusalReason registry (version 1):
+========================+=========================================+
| 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 |
Petkov & Panitch Expires 21 March 2027 [Page 24]
Internet-Draft The Pulse Protocol September 2026
| | 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. |
+------------------------+-----------------------------------------+
Table 10
*Every refusal additionally carries:*
+===================+==============+==============================+
| Field | Present | Meaning |
+===================+==============+==============================+
| retryable | always on | Whether the identical |
| | refuse | 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 | Seconds to wait before |
| | when | retrying the identical |
| | retryable: | request. |
| | true | |
+-------------------+--------------+------------------------------+
| grade | always on | The evidence behind the |
| | refuse | 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). |
Petkov & Panitch Expires 21 March 2027 [Page 25]
Internet-Draft The Pulse Protocol September 2026
| | | A client MUST NOT present |
| | | more certainty to the end |
| | | user than the grade holds. |
+-------------------+--------------+------------------------------+
| limitOwner | on a party- | Names whose limit was |
| | scoped bound | 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 | true in version 1: the |
| | refuse, from | refusal was decided by |
| | version 1.1 | machine, with no human in |
| | (Section 5) | the loop (Section 15.7). |
| | | Carried on the decision |
| | | itself so the notice travels |
| | | with the refusal it |
| | | describes. |
+-------------------+--------------+------------------------------+
| reviewContact | when the | A human-review channel — a |
| | tenant | URI or plain-text contact — |
| | configures | through which the refused |
| | one, from | party can have the decision |
| | version 1.1 | re-examined by a person. |
| | (Section 5) | 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. |
+-------------------+--------------+------------------------------+
Table 11
Version-1 grades: NO_CAPACITY and PREEMPTED are BOUNDED;
SERVICE_BARRED and DRAINING are ATTESTED; every other reason is
PROVEN.
Petkov & Panitch Expires 21 March 2027 [Page 26]
Internet-Draft The Pulse Protocol September 2026
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).
Petkov & Panitch Expires 21 March 2027 [Page 27]
Internet-Draft The Pulse Protocol September 2026
* 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
+===============+=============+====================================+
| Field | Requirement | Meaning |
+===============+=============+====================================+
| schemaVersion | REQUIRED | See Section 5. |
+---------------+-------------+------------------------------------+
| actionId | REQUIRED | The Action being reported. |
+---------------+-------------+------------------------------------+
| tenantRef, | REQUIRED | Identity resolution; MUST match |
| accountRef | | 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). |
+---------------+-------------+------------------------------------+
Petkov & Panitch Expires 21 March 2027 [Page 28]
Internet-Draft The Pulse Protocol September 2026
Table 12
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
Petkov & Panitch Expires 21 March 2027 [Page 29]
Internet-Draft The Pulse Protocol September 2026
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
+===========+=======================================================+
| 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(meterCumulative − |
| | baselineCumulative). |
+-----------+-------------------------------------------------------+
Table 13
Petkov & Panitch Expires 21 March 2027 [Page 30]
Internet-Draft The Pulse Protocol September 2026
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.
Petkov & Panitch Expires 21 March 2027 [Page 31]
Internet-Draft The Pulse Protocol September 2026
*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)
+=====================+=========================================+
| 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). |
+---------------------+-----------------------------------------+
Petkov & Panitch Expires 21 March 2027 [Page 32]
Internet-Draft The Pulse Protocol September 2026
| 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). |
+---------------------+-----------------------------------------+
Table 14
*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.
Petkov & Panitch Expires 21 March 2027 [Page 33]
Internet-Draft The Pulse Protocol September 2026
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.
Petkov & Panitch Expires 21 March 2027 [Page 34]
Internet-Draft The Pulse Protocol September 2026
*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.
Petkov & Panitch Expires 21 March 2027 [Page 35]
Internet-Draft The Pulse Protocol September 2026
*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
+=================+==========+=====================================+
| 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 + | terminal | Late-window end: recognition |
| 24 h, expiresAt | response | unconditional until it; late |
| + 1 h) | + last | consumption settleable within it |
| | grant | (Section 7.6). |
+-----------------+----------+-------------------------------------+
Table 15
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.
Petkov & Panitch Expires 21 March 2027 [Page 36]
Internet-Draft The Pulse Protocol September 2026
*Fixed in protocol.*
+==================+===============================+============+
| Name | Value | Defining |
| | | section |
+==================+===============================+============+
| Protocol | Version 1 (/v1/) | Section |
| generation | | 1.4, |
| | | Section |
| | | 4.1, |
| | | Section 5 |
+------------------+-------------------------------+------------+
| Charging | Exactly two: Authorise and | Section |
| operation count | Report | 3.2 |
+------------------+-------------------------------+------------+
| Authorise | POST /ocs/ai/v1/ | Section |
| endpoint | authorizations | 4.1 |
+------------------+-------------------------------+------------+
| Report endpoint | POST /ocs/ai/v1/ | Section |
| | grants/{grantId}/reports | 4.1 |
+------------------+-------------------------------+------------+
| Media and | application/json over HTTPS; | Section |
| transport | plaintext refused | 4.1 |
+------------------+-------------------------------+------------+
| Request | A per-gateway bearer API key | Section |
| authentication | in every request; OAuth2 is | 4.2 |
| | not in version 1 | |
+------------------+-------------------------------+------------+
| Redirect posture | HTTP 3xx is not used | Section |
| | | 1.4 |
+------------------+-------------------------------+------------+
| Charging-domain | One logical charging domain | Section |
| topology | per tenant in version 1 | 1.4 |
+------------------+-------------------------------+------------+
| Action lifecycle | One successful Authorise; one | Section |
| cardinality | ending | 3.1 |
+------------------+-------------------------------+------------+
| Current grant | Exactly one per live Action | Section 2 |
| cardinality | | |
+------------------+-------------------------------+------------+
| Outstanding | At most one logical Report | Section |
| Report | per Action | 3.4 |
| cardinality | | |
+------------------+-------------------------------+------------+
| Opening and mint | Lifecycle opening is G1; each | Section 2, |
| generations | mint is exactly +1; a flat | Section |
| | terminal envelope is | 3.3, |
| | generation 2 | Section |
Petkov & Panitch Expires 21 March 2027 [Page 37]
Internet-Draft The Pulse Protocol September 2026
| | | 3.5 |
+------------------+-------------------------------+------------+
| Outcome | Terminal outcome plus the | Section |
| retention ring | last 4 non-terminal outcomes; | 3.5, |
| | cycles and late adjustments | Section |
| | share the ring | 7.3 |
+------------------+-------------------------------+------------+
| Lapse grace | 5 s | Section |
| | | 3.5, |
| | | Section |
| | | 7.5, |
| | | Section 8 |
+------------------+-------------------------------+------------+
| LAPSE instant | endedAt = expiresAt + 5 s | Section |
| | | 7.5 |
+------------------+-------------------------------+------------+
| Recognition / | Until max(endedAt + 24 h, | Section 2, |
| late window | expiresAt + 1 h) | Section |
| | | 3.5, |
| | | Section |
| | | 7.6, |
| | | Section 8 |
+------------------+-------------------------------+------------+
| Decimal scale | Canonical decimal JSON | Section |
| and rendering | string; scale at most 6; the | 4.3 |
| | lexical exclusions are in | |
| | Section 4.3 | |
+------------------+-------------------------------+------------+
| Request-quantity | [0, 10^9], scale at most 6 | Section |
| domain | | 4.3 |
+------------------+-------------------------------+------------+
| Per-call and | Each is at most 10^9 | Section |
| Action/unit | | 4.4 |
| parser caps | | |
+------------------+-------------------------------+------------+
| Per-call | Terminate or segment before a | Section |
| operational | Call's cumulative value | 4.3 |
| segmentation | reaches 10^9 | |
| point | | |
+------------------+-------------------------------+------------+
| Weight domain | [0, 10^6], scale at most 6; | Section |
| | issued weights are positive | 4.3, |
| | | Section |
| | | 6.3 |
+------------------+-------------------------------+------------+
| Derived pooled | [0, 10^17), scale at most 6, | Section |
| domain | for the fields named in | 4.3 |
| | Section 4.3 | |
Petkov & Panitch Expires 21 March 2027 [Page 38]
Internet-Draft The Pulse Protocol September 2026
+------------------+-------------------------------+------------+
| Identifier | At most 128 characters from | Section |
| syntax | [A-Za-z0-9._:-] | 4.3 |
+------------------+-------------------------------+------------+
| Display-label | At most 64 printable UTF-8 | Section |
| syntax | characters; no control or | 4.3 |
| | bidirectional-control | |
| | characters | |
+------------------+-------------------------------+------------+
| Unit-type syntax | [a-z0-9_]{1,64} | Section |
| | | 4.3 |
+------------------+-------------------------------+------------+
| Timestamp | YYYY-MM-DDThh:mm:ss.sssZ; 3 | Section |
| profile | fractional digits; years | 4.3 |
| | 1970-9999; no leap seconds | |
+------------------+-------------------------------+------------+
| Count/version | JSON integer 0 <= n < 2^53; | Section |
| integer domain | schemaVersion >= 1 | 4.3, |
| | | Section 5 |
+------------------+-------------------------------+------------+
| Compatible | Every version from 1 through | Section 5 |
| schema support | maxSchemaVersion | |
+------------------+-------------------------------+------------+
| Request-body | At most 256 KB; the memo does | Section |
| limit | not define the KB base | 4.4 |
+------------------+-------------------------------+------------+
| Report patch | At most 256 calls[] entries | Section |
| size | | 4.4 |
+------------------+-------------------------------+------------+
| Unit-map size | At most 16 entries per Call; | Section |
| | estimate and response maps | 4.3, |
| | share the 16-unit bound | Section |
| | | 4.4, |
| | | Section |
| | | 6.1 |
+------------------+-------------------------------+------------+
| Receipt call | acceptedCalls has at most 256 | Section |
| count | entries and covers exactly | 4.3, |
| | the Calls in the settling | Section |
| | Report | 7.3 |
+------------------+-------------------------------+------------+
| Grant | grantedUnits > 0; 0 < | Section |
| inequalities | reportAt <= grantedUnits; | 6.3 |
| | quantumUnits > 0 where | |
| | present; expiresAt > | |
| | grantIssuedAt | |
+------------------+-------------------------------+------------+
| Pooled product | HALF_UP at scale 6 per | Section |
Petkov & Panitch Expires 21 March 2027 [Page 39]
Internet-Draft The Pulse Protocol September 2026
| rounding | cumulative unit-type product; | 6.3, |
| | exact sum | Section |
| | | 7.2 |
+------------------+-------------------------------+------------+
| Quantised rating | ceil(cumulativePooled / | Section |
| | quantumUnits); charge block | 7.2 |
| | deltas | |
+------------------+-------------------------------+------------+
| Threshold and | Raw, unquantised pooled-total | Section |
| exhaustion basis | delta from the cycle baseline | 7.2 |
+------------------+-------------------------------+------------+
| Report threshold | Dispatch when the cycle delta | Section |
| comparator | is >= reportAt | 7.7 |
+------------------+-------------------------------+------------+
| Threshold / | Section 7.7 requires | Section |
| closing cadence | threshold dispatch and | 7.7, |
| | recommends a prompt client | Section |
| | close; Section 12.4 says both | 12.4 |
| | are due immediately outside | |
| | the profile bound | |
+------------------+-------------------------------+------------+
| STOP flush | First chunk within 30 s, once | Section |
| | per ending; chunks of at most | 7.7, |
| | 256 changed Calls back-to- | Section 14 |
| | back | |
+------------------+-------------------------------+------------+
| Retry backoff | Initial 1 s, factor 2, full | Section |
| recommendation | jitter, cap 30 s | 13.3 |
| | (recommendation) | |
+------------------+-------------------------------+------------+
| Client attempt | 2 s per attempt | Section |
| deadline | (recommendation) | 13.5 |
| recommendation | | |
+------------------+-------------------------------+------------+
| TLS versions | TLS 1.2 or later required; | Section |
| | TLS 1.3 recommended; | 4.1, |
| | [RFC9325] configuration | Section |
| | | 15.1 |
+------------------+-------------------------------+------------+
| Request-identity | SHA-256 over JCS [RFC8785]; | Section |
| algorithm | append path grantId for | 6.5 |
| | Report | |
+------------------+-------------------------------+------------+
| Explicit HTTP | 200, 400, 401, 403, 404, 405, | Section |
| status mappings | 415, 429, and 503 as mapped | 3.5, |
| | | Section |
| | | 4.1, |
| | | Section |
Petkov & Panitch Expires 21 March 2027 [Page 40]
Internet-Draft The Pulse Protocol September 2026
| | | 13.1 |
+------------------+-------------------------------+------------+
| Additional | INTERNAL is status-mirroring; | Section |
| server-error | retry guidance covers 5xx | 13.1, |
| class | generally, whose exact | Section |
| | statuses are not enumerated | 13.3 |
+------------------+-------------------------------+------------+
| HTTP retry | Retry-After on 429 and 503 | Section |
| header | | 13.1 |
+------------------+-------------------------------+------------+
| HTTP connection | HTTP/2 connection reuse is | Section |
| posture | assumed (operational | 13.5 |
| | assumption) | |
+------------------+-------------------------------+------------+
| Current protocol | 1.1 — generation 1, wire | Section 5 |
| version | schemaVersion 2; each minor | |
| | binds to one schemaVersion | |
+------------------+-------------------------------+------------+
| Retryable | Exactly seven reasons from | Section 5, |
| refusal set | version 1.1 (six in 1.0), | Section |
| | enumerated in Section 6.4 | 6.4 |
+------------------+-------------------------------+------------+
| Closed per- | Decision, refusal reason, | Section 5, |
| version value | stop reason, error code, | Section 17 |
| sets | ending, directive, grade, and | |
| | reduce cause; initial values | |
| | in Section 17; additions bind | |
| | to minor versions per | |
| | Section 5's version table | |
+------------------+-------------------------------+------------+
| API-key rotation | Dual-active rotation; | Section |
| and revocation | revocation effective "within | 4.2, |
| posture | seconds" | Section |
| | | 15.1 |
+------------------+-------------------------------+------------+
Table 16
*Deployment-configurable.*
+=============================+==========================+==========+
| Name | Value | Defining |
| | | section |
+=============================+==========================+==========+
| Refusal human-review | Tenant-configured; | Section |
| channel (reviewContact) | absent unless | 6.4, |
| | configured | Section |
| | | 15.7 |
Petkov & Panitch Expires 21 March 2027 [Page 41]
Internet-Draft The Pulse Protocol September 2026
+-----------------------------+--------------------------+----------+
| Sponsor-cap accounting | Part of the sponsor's | Section |
| window | declaration (credits | 12.1 |
| | per period); boundary | |
| | mechanics deployment- | |
| | profile in version 1 | |
+-----------------------------+--------------------------+----------+
| maxSchemaVersion | Highest compatible | Section |
| | schema supported; 1 <= | 4.3, |
| | n < 2^53 | Section |
| | | 5 |
+-----------------------------+--------------------------+----------+
| grantedUnits | Server-selected per | Section |
| | cycle within the | 4.3, |
| | pooled domain and | Section |
| | grant inequalities | 6.3, |
| | | Section |
| | | 12.2 to |
| | | Section |
| | | 12.3 |
+-----------------------------+--------------------------+----------+
| reportAt | Server-selected per | Section |
| | cycle; 0 < reportAt <= | 6.3, |
| | grantedUnits | Section |
| | | 7.7 |
+-----------------------------+--------------------------+----------+
| Grant validity / expiresAt | Server-selected | Section |
| | instant after | 6.3, |
| | grantIssuedAt; no | Section |
| | default duration | 8, |
| | stated | Section |
| | | 12.3 |
+-----------------------------+--------------------------+----------+
| Action-Type vocabulary | Catalogue-defined, | Section |
| | whitelisted, and | 1.4, |
| | distributed out of | Section |
| | band | 6.1 |
+-----------------------------+--------------------------+----------+
| unitWeights | Positive plan/law | Section |
| | ratios, at most 10^6, | 4.3, |
| | pinned for the Action | Section |
| | | 6.3 |
+-----------------------------+--------------------------+----------+
| Price form and lawVersion | Catalogue/plan choice; | Section |
| | the selected law | 3.3, |
| | version is pinned | Section |
| | | 6.2 to |
| | | Section |
Petkov & Panitch Expires 21 March 2027 [Page 42]
Internet-Draft The Pulse Protocol September 2026
| | | 6.3 |
+-----------------------------+--------------------------+----------+
| Quantisation / quantumUnits | Action-Type choice; | Section |
| | positive pooled | 6.3, |
| | decimal where used | Section |
| | | 7.2 |
+-----------------------------+--------------------------+----------+
| Time-band schedule / | Tenant-published full- | Section |
| timeBandId | week schedule; band | 6.3, |
| | selected once at | Section |
| | opening | 10 |
+-----------------------------+--------------------------+----------+
| Action or family ceiling | Plan-defined internal | Section |
| | ceiling; no monetary | 2, |
| | value appears on the | Section |
| | wire | 6.2, |
| | | Section |
| | | 9, |
| | | Section |
| | | 15.7 |
+-----------------------------+--------------------------+----------+
| Contractual minimum | Plan-defined; applied | Section |
| | as described at open | 6.2, |
| | and not a second time | Section |
| | late | 7.6 |
+-----------------------------+--------------------------+----------+
| Funding scope and buckets | Plan-defined scope, | Section |
| | eligibility, and | 6.1 |
| | exclusivity | |
+-----------------------------+--------------------------+----------+
| limits.maxCallsPerAction | Always disclosed; | Section |
| | integer in the count | 4.3, |
| | domain; counts | Section |
| | distinct Calls over | 6.3, |
| | the Action | Section |
| | | 7.2 |
+-----------------------------+--------------------------+----------+
| limits.maxRuntimeSec | Section 6.3 says | Section |
| | always disclosed as a | 4.3, |
| | plan value or platform | Section |
| | default and exceeded | 6.3, |
| | when now > open + | Section |
| | value; Section 15.4 | 15.4 |
| | calls maximum runtime | |
| | optional per | |
| | deployment/plan | |
+-----------------------------+--------------------------+----------+
| limits.maxConcurrentActions | Always disclosed | Section |
Petkov & Panitch Expires 21 March 2027 [Page 43]
Internet-Draft The Pulse Protocol September 2026
| | subscriber-level | 4.3, |
| | integer cap | Section |
| | | 6.3, |
| | | Section |
| | | 15.4 |
+-----------------------------+--------------------------+----------+
| limits.signaturesRequired | Boolean where a | Section |
| | bilateral signature | 6.3, |
| | profile is configured | Section |
| | | 7.2, |
| | | Section |
| | | 15.4 |
+-----------------------------+--------------------------+----------+
| Call-signature profile | Bilateral and out of | Section |
| | band; no field- | 1.4, |
| | specific size cap is | Section |
| | stated | 7.2 |
+-----------------------------+--------------------------+----------+
| limits.minRate | Capacity-only pooled | Section |
| | units per second; | 4.3, |
| | exact upper numeric | Section |
| | domain is not stated | 6.3, |
| | | Section |
| | | 12.2 to |
| | | Section |
| | | 12.3 |
+-----------------------------+--------------------------+----------+
| limits.enforcementWindowSec | Capacity-only integer | Section |
| | seconds, greater than | 4.3, |
| | 0; exact algorithm | Section |
| | provisional | 6.3, |
| | | Section |
| | | 12.2 |
+-----------------------------+--------------------------+----------+
| Sponsor cap and owner label | Sponsor-defined | Section |
| | aggregate internal | 12.1, |
| | bound and | Section |
| | presentation-safe | 15.7 |
| | label | |
+-----------------------------+--------------------------+----------+
| Capacity pool | Tenant-defined | Section |
| | provisioned rate and | 12.2 |
| | named Action-Type | |
| | membership | |
+-----------------------------+--------------------------+----------+
| Capacity pressure / | Deployment-declared; | Section |
| reclamation policy | no numeric default | 6.4, |
| | stated | Section |
Petkov & Panitch Expires 21 March 2027 [Page 44]
Internet-Draft The Pulse Protocol September 2026
| | | 12.2 to |
| | | Section |
| | | 12.3 |
+-----------------------------+--------------------------+----------+
| Integration promptness | Agreed per | Section |
| bound | integration; | 12.4 |
| | provisional version-1 | |
| | default is 30 s at p95 | |
+-----------------------------+--------------------------+----------+
| Refusal-body retryAfter | Server-selected | Section |
| | integer seconds, | 4.3, |
| | required when | Section |
| | retryable: true | 6.4 |
+-----------------------------+--------------------------+----------+
| Error-envelope retry | retryable and | Section |
| members | retryAfter are | 13.1 |
| | independently | |
| | optional; no status | |
| | coupling or numeric | |
| | default is stated | |
+-----------------------------+--------------------------+----------+
| HTTP Retry-After value | Server-selected on 429 | Section |
| | and 503; Pulse states | 13.1 |
| | no minimum or maximum | |
+-----------------------------+--------------------------+----------+
| Client pre-lapse margin | Measured server-clock | Section |
| | offset plus round-trip | 8 |
| | margin; no fixed value | |
+-----------------------------+--------------------------+----------+
| Velocity limits | Optional account/ | Section |
| | balance controls; | 6.4, |
| | thresholds and windows | Section |
| | not stated | 15.4 |
+-----------------------------+--------------------------+----------+
| Consumption-inflation wire | Optional control; | Section |
| | threshold/window not | 7.4, |
| | stated | Section |
| | | 15.4 |
+-----------------------------+--------------------------+----------+
| Per-credential API rate | Token-bucket rate/ | Section |
| limit | capacity; no numeric | 13.1, |
| | default stated | Section |
| | | 15.5 |
+-----------------------------+--------------------------+----------+
| Draining state | Operator-controlled | Section |
| | temporary state; delay | 6.4, |
| | returned in retryAfter | Section |
| | | 11 |
Petkov & Panitch Expires 21 March 2027 [Page 45]
Internet-Draft The Pulse Protocol September 2026
+-----------------------------+--------------------------+----------+
| Mutual TLS | Optional deployment | Section |
| | choice | 4.2 |
+-----------------------------+--------------------------+----------+
| Credential tenant set | One or more bound | Section |
| | tenants; exact | 2, |
| | cardinality not stated | Section |
| | | 15.2 |
+-----------------------------+--------------------------+----------+
| API-key material and exact | Deployment credential | Section |
| revocation SLA | configuration; no | 4.2, |
| | exact seconds bound is | Section |
| | stated | 15.1 |
+-----------------------------+--------------------------+----------+
| Persistent clock-skew | Integration trust | Section |
| tolerance | threshold; no value | 15.6 |
| | stated | |
+-----------------------------+--------------------------+----------+
| Audit, gatewayTime, and | Operator retention | Section |
| startedAt retention | policy, apart from the | 15.7 |
| | fixed late window | |
+-----------------------------+--------------------------+----------+
Table 17
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
Petkov & Panitch Expires 21 March 2027 [Page 46]
Internet-Draft The Pulse Protocol September 2026
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:
* The band is resolved *once, at authorisation, on the server clock*
— never from any client-asserted time — and disclosed as
timeBandId in the response.
* The entire Action rates in its opening band: *the band never
switches mid-Action*, however long the Action runs or when its
reports arrive. (Deliberate divergence from telecom voice
charging; exposure is bounded by grant validity and maximum
runtime.)
* Published schedules always cover every instant (the platform
validates full-week coverage before publication), so band
resolution cannot fail.
Petkov & Panitch Expires 21 March 2027 [Page 47]
Internet-Draft The Pulse Protocol September 2026
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.
Petkov & Panitch Expires 21 March 2027 [Page 48]
Internet-Draft The Pulse Protocol September 2026
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.
Petkov & Panitch Expires 21 March 2027 [Page 49]
Internet-Draft The Pulse Protocol September 2026
* *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
+=============+===================================================+
| 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 |
Petkov & Panitch Expires 21 March 2027 [Page 50]
Internet-Draft The Pulse Protocol September 2026
| | 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 + | The server's own condition — transient contention |
| Retry-After | 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. |
+-------------+---------------------------------------------------+
Table 18
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.
Petkov & Panitch Expires 21 March 2027 [Page 51]
Internet-Draft The Pulse Protocol September 2026
13.2. errorCode registry (400 family, version 1)
+====================+==================================+
| 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. |
+--------------------+----------------------------------+
Table 19
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)
Petkov & Panitch Expires 21 March 2027 [Page 52]
Internet-Draft The Pulse Protocol September 2026
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.
Petkov & Panitch Expires 21 March 2027 [Page 53]
Internet-Draft The Pulse Protocol September 2026
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).
Petkov & Panitch Expires 21 March 2027 [Page 54]
Internet-Draft The Pulse Protocol September 2026
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
Petkov & Panitch Expires 21 March 2027 [Page 55]
Internet-Draft The Pulse Protocol September 2026
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.
Petkov & Panitch Expires 21 March 2027 [Page 56]
Internet-Draft The Pulse Protocol September 2026
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.
Petkov & Panitch Expires 21 March 2027 [Page 57]
Internet-Draft The Pulse Protocol September 2026
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.
Petkov & Panitch Expires 21 March 2027 [Page 58]
Internet-Draft The Pulse Protocol September 2026
+===========+=========================================+==========+
| Registry | Initial contents | Defining |
| | | section |
+===========+=========================================+==========+
| Pulse | GRANT, REFUSE | Section |
| Decision | | 6.3, |
| | | Section |
| | | 6.4 |
+-----------+-----------------------------------------+----------+
| Pulse | NO_HEADROOM, CEILING_PREEMPT, | Section |
| Refusal | ACCOUNT_VELOCITY, BALANCE_VELOCITY, | 6.4 |
| Reason | 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) | |
+-----------+-----------------------------------------+----------+
| Pulse | CEILING_REACHED, NO_HEADROOM, | Section |
| Stop | CALL_LIMIT, MAX_RUNTIME, INFLATION, | 7.4 |
| Reason | SIGNATURES_REQUIRED, PARENT_STOPPED, | |
| | ADMIN_FORCED, DRAINING, NO_CAPACITY, | |
| | EXPOSURE_CAP, LAPSED | |
+-----------+-----------------------------------------+----------+
| Pulse | UNSUPPORTED_SCHEMA, LIMIT_EXCEEDED, | Section |
| Error | MALFORMED_REPORT, MALFORMED_REQUEST, | 13.1, |
| Code | UNAUTHORIZED, FORBIDDEN, NOT_FOUND, | Section |
| | METHOD_NOT_ALLOWED, UNSUPPORTED_MEDIA, | 13.2 |
| | RATE_LIMITED, UNAVAILABLE, INTERNAL | |
+-----------+-----------------------------------------+----------+
| Pulse | CLOSE, STOP, LAPSE | Section |
| Ending | | 2, |
| | | Section |
| | | 3.5 |
+-----------+-----------------------------------------+----------+
| Pulse | CONTINUE, CURRENT, SETTLED, STOP | Section |
| Directive | | 7.3 |
+-----------+-----------------------------------------+----------+
| Pulse | PROVEN, ATTESTED, BOUNDED | Section |
| Grade | | 6.4 |
+-----------+-----------------------------------------+----------+
| Pulse | CAPACITY | Section |
| Reduce | | 12.3 |
| Cause | | |
+-----------+-----------------------------------------+----------+
Table 20
Petkov & Panitch Expires 21 March 2027 [Page 59]
Internet-Draft The Pulse Protocol September 2026
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, March 1997,
<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, July 2002,
<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, August 2008,
<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,
May 2017, <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, December 2017,
<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, August 2018,
<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,
June 2019, <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, June 2020,
<https://www.rfc-editor.org/rfc/rfc8785>.
Petkov & Panitch Expires 21 March 2027 [Page 60]
Internet-Draft The Pulse Protocol September 2026
[RFC9110] Fielding, R., Ed., Nottingham, M., Ed., and J. Reschke,
Ed., "HTTP Semantics", STD 97, RFC 9110,
DOI 10.17487/RFC9110, June 2022,
<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, November
2022, <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, August 2005,
<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, July 2016,
<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, June 2017,
<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, March 2019,
<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.
Petkov & Panitch Expires 21 March 2027 [Page 61]
Internet-Draft The Pulse Protocol September 2026
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
Petkov & Panitch Expires 21 March 2027 [Page 62]
Internet-Draft The Pulse Protocol September 2026
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" }
Petkov & Panitch Expires 21 March 2027 [Page 63]
Internet-Draft The Pulse Protocol September 2026
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
Petkov & Panitch Expires 21 March 2027 [Page 64]
Internet-Draft The Pulse Protocol September 2026
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.
Petkov & Panitch Expires 21 March 2027 [Page 65]
Internet-Draft The Pulse Protocol September 2026
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)
*PROVISIONAL — HAND-WRITTEN:* These schemas are hand-written from
the prose of this memo. They are to be regenerated from the
reference implementation's validated model, as Status of This Memo
already specifies for the field-presence matrix. On any conflict,
the prose is authoritative.
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}"
Petkov & Panitch Expires 21 March 2027 [Page 66]
Internet-Draft The Pulse Protocol September 2026
; [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"
Petkov & Panitch Expires 21 March 2027 [Page 67]
Internet-Draft The Pulse Protocol September 2026
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
)
Petkov & Panitch Expires 21 March 2027 [Page 68]
Internet-Draft The Pulse Protocol September 2026
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
)
Petkov & Panitch Expires 21 March 2027 [Page 69]
Internet-Draft The Pulse Protocol September 2026
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
)
Petkov & Panitch Expires 21 March 2027 [Page 70]
Internet-Draft The Pulse Protocol September 2026
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)
; ----------
Petkov & Panitch Expires 21 March 2027 [Page 71]
Internet-Draft The Pulse Protocol September 2026
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,
Petkov & Panitch Expires 21 March 2027 [Page 72]
Internet-Draft The Pulse Protocol September 2026
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
)
Petkov & Panitch Expires 21 March 2027 [Page 73]
Internet-Draft The Pulse Protocol September 2026
//
(
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 = {
Petkov & Panitch Expires 21 March 2027 [Page 74]
Internet-Draft The Pulse Protocol September 2026
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,
Petkov & Panitch Expires 21 March 2027 [Page 75]
Internet-Draft The Pulse Protocol September 2026
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,
Petkov & Panitch Expires 21 March 2027 [Page 76]
Internet-Draft The Pulse Protocol September 2026
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.
Petkov & Panitch Expires 21 March 2027 [Page 77]
Internet-Draft The Pulse Protocol September 2026
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
Petkov & Panitch Expires 21 March 2027 [Page 78]
Internet-Draft The Pulse Protocol September 2026
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
Petkov & Panitch Expires 21 March 2027 [Page 79]
Internet-Draft The Pulse Protocol September 2026
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:
+============+==========+
| Unit type | Weight |
+============+==========+
| tokens_in | 0.333333 |
+------------+----------+
| tokens_out | 2.5 |
+------------+----------+
Table 21
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:
Petkov & Panitch Expires 21 March 2027 [Page 80]
Internet-Draft The Pulse Protocol September 2026
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:
Petkov & Panitch Expires 21 March 2027 [Page 81]
Internet-Draft The Pulse Protocol September 2026
{
"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:
Petkov & Panitch Expires 21 March 2027 [Page 82]
Internet-Draft The Pulse Protocol September 2026
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)
Petkov & Panitch Expires 21 March 2027 [Page 83]
Internet-Draft The Pulse Protocol September 2026
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:
Petkov & Panitch Expires 21 March 2027 [Page 84]
Internet-Draft The Pulse Protocol September 2026
{
"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.
Petkov & Panitch Expires 21 March 2027 [Page 85]
Internet-Draft The Pulse Protocol September 2026
*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
+====+===========================+==================================+
| # | Framework term — the | This specification |
| | opening of its | |
| | definition box | |
+====+===========================+==================================+
| 1 | *Action* — _One | *Action* (shared term) |
| | customer-visible unit | |
| | of work with one price | |
| | question: a question | |
| | answered, a picture | |
| | generated, a research | |
| | task completed._ | |
+----+---------------------------+----------------------------------+
| 2 | *Balance* — _Money | _No wire concept_ — the server |
| | with rules: an amount, | holds it; the gateway sees |
| | a priority in the | refusals and stops. scope names |
| | draw-down order, | a Balance's Scope (Section 6.1) |
| | optionally a Scope and | |
| | an Expiry, and a | |
| | declared behaviour at | |
| | its limit._ | |
+----+---------------------------+----------------------------------+
| 3 | *Ceiling* — _The most | *Ceiling / headroom* (shared |
| | one Action may charge | term, Section 2); it reaches the |
| | — "up to" as an | wire only as CEILING_REACHED |
| | enforced number._ | (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 | *Countable* — _A | _No wire concept_ — the meter's |
| | number computed from | unit is the framework's _counted |
| | records that answers | unit_: units and the unit types |
Petkov & Panitch Expires 21 March 2027 [Page 86]
Internet-Draft The Pulse Protocol September 2026
| | one operational | (Section 7.2) |
| | question, on a stated | |
| | cadence and at a | |
| | stated grain._ | |
+----+---------------------------+----------------------------------+
| 5 | *Cycle* — _One Quota's | *Cycle* (shared term); the cycle |
| | span: grant, consume, | Report rides it (Section 7.3) |
| | settle. A reserved | |
| | Action is a chain of | |
| | Cycles._ | |
+----+---------------------------+----------------------------------+
| 6 | *Expiry* — _A date | _No wire concept._ Through |
| | after which a | draft-03 this document's ending |
| | Balance's unreserved | for a lapsed grant was named |
| | credits are written | EXPIRY and the two were false |
| | off. Reserved credits | friends; from draft-04 that |
| | are spared until their | ending is LAPSE (D.2) |
| | Actions settle._ | |
+----+---------------------------+----------------------------------+
| 7 | *Facility* — _One | _No wire concept_ — tenantRef |
| | regulatory regime, as | identifies the Tenant, which a |
| | fine-grained as | Facility contains (D.2) |
| | reality requires — | |
| | Belgium; Kenya; a | |
| | single US state where | |
| | its rules differ._ | |
+----+---------------------------+----------------------------------+
| 8 | *Grant* — no | decision: GRANT (Section 6.3) — |
| | definition box: the | the act. The object the |
| | second step of the | framework calls a Quota is this |
| | loop _Authorise · | document's *Grant* (row 16) |
| | Grant · Supervise · | |
| | Settle_, the act of | |
| | issuing a Quota | |
+----+---------------------------+----------------------------------+
| 9 | *Metering Point* — | The *Gateway* (Section 2) — |
| | _The component that | Section 15.7's attesting party |
| | executes Inference | |
| | Calls, counts | |
| | consumption and obeys | |
| | the Quota. In the AI | |
| | ecosystem the role is | |
| | played by the | |
| | gateway._ | |
+----+---------------------------+----------------------------------+
| 10 | *Node* — no definition | tenantRef, and accountRef for |
| | box; the reference | the position below — *not scope* |
| | engine's class Node: | |
Petkov & Panitch Expires 21 March 2027 [Page 87]
Internet-Draft The Pulse Protocol September 2026
| | _one path from the | |
| | Platform through the | |
| | Facility chain to a | |
| | Tenant_ | |
+----+---------------------------+----------------------------------+
| 11 | *Packet* | Partial: the per-settlement |
| | (Substantiation | receipt (Section 7.3) is the |
| | Packet) — _Everything | wire-visible record; the Packet |
| | needed to defend one | itself is off-wire |
| | Charge to a regulator | |
| | or a court, in five | |
| | limbs._ | |
+----+---------------------------+----------------------------------+
| 12 | *Party* — _A legal or | The roles a party takes on this |
| | natural person with | wire: sponsor (limitOwner), |
| | standing in at least | subscriber (accountRef), gateway |
| | one relation. The | (the credential), tenant |
| | Framework records that | (Section 2); limitOwner names |
| | a party holds a | the exposed one (Section 12.1) |
| | relation, or changed; | |
| | it never verifies who | |
| | they are._ | |
+----+---------------------------+----------------------------------+
| 13 | *Period* — _The Bill's | The sponsor's declared |
| | integration window, | accounting window (Section 12.1) |
| | opened and closed on | — narrower; the Bill's Period |
| | the Facility's | has no wire term |
| | calendar._ | |
+----+---------------------------+----------------------------------+
| 14 | *Price Book* — _A | No wire object — lawVersion is |
| | Tenant's complete set | "the pinned plan/price-book |
| | of Action Types with | version" (Section 6.3); the book |
| | their price forms and | itself is the tenant's published |
| | elements, held as a | catalogue, server-side |
| | versioned object with | |
| | an identity._ | |
+----+---------------------------+----------------------------------+
| 15 | *Profile* (Metering | *Integration profile* |
| | Point Profile) — _The | (Section 2, Section 12.4) — a |
| | record of what an | related object under a different |
| | integration actually | name; *not this document's |
| | does, per route … | _deployment profile_* — the |
| | Established by running | false friend below |
| | the conformance suite | |
| | against it, never by | |
| | reading its | |
| | documentation._ | |
+----+---------------------------+----------------------------------+
Petkov & Panitch Expires 21 March 2027 [Page 88]
Internet-Draft The Pulse Protocol September 2026
| 16 | *Quota* — _The | *Grant* (grantedUnits, reportAt, |
| | Reservation's outside | expiresAt) — the RFC 4006 |
| | face: the same amount | granted-quota concept |
| | expressed to the | (Section 1.2) |
| | Metering Point in work | |
| | units, carrying a | |
| | report threshold and a | |
| | Validity._ | |
+----+---------------------------+----------------------------------+
| 17 | *Relation* — _A dated, | _No wire concept_ — the wire |
| | typed, scoped link | sees the sponsor relation only |
| | between a Party and | through limitOwner; the exposure |
| | work: EXPOSED or | relation's aggregate is |
| | EXECUTES._ (v3.1's | EXPOSURE_CAP's basis |
| | box; the current | (Section 12.1) |
| | edition adds _the | |
| | record relation, HOLDS | |
| | and RELIES_) | |
+----+---------------------------+----------------------------------+
| 18 | *Reservation* — | An open grant (pool exposure) — |
| | _Credits set aside on | "spend the sponsor is already |
| | a Balance for one | standing behind" (Section 12.1) |
| | Action, under the | |
| | Solvency Rule. The | |
| | promise held against | |
| | money._ | |
+----+---------------------------+----------------------------------+
| 19 | *Action Type* — _The | *Action Type* (actionType; |
| | Price Book row an | Section 2, Section 6.1) |
| | Action instantiates: | |
| | its price form, its | |
| | elements, its counted | |
| | unit, its thresholds._ | |
+----+---------------------------+----------------------------------+
| 20 | *Effective Law* — _The | The law pinned at open |
| | single package of | (Section 2, Section 6.2); |
| | properties an Action | lawVersion identifies its |
| | receives at open: the | version |
| | path from Platform | |
| | through its Facility | |
| | chain to its Tenant, | |
| | resolved top-down._ | |
+----+---------------------------+----------------------------------+
| 21 | *Tenant* — _Whose | *Tenant* (tenantRef; Section 2, |
| | money and commercial | Section 6.1) |
| | law. A Tenant owns | |
| | its Price Book, its | |
| | Balances and one | |
Petkov & Panitch Expires 21 March 2027 [Page 89]
Internet-Draft The Pulse Protocol September 2026
| | settlement | |
| | relationship, and | |
| | attaches to exactly | |
| | one Facility._ | |
+----+---------------------------+----------------------------------+
| 22 | *Weights* — _Per-type | unitWeights (Section 6.3) — the |
| | conversion from | complete map, pinned with the |
| | counted units to | law |
| | billing units … Part | |
| | of the price, pinned | |
| | at open._ | |
+----+---------------------------+----------------------------------+
| 23 | *stop_reason* (the | directive: STOP with stopReason |
| | Section 9 field) — | (Section 7.3, Section 7.4) — the |
| | _Why the Action must | stop obligation |
| | end now: the Trip- | |
| | wire, the cascade …_ | |
+----+---------------------------+----------------------------------+
Table 22
*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
+============================+======================================+
| Implementation / telecom | This specification |
| term | |
+============================+======================================+
| Facility / region (tenancy | _No wire concept_ — subscriber- |
| detail below the operator) | level detail below the tenant |
| | boundary |
Petkov & Panitch Expires 21 March 2027 [Page 90]
Internet-Draft The Pulse Protocol September 2026
+----------------------------+--------------------------------------+
| Subscriber (MSISDN) | Account (accountRef — |
| | pseudonymous on the wire, |
| | Section 15.7) |
+----------------------------+--------------------------------------+
| Charging session | Action |
+----------------------------+--------------------------------------+
| Decision-tree | Action Type (Actions arrive |
| classification | _typed_; classification collapses |
| | into a catalogue lookup) |
+----------------------------+--------------------------------------+
| Interim usage report | Cycle Report (final: false) |
| (opIntermediate) | |
+----------------------------+--------------------------------------+
| Final usage report | Closing Report (final: true, the |
| (opFinal) | CLOSE ending) |
+----------------------------+--------------------------------------+
| CDR (the concluded-session | The concluded Action record |
| record) | (emitted at ending; not a wire |
| | object) |
+----------------------------+--------------------------------------+
| CODR (per-settlement | The cycle settlement and its |
| record) | receipt |
+----------------------------+--------------------------------------+
| Time band | Time band (shared term — |
| | Section 10 adopted it from the |
| | implementation) |
+----------------------------+--------------------------------------+
Table 23
*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
Petkov & Panitch Expires 21 March 2027 [Page 91]
Internet-Draft The Pulse Protocol September 2026
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
Email: martin.petkov@effortel.com
Arkadi Panitch
Effortel Technologies SA
Petkov & Panitch Expires 21 March 2027 [Page 92]
Internet-Draft The Pulse Protocol September 2026
Email: arkadi.panitch@effortel.com
Petkov & Panitch Expires 21 March 2027 [Page 93]