GRIP Protocol Specification

Version: 0.1 (stable) · Published 2026-08 Change control: see §10. v0.1 message schemas are stable — backwards-compatible (additive) changes only until v0.2.

The key words MUST, MUST NOT, SHOULD, SHOULD NOT, and MAY are to be interpreted as described in RFC 2119.


0. Overview and the five primitives

GRIP is a control-plane protocol for grid-connected assets. The physical grid is the data plane; GRIP carries identity, authorization (operating envelopes), price/congestion signals, telemetry, and settlement. A forward-market layer (the Capacity Exchange, §4) sits on top of the spot layer and feeds back into it: a cleared firm forward position becomes a floor under the holder's granted envelope (§4.4). That coupling is the load-bearing idea of the protocol.

0.1 The five primitives (normative naming)

# Primitive Core objects (this spec)
P1 Identity & registration identity.cert, identity.revoke (§3.1)
P2 Envelope (Dynamic Operating Envelope lease) envelope.request, envelope.grant, envelope.firm_breach (§3.2)
P3 Congestion signal signal.tick, signal.flag (§3.3)
P4 Firmness (QoS class) the firmness field, present on every cert, envelope, product, and settlement record (§3.4)
P5 Telemetry & settlement telemetry.report, settlement.* (§3.5)

Two cross-cutting mechanisms are used by all five primitives and by the forward layer, and are specified once:

  • Attestation (§1.5, §2): every protocol-significant message is a signed record (GripRecord) whose signature is computed over a canonical JSON preimage, and whose hash is appended to the issuing zone's transparency log (§6). "Attestation" in GRIP means signature + log inclusion, not just signature.
  • Position (§4.3): the cleared-forward object produced by the Capacity Exchange and consumed by the envelope server (P2) and the settlement engine (P5).

0.2 Layering

            ┌────────────────────────────────────────────┐
            │  Capacity Exchange (forward layer, §4)     │
            │  orders → trades → POSITIONS               │
            └───────┬───────────────────────┬────────────┘
              floors on grants        forward settlement leg
            ┌───────▼────────┐  ┌───────────▼────────────┐
 P1 identity│ P2 envelope    │  │ P5 telemetry/settlement│
 (certs, §3.1)  (leases, §3.2) │  │ (signed, 5-min, §3.5) │
            └───────┬────────┘  └───────────▲────────────┘
                    │      P3 signals       │
                    └── (ticks + flags, §3.3)┘
     All records attested: canonical JSON (§2) + transparency log (§6)

1. Conventions and data model

1.1 Node identifiers

node_id is a DNS-style hierarchical name, leaf-to-root, dot-separated:

bldg7.feeder12.coshocton.aep.pjm.grid
└leaf┘ └─────────ancestor zones────┘root

Grammar (ABNF):

node-id = label *("." label)
label   = lowchar *62(lowchar / DIGIT / "-")   ; no leading digit/hyphen, no trailing hyphen
lowchar = %x61-7A                              ; a-z
  • Labels are lowercase ASCII only; max total length 253 bytes.
  • The root label for the reference deployment is grid. The root label is deployment configuration (§9), not a protocol constant.
  • A zone is any suffix of a node_id operated by one authority (§6). The zone of a node is its nearest ancestor registrar's name.

1.2 Time

All timestamps are RFC 3339 UTC with exactly millisecond precision and the Z suffix: YYYY-MM-DDTHH:MM:SS.sssZ. No other form is valid in signed payloads (determinism requirement, §2). Durations are integer seconds in fields suffixed _s. Intervals are half-open: [valid_from, valid_until).

1.3 Quantities — integers only in signed payloads

Signed payloads MUST NOT contain JSON floats (§2.2). Normative units:

Quantity Unit Field suffix Example
Power integer watts _w "import_max_w": 450000 (= 450 kW)
Ramp integer watts/second _w_s "ramp_max_w_s": 50000
Energy integer watt-hours _wh "kwh_interval""interval_wh"
Ratios (congestion, ELCC…) integer basis points 0–10000 _bp "congestion_bp": 7300 (= 0.73)
Latency/durations integer ms / s _ms / _s "control_latency_ms": 500

Display vs wire. 450 kW and $142.50/MWh are display forms. Wire fields are integer watts and Money objects (below), because canonical signing across SDK languages cannot tolerate floating point. Display layers SHOULD render kW/MW and decimal dollars.

1.4 Money

Every monetary value is a Money object — integer minor units plus ISO 4217 currency:

{ "amount": 14250, "currency": "USD" }
  • amount — integer minor units (cents for USD). MAY be negative in settlement records.
  • Prices carry an explicit denominator field per on the enclosing object where ambiguity is possible: "per": "MWh" (energy) or "per": "MW-day" (capacity, deliberately matching PJM RPM's unit).
  • The sandbox ledger uses the reserved private currency code XGD ("grid dollars", play money). Real-money deployments use real ISO codes and are out of scope for v0.1 (§5).

1.5 The signed record wrapper: GripRecord

Every protocol-significant message is carried as a GripRecord:

{
  "grip": "0.1",
  "kind": "envelope.grant",
  "id": "0d9f1c2a-7b1e-4b9a-9c3d-2f4e5a6b7c8d",
  "zone": "aep.pjm.grid",
  "ts": "2026-08-11T17:00:00.000Z",
  "payload": { "…kind-specific schema…": "…" },
  "att": {
    "alg": "ed25519",
    "kid": "k1_9f2c…",
    "sig": "base64url(64-byte Ed25519 signature)",
    "log": { "log_id": "aep.pjm.grid#0", "leaf_index": 182931, "checkpoint": "…" }
  }
}
Field Req Meaning
grip MUST Protocol version token. Fixed literal "0.1" for this spec. The key name grip is a wire constant, not a brand string (§9).
kind MUST Record kind, lowercase dotted token (registry in §10.2).
id MUST UUIDv4, unique per record, assigned by the signer.
zone MUST Zone of the signing authority (or the asset's node for asset-signed records).
ts MUST Signing time (§1.2).
payload MUST Kind-specific object (schemas in §3, §4, §6).
att MUST Attestation block. sig covers everything except att itself (§2.3). att.log is absent at emit time for latency-critical kinds and attached asynchronously (§6.2); all other fields are mandatory.

kid is the key identifier: k1_ + first 16 bytes (hex) of SHA-256 of the signer's raw Ed25519 public key. Verifiers resolve kid → certificate → delegation chain per §6.3.

Signers. Zone services (registrar, envelope server, exchange, settlement engine) sign with zone keys; assets sign (telemetry.report, envelope.request, order messages) with their certificate key. Asset private keys MUST NOT leave the device/SDK.


2. Canonical JSON encoding (GRIP-CJ)

Signatures and hashes must be stable across TypeScript, Python, and any future SDK. GRIP adopts RFC 8785 (JSON Canonicalization Scheme, JCS) with a restricting profile that removes every JCS pain point (float serialization) rather than re-specifying it per language.

2.1 Canonicalization

GRIP-CJ(value) = the octet string produced by RFC 8785 applied to value. In practice, for payloads valid under §2.2 this equals:

  1. UTF-8 encoding, no BOM.
  2. Object keys sorted lexicographically by UTF-16 code units (RFC 8785 §3.2.3).
  3. No insignificant whitespace; separators exactly , and :.
  4. Strings serialized per RFC 8785 §3.2.2.2 (minimal escaping; non-ASCII emitted literally, not \u-escaped).
  5. Numbers per §2.2 (integers only, so serialization is the shortest decimal form with no exponent, no leading zeros; negative zero serializes as 0).
  6. Array order is significant and preserved.

Reference implementations: json.dumps(v, sort_keys=True, separators=(",", ":"), ensure_ascii=False) (Python) and a JCS library or equivalent stable-stringify (TypeScript) — every SDK's conformance suite MUST include cross-language vector tests (Appendix A).

2.2 Signed-payload restrictions (normative)

A value is GRIP-signable iff, recursively:

  • Numbers are integers in the range ±2⁵³−1 (IEEE-754 exact). Floats are forbidden; anything fractional uses the integer units of §1.3/§1.4 or a decimal string.
  • Object keys are unique after canonicalization.
  • Strings are valid Unicode (no unpaired surrogates).
  • null is permitted but a field set to null is NOT the same record as the field absent — producers MUST omit unset optional fields rather than emitting null.

Producers MUST reject (not silently coerce) non-signable values. Verifiers MUST verify against received canonical bytes recomputed from the parsed value — if recanonicalization differs from what was signed, the record is invalid.

2.3 Hash and signature preimage

record_hash    = SHA-256( GRIP-CJ(record minus "att") )
sig_preimage   = "GRIP-SIG-V0" 0x0A kind 0x0A GRIP-CJ(record minus "att")
att.sig        = Ed25519-sign(signer_key, sig_preimage)
  • "record minus att" = the GripRecord object with the att member removed entirely.
  • The domain-separation prefix (GRIP-SIG-V0\n<kind>\n) prevents cross-kind signature replay; kind appears in both the prefix and the signed body.
  • record_hash is the transparency-log leaf value (§6.2) and the canonical way to reference a record from another record (fields suffixed _hash).
  • Derived deterministic identifiers use their own prefixes, e.g. instrument ids: gi_ + first 16 bytes (hex) of SHA-256("GRIP-INSTR-V0" 0x0A GRIP-CJ(listing)) (§4.2).

Algorithms are fixed for v0.1: SHA-256 + Ed25519. att.alg exists so v0.2+ can add algorithms without changing the wrapper.


3. The five primitives — wire schemas

3.1 P1 — Identity & registration

Registration is one API call plus client-side key generation. The sandbox registrar issues test certs instantly.

identity.cert (payload; signed by the Zone Registrar — this record is the certificate; an X.509 carrier profile is deferred to v0.2, the JSON form is normative):

{
  "serial": "c_5f21…",
  "node_id": "bldg7.feeder12.coshocton.aep.pjm.grid",
  "public_key": "base64url(32-byte Ed25519 public key)",
  "asset_class": "storage",
  "capability": {
    "import_max_w": 500000,
    "export_max_w": 500000,
    "ramp_max_w_s": 100000,
    "energy_capacity_wh": 2000000,
    "control_latency_ms": 500
  },
  "firmness": "flexible",
  "org_id": "org_7ac2…",
  "not_before": "2026-08-11T00:00:00.000Z",
  "not_after": "2027-08-11T00:00:00.000Z"
}
  • asset_classload | generator | storage | prosumer.
  • firmness per §3.4. org_id binds the asset to the account that owns exchange positions and settlement balances.
  • The cert chain (§6.3) runs from this record up through zone delegation records to the root. Anyone can verify a cert without contacting the registrar (offline verification), given the zone's delegation chain.

identity.revoke: { "serial": "…", "reason": "key_compromise | superseded | decommissioned" }, signed by the issuing registrar. Both kinds MUST be logged (§6.2).

Org-key certs (§4.3 order auth; added in v0.1.x). An org key authenticates an organization's Capacity Exchange order-signing key, not a grid-connected device — "an asset-class-less cert bound to org_id" (§4.3). It is still kind identity.cert (no new record kind) but the payload omits node_id, asset_class, capability, and firmness (§2.2: producers MUST omit unset optional fields rather than emit null):

{ "serial": "c_…", "org_id": "org_7ac2…", "public_key": "base64url(32-byte Ed25519 public key)", "not_before": "…", "not_after": "…" }

Issued the same way as an asset cert — POST /v1/register — distinguished by a request-side discriminator (e.g. cert_kind: "org"); the exact request field name is a sandbox implementation detail, not fixed wire schema, since the request is not itself a GripRecord. A verifier resolves an org key the same way as any other signer: att.kid → this cert → the cert's own validity window (§6.3 step 1 subset); the full delegation-chain walk (§6.3 step 2) is unaffected since org-key certs are issued by the same zone registrar as asset certs.

Registration API (DEV CONSOLE tier key required; §7):

POST /v1/register        body: cert request (node label, asset_class, capability, firmness, public_key)
→ 201 { "cert": GripRecord<identity.cert>, "chain": [GripRecord<federation.delegation>, …] }

3.2 P2 — Envelope (operating-envelope leases)

Envelopes are short-lived leases on operating headroom. If you know DHCP, you know this: an envelope is a lease on watts instead of an IP address — request it, hold it, renew it. An asset without a valid envelope MUST fall back to the safe default (below).

Request: GET /v1/envelope (authenticated by cert key, §7.1; the asset is identified by its cert — never by a request parameter). Optionally POST /v1/envelope with an envelope.request payload for hinted asks: { "want_import_w": 450000, "want_export_w": 0, "want_duration_s": 900 }.

envelope.grant (payload; signed by the zone Envelope Server):

{
  "node_id": "bldg7.feeder12.coshocton.aep.pjm.grid",
  "valid_from": "2026-08-11T17:00:00.000Z",
  "valid_until": "2026-08-11T17:15:00.000Z",
  "import_max_w": 450000,
  "export_max_w": 200000,
  "ramp_max_w_s": 50000,
  "firmness": "flexible",
  "firm_floor_import_w": 0,
  "firm_floor_export_w": 0,
  "renewal_hint_s": 600,
  "positions": []
}
  • firm_floor_import_w / firm_floor_export_w — the portion of this grant guaranteed by cleared forward positions (§4.4). positions lists the contributing position record hashes (ph_… = record_hash of exchange.position). Zero/empty when no positions apply.
  • firmness on the grant is the firmness class of the grant: firm for the floored portion's window, otherwise the asset's class.

Lease semantics (normative):

  • G1 — Renewal. Assets SHOULD renew at renewal_hint_s; MUST renew before valid_until.
  • G2 — Expiry fallback. On expiry without renewal, the asset MUST fall back to: import_max_w = 10% of certified capability.import_max_w, export_max_w = 0except that a holder of an active firm position (§4.3) MAY continue to operate at min(firm position quantity, certified capability) in the position's direction for the remainder of the delivery window. Surviving control-plane outages is part of what a firm position purchases.
  • G3 — Shrink/growth. Outside firm floors, the server MAY shrink grants under congestion and SHOULD grow grants with telemetry-compliance history (trust). The shrink/growth function is implementation-defined; the floor behavior is not (§4.4).
  • G4 — Window alignment. A grant's validity window MUST NOT straddle a boundary of any position window that applies to the node (truncate valid_until to the next boundary) so that floors are constant over any single grant.

envelope.firm_breach (payload; signed by the envelope server; MUST be logged): emitted whenever an emergency state forces a grant below an active floor — see §4.4 E3.

{
  "node_id": "…",
  "position_hashes": ["ph_…"],
  "granted_import_w": 100000,
  "floor_import_w": 300000,
  "flag_hash": "rh_…",
  "window_from": "…", "window_until": "…"
}

3.3 P3 — Congestion signal

Streaming subscription: wss://…/v1/signals/{node_id} (DEV CONSOLE key; PUBLIC aggregate stream exists for the sandbox explorer, §7). Two payload kinds:

signal.tick (price tick; signed; log-exempt for latency, §6.2):

{
  "node_id": "feeder12.coshocton.aep.pjm.grid",
  "ts": "2026-08-11T17:00:05.000Z",
  "price": { "amount": 14250, "currency": "USD", "per": "MWh" },
  "congestion_bp": 7300,
  "flag": "none"
}

signal.flag (discrete event; signed; MUST be logged):

{
  "node_id": "…",
  "flag": "curtail",
  "target_w": 100000,
  "notice_s": 600,
  "duration_s": 14400,
  "contract_ref": "gi_c8c4…#ph_91ab…",
  "reason": "option_exercise"
}
  • flagcurtail | emergency | clear. clear ends a prior flag.
  • contract_ref is present iff the flag exercises a sold curtailment option (§4.5): instrument_id # position record hash. Flags without contract_ref are ordinary reliability curtailment addressed to flexible-class assets.
  • Contract: flexible assets MUST respond to curtail within their certified control_latency_ms; SHOULD implement price-responsive backoff (AIMD; the SDKs ship a default controller). Assets whose org sold the referenced option are held to the option's notice_s/duration_s terms and verified against telemetry (§4.5).

3.4 P4 — Firmness classes (QoS)

Class Semantics Curtailment obligation Typical price
firm Studied or forward-purchased; envelope floored per §4.4 none (except logged emergency, compensated §3.5 S3) highest
interruptible Curtailable with declared N-minute notice must honor curtail with notice_s ≥ its declared notice middle
flexible Best-effort must honor curtail within control_latency_ms lowest

firmness MUST appear on: every identity.cert, every envelope.grant, every exchange instrument (§4.2), and every settlement record (§3.5). A single asset can hold different firmness for import vs export only via positions (the cert class is its baseline).

3.5 P5 — Telemetry & settlement

telemetry.report (payload; signed by the asset's cert key; batched cadence 1 s–60 s):

{
  "node_id": "…",
  "interval_from": "2026-08-11T17:00:00.000Z",
  "interval_until": "2026-08-11T17:00:05.000Z",
  "avg_w": 412000,
  "interval_wh": 572
}

Sign convention: positive = import (consumption), negative = export. Telemetry records are the evidence base for settlement and option-performance verification; zones MUST retain them for the dispute window (config; sandbox default 90 days) and MUST log daily Merkle roots of telemetry batches (not each report) per §6.2.

Two-settlement system (mirrors PJM DA/RT; all records signed by the zone settlement engine, all logged):

  • S1 — Forward leg. Per position per day of delivery window: settlement.forward_accrual { "position_hash": "ph_…", "day": "2027-07-01", "qty_w": 50000000, "price": {"amount": 33300, "currency": "XGD", "per": "MW-day"}, "amount": {"amount": 1665000, "currency": "XGD"} }. amount = price × qty(MW), buyer pays seller (option premiums accrue seller-positive, §4.5).
  • S2 — Real-time leg. Per node per 5-minute interval: settlement.rt_interval { "node_id": "…", "interval_from": "…", "interval_until": "…", "metered_wh": 34333, "price": {"amount": 14250, "currency": "XGD", "per": "MWh"}, "amount": {"amount": -49, "currency": "XGD"}, "telemetry_root": "th_…" }. Net metered energy settles against the spot nodal price; deviations from forward-covered behavior are inherently netted here because S1 pays for capacity (MW-day availability), not energy.
  • S3 — Performance leg. settlement.option_performance (§4.5: verified delivery, or collateral burn on non-delivery) and settlement.firm_breach_credit (compensation for §3.2 breaches: shortfall energy — the floored-but-curtailed portion integrated over the breach window — credited at the concurrent RT nodal price; punitive multipliers are zone policy, out of v0.1 scope).

Settlement records are downloadable as JSON from the ledger surface (DEV CONSOLE, §7) and verifiable with grip verify (§6.4). Sandbox balances are play-money XGD (§1.4).


4. Capacity Exchange — the forward layer

Mental model (normative for docs): the envelope server is spot/on-demand; the Capacity Exchange is reserved instances. Spot disciplines real-time behavior; forwards finance steel in the ground.

4.1 Products

All products are keyed to {node_id, quantity, delivery window, firmness}:

Product token Name Buyer gets Seller gets
fwd-import Firm capacity forward Guaranteed import envelope floor over the window (§4.4) Capacity price accrual $/MW-day (S1)
fwd-export Export capacity forward Guaranteed export envelope floor (parent zone buys firm supply) Capacity price accrual
opt-curtail Curtailment option Right to curtail seller to target_w with notice_s, bounded events/duration Premium accrual $/MW-day; obligation to perform (§4.5)

4.2 Instruments and listings

Instruments are standardized listings created by the zone exchange operator. exchange.listing (signed, logged):

{
  "product": "fwd-import",
  "node_id": "campus3.aep.pjm.grid",
  "window_from": "2027-07-01T04:00:00.000Z",
  "window_until": "2027-10-01T04:00:00.000Z",
  "firmness": "firm",
  "quote_unit": "MW-day",
  "currency": "USD",
  "lot_w": 100000,
  "tick": { "amount": 100, "currency": "USD", "per": "MW-day" },
  "terms": null
}
  • instrument_id = gi_ + first 16 bytes (hex) of SHA-256("GRIP-INSTR-V0" 0x0A GRIP-CJ(listing minus lot/tick/terms defaults…)) — precisely: the derivation preimage is the listing payload restricted to {product, node_id, window_from, window_until, firmness, quote_unit, currency} (Appendix A, V3). Same economic contract ⇒ same id, deterministically, across zones and languages.
  • Standard windows: calendar months, quarters, and PJM-style delivery years (June 1–May 31), zone-configurable.
  • For opt-curtail, terms is REQUIRED: { "target_w": …, "notice_s": …, "max_events": 40, "max_event_duration_s": 14400, "season_window": {…} } and joins the id-derivation preimage.

4.3 Order book messages

Continuous limit order book, price-time priority, per instrument. Forward market: matching latency target < 1 s; there are deliberately no HFT accommodations. Order entry is SP DASHBOARD tier (§7); aggregate market data is PUBLIC.

exchange.order (signed by the submitting org's key — an org key is an asset-class-less cert bound to org_id):

{
  "order_id": "o_…uuid…",
  "instrument_id": "gi_c8c4f82d4d63638bc9e7b08df388f201",
  "side": "buy",
  "qty_w": 50000000,
  "limit": { "amount": 33300, "currency": "USD", "per": "MW-day" },
  "tif": "gtc",
  "account": "org_7ac2…"
}

tifgtc | ioc | day. exchange.cancel: { "order_id": "…" }. The exchange responds with exchange.ack { "order_id", "status": "accepted | rejected | cancelled", "reason": null } (signed by the exchange).

Admission checks (normative): sell orders for fwd-* and opt-curtail MUST NOT exceed the seller's certified capability at the node net of already-sold positions for overlapping windows; buy orders for fwd-import/fwd-export at a node MUST NOT exceed the buyer's certified capability there (a floor above physical capability is meaningless — see §4.4 F2); collateral per §4.6 must be posted at order time.

exchange.trade (signed by exchange; MUST be logged): { "trade_id", "instrument_id", "price": {…}, "qty_w", "buyer_account", "seller_account", "buy_order_id", "sell_order_id" }. Counterparty fields are private to the parties and the zone; the logged leaf is the record hash, and the PUBLIC market feed carries only {instrument_id, price, qty_w, ts} (§4.7).

exchange.position (signed by exchange; MUST be logged): the netted holding that the rest of the system consumes.

{
  "position_id": "p_…uuid…",
  "instrument_id": "gi_…",
  "account": "org_7ac2…",
  "node_id": "campus3.aep.pjm.grid",
  "product": "fwd-import",
  "firmness": "firm",
  "qty_w": 50000000,
  "window_from": "2027-07-01T04:00:00.000Z",
  "window_until": "2027-10-01T04:00:00.000Z",
  "vwap": { "amount": 33300, "currency": "USD", "per": "MW-day" },
  "venue": "book",
  "terms": null
}
  • venuebook (cleared on the GRIP order book) | otc (imported bilateral deal, §5). Envelope floors and settlement treat both identically — that is the convergence contract.

  • Positions are fungible per instrument and resellable: selling on the book reduces qty_w; exchange.position_transfer { "position_id", "to_account", "qty_w" } (signed by transferor org + countersigned by exchange, logged) supports off-book assignment.

    Countersignature convention (implementation note; added in v0.1.x). The wire wrapper (§1.5) carries exactly one att block per record, so "signed by transferor org + countersigned by exchange" is realized as two separate attestations rather than a multi-sig att: the transferor's signed exchange.position_transfer record is the authorization (logged under its own kind, per the MUST-log list, §6.2); the exchange's countersignature takes the form of the resulting signed exchange.position records it issues for both the transferor's (reduced) and transferee's (increased) holdings — the same mechanism §4.3 already uses to attest to trade-driven position changes. A verifier confirms a transfer happened by checking that both are logged: the org's signed request, and the exchange.position deltas it implies.

4.4 Cleared forward position → envelope floor (normative coupling)

This is the mechanism by which firmness stops being a tariff artifact and becomes a purchasable, tradable instrument. Rules, all MUST:

  • F1 — Floor definition. For node N, direction D ∈ {import, export}, at time t: floor_D(N, t) = min( Σ qty_w of active positions, capability_D(N) ) where active positions = all exchange.position records with node_id = N, product direction D, firmness = firm, whose account owns or is the org of N's cert, and whose [window_from, window_until) contains t. (The min with capability is belt-and-braces; §4.3 admission makes the sum ≤ capability by construction.)
  • F2 — Grant floor. Every envelope.grant for N whose validity window lies within t's position windows MUST satisfy <D>_max_w ≥ floor_D and MUST report the floor in firm_floor_<D>_w and the contributing position hashes in positions. Grant windows are truncated at position boundaries (§3.2 G4) so F2 is checkable per grant.
  • F3 — Emergency exception. A zone in declared emergency state (a logged signal.flag with flag: "emergency" covering N) MAY grant below the floor. Each such grant MUST be accompanied by an envelope.firm_breach record (§3.2) referencing the emergency flag, and triggers S3 compensation (§3.5). An unlogged breach is a protocol violation detectable from the public log: the grant, the floor-constituting positions, and the (absent) emergency flag are all logged records.
  • F4 — Outage survival. Per §3.2 G2, an expired envelope degrades to the safe default except the firm-floored portion, which persists for the delivery window.
  • F5 — No double-floor. A position floors exactly one node (its node_id). Aggregation of child floors into a parent zone's advertised capacity is the aggregator's business (§6.5), not an automatic protocol effect.

4.5 Curtailment options — exercise and verification

  • Exercise. The option holder (zone operator) exercises by emitting a signal.flag with contract_ref (§3.3). Exercise MUST honor the instrument terms: notice ≥ notice_s, event duration ≤ max_event_duration_s, events per season ≤ max_events, within season_window. Out-of-terms exercises are invalid; the seller is not obligated and the flag is disregarded for S3 (still logged).
  • Performance. The seller performs iff its metered net consumption at the node (from signed telemetry.reports) is ≤ target_w from notice_s after the flag through the event end, evaluated per 5-minute interval with one grace interval.
  • Settlement (S3). settlement.option_performance { "position_hash", "flag_hash", "performed": true, "measured_max_w": …, "penalty": {"amount": 0, "currency": "XGD"} }. Non-performance burns the seller's posted collateral proportionally to the shortfall (formula: zone config; sandbox default penalty = 2 × premium accrued to date on the shorted quantity) and MUST decrement the seller's compliance score (§4.6).

4.6 Collateral and compliance

Sellers post collateral (play-money in sandbox) scaled by position size and compliance history: required ≥ κ(compliance_score) × notional_days × price. κ and the score function are zone policy (implementation-defined), but: the compliance score MUST be derived only from attested records (telemetry, option performance, firm breaches), MUST be visible to the scored org (DEV CONSOLE), and MUST feed exchange admission. The compliance score is the exchange's credit rating.

4.7 Market data

PUBLIC (no auth, §7): per instrument — last price, daily VWAP, volume, open interest, and the clearing-price history; capacity-market context feed (BRA-seeded prices per delivery year). PRIVATE to counterparties + zone: orders, trades with identities, positions, collateral. This split is normative: market transparency drives adoption; order books are counterparty-private.

Worked figure (for any doc citing campus economics): a 500 MW campus proving 60% curtailability = 300 MW nameplate flexibility; at DR ELCC 92% ⇒ ~276 MW UCAP; at the 2027/28 BRA clearing price ($121,705/MW-year, the FERC cap ≈ $333/MW-day) ⇒ ~$33.6M/yr gross.

4.8 RPM-equivalent roll-up (SP DASHBOARD)

The dashboard's roll-up (campus-held options → net campus flexibility → RPM-equivalent UCAP position → dollars) is an analytics view over exchange.position + compliance records; it introduces no new protocol surface. The exportable position report for capacity-market analysts is a signed document, settlement.position_report (payload; signed by the zone settlement engine; MUST be logged per the §6.2 MUST-log list). v0.1 reserved the kind token only; this schema was added additively per §10.1 (new record kind, no existing schema changed):

{
  "account": "org_7ac2…",
  "as_of": "2026-08-11T17:00:00.000Z",
  "positions": [
    {
      "position_hash": "ph_91ab…",
      "instrument_id": "gi_c8c4f82d4d63638bc9e7b08df388f201",
      "node_id": "campus3.aep.pjm.grid",
      "product": "fwd-import",
      "firmness": "firm",
      "qty_w": 50000000,
      "window_from": "2027-07-01T04:00:00.000Z",
      "window_until": "2027-10-01T04:00:00.000Z",
      "vwap": { "amount": 33300, "currency": "USD", "per": "MW-day" },
      "venue": "book"
    }
  ],
  "compliance_score_bp": 9500,
  "collateral_posted": { "amount": 0, "currency": "XGD" },
  "capacity_value": { "amount": 559107000, "currency": "USD" }
}
  • positions is the account's netted exchange.position holdings (§4.3) as of as_of, each entry restricted to the fields the roll-up needs — no order/trade identities (§4.7's PRIVATE split still applies to the underlying records; this report is itself PRIVATE to the account and DEV CONSOLE, §7). venue carries no differential treatment: book and otc (§5) entries have identical shape, per the convergence contract.
  • compliance_score_bp is the account's exchange compliance score (§4.6) at as_of, in basis points (§1.3 ratio convention).
  • collateral_posted is the account's total posted collateral (§4.6) across positions, in the position currency's Money form (§1.4).
  • capacity_value (additive, this revision) is the account's ELCC-adjusted annual dollar capacity-value rollup (§4.7 worked figure) over its fwd-import (firm capacity forward, §4.2) positions with positive qty_wopt-curtail positions and short positions do not contribute. This is definitionally the ELCC-adjusted figure; the gross, un-derated qty_w x vwap product MUST NOT be exposed as a field on this or any other public-facing record.
  • One report per account per as_of snapshot; zones MAY serve a live-computed report (no separate storage requirement) or a periodically-anchored one — implementation-defined, same latitude as §4.6's compliance-score function.

5. Relationship to Standard Power's Capacity Exchange

Standard Power operates the Capacity Exchange, a bilateral (OTC) marketplace where master-agreement, paper-contract capacity deals are negotiated and executed off the GRIP order book. The protocol contract between the two is deliberately small:

  • Executed bilateral deals import as positions. An executed OTC deal MUST be importable as an exchange.position with venue: "otc" (§4.3), signed by the zone exchange and logged (§6.2), keyed to the same {node_id, qty_w, window, firmness} shape as a book-cleared position.
  • Downstream treatment is identical. From the moment of import, the envelope server (§4.4), settlement engine (§3.5), transparency log (§6), and position reporting (§4.8) make no distinction between venue: "book" and venue: "otc". Firmness purchased on paper and firmness cleared on the book are the same instrument downstream.
  • Real-money settlement stays off-protocol in v0.1. GRIP v0.1 settlement is play-money (XGD, §1.4); real-money capacity deals settle on the Capacity Exchange's own contract rails. A future protocol version may define a real-money settlement adapter behind the same settlement surface (§10.3).

6. Federation model — registrar + transparency log

The federation model borrows deliberately from the internet's own infrastructure: zones delegate like DNS, the log works like Certificate Transparency, and cross-zone coordination is reserved for a BGP-like layer. No blockchain: physical authority is local (the wire's operator is authoritative for its constraint); curtailment fan-out is sub-second and consensus is orders of magnitude off; DNS/BGP/TLS prove decentralization without global consensus. GRIP is decentralized like the internet, not like Bitcoin.

v0.1 specifies the federation interfaces; multi-zone implementations arrive in a later release.

6.1 Zones and delegation

Each zone is operated by its natural authority (RTO at root, utility per distribution zone, campus operator per site). A zone runs a Zone Registrar (issues identity.certs, §3.1) and an Envelope Server (§3.2), and typically a signal service, exchange, settlement engine, and transparency log.

federation.delegation (signed by the PARENT zone's key; logged in both parent and child logs):

{
  "zone": "aep.pjm.grid",
  "parent": "pjm.grid",
  "zone_key": "base64url(Ed25519 pub)",
  "services": {
    "registrar": "https://registrar.aep.example",
    "envelope": "https://env.aep.example",
    "signals": "wss://signals.aep.example",
    "exchange": "https://exchange.aep.example",
    "log": "https://log.aep.example"
  },
  "not_before": "…", "not_after": "…"
}

Root zone keys are distributed out of band (pinned in SDKs, like root DNS/CA sets). In the sandbox, Standard Power operates all zones; the interfaces are what let real utilities run their own later without re-platforming.

6.2 Transparency log

Per zone: a public, append-only Merkle tree (RFC 6962 conventions: leaf hash = SHA-256(0x00 ‖ leaf), node = SHA-256(0x01 ‖ l ‖ r)). Leaves are record_hash values (§2.3).

MUST-log kinds: identity.cert, identity.revoke, federation.delegation, envelope.grant, envelope.firm_breach, signal.flag, exchange.listing, exchange.trade, exchange.position, exchange.position_transfer, settlement.*. Log-exempt (latency/volume): signal.tick and telemetry.report — for these, zones MUST log periodic batch Merkle roots (ticks: per hour; telemetry: per day) so bulk data remains commit-verifiable.

Checkpoints. The zone periodically signs log.checkpoint { "log_id", "tree_size", "root_hash", "ts" } (≤ every 5 minutes under write load). Cross-signing (no consensus, mutual accountability): each zone SHOULD countersign its parent's and children's checkpoints — log.checkpoint_cosign { "checkpoint_hash", "log_id", "by_zone" } — making split-view attacks detectable. A zone cannot rewrite the history of envelopes it granted, flags it sent, or trades it cleared; disputes resolve on log evidence.

Read interface (PUBLIC, no auth — anonymous verification is the point):

GET /v1/log/checkpoint                     → latest signed checkpoint (+ recent cosigns)
GET /v1/log/proof?leaf_hash=…&tree_size=…  → inclusion proof (audit path)
GET /v1/log/consistency?from=…&to=…        → consistency proof between checkpoints
GET /v1/log/entries?start=…&end=…          → leaf range (hashes; payload retrieval is tiered per §7)

Only the zone's own services append (there is no public write).

6.3 Verification algorithm (what grip verify <record> does)

Given a GripRecord: (1) recanonicalize and check att.sig against att.kid; (2) resolve kididentity.cert or federation.delegation, walk the delegation chain to a pinned root, checking validity windows and revocations; (3) compute record_hash, fetch an inclusion proof from the issuing zone's log, verify up to a signed checkpoint; (4) optionally check checkpoint cosignatures. Output: VALID (signature + chain + logged) / SIGNED-NOT-LOGGED / INVALID. The CLI and the web verifier are PUBLIC surfaces (§7).

6.4 Settlement anchoring (stub)

Merkle roots of settlement and exchange-clearing batches publish to the zone log. An anchor stub interface (POST /internal/anchor — reachable only by the zone's own services, a no-op in v0.1) reserves the option of committing checkpoint roots to an external public notary in the future — never in the control loop, never per-transaction. v1: no chain.

6.5 Cross-zone coordination (reserved)

Zone operators advertise aggregate flexibility and capacity positions upward with federation.advertisement { "zone", "window_from", "window_until", "firm_import_w", "firm_export_w", "flex_w", "position_roots": […] } (signed, logged). v0.1 fixes this schema as reserved: emitted by aggregators, consumed by parents, no protocol-mandated behavior yet (a later release defines admission/netting semantics).


7. Access tiers — surface map (normative)

Three tiers. Rule of thumb: protocol and market aggregates are public; positions and operations are private; anything with the operator's name on the P&L is dashboard-tier.

Hard rule (normative): NO PRIVILEGED BACKDOORS. Every capability reachable from the SP dashboard MUST exist as a documented, authenticated API that any zone operator or participant at the same tier could use. The dashboard is a client of the protocol, never a side door into it. Dev-console credentials MUST NOT reach SP-dashboard surfaces.

Surface / message family Tier Auth
This spec, docs, landing page PUBLIC none
Sandbox explorer, aggregate signal stream, event calendar, leaderboards PUBLIC none
Transparency log read + verifier (web, grip verify) — §6.2, §6.3 PUBLIC none
Exchange aggregate market data (§4.7): prices, volume, open interest PUBLIC none
Status page PUBLIC none
POST /v1/register, cert lifecycle (§3.1) DEV CONSOLE API key (self-serve OIDC)
GET/POST /v1/envelope (§3.2) DEV CONSOLE cert key
wss /v1/signals/{node_id} per-node (§3.3) DEV CONSOLE cert key
telemetry.report ingest (§3.5) DEV CONSOLE cert key
Device registry, compliance score, play-money ledger + settlement records DEV CONSOLE self-serve login
Spot flexibility offers (device.offer) DEV CONSOLE API key
Exchange order entry, cancels, own orders/trades/positions (§4.3), depth SP DASHBOARD¹ SSO (SP + invited partners)
OTC position import (venue: "otc", §5) SP DASHBOARD SSO
Campus ops: site roll-up, curtailment planning, RPM-equivalent report (§4.8) SP DASHBOARD SSO
Tenant allocation + tenant portal (restricted dashboard view) SP DASHBOARD SSO / tenant invite
Partner/utility CRM intake SP DASHBOARD SSO
Log append, anchor stub, zone key ops zone services only service identity

¹ Tier assignment of order entry is a deployment policy of the sandbox/SP zones, not a protocol property: the messages themselves (§4.3) are standard authenticated surfaces any zone exchange could expose — this is what keeps the no-backdoor rule and the neutrality claim consistent.

DEV CONSOLE and SP DASHBOARD are separate apps on separate origins with separate auth stacks (self-serve OIDC vs corporate SSO). The dashboard consumes the same public protocol APIs plus the authenticated exchange/ops surfaces above.

7.1 cert key auth — the GRIP-Auth header (normative)

Surfaces tagged cert key in the table above (GET/POST /v1/envelope, wss /v1/signals/{node_id}, telemetry.report ingest) authenticate a request as coming from the holder of a specific asset's Ed25519 private key. Where the message itself is a signed record (telemetry.report), the record's own att block (§1.5, §2.3) is sufficient proof and this section does not apply. Where the request has no signable body — a bare GET, or a WebSocket upgrade — the caller instead sends a signed-request header:

GRIP-Auth: kid=<kid> ts=<ts> nonce=<nonce> sig=<sig>
  • kid — the requester's identity.cert key id (§1.5): resolves to the cert the verifier checks against.
  • ts — request time, RFC 3339 UTC millisecond precision (§1.2). MUST be within ±60s of the verifier's clock; this is both the freshness window and the clock-skew budget.
  • nonce — 8 random bytes, lowercase hex (16 chars). MUST NOT repeat for the same kid within the freshness window.
  • sig — base64url(Ed25519-sign(asset's private key, sig_preimage)).

sig_preimage (domain-separated, same convention as §2.3's record preimage):

sig_preimage = "GRIP-SIG-V0" 0x0A method 0x0A path 0x0A ts 0x0A nonce

method is the HTTP method, uppercase ASCII (GET, POST, …). path is the request-target path only — no scheme, host, query string, or fragment. Binding method and path into the preimage is what stops a signature captured for one cert-key surface from being replayed against another (a GET /v1/envelope header cannot authenticate a different request).

Verification (normative, MUST, in order):

  1. The header MUST be present and match the kid=… ts=… nonce=… sig=… format above; otherwise reject.
  2. ts MUST parse as RFC 3339 UTC and lie within ±60s of the verifier's current time; otherwise reject (A1 — freshness).
  3. (kid, nonce) MUST NOT have been accepted before within the freshness window; otherwise reject (A2 — replay). Verifiers MAY bound the dedup set to two consecutive 60s buckets keyed on ts, since anything older is already rejected by A1.
  4. kid MUST resolve to a currently valid (not expired, not revoked) identity.cert at the zone registrar; otherwise reject (A3 — unknown/invalid cert).
  5. The verifier MUST recompute sig_preimage from the actual inbound request's method and path (never from client-supplied values) and verify sig against the resolved cert's public key; otherwise reject (A4 — bad signature).
  6. On success, the authenticated identity is the cert's payload.node_id (or payload.org_id for an org-key cert, §4.3). A handler that passes this auth check MUST derive the identity used for the rest of the request from the cert payload — it MUST NOT also honor an identity supplied via request body/query/path for the same purpose, or the possession-proof is moot.

Any rejection (A1–A4) MUST return the RFC 9457 unauthorized-signer problem (§8) with HTTP 401.

This reuses the GRIP-SIG-V0 domain-separation prefix and Ed25519 verification already defined for record signatures (§2.3) so there is exactly one signing/verification primitive in the protocol, applied to two transports (a record's att block, or this header) depending on whether the request has a body to sign. A conforming client library implements this section directly — it MUST NOT depend on any single zone's server-side code to interoperate.


8. Transport bindings and errors

  • HTTPS + WSS only; mTLS for asset↔zone service connections where the asset holds a cert (§3.1); TLS + bearer key for console-tier REST. HTTP APIs are JSON request/response carrying GripRecords where the payload is protocol-significant.
  • Versioning: URL prefix /v1/ tracks spec major line; grip field tracks record schema version ("0.1").
  • Errors: RFC 9457 problem+json, with type tokens under urn:grip:error:* (e.g. urn:grip:error:envelope-expired, urn:grip:error:floor-violation, urn:grip:error:insufficient-collateral, urn:grip:error:out-of-terms-exercise, urn:grip:error:unauthorized-signer — a record's att.sig failed verification against its claimed signer, or the signer is not authorized for the account/position/order acted on, §4.3).
  • Latency budgets (normative targets): envelope query p95 < 200 ms; signal fan-out p95 < 1 s; cert issuance < 5 s; exchange matching < 1 s; sandbox availability 99.5% — expiry fallback (§3.2 G2) makes outages degrade safely.

9. Branding rule (normative)

"GRIP" is a working name and MUST be rebrandable:

  • One config point. Each service/SDK/site reads brand values from a single configuration object brand = { display_name, root_domain, package_scope, cli_name, support_url, docs_url }. No brand string (display name, domain, package scope, CLI binary name) may be hardcoded anywhere else. Implementations SHOULD grep-gate for stray brand literals in CI.
  • Wire constants are not brand strings. The record field grip, the GRIP-SIG-V0 / GRIP-INSTR-V0 domain-separation prefixes, and urn:grip:error:* tokens are immutable protocol identifiers — changing them breaks every signature and is a major-version event. They survive a rebrand the way "http" survived its authors. Rebranding changes the §9 config object and packaging, never the wire.
  • The root DNS label (grid) and sandbox currency (XGD) are deployment config (§1.1, §1.4), already brand-neutral.

10. Versioning and change control

10.1 Stability semantics

v0.1 is stable: every schema in §1–§7 may change only additively (new optional fields, new record kinds, new error tokens) until v0.2. Breaking changes require a new grip version token and a spec revision. Implementations MUST cite section numbers from this document (single source of truth).

10.2 Record-kind registry (v0.1 complete list)

identity.cert, identity.revoke, envelope.request, envelope.grant, envelope.firm_breach, signal.tick, signal.flag, telemetry.report, settlement.forward_accrual, settlement.rt_interval, settlement.option_performance, settlement.firm_breach_credit, settlement.position_report (reserved), exchange.listing, exchange.order, exchange.cancel, exchange.ack, exchange.trade, exchange.position, exchange.position_transfer, federation.delegation, federation.advertisement (reserved), log.checkpoint, log.checkpoint_cosign.

10.3 Out of scope for v0.1 (recorded so nobody "helpfully" adds them)

Real-money settlement (Capacity Exchange contract rails only, §5); X.509 carrier certs (§3.1); power-flow-accurate simulation (the sandbox is physics-lite by design); chain anchoring beyond the stub (§6.4); cross-zone advertisement semantics (§6.5); HFT market microstructure.


Appendix A — Canonical JSON test vectors (normative)

Conformance suites in every SDK language MUST reproduce these byte-for-byte. (Generated with the Python reference expression in §2.1.)

V1 — envelope.grant payload canonicalization. Input (any key order, any whitespace) → canonical form:

{"export_max_w":200000,"firm_floor_import_w":0,"firmness":"flexible","import_max_w":450000,"node_id":"bldg7.feeder12.coshocton.aep.pjm.grid","ramp_max_w_s":50000,"renewal_hint_s":600,"valid_from":"2026-08-11T17:00:00.000Z","valid_until":"2026-08-11T17:15:00.000Z"}

SHA-256 = 4c91e29549789aefe20cab505d6aa4e02ec718f44714f63ee95f5b4435fe5a38

V2 — signature preimage hash. For the GripRecord wrapping V1 with grip="0.1", kind="envelope.grant", id="0d9f1c2a-7b1e-4b9a-9c3d-2f4e5a6b7c8d", zone="aep.pjm.grid", ts="2026-08-11T17:00:00.000Z" (no att): SHA-256 of "GRIP-SIG-V0" 0x0A "envelope.grant" 0x0A GRIP-CJ(record) = 9c92510108135361cece146726a05362dd37537c926c23ac1dde243a97f9e33f

V3 — instrument id derivation. Listing preimage:

{"currency":"USD","firmness":"firm","node_id":"campus3.aep.pjm.grid","product":"fwd-import","quote_unit":"MW-day","window_from":"2027-07-01T04:00:00.000Z","window_until":"2027-10-01T04:00:00.000Z"}

instrument_id = gi_c8c4f82d4d63638bc9e7b08df388f201

V4 — edge cases (key ordering incl. non-ASCII, minus-zero, null, bool, array). Input {"z": -0, "a": 10, "é": "café", "n": null, "b": true, "arr": [1,2,3]} → canonical:

{"a":10,"arr":[1,2,3],"b":true,"n":null,"z":0,"é":"café"}

SHA-256 = 0838d453f8c71f495a223f2c09e5d6a70b602c7c88efb429a14f4966941873f4 (Note: -0 canonicalizes to 0 per RFC 8785; é (U+00E9) sorts after all ASCII keys; producers should have omitted n per §2.2 — the vector exercises the encoder, not the producer rule.)

Ed25519 signature vectors (fixed test keypair) ship with the SDK conformance suites; the hash preimages above pin the cross-language contract.


GRIP Protocol Specification v0.1 — © Standard Power.