openvibers/openvibe.contracts Commits

0.289.0: Balance and Vibes Pay contracts (D12) (#298)

OP OpenVibers committed 9 days ago · 248df327e4 · 115 files, +2938 −141

Changes

ddiff --git a/CHANGELOG.md b/CHANGELOG.md
iindex 8c70d9d3..91b1224c 100644
---- a/CHANGELOG.md
++++ b/CHANGELOG.md
@@@ -1,6 +1,39 @@
# Changelog
++## 0.289.0 — 2026-11-04
++
++- **Balance and Vibes Pay (AI Exchange D12, MONEY.md build order step 1).**
++ - **Vibes Pay schemas:** `billing.vibes-charge@1` (id `vch_<ULID>`, app, project_id, `amount` in whole Vibes,
++ a 1–140 description, the app's reference, status pending|paid|refunded|expired|cancelled, pay_url, return_url,
++ the timestamps and sandbox), `billing.vibes-charge-create-request@1` (`amount`, `description`, `reference` — the
++ app's idempotency key — and an optional `return_url`) and `billing.vibes-charge-refund-request@1` (an optional
++ `reason`; a refund is always for the whole charge).
++ - **Vibes Pay events:** `billing.vibes.charge.paid@1` and `billing.vibes.charge.refunded@1` are also the signed
++ webhook bodies (HMAC-SHA256, retried with backoff): the charge fields plus an opaque `payer_ref` that is stable
++ per app, and deliberately no person id and no email. Billing's manifest lists both in `eventsProduced`.
++ - **Balance:** `billing.balance@1` gains the D12 Balance branch — `owner` (user or project), `balance_nano_usd`,
++ `held_nano_usd`, `available_nano_usd` and an optional `sandbox`, with money as integer nano-USD decimal strings
++ (D1, never a float) — answered by `GET /api/me/balance`. The contract id is permanent, so the Vibes wallet body
++ that `billing.balance.read` already answered stays as the second branch; both branches are closed with
++ `additionalProperties: false`, and new code sends the Balance branch.
++ - **Balance requests:** `billing.balance-transfer-request@1` (`to_project_id`, positive `amount_nano_usd`,
++ `idempotency_key`) for moving a person's Balance to a project's, and
++ `billing.vibes-purchase-from-balance-request@1` (`vibes` as a whole positive integer, `idempotency_key`) for
++ Get Vibes with Balance.
++ - **Vibes read:** `billing.vibes-wallet@1` is the person's own Vibes — credit, creator payable and bits_per_usd,
++ never a Balance — answered by `GET /api/v1/vibes/me`.
++ - **Capabilities:** `billing.vibes.charge` (public, grantable to apps, a sandbox app included, which charges
++ sandbox Vibes only) implemented by `POST /api/v1/vibes/charges`, `GET /api/v1/vibes/charges/:id` and
++ `POST /api/v1/vibes/charges/:id/refund`; `billing.vibes.read` (public, a person-delegated read of their own
++ Vibes for an app they approved) implemented by `GET /api/v1/vibes/me`.
++ - **Personal holds:** `billing.credit-hold-create-request@1` no longer requires `project_id`; without it the hold
++ is personal and spends the subject's own Balance. An `app` subject must still name its project (`if`/`then`),
++ so the accepted set only grows and the compat gate stays green.
++ - Valid and invalid fixtures cover both Balance branches, whole-number Vibes, D1 decimal-string money, a personal
++ hold, an app subject without a project, a partial-refund body, and webhook payloads with a person id or an
++ email-shaped payer reference.
++
## 0.288.0 — 2026-11-04
- **Configuration contracts (AI Exchange D11, S1), built on openvibe-shared/config:
ddiff --git a/README.md b/README.md
iindex 85b197e5..74bda59a 100644
---- a/README.md
++++ b/README.md
@@@ -2,7 +2,7 @@
> Machine-readable contracts for the whole OpenVibe network.
--**Status:** alpha (first released in Wave 1; the release is package.json's `version`, tagged). At v0.288.0: 978 schemas in `contracts/catalog.json`, 60 service manifests, 450 capability manifests, 13 namespace manifests and the staff capability map. Service repositories run `openvibe-contracts-check` and the pin-drift check in CI, and deployed services verify service and app tokens with `serviceAuth` in production. Consumers pin release tags (each repository's `STATUS.json` names its pin). `node scripts/docs-currency.js <root>` checks every checkout's README and `STATUS.json` against its code: the pins and versions they name, a library's consumer list, the deployed flag against the manifest, README links, and the paths and migrations they name. `node scripts/estate.js` generates `docs/ESTATE.md` and `manifests/repositories/` from the estate checkouts, and `--check` fails on drift (plan T1 step 3).
++**Status:** alpha (first released in Wave 1; the release is package.json's `version`, tagged). At v0.289.0: 986 schemas in `contracts/catalog.json`, 60 service manifests, 452 capability manifests, 13 namespace manifests and the staff capability map. Service repositories run `openvibe-contracts-check` and the pin-drift check in CI, and deployed services verify service and app tokens with `serviceAuth` in production. Consumers pin release tags (each repository's `STATUS.json` names its pin). `node scripts/docs-currency.js <root>` checks every checkout's README and `STATUS.json` against its code: the pins and versions they name, a library's consumer list, the deployed flag against the manifest, README links, and the paths and migrations they name. `node scripts/estate.js` generates `docs/ESTATE.md` and `manifests/repositories/` from the estate checkouts, and `--check` fails on drift (plan T1 step 3).
**Plan:** OpenVibe Development Roadmap, Wave 1 (implementation plan rev 3, §3.1 and §18.11).
**License:** AGPL-3.0 (same as every OpenVibe service).
ddiff --git a/contracts/billing/balance-transfer-request.v1.json b/contracts/billing/balance-transfer-request.v1.json
nnew file mode 100644
iindex 00000000..9ae8b7f7
---- /dev/null
++++ b/contracts/billing/balance-transfer-request.v1.json
@@@ -0,0 +1,30 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/billing/balance-transfer-request.v1.json",
++ "title": "BillingBalanceTransferRequest",
++ "description": "billing.balance-transfer-request@1: body of POST /api/me/balance/transfers (D12). Moves amount_nano_usd from the signed-in person's Balance to a project they administer, into that project's Balance. Money is positive integer nano-USD as a decimal string (D1), never a float; the transfer is refused when the Balance or the person's project role is short. idempotency_key makes the transfer exactly once: a replay with the same key and body returns the first answer and moves nothing.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "to_project_id",
++ "amount_nano_usd",
++ "idempotency_key"
++ ],
++ "properties": {
++ "to_project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The project whose Balance receives the money; the person must administer it."
++ },
++ "amount_nano_usd": {
++ "type": "string",
++ "pattern": "^[1-9][0-9]{0,18}$",
++ "description": "A positive integer nano-USD (1e-9 USD) as a decimal string (D1), never a float."
++ },
++ "idempotency_key": {
++ "type": "string",
++ "pattern": "^[A-Za-z0-9._:-]{8,128}$",
++ "description": "Unique per mutation. A replay with the same key and body returns the first answer and changes nothing."
++ }
++ }
++}
ddiff --git a/contracts/billing/balance.v1.json b/contracts/billing/balance.v1.json
iindex 83e47946..340b0d40 100644
---- a/contracts/billing/balance.v1.json
++++ b/contracts/billing/balance.v1.json
@@@ -2,16 +2,145 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/billing/balance.v1.json",
"title": "BillingBalance",
-- "description": "billing.balance@1: a person's Vibes balances, GET /api/v1/balances/:subject on OpenVibe.Billing (also returned after a recycle). credit is spendable (bought or given back), payable is what others gave the person (the only balance that can be cashed out), pending_payouts is held for requested cashouts.",
++ "description": "billing.balance@1: a Balance account (D12), held by a person (owner user:<usr_id>) or a project (owner project:<prj_id>). GET /api/me/balance answers { personal, projects } of these. balance_nano_usd is the whole account; held_nano_usd is reserved by active holds; available_nano_usd is spendable (balance = held + available). Money is integer nano-USD as decimal strings (D1), never a float. sandbox marks a sandbox account. An id is permanent, so this contract still also describes the Vibes wallet body that billing.balance.read has answered: a person's Vibes (currency vibes-bits), not a Balance. Each branch is closed with additionalProperties: false; new code sends the Balance branch.",
"type": "object",
-- "required": ["subject", "currency", "credit", "payable", "pending_payouts", "payable_value_cents", "bits_per_usd"],
"properties": {
-- "subject": { "$ref": "../identity/subject-ref.v1.json" },
-- "currency": { "const": "vibes-bits" },
-- "credit": { "type": "integer" },
-- "payable": { "type": "integer" },
-- "pending_payouts": { "type": "integer" },
-- "payable_value_cents": { "type": "integer", "description": "What payable is worth in US cents at today's rate." },
-- "bits_per_usd": { "type": "number" }
-- }
++ "subject": {
++ "$ref": "../identity/subject-ref.v1.json"
++ },
++ "currency": {
++ "const": "vibes-bits"
++ },
++ "credit": {
++ "type": "integer"
++ },
++ "payable": {
++ "type": "integer"
++ },
++ "pending_payouts": {
++ "type": "integer"
++ },
++ "payable_value_cents": {
++ "type": "integer",
++ "description": "What payable is worth in US cents at today's rate."
++ },
++ "bits_per_usd": {
++ "type": "number"
++ }
++ },
++ "$defs": {
++ "money-nano-usd": {
++ "type": "string",
++ "pattern": "^[0-9]{1,19}$",
++ "description": "Integer nano-USD (1e-9 USD), stored as a decimal string (D1), never a float."
++ },
++ "balance-owner": {
++ "oneOf": [
++ {
++ "type": "object",
++ "required": [
++ "type",
++ "id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {
++ "const": "user"
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ },
++ "description": "A person: usd_credit owned by user:<usr_id>."
++ },
++ {
++ "type": "object",
++ "required": [
++ "type",
++ "id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {
++ "const": "project"
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ },
++ "description": "A project: usd_credit owned by project:<prj_id>."
++ }
++ ]
++ }
++ },
++ "oneOf": [
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "owner",
++ "balance_nano_usd",
++ "held_nano_usd",
++ "available_nano_usd"
++ ],
++ "properties": {
++ "owner": {
++ "$ref": "#/$defs/balance-owner"
++ },
++ "balance_nano_usd": {
++ "$ref": "#/$defs/money-nano-usd"
++ },
++ "held_nano_usd": {
++ "$ref": "#/$defs/money-nano-usd"
++ },
++ "available_nano_usd": {
++ "$ref": "#/$defs/money-nano-usd"
++ },
++ "sandbox": {
++ "type": "boolean",
++ "default": false,
++ "description": "True for a sandbox account (D10): sandbox credit only, never real money."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "subject",
++ "currency",
++ "credit",
++ "payable",
++ "pending_payouts",
++ "payable_value_cents",
++ "bits_per_usd"
++ ],
++ "properties": {
++ "subject": {
++ "$ref": "../identity/subject-ref.v1.json"
++ },
++ "currency": {
++ "const": "vibes-bits"
++ },
++ "credit": {
++ "type": "integer"
++ },
++ "payable": {
++ "type": "integer"
++ },
++ "pending_payouts": {
++ "type": "integer"
++ },
++ "payable_value_cents": {
++ "type": "integer",
++ "description": "What payable is worth in US cents at today's rate."
++ },
++ "bits_per_usd": {
++ "type": "number"
++ }
++ }
++ }
++ ]
}
ddiff --git a/contracts/billing/credit-hold-create-request.v1.json b/contracts/billing/credit-hold-create-request.v1.json
iindex b6698918..9d8a9eaf 100644
---- a/contracts/billing/credit-hold-create-request.v1.json
++++ b/contracts/billing/credit-hold-create-request.v1.json
@@@ -2,17 +2,45 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/billing/credit-hold-create-request.v1.json",
"title": "BillingCreditHoldCreateRequest",
-- "description": "billing.credit-hold-create-request@1: body of POST /internal/credits/holds. Reserves inference credit up to max_nano_usd against a (subject, project_id) account, priced at the given immutable rate version.",
++ "description": "billing.credit-hold-create-request@1: body of POST /internal/credits/holds. Reserves inference credit up to max_nano_usd against a Balance account, priced at the given immutable rate version. With project_id the hold spends that project's Balance; without it the hold is personal and spends the subject's own Balance (D12). An app subject always names its project.",
"type": "object",
"required": [
"idempotency_key",
"subject",
-- "project_id",
"max_nano_usd",
"rate_version",
"request_fingerprint"
],
"additionalProperties": false,
++ "allOf": [
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "subject": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "const": "app"
++ }
++ },
++ "required": [
++ "type"
++ ]
++ }
++ },
++ "required": [
++ "subject"
++ ]
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "project_id"
++ ]
++ }
++ }
++ ],
"$defs": {
"money-nano-usd": {
"type": "string",
@@@ -30,7 +58,8 @@
},
"project_id": {
"type": "string",
-- "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The project whose Balance the hold spends. Optional: without it the hold is personal, on the subject's own Balance. Required when the subject is an app."
},
"credential_id": {
"type": [
ddiff --git a/contracts/billing/vibes-charge-create-request.v1.json b/contracts/billing/vibes-charge-create-request.v1.json
nnew file mode 100644
iindex 00000000..d4a33cce
---- /dev/null
++++ b/contracts/billing/vibes-charge-create-request.v1.json
@@@ -0,0 +1,37 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/billing/vibes-charge-create-request.v1.json",
++ "title": "BillingVibesChargeCreateRequest",
++ "description": "billing.vibes-charge-create-request@1: body of POST /api/v1/vibes/charges, sent from the app's server with its app token (capability billing.vibes.charge). amount is whole Vibes. reference is the app's idempotency key: a replay with the same reference and body returns the same charge. return_url is optional. The app token decides the project, the sandbox environment and the owner-set limits.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "amount",
++ "description",
++ "reference"
++ ],
++ "properties": {
++ "amount": {
++ "type": "integer",
++ "minimum": 1,
++ "description": "Price in whole Vibes, a positive integer; never a fraction or a nano-USD amount."
++ },
++ "description": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 140,
++ "description": "What the person is paying for."
++ },
++ "reference": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 128,
++ "description": "The app's own reference; it is the idempotency key."
++ },
++ "return_url": {
++ "type": "string",
++ "format": "uri",
++ "description": "Where the browser returns after paying. Optional; the app may instead poll the charge or wait for the webhook."
++ }
++ }
++}
ddiff --git a/contracts/billing/vibes-charge-refund-request.v1.json b/contracts/billing/vibes-charge-refund-request.v1.json
nnew file mode 100644
iindex 00000000..8d0431a5
---- /dev/null
++++ b/contracts/billing/vibes-charge-refund-request.v1.json
@@@ -0,0 +1,18 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/billing/vibes-charge-refund-request.v1.json",
++ "title": "BillingVibesChargeRefundRequest",
++ "description": "billing.vibes-charge-refund-request@1: the optional body of POST /api/v1/vibes/charges/:id/refund. A refund is always for the whole charge: the Vibes go back from the project's Vibes wallet to the person. reason is recorded on the refund. No amount may be given, because a partial refund is not a Vibes Pay refund.",
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 300,
++ "description": "Why the app refunded, for its own and the person's record."
++ }
++ }
++}
ddiff --git a/contracts/billing/vibes-charge.v1.json b/contracts/billing/vibes-charge.v1.json
nnew file mode 100644
iindex 00000000..45a40050
---- /dev/null
++++ b/contracts/billing/vibes-charge.v1.json
@@@ -0,0 +1,109 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/billing/vibes-charge.v1.json",
++ "title": "BillingVibesCharge",
++ "description": "billing.vibes-charge@1: one Vibes Pay charge (D12). An app creates it for a person to pay with Vibes; GET /api/v1/vibes/charges/:id answers it, and a replay of the same app reference returns the same charge. amount is a whole number of Vibes (never a fraction, never nano-USD). status moves pending -> paid -> refunded, or pending -> expired|cancelled. sandbox charges use test Vibes only, worth nothing. The paid and refunded webhook/event payloads carry the same fields plus an opaque payer_ref and no person id or email.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "app",
++ "project_id",
++ "amount",
++ "description",
++ "reference",
++ "status",
++ "pay_url",
++ "return_url",
++ "created_at",
++ "paid_at",
++ "refunded_at",
++ "expires_at",
++ "sandbox"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^vch_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "ULID charge id."
++ },
++ "app": {
++ "type": "string",
++ "pattern": "^app_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The app that created the charge (its app_ id)."
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The app's project; paid Vibes land in that project's Vibes wallet."
++ },
++ "amount": {
++ "type": "integer",
++ "minimum": 1,
++ "description": "Price in whole Vibes. A positive integer; Vibes have no fractions."
++ },
++ "description": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 140,
++ "description": "What the person is paying for."
++ },
++ "reference": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 128,
++ "description": "The app's own reference for this charge. It is the idempotency key: a replay with the same reference returns the same charge."
++ },
++ "status": {
++ "enum": [
++ "pending",
++ "paid",
++ "refunded",
++ "expired",
++ "cancelled"
++ ]
++ },
++ "pay_url": {
++ "type": "string",
++ "format": "uri",
++ "description": "OpenVibe's pay sheet for this charge, e.g. https://openvibe.network/pay/vch_..."
++ },
++ "return_url": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "uri",
++ "description": "Where the browser returns after paying; null when the app gave no return URL."
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "paid_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time",
++ "description": "When the Vibes moved, or null while the charge is unpaid."
++ },
++ "refunded_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time",
++ "description": "When the Vibes went back to the person, or null."
++ },
++ "expires_at": {
++ "type": "string",
++ "format": "date-time",
++ "description": "The pay sheet stops accepting payment after this instant."
++ },
++ "sandbox": {
++ "type": "boolean",
++ "description": "True for a sandbox app: sandbox Vibes only, worth nothing."
++ }
++ }
++}
ddiff --git a/contracts/billing/vibes-purchase-from-balance-request.v1.json b/contracts/billing/vibes-purchase-from-balance-request.v1.json
nnew file mode 100644
iindex 00000000..82accc43
---- /dev/null
++++ b/contracts/billing/vibes-purchase-from-balance-request.v1.json
@@@ -0,0 +1,24 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/billing/vibes-purchase-from-balance-request.v1.json",
++ "title": "BillingVibesPurchaseFromBalanceRequest",
++ "description": "billing.vibes-purchase-from-balance-request@1: body of POST /api/me/vibes/from-balance (D12). Buys a whole number of Vibes with the signed-in person's Balance at the same Vibes price as a pack, charging exact nano-USD; Vibes -> Balance is never allowed. vibes is a whole integer (Vibes have no fractions); the money side is integer nano-USD (D1), never a float. idempotency_key makes the purchase exactly once.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "vibes",
++ "idempotency_key"
++ ],
++ "properties": {
++ "vibes": {
++ "type": "integer",
++ "minimum": 1,
++ "description": "How many whole Vibes to buy."
++ },
++ "idempotency_key": {
++ "type": "string",
++ "pattern": "^[A-Za-z0-9._:-]{8,128}$",
++ "description": "Unique per mutation. A replay with the same key and body returns the first answer and changes nothing."
++ }
++ }
++}
ddiff --git a/contracts/billing/vibes-wallet.v1.json b/contracts/billing/vibes-wallet.v1.json
nnew file mode 100644
iindex 00000000..98d37367
---- /dev/null
++++ b/contracts/billing/vibes-wallet.v1.json
@@@ -0,0 +1,52 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/billing/vibes-wallet.v1.json",
++ "title": "BillingVibesWallet",
++ "description": "billing.vibes-wallet@1: a person's own Vibes, answered by GET /api/v1/vibes/me (capability billing.vibes.read) to an app the person approved. credit is Vibes to spend; payable is Vibes the person received as a creator and may cash out under the payout rules (never into Balance); bits_per_usd is today's rate. Vibes are whole integers; this is not a Balance and carries no nano-USD.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "subject",
++ "currency",
++ "credit",
++ "payable",
++ "bits_per_usd"
++ ],
++ "properties": {
++ "subject": {
++ "type": "object",
++ "required": [
++ "type",
++ "id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {
++ "const": "user"
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ },
++ "description": "The person the app reads for (the approved delegation). Only a user, never an app or a service."
++ },
++ "currency": {
++ "const": "vibes-bits"
++ },
++ "credit": {
++ "type": "integer",
++ "minimum": 0,
++ "description": "Vibes to spend."
++ },
++ "payable": {
++ "type": "integer",
++ "minimum": 0,
++ "description": "Vibes received as a creator, to cash out; never Balance."
++ },
++ "bits_per_usd": {
++ "type": "number",
++ "description": "Today's Vibes per USD rate, the same one a pack is priced with."
++ }
++ }
++}
ddiff --git a/contracts/catalog.json b/contracts/catalog.json
iindex 5923f9fa..5342c09b 100644
---- a/contracts/catalog.json
++++ b/contracts/catalog.json
@@@ -2295,6 +2295,78 @@
"compatibility": "backward",
"status": "active"
},
++ {
++ "id": "billing.vibes-charge",
++ "version": "1.0.0",
++ "owner": "billing",
++ "schema": "billing/vibes-charge.v1.json",
++ "visibility": "public",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "billing.vibes-charge-create-request",
++ "version": "1.0.0",
++ "owner": "billing",
++ "schema": "billing/vibes-charge-create-request.v1.json",
++ "visibility": "public",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "billing.vibes-charge-refund-request",
++ "version": "1.0.0",
++ "owner": "billing",
++ "schema": "billing/vibes-charge-refund-request.v1.json",
++ "visibility": "public",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "billing.vibes-wallet",
++ "version": "1.0.0",
++ "owner": "billing",
++ "schema": "billing/vibes-wallet.v1.json",
++ "visibility": "public",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "billing.balance-transfer-request",
++ "version": "1.0.0",
++ "owner": "billing",
++ "schema": "billing/balance-transfer-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "billing.vibes-purchase-from-balance-request",
++ "version": "1.0.0",
++ "owner": "billing",
++ "schema": "billing/vibes-purchase-from-balance-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "billing.vibes.charge.paid",
++ "version": "1.0.0",
++ "owner": "billing",
++ "schema": "events/payloads/billing.vibes.charge.paid.v1.json",
++ "visibility": "public",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "billing.vibes.charge.refunded",
++ "version": "1.0.0",
++ "owner": "billing",
++ "schema": "events/payloads/billing.vibes.charge.refunded.v1.json",
++ "visibility": "public",
++ "compatibility": "backward",
++ "status": "active"
++ },
{
"id": "billing.cashout-decision",
"version": "1.0.0",
ddiff --git a/contracts/events/payloads/billing.vibes.charge.paid.v1.json b/contracts/events/payloads/billing.vibes.charge.paid.v1.json
nnew file mode 100644
iindex 00000000..4c400611
---- /dev/null
++++ b/contracts/events/payloads/billing.vibes.charge.paid.v1.json
@@@ -0,0 +1,87 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/billing.vibes.charge.paid.v1.json",
++ "title": "BillingVibesChargePaidPayload",
++ "description": "billing.vibes.charge.paid v1 (OpenVibe.Billing, written when a Vibes Pay charge is paid): the app's charge and the opaque payer reference. This is also the Vibes Pay webhook body (HMAC-SHA256 with the app's webhook secret, retried with backoff). It deliberately carries no person id and no email, only payer_ref, stable per app, so an app can recognise a returning payer without receiving their identity. amount is whole Vibes. Envelope: subject { type: vibes-charge, id: vch_... }, actor service:billing.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "app",
++ "project_id",
++ "amount",
++ "description",
++ "reference",
++ "status",
++ "created_at",
++ "paid_at",
++ "refunded_at",
++ "expires_at",
++ "sandbox",
++ "payer_ref"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^vch_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The charge; GET /api/v1/vibes/charges/:id returns its current state."
++ },
++ "app": {
++ "type": "string",
++ "pattern": "^app_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The app that created the charge."
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The app's project, whose Vibes wallet received the charge."
++ },
++ "amount": {
++ "type": "integer",
++ "minimum": 1,
++ "description": "The whole number of Vibes paid."
++ },
++ "description": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 140
++ },
++ "reference": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 128,
++ "description": "The app's reference, as given when the charge was created."
++ },
++ "status": {
++ "const": "paid"
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "paid_at": {
++ "type": "string",
++ "format": "date-time",
++ "description": "When the Vibes moved from the person to the project."
++ },
++ "refunded_at": {
++ "type": "null",
++ "description": "Null: a paid charge has not been refunded."
++ },
++ "expires_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "sandbox": {
++ "type": "boolean",
++ "description": "True for a sandbox app: sandbox Vibes only, worth nothing."
++ },
++ "payer_ref": {
++ "type": "string",
++ "minLength": 8,
++ "maxLength": 128,
++ "pattern": "^[A-Za-z0-9._:-]+$",
++ "description": "Opaque reference for the paying person, stable per app. Never the person's id, email or any other identity; an app uses it to recognise a returning payer."
++ }
++ }
++}
ddiff --git a/contracts/events/payloads/billing.vibes.charge.refunded.v1.json b/contracts/events/payloads/billing.vibes.charge.refunded.v1.json
nnew file mode 100644
iindex 00000000..942694a0
---- /dev/null
++++ b/contracts/events/payloads/billing.vibes.charge.refunded.v1.json
@@@ -0,0 +1,88 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/billing.vibes.charge.refunded.v1.json",
++ "title": "BillingVibesChargeRefundedPayload",
++ "description": "billing.vibes.charge.refunded v1 (OpenVibe.Billing, written when a Vibes Pay charge is refunded): the app's charge and the opaque payer reference. This is also the Vibes Pay webhook body (HMAC-SHA256 with the app's webhook secret, retried with backoff). It deliberately carries no person id and no email, only payer_ref, stable per app, so an app can recognise a returning payer without receiving their identity. amount is whole Vibes. Envelope: subject { type: vibes-charge, id: vch_... }, actor service:billing.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "app",
++ "project_id",
++ "amount",
++ "description",
++ "reference",
++ "status",
++ "created_at",
++ "paid_at",
++ "refunded_at",
++ "expires_at",
++ "sandbox",
++ "payer_ref"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^vch_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The charge; GET /api/v1/vibes/charges/:id returns its current state."
++ },
++ "app": {
++ "type": "string",
++ "pattern": "^app_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The app that created the charge."
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The app's project, whose Vibes wallet received the charge."
++ },
++ "amount": {
++ "type": "integer",
++ "minimum": 1,
++ "description": "The whole number of Vibes paid."
++ },
++ "description": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 140
++ },
++ "reference": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 128,
++ "description": "The app's reference, as given when the charge was created."
++ },
++ "status": {
++ "const": "refunded"
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "paid_at": {
++ "type": "string",
++ "format": "date-time",
++ "description": "When the Vibes moved from the person to the project."
++ },
++ "refunded_at": {
++ "type": "string",
++ "format": "date-time",
++ "description": "When the Vibes went back to the person."
++ },
++ "expires_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "sandbox": {
++ "type": "boolean",
++ "description": "True for a sandbox app: sandbox Vibes only, worth nothing."
++ },
++ "payer_ref": {
++ "type": "string",
++ "minLength": 8,
++ "maxLength": 128,
++ "pattern": "^[A-Za-z0-9._:-]+$",
++ "description": "Opaque reference for the paying person, stable per app. Never the person's id, email or any other identity; an app uses it to recognise a returning payer."
++ }
++ }
++}
ddiff --git a/fixtures/billing.balance-transfer-request/invalid/extra-field.json b/fixtures/billing.balance-transfer-request/invalid/extra-field.json
nnew file mode 100644
iindex 00000000..08a7ac99
---- /dev/null
++++ b/fixtures/billing.balance-transfer-request/invalid/extra-field.json
@@@ -0,0 +1,6 @@
++{
++ "to_project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "amount_nano_usd": "5000000000",
++ "idempotency_key": "transfer-key-001",
++ "to_user_id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++}
ddiff --git a/fixtures/billing.balance-transfer-request/invalid/missing-project.json b/fixtures/billing.balance-transfer-request/invalid/missing-project.json
nnew file mode 100644
iindex 00000000..d93010ab
---- /dev/null
++++ b/fixtures/billing.balance-transfer-request/invalid/missing-project.json
@@@ -0,0 +1,4 @@
++{
++ "amount_nano_usd": "5000000000",
++ "idempotency_key": "transfer-key-001"
++}
ddiff --git a/fixtures/billing.balance-transfer-request/invalid/money-number.json b/fixtures/billing.balance-transfer-request/invalid/money-number.json
nnew file mode 100644
iindex 00000000..f605c82b
---- /dev/null
++++ b/fixtures/billing.balance-transfer-request/invalid/money-number.json
@@@ -0,0 +1,5 @@
++{
++ "to_project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "amount_nano_usd": 5000000000,
++ "idempotency_key": "transfer-key-001"
++}
ddiff --git a/fixtures/billing.balance-transfer-request/invalid/zero-amount.json b/fixtures/billing.balance-transfer-request/invalid/zero-amount.json
nnew file mode 100644
iindex 00000000..2172a236
---- /dev/null
++++ b/fixtures/billing.balance-transfer-request/invalid/zero-amount.json
@@@ -0,0 +1,5 @@
++{
++ "to_project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "amount_nano_usd": "0",
++ "idempotency_key": "transfer-key-001"
++}
ddiff --git a/fixtures/billing.balance-transfer-request/valid/to-project.json b/fixtures/billing.balance-transfer-request/valid/to-project.json
nnew file mode 100644
iindex 00000000..06549ca1
---- /dev/null
++++ b/fixtures/billing.balance-transfer-request/valid/to-project.json
@@@ -0,0 +1,5 @@
++{
++ "to_project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "amount_nano_usd": "5000000000",
++ "idempotency_key": "transfer-key-001"
++}
ddiff --git a/fixtures/billing.balance/invalid/extra-promo-bits.json b/fixtures/billing.balance/invalid/extra-promo-bits.json
nnew file mode 100644
iindex 00000000..c1a33b88
---- /dev/null
++++ b/fixtures/billing.balance/invalid/extra-promo-bits.json
@@@ -0,0 +1,10 @@
++{
++ "owner": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "balance_nano_usd": "1",
++ "held_nano_usd": "0",
++ "available_nano_usd": "1",
++ "promo_bits": 5
++}
ddiff --git a/fixtures/billing.balance/invalid/missing-available.json b/fixtures/billing.balance/invalid/missing-available.json
nnew file mode 100644
iindex 00000000..9f3c1667
---- /dev/null
++++ b/fixtures/billing.balance/invalid/missing-available.json
@@@ -0,0 +1,8 @@
++{
++ "owner": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "balance_nano_usd": "12400000000",
++ "held_nano_usd": "0"
++}
ddiff --git a/fixtures/billing.balance/invalid/mixed-shapes.json b/fixtures/billing.balance/invalid/mixed-shapes.json
nnew file mode 100644
iindex 00000000..038c1f05
---- /dev/null
++++ b/fixtures/billing.balance/invalid/mixed-shapes.json
@@@ -0,0 +1,19 @@
++{
++ "owner": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "balance_nano_usd": "1",
++ "held_nano_usd": "0",
++ "available_nano_usd": "1",
++ "subject": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "currency": "vibes-bits",
++ "credit": 1,
++ "payable": 0,
++ "pending_payouts": 0,
++ "payable_value_cents": 0,
++ "bits_per_usd": 100
++}
ddiff --git a/fixtures/billing.balance/invalid/money-number.json b/fixtures/billing.balance/invalid/money-number.json
nnew file mode 100644
iindex 00000000..b327c18e
---- /dev/null
++++ b/fixtures/billing.balance/invalid/money-number.json
@@@ -0,0 +1,9 @@
++{
++ "owner": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "balance_nano_usd": 12400000000,
++ "held_nano_usd": "0",
++ "available_nano_usd": "12400000000"
++}
ddiff --git a/fixtures/billing.balance/invalid/unknown-owner-type.json b/fixtures/billing.balance/invalid/unknown-owner-type.json
nnew file mode 100644
iindex 00000000..c07d6240
---- /dev/null
++++ b/fixtures/billing.balance/invalid/unknown-owner-type.json
@@@ -0,0 +1,9 @@
++{
++ "owner": {
++ "type": "guest",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "balance_nano_usd": "1",
++ "held_nano_usd": "0",
++ "available_nano_usd": "1"
++}
ddiff --git a/fixtures/billing.balance/valid/personal.json b/fixtures/billing.balance/valid/personal.json
nnew file mode 100644
iindex 00000000..0bb4b71c
---- /dev/null
++++ b/fixtures/billing.balance/valid/personal.json
@@@ -0,0 +1,10 @@
++{
++ "owner": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "balance_nano_usd": "12400000000",
++ "held_nano_usd": "0",
++ "available_nano_usd": "12400000000",
++ "sandbox": false
++}
ddiff --git a/fixtures/billing.balance/valid/project.json b/fixtures/billing.balance/valid/project.json
nnew file mode 100644
iindex 00000000..b9033cb4
---- /dev/null
++++ b/fixtures/billing.balance/valid/project.json
@@@ -0,0 +1,9 @@
++{
++ "owner": {
++ "type": "project",
++ "id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "balance_nano_usd": "5000000000",
++ "held_nano_usd": "1200000000",
++ "available_nano_usd": "3800000000"
++}
ddiff --git a/fixtures/billing.balance/valid/sandbox.json b/fixtures/billing.balance/valid/sandbox.json
nnew file mode 100644
iindex 00000000..faab1f67
---- /dev/null
++++ b/fixtures/billing.balance/valid/sandbox.json
@@@ -0,0 +1,10 @@
++{
++ "owner": {
++ "type": "project",
++ "id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "balance_nano_usd": "100000000",
++ "held_nano_usd": "0",
++ "available_nano_usd": "100000000",
++ "sandbox": true
++}
ddiff --git a/fixtures/billing.credit-hold-create-request/invalid/app-without-project.json b/fixtures/billing.credit-hold-create-request/invalid/app-without-project.json
nnew file mode 100644
iindex 00000000..d6e79540
---- /dev/null
++++ b/fixtures/billing.credit-hold-create-request/invalid/app-without-project.json
@@@ -0,0 +1,10 @@
++{
++ "idempotency_key": "hold-key-app-001",
++ "subject": {
++ "type": "app",
++ "id": "app_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "max_nano_usd": "50000000",
++ "rate_version": "rr-2026-11-03-001",
++ "request_fingerprint": "fp-chat-app-001"
++}
ddiff --git a/fixtures/billing.credit-hold-create-request/valid/personal-hold.json b/fixtures/billing.credit-hold-create-request/valid/personal-hold.json
nnew file mode 100644
iindex 00000000..bf9c1311
---- /dev/null
++++ b/fixtures/billing.credit-hold-create-request/valid/personal-hold.json
@@@ -0,0 +1,11 @@
++{
++ "idempotency_key": "hold-key-personal-001",
++ "subject": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "max_nano_usd": "100000000",
++ "rate_version": "rr-2026-11-03-001",
++ "request_fingerprint": "fp-chat-personal-001",
++ "service": "ai"
++}
ddiff --git a/fixtures/billing.vibes-charge-create-request/invalid/empty-description.json b/fixtures/billing.vibes-charge-create-request/invalid/empty-description.json
nnew file mode 100644
iindex 00000000..baa44824
---- /dev/null
++++ b/fixtures/billing.vibes-charge-create-request/invalid/empty-description.json
@@@ -0,0 +1,5 @@
++{
++ "amount": 50,
++ "description": "",
++ "reference": "order_123"
++}
ddiff --git a/fixtures/billing.vibes-charge-create-request/invalid/float-amount.json b/fixtures/billing.vibes-charge-create-request/invalid/float-amount.json
nnew file mode 100644
iindex 00000000..56b6db9f
---- /dev/null
++++ b/fixtures/billing.vibes-charge-create-request/invalid/float-amount.json
@@@ -0,0 +1,5 @@
++{
++ "amount": 0.5,
++ "description": "Sticker pack: Neon cats",
++ "reference": "order_123"
++}
ddiff --git a/fixtures/billing.vibes-charge-create-request/invalid/missing-reference.json b/fixtures/billing.vibes-charge-create-request/invalid/missing-reference.json
nnew file mode 100644
iindex 00000000..8a84b8e8
---- /dev/null
++++ b/fixtures/billing.vibes-charge-create-request/invalid/missing-reference.json
@@@ -0,0 +1,4 @@
++{
++ "amount": 50,
++ "description": "Sticker pack: Neon cats"
++}
ddiff --git a/fixtures/billing.vibes-charge-create-request/invalid/person-id.json b/fixtures/billing.vibes-charge-create-request/invalid/person-id.json
nnew file mode 100644
iindex 00000000..30cdc28b
---- /dev/null
++++ b/fixtures/billing.vibes-charge-create-request/invalid/person-id.json
@@@ -0,0 +1,6 @@
++{
++ "amount": 50,
++ "description": "Sticker pack: Neon cats",
++ "reference": "order_123",
++ "user_id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++}
ddiff --git a/fixtures/billing.vibes-charge-create-request/invalid/return-url-not-uri.json b/fixtures/billing.vibes-charge-create-request/invalid/return-url-not-uri.json
nnew file mode 100644
iindex 00000000..746b28e9
---- /dev/null
++++ b/fixtures/billing.vibes-charge-create-request/invalid/return-url-not-uri.json
@@@ -0,0 +1,6 @@
++{
++ "amount": 50,
++ "description": "Sticker pack: Neon cats",
++ "reference": "order_123",
++ "return_url": "not a url"
++}
ddiff --git a/fixtures/billing.vibes-charge-create-request/valid/no-return-url.json b/fixtures/billing.vibes-charge-create-request/valid/no-return-url.json
nnew file mode 100644
iindex 00000000..a0c91d03
---- /dev/null
++++ b/fixtures/billing.vibes-charge-create-request/valid/no-return-url.json
@@@ -0,0 +1,5 @@
++{
++ "amount": 1,
++ "description": "One cheer",
++ "reference": "order_124"
++}
ddiff --git a/fixtures/billing.vibes-charge-create-request/valid/sticker-pack.json b/fixtures/billing.vibes-charge-create-request/valid/sticker-pack.json
nnew file mode 100644
iindex 00000000..fd780932
---- /dev/null
++++ b/fixtures/billing.vibes-charge-create-request/valid/sticker-pack.json
@@@ -0,0 +1,6 @@
++{
++ "amount": 50,
++ "description": "Sticker pack: Neon cats",
++ "reference": "order_123",
++ "return_url": "https://myapp.example/thanks"
++}
ddiff --git a/fixtures/billing.vibes-charge-refund-request/invalid/partial-amount.json b/fixtures/billing.vibes-charge-refund-request/invalid/partial-amount.json
nnew file mode 100644
iindex 00000000..f04ab5ae
---- /dev/null
++++ b/fixtures/billing.vibes-charge-refund-request/invalid/partial-amount.json
@@@ -0,0 +1,3 @@
++{
++ "amount": 25
++}
ddiff --git a/fixtures/billing.vibes-charge-refund-request/invalid/reason-too-long.json b/fixtures/billing.vibes-charge-refund-request/invalid/reason-too-long.json
nnew file mode 100644
iindex 00000000..f3712834
---- /dev/null
++++ b/fixtures/billing.vibes-charge-refund-request/invalid/reason-too-long.json
@@@ -0,0 +1,3 @@
++{
++ "reason": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
++}
ddiff --git a/fixtures/billing.vibes-charge-refund-request/valid/empty.json b/fixtures/billing.vibes-charge-refund-request/valid/empty.json
nnew file mode 100644
iindex 00000000..0967ef42
---- /dev/null
++++ b/fixtures/billing.vibes-charge-refund-request/valid/empty.json
@@@ -0,0 +1 @@
++{}
ddiff --git a/fixtures/billing.vibes-charge-refund-request/valid/reason.json b/fixtures/billing.vibes-charge-refund-request/valid/reason.json
nnew file mode 100644
iindex 00000000..126d81e3
---- /dev/null
++++ b/fixtures/billing.vibes-charge-refund-request/valid/reason.json
@@@ -0,0 +1,3 @@
++{
++ "reason": "The sticker pack was removed"
++}
ddiff --git a/fixtures/billing.vibes-charge/invalid/bad-id.json b/fixtures/billing.vibes-charge/invalid/bad-id.json
nnew file mode 100644
iindex 00000000..65a75d3a
---- /dev/null
++++ b/fixtures/billing.vibes-charge/invalid/bad-id.json
@@@ -0,0 +1,16 @@
++{
++ "id": "vch_1",
++ "app": "app_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "amount": 50,
++ "description": "Sticker pack: Neon cats",
++ "reference": "order_123",
++ "status": "pending",
++ "pay_url": "https://openvibe.network/pay/vch_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "return_url": "https://myapp.example/thanks",
++ "created_at": "2026-11-04T12:00:00.000Z",
++ "paid_at": null,
++ "refunded_at": null,
++ "expires_at": "2026-11-04T12:30:00.000Z",
++ "sandbox": false
++}
ddiff --git a/fixtures/billing.vibes-charge/invalid/description-too-long.json b/fixtures/billing.vibes-charge/invalid/description-too-long.json
nnew file mode 100644
iindex 00000000..deceebe2
---- /dev/null
++++ b/fixtures/billing.vibes-charge/invalid/description-too-long.json
@@@ -0,0 +1,16 @@
++{
++ "id": "vch_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "app": "app_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "amount": 50,
++ "description": "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
++ "reference": "order_123",
++ "status": "pending",
++ "pay_url": "https://openvibe.network/pay/vch_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "return_url": "https://myapp.example/thanks",
++ "created_at": "2026-11-04T12:00:00.000Z",
++ "paid_at": null,
++ "refunded_at": null,
++ "expires_at": "2026-11-04T12:30:00.000Z",
++ "sandbox": false
++}
ddiff --git a/fixtures/billing.vibes-charge/invalid/float-amount.json b/fixtures/billing.vibes-charge/invalid/float-amount.json
nnew file mode 100644
iindex 00000000..673b8ac9
---- /dev/null
++++ b/fixtures/billing.vibes-charge/invalid/float-amount.json
@@@ -0,0 +1,16 @@
++{
++ "id": "vch_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "app": "app_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "amount": 12.5,
++ "description": "Sticker pack: Neon cats",
++ "reference": "order_123",
++ "status": "pending",
++ "pay_url": "https://openvibe.network/pay/vch_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "return_url": "https://myapp.example/thanks",
++ "created_at": "2026-11-04T12:00:00.000Z",
++ "paid_at": null,
++ "refunded_at": null,
++ "expires_at": "2026-11-04T12:30:00.000Z",
++ "sandbox": false
++}
ddiff --git a/fixtures/billing.vibes-charge/invalid/person-id.json b/fixtures/billing.vibes-charge/invalid/person-id.json
nnew file mode 100644
iindex 00000000..0b11e5e4
---- /dev/null
++++ b/fixtures/billing.vibes-charge/invalid/person-id.json
@@@ -0,0 +1,17 @@
++{
++ "id": "vch_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "app": "app_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "amount": 50,
++ "description": "Sticker pack: Neon cats",
++ "reference": "order_123",
++ "status": "pending",
++ "pay_url": "https://openvibe.network/pay/vch_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "return_url": "https://myapp.example/thanks",
++ "created_at": "2026-11-04T12:00:00.000Z",
++ "paid_at": null,
++ "refunded_at": null,
++ "expires_at": "2026-11-04T12:30:00.000Z",
++ "sandbox": false,
++ "user_id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++}
ddiff --git a/fixtures/billing.vibes-charge/invalid/unknown-status.json b/fixtures/billing.vibes-charge/invalid/unknown-status.json
nnew file mode 100644
iindex 00000000..9eb4dd3b
---- /dev/null
++++ b/fixtures/billing.vibes-charge/invalid/unknown-status.json
@@@ -0,0 +1,16 @@
++{
++ "id": "vch_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "app": "app_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "amount": 50,
++ "description": "Sticker pack: Neon cats",
++ "reference": "order_123",
++ "status": "failed",
++ "pay_url": "https://openvibe.network/pay/vch_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "return_url": "https://myapp.example/thanks",
++ "created_at": "2026-11-04T12:00:00.000Z",
++ "paid_at": null,
++ "refunded_at": null,
++ "expires_at": "2026-11-04T12:30:00.000Z",
++ "sandbox": false
++}
ddiff --git a/fixtures/billing.vibes-charge/invalid/zero-amount.json b/fixtures/billing.vibes-charge/invalid/zero-amount.json
nnew file mode 100644
iindex 00000000..25183a34
---- /dev/null
++++ b/fixtures/billing.vibes-charge/invalid/zero-amount.json
@@@ -0,0 +1,16 @@
++{
++ "id": "vch_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "app": "app_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "amount": 0,
++ "description": "Sticker pack: Neon cats",
++ "reference": "order_123",
++ "status": "pending",
++ "pay_url": "https://openvibe.network/pay/vch_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "return_url": "https://myapp.example/thanks",
++ "created_at": "2026-11-04T12:00:00.000Z",
++ "paid_at": null,
++ "refunded_at": null,
++ "expires_at": "2026-11-04T12:30:00.000Z",
++ "sandbox": false
++}
ddiff --git a/fixtures/billing.vibes-charge/valid/expired.json b/fixtures/billing.vibes-charge/valid/expired.json
nnew file mode 100644
iindex 00000000..285f4078
---- /dev/null
++++ b/fixtures/billing.vibes-charge/valid/expired.json
@@@ -0,0 +1,16 @@
++{
++ "id": "vch_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "app": "app_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "amount": 50,
++ "description": "Sticker pack: Neon cats",
++ "reference": "order_123",
++ "status": "expired",
++ "pay_url": "https://openvibe.network/pay/vch_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "return_url": "https://myapp.example/thanks",
++ "created_at": "2026-11-04T12:00:00.000Z",
++ "paid_at": null,
++ "refunded_at": null,
++ "expires_at": "2026-11-04T12:30:00.000Z",
++ "sandbox": false
++}
ddiff --git a/fixtures/billing.vibes-charge/valid/paid-sandbox.json b/fixtures/billing.vibes-charge/valid/paid-sandbox.json
nnew file mode 100644
iindex 00000000..6731564b
---- /dev/null
++++ b/fixtures/billing.vibes-charge/valid/paid-sandbox.json
@@@ -0,0 +1,16 @@
++{
++ "id": "vch_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "app": "app_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "amount": 50,
++ "description": "Sticker pack: Neon cats",
++ "reference": "order_123",
++ "status": "paid",
++ "pay_url": "https://openvibe.network/pay/vch_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "return_url": null,
++ "created_at": "2026-11-04T12:00:00.000Z",
++ "paid_at": "2026-11-04T12:01:00.000Z",
++ "refunded_at": null,
++ "expires_at": "2026-11-04T12:30:00.000Z",
++ "sandbox": true
++}
ddiff --git a/fixtures/billing.vibes-charge/valid/pending.json b/fixtures/billing.vibes-charge/valid/pending.json
nnew file mode 100644
iindex 00000000..ebe6ac6e
---- /dev/null
++++ b/fixtures/billing.vibes-charge/valid/pending.json
@@@ -0,0 +1,16 @@
++{
++ "id": "vch_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "app": "app_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "amount": 50,
++ "description": "Sticker pack: Neon cats",
++ "reference": "order_123",
++ "status": "pending",
++ "pay_url": "https://openvibe.network/pay/vch_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "return_url": "https://myapp.example/thanks",
++ "created_at": "2026-11-04T12:00:00.000Z",
++ "paid_at": null,
++ "refunded_at": null,
++ "expires_at": "2026-11-04T12:30:00.000Z",
++ "sandbox": false
++}
ddiff --git a/fixtures/billing.vibes-charge/valid/refunded.json b/fixtures/billing.vibes-charge/valid/refunded.json
nnew file mode 100644
iindex 00000000..2eaccb04
---- /dev/null
++++ b/fixtures/billing.vibes-charge/valid/refunded.json
@@@ -0,0 +1,16 @@
++{
++ "id": "vch_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "app": "app_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "amount": 50,
++ "description": "Sticker pack: Neon cats",
++ "reference": "order_123",
++ "status": "refunded",
++ "pay_url": "https://openvibe.network/pay/vch_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "return_url": "https://myapp.example/thanks",
++ "created_at": "2026-11-04T12:00:00.000Z",
++ "paid_at": "2026-11-04T12:01:00.000Z",
++ "refunded_at": "2026-11-04T13:00:00.000Z",
++ "expires_at": "2026-11-04T12:30:00.000Z",
++ "sandbox": false
++}
ddiff --git a/fixtures/billing.vibes-purchase-from-balance-request/invalid/fraction.json b/fixtures/billing.vibes-purchase-from-balance-request/invalid/fraction.json
nnew file mode 100644
iindex 00000000..b1a02320
---- /dev/null
++++ b/fixtures/billing.vibes-purchase-from-balance-request/invalid/fraction.json
@@@ -0,0 +1,4 @@
++{
++ "vibes": 12.5,
++ "idempotency_key": "vibes-buy-001"
++}
ddiff --git a/fixtures/billing.vibes-purchase-from-balance-request/invalid/string-vibes.json b/fixtures/billing.vibes-purchase-from-balance-request/invalid/string-vibes.json
nnew file mode 100644
iindex 00000000..1d8c7b34
---- /dev/null
++++ b/fixtures/billing.vibes-purchase-from-balance-request/invalid/string-vibes.json
@@@ -0,0 +1,4 @@
++{
++ "vibes": "1250",
++ "idempotency_key": "vibes-buy-001"
++}
ddiff --git a/fixtures/billing.vibes-purchase-from-balance-request/invalid/zero.json b/fixtures/billing.vibes-purchase-from-balance-request/invalid/zero.json
nnew file mode 100644
iindex 00000000..209a800b
---- /dev/null
++++ b/fixtures/billing.vibes-purchase-from-balance-request/invalid/zero.json
@@@ -0,0 +1,4 @@
++{
++ "vibes": 0,
++ "idempotency_key": "vibes-buy-001"
++}
ddiff --git a/fixtures/billing.vibes-purchase-from-balance-request/valid/pack.json b/fixtures/billing.vibes-purchase-from-balance-request/valid/pack.json
nnew file mode 100644
iindex 00000000..ee6279e1
---- /dev/null
++++ b/fixtures/billing.vibes-purchase-from-balance-request/valid/pack.json
@@@ -0,0 +1,4 @@
++{
++ "vibes": 1250,
++ "idempotency_key": "vibes-buy-001"
++}
ddiff --git a/fixtures/billing.vibes-wallet/invalid/app-subject.json b/fixtures/billing.vibes-wallet/invalid/app-subject.json
nnew file mode 100644
iindex 00000000..a51a4f92
---- /dev/null
++++ b/fixtures/billing.vibes-wallet/invalid/app-subject.json
@@@ -0,0 +1,10 @@
++{
++ "subject": {
++ "type": "app",
++ "id": "app_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "currency": "vibes-bits",
++ "credit": 0,
++ "payable": 0,
++ "bits_per_usd": 100
++}
ddiff --git a/fixtures/billing.vibes-wallet/invalid/hidden-balance.json b/fixtures/billing.vibes-wallet/invalid/hidden-balance.json
nnew file mode 100644
iindex 00000000..fa21ede7
---- /dev/null
++++ b/fixtures/billing.vibes-wallet/invalid/hidden-balance.json
@@@ -0,0 +1,11 @@
++{
++ "subject": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "currency": "vibes-bits",
++ "credit": 1250,
++ "payable": 0,
++ "bits_per_usd": 100,
++ "balance_nano_usd": "1"
++}
ddiff --git a/fixtures/billing.vibes-wallet/invalid/negative-credit.json b/fixtures/billing.vibes-wallet/invalid/negative-credit.json
nnew file mode 100644
iindex 00000000..00d9f44e
---- /dev/null
++++ b/fixtures/billing.vibes-wallet/invalid/negative-credit.json
@@@ -0,0 +1,10 @@
++{
++ "subject": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "currency": "vibes-bits",
++ "credit": -1,
++ "payable": 0,
++ "bits_per_usd": 100
++}
ddiff --git a/fixtures/billing.vibes-wallet/invalid/usd.json b/fixtures/billing.vibes-wallet/invalid/usd.json
nnew file mode 100644
iindex 00000000..487bf42d
---- /dev/null
++++ b/fixtures/billing.vibes-wallet/invalid/usd.json
@@@ -0,0 +1,10 @@
++{
++ "subject": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "currency": "usd-cents",
++ "credit": 1250,
++ "payable": 0,
++ "bits_per_usd": 100
++}
ddiff --git a/fixtures/billing.vibes-wallet/valid/person.json b/fixtures/billing.vibes-wallet/valid/person.json
nnew file mode 100644
iindex 00000000..f4285e0a
---- /dev/null
++++ b/fixtures/billing.vibes-wallet/valid/person.json
@@@ -0,0 +1,10 @@
++{
++ "subject": {
++ "type": "user",
++ "id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++ },
++ "currency": "vibes-bits",
++ "credit": 1250,
++ "payable": 340,
++ "bits_per_usd": 100
++}
ddiff --git a/fixtures/billing.vibes.charge.paid/invalid/float-amount.json b/fixtures/billing.vibes.charge.paid/invalid/float-amount.json
nnew file mode 100644
iindex 00000000..4fa02872
---- /dev/null
++++ b/fixtures/billing.vibes.charge.paid/invalid/float-amount.json
@@@ -0,0 +1,15 @@
++{
++ "id": "vch_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "app": "app_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "amount": 50.5,
++ "description": "Sticker pack: Neon cats",
++ "reference": "order_123",
++ "status": "paid",
++ "created_at": "2026-11-04T12:00:00.000Z",
++ "paid_at": "2026-11-04T12:01:00.000Z",
++ "refunded_at": null,
++ "expires_at": "2026-11-04T12:30:00.000Z",
++ "sandbox": false,
++ "payer_ref": "payer_7f3c9a12"
++}
ddiff --git a/fixtures/billing.vibes.charge.paid/invalid/payer-ref-email.json b/fixtures/billing.vibes.charge.paid/invalid/payer-ref-email.json
nnew file mode 100644
iindex 00000000..613baf8e
---- /dev/null
++++ b/fixtures/billing.vibes.charge.paid/invalid/payer-ref-email.json
@@@ -0,0 +1,15 @@
++{
++ "id": "vch_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "app": "app_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "amount": 50,
++ "description": "Sticker pack: Neon cats",
++ "reference": "order_123",
++ "status": "paid",
++ "created_at": "2026-11-04T12:00:00.000Z",
++ "paid_at": "2026-11-04T12:01:00.000Z",
++ "refunded_at": null,
++ "expires_at": "2026-11-04T12:30:00.000Z",
++ "sandbox": false,
++ "payer_ref": "[email protected]"
++}
ddiff --git a/fixtures/billing.vibes.charge.paid/invalid/person-id.json b/fixtures/billing.vibes.charge.paid/invalid/person-id.json
nnew file mode 100644
iindex 00000000..a3bda7b9
---- /dev/null
++++ b/fixtures/billing.vibes.charge.paid/invalid/person-id.json
@@@ -0,0 +1,16 @@
++{
++ "id": "vch_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "app": "app_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "amount": 50,
++ "description": "Sticker pack: Neon cats",
++ "reference": "order_123",
++ "status": "paid",
++ "created_at": "2026-11-04T12:00:00.000Z",
++ "paid_at": "2026-11-04T12:01:00.000Z",
++ "refunded_at": null,
++ "expires_at": "2026-11-04T12:30:00.000Z",
++ "sandbox": false,
++ "payer_ref": "payer_7f3c9a12",
++ "user_id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++}
ddiff --git a/fixtures/billing.vibes.charge.paid/valid/paid.json b/fixtures/billing.vibes.charge.paid/valid/paid.json
nnew file mode 100644
iindex 00000000..ad0a03f9
---- /dev/null
++++ b/fixtures/billing.vibes.charge.paid/valid/paid.json
@@@ -0,0 +1,15 @@
++{
++ "id": "vch_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "app": "app_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "amount": 50,
++ "description": "Sticker pack: Neon cats",
++ "reference": "order_123",
++ "status": "paid",
++ "created_at": "2026-11-04T12:00:00.000Z",
++ "paid_at": "2026-11-04T12:01:00.000Z",
++ "refunded_at": null,
++ "expires_at": "2026-11-04T12:30:00.000Z",
++ "sandbox": false,
++ "payer_ref": "payer_7f3c9a12"
++}
ddiff --git a/fixtures/billing.vibes.charge.paid/valid/sandbox.json b/fixtures/billing.vibes.charge.paid/valid/sandbox.json
nnew file mode 100644
iindex 00000000..7dfa65f1
---- /dev/null
++++ b/fixtures/billing.vibes.charge.paid/valid/sandbox.json
@@@ -0,0 +1,15 @@
++{
++ "id": "vch_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "app": "app_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "amount": 50,
++ "description": "Sticker pack: Neon cats",
++ "reference": "order_123",
++ "status": "paid",
++ "created_at": "2026-11-04T12:00:00.000Z",
++ "paid_at": "2026-11-04T12:01:00.000Z",
++ "refunded_at": null,
++ "expires_at": "2026-11-04T12:30:00.000Z",
++ "sandbox": true,
++ "payer_ref": "payer_7f3c9a12"
++}
ddiff --git a/fixtures/billing.vibes.charge.refunded/invalid/person-id.json b/fixtures/billing.vibes.charge.refunded/invalid/person-id.json
nnew file mode 100644
iindex 00000000..1f632afb
---- /dev/null
++++ b/fixtures/billing.vibes.charge.refunded/invalid/person-id.json
@@@ -0,0 +1,16 @@
++{
++ "id": "vch_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "app": "app_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "amount": 50,
++ "description": "Sticker pack: Neon cats",
++ "reference": "order_123",
++ "status": "refunded",
++ "created_at": "2026-11-04T12:00:00.000Z",
++ "paid_at": "2026-11-04T12:01:00.000Z",
++ "refunded_at": "2026-11-04T13:00:00.000Z",
++ "expires_at": "2026-11-04T12:30:00.000Z",
++ "sandbox": false,
++ "payer_ref": "payer_7f3c9a12",
++ "user_id": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
++}
ddiff --git a/fixtures/billing.vibes.charge.refunded/invalid/refunded-without-time.json b/fixtures/billing.vibes.charge.refunded/invalid/refunded-without-time.json
nnew file mode 100644
iindex 00000000..ad8b6dde
---- /dev/null
++++ b/fixtures/billing.vibes.charge.refunded/invalid/refunded-without-time.json
@@@ -0,0 +1,15 @@
++{
++ "id": "vch_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "app": "app_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "amount": 50,
++ "description": "Sticker pack: Neon cats",
++ "reference": "order_123",
++ "status": "refunded",
++ "created_at": "2026-11-04T12:00:00.000Z",
++ "paid_at": "2026-11-04T12:01:00.000Z",
++ "refunded_at": null,
++ "expires_at": "2026-11-04T12:30:00.000Z",
++ "sandbox": false,
++ "payer_ref": "payer_7f3c9a12"
++}
ddiff --git a/fixtures/billing.vibes.charge.refunded/valid/refunded.json b/fixtures/billing.vibes.charge.refunded/valid/refunded.json
nnew file mode 100644
iindex 00000000..98a00de3
---- /dev/null
++++ b/fixtures/billing.vibes.charge.refunded/valid/refunded.json
@@@ -0,0 +1,15 @@
++{
++ "id": "vch_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "app": "app_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
++ "amount": 50,
++ "description": "Sticker pack: Neon cats",
++ "reference": "order_123",
++ "status": "refunded",
++ "created_at": "2026-11-04T12:00:00.000Z",
++ "paid_at": "2026-11-04T12:01:00.000Z",
++ "refunded_at": "2026-11-04T13:00:00.000Z",
++ "expires_at": "2026-11-04T12:30:00.000Z",
++ "sandbox": false,
++ "payer_ref": "payer_7f3c9a12"
++}
ddiff --git a/generated/json-schema/bundle.json b/generated/json-schema/bundle.json
iindex c9358d6d..ee36bfde 100644
---- a/generated/json-schema/bundle.json
++++ b/generated/json-schema/bundle.json
@@@ -26246,17 +26246,8 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/billing/balance.v1.json",
"title": "BillingBalance",
-- "description": "billing.balance@1: a person's Vibes balances, GET /api/v1/balances/:subject on OpenVibe.Billing (also returned after a recycle). credit is spendable (bought or given back), payable is what others gave the person (the only balance that can be cashed out), pending_payouts is held for requested cashouts.",
++ "description": "billing.balance@1: a Balance account (D12), held by a person (owner user:<usr_id>) or a project (owner project:<prj_id>). GET /api/me/balance answers { personal, projects } of these. balance_nano_usd is the whole account; held_nano_usd is reserved by active holds; available_nano_usd is spendable (balance = held + available). Money is integer nano-USD as decimal strings (D1), never a float. sandbox marks a sandbox account. An id is permanent, so this contract still also describes the Vibes wallet body that billing.balance.read has answered: a person's Vibes (currency vibes-bits), not a Balance. Each branch is closed with additionalProperties: false; new code sends the Balance branch.",
"type": "object",
-- "required": [
-- "subject",
-- "currency",
-- "credit",
-- "payable",
-- "pending_payouts",
-- "payable_value_cents",
-- "bits_per_usd"
-- ],
"properties": {
"subject": {
"$ref": "../identity/subject-ref.v1.json"
@@@ -26280,6 +26271,606 @@
"bits_per_usd": {
"type": "number"
}
++ },
++ "$defs": {
++ "money-nano-usd": {
++ "type": "string",
++ "pattern": "^[0-9]{1,19}$",
++ "description": "Integer nano-USD (1e-9 USD), stored as a decimal string (D1), never a float."
++ },
++ "balance-owner": {
++ "oneOf": [
++ {
++ "type": "object",
++ "required": [
++ "type",
++ "id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {
++ "const": "user"
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ },
++ "description": "A person: usd_credit owned by user:<usr_id>."
++ },
++ {
++ "type": "object",
++ "required": [
++ "type",
++ "id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {
++ "const": "project"
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ },
++ "description": "A project: usd_credit owned by project:<prj_id>."
++ }
++ ]
++ }
++ },
++ "oneOf": [
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "owner",
++ "balance_nano_usd",
++ "held_nano_usd",
++ "available_nano_usd"
++ ],
++ "properties": {
++ "owner": {
++ "$ref": "#/$defs/balance-owner"
++ },
++ "balance_nano_usd": {
++ "$ref": "#/$defs/money-nano-usd"
++ },
++ "held_nano_usd": {
++ "$ref": "#/$defs/money-nano-usd"
++ },
++ "available_nano_usd": {
++ "$ref": "#/$defs/money-nano-usd"
++ },
++ "sandbox": {
++ "type": "boolean",
++ "default": false,
++ "description": "True for a sandbox account (D10): sandbox credit only, never real money."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "subject",
++ "currency",
++ "credit",
++ "payable",
++ "pending_payouts",
++ "payable_value_cents",
++ "bits_per_usd"
++ ],
++ "properties": {
++ "subject": {
++ "$ref": "../identity/subject-ref.v1.json"
++ },
++ "currency": {
++ "const": "vibes-bits"
++ },
++ "credit": {
++ "type": "integer"
++ },
++ "payable": {
++ "type": "integer"
++ },
++ "pending_payouts": {
++ "type": "integer"
++ },
++ "payable_value_cents": {
++ "type": "integer",
++ "description": "What payable is worth in US cents at today's rate."
++ },
++ "bits_per_usd": {
++ "type": "number"
++ }
++ }
++ }
++ ]
++ }
++ },
++ "billing.vibes-charge": {
++ "version": "1.0.0",
++ "owner": "billing",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/billing/vibes-charge.v1.json",
++ "title": "BillingVibesCharge",
++ "description": "billing.vibes-charge@1: one Vibes Pay charge (D12). An app creates it for a person to pay with Vibes; GET /api/v1/vibes/charges/:id answers it, and a replay of the same app reference returns the same charge. amount is a whole number of Vibes (never a fraction, never nano-USD). status moves pending -> paid -> refunded, or pending -> expired|cancelled. sandbox charges use test Vibes only, worth nothing. The paid and refunded webhook/event payloads carry the same fields plus an opaque payer_ref and no person id or email.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "app",
++ "project_id",
++ "amount",
++ "description",
++ "reference",
++ "status",
++ "pay_url",
++ "return_url",
++ "created_at",
++ "paid_at",
++ "refunded_at",
++ "expires_at",
++ "sandbox"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^vch_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "ULID charge id."
++ },
++ "app": {
++ "type": "string",
++ "pattern": "^app_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The app that created the charge (its app_ id)."
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The app's project; paid Vibes land in that project's Vibes wallet."
++ },
++ "amount": {
++ "type": "integer",
++ "minimum": 1,
++ "description": "Price in whole Vibes. A positive integer; Vibes have no fractions."
++ },
++ "description": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 140,
++ "description": "What the person is paying for."
++ },
++ "reference": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 128,
++ "description": "The app's own reference for this charge. It is the idempotency key: a replay with the same reference returns the same charge."
++ },
++ "status": {
++ "enum": [
++ "pending",
++ "paid",
++ "refunded",
++ "expired",
++ "cancelled"
++ ]
++ },
++ "pay_url": {
++ "type": "string",
++ "format": "uri",
++ "description": "OpenVibe's pay sheet for this charge, e.g. https://openvibe.network/pay/vch_..."
++ },
++ "return_url": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "uri",
++ "description": "Where the browser returns after paying; null when the app gave no return URL."
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "paid_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time",
++ "description": "When the Vibes moved, or null while the charge is unpaid."
++ },
++ "refunded_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time",
++ "description": "When the Vibes went back to the person, or null."
++ },
++ "expires_at": {
++ "type": "string",
++ "format": "date-time",
++ "description": "The pay sheet stops accepting payment after this instant."
++ },
++ "sandbox": {
++ "type": "boolean",
++ "description": "True for a sandbox app: sandbox Vibes only, worth nothing."
++ }
++ }
++ }
++ },
++ "billing.vibes-charge-create-request": {
++ "version": "1.0.0",
++ "owner": "billing",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/billing/vibes-charge-create-request.v1.json",
++ "title": "BillingVibesChargeCreateRequest",
++ "description": "billing.vibes-charge-create-request@1: body of POST /api/v1/vibes/charges, sent from the app's server with its app token (capability billing.vibes.charge). amount is whole Vibes. reference is the app's idempotency key: a replay with the same reference and body returns the same charge. return_url is optional. The app token decides the project, the sandbox environment and the owner-set limits.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "amount",
++ "description",
++ "reference"
++ ],
++ "properties": {
++ "amount": {
++ "type": "integer",
++ "minimum": 1,
++ "description": "Price in whole Vibes, a positive integer; never a fraction or a nano-USD amount."
++ },
++ "description": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 140,
++ "description": "What the person is paying for."
++ },
++ "reference": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 128,
++ "description": "The app's own reference; it is the idempotency key."
++ },
++ "return_url": {
++ "type": "string",
++ "format": "uri",
++ "description": "Where the browser returns after paying. Optional; the app may instead poll the charge or wait for the webhook."
++ }
++ }
++ }
++ },
++ "billing.vibes-charge-refund-request": {
++ "version": "1.0.0",
++ "owner": "billing",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/billing/vibes-charge-refund-request.v1.json",
++ "title": "BillingVibesChargeRefundRequest",
++ "description": "billing.vibes-charge-refund-request@1: the optional body of POST /api/v1/vibes/charges/:id/refund. A refund is always for the whole charge: the Vibes go back from the project's Vibes wallet to the person. reason is recorded on the refund. No amount may be given, because a partial refund is not a Vibes Pay refund.",
++ "type": "object",
++ "additionalProperties": false,
++ "properties": {
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 300,
++ "description": "Why the app refunded, for its own and the person's record."
++ }
++ }
++ }
++ },
++ "billing.vibes-wallet": {
++ "version": "1.0.0",
++ "owner": "billing",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/billing/vibes-wallet.v1.json",
++ "title": "BillingVibesWallet",
++ "description": "billing.vibes-wallet@1: a person's own Vibes, answered by GET /api/v1/vibes/me (capability billing.vibes.read) to an app the person approved. credit is Vibes to spend; payable is Vibes the person received as a creator and may cash out under the payout rules (never into Balance); bits_per_usd is today's rate. Vibes are whole integers; this is not a Balance and carries no nano-USD.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "subject",
++ "currency",
++ "credit",
++ "payable",
++ "bits_per_usd"
++ ],
++ "properties": {
++ "subject": {
++ "type": "object",
++ "required": [
++ "type",
++ "id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {
++ "const": "user"
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ },
++ "description": "The person the app reads for (the approved delegation). Only a user, never an app or a service."
++ },
++ "currency": {
++ "const": "vibes-bits"
++ },
++ "credit": {
++ "type": "integer",
++ "minimum": 0,
++ "description": "Vibes to spend."
++ },
++ "payable": {
++ "type": "integer",
++ "minimum": 0,
++ "description": "Vibes received as a creator, to cash out; never Balance."
++ },
++ "bits_per_usd": {
++ "type": "number",
++ "description": "Today's Vibes per USD rate, the same one a pack is priced with."
++ }
++ }
++ }
++ },
++ "billing.balance-transfer-request": {
++ "version": "1.0.0",
++ "owner": "billing",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/billing/balance-transfer-request.v1.json",
++ "title": "BillingBalanceTransferRequest",
++ "description": "billing.balance-transfer-request@1: body of POST /api/me/balance/transfers (D12). Moves amount_nano_usd from the signed-in person's Balance to a project they administer, into that project's Balance. Money is positive integer nano-USD as a decimal string (D1), never a float; the transfer is refused when the Balance or the person's project role is short. idempotency_key makes the transfer exactly once: a replay with the same key and body returns the first answer and moves nothing.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "to_project_id",
++ "amount_nano_usd",
++ "idempotency_key"
++ ],
++ "properties": {
++ "to_project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The project whose Balance receives the money; the person must administer it."
++ },
++ "amount_nano_usd": {
++ "type": "string",
++ "pattern": "^[1-9][0-9]{0,18}$",
++ "description": "A positive integer nano-USD (1e-9 USD) as a decimal string (D1), never a float."
++ },
++ "idempotency_key": {
++ "type": "string",
++ "pattern": "^[A-Za-z0-9._:-]{8,128}$",
++ "description": "Unique per mutation. A replay with the same key and body returns the first answer and changes nothing."
++ }
++ }
++ }
++ },
++ "billing.vibes-purchase-from-balance-request": {
++ "version": "1.0.0",
++ "owner": "billing",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/billing/vibes-purchase-from-balance-request.v1.json",
++ "title": "BillingVibesPurchaseFromBalanceRequest",
++ "description": "billing.vibes-purchase-from-balance-request@1: body of POST /api/me/vibes/from-balance (D12). Buys a whole number of Vibes with the signed-in person's Balance at the same Vibes price as a pack, charging exact nano-USD; Vibes -> Balance is never allowed. vibes is a whole integer (Vibes have no fractions); the money side is integer nano-USD (D1), never a float. idempotency_key makes the purchase exactly once.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "vibes",
++ "idempotency_key"
++ ],
++ "properties": {
++ "vibes": {
++ "type": "integer",
++ "minimum": 1,
++ "description": "How many whole Vibes to buy."
++ },
++ "idempotency_key": {
++ "type": "string",
++ "pattern": "^[A-Za-z0-9._:-]{8,128}$",
++ "description": "Unique per mutation. A replay with the same key and body returns the first answer and changes nothing."
++ }
++ }
++ }
++ },
++ "billing.vibes.charge.paid": {
++ "version": "1.0.0",
++ "owner": "billing",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/billing.vibes.charge.paid.v1.json",
++ "title": "BillingVibesChargePaidPayload",
++ "description": "billing.vibes.charge.paid v1 (OpenVibe.Billing, written when a Vibes Pay charge is paid): the app's charge and the opaque payer reference. This is also the Vibes Pay webhook body (HMAC-SHA256 with the app's webhook secret, retried with backoff). It deliberately carries no person id and no email, only payer_ref, stable per app, so an app can recognise a returning payer without receiving their identity. amount is whole Vibes. Envelope: subject { type: vibes-charge, id: vch_... }, actor service:billing.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "app",
++ "project_id",
++ "amount",
++ "description",
++ "reference",
++ "status",
++ "created_at",
++ "paid_at",
++ "refunded_at",
++ "expires_at",
++ "sandbox",
++ "payer_ref"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^vch_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The charge; GET /api/v1/vibes/charges/:id returns its current state."
++ },
++ "app": {
++ "type": "string",
++ "pattern": "^app_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The app that created the charge."
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The app's project, whose Vibes wallet received the charge."
++ },
++ "amount": {
++ "type": "integer",
++ "minimum": 1,
++ "description": "The whole number of Vibes paid."
++ },
++ "description": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 140
++ },
++ "reference": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 128,
++ "description": "The app's reference, as given when the charge was created."
++ },
++ "status": {
++ "const": "paid"
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "paid_at": {
++ "type": "string",
++ "format": "date-time",
++ "description": "When the Vibes moved from the person to the project."
++ },
++ "refunded_at": {
++ "type": "null",
++ "description": "Null: a paid charge has not been refunded."
++ },
++ "expires_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "sandbox": {
++ "type": "boolean",
++ "description": "True for a sandbox app: sandbox Vibes only, worth nothing."
++ },
++ "payer_ref": {
++ "type": "string",
++ "minLength": 8,
++ "maxLength": 128,
++ "pattern": "^[A-Za-z0-9._:-]+$",
++ "description": "Opaque reference for the paying person, stable per app. Never the person's id, email or any other identity; an app uses it to recognise a returning payer."
++ }
++ }
++ }
++ },
++ "billing.vibes.charge.refunded": {
++ "version": "1.0.0",
++ "owner": "billing",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/billing.vibes.charge.refunded.v1.json",
++ "title": "BillingVibesChargeRefundedPayload",
++ "description": "billing.vibes.charge.refunded v1 (OpenVibe.Billing, written when a Vibes Pay charge is refunded): the app's charge and the opaque payer reference. This is also the Vibes Pay webhook body (HMAC-SHA256 with the app's webhook secret, retried with backoff). It deliberately carries no person id and no email, only payer_ref, stable per app, so an app can recognise a returning payer without receiving their identity. amount is whole Vibes. Envelope: subject { type: vibes-charge, id: vch_... }, actor service:billing.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "app",
++ "project_id",
++ "amount",
++ "description",
++ "reference",
++ "status",
++ "created_at",
++ "paid_at",
++ "refunded_at",
++ "expires_at",
++ "sandbox",
++ "payer_ref"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^vch_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The charge; GET /api/v1/vibes/charges/:id returns its current state."
++ },
++ "app": {
++ "type": "string",
++ "pattern": "^app_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The app that created the charge."
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The app's project, whose Vibes wallet received the charge."
++ },
++ "amount": {
++ "type": "integer",
++ "minimum": 1,
++ "description": "The whole number of Vibes paid."
++ },
++ "description": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 140
++ },
++ "reference": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 128,
++ "description": "The app's reference, as given when the charge was created."
++ },
++ "status": {
++ "const": "refunded"
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "paid_at": {
++ "type": "string",
++ "format": "date-time",
++ "description": "When the Vibes moved from the person to the project."
++ },
++ "refunded_at": {
++ "type": "string",
++ "format": "date-time",
++ "description": "When the Vibes went back to the person."
++ },
++ "expires_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "sandbox": {
++ "type": "boolean",
++ "description": "True for a sandbox app: sandbox Vibes only, worth nothing."
++ },
++ "payer_ref": {
++ "type": "string",
++ "minLength": 8,
++ "maxLength": 128,
++ "pattern": "^[A-Za-z0-9._:-]+$",
++ "description": "Opaque reference for the paying person, stable per app. Never the person's id, email or any other identity; an app uses it to recognise a returning payer."
++ }
}
}
},
@@@ -28052,17 +28643,45 @@
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://openvibe.network/contracts/billing/credit-hold-create-request.v1.json",
"title": "BillingCreditHoldCreateRequest",
-- "description": "billing.credit-hold-create-request@1: body of POST /internal/credits/holds. Reserves inference credit up to max_nano_usd against a (subject, project_id) account, priced at the given immutable rate version.",
++ "description": "billing.credit-hold-create-request@1: body of POST /internal/credits/holds. Reserves inference credit up to max_nano_usd against a Balance account, priced at the given immutable rate version. With project_id the hold spends that project's Balance; without it the hold is personal and spends the subject's own Balance (D12). An app subject always names its project.",
"type": "object",
"required": [
"idempotency_key",
"subject",
-- "project_id",
"max_nano_usd",
"rate_version",
"request_fingerprint"
],
"additionalProperties": false,
++ "allOf": [
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "subject": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "const": "app"
++ }
++ },
++ "required": [
++ "type"
++ ]
++ }
++ },
++ "required": [
++ "subject"
++ ]
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "project_id"
++ ]
++ }
++ }
++ ],
"$defs": {
"money-nano-usd": {
"type": "string",
@@@ -28080,7 +28699,8 @@
},
"project_id": {
"type": "string",
-- "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The project whose Balance the hold spends. Optional: without it the hold is personal, on the subject's own Balance. Required when the subject is an app."
},
"credential_id": {
"type": [
ddiff --git a/generated/openapi/actor.json b/generated/openapi/actor.json
iindex 305cce79..259d13d8 100644
---- a/generated/openapi/actor.json
++++ b/generated/openapi/actor.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Actor API",
-- "version": "0.288.0",
-- "description": "Routes of OpenVibe.Actor as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Actor as the OpenVibe contracts describe them (openvibe-contracts 0.289.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/ai.json b/generated/openapi/ai.json
iindex 216661d7..30b89517 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.288.0",
-- "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.AI as the OpenVibe contracts describe them (openvibe-contracts 0.289.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/bench.json b/generated/openapi/bench.json
iindex a521ad68..e23868ab 100644
---- a/generated/openapi/bench.json
++++ b/generated/openapi/bench.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Bench API",
-- "version": "0.288.0",
-- "description": "Routes of OpenVibe.Bench as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Bench as the OpenVibe contracts describe them (openvibe-contracts 0.289.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 8614b049..0dbf45a8 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.288.0",
-- "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Billing as the OpenVibe contracts describe them (openvibe-contracts 0.289.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"
@@@ -1933,6 +1933,279 @@
}
}
},
++ "/api/v1/vibes/charges": {
++ "post": {
++ "operationId": "post_api_v1_vibes_charges",
++ "summary": "Sell something for Vibes as a developer app (Vibes Pay, D12).",
++ "description": "**billing.vibes.charge** (public): Sell something for Vibes as a developer app (Vibes Pay, D12). POST /api/v1/vibes/charges creates a charge from the app's token (billing.vibes-charge-create-request@1; idempotent on the app's reference) and answers billing.vibes-charge@1 with a pay_url; GET /api/v1/vibes/charges/:id reads it; POST /api/v1/vibes/charges/:id/refund returns the Vibes from the project's Vibes wallet to the person (billing.vibes-charge-refund-request@1). The person pays on OpenVibe's pay sheet; Billing moves the Vibes atomically and once to the app's project Vibes wallet, then POSTs a signed webhook (HMAC-SHA256 with the app's webhook secret, retried with backoff) whose body is billing.vibes.charge.paid@1 or billing.vibes.charge.refunded@1 — no person id or email, only an opaque payer_ref stable per app. A sandbox app charges sandbox Vibes only (test grants, worth nothing). The owner's billing.vibes settings cap the amount, the daily per-person spend and whether Vibes Pay is on.",
++ "tags": [
++ "billing.vibes"
++ ],
++ "x-openvibe-capabilities": [
++ "billing.vibes.charge"
++ ],
++ "x-openvibe-visibility": [
++ "public"
++ ],
++ "x-openvibe-quota-class": "billing-vibes-write",
++ "security": [
++ {
++ "openvibeToken": [
++ "billing.vibes.charge"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/billing.vibes-charge-create-request.v1"
++ }
++ }
++ }
++ },
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/billing.vibes-charge.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/vibes/charges/{id}": {
++ "get": {
++ "operationId": "get_api_v1_vibes_charges_id",
++ "summary": "Sell something for Vibes as a developer app (Vibes Pay, D12).",
++ "description": "**billing.vibes.charge** (public): Sell something for Vibes as a developer app (Vibes Pay, D12). POST /api/v1/vibes/charges creates a charge from the app's token (billing.vibes-charge-create-request@1; idempotent on the app's reference) and answers billing.vibes-charge@1 with a pay_url; GET /api/v1/vibes/charges/:id reads it; POST /api/v1/vibes/charges/:id/refund returns the Vibes from the project's Vibes wallet to the person (billing.vibes-charge-refund-request@1). The person pays on OpenVibe's pay sheet; Billing moves the Vibes atomically and once to the app's project Vibes wallet, then POSTs a signed webhook (HMAC-SHA256 with the app's webhook secret, retried with backoff) whose body is billing.vibes.charge.paid@1 or billing.vibes.charge.refunded@1 — no person id or email, only an opaque payer_ref stable per app. A sandbox app charges sandbox Vibes only (test grants, worth nothing). The owner's billing.vibes settings cap the amount, the daily per-person spend and whether Vibes Pay is on.",
++ "tags": [
++ "billing.vibes"
++ ],
++ "x-openvibe-capabilities": [
++ "billing.vibes.charge"
++ ],
++ "x-openvibe-visibility": [
++ "public"
++ ],
++ "x-openvibe-quota-class": "billing-vibes-write",
++ "security": [
++ {
++ "openvibeToken": [
++ "billing.vibes.charge"
++ ]
++ }
++ ],
++ "x-openvibe-input": {
++ "$ref": "#/components/schemas/billing.vibes-charge-create-request.v1"
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ },
++ {
++ "name": "amount",
++ "in": "query",
++ "required": true,
++ "schema": {
++ "type": "integer",
++ "minimum": 1,
++ "description": "Price in whole Vibes, a positive integer; never a fraction or a nano-USD amount."
++ },
++ "description": "Price in whole Vibes, a positive integer; never a fraction or a nano-USD amount."
++ },
++ {
++ "name": "description",
++ "in": "query",
++ "required": true,
++ "schema": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 140,
++ "description": "What the person is paying for."
++ },
++ "description": "What the person is paying for."
++ },
++ {
++ "name": "reference",
++ "in": "query",
++ "required": true,
++ "schema": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 128,
++ "description": "The app's own reference; it is the idempotency key."
++ },
++ "description": "The app's own reference; it is the idempotency key."
++ },
++ {
++ "name": "return_url",
++ "in": "query",
++ "required": false,
++ "schema": {
++ "type": "string",
++ "format": "uri",
++ "description": "Where the browser returns after paying. Optional; the app may instead poll the charge or wait for the webhook."
++ },
++ "description": "Where the browser returns after paying. Optional; the app may instead poll the charge or wait for the webhook."
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/billing.vibes-charge.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/vibes/charges/{id}/refund": {
++ "post": {
++ "operationId": "post_api_v1_vibes_charges_id_refund",
++ "summary": "Sell something for Vibes as a developer app (Vibes Pay, D12).",
++ "description": "**billing.vibes.charge** (public): Sell something for Vibes as a developer app (Vibes Pay, D12). POST /api/v1/vibes/charges creates a charge from the app's token (billing.vibes-charge-create-request@1; idempotent on the app's reference) and answers billing.vibes-charge@1 with a pay_url; GET /api/v1/vibes/charges/:id reads it; POST /api/v1/vibes/charges/:id/refund returns the Vibes from the project's Vibes wallet to the person (billing.vibes-charge-refund-request@1). The person pays on OpenVibe's pay sheet; Billing moves the Vibes atomically and once to the app's project Vibes wallet, then POSTs a signed webhook (HMAC-SHA256 with the app's webhook secret, retried with backoff) whose body is billing.vibes.charge.paid@1 or billing.vibes.charge.refunded@1 — no person id or email, only an opaque payer_ref stable per app. A sandbox app charges sandbox Vibes only (test grants, worth nothing). The owner's billing.vibes settings cap the amount, the daily per-person spend and whether Vibes Pay is on.",
++ "tags": [
++ "billing.vibes"
++ ],
++ "x-openvibe-capabilities": [
++ "billing.vibes.charge"
++ ],
++ "x-openvibe-visibility": [
++ "public"
++ ],
++ "x-openvibe-quota-class": "billing-vibes-write",
++ "security": [
++ {
++ "openvibeToken": [
++ "billing.vibes.charge"
++ ]
++ }
++ ],
++ "requestBody": {
++ "required": false,
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/billing.vibes-charge-create-request.v1"
++ }
++ }
++ }
++ },
++ "parameters": [
++ {
++ "name": "id",
++ "in": "path",
++ "required": true,
++ "schema": {
++ "type": "string"
++ }
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/billing.vibes-charge.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
++ "/api/v1/vibes/me": {
++ "get": {
++ "operationId": "get_api_v1_vibes_me",
++ "summary": "Read a person's own Vibes as an app the person approved (D12): GET /api/v1/vibes/me with a person-delegated app token answers billing.vibes-wallet@1 — the Vibes to spend, the creator payable that can ",
++ "description": "**billing.vibes.read** (public): Read a person's own Vibes as an app the person approved (D12): GET /api/v1/vibes/me with a person-delegated app token answers billing.vibes-wallet@1 — the Vibes to spend, the creator payable that can be cashed out (never into Balance) and today's bits_per_usd. The app sees only the person who approved it, never another person's Vibes.",
++ "tags": [
++ "billing.vibes"
++ ],
++ "x-openvibe-capabilities": [
++ "billing.vibes.read"
++ ],
++ "x-openvibe-visibility": [
++ "public"
++ ],
++ "x-openvibe-quota-class": "billing-vibes-read",
++ "security": [
++ {
++ "openvibeToken": [
++ "billing.vibes.read"
++ ]
++ }
++ ],
++ "responses": {
++ "2XX": {
++ "description": "Success",
++ "content": {
++ "application/json": {
++ "schema": {
++ "$ref": "#/components/schemas/billing.vibes-wallet.v1"
++ }
++ }
++ }
++ },
++ "default": {
++ "description": "An error (RFC 9457 problem details)",
++ "content": {
++ "application/problem+json": {
++ "schema": {
++ "$ref": "#/components/schemas/errors.problem.v1"
++ }
++ }
++ }
++ }
++ }
++ }
++ },
"/internal/config": {
"get": {
"operationId": "get_internal_config",
@@@ -3228,17 +3501,8 @@
},
"billing.balance.v1": {
"title": "BillingBalance",
-- "description": "billing.balance@1: a person's Vibes balances, GET /api/v1/balances/:subject on OpenVibe.Billing (also returned after a recycle). credit is spendable (bought or given back), payable is what others gave the person (the only balance that can be cashed out), pending_payouts is held for requested cashouts.",
++ "description": "billing.balance@1: a Balance account (D12), held by a person (owner user:<usr_id>) or a project (owner project:<prj_id>). GET /api/me/balance answers { personal, projects } of these. balance_nano_usd is the whole account; held_nano_usd is reserved by active holds; available_nano_usd is spendable (balance = held + available). Money is integer nano-USD as decimal strings (D1), never a float. sandbox marks a sandbox account. An id is permanent, so this contract still also describes the Vibes wallet body that billing.balance.read has answered: a person's Vibes (currency vibes-bits), not a Balance. Each branch is closed with additionalProperties: false; new code sends the Balance branch.",
"type": "object",
-- "required": [
-- "subject",
-- "currency",
-- "credit",
-- "payable",
-- "pending_payouts",
-- "payable_value_cents",
-- "bits_per_usd"
-- ],
"properties": {
"subject": {
"$ref": "#/components/schemas/identity.subject-ref.v1"
@@@ -3262,7 +3526,122 @@
"bits_per_usd": {
"type": "number"
}
-- }
++ },
++ "$defs": {
++ "money-nano-usd": {
++ "type": "string",
++ "pattern": "^[0-9]{1,19}$",
++ "description": "Integer nano-USD (1e-9 USD), stored as a decimal string (D1), never a float."
++ },
++ "balance-owner": {
++ "oneOf": [
++ {
++ "type": "object",
++ "required": [
++ "type",
++ "id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {
++ "const": "user"
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ },
++ "description": "A person: usd_credit owned by user:<usr_id>."
++ },
++ {
++ "type": "object",
++ "required": [
++ "type",
++ "id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {
++ "const": "project"
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ },
++ "description": "A project: usd_credit owned by project:<prj_id>."
++ }
++ ]
++ }
++ },
++ "oneOf": [
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "owner",
++ "balance_nano_usd",
++ "held_nano_usd",
++ "available_nano_usd"
++ ],
++ "properties": {
++ "owner": {
++ "$ref": "#/components/schemas/billing.balance.v1/$defs/balance-owner"
++ },
++ "balance_nano_usd": {
++ "$ref": "#/components/schemas/billing.balance.v1/$defs/money-nano-usd"
++ },
++ "held_nano_usd": {
++ "$ref": "#/components/schemas/billing.balance.v1/$defs/money-nano-usd"
++ },
++ "available_nano_usd": {
++ "$ref": "#/components/schemas/billing.balance.v1/$defs/money-nano-usd"
++ },
++ "sandbox": {
++ "type": "boolean",
++ "default": false,
++ "description": "True for a sandbox account (D10): sandbox credit only, never real money."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "subject",
++ "currency",
++ "credit",
++ "payable",
++ "pending_payouts",
++ "payable_value_cents",
++ "bits_per_usd"
++ ],
++ "properties": {
++ "subject": {
++ "$ref": "#/components/schemas/identity.subject-ref.v1"
++ },
++ "currency": {
++ "const": "vibes-bits"
++ },
++ "credit": {
++ "type": "integer"
++ },
++ "payable": {
++ "type": "integer"
++ },
++ "pending_payouts": {
++ "type": "integer"
++ },
++ "payable_value_cents": {
++ "type": "integer",
++ "description": "What payable is worth in US cents at today's rate."
++ },
++ "bits_per_usd": {
++ "type": "number"
++ }
++ }
++ }
++ ]
},
"billing.cashout-decision.v1": {
"title": "BillingCashoutDecision",
@@@ -3534,17 +3913,45 @@
},
"billing.credit-hold-create-request.v1": {
"title": "BillingCreditHoldCreateRequest",
-- "description": "billing.credit-hold-create-request@1: body of POST /internal/credits/holds. Reserves inference credit up to max_nano_usd against a (subject, project_id) account, priced at the given immutable rate version.",
++ "description": "billing.credit-hold-create-request@1: body of POST /internal/credits/holds. Reserves inference credit up to max_nano_usd against a Balance account, priced at the given immutable rate version. With project_id the hold spends that project's Balance; without it the hold is personal and spends the subject's own Balance (D12). An app subject always names its project.",
"type": "object",
"required": [
"idempotency_key",
"subject",
-- "project_id",
"max_nano_usd",
"rate_version",
"request_fingerprint"
],
"additionalProperties": false,
++ "allOf": [
++ {
++ "if": {
++ "type": "object",
++ "properties": {
++ "subject": {
++ "type": "object",
++ "properties": {
++ "type": {
++ "const": "app"
++ }
++ },
++ "required": [
++ "type"
++ ]
++ }
++ },
++ "required": [
++ "subject"
++ ]
++ },
++ "then": {
++ "type": "object",
++ "required": [
++ "project_id"
++ ]
++ }
++ }
++ ],
"$defs": {
"money-nano-usd": {
"type": "string",
@@@ -3562,7 +3969,8 @@
},
"project_id": {
"type": "string",
-- "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$"
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The project whose Balance the hold spends. Optional: without it the hold is personal, on the subject's own Balance. Required when the subject is an app."
},
"credential_id": {
"type": [
@@@ -4985,6 +5393,198 @@
}
}
},
++ "billing.vibes-charge-create-request.v1": {
++ "title": "BillingVibesChargeCreateRequest",
++ "description": "billing.vibes-charge-create-request@1: body of POST /api/v1/vibes/charges, sent from the app's server with its app token (capability billing.vibes.charge). amount is whole Vibes. reference is the app's idempotency key: a replay with the same reference and body returns the same charge. return_url is optional. The app token decides the project, the sandbox environment and the owner-set limits.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "amount",
++ "description",
++ "reference"
++ ],
++ "properties": {
++ "amount": {
++ "type": "integer",
++ "minimum": 1,
++ "description": "Price in whole Vibes, a positive integer; never a fraction or a nano-USD amount."
++ },
++ "description": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 140,
++ "description": "What the person is paying for."
++ },
++ "reference": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 128,
++ "description": "The app's own reference; it is the idempotency key."
++ },
++ "return_url": {
++ "type": "string",
++ "format": "uri",
++ "description": "Where the browser returns after paying. Optional; the app may instead poll the charge or wait for the webhook."
++ }
++ }
++ },
++ "billing.vibes-charge.v1": {
++ "title": "BillingVibesCharge",
++ "description": "billing.vibes-charge@1: one Vibes Pay charge (D12). An app creates it for a person to pay with Vibes; GET /api/v1/vibes/charges/:id answers it, and a replay of the same app reference returns the same charge. amount is a whole number of Vibes (never a fraction, never nano-USD). status moves pending -> paid -> refunded, or pending -> expired|cancelled. sandbox charges use test Vibes only, worth nothing. The paid and refunded webhook/event payloads carry the same fields plus an opaque payer_ref and no person id or email.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "id",
++ "app",
++ "project_id",
++ "amount",
++ "description",
++ "reference",
++ "status",
++ "pay_url",
++ "return_url",
++ "created_at",
++ "paid_at",
++ "refunded_at",
++ "expires_at",
++ "sandbox"
++ ],
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^vch_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "ULID charge id."
++ },
++ "app": {
++ "type": "string",
++ "pattern": "^app_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The app that created the charge (its app_ id)."
++ },
++ "project_id": {
++ "type": "string",
++ "pattern": "^prj_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The app's project; paid Vibes land in that project's Vibes wallet."
++ },
++ "amount": {
++ "type": "integer",
++ "minimum": 1,
++ "description": "Price in whole Vibes. A positive integer; Vibes have no fractions."
++ },
++ "description": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 140,
++ "description": "What the person is paying for."
++ },
++ "reference": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 128,
++ "description": "The app's own reference for this charge. It is the idempotency key: a replay with the same reference returns the same charge."
++ },
++ "status": {
++ "enum": [
++ "pending",
++ "paid",
++ "refunded",
++ "expired",
++ "cancelled"
++ ]
++ },
++ "pay_url": {
++ "type": "string",
++ "format": "uri",
++ "description": "OpenVibe's pay sheet for this charge, e.g. https://openvibe.network/pay/vch_..."
++ },
++ "return_url": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "uri",
++ "description": "Where the browser returns after paying; null when the app gave no return URL."
++ },
++ "created_at": {
++ "type": "string",
++ "format": "date-time"
++ },
++ "paid_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time",
++ "description": "When the Vibes moved, or null while the charge is unpaid."
++ },
++ "refunded_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time",
++ "description": "When the Vibes went back to the person, or null."
++ },
++ "expires_at": {
++ "type": "string",
++ "format": "date-time",
++ "description": "The pay sheet stops accepting payment after this instant."
++ },
++ "sandbox": {
++ "type": "boolean",
++ "description": "True for a sandbox app: sandbox Vibes only, worth nothing."
++ }
++ }
++ },
++ "billing.vibes-wallet.v1": {
++ "title": "BillingVibesWallet",
++ "description": "billing.vibes-wallet@1: a person's own Vibes, answered by GET /api/v1/vibes/me (capability billing.vibes.read) to an app the person approved. credit is Vibes to spend; payable is Vibes the person received as a creator and may cash out under the payout rules (never into Balance); bits_per_usd is today's rate. Vibes are whole integers; this is not a Balance and carries no nano-USD.",
++ "type": "object",
++ "additionalProperties": false,
++ "required": [
++ "subject",
++ "currency",
++ "credit",
++ "payable",
++ "bits_per_usd"
++ ],
++ "properties": {
++ "subject": {
++ "type": "object",
++ "required": [
++ "type",
++ "id"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {
++ "const": "user"
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^usr_[0-9A-HJKMNP-TV-Z]{26}$"
++ }
++ },
++ "description": "The person the app reads for (the approved delegation). Only a user, never an app or a service."
++ },
++ "currency": {
++ "const": "vibes-bits"
++ },
++ "credit": {
++ "type": "integer",
++ "minimum": 0,
++ "description": "Vibes to spend."
++ },
++ "payable": {
++ "type": "integer",
++ "minimum": 0,
++ "description": "Vibes received as a creator, to cash out; never Balance."
++ },
++ "bits_per_usd": {
++ "type": "number",
++ "description": "Today's Vibes per USD rate, the same one a pack is priced with."
++ }
++ }
++ },
"common.config-change.v1": {
"title": "ConfigChange",
"description": "common.config-change@1 (AI Exchange D11): body of POST /internal/config/:namespace on a service's openvibe-shared/config admin routes. It proposes a new revision of the namespace and activates it (the store validates against the namespace schema, applies env pins and calls onActivate). `merge: true` starts from the active values; otherwise `values` is the whole new set. `unset` removes keys. A secret-class key's value is never sent here: it is 422 config.secret_write_only; use common.config-secret-put@1.",
ddiff --git a/generated/openapi/blog.json b/generated/openapi/blog.json
iindex 417adc05..0f8a10d7 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.288.0",
-- "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Blog as the OpenVibe contracts describe them (openvibe-contracts 0.289.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/bot.json b/generated/openapi/bot.json
iindex e26f6dd8..ac4c6731 100644
---- a/generated/openapi/bot.json
++++ b/generated/openapi/bot.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Bot API",
-- "version": "0.288.0",
-- "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Bot as the OpenVibe contracts describe them (openvibe-contracts 0.289.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 58c5246d..a309d5c3 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.288.0",
-- "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Chat as the OpenVibe contracts describe them (openvibe-contracts 0.289.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 238c32c4..f6a748b6 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.288.0",
-- "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Codes as the OpenVibe contracts describe them (openvibe-contracts 0.289.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 665e590c..bb9ce7f2 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.288.0",
-- "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Community as the OpenVibe contracts describe them (openvibe-contracts 0.289.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 dd13ba29..fa78dbce 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.288.0",
-- "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Coupons as the OpenVibe contracts describe them (openvibe-contracts 0.289.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 6fc80c5f..3884a0af 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.288.0",
-- "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Deals as the OpenVibe contracts describe them (openvibe-contracts 0.289.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/domains.json b/generated/openapi/domains.json
iindex 17015689..c0ccb4b7 100644
---- a/generated/openapi/domains.json
++++ b/generated/openapi/domains.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Domains API",
-- "version": "0.288.0",
-- "description": "Routes of OpenVibe.Domains as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Domains as the OpenVibe contracts describe them (openvibe-contracts 0.289.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 1750e182..2004f4eb 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.288.0",
-- "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Events as the OpenVibe contracts describe them (openvibe-contracts 0.289.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/food.json b/generated/openapi/food.json
iindex 1b088019..75bc8e27 100644
---- a/generated/openapi/food.json
++++ b/generated/openapi/food.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Food API",
-- "version": "0.288.0",
-- "description": "Routes of OpenVibe.Food as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Food as the OpenVibe contracts describe them (openvibe-contracts 0.289.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 461f8816..81efabfb 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.288.0",
-- "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Games as the OpenVibe contracts describe them (openvibe-contracts 0.289.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 d20afd0e..e65be8d3 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.288.0",
-- "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Host as the OpenVibe contracts describe them (openvibe-contracts 0.289.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 63636ec7..40ad2510 100644
---- a/generated/openapi/index.json
++++ b/generated/openapi/index.json
@@@ -26,8 +26,8 @@
"service": "billing",
"name": "OpenVibe.Billing",
"origin": "https://billing.openvibe.network",
-- "operations": 43,
-- "capabilities": 17
++ "operations": 47,
++ "capabilities": 19
},
{
"service": "blog",
ddiff --git a/generated/openapi/inventory.json b/generated/openapi/inventory.json
iindex 8384ea8f..e2c88b70 100644
---- a/generated/openapi/inventory.json
++++ b/generated/openapi/inventory.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Inventory API",
-- "version": "0.288.0",
-- "description": "Routes of OpenVibe.Inventory as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Inventory as the OpenVibe contracts describe them (openvibe-contracts 0.289.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/live.json b/generated/openapi/live.json
iindex 36f28b4e..0296cbd0 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.288.0",
-- "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Live as the OpenVibe contracts describe them (openvibe-contracts 0.289.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-hub.json b/generated/openapi/media-hub.json
iindex 69287f3c..ecfa7667 100644
---- a/generated/openapi/media-hub.json
++++ b/generated/openapi/media-hub.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.MediaHub API",
-- "version": "0.288.0",
-- "description": "Routes of OpenVibe.MediaHub as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.MediaHub as the OpenVibe contracts describe them (openvibe-contracts 0.289.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 2e98f570..e6fff526 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.288.0",
-- "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Media as the OpenVibe contracts describe them (openvibe-contracts 0.289.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 2454bc6f..15f703ec 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.288.0",
-- "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Network as the OpenVibe contracts describe them (openvibe-contracts 0.289.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 71d21296..836bbc3d 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.288.0",
-- "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.News as the OpenVibe contracts describe them (openvibe-contracts 0.289.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 524f6728..d2b4a8f0 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": "OpenRestream API",
-- "version": "0.288.0",
-- "description": "Routes of OpenRestream as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenRestream as the OpenVibe contracts describe them (openvibe-contracts 0.289.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/play.json b/generated/openapi/play.json
iindex 7a790588..f22adb46 100644
---- a/generated/openapi/play.json
++++ b/generated/openapi/play.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Play API",
-- "version": "0.288.0",
-- "description": "Routes of OpenVibe.Play as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Play as the OpenVibe contracts describe them (openvibe-contracts 0.289.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/quest.json b/generated/openapi/quest.json
iindex 65f0efa4..bc0058a5 100644
---- a/generated/openapi/quest.json
++++ b/generated/openapi/quest.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Quest API",
-- "version": "0.288.0",
-- "description": "Routes of OpenVibe.Quest as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Quest as the OpenVibe contracts describe them (openvibe-contracts 0.289.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/report.json b/generated/openapi/report.json
iindex 7667b52f..1b198625 100644
---- a/generated/openapi/report.json
++++ b/generated/openapi/report.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Report API",
-- "version": "0.288.0",
-- "description": "Routes of OpenVibe.Report as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Report as the OpenVibe contracts describe them (openvibe-contracts 0.289.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 ad44ef54..7e4b8373 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.288.0",
-- "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Reviews as the OpenVibe contracts describe them (openvibe-contracts 0.289.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/run.json b/generated/openapi/run.json
iindex 9ec3b334..42cf93e0 100644
---- a/generated/openapi/run.json
++++ b/generated/openapi/run.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Run API",
-- "version": "0.288.0",
-- "description": "Routes of OpenVibe.Run as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Run as the OpenVibe contracts describe them (openvibe-contracts 0.289.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 d7dd7d39..80595606 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.288.0",
-- "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Search as the OpenVibe contracts describe them (openvibe-contracts 0.289.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/services.json b/generated/openapi/services.json
iindex 485bb9a8..bde713d7 100644
---- a/generated/openapi/services.json
++++ b/generated/openapi/services.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Services API",
-- "version": "0.288.0",
-- "description": "Routes of OpenVibe.Services as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Services as the OpenVibe contracts describe them (openvibe-contracts 0.289.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 41a2d23e..878a4f98 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.288.0",
-- "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Sources as the OpenVibe contracts describe them (openvibe-contracts 0.289.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 4a18a24f..30164f19 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.288.0",
-- "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Space as the OpenVibe contracts describe them (openvibe-contracts 0.289.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 d7024c69..c25f860c 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.288.0",
-- "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Tips as the OpenVibe contracts describe them (openvibe-contracts 0.289.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/today.json b/generated/openapi/today.json
iindex a18c1740..de91d1d4 100644
---- a/generated/openapi/today.json
++++ b/generated/openapi/today.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Today API",
-- "version": "0.288.0",
-- "description": "Routes of OpenVibe.Today as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Today as the OpenVibe contracts describe them (openvibe-contracts 0.289.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 33b81238..ba57f234 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.288.0",
-- "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Tools as the OpenVibe contracts describe them (openvibe-contracts 0.289.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 179fdcba..36245dfc 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.288.0",
-- "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Trade as the OpenVibe contracts describe them (openvibe-contracts 0.289.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 07ddf1a0..a73f2c17 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.288.0",
-- "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.VIP as the OpenVibe contracts describe them (openvibe-contracts 0.289.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/vision.json b/generated/openapi/vision.json
iindex b6bf82c3..fbbfab6a 100644
---- a/generated/openapi/vision.json
++++ b/generated/openapi/vision.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Vision API",
-- "version": "0.288.0",
-- "description": "Routes of OpenVibe.Vision as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Vision as the OpenVibe contracts describe them (openvibe-contracts 0.289.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/watch.json b/generated/openapi/watch.json
iindex 6cf536ce..97d2ff55 100644
---- a/generated/openapi/watch.json
++++ b/generated/openapi/watch.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Watch API",
-- "version": "0.288.0",
-- "description": "Routes of OpenVibe.Watch as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Watch as the OpenVibe contracts describe them (openvibe-contracts 0.289.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 0c2fec4c..81324c9a 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.288.0",
-- "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Wiki as the OpenVibe contracts describe them (openvibe-contracts 0.289.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/work.json b/generated/openapi/work.json
iindex 13700874..a93c7120 100644
---- a/generated/openapi/work.json
++++ b/generated/openapi/work.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Work API",
-- "version": "0.288.0",
-- "description": "Routes of OpenVibe.Work as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Work as the OpenVibe contracts describe them (openvibe-contracts 0.289.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/zone.json b/generated/openapi/zone.json
iindex d4154e44..1c355f9e 100644
---- a/generated/openapi/zone.json
++++ b/generated/openapi/zone.json
@@@ -3,8 +3,8 @@
"jsonSchemaDialect": "https://json-schema.org/draft/2020-12/schema",
"info": {
"title": "OpenVibe.Zone API",
-- "version": "0.288.0",
-- "description": "Routes of OpenVibe.Zone as the OpenVibe contracts describe them (openvibe-contracts 0.288.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.289.0",
++ "description": "Routes of OpenVibe.Zone as the OpenVibe contracts describe them (openvibe-contracts 0.289.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 e63b9407..bef20988 100644
---- a/generated/typescript/index.d.ts
++++ b/generated/typescript/index.d.ts
@@@ -164241,24 +164241,309 @@ export type BillingBalanceReadResult =
transaction: BillingTransaction;
reversed_by: string[];
};
--
/** [email protected] (owner: billing) */
/**
-- * billing.balance@1: a person's Vibes balances, GET /api/v1/balances/:subject on OpenVibe.Billing (also returned after a recycle). credit is spendable (bought or given back), payable is what others gave the person (the only balance that can be cashed out), pending_payouts is held for requested cashouts.
++ * billing.balance@1: a Balance account (D12), held by a person (owner user:<usr_id>) or a project (owner project:<prj_id>). GET /api/me/balance answers { personal, projects } of these. balance_nano_usd is the whole account; held_nano_usd is reserved by active holds; available_nano_usd is spendable (balance = held + available). Money is integer nano-USD as decimal strings (D1), never a float. sandbox marks a sandbox account. An id is permanent, so this contract still also describes the Vibes wallet body that billing.balance.read has answered: a person's Vibes (currency vibes-bits), not a Balance. Each branch is closed with additionalProperties: false; new code sends the Balance branch.
*/
--export interface BillingBalance {
-- subject: SubjectRef;
++export type BillingBalance = {
++ subject?: SubjectRef;
++ currency?: "vibes-bits";
++ credit?: number;
++ payable?: number;
++ pending_payouts?: number;
++ /**
++ * What payable is worth in US cents at today's rate.
++ */
++ payable_value_cents?: number;
++ bits_per_usd?: number;
++} & (
++ | {
++ owner:
++ | {
++ type: "user";
++ id: string;
++ }
++ | {
++ type: "project";
++ id: string;
++ };
++ /**
++ * Integer nano-USD (1e-9 USD), stored as a decimal string (D1), never a float.
++ */
++ balance_nano_usd: string;
++ /**
++ * Integer nano-USD (1e-9 USD), stored as a decimal string (D1), never a float.
++ */
++ held_nano_usd: string;
++ /**
++ * Integer nano-USD (1e-9 USD), stored as a decimal string (D1), never a float.
++ */
++ available_nano_usd: string;
++ /**
++ * True for a sandbox account (D10): sandbox credit only, never real money.
++ */
++ sandbox?: boolean;
++ }
++ | {
++ subject: SubjectRef;
++ currency: "vibes-bits";
++ credit: number;
++ payable: number;
++ pending_payouts: number;
++ /**
++ * What payable is worth in US cents at today's rate.
++ */
++ payable_value_cents: number;
++ bits_per_usd: number;
++ }
++);
++
++/** [email protected] (owner: billing) */
++/**
++ * billing.vibes-charge@1: one Vibes Pay charge (D12). An app creates it for a person to pay with Vibes; GET /api/v1/vibes/charges/:id answers it, and a replay of the same app reference returns the same charge. amount is a whole number of Vibes (never a fraction, never nano-USD). status moves pending -> paid -> refunded, or pending -> expired|cancelled. sandbox charges use test Vibes only, worth nothing. The paid and refunded webhook/event payloads carry the same fields plus an opaque payer_ref and no person id or email.
++ */
++export interface BillingVibesCharge {
++ /**
++ * ULID charge id.
++ */
++ id: string;
++ /**
++ * The app that created the charge (its app_ id).
++ */
++ app: string;
++ /**
++ * The app's project; paid Vibes land in that project's Vibes wallet.
++ */
++ project_id: string;
++ /**
++ * Price in whole Vibes. A positive integer; Vibes have no fractions.
++ */
++ amount: number;
++ /**
++ * What the person is paying for.
++ */
++ description: string;
++ /**
++ * The app's own reference for this charge. It is the idempotency key: a replay with the same reference returns the same charge.
++ */
++ reference: string;
++ status: "pending" | "paid" | "refunded" | "expired" | "cancelled";
++ /**
++ * OpenVibe's pay sheet for this charge, e.g. https://openvibe.network/pay/vch_...
++ */
++ pay_url: string;
++ /**
++ * Where the browser returns after paying; null when the app gave no return URL.
++ */
++ return_url: string | null;
++ created_at: string;
++ /**
++ * When the Vibes moved, or null while the charge is unpaid.
++ */
++ paid_at: string | null;
++ /**
++ * When the Vibes went back to the person, or null.
++ */
++ refunded_at: string | null;
++ /**
++ * The pay sheet stops accepting payment after this instant.
++ */
++ expires_at: string;
++ /**
++ * True for a sandbox app: sandbox Vibes only, worth nothing.
++ */
++ sandbox: boolean;
++}
++
++/** [email protected] (owner: billing) */
++/**
++ * billing.vibes-charge-create-request@1: body of POST /api/v1/vibes/charges, sent from the app's server with its app token (capability billing.vibes.charge). amount is whole Vibes. reference is the app's idempotency key: a replay with the same reference and body returns the same charge. return_url is optional. The app token decides the project, the sandbox environment and the owner-set limits.
++ */
++export interface BillingVibesChargeCreateRequest {
++ /**
++ * Price in whole Vibes, a positive integer; never a fraction or a nano-USD amount.
++ */
++ amount: number;
++ /**
++ * What the person is paying for.
++ */
++ description: string;
++ /**
++ * The app's own reference; it is the idempotency key.
++ */
++ reference: string;
++ /**
++ * Where the browser returns after paying. Optional; the app may instead poll the charge or wait for the webhook.
++ */
++ return_url?: string;
++}
++
++/** [email protected] (owner: billing) */
++/**
++ * billing.vibes-charge-refund-request@1: the optional body of POST /api/v1/vibes/charges/:id/refund. A refund is always for the whole charge: the Vibes go back from the project's Vibes wallet to the person. reason is recorded on the refund. No amount may be given, because a partial refund is not a Vibes Pay refund.
++ */
++export interface BillingVibesChargeRefundRequest {
++ /**
++ * Why the app refunded, for its own and the person's record.
++ */
++ reason?: string | null;
++}
++
++/** [email protected] (owner: billing) */
++/**
++ * billing.vibes-wallet@1: a person's own Vibes, answered by GET /api/v1/vibes/me (capability billing.vibes.read) to an app the person approved. credit is Vibes to spend; payable is Vibes the person received as a creator and may cash out under the payout rules (never into Balance); bits_per_usd is today's rate. Vibes are whole integers; this is not a Balance and carries no nano-USD.
++ */
++export interface BillingVibesWallet {
++ /**
++ * The person the app reads for (the approved delegation). Only a user, never an app or a service.
++ */
++ subject: {
++ type: "user";
++ id: string;
++ };
currency: "vibes-bits";
++ /**
++ * Vibes to spend.
++ */
credit: number;
++ /**
++ * Vibes received as a creator, to cash out; never Balance.
++ */
payable: number;
-- pending_payouts: number;
/**
-- * What payable is worth in US cents at today's rate.
++ * Today's Vibes per USD rate, the same one a pack is priced with.
*/
-- payable_value_cents: number;
bits_per_usd: number;
}
++/** [email protected] (owner: billing) */
++/**
++ * billing.balance-transfer-request@1: body of POST /api/me/balance/transfers (D12). Moves amount_nano_usd from the signed-in person's Balance to a project they administer, into that project's Balance. Money is positive integer nano-USD as a decimal string (D1), never a float; the transfer is refused when the Balance or the person's project role is short. idempotency_key makes the transfer exactly once: a replay with the same key and body returns the first answer and moves nothing.
++ */
++export interface BillingBalanceTransferRequest {
++ /**
++ * The project whose Balance receives the money; the person must administer it.
++ */
++ to_project_id: string;
++ /**
++ * A positive integer nano-USD (1e-9 USD) as a decimal string (D1), never a float.
++ */
++ amount_nano_usd: string;
++ /**
++ * Unique per mutation. A replay with the same key and body returns the first answer and changes nothing.
++ */
++ idempotency_key: string;
++}
++
++/** [email protected] (owner: billing) */
++/**
++ * billing.vibes-purchase-from-balance-request@1: body of POST /api/me/vibes/from-balance (D12). Buys a whole number of Vibes with the signed-in person's Balance at the same Vibes price as a pack, charging exact nano-USD; Vibes -> Balance is never allowed. vibes is a whole integer (Vibes have no fractions); the money side is integer nano-USD (D1), never a float. idempotency_key makes the purchase exactly once.
++ */
++export interface BillingVibesPurchaseFromBalanceRequest {
++ /**
++ * How many whole Vibes to buy.
++ */
++ vibes: number;
++ /**
++ * Unique per mutation. A replay with the same key and body returns the first answer and changes nothing.
++ */
++ idempotency_key: string;
++}
++
++/** [email protected] (owner: billing) */
++/**
++ * billing.vibes.charge.paid v1 (OpenVibe.Billing, written when a Vibes Pay charge is paid): the app's charge and the opaque payer reference. This is also the Vibes Pay webhook body (HMAC-SHA256 with the app's webhook secret, retried with backoff). It deliberately carries no person id and no email, only payer_ref, stable per app, so an app can recognise a returning payer without receiving their identity. amount is whole Vibes. Envelope: subject { type: vibes-charge, id: vch_... }, actor service:billing.
++ */
++export interface BillingVibesChargePaidPayload {
++ /**
++ * The charge; GET /api/v1/vibes/charges/:id returns its current state.
++ */
++ id: string;
++ /**
++ * The app that created the charge.
++ */
++ app: string;
++ /**
++ * The app's project, whose Vibes wallet received the charge.
++ */
++ project_id: string;
++ /**
++ * The whole number of Vibes paid.
++ */
++ amount: number;
++ description: string;
++ /**
++ * The app's reference, as given when the charge was created.
++ */
++ reference: string;
++ status: "paid";
++ created_at: string;
++ /**
++ * When the Vibes moved from the person to the project.
++ */
++ paid_at: string;
++ /**
++ * Null: a paid charge has not been refunded.
++ */
++ refunded_at: null;
++ expires_at: string;
++ /**
++ * True for a sandbox app: sandbox Vibes only, worth nothing.
++ */
++ sandbox: boolean;
++ /**
++ * Opaque reference for the paying person, stable per app. Never the person's id, email or any other identity; an app uses it to recognise a returning payer.
++ */
++ payer_ref: string;
++}
++
++/** [email protected] (owner: billing) */
++/**
++ * billing.vibes.charge.refunded v1 (OpenVibe.Billing, written when a Vibes Pay charge is refunded): the app's charge and the opaque payer reference. This is also the Vibes Pay webhook body (HMAC-SHA256 with the app's webhook secret, retried with backoff). It deliberately carries no person id and no email, only payer_ref, stable per app, so an app can recognise a returning payer without receiving their identity. amount is whole Vibes. Envelope: subject { type: vibes-charge, id: vch_... }, actor service:billing.
++ */
++export interface BillingVibesChargeRefundedPayload {
++ /**
++ * The charge; GET /api/v1/vibes/charges/:id returns its current state.
++ */
++ id: string;
++ /**
++ * The app that created the charge.
++ */
++ app: string;
++ /**
++ * The app's project, whose Vibes wallet received the charge.
++ */
++ project_id: string;
++ /**
++ * The whole number of Vibes paid.
++ */
++ amount: number;
++ description: string;
++ /**
++ * The app's reference, as given when the charge was created.
++ */
++ reference: string;
++ status: "refunded";
++ created_at: string;
++ /**
++ * When the Vibes moved from the person to the project.
++ */
++ paid_at: string;
++ /**
++ * When the Vibes went back to the person.
++ */
++ refunded_at: string;
++ expires_at: string;
++ /**
++ * True for a sandbox app: sandbox Vibes only, worth nothing.
++ */
++ sandbox: boolean;
++ /**
++ * Opaque reference for the paying person, stable per app. Never the person's id, email or any other identity; an app uses it to recognise a returning payer.
++ */
++ payer_ref: string;
++}
++
/** [email protected] (owner: billing) */
/**
* billing.cashout-decision@1: bodies of billing.cashout.manage on OpenVibe.Billing (staff). POST /api/v1/cashouts/:id/approve: { payout_reference, payout_provider? } after the payout was sent (payout_provider defaults to paypal; refused while the cashout is in escrow). POST /api/v1/cashouts/:id/deny: { reason? } returns the bits to the person's payable. GET /api/v1/cashouts takes no body. Every mutating call needs an Idempotency-Key header (a replay with the same key and body returns the first answer) and is refused with 503 billing.frozen while the economy is frozen. Refusals are problem+json (billing.invalid_input, billing.invalid_amount, billing.insufficient_funds, …).
@@@ -164902,12 +165187,17 @@ export interface BillingCreditSettlement {
/** [email protected] (owner: billing) */
/**
-- * billing.credit-hold-create-request@1: body of POST /internal/credits/holds. Reserves inference credit up to max_nano_usd against a (subject, project_id) account, priced at the given immutable rate version.
++ * billing.credit-hold-create-request@1: body of POST /internal/credits/holds. Reserves inference credit up to max_nano_usd against a Balance account, priced at the given immutable rate version. With project_id the hold spends that project's Balance; without it the hold is personal and spends the subject's own Balance (D12). An app subject always names its project.
*/
--export interface BillingCreditHoldCreateRequest {
++export type BillingCreditHoldCreateRequest = {
++ [k: string]: unknown | undefined;
++} & {
idempotency_key: string;
subject: SubjectRef;
-- project_id: string;
++ /**
++ * The project whose Balance the hold spends. Optional: without it the hold is personal, on the subject's own Balance. Required when the subject is an app.
++ */
++ project_id?: string;
credential_id?: string | null;
/**
* Integer nano-USD (1e-9 USD), stored as a string.
@@@ -164923,7 +165213,7 @@ export interface BillingCreditHoldCreateRequest {
* D10. A sandbox caller's hold draws on that person's sandbox credit for the project (account kind usd_credit_sandbox, owner subject:project), never on real Credits, and a real hold never draws on sandbox credit. Sandbox grants (billing.credit-sandbox-grant-request@1) land in that sandbox account. Optional; defaults to false.
*/
sandbox?: boolean;
--}
++};
/** [email protected] (owner: billing) */
/**
ddiff --git a/manifests/capabilities/billing.vibes.charge.json b/manifests/capabilities/billing.vibes.charge.json
nnew file mode 100644
iindex 00000000..73cc90ea
---- /dev/null
++++ b/manifests/capabilities/billing.vibes.charge.json
@@@ -0,0 +1,27 @@
++{
++ "id": "billing.vibes.charge",
++ "version": "1.0",
++ "owner": "billing",
++ "status": "active",
++ "visibility": "public",
++ "sensitive": true,
++ "description": "Sell something for Vibes as a developer app (Vibes Pay, D12). POST /api/v1/vibes/charges creates a charge from the app's token (billing.vibes-charge-create-request@1; idempotent on the app's reference) and answers billing.vibes-charge@1 with a pay_url; GET /api/v1/vibes/charges/:id reads it; POST /api/v1/vibes/charges/:id/refund returns the Vibes from the project's Vibes wallet to the person (billing.vibes-charge-refund-request@1). The person pays on OpenVibe's pay sheet; Billing moves the Vibes atomically and once to the app's project Vibes wallet, then POSTs a signed webhook (HMAC-SHA256 with the app's webhook secret, retried with backoff) whose body is billing.vibes.charge.paid@1 or billing.vibes.charge.refunded@1 — no person id or email, only an opaque payer_ref stable per app. A sandbox app charges sandbox Vibes only (test grants, worth nothing). The owner's billing.vibes settings cap the amount, the daily per-person spend and whether Vibes Pay is on.",
++ "permissions": [
++ "billing:vibes:charge"
++ ],
++ "resourceConstraints": [
++ "project"
++ ],
++ "quotaClass": "billing-vibes-write",
++ "events": [
++ "billing.vibes.charge.paid",
++ "billing.vibes.charge.refunded"
++ ],
++ "implementedBy": [
++ "POST /api/v1/vibes/charges",
++ "GET /api/v1/vibes/charges/:id",
++ "POST /api/v1/vibes/charges/:id/refund"
++ ],
++ "inputSchema": "billing.vibes-charge-create-request@1",
++ "outputSchema": "billing.vibes-charge@1"
++}
ddiff --git a/manifests/capabilities/billing.vibes.read.json b/manifests/capabilities/billing.vibes.read.json
nnew file mode 100644
iindex 00000000..3924398a
---- /dev/null
++++ b/manifests/capabilities/billing.vibes.read.json
@@@ -0,0 +1,21 @@
++{
++ "id": "billing.vibes.read",
++ "version": "1.0",
++ "owner": "billing",
++ "status": "active",
++ "visibility": "public",
++ "description": "Read a person's own Vibes as an app the person approved (D12): GET /api/v1/vibes/me with a person-delegated app token answers billing.vibes-wallet@1 — the Vibes to spend, the creator payable that can be cashed out (never into Balance) and today's bits_per_usd. The app sees only the person who approved it, never another person's Vibes.",
++ "permissions": [
++ "billing:vibes:read"
++ ],
++ "resourceConstraints": [
++ "owner"
++ ],
++ "quotaClass": "billing-vibes-read",
++ "events": [],
++ "implementedBy": [
++ "GET /api/v1/vibes/me"
++ ],
++ "inputSchema": "common.no-body@1",
++ "outputSchema": "billing.vibes-wallet@1"
++}
ddiff --git a/manifests/services/billing.json b/manifests/services/billing.json
iindex 651e7f3c..4b5f08fc 100644
---- a/manifests/services/billing.json
++++ b/manifests/services/billing.json
@@@ -36,7 +36,9 @@
"billing.credit.hold.settle",
"billing.credit.hold.release",
"billing.credit.sandbox.grant",
-- "billing.config.manage"
++ "billing.config.manage",
++ "billing.vibes.charge",
++ "billing.vibes.read"
],
"eventsProduced": [
"billing.transaction.settled",
@@@ -48,7 +50,9 @@
"billing.cashout.denied",
"billing.staff.action",
"billing.receipt.external",
-- "billing.credit.settled"
++ "billing.credit.settled",
++ "billing.vibes.charge.paid",
++ "billing.vibes.charge.refunded"
],
"eventsConsumed": [],
"namespacesOwned": [
ddiff --git a/package.json b/package.json
iindex cf81f940..1b6a23be 100644
---- a/package.json
++++ b/package.json
@@@ -1,6 +1,6 @@
{
"name": "openvibe-contracts",
-- "version": "0.288.0",
++ "version": "0.289.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",