openvibers/openvibe.contracts Commits

Release the billing.usage.record capability in OpenVibe.Contracts so… (#5)

OP OpenVibers committed 2 Oct 2026 · 97c4f72a74 · 40 files, +808 −61

local checks ✓ 3/3 · CI ✓ 3/3 · review SHIP by DeepSeek harness (QUALITY=high) · 2 fix rounds

https://github.com/OpenVibers/OpenVibe.Contracts/pull/5

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 47981d92..ef2beb3a 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -4,6 +4,18 @@ All notable changes to `openvibe-contracts`. Releases are git tags (`vX.Y.Z`) th
from `https://codeload.github.com/OpenVibers/OpenVibe.Contracts/tar.gz/refs/tags/<tag>`. Before v0.30.0,
the notes were in the tag and commit messages (`git tag -n1`).
++## 0.85.0 — 2026-10-02
++
++**Billing records usage readings** (plan T5 lane F): the capability `billing.usage.record` (active, internal,
++`billing:usage`, resource constraint `none`, quota class `ledger-write`), released from OpenVibe.Billing's proposal and
++listed by `manifests/services/billing.json`. `POST /api/v1/usage` takes one `platform.usage-sample@1` reading (its
++`inputSchema`) and stores it without moving money; the reading's `idempotency_key` deduplicates retries (same reading:
++200 with the stored row; a different one under the same key: 409 `billing.usage_key_reused`). Listing all readings
++(`GET /api/v1/usage`) stays under `billing.ledger.admin`. New `billing.usage-record-result@1` (its `outputSchema`):
++`{ record }` with the stored row's `id`, `idempotency_key`, `project`, `subject`, `service`, `at`, `received_at`,
++`principal` and the `reading` itself; fixtures `recorded` (valid) and `reading-without-idempotency-key` (invalid).
++Network grants for the usage producers follow this release. Additive.
++
## 0.84.0 — 2026-10-02
Add nine public `platform.*@1` schemas: usage and telemetry samples; node capabilities and
ddiff --git a/contracts/billing/admin-result.v1.json b/contracts/billing/admin-result.v1.json
iindex 88f6f745..ee175985 100644
---- a/contracts/billing/admin-result.v1.json
++++ b/contracts/billing/admin-result.v1.json
@@@ -2,7 +2,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/billing/admin-result.v1.json",
"title": "BillingAdminResult",
-- "description": "billing.admin-result@1: answers of billing.ledger.admin on OpenVibe.Billing. GET|POST /api/v1/admin/freeze → { frozen, reason, frozen_at, frozen_by } (POST adds processed_after_unfreeze); GET /api/v1/admin/reconcile → the reconciliation report { id, ok, trigger, started_at, finished_at, checks, warnings, totals }; POST /api/v1/admin/adjustments and POST /api/v1/purchases/settle → { transaction } (settle adds duplicate_receipt); GET /api/v1/admin/provider-events → { events }; POST …/provider-events/:id/reprocess → { event }; POST /api/v1/admin/sweep → { renewed, expired, canceled, skipped }; GET /api/v1/admin/import-holds → { holds }.",
++ "description": "billing.admin-result@1: answers of billing.ledger.admin on OpenVibe.Billing. GET|POST /api/v1/admin/freeze → { frozen, reason, frozen_at, frozen_by } (POST adds processed_after_unfreeze); GET /api/v1/admin/reconcile → the reconciliation report { id, ok, trigger, started_at, finished_at, checks, warnings, totals }; POST /api/v1/admin/adjustments and POST /api/v1/purchases/settle → { transaction } (settle adds duplicate_receipt); GET /api/v1/admin/provider-events → { events }; POST …/provider-events/:id/reprocess → { event }; POST /api/v1/admin/sweep → { renewed, expired, canceled, skipped }; GET /api/v1/admin/import-holds → { holds }; GET /api/v1/usage → { records, next_cursor }.",
"anyOf": [
{
"type": "object",
@@@ -210,6 +210,69 @@
}
}
}
++ },
++ {
++ "type": "object",
++ "required": [
++ "records",
++ "next_cursor"
++ ],
++ "properties": {
++ "records": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "id",
++ "idempotency_key",
++ "service",
++ "at",
++ "received_at",
++ "principal",
++ "reading"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "idempotency_key": {
++ "type": "string",
++ "description": "The reading's own idempotency_key"
++ },
++ "project": {
++ "type": ["string", "null"]
++ },
++ "subject": {
++ "type": ["string", "null"]
++ },
++ "service": {
++ "type": "string"
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "received_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "principal": {
++ "type": "string",
++ "description": "The service principal that recorded the reading"
++ },
++ "reading": {
++ "$ref": "../platform/usage-sample.v1.json"
++ }
++ }
++ }
++ },
++ "next_cursor": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
}
]
}
ddiff --git a/contracts/billing/usage-record-result.v1.json b/contracts/billing/usage-record-result.v1.json
nnew file mode 100644
iindex 00000000..76317305
---- /dev/null
++++ b/contracts/billing/usage-record-result.v1.json
@@@ -0,0 +1,63 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/billing/usage-record-result.v1.json",
++ "title": "BillingUsageRecordResult",
++ "description": "billing.usage-record-result@1: answer of billing.usage.record on OpenVibe.Billing. POST /api/v1/usage → { record } (201 when stored; 200 with `Idempotent-Replayed: true` when the same reading was already stored under its idempotency_key). Storing a reading charges nothing.",
++ "type": "object",
++ "required": [
++ "record"
++ ],
++ "properties": {
++ "record": {
++ "type": "object",
++ "required": [
++ "id",
++ "idempotency_key",
++ "service",
++ "at",
++ "received_at",
++ "principal",
++ "reading"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "idempotency_key": {
++ "type": "string",
++ "description": "The reading's own idempotency_key"
++ },
++ "project": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "subject": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "service": {
++ "type": "string"
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "received_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "principal": {
++ "type": "string",
++ "description": "The service principal that recorded the reading"
++ },
++ "reading": {
++ "$ref": "../platform/usage-sample.v1.json"
++ }
++ }
++ }
++ }
++}
ddiff --git a/contracts/catalog.json b/contracts/catalog.json
iindex 9857f794..de9dffad 100644
---- a/contracts/catalog.json
++++ b/contracts/catalog.json
@@@ -2229,6 +2229,15 @@
"compatibility": "backward",
"status": "active"
},
++ {
++ "id": "billing.usage-record-result",
++ "version": "1.0.0",
++ "owner": "billing",
++ "schema": "billing/usage-record-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
{
"id": "deals.watch.matched",
"version": "1.0.0",
ddiff --git a/fixtures/billing.admin-result/valid/usage-page.json b/fixtures/billing.admin-result/valid/usage-page.json
nnew file mode 100644
iindex 00000000..fac7f28f
---- /dev/null
++++ b/fixtures/billing.admin-result/valid/usage-page.json
@@@ -0,0 +1,29 @@
++{
++ "records": [
++ {
++ "id": "1",
++ "idempotency_key": "media:delivery:2",
++ "project": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "subject": "user:usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "service": "media",
++ "at": "2026-09-30T10:00:00.000Z",
++ "received_at": "2026-09-30T10:00:01.000Z",
++ "principal": "media",
++ "reading": {
++ "id": "use-2",
++ "idempotency_key": "media:delivery:2",
++ "service": "media",
++ "project": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "subject": "user:usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "operation": "deliver",
++ "quantity": 1.5,
++ "unit": "GiB",
++ "at": "2026-09-30T10:00:00Z",
++ "source": "media.egress",
++ "free_allowance_used": 1,
++ "vibes_charged": 4
++ }
++ }
++ ],
++ "next_cursor": null
++}
ddiff --git a/fixtures/billing.usage-record-result/invalid/reading-without-idempotency-key.json b/fixtures/billing.usage-record-result/invalid/reading-without-idempotency-key.json
nnew file mode 100644
iindex 00000000..9fc53db3
---- /dev/null
++++ b/fixtures/billing.usage-record-result/invalid/reading-without-idempotency-key.json
@@@ -0,0 +1 @@
++{ "record": { "id": "1", "idempotency_key": "media:delivery:2", "project": null, "subject": null, "service": "media", "at": "2026-09-30T10:00:00.000Z", "received_at": "2026-09-30T10:00:01.000Z", "principal": "media", "reading": { "id": "use-2", "service": "media", "operation": "deliver", "quantity": 1.5, "unit": "GiB", "at": "2026-09-30T10:00:00Z", "source": "media.egress" } } }
ddiff --git a/fixtures/billing.usage-record-result/valid/recorded.json b/fixtures/billing.usage-record-result/valid/recorded.json
nnew file mode 100644
iindex 00000000..2b45d786
---- /dev/null
++++ b/fixtures/billing.usage-record-result/valid/recorded.json
@@@ -0,0 +1 @@
++{ "record": { "id": "1", "idempotency_key": "media:delivery:2", "project": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ", "subject": "user:usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "service": "media", "at": "2026-09-30T10:00:00.000Z", "received_at": "2026-09-30T10:00:01.000Z", "principal": "media", "reading": { "id": "use-2", "idempotency_key": "media:delivery:2", "service": "media", "project": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ", "subject": "user:usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "operation": "deliver", "quantity": 1.5, "unit": "GiB", "at": "2026-09-30T10:00:00Z", "source": "media.egress", "free_allowance_used": 1, "vibes_charged": 4 } } }
ddiff --git a/generated/json-schema/bundle.json b/generated/json-schema/bundle.json
iindex 04a37b95..4bc3ce56 100644
---- a/generated/json-schema/bundle.json
++++ b/generated/json-schema/bundle.json
@@@ -22102,7 +22102,7 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/billing/admin-result.v1.json",
"title": "BillingAdminResult",
-- "description": "billing.admin-result@1: answers of billing.ledger.admin on OpenVibe.Billing. GET|POST /api/v1/admin/freeze → { frozen, reason, frozen_at, frozen_by } (POST adds processed_after_unfreeze); GET /api/v1/admin/reconcile → the reconciliation report { id, ok, trigger, started_at, finished_at, checks, warnings, totals }; POST /api/v1/admin/adjustments and POST /api/v1/purchases/settle → { transaction } (settle adds duplicate_receipt); GET /api/v1/admin/provider-events → { events }; POST …/provider-events/:id/reprocess → { event }; POST /api/v1/admin/sweep → { renewed, expired, canceled, skipped }; GET /api/v1/admin/import-holds → { holds }.",
++ "description": "billing.admin-result@1: answers of billing.ledger.admin on OpenVibe.Billing. GET|POST /api/v1/admin/freeze → { frozen, reason, frozen_at, frozen_by } (POST adds processed_after_unfreeze); GET /api/v1/admin/reconcile → the reconciliation report { id, ok, trigger, started_at, finished_at, checks, warnings, totals }; POST /api/v1/admin/adjustments and POST /api/v1/purchases/settle → { transaction } (settle adds duplicate_receipt); GET /api/v1/admin/provider-events → { events }; POST …/provider-events/:id/reprocess → { event }; POST /api/v1/admin/sweep → { renewed, expired, canceled, skipped }; GET /api/v1/admin/import-holds → { holds }; GET /api/v1/usage → { records, next_cursor }.",
"anyOf": [
{
"type": "object",
@@@ -22310,6 +22310,75 @@
}
}
}
++ },
++ {
++ "type": "object",
++ "required": [
++ "records",
++ "next_cursor"
++ ],
++ "properties": {
++ "records": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "id",
++ "idempotency_key",
++ "service",
++ "at",
++ "received_at",
++ "principal",
++ "reading"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "idempotency_key": {
++ "type": "string",
++ "description": "The reading's own idempotency_key"
++ },
++ "project": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "subject": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "service": {
++ "type": "string"
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "received_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "principal": {
++ "type": "string",
++ "description": "The service principal that recorded the reading"
++ },
++ "reading": {
++ "$ref": "../platform/usage-sample.v1.json"
++ }
++ }
++ }
++ },
++ "next_cursor": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
}
]
}
@@@ -23575,6 +23644,74 @@
}
}
},
++ "billing.usage-record-result": {
++ "version": "1.0.0",
++ "owner": "billing",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/billing/usage-record-result.v1.json",
++ "title": "BillingUsageRecordResult",
++ "description": "billing.usage-record-result@1: answer of billing.usage.record on OpenVibe.Billing. POST /api/v1/usage → { record } (201 when stored; 200 with `Idempotent-Replayed: true` when the same reading was already stored under its idempotency_key). Storing a reading charges nothing.",
++ "type": "object",
++ "required": [
++ "record"
++ ],
++ "properties": {
++ "record": {
++ "type": "object",
++ "required": [
++ "id",
++ "idempotency_key",
++ "service",
++ "at",
++ "received_at",
++ "principal",
++ "reading"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "idempotency_key": {
++ "type": "string",
++ "description": "The reading's own idempotency_key"
++ },
++ "project": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "subject": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "service": {
++ "type": "string"
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "received_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "principal": {
++ "type": "string",
++ "description": "The service principal that recorded the reading"
++ },
++ "reading": {
++ "$ref": "../platform/usage-sample.v1.json"
++ }
++ }
++ }
++ }
++ }
++ },
"deals.watch.matched": {
"version": "1.0.0",
"owner": "deals",
ddiff --git a/generated/openapi/ai.json b/generated/openapi/ai.json
iindex aa9e8416..fc79a9c4 100644
---- a/generated/openapi/ai.json
++++ b/generated/openapi/ai.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.AI API",
-- "version": "0.84.0",
-- "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.84.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.85.0",
++ "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.85.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/billing.json b/generated/openapi/billing.json
iindex 8afd2432..9f751678 100644
---- a/generated/openapi/billing.json
++++ b/generated/openapi/billing.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Billing API",
-- "version": "0.84.0",
-- "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.84.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.85.0",
++ "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.85.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
@@@ -1622,6 +1622,109 @@
}
}
}
++ },
++ "/api/v1/usage": {
++ "get": {
++ "operationId": "get_api_v1_usage",
++ "summary": "Operate the ledger: economy freeze switch (writes refused, reads served, webhooks queued), reconciliation runs, balanced manual adjustments with a reason, settling a verified provider receipt, provide",
++ "description": "**billing.ledger.admin** (internal): Operate the ledger: economy freeze switch (writes refused, reads served, webhooks queued), reconciliation runs, balanced manual adjustments with a reason, settling a verified provider receipt, provider-event reprocessing, the renewal sweep and import holds.",
++ "tags": [
++ "billing.ledger"
++ ],
++ "x-openvibe-capabilities": [
++ "billing.ledger.admin"
++ ],
++ "x-openvibe-visibility": [
++ "internal"
++ ],
++ "x-openvibe-quota-class": "ledger-admin",
++ "security": [
++ {
++ "openvibeToken": [
++ "billing.ledger.admin"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/billing.admin-request.v1"
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/billing.admin-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ },
++ "post": {
++ "operationId": "post_api_v1_usage",
++ "summary": "Record a platform.usage-sample@1 reading without moving money.",
++ "description": "**billing.usage.record** (internal): Record a platform.usage-sample@1 reading without moving money. The reading's idempotency_key deduplicates retries. Listing all readings requires billing.ledger.admin.",
++ "tags": [
++ "billing.usage"
++ ],
++ "x-openvibe-capabilities": [
++ "billing.usage.record"
++ ],
++ "x-openvibe-visibility": [
++ "internal"
++ ],
++ "x-openvibe-quota-class": "ledger-write",
++ "security": [
++ {
++ "openvibeToken": [
++ "billing.usage.record"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/platform.usage-sample.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/billing.usage-record-result.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
}
},
"components": {
@@@ -1806,7 +1909,7 @@
},
"billing.admin-result.v1": {
"title": "BillingAdminResult",
-- "description": "billing.admin-result@1: answers of billing.ledger.admin on OpenVibe.Billing. GET|POST /api/v1/admin/freeze → { frozen, reason, frozen_at, frozen_by } (POST adds processed_after_unfreeze); GET /api/v1/admin/reconcile → the reconciliation report { id, ok, trigger, started_at, finished_at, checks, warnings, totals }; POST /api/v1/admin/adjustments and POST /api/v1/purchases/settle → { transaction } (settle adds duplicate_receipt); GET /api/v1/admin/provider-events → { events }; POST …/provider-events/:id/reprocess → { event }; POST /api/v1/admin/sweep → { renewed, expired, canceled, skipped }; GET /api/v1/admin/import-holds → { holds }.",
++ "description": "billing.admin-result@1: answers of billing.ledger.admin on OpenVibe.Billing. GET|POST /api/v1/admin/freeze → { frozen, reason, frozen_at, frozen_by } (POST adds processed_after_unfreeze); GET /api/v1/admin/reconcile → the reconciliation report { id, ok, trigger, started_at, finished_at, checks, warnings, totals }; POST /api/v1/admin/adjustments and POST /api/v1/purchases/settle → { transaction } (settle adds duplicate_receipt); GET /api/v1/admin/provider-events → { events }; POST …/provider-events/:id/reprocess → { event }; POST /api/v1/admin/sweep → { renewed, expired, canceled, skipped }; GET /api/v1/admin/import-holds → { holds }; GET /api/v1/usage → { records, next_cursor }.",
"anyOf": [
{
"type": "object",
@@@ -2014,6 +2117,75 @@
}
}
}
++ },
++ {
++ "type": "object",
++ "required": [
++ "records",
++ "next_cursor"
++ ],
++ "properties": {
++ "records": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "id",
++ "idempotency_key",
++ "service",
++ "at",
++ "received_at",
++ "principal",
++ "reading"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "idempotency_key": {
++ "type": "string",
++ "description": "The reading's own idempotency_key"
++ },
++ "project": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "subject": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "service": {
++ "type": "string"
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "received_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "principal": {
++ "type": "string",
++ "description": "The service principal that recorded the reading"
++ },
++ "reading": {
++ "$ref": "#/components/schemas/platform.usage-sample.v1"
++ }
++ }
++ }
++ },
++ "next_cursor": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
}
]
},
@@@ -3159,6 +3331,67 @@
}
}
},
++ "billing.usage-record-result.v1": {
++ "title": "BillingUsageRecordResult",
++ "description": "billing.usage-record-result@1: answer of billing.usage.record on OpenVibe.Billing. POST /api/v1/usage → { record } (201 when stored; 200 with `Idempotent-Replayed: true` when the same reading was already stored under its idempotency_key). Storing a reading charges nothing.",
++ "type": "object",
++ "required": [
++ "record"
++ ],
++ "properties": {
++ "record": {
++ "type": "object",
++ "required": [
++ "id",
++ "idempotency_key",
++ "service",
++ "at",
++ "received_at",
++ "principal",
++ "reading"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "idempotency_key": {
++ "type": "string",
++ "description": "The reading's own idempotency_key"
++ },
++ "project": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "subject": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "service": {
++ "type": "string"
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "received_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "principal": {
++ "type": "string",
++ "description": "The service principal that recorded the reading"
++ },
++ "reading": {
++ "$ref": "#/components/schemas/platform.usage-sample.v1"
++ }
++ }
++ }
++ }
++ },
"common.entity-ref.v1": {
"title": "EntityRef",
"description": "Typed reference to another service's entity. Store this instead of a foreign key; resolve a display projection from the owner.",
@@@ -3382,6 +3615,138 @@
"description": "AgentSubject: an agent (OpenVibe.Actor, or a developer app's agent) acting under grants its owner delegated (roadmap WS-Z2). In an event envelope, on_behalf_of names the owner."
}
]
++ },
++ "platform.usage-sample.v1": {
++ "title": "UsageSample",
++ "description": "One metered usage reading with retry-safe attribution (T1 Universal Fabric; ADR-034 proposed). Consumers dedupe on `idempotency_key`.",
++ "$comment": "Additive optional fields may extend this v1 contract.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "idempotency_key",
++ "service",
++ "operation",
++ "quantity",
++ "unit",
++ "at",
++ "source"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "idempotency_key": {
++ "type": "string",
++ "description": "Stable across retries"
++ },
++ "service": {
++ "type": "string"
++ },
++ "project": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "subject": {
++ "type": "string"
++ },
++ "resource": {
++ "type": "string"
++ },
++ "provider": {
++ "type": "string"
++ },
++ "node": {
++ "type": "string"
++ },
++ "cell": {
++ "type": "string"
++ },
++ "region": {
++ "type": "string"
++ },
++ "operation": {
++ "type": "string"
++ },
++ "quantity": {
++ "type": "number",
++ "minimum": 0
++ },
++ "unit": {
++ "type": "string"
++ },
++ "at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "cost_estimate": {
++ "type": "number",
++ "minimum": 0,
++ "description": "Estimated USD"
++ },
++ "free_allowance_used": {
++ "type": "number",
++ "minimum": 0,
++ "description": "How much of this reading's `quantity` the free allowance covered (platform.rate-card@1 `free_allowance` for this provider/metric and period), in the reading's own `unit`, not in rate-card units, money or Vibes: a 1.5 GiB reading with 1 GiB free carries 1. Never more than `quantity`; 0 when nothing was free. Absent: not rated against a rate card."
++ },
++ "vibes_charged": {
++ "type": "integer",
++ "minimum": 0,
++ "description": "Vibes charged for this reading, as an integer count of vibes-bits (Billing's ledger minor unit, currency `vibes-bits`; balance credit and ledger amounts use the same unit). Never whole Vibes, USD or a fraction: round once, when rating. Covers only `quantity` minus `free_allowance_used`; 0 means rated and nothing charged. Absent means not rated (yet). Records a charge already made in Billing's ledger; it is never a request to charge."
++ },
++ "route_epoch": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "trace_id": {
++ "type": "string"
++ },
++ "source": {
++ "type": "string"
++ }
++ },
++ "examples": [
++ {
++ "id": "use-1",
++ "idempotency_key": "media:delivery:1",
++ "service": "media",
++ "project": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "subject": "user:usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "resource": "object-1",
++ "provider": "local",
++ "node": "node-1",
++ "cell": "cell-west",
++ "region": "us-west",
++ "operation": "deliver",
++ "quantity": 1.5,
++ "unit": "GiB",
++ "at": "2026-09-30T10:00:00Z",
++ "cost_estimate": 0.03,
++ "source": "media.egress"
++ },
++ {
++ "id": "use-2",
++ "idempotency_key": "media:delivery:2",
++ "service": "media",
++ "project": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "subject": "user:usr_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "resource": "object-1",
++ "provider": "local",
++ "node": "node-1",
++ "cell": "cell-west",
++ "region": "us-west",
++ "operation": "deliver",
++ "quantity": 1.5,
++ "unit": "GiB",
++ "at": "2026-09-30T10:00:00Z",
++ "cost_estimate": 0.03,
++ "source": "media.egress",
++ "free_allowance_used": 1,
++ "vibes_charged": 4,
++ "route_epoch": 3,
++ "trace_id": "tr-42"
++ }
++ ]
}
},
"securitySchemes": {
ddiff --git a/generated/openapi/blog.json b/generated/openapi/blog.json
iindex 05d47dda..b21d3c71 100644
---- a/generated/openapi/blog.json
++++ b/generated/openapi/blog.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Blog API",
-- "version": "0.84.0",
-- "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.84.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.85.0",
++ "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.85.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/chat.json b/generated/openapi/chat.json
iindex 9082179d..1c033bff 100644
---- a/generated/openapi/chat.json
++++ b/generated/openapi/chat.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Chat API",
-- "version": "0.84.0",
-- "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.84.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.85.0",
++ "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.85.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/codes.json b/generated/openapi/codes.json
iindex 8f1ef938..2837a007 100644
---- a/generated/openapi/codes.json
++++ b/generated/openapi/codes.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Codes API",
-- "version": "0.84.0",
-- "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.84.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.85.0",
++ "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.85.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/community.json b/generated/openapi/community.json
iindex 35a9d835..c523f97b 100644
---- a/generated/openapi/community.json
++++ b/generated/openapi/community.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Community API",
-- "version": "0.84.0",
-- "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.84.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.85.0",
++ "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.85.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/coupons.json b/generated/openapi/coupons.json
iindex b60e9037..6318cecd 100644
---- a/generated/openapi/coupons.json
++++ b/generated/openapi/coupons.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Coupons API",
-- "version": "0.84.0",
-- "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.84.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.85.0",
++ "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.85.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/deals.json b/generated/openapi/deals.json
iindex d95c7ca9..d53d3a61 100644
---- a/generated/openapi/deals.json
++++ b/generated/openapi/deals.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Deals API",
-- "version": "0.84.0",
-- "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.84.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.85.0",
++ "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.85.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/events.json b/generated/openapi/events.json
iindex fa759af3..1aeacbec 100644
---- a/generated/openapi/events.json
++++ b/generated/openapi/events.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Events API",
-- "version": "0.84.0",
-- "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.84.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.85.0",
++ "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.85.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/games.json b/generated/openapi/games.json
iindex 21041823..3ef378e2 100644
---- a/generated/openapi/games.json
++++ b/generated/openapi/games.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Games API",
-- "version": "0.84.0",
-- "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.84.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.85.0",
++ "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.85.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/host.json b/generated/openapi/host.json
iindex 33c3aafe..452e9976 100644
---- a/generated/openapi/host.json
++++ b/generated/openapi/host.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Host API",
-- "version": "0.84.0",
-- "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.84.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.85.0",
++ "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.85.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/index.json b/generated/openapi/index.json
iindex 246b10d3..bca11053 100644
---- a/generated/openapi/index.json
++++ b/generated/openapi/index.json
@@@ -12,8 +12,8 @@
"service": "billing",
"name": "OpenVibe.Billing",
"origin": "https://billing.openvibe.network",
-- "operations": 28,
-- "capabilities": 8
++ "operations": 30,
++ "capabilities": 9
},
{
"service": "blog",
ddiff --git a/generated/openapi/live.json b/generated/openapi/live.json
iindex 83ddfd2d..40951a4d 100644
---- a/generated/openapi/live.json
++++ b/generated/openapi/live.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Live API",
-- "version": "0.84.0",
-- "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.84.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.85.0",
++ "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.85.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/media.json b/generated/openapi/media.json
iindex 1fc6fdd9..a0535e30 100644
---- a/generated/openapi/media.json
++++ b/generated/openapi/media.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Media API",
-- "version": "0.84.0",
-- "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.84.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.85.0",
++ "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.85.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/network.json b/generated/openapi/network.json
iindex bf4a69cc..308a9fbc 100644
---- a/generated/openapi/network.json
++++ b/generated/openapi/network.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Network API",
-- "version": "0.84.0",
-- "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.84.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.85.0",
++ "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.85.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/news.json b/generated/openapi/news.json
iindex 2c1d24d9..a7fe69bc 100644
---- a/generated/openapi/news.json
++++ b/generated/openapi/news.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.News API",
-- "version": "0.84.0",
-- "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.84.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.85.0",
++ "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.85.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/openre.json b/generated/openapi/openre.json
iindex 759e326d..ae4500b9 100644
---- a/generated/openapi/openre.json
++++ b/generated/openapi/openre.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenRe.Stream API",
-- "version": "0.84.0",
-- "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.84.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.85.0",
++ "description": "Routes of OpenRe.Stream as the OpenVibe contracts describe them (openvibe-contracts 0.85.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/reviews.json b/generated/openapi/reviews.json
iindex 86a4e70d..7b9ceb6e 100644
---- a/generated/openapi/reviews.json
++++ b/generated/openapi/reviews.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Reviews API",
-- "version": "0.84.0",
-- "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.84.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.85.0",
++ "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.85.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/search.json b/generated/openapi/search.json
iindex e0a6c8bb..bc7c9233 100644
---- a/generated/openapi/search.json
++++ b/generated/openapi/search.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Search API",
-- "version": "0.84.0",
-- "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.84.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.85.0",
++ "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.85.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/sources.json b/generated/openapi/sources.json
iindex aff37b54..f27eaa9b 100644
---- a/generated/openapi/sources.json
++++ b/generated/openapi/sources.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Sources API",
-- "version": "0.84.0",
-- "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.84.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.85.0",
++ "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.85.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/space.json b/generated/openapi/space.json
iindex 78c86740..3d3c062f 100644
---- a/generated/openapi/space.json
++++ b/generated/openapi/space.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Space API",
-- "version": "0.84.0",
-- "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.84.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.85.0",
++ "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.85.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/tips.json b/generated/openapi/tips.json
iindex 8560d3ba..8fa68b13 100644
---- a/generated/openapi/tips.json
++++ b/generated/openapi/tips.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Tips API",
-- "version": "0.84.0",
-- "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.84.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.85.0",
++ "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.85.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/tools.json b/generated/openapi/tools.json
iindex 6f82cb75..e981134d 100644
---- a/generated/openapi/tools.json
++++ b/generated/openapi/tools.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Tools API",
-- "version": "0.84.0",
-- "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.84.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.85.0",
++ "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.85.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/trade.json b/generated/openapi/trade.json
iindex 62bbbaf1..2e7b1efb 100644
---- a/generated/openapi/trade.json
++++ b/generated/openapi/trade.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Trade API",
-- "version": "0.84.0",
-- "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.84.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.85.0",
++ "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.85.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/vip.json b/generated/openapi/vip.json
iindex 786956c2..f89ca3e8 100644
---- a/generated/openapi/vip.json
++++ b/generated/openapi/vip.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.VIP API",
-- "version": "0.84.0",
-- "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.84.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.85.0",
++ "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.85.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/openapi/wiki.json b/generated/openapi/wiki.json
iindex fd941ce1..8ab0d7a9 100644
---- a/generated/openapi/wiki.json
++++ b/generated/openapi/wiki.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Wiki API",
-- "version": "0.84.0",
-- "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.84.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
++ "version": "0.85.0",
++ "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.85.0): each operation lists the capabilities it performs; a token for the service's audience must carry one of them unless the route is open. Generated by openvibe-contracts lib/openapi.js; do not edit.",
"license": {
"name": "MIT",
"identifier": "MIT"
ddiff --git a/generated/typescript/index.d.ts b/generated/typescript/index.d.ts
iindex 1519d17d..c80af1ad 100644
---- a/generated/typescript/index.d.ts
++++ b/generated/typescript/index.d.ts
@@@ -28805,7 +28805,7 @@ export type BillingAdminRequest =
/** [email protected] (owner: billing) */
/**
-- * billing.admin-result@1: answers of billing.ledger.admin on OpenVibe.Billing. GET|POST /api/v1/admin/freeze → { frozen, reason, frozen_at, frozen_by } (POST adds processed_after_unfreeze); GET /api/v1/admin/reconcile → the reconciliation report { id, ok, trigger, started_at, finished_at, checks, warnings, totals }; POST /api/v1/admin/adjustments and POST /api/v1/purchases/settle → { transaction } (settle adds duplicate_receipt); GET /api/v1/admin/provider-events → { events }; POST …/provider-events/:id/reprocess → { event }; POST /api/v1/admin/sweep → { renewed, expired, canceled, skipped }; GET /api/v1/admin/import-holds → { holds }.
++ * billing.admin-result@1: answers of billing.ledger.admin on OpenVibe.Billing. GET|POST /api/v1/admin/freeze → { frozen, reason, frozen_at, frozen_by } (POST adds processed_after_unfreeze); GET /api/v1/admin/reconcile → the reconciliation report { id, ok, trigger, started_at, finished_at, checks, warnings, totals }; POST /api/v1/admin/adjustments and POST /api/v1/purchases/settle → { transaction } (settle adds duplicate_receipt); GET /api/v1/admin/provider-events → { events }; POST …/provider-events/:id/reprocess → { event }; POST /api/v1/admin/sweep → { renewed, expired, canceled, skipped }; GET /api/v1/admin/import-holds → { holds }; GET /api/v1/usage → { records, next_cursor }.
*/
export type BillingAdminResult =
| {
@@@ -28855,6 +28855,26 @@ export type BillingAdminResult =
}
| {
holds: {}[];
++ }
++ | {
++ records: {
++ id: string;
++ /**
++ * The reading's own idempotency_key
++ */
++ idempotency_key: string;
++ project?: string | null;
++ subject?: string | null;
++ service: string;
++ at: string;
++ received_at: string;
++ /**
++ * The service principal that recorded the reading
++ */
++ principal: string;
++ reading: UsageSample;
++ }[];
++ next_cursor: string | null;
};
/** [email protected] (owner: billing) */
@@@ -29302,6 +29322,30 @@ export interface BillingTransferResult {
};
}
++/** [email protected] (owner: billing) */
++/**
++ * billing.usage-record-result@1: answer of billing.usage.record on OpenVibe.Billing. POST /api/v1/usage → { record } (201 when stored; 200 with `Idempotent-Replayed: true` when the same reading was already stored under its idempotency_key). Storing a reading charges nothing.
++ */
++export interface BillingUsageRecordResult {
++ record: {
++ id: string;
++ /**
++ * The reading's own idempotency_key
++ */
++ idempotency_key: string;
++ project?: string | null;
++ subject?: string | null;
++ service: string;
++ at: string;
++ received_at: string;
++ /**
++ * The service principal that recorded the reading
++ */
++ principal: string;
++ reading: UsageSample;
++ };
++}
++
/** [email protected] (owner: deals) */
/**
* deals.watch.matched v1 (OpenVibe.Deals server/domain/watches.js onObservation). A person's keyword, product or price-below watch matched a fresh observation of an active offer, inside the transaction that recorded the observation. At most one event per (watch, observation); a keyword/product watch notifies once per offer, a price-below watch again only for a lower price. Saved searches (kind search) never notify. Envelope: subject { type: watch, id: <watch_id> }, visibility internal, priority important, actor service:deals. OpenVibe.Network turns it into a DEAL_WATCH_MATCH notification for payload.recipient (server/notifications/events-consumer.js). A price is the decimal exactly as stated, never inferred; null when the observation stated none.
ddiff --git a/manifests/capabilities/billing.ledger.admin.json b/manifests/capabilities/billing.ledger.admin.json
iindex 4fd87ebc..ad65b44d 100644
---- a/manifests/capabilities/billing.ledger.admin.json
++++ b/manifests/capabilities/billing.ledger.admin.json
@@@ -23,7 +23,8 @@
"GET /api/v1/admin/provider-events",
"POST /api/v1/admin/provider-events/:id/reprocess",
"POST /api/v1/admin/sweep",
-- "GET /api/v1/admin/import-holds"
++ "GET /api/v1/admin/import-holds",
++ "GET /api/v1/usage"
],
"inputSchema": "billing.admin-request@1",
"outputSchema": "billing.admin-result@1"
ddiff --git a/manifests/capabilities/billing.usage.record.json b/manifests/capabilities/billing.usage.record.json
nnew file mode 100644
iindex 00000000..ac8dcc6d
---- /dev/null
++++ b/manifests/capabilities/billing.usage.record.json
@@@ -0,0 +1,21 @@
++{
++ "id": "billing.usage.record",
++ "version": "1.0",
++ "owner": "billing",
++ "status": "active",
++ "visibility": "internal",
++ "description": "Record a platform.usage-sample@1 reading without moving money. The reading's idempotency_key deduplicates retries. Listing all readings requires billing.ledger.admin.",
++ "permissions": [
++ "billing:usage"
++ ],
++ "resourceConstraints": [
++ "none"
++ ],
++ "quotaClass": "ledger-write",
++ "events": [],
++ "implementedBy": [
++ "POST /api/v1/usage"
++ ],
++ "inputSchema": "platform.usage-sample@1",
++ "outputSchema": "billing.usage-record-result@1"
++}
ddiff --git a/manifests/services/billing.json b/manifests/services/billing.json
iindex 74974204..761f0ac6 100644
---- a/manifests/services/billing.json
++++ b/manifests/services/billing.json
@@@ -27,7 +27,8 @@
"billing.cashout.manage",
"billing.subscription.manage",
"billing.entitlement.check",
-- "billing.ledger.admin"
++ "billing.ledger.admin",
++ "billing.usage.record"
],
"eventsProduced": [
"billing.transaction.settled",
ddiff --git a/package-lock.json b/package-lock.json
iindex dfe4a95c..db9cfc26 100644
---- a/package-lock.json
++++ b/package-lock.json
@@@ -1,12 +1,12 @@
{
"name": "openvibe-contracts",
-- "version": "0.84.0",
++ "version": "0.85.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "openvibe-contracts",
-- "version": "0.84.0",
++ "version": "0.85.0",
"license": "AGPL-3.0-only",
"dependencies": {
"ajv": "^8.17.1",
ddiff --git a/package.json b/package.json
iindex 80f92f4c..f2186dce 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-contracts",
-- "version": "0.84.0",
++ "version": "0.85.0",
"description": "Machine-readable contracts for the OpenVibe network: schemas, manifests, ids, error envelope, capability checks.",
"main": "index.js",
"types": "generated/typescript/index.d.ts",