UsageRecorded common.usage-recorded@1
Generated at from
openvibe-contracts v0.76.0 and
openvibe-sdk v0.20.1.
- Version
- 1.0.0
- Owner
- network
- Visibility
- first-party
- Status
- active
- Compatibility
- backward
- Decision
- ADR-014
- Schema
https://openvibe.network/contracts/common/usage-recorded.v1.json
common.usage-recorded@1 (roadmap WS-N task 4, ADR-014): one rollup of a developer project's use of one capability in one environment over one closed window (an hour or a day), as the service that owns the capability counted it. The payload of every <service>.usage.recorded event; OpenVibe.Network adds them up per project and day for the project's dashboard on OpenVibe.Codes. The producer counts in the transaction that does its own accounting (a job's end, a stored event, a delivery attempt) and writes the rollup to its outbox after the window has closed: never one event per request. Totals, not deltas: a later event with the same key (source, project_id, env, capability, dimension, unit, window_start) replaces the earlier one, and a lower revision never replaces a higher one. Envelope: subject { type: project, id: <project_id> }, visibility internal, priority low, actor the producing service. Never carries a subject id, an address, a session, a request's input or content, or a file name.
Fields
| Field | Type | Required | Description | Constraints |
|---|---|---|---|---|
project_id | string | yes | The developer project whose app token did the work. |
|
env | enum | yes | The environment of that app token. |
|
capability | string | yes | The capability the usage counts against: tools.job.create, tools.tool.run, events.app.publish, events.app.subscribe. |
|
dimension | string | Optional finer key inside the capability, as the producer names it: a job type (img.process) or a tool id (image-resize). Never the id of a person, an app, a request or a job. |
| |
unit | string | yes | What quantity counts (jobs, events, deliveries, requests, bytes, tokens): the same word a Network project quota uses for its unit. |
|
window | enum | yes | The length of the window. |
|
window_start | string | yes | UTC, on the hour (midnight for a day). |
|
window_end | string | yes | window_start plus the window, exclusive. |
|
quantity | integer | yes | Units used in the window, by work that succeeded and work that failed alike. |
|
errors | integer | yes | Operations in the window that failed or were refused: a job that failed, a publish refused with a problem, a webhook attempt without a 2xx. A refused operation may use no unit, so errors can exceed quantity. |
|
error_codes | object | The errors by problem code (at most 20 codes; the rest are only in errors). | ||
samples | array of object | The last failures of the window, newest first, so a developer can find them in their own logs. |
| |
samples[].at | string | yes |
| |
samples[].code | string | yes |
| |
samples[].status | integer | The HTTP status the failure was answered with, when there was one. |
| |
samples[].trace_id | string | The W3C trace id of the request (or event) that failed. |
| |
samples[].ref | string | What failed, when the project can look it up with its own token: a job, an event, an AI run, a Media object. Never a subject. |
| |
revision | integer | 1 when absent. A producer that re-emits a window with corrected totals raises it. |
|
Examples
From the contract's own test fixtures: valid ones validate, rejected ones must fail.
Valid: events-publish-refused
{
"project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"env": "sandbox",
"capability": "events.app.publish",
"unit": "events",
"window": "hour",
"window_start": "2026-09-26T09:00:00.000Z",
"window_end": "2026-09-26T10:00:00.000Z",
"quantity": 1800,
"errors": 61,
"error_codes": {
"events.quota_exceeded": 60,
"events.type_not_allowed": 1
},
"samples": [
{
"at": "2026-09-26T09:59:58.120Z",
"code": "events.quota_exceeded",
"status": 429,
"trace_id": "0af7651916cd43dd8448eb211c80319c"
}
]
}Valid: minimal-day
{
"project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"env": "production",
"capability": "tools.tool.run",
"unit": "jobs",
"window": "day",
"window_start": "2026-09-25T00:00:00.000Z",
"window_end": "2026-09-26T00:00:00.000Z",
"quantity": 0,
"errors": 0
}Valid: revised-totals
{
"project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"env": "production",
"capability": "tools.tool.run",
"unit": "jobs",
"window": "day",
"window_start": "2026-09-25T00:00:00.000Z",
"window_end": "2026-09-26T00:00:00.000Z",
"quantity": 7,
"errors": 0,
"revision": 2
}Valid: tools-jobs-hour
{
"project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"env": "production",
"capability": "tools.job.create",
"dimension": "img.process",
"unit": "jobs",
"window": "hour",
"window_start": "2026-09-26T14:00:00.000Z",
"window_end": "2026-09-26T15:00:00.000Z",
"quantity": 42,
"errors": 3,
"error_codes": {
"tools.job.failed": 2,
"tools.job.timeout": 1
},
"samples": [
{
"at": "2026-09-26T14:51:07.412Z",
"code": "tools.job.timeout",
"status": 504,
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"ref": "job_01JAB2C3D4E5F6G7H8J9K0MNPR"
},
{
"at": "2026-09-26T14:20:13.001Z",
"code": "tools.job.failed",
"status": 422,
"ref": "job_01JAB2C3D4E5F6G7H8J9K0MNPS"
}
]
}Rejected: carries-an-address
{
"project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"env": "production",
"capability": "tools.job.create",
"dimension": "img.process",
"unit": "jobs",
"window": "hour",
"window_start": "2026-09-26T14:00:00.000Z",
"window_end": "2026-09-26T15:00:00.000Z",
"quantity": 42,
"errors": 3,
"error_codes": {
"tools.job.failed": 2,
"tools.job.timeout": 1
},
"samples": [
{
"at": "2026-09-26T14:51:07.412Z",
"code": "tools.job.timeout",
"status": 504,
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"ref": "job_01JAB2C3D4E5F6G7H8J9K0MNPR"
},
{
"at": "2026-09-26T14:20:13.001Z",
"code": "tools.job.failed",
"status": 422,
"ref": "job_01JAB2C3D4E5F6G7H8J9K0MNPS"
}
],
"ip": "203.0.113.7"
}Rejected: carries-the-app
{
"project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"env": "production",
"capability": "tools.job.create",
"dimension": "img.process",
"unit": "jobs",
"window": "hour",
"window_start": "2026-09-26T14:00:00.000Z",
"window_end": "2026-09-26T15:00:00.000Z",
"quantity": 42,
"errors": 3,
"error_codes": {
"tools.job.failed": 2,
"tools.job.timeout": 1
},
"samples": [
{
"at": "2026-09-26T14:51:07.412Z",
"code": "tools.job.timeout",
"status": 504,
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"ref": "job_01JAB2C3D4E5F6G7H8J9K0MNPR"
},
{
"at": "2026-09-26T14:20:13.001Z",
"code": "tools.job.failed",
"status": 422,
"ref": "job_01JAB2C3D4E5F6G7H8J9K0MNPS"
}
],
"app_id": "app_01JAB2C3D4E5F6G7H8J9K0MNPQ"
}Rejected: dimension-is-a-person
{
"project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"env": "production",
"capability": "tools.job.create",
"dimension": "usr_01jab2c3d4e5f6g7h8j9k0mnpq",
"unit": "jobs",
"window": "hour",
"window_start": "2026-09-26T14:00:00.000Z",
"window_end": "2026-09-26T15:00:00.000Z",
"quantity": 42,
"errors": 3,
"error_codes": {
"tools.job.failed": 2,
"tools.job.timeout": 1
},
"samples": [
{
"at": "2026-09-26T14:51:07.412Z",
"code": "tools.job.timeout",
"status": 504,
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"ref": "job_01JAB2C3D4E5F6G7H8J9K0MNPR"
},
{
"at": "2026-09-26T14:20:13.001Z",
"code": "tools.job.failed",
"status": 422,
"ref": "job_01JAB2C3D4E5F6G7H8J9K0MNPS"
}
]
}Rejected: error-code-not-an-id
{
"project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"env": "production",
"capability": "tools.job.create",
"dimension": "img.process",
"unit": "jobs",
"window": "hour",
"window_start": "2026-09-26T14:00:00.000Z",
"window_end": "2026-09-26T15:00:00.000Z",
"quantity": 42,
"errors": 3,
"error_codes": {
"Timeout": 1
},
"samples": [
{
"at": "2026-09-26T14:51:07.412Z",
"code": "tools.job.timeout",
"status": 504,
"trace_id": "4bf92f3577b34da6a3ce929d0e0e4736",
"ref": "job_01JAB2C3D4E5F6G7H8J9K0MNPR"
},
{
"at": "2026-09-26T14:20:13.001Z",
"code": "tools.job.failed",
"status": 422,
"ref": "job_01JAB2C3D4E5F6G7H8J9K0MNPS"
}
]
}Rejected: negative-quantity
{
"project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"env": "production",
"capability": "tools.tool.run",
"unit": "jobs",
"window": "day",
"window_start": "2026-09-25T00:00:00.000Z",
"window_end": "2026-09-26T00:00:00.000Z",
"quantity": -1,
"errors": 0
}Rejected: no-window-start
{
"project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"env": "production",
"capability": "tools.tool.run",
"unit": "jobs",
"window": "day",
"window_end": "2026-09-26T00:00:00.000Z",
"quantity": 0,
"errors": 0
}Rejected: per-minute-window
{
"project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"env": "production",
"capability": "tools.tool.run",
"unit": "jobs",
"window": "minute",
"window_start": "2026-09-25T00:00:00.000Z",
"window_end": "2026-09-26T00:00:00.000Z",
"quantity": 0,
"errors": 0
}Rejected: sample-with-message
{
"project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"env": "production",
"capability": "tools.job.create",
"dimension": "img.process",
"unit": "jobs",
"window": "hour",
"window_start": "2026-09-26T14:00:00.000Z",
"window_end": "2026-09-26T15:00:00.000Z",
"quantity": 42,
"errors": 3,
"error_codes": {
"tools.job.failed": 2,
"tools.job.timeout": 1
},
"samples": [
{
"at": "2026-09-26T14:51:07.412Z",
"code": "tools.job.failed",
"detail": "Cannot read holiday.jpg"
}
]
}Rejected: subject-in-sample
{
"project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"env": "production",
"capability": "tools.job.create",
"dimension": "img.process",
"unit": "jobs",
"window": "hour",
"window_start": "2026-09-26T14:00:00.000Z",
"window_end": "2026-09-26T15:00:00.000Z",
"quantity": 42,
"errors": 3,
"error_codes": {
"tools.job.failed": 2,
"tools.job.timeout": 1
},
"samples": [
{
"at": "2026-09-26T14:51:07.412Z",
"code": "tools.job.failed",
"ref": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ"
}
]
}Rejected: unknown-env
{
"project_id": "prj_01JAB2C3D4E5F6G7H8J9K0MNPQ",
"env": "staging",
"capability": "tools.tool.run",
"unit": "jobs",
"window": "day",
"window_start": "2026-09-25T00:00:00.000Z",
"window_end": "2026-09-26T00:00:00.000Z",
"quantity": 0,
"errors": 0
}Validate
const contracts = require('openvibe-contracts');
contracts.validate('common.usage-recorded@1', value); // { valid, errors: [{ path, message }] }