openvibers/openvibe.contracts Commits

Capability schemas (WS-C task 4) for Deals and Trade: deals offer record, submissions, updates and price observations, actions, votes, flags, moderation, product resolve and watches; trade instrument, observation, alert rule, watchlist and context revision records, instrument management and resolve, observation writes, context reads and proposals, alerts and watchlists. 24 capabilities complete; gaps 46 -> 22 (unreleased)

OP OpenVibers committed 25 Sept 2026 · 6a58762e5e · 135 files, +8410 −385


Changes

ddiff --git a/compatibility/capability-schema-gaps.json b/compatibility/capability-schema-gaps.json
iindex 28e2499d..85626162 100644
---- a/compatibility/capability-schema-gaps.json
++++ b/compatibility/capability-schema-gaps.json
@@@ -1,90 +1,6 @@
{
"_about": "Active capabilities still without an input or output schema (roadmap WS-C task 4). test/capability-schemas.test.js fails on any active capability missing a schema that is not listed here, and on a listed one that has since gained it (remove it from this list). The list only shrinks: fill a capability's schemas, then delete its row. A request with no body names common.no-body@1; a response of bytes names common.binary@1.",
"gaps": [
-- {
-- "id": "deals.flag.create",
-- "missing": [
-- "inputSchema",
-- "outputSchema"
-- ]
-- },
-- {
-- "id": "deals.offer.expire",
-- "missing": [
-- "inputSchema",
-- "outputSchema"
-- ]
-- },
-- {
-- "id": "deals.offer.merge",
-- "missing": [
-- "inputSchema",
-- "outputSchema"
-- ]
-- },
-- {
-- "id": "deals.offer.moderate",
-- "missing": [
-- "inputSchema",
-- "outputSchema"
-- ]
-- },
-- {
-- "id": "deals.offer.submit",
-- "missing": [
-- "inputSchema",
-- "outputSchema"
-- ]
-- },
-- {
-- "id": "deals.offer.update",
-- "missing": [
-- "inputSchema",
-- "outputSchema"
-- ]
-- },
-- {
-- "id": "deals.product.resolve",
-- "missing": [
-- "inputSchema",
-- "outputSchema"
-- ]
-- },
-- {
-- "id": "deals.vote.remove",
-- "missing": [
-- "inputSchema",
-- "outputSchema"
-- ]
-- },
-- {
-- "id": "deals.vote.set",
-- "missing": [
-- "inputSchema",
-- "outputSchema"
-- ]
-- },
-- {
-- "id": "deals.watch.create",
-- "missing": [
-- "inputSchema",
-- "outputSchema"
-- ]
-- },
-- {
-- "id": "deals.watch.delete",
-- "missing": [
-- "inputSchema",
-- "outputSchema"
-- ]
-- },
-- {
-- "id": "deals.watch.read",
-- "missing": [
-- "inputSchema",
-- "outputSchema"
-- ]
-- },
{
"id": "host.deploy.create",
"missing": [
@@@ -238,90 +154,6 @@
"inputSchema",
"outputSchema"
]
-- },
-- {
-- "id": "trade.alert.create",
-- "missing": [
-- "inputSchema",
-- "outputSchema"
-- ]
-- },
-- {
-- "id": "trade.alert.delete",
-- "missing": [
-- "inputSchema",
-- "outputSchema"
-- ]
-- },
-- {
-- "id": "trade.alert.read",
-- "missing": [
-- "inputSchema",
-- "outputSchema"
-- ]
-- },
-- {
-- "id": "trade.context.propose",
-- "missing": [
-- "inputSchema",
-- "outputSchema"
-- ]
-- },
-- {
-- "id": "trade.context.read",
-- "missing": [
-- "inputSchema",
-- "outputSchema"
-- ]
-- },
-- {
-- "id": "trade.instrument.manage",
-- "missing": [
-- "inputSchema",
-- "outputSchema"
-- ]
-- },
-- {
-- "id": "trade.instrument.resolve",
-- "missing": [
-- "inputSchema",
-- "outputSchema"
-- ]
-- },
-- {
-- "id": "trade.observation.write",
-- "missing": [
-- "inputSchema",
-- "outputSchema"
-- ]
-- },
-- {
-- "id": "trade.watchlist.create",
-- "missing": [
-- "inputSchema",
-- "outputSchema"
-- ]
-- },
-- {
-- "id": "trade.watchlist.delete",
-- "missing": [
-- "inputSchema",
-- "outputSchema"
-- ]
-- },
-- {
-- "id": "trade.watchlist.read",
-- "missing": [
-- "inputSchema",
-- "outputSchema"
-- ]
-- },
-- {
-- "id": "trade.watchlist.update",
-- "missing": [
-- "inputSchema",
-- "outputSchema"
-- ]
}
]
}
ddiff --git a/contracts/catalog.json b/contracts/catalog.json
iindex 9a3edbc9..31f71a87 100644
---- a/contracts/catalog.json
++++ b/contracts/catalog.json
@@@ -2724,6 +2724,141 @@
"compatibility": "backward",
"status": "active"
},
++ {
++ "id": "deals.flag-request",
++ "version": "1.0.0",
++ "owner": "deals",
++ "schema": "deals/flag-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "deals.flag-result",
++ "version": "1.0.0",
++ "owner": "deals",
++ "schema": "deals/flag-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "deals.moderation-request",
++ "version": "1.0.0",
++ "owner": "deals",
++ "schema": "deals/moderation-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "deals.moderation-result",
++ "version": "1.0.0",
++ "owner": "deals",
++ "schema": "deals/moderation-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "deals.offer-action-request",
++ "version": "1.0.0",
++ "owner": "deals",
++ "schema": "deals/offer-action-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "deals.offer-result",
++ "version": "1.0.0",
++ "owner": "deals",
++ "schema": "deals/offer-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "deals.offer-submit-request",
++ "version": "1.0.0",
++ "owner": "deals",
++ "schema": "deals/offer-submit-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "deals.offer-update-request",
++ "version": "1.0.0",
++ "owner": "deals",
++ "schema": "deals/offer-update-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "deals.offer",
++ "version": "1.0.0",
++ "owner": "deals",
++ "schema": "deals/offer.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "deals.product-resolve-request",
++ "version": "1.0.0",
++ "owner": "deals",
++ "schema": "deals/product-resolve-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "deals.product-resolve-result",
++ "version": "1.0.0",
++ "owner": "deals",
++ "schema": "deals/product-resolve-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "deals.vote-request",
++ "version": "1.0.0",
++ "owner": "deals",
++ "schema": "deals/vote-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "deals.vote-result",
++ "version": "1.0.0",
++ "owner": "deals",
++ "schema": "deals/vote-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "deals.watch-request",
++ "version": "1.0.0",
++ "owner": "deals",
++ "schema": "deals/watch-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "deals.watch-result",
++ "version": "1.0.0",
++ "owner": "deals",
++ "schema": "deals/watch-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
{
"id": "trade.observation.created",
"version": "1.0.0",
@@@ -2769,6 +2904,177 @@
"compatibility": "backward",
"status": "active"
},
++ {
++ "id": "trade.alert-read-result",
++ "version": "1.0.0",
++ "owner": "trade",
++ "schema": "trade/alert-read-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "trade.alert-rule-request",
++ "version": "1.0.0",
++ "owner": "trade",
++ "schema": "trade/alert-rule-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "trade.alert-rule-result",
++ "version": "1.0.0",
++ "owner": "trade",
++ "schema": "trade/alert-rule-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "trade.alert-rule",
++ "version": "1.0.0",
++ "owner": "trade",
++ "schema": "trade/alert-rule.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "trade.context-proposal-result",
++ "version": "1.0.0",
++ "owner": "trade",
++ "schema": "trade/context-proposal-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "trade.context-proposal",
++ "version": "1.0.0",
++ "owner": "trade",
++ "schema": "trade/context-proposal.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "trade.context-read-result",
++ "version": "1.0.0",
++ "owner": "trade",
++ "schema": "trade/context-read-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "trade.context-revision",
++ "version": "1.0.0",
++ "owner": "trade",
++ "schema": "trade/context-revision.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "trade.deleted-result",
++ "version": "1.0.0",
++ "owner": "trade",
++ "schema": "trade/deleted-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "trade.instrument-manage-request",
++ "version": "1.0.0",
++ "owner": "trade",
++ "schema": "trade/instrument-manage-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "trade.instrument-manage-result",
++ "version": "1.0.0",
++ "owner": "trade",
++ "schema": "trade/instrument-manage-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "trade.instrument",
++ "version": "1.0.0",
++ "owner": "trade",
++ "schema": "trade/instrument.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "trade.observation-request",
++ "version": "1.0.0",
++ "owner": "trade",
++ "schema": "trade/observation-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "trade.observation-result",
++ "version": "1.0.0",
++ "owner": "trade",
++ "schema": "trade/observation-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "trade.observation",
++ "version": "1.0.0",
++ "owner": "trade",
++ "schema": "trade/observation.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "trade.resolve-result",
++ "version": "1.0.0",
++ "owner": "trade",
++ "schema": "trade/resolve-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "trade.watchlist-request",
++ "version": "1.0.0",
++ "owner": "trade",
++ "schema": "trade/watchlist-request.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "trade.watchlist-result",
++ "version": "1.0.0",
++ "owner": "trade",
++ "schema": "trade/watchlist-result.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
++ {
++ "id": "trade.watchlist",
++ "version": "1.0.0",
++ "owner": "trade",
++ "schema": "trade/watchlist.v1.json",
++ "visibility": "first-party",
++ "compatibility": "backward",
++ "status": "active"
++ },
{
"id": "games.player.joined",
"version": "1.0.0",
ddiff --git a/contracts/deals/flag-request.v1.json b/contracts/deals/flag-request.v1.json
nnew file mode 100644
iindex 00000000..9744778d
---- /dev/null
++++ b/contracts/deals/flag-request.v1.json
@@@ -0,0 +1,46 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/deals/flag-request.v1.json",
++ "title": "DealsFlagRequest",
++ "description": "deals.flag-request@1: the body of POST /api/v1/offers/:id/flags on OpenVibe.Deals (deals.flag.create; a person): { kind, reason?, duplicate_of? } — duplicate_of (the other deal, id, slug or /d/ link) is required for kind duplicate. An open flag of the same kind by the same person is refreshed, not repeated.",
++ "type": "object",
++ "required": [
++ "kind"
++ ],
++ "properties": {
++ "kind": {
++ "enum": [
++ "expired",
++ "price_wrong",
++ "spam",
++ "duplicate",
++ "other"
++ ]
++ },
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 500
++ },
++ "duplicate_of": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ },
++ "if": {
++ "properties": {
++ "kind": {
++ "const": "duplicate"
++ }
++ }
++ },
++ "then": {
++ "required": [
++ "duplicate_of"
++ ]
++ }
++}
ddiff --git a/contracts/deals/flag-result.v1.json b/contracts/deals/flag-result.v1.json
nnew file mode 100644
iindex 00000000..e752bfa1
---- /dev/null
++++ b/contracts/deals/flag-result.v1.json
@@@ -0,0 +1,35 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/deals/flag-result.v1.json",
++ "title": "DealsFlagResult",
++ "description": "deals.flag-result@1: the answer of POST /api/v1/offers/:id/flags on OpenVibe.Deals: { flag: { id, kind, status }, created } (201 when created, 200 when an open flag was refreshed).",
++ "type": "object",
++ "required": [
++ "flag",
++ "created"
++ ],
++ "properties": {
++ "flag": {
++ "type": "object",
++ "required": [
++ "id",
++ "kind",
++ "status"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "kind": {
++ "type": "string"
++ },
++ "status": {
++ "type": "string"
++ }
++ }
++ },
++ "created": {
++ "type": "boolean"
++ }
++ }
++}
ddiff --git a/contracts/deals/moderation-request.v1.json b/contracts/deals/moderation-request.v1.json
nnew file mode 100644
iindex 00000000..6794e0dd
---- /dev/null
++++ b/contracts/deals/moderation-request.v1.json
@@@ -0,0 +1,48 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/deals/moderation-request.v1.json",
++ "title": "DealsModerationRequest",
++ "description": "deals.moderation-request@1: bodies of deals.offer.moderate on OpenVibe.Deals (moderators). POST /api/v1/offers/:id/disable: { reason } (3-300 characters); POST …/enable: { reason? }; POST …/review (a person): { note? }; POST /api/v1/flags/:id/resolve: { status?: resolved (default) | dismissed, resolution? }. GET /api/v1/flags?status= takes no body.",
++ "anyOf": [
++ {
++ "type": "object",
++ "properties": {
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 300
++ },
++ "note": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 300
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "status"
++ ],
++ "properties": {
++ "status": {
++ "enum": [
++ "resolved",
++ "dismissed"
++ ]
++ },
++ "resolution": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 300
++ }
++ }
++ }
++ ]
++}
ddiff --git a/contracts/deals/moderation-result.v1.json b/contracts/deals/moderation-result.v1.json
nnew file mode 100644
iindex 00000000..29a83448
---- /dev/null
++++ b/contracts/deals/moderation-result.v1.json
@@@ -0,0 +1,204 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/deals/moderation-result.v1.json",
++ "title": "DealsModerationResult",
++ "description": "deals.moderation-result@1: answers of deals.offer.moderate on OpenVibe.Deals. POST /api/v1/offers/:id/disable|enable|review → { offer }; GET /api/v1/flags?status=open|resolved|dismissed → { flags }; POST /api/v1/flags/:id/resolve → { flag, changed } (changed false when it was no longer open).",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "offer"
++ ],
++ "properties": {
++ "offer": {
++ "$ref": "offer.v1.json"
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "flags"
++ ],
++ "properties": {
++ "flags": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "id",
++ "kind",
++ "origin",
++ "status"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "kind": {
++ "type": "string"
++ },
++ "origin": {
++ "type": "string"
++ },
++ "reporter": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "status": {
++ "enum": [
++ "open",
++ "resolved",
++ "dismissed"
++ ]
++ },
++ "details": {
++ "type": "object"
++ },
++ "created_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "updated_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "resolved_by": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "resolved_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "resolution": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "offer": {
++ "type": [
++ "object",
++ "null"
++ ]
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "flag",
++ "changed"
++ ],
++ "properties": {
++ "flag": {
++ "type": "object",
++ "required": [
++ "id",
++ "kind",
++ "origin",
++ "status"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "kind": {
++ "type": "string"
++ },
++ "origin": {
++ "type": "string"
++ },
++ "reporter": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "status": {
++ "enum": [
++ "open",
++ "resolved",
++ "dismissed"
++ ]
++ },
++ "details": {
++ "type": "object"
++ },
++ "created_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "updated_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "resolved_by": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "resolved_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "resolution": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "offer": {
++ "type": [
++ "object",
++ "null"
++ ]
++ }
++ }
++ },
++ "changed": {
++ "type": "boolean"
++ }
++ }
++ }
++ ]
++}
ddiff --git a/contracts/deals/offer-action-request.v1.json b/contracts/deals/offer-action-request.v1.json
nnew file mode 100644
iindex 00000000..d64a7ed3
---- /dev/null
++++ b/contracts/deals/offer-action-request.v1.json
@@@ -0,0 +1,45 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/deals/offer-action-request.v1.json",
++ "title": "DealsOfferActionRequest",
++ "description": "deals.offer-action-request@1: bodies of the deal actions on OpenVibe.Deals. POST /api/v1/offers/:id/expire (deals.offer.expire; the submitter or a moderator): { reason? } (a moderator's reason is recorded). POST …/disable (deals.offer.moderate): { reason } (3-300 characters, required); POST …/enable: { reason? }; POST …/review (a person): { note? }. POST …/merge (deals.offer.merge, moderators): { into, reason? } (the canonical deal, id or slug; no cycles, not an already merged offer); POST …/unmerge: { reason? }.",
++ "anyOf": [
++ {
++ "type": "object",
++ "properties": {
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 300
++ },
++ "note": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 300
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "into"
++ ],
++ "properties": {
++ "into": {
++ "type": "string",
++ "minLength": 1
++ },
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ }
++ ]
++}
ddiff --git a/contracts/deals/offer-result.v1.json b/contracts/deals/offer-result.v1.json
nnew file mode 100644
iindex 00000000..126cc806
---- /dev/null
++++ b/contracts/deals/offer-result.v1.json
@@@ -0,0 +1,163 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/deals/offer-result.v1.json",
++ "title": "DealsOfferResult",
++ "description": "deals.offer-result@1: the answer of the deal writes on OpenVibe.Deals: { offer } (the canonical offer after the change). POST /api/v1/offers (201), PATCH /api/v1/offers/:id, POST …/expire, …/disable, …/enable and …/review answer exactly that; POST …/observations (201) adds the observation recorded; POST …/merge adds merged { id, slug } and the before/after of the change; POST …/unmerge adds from { id, slug } and before/after.",
++ "type": "object",
++ "required": [
++ "offer"
++ ],
++ "properties": {
++ "offer": {
++ "$ref": "offer.v1.json"
++ },
++ "observation": {
++ "type": "object",
++ "required": [
++ "id",
++ "observed_at",
++ "origin",
++ "source"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "observed_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "recorded_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "price": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "currency": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "shipping": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "shipping_note": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "condition": {
++ "enum": [
++ "new",
++ "used",
++ "refurbished",
++ "damaged",
++ null
++ ]
++ },
++ "availability": {
++ "enum": [
++ "in_stock",
++ "out_of_stock",
++ "preorder",
++ "discontinued",
++ "limited",
++ "sold_out",
++ "online_only",
++ "in_store_only",
++ null
++ ]
++ },
++ "origin": {
++ "type": "string"
++ },
++ "observed_by": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "listing": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "source": {
++ "type": "object",
++ "properties": {
++ "id": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "kind": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "ref": {
++ "type": [
++ "object",
++ "null"
++ ]
++ },
++ "key": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "url": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ }
++ }
++ },
++ "merged": {
++ "type": "object",
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "slug": {
++ "type": "string"
++ }
++ }
++ },
++ "from": {
++ "type": "object",
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "slug": {
++ "type": "string"
++ }
++ }
++ },
++ "before": {},
++ "after": {}
++ }
++}
ddiff --git a/contracts/deals/offer-submit-request.v1.json b/contracts/deals/offer-submit-request.v1.json
nnew file mode 100644
iindex 00000000..0b8e6ff3
---- /dev/null
++++ b/contracts/deals/offer-submit-request.v1.json
@@@ -0,0 +1,155 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/deals/offer-submit-request.v1.json",
++ "title": "DealsOfferSubmitRequest",
++ "description": "deals.offer-submit-request@1: the body of POST /api/v1/offers on OpenVibe.Deals (deals.offer.submit; a signed-in person, or a service acting for one; X-OV-Origin: ai holds the text for review). url (an http(s) link to the offer; a link already posted is 409 offer.duplicate) and title (3-200 characters) are required; a price or shipping cost needs its currency; expires_at may not be in the past. The first price observation is recorded with it.",
++ "type": "object",
++ "required": [
++ "url",
++ "title"
++ ],
++ "properties": {
++ "url": {
++ "type": "string",
++ "pattern": "^https?://"
++ },
++ "title": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 200
++ },
++ "description": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 4000
++ },
++ "category": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 60
++ },
++ "expires_at": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "store_name": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 120
++ },
++ "product_slug": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "product": {
++ "type": "object",
++ "description": "Product aliases to resolve (or create) the product.",
++ "properties": {
++ "name": {
++ "type": "string"
++ },
++ "brand": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "gtin": {
++ "type": "string",
++ "pattern": "^[0-9]{8,14}$"
++ },
++ "mpn": {
++ "type": "string"
++ },
++ "sku": {
++ "type": "string"
++ },
++ "url": {
++ "type": "string"
++ },
++ "category": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "description": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ },
++ "price": {
++ "type": [
++ "string",
++ "number",
++ "null"
++ ],
++ "description": "A decimal amount; always with its currency."
++ },
++ "currency": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^[A-Za-z]{3}$",
++ "description": "ISO 4217; Deals never assumes one."
++ },
++ "shipping": {
++ "type": [
++ "string",
++ "number",
++ "null"
++ ],
++ "description": "A decimal amount; always with its currency."
++ },
++ "shipping_note": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 200
++ },
++ "condition": {
++ "enum": [
++ "new",
++ "used",
++ "refurbished",
++ "damaged",
++ null
++ ]
++ },
++ "availability": {
++ "enum": [
++ "in_stock",
++ "out_of_stock",
++ "preorder",
++ "discontinued",
++ "limited",
++ "sold_out",
++ "online_only",
++ "in_store_only",
++ null
++ ]
++ },
++ "note": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 300
++ }
++ }
++}
ddiff --git a/contracts/deals/offer-update-request.v1.json b/contracts/deals/offer-update-request.v1.json
nnew file mode 100644
iindex 00000000..46a2ef8a
---- /dev/null
++++ b/contracts/deals/offer-update-request.v1.json
@@@ -0,0 +1,191 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/deals/offer-update-request.v1.json",
++ "title": "DealsOfferUpdateRequest",
++ "description": "deals.offer-update-request@1: bodies of deals.offer.update on OpenVibe.Deals. PATCH /api/v1/offers/:id (the submitter or a moderator): { title?, description?, category?, expires_at?, product_slug? | product?, ai_summary? } (ai_summary only from OpenVibe.AI with X-OV-Origin: ai; product null detaches it). POST /api/v1/offers/:id/observations (a person): a price report — at least a price, shipping cost, availability or condition, with the currency of any amount; services may give observed_at (not in the future).",
++ "anyOf": [
++ {
++ "type": "object",
++ "properties": {
++ "title": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 200
++ },
++ "description": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 4000
++ },
++ "category": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 60
++ },
++ "expires_at": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "product_slug": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "product": {
++ "anyOf": [
++ {
++ "type": "object",
++ "description": "Product aliases to resolve (or create) the product.",
++ "properties": {
++ "name": {
++ "type": "string"
++ },
++ "brand": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "gtin": {
++ "type": "string",
++ "pattern": "^[0-9]{8,14}$"
++ },
++ "mpn": {
++ "type": "string"
++ },
++ "sku": {
++ "type": "string"
++ },
++ "url": {
++ "type": "string"
++ },
++ "category": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "description": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "ai_summary": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 2000
++ }
++ }
++ },
++ {
++ "type": "object",
++ "anyOf": [
++ {
++ "required": [
++ "price"
++ ]
++ },
++ {
++ "required": [
++ "shipping"
++ ]
++ },
++ {
++ "required": [
++ "availability"
++ ]
++ },
++ {
++ "required": [
++ "condition"
++ ]
++ }
++ ],
++ "properties": {
++ "price": {
++ "type": [
++ "string",
++ "number",
++ "null"
++ ],
++ "description": "A decimal amount; always with its currency."
++ },
++ "currency": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^[A-Za-z]{3}$",
++ "description": "ISO 4217; Deals never assumes one."
++ },
++ "shipping": {
++ "type": [
++ "string",
++ "number",
++ "null"
++ ],
++ "description": "A decimal amount; always with its currency."
++ },
++ "shipping_note": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 200
++ },
++ "condition": {
++ "enum": [
++ "new",
++ "used",
++ "refurbished",
++ "damaged",
++ null
++ ]
++ },
++ "availability": {
++ "enum": [
++ "in_stock",
++ "out_of_stock",
++ "preorder",
++ "discontinued",
++ "limited",
++ "sold_out",
++ "online_only",
++ "in_store_only",
++ null
++ ]
++ },
++ "note": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 300
++ },
++ "observed_at": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ }
++ ]
++}
ddiff --git a/contracts/deals/offer.v1.json b/contracts/deals/offer.v1.json
nnew file mode 100644
iindex 00000000..89d6e39c
---- /dev/null
++++ b/contracts/deals/offer.v1.json
@@@ -0,0 +1,501 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/deals/offer.v1.json",
++ "title": "DealsOffer",
++ "description": "deals.offer@1: one deal as OpenVibe.Deals presents it (server/domain/publication.js offerDto; the API and /d/:slug.json): the canonical (root) offer of a merge group with its store, product, price observations (the latest is \"latest\", never \"current\": freshness says whether it still holds), sources, the offers merged into it, votes, hotness and the indexability decision.",
++ "type": "object",
++ "required": [
++ "id",
++ "slug",
++ "url",
++ "title",
++ "link",
++ "status",
++ "origin",
++ "created_at",
++ "observations",
++ "votes",
++ "indexability"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "slug": {
++ "type": "string"
++ },
++ "url": {
++ "type": "string"
++ },
++ "title": {
++ "type": "string"
++ },
++ "description": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "link": {
++ "type": "string",
++ "description": "The offer on the store."
++ },
++ "status": {
++ "type": "string",
++ "description": "active, expired, disabled"
++ },
++ "category": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "origin": {
++ "type": "string"
++ },
++ "submitted_by": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "text_origin": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "review_state": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "ai_summary": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "expires_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "expired_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "expired_reason": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "disabled_reason": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "created_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "updated_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "store": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "domain": {
++ "type": "string"
++ },
++ "name": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "url": {
++ "type": "string"
++ }
++ }
++ },
++ "product": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "slug": {
++ "type": "string"
++ },
++ "name": {
++ "type": "string"
++ },
++ "url": {
++ "type": "string"
++ }
++ }
++ },
++ "latest_observation": {
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "id",
++ "observed_at",
++ "origin",
++ "source"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "observed_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "recorded_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "price": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "currency": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "shipping": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "shipping_note": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "condition": {
++ "enum": [
++ "new",
++ "used",
++ "refurbished",
++ "damaged",
++ null
++ ]
++ },
++ "availability": {
++ "enum": [
++ "in_stock",
++ "out_of_stock",
++ "preorder",
++ "discontinued",
++ "limited",
++ "sold_out",
++ "online_only",
++ "in_store_only",
++ null
++ ]
++ },
++ "origin": {
++ "type": "string"
++ },
++ "observed_by": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "listing": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "source": {
++ "type": "object",
++ "properties": {
++ "id": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "kind": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "ref": {
++ "type": [
++ "object",
++ "null"
++ ]
++ },
++ "key": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "url": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "freshness": {},
++ "observations": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "id",
++ "observed_at",
++ "origin",
++ "source"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "observed_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "recorded_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "price": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "currency": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "shipping": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "shipping_note": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "condition": {
++ "enum": [
++ "new",
++ "used",
++ "refurbished",
++ "damaged",
++ null
++ ]
++ },
++ "availability": {
++ "enum": [
++ "in_stock",
++ "out_of_stock",
++ "preorder",
++ "discontinued",
++ "limited",
++ "sold_out",
++ "online_only",
++ "in_store_only",
++ null
++ ]
++ },
++ "origin": {
++ "type": "string"
++ },
++ "observed_by": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "listing": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "source": {
++ "type": "object",
++ "properties": {
++ "id": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "kind": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "ref": {
++ "type": [
++ "object",
++ "null"
++ ]
++ },
++ "key": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "url": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ }
++ }
++ }
++ },
++ "sources": {
++ "type": "array",
++ "items": {
++ "type": "object"
++ }
++ },
++ "merged_from": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "slug": {
++ "type": "string"
++ },
++ "title": {
++ "type": "string"
++ },
++ "merged_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ }
++ }
++ }
++ },
++ "votes": {
++ "type": "object",
++ "required": [
++ "up",
++ "down",
++ "up_weight",
++ "down_weight"
++ ],
++ "properties": {
++ "up": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "down": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "up_weight": {
++ "type": "number"
++ },
++ "down_weight": {
++ "type": "number"
++ }
++ }
++ },
++ "hotness": {
++ "type": [
++ "object",
++ "null"
++ ]
++ },
++ "indexability": {
++ "type": "object",
++ "required": [
++ "indexable"
++ ],
++ "properties": {
++ "indexable": {
++ "type": "boolean"
++ },
++ "robots": {
++ "type": "string"
++ },
++ "reasons": {
++ "type": "array"
++ }
++ }
++ }
++ }
++}
ddiff --git a/contracts/deals/product-resolve-request.v1.json b/contracts/deals/product-resolve-request.v1.json
nnew file mode 100644
iindex 00000000..f8188c94
---- /dev/null
++++ b/contracts/deals/product-resolve-request.v1.json
@@@ -0,0 +1,46 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/deals/product-resolve-request.v1.json",
++ "title": "DealsProductResolveRequest",
++ "description": "deals.product-resolve-request@1: the body of POST /api/v1/products/resolve on OpenVibe.Deals (deals.product.resolve): product aliases (gtin 8-14 digits, mpn, sku, url, name) to find the product by; with create (default true) a product is made when none matches (name then required, 2-200 characters).",
++ "type": "object",
++ "properties": {
++ "name": {
++ "type": "string"
++ },
++ "brand": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "gtin": {
++ "type": "string",
++ "pattern": "^[0-9]{8,14}$"
++ },
++ "mpn": {
++ "type": "string"
++ },
++ "sku": {
++ "type": "string"
++ },
++ "url": {
++ "type": "string"
++ },
++ "category": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "description": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "create": {
++ "type": "boolean"
++ }
++ }
++}
ddiff --git a/contracts/deals/product-resolve-result.v1.json b/contracts/deals/product-resolve-result.v1.json
nnew file mode 100644
iindex 00000000..260bce59
---- /dev/null
++++ b/contracts/deals/product-resolve-result.v1.json
@@@ -0,0 +1,43 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/deals/product-resolve-result.v1.json",
++ "title": "DealsProductResolveResult",
++ "description": "deals.product-resolve-result@1: the answer of POST /api/v1/products/resolve on OpenVibe.Deals: { product, created, conflicts } — 201 when the product was created; conflicts lists aliases that pointed elsewhere. Nothing matching with create false is 404 product.not_found.",
++ "type": "object",
++ "required": [
++ "product",
++ "created",
++ "conflicts"
++ ],
++ "properties": {
++ "product": {
++ "type": "object",
++ "required": [
++ "id",
++ "slug",
++ "name",
++ "url"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "slug": {
++ "type": "string"
++ },
++ "name": {
++ "type": "string"
++ },
++ "url": {
++ "type": "string"
++ }
++ }
++ },
++ "created": {
++ "type": "boolean"
++ },
++ "conflicts": {
++ "type": "array"
++ }
++ }
++}
ddiff --git a/contracts/deals/vote-request.v1.json b/contracts/deals/vote-request.v1.json
nnew file mode 100644
iindex 00000000..e6a969c0
---- /dev/null
++++ b/contracts/deals/vote-request.v1.json
@@@ -0,0 +1,27 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/deals/vote-request.v1.json",
++ "title": "DealsVoteRequest",
++ "description": "deals.vote-request@1: the body of PUT /api/v1/offers/:id/vote on OpenVibe.Deals (deals.vote.set; a person): { value: 1 | -1 }. DELETE /api/v1/offers/:id/vote (deals.vote.remove) takes no body.",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "value"
++ ],
++ "properties": {
++ "value": {
++ "enum": [
++ 1,
++ -1,
++ "1",
++ "-1"
++ ]
++ }
++ }
++ },
++ {
++ "$ref": "../common/no-body.v1.json"
++ }
++ ]
++}
ddiff --git a/contracts/deals/vote-result.v1.json b/contracts/deals/vote-result.v1.json
nnew file mode 100644
iindex 00000000..52ca7311
---- /dev/null
++++ b/contracts/deals/vote-result.v1.json
@@@ -0,0 +1,62 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/deals/vote-result.v1.json",
++ "title": "DealsVoteResult",
++ "description": "deals.vote-result@1: the answer of PUT and DELETE /api/v1/offers/:id/vote on OpenVibe.Deals: { offer_id, value (0 after a removal), previous, changed, votes } — the vote lands on the canonical offer of a merge group.",
++ "type": "object",
++ "required": [
++ "offer_id",
++ "value",
++ "previous",
++ "changed",
++ "votes"
++ ],
++ "properties": {
++ "offer_id": {
++ "type": "string"
++ },
++ "value": {
++ "enum": [
++ 1,
++ -1,
++ 0
++ ]
++ },
++ "previous": {
++ "enum": [
++ 1,
++ -1,
++ 0,
++ null
++ ]
++ },
++ "changed": {
++ "type": "boolean"
++ },
++ "votes": {
++ "type": "object",
++ "required": [
++ "up",
++ "down",
++ "up_weight",
++ "down_weight"
++ ],
++ "properties": {
++ "up": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "down": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "up_weight": {
++ "type": "number"
++ },
++ "down_weight": {
++ "type": "number"
++ }
++ }
++ }
++ }
++}
ddiff --git a/contracts/deals/watch-request.v1.json b/contracts/deals/watch-request.v1.json
nnew file mode 100644
iindex 00000000..7ceb0d98
---- /dev/null
++++ b/contracts/deals/watch-request.v1.json
@@@ -0,0 +1,104 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/deals/watch-request.v1.json",
++ "title": "DealsWatchRequest",
++ "description": "deals.watch-request@1: the body of POST /api/v1/watches on OpenVibe.Deals (deals.watch.create; the acting person): kind keyword or search (query with at least one word; a search is saved without notifications), product (product, a slug or id), or price_below (max_price and currency, with a product or keywords). label is optional (120 characters). There is a per-person limit (429 watch.limit).",
++ "type": "object",
++ "required": [
++ "kind"
++ ],
++ "properties": {
++ "kind": {
++ "enum": [
++ "keyword",
++ "product",
++ "price_below",
++ "search"
++ ]
++ },
++ "query": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 200
++ },
++ "product": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "max_price": {
++ "type": [
++ "string",
++ "number",
++ "null"
++ ],
++ "description": "A decimal amount; always with its currency."
++ },
++ "currency": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^[A-Za-z]{3}$",
++ "description": "ISO 4217; Deals never assumes one."
++ },
++ "label": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 120
++ }
++ },
++ "allOf": [
++ {
++ "if": {
++ "properties": {
++ "kind": {
++ "enum": [
++ "keyword",
++ "search"
++ ]
++ }
++ }
++ },
++ "then": {
++ "required": [
++ "query"
++ ]
++ }
++ },
++ {
++ "if": {
++ "properties": {
++ "kind": {
++ "const": "product"
++ }
++ }
++ },
++ "then": {
++ "required": [
++ "product"
++ ]
++ }
++ },
++ {
++ "if": {
++ "properties": {
++ "kind": {
++ "const": "price_below"
++ }
++ }
++ },
++ "then": {
++ "required": [
++ "max_price",
++ "currency"
++ ]
++ }
++ }
++ ]
++}
ddiff --git a/contracts/deals/watch-result.v1.json b/contracts/deals/watch-result.v1.json
nnew file mode 100644
iindex 00000000..78824e65
---- /dev/null
++++ b/contracts/deals/watch-result.v1.json
@@@ -0,0 +1,174 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/deals/watch-result.v1.json",
++ "title": "DealsWatchResult",
++ "description": "deals.watch-result@1: answers of the watch routes on OpenVibe.Deals: GET /api/v1/watches → { watches } (the acting person's; deals.watch.read); POST /api/v1/watches → 201 { watch } (deals.watch.create); DELETE /api/v1/watches/:id → { id, deleted: true } (deals.watch.delete; someone else's watch is 404).",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "watches"
++ ],
++ "properties": {
++ "watches": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "id",
++ "kind",
++ "notify",
++ "created_at"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "kind": {
++ "enum": [
++ "keyword",
++ "product",
++ "price_below",
++ "search"
++ ]
++ },
++ "query": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "label": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "notify": {
++ "type": "boolean"
++ },
++ "product": {
++ "type": [
++ "object",
++ "null"
++ ]
++ },
++ "max_price": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "currency": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "created_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "notifications": {
++ "type": "integer",
++ "minimum": 0
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "watch"
++ ],
++ "properties": {
++ "watch": {
++ "type": "object",
++ "required": [
++ "id",
++ "kind",
++ "notify",
++ "created_at"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "kind": {
++ "enum": [
++ "keyword",
++ "product",
++ "price_below",
++ "search"
++ ]
++ },
++ "query": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "label": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "notify": {
++ "type": "boolean"
++ },
++ "product": {
++ "type": [
++ "object",
++ "null"
++ ]
++ },
++ "max_price": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "currency": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "created_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "notifications": {
++ "type": "integer",
++ "minimum": 0
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "id",
++ "deleted"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "deleted": {
++ "const": true
++ }
++ }
++ }
++ ]
++}
ddiff --git a/contracts/trade/alert-read-result.v1.json b/contracts/trade/alert-read-result.v1.json
nnew file mode 100644
iindex 00000000..d900187d
---- /dev/null
++++ b/contracts/trade/alert-read-result.v1.json
@@@ -0,0 +1,80 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/trade/alert-read-result.v1.json",
++ "title": "TradeAlertReadResult",
++ "description": "trade.alert-read-result@1: answers of trade.alert.read on OpenVibe.Trade (the acting person's): GET /api/v1/alerts → { rules }; GET /api/v1/alerts/deliveries?limit= → { deliveries } (newest first, at most 200: which rule fired on what, and the event sent).",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "rules"
++ ],
++ "properties": {
++ "rules": {
++ "type": "array",
++ "items": {
++ "$ref": "alert-rule.v1.json"
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "deliveries"
++ ],
++ "properties": {
++ "deliveries": {
++ "type": "array",
++ "maxItems": 200,
++ "items": {
++ "type": "object",
++ "required": [
++ "id",
++ "rule_id",
++ "trigger"
++ ],
++ "properties": {
++ "id": {
++ "type": [
++ "string",
++ "integer"
++ ]
++ },
++ "rule_id": {
++ "type": "string"
++ },
++ "rule_kind": {
++ "type": "string"
++ },
++ "trigger": {
++ "type": "object",
++ "properties": {
++ "kind": {
++ "type": "string"
++ },
++ "id": {
++ "type": "string"
++ }
++ }
++ },
++ "event_id": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "created_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ }
++ }
++ }
++ }
++ }
++ }
++ ]
++}
ddiff --git a/contracts/trade/alert-rule-request.v1.json b/contracts/trade/alert-rule-request.v1.json
nnew file mode 100644
iindex 00000000..e1655e03
---- /dev/null
++++ b/contracts/trade/alert-rule-request.v1.json
@@@ -0,0 +1,93 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/trade/alert-rule-request.v1.json",
++ "title": "TradeAlertRuleRequest",
++ "description": "trade.alert-rule-request@1: the body of POST /api/v1/alerts on OpenVibe.Trade (trade.alert.create; the acting person): an active instrument by symbol and the rule kind. threshold needs metric, operator (above or below), threshold (a finite decimal) and unit, and may name a currency; filing_type needs 1-10 form_types (a list or comma-separated, e.g. 10-K, 8-K); new_document needs nothing more. There is a per-person limit (429 alert.limit).",
++ "type": "object",
++ "required": [
++ "symbol",
++ "kind"
++ ],
++ "properties": {
++ "symbol": {
++ "type": "string"
++ },
++ "kind": {
++ "enum": [
++ "threshold",
++ "filing_type",
++ "new_document"
++ ]
++ },
++ "metric": {
++ "type": "string",
++ "maxLength": 80
++ },
++ "operator": {
++ "enum": [
++ "above",
++ "below"
++ ]
++ },
++ "threshold": {
++ "type": [
++ "string",
++ "number"
++ ]
++ },
++ "unit": {
++ "type": "string",
++ "maxLength": 20
++ },
++ "currency": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^\\s*[A-Za-z]{3}\\s*$|^$",
++ "description": "ISO 4217."
++ },
++ "form_types": {
++ "type": [
++ "array",
++ "string"
++ ],
++ "items": {
++ "type": "string"
++ }
++ }
++ },
++ "allOf": [
++ {
++ "if": {
++ "properties": {
++ "kind": {
++ "const": "threshold"
++ }
++ }
++ },
++ "then": {
++ "required": [
++ "metric",
++ "operator",
++ "threshold",
++ "unit"
++ ]
++ }
++ },
++ {
++ "if": {
++ "properties": {
++ "kind": {
++ "const": "filing_type"
++ }
++ }
++ },
++ "then": {
++ "required": [
++ "form_types"
++ ]
++ }
++ }
++ ]
++}
ddiff --git a/contracts/trade/alert-rule-result.v1.json b/contracts/trade/alert-rule-result.v1.json
nnew file mode 100644
iindex 00000000..0f9d1f7b
---- /dev/null
++++ b/contracts/trade/alert-rule-result.v1.json
@@@ -0,0 +1,15 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/trade/alert-rule-result.v1.json",
++ "title": "TradeAlertRuleResult",
++ "description": "trade.alert-rule-result@1: the answer of POST /api/v1/alerts on OpenVibe.Trade: 201 { rule }.",
++ "type": "object",
++ "required": [
++ "rule"
++ ],
++ "properties": {
++ "rule": {
++ "$ref": "alert-rule.v1.json"
++ }
++ }
++}
ddiff --git a/contracts/trade/alert-rule.v1.json b/contracts/trade/alert-rule.v1.json
nnew file mode 100644
iindex 00000000..310ac38b
---- /dev/null
++++ b/contracts/trade/alert-rule.v1.json
@@@ -0,0 +1,105 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/trade/alert-rule.v1.json",
++ "title": "TradeAlertRule",
++ "description": "trade.alert-rule@1: one alert rule of a person on OpenVibe.Trade (server/domain/alerts.js ruleDto): a threshold on a metric, filings of some form types, or any new document for an instrument. Alerts inform; they never trade.",
++ "type": "object",
++ "required": [
++ "id",
++ "kind",
++ "status",
++ "instrument"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "kind": {
++ "enum": [
++ "threshold",
++ "filing_type",
++ "new_document"
++ ]
++ },
++ "status": {
++ "type": "string"
++ },
++ "instrument": {
++ "type": "object",
++ "required": [
++ "id"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "symbol": {
++ "type": "string"
++ },
++ "name": {
++ "type": "string"
++ }
++ }
++ },
++ "metric": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "operator": {
++ "enum": [
++ "above",
++ "below",
++ null
++ ]
++ },
++ "threshold": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "unit": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "currency": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "form_types": {
++ "type": [
++ "array",
++ "null"
++ ],
++ "items": {
++ "type": "string"
++ }
++ },
++ "armed": {
++ "type": [
++ "boolean",
++ "null"
++ ]
++ },
++ "created_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "updated_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ }
++ }
++}
ddiff --git a/contracts/trade/context-proposal-result.v1.json b/contracts/trade/context-proposal-result.v1.json
nnew file mode 100644
iindex 00000000..0d4bd263
---- /dev/null
++++ b/contracts/trade/context-proposal-result.v1.json
@@@ -0,0 +1,19 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/trade/context-proposal-result.v1.json",
++ "title": "TradeContextProposalResult",
++ "description": "trade.context-proposal-result@1: the answer of POST /api/v1/instruments/:symbol/context on OpenVibe.Trade: 201 { revision, published } (published only for an editor's revision sent with publish).",
++ "type": "object",
++ "required": [
++ "revision",
++ "published"
++ ],
++ "properties": {
++ "revision": {
++ "$ref": "context-revision.v1.json"
++ },
++ "published": {
++ "type": "boolean"
++ }
++ }
++}
ddiff --git a/contracts/trade/context-proposal.v1.json b/contracts/trade/context-proposal.v1.json
nnew file mode 100644
iindex 00000000..e2c121a4
---- /dev/null
++++ b/contracts/trade/context-proposal.v1.json
@@@ -0,0 +1,167 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/trade/context-proposal.v1.json",
++ "title": "TradeContextProposal",
++ "description": "trade.context-proposal@1: the body of POST /api/v1/instruments/:symbol/context on OpenVibe.Trade (trade.context.propose). An editor: { body, cites?, expected_revision?, publish?, message? } (Markdown up to 20000 characters). OpenVibe.AI (X-OV-Origin: ai): { workflow: { id: trade.summarize_market_context, run_id, version?, model? }, input_sources: [{ source_type, source_id }], output: { summary, observations?, gaps?, citations? }, stub_provider? } — always a draft that needs a person's review. Context that recommends buying, selling or holding, or gives price targets, is refused (422 context.advice_refused).",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "body"
++ ],
++ "properties": {
++ "body": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 20000
++ },
++ "cites": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "properties": {
++ "kind": {
++ "enum": [
++ "observation",
++ "document"
++ ]
++ },
++ "id": {
++ "type": "string"
++ }
++ }
++ }
++ },
++ "expected_revision": {
++ "type": [
++ "integer",
++ "null"
++ ]
++ },
++ "publish": {
++ "type": [
++ "boolean",
++ "string"
++ ]
++ },
++ "message": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "workflow",
++ "output"
++ ],
++ "properties": {
++ "workflow": {
++ "type": "object",
++ "required": [
++ "id"
++ ],
++ "anyOf": [
++ {
++ "required": [
++ "run_id"
++ ]
++ },
++ {
++ "required": [
++ "runId"
++ ]
++ }
++ ],
++ "properties": {
++ "id": {
++ "const": "trade.summarize_market_context"
++ },
++ "run_id": {
++ "type": "string"
++ },
++ "runId": {
++ "type": "string"
++ },
++ "version": {},
++ "model": {}
++ }
++ },
++ "input_sources": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "source_type",
++ "source_id"
++ ],
++ "properties": {
++ "source_type": {
++ "enum": [
++ "trade.document",
++ "trade.observation"
++ ]
++ },
++ "source_id": {
++ "type": "string"
++ }
++ }
++ }
++ },
++ "output": {
++ "type": "object",
++ "required": [
++ "summary"
++ ],
++ "properties": {
++ "summary": {
++ "type": "string",
++ "minLength": 1
++ },
++ "citations": {
++ "type": "array",
++ "items": {
++ "type": "integer",
++ "minimum": 0
++ }
++ },
++ "observations": {
++ "type": "array",
++ "maxItems": 30,
++ "items": {
++ "type": "object",
++ "properties": {
++ "text": {
++ "type": "string"
++ },
++ "observed_at": {
++ "type": "string"
++ },
++ "citations": {
++ "type": "array",
++ "items": {
++ "type": "integer"
++ }
++ }
++ }
++ }
++ },
++ "gaps": {
++ "type": "array",
++ "maxItems": 20,
++ "items": {
++ "type": "string"
++ }
++ }
++ }
++ },
++ "stub_provider": {
++ "type": "boolean"
++ }
++ }
++ }
++ ]
++}
ddiff --git a/contracts/trade/context-read-result.v1.json b/contracts/trade/context-read-result.v1.json
nnew file mode 100644
iindex 00000000..ce5b59c8
---- /dev/null
++++ b/contracts/trade/context-read-result.v1.json
@@@ -0,0 +1,104 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/trade/context-read-result.v1.json",
++ "title": "TradeContextReadResult",
++ "description": "trade.context-read-result@1: answers of trade.context.read on OpenVibe.Trade. GET /api/v1/instruments/:symbol/context → { instrument, published, disclaimer } (published is the published context revision or null; ?all=1 adds pending drafts for editors and services); GET …/context/input → { workflow, input, deliver_to } (what OpenVibe.AI's trade.summarize_market_context workflow takes: the instrument, numbered sources with freshness, and where to POST the result).",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "instrument",
++ "published",
++ "disclaimer"
++ ],
++ "properties": {
++ "instrument": {
++ "$ref": "instrument.v1.json"
++ },
++ "published": {
++ "anyOf": [
++ {
++ "$ref": "context-revision.v1.json"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "disclaimer": {
++ "type": "string",
++ "description": "Information only — not investment advice; no trading here."
++ },
++ "pending": {
++ "type": "array",
++ "items": {
++ "$ref": "context-revision.v1.json"
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "workflow",
++ "input",
++ "deliver_to"
++ ],
++ "properties": {
++ "workflow": {
++ "type": "string"
++ },
++ "input": {
++ "type": "object",
++ "required": [
++ "instrument",
++ "sources"
++ ],
++ "properties": {
++ "instrument": {
++ "type": "object"
++ },
++ "stale_after_hours": {
++ "type": "integer"
++ },
++ "sources": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "source_type",
++ "source_id"
++ ],
++ "properties": {
++ "source_type": {
++ "enum": [
++ "trade.observation",
++ "trade.document"
++ ]
++ },
++ "source_id": {
++ "type": "string"
++ }
++ }
++ }
++ }
++ }
++ },
++ "deliver_to": {
++ "type": "object",
++ "properties": {
++ "method": {
++ "const": "POST"
++ },
++ "path": {
++ "type": "string"
++ },
++ "headers": {
++ "type": "object"
++ }
++ }
++ }
++ }
++ }
++ ]
++}
ddiff --git a/contracts/trade/context-revision.v1.json b/contracts/trade/context-revision.v1.json
nnew file mode 100644
iindex 00000000..c8ae6ec3
---- /dev/null
++++ b/contracts/trade/context-revision.v1.json
@@@ -0,0 +1,107 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/trade/context-revision.v1.json",
++ "title": "TradeContextRevision",
++ "description": "trade.context-revision@1: one revision of an instrument's market context on OpenVibe.Trade (server/domain/context.js view): the Markdown, HTML and text, what it cites (observations and documents; a citation whose source is gone is marked missing), its authorship and disclosure, and whether it still needs a person's review (AI drafts do).",
++ "type": "object",
++ "required": [
++ "revision",
++ "published",
++ "body_markdown",
++ "cites",
++ "needs_review"
++ ],
++ "properties": {
++ "revision": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "published": {
++ "type": "boolean"
++ },
++ "published_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "written_at": {
++ "type": [
++ "string",
++ "integer",
++ "null"
++ ]
++ },
++ "as_of": {},
++ "origin": {
++ "enum": [
++ "editor",
++ "ai",
++ null
++ ]
++ },
++ "body_markdown": {
++ "type": "string"
++ },
++ "body_html": {
++ "type": "string"
++ },
++ "text": {
++ "type": "string"
++ },
++ "cites": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "kind",
++ "id"
++ ],
++ "properties": {
++ "kind": {
++ "enum": [
++ "observation",
++ "document"
++ ]
++ },
++ "id": {
++ "type": "string"
++ },
++ "observation": {
++ "$ref": "observation.v1.json"
++ },
++ "document": {
++ "type": "object"
++ },
++ "missing": {
++ "const": true
++ }
++ }
++ }
++ },
++ "authorship": {},
++ "disclosure": {},
++ "review": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "properties": {
++ "decision": {
++ "type": "string"
++ },
++ "reviewed_at": {},
++ "reviewer": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ },
++ "needs_review": {
++ "type": "boolean"
++ }
++ }
++}
ddiff --git a/contracts/trade/deleted-result.v1.json b/contracts/trade/deleted-result.v1.json
nnew file mode 100644
iindex 00000000..b7a1ede3
---- /dev/null
++++ b/contracts/trade/deleted-result.v1.json
@@@ -0,0 +1,15 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/trade/deleted-result.v1.json",
++ "title": "TradeDeletedResult",
++ "description": "trade.deleted-result@1: the answer of DELETE /api/v1/alerts/:id (trade.alert.delete) and DELETE /api/v1/watchlists/:id (trade.watchlist.delete) on OpenVibe.Trade: { deleted }. Someone else's rule or watchlist is 404.",
++ "type": "object",
++ "required": [
++ "deleted"
++ ],
++ "properties": {
++ "deleted": {
++ "type": "boolean"
++ }
++ }
++}
ddiff --git a/contracts/trade/instrument-manage-request.v1.json b/contracts/trade/instrument-manage-request.v1.json
nnew file mode 100644
iindex 00000000..0dcd7bd8
---- /dev/null
++++ b/contracts/trade/instrument-manage-request.v1.json
@@@ -0,0 +1,132 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/trade/instrument-manage-request.v1.json",
++ "title": "TradeInstrumentManageRequest",
++ "description": "trade.instrument-manage-request@1: bodies of trade.instrument.manage on OpenVibe.Trade (editors, or a service with the capability). POST /api/v1/instruments: { symbol, name, kind?, exchange?, cik?, currency? } (kind defaults to equity; aliases for the symbol, CIK and name are added). PATCH /api/v1/instruments/:symbol: any of name, kind, exchange, cik, currency, status (active or archived). POST /api/v1/instruments/:symbol/aliases: { kind: ticker | cik | name, value }. Text fields must be strings; a symbol or CIK another instrument holds is 409.",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "symbol",
++ "name"
++ ],
++ "properties": {
++ "symbol": {
++ "type": "string",
++ "pattern": "^\\s*[A-Za-z0-9][A-Za-z0-9.-]{0,15}\\s*$",
++ "description": "1-16 characters of A-Z, 0-9, . or - (upper-cased)."
++ },
++ "name": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 200
++ },
++ "kind": {
++ "enum": [
++ "equity",
++ "fund",
++ "index",
++ "currency",
++ "commodity",
++ "crypto",
++ "other",
++ null,
++ ""
++ ]
++ },
++ "exchange": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 40
++ },
++ "cik": {
++ "type": [
++ "string",
++ "integer",
++ "null"
++ ],
++ "description": "1-10 digits (zero-padded to 10)."
++ },
++ "currency": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^\\s*[A-Za-z]{3}\\s*$|^$",
++ "description": "ISO 4217."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "properties": {
++ "name": {
++ "type": "string",
++ "maxLength": 200
++ },
++ "kind": {
++ "enum": [
++ "equity",
++ "fund",
++ "index",
++ "currency",
++ "commodity",
++ "crypto",
++ "other"
++ ]
++ },
++ "exchange": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 40
++ },
++ "cik": {
++ "type": [
++ "string",
++ "integer",
++ "null"
++ ]
++ },
++ "currency": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^\\s*[A-Za-z]{3}\\s*$|^$",
++ "description": "ISO 4217."
++ },
++ "status": {
++ "enum": [
++ "active",
++ "archived"
++ ]
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "kind",
++ "value"
++ ],
++ "properties": {
++ "kind": {
++ "enum": [
++ "ticker",
++ "cik",
++ "name"
++ ]
++ },
++ "value": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 200
++ }
++ }
++ }
++ ]
++}
ddiff --git a/contracts/trade/instrument-manage-result.v1.json b/contracts/trade/instrument-manage-result.v1.json
nnew file mode 100644
iindex 00000000..709d1e6c
---- /dev/null
++++ b/contracts/trade/instrument-manage-result.v1.json
@@@ -0,0 +1,71 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/trade/instrument-manage-result.v1.json",
++ "title": "TradeInstrumentManageResult",
++ "description": "trade.instrument-manage-result@1: answers of trade.instrument.manage on OpenVibe.Trade: POST /api/v1/instruments → 201 { instrument, aliases }; PATCH → { instrument }; POST …/aliases → 201 { alias, aliases } (alias { kind, value, normalized }).",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "instrument"
++ ],
++ "properties": {
++ "instrument": {
++ "$ref": "instrument.v1.json"
++ },
++ "aliases": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "properties": {
++ "kind": {
++ "type": "string"
++ },
++ "value": {
++ "type": "string"
++ },
++ "normalized": {
++ "type": "string"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "alias",
++ "aliases"
++ ],
++ "properties": {
++ "alias": {
++ "type": "object",
++ "required": [
++ "kind",
++ "value",
++ "normalized"
++ ],
++ "properties": {
++ "kind": {
++ "enum": [
++ "ticker",
++ "cik",
++ "name"
++ ]
++ },
++ "value": {
++ "type": "string"
++ },
++ "normalized": {
++ "type": "string"
++ }
++ }
++ },
++ "aliases": {
++ "type": "array"
++ }
++ }
++ }
++ ]
++}
ddiff --git a/contracts/trade/instrument.v1.json b/contracts/trade/instrument.v1.json
nnew file mode 100644
iindex 00000000..6196de30
---- /dev/null
++++ b/contracts/trade/instrument.v1.json
@@@ -0,0 +1,79 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/trade/instrument.v1.json",
++ "title": "TradeInstrument",
++ "description": "trade.instrument@1: one instrument as OpenVibe.Trade's API shows it (server/domain/reading.js instrumentDto).",
++ "type": "object",
++ "required": [
++ "id",
++ "symbol",
++ "name",
++ "kind",
++ "status",
++ "url"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "symbol": {
++ "type": "string"
++ },
++ "name": {
++ "type": "string"
++ },
++ "kind": {
++ "enum": [
++ "equity",
++ "fund",
++ "index",
++ "currency",
++ "commodity",
++ "crypto",
++ "other"
++ ]
++ },
++ "exchange": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "cik": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^[0-9]{10}$"
++ },
++ "currency": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "status": {
++ "enum": [
++ "active",
++ "archived"
++ ]
++ },
++ "url": {
++ "type": "string"
++ },
++ "created_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "updated_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ }
++ }
++}
ddiff --git a/contracts/trade/observation-request.v1.json b/contracts/trade/observation-request.v1.json
nnew file mode 100644
iindex 00000000..69341ffd
---- /dev/null
++++ b/contracts/trade/observation-request.v1.json
@@@ -0,0 +1,102 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/trade/observation-request.v1.json",
++ "title": "TradeObservationRequest",
++ "description": "trade.observation-request@1: the body of POST /api/v1/observations on OpenVibe.Trade (trade.observation.write; services only): one value a source stated for an instrument (symbol or instrument_id). value is the finite decimal as stated; observed_at and retrieved_at are date-times with a zone and not in the future; source_key names the OpenVibe.Sources source and source_ref the statement within it. The same source reference with the same value is answered as it was (200); a different value under the same reference is 409 observation.conflict (a correction needs its own source_ref).",
++ "type": "object",
++ "required": [
++ "metric",
++ "value",
++ "unit",
++ "observed_at",
++ "retrieved_at",
++ "source_key",
++ "source_ref"
++ ],
++ "anyOf": [
++ {
++ "required": [
++ "symbol"
++ ]
++ },
++ {
++ "required": [
++ "instrument_id"
++ ]
++ }
++ ],
++ "properties": {
++ "symbol": {
++ "type": "string"
++ },
++ "instrument_id": {
++ "type": "string"
++ },
++ "metric": {
++ "type": "string",
++ "pattern": "^\\s*[A-Za-z][A-Za-z0-9_.:-]{0,79}\\s*$"
++ },
++ "value": {
++ "type": [
++ "string",
++ "number"
++ ]
++ },
++ "unit": {
++ "type": "string",
++ "pattern": "^\\s*[A-Za-z][A-Za-z0-9/%._-]{0,19}\\s*$"
++ },
++ "currency": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^\\s*([A-Z]{3})?\\s*$"
++ },
++ "period": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 40
++ },
++ "observed_at": {
++ "type": "string"
++ },
++ "retrieved_at": {
++ "type": "string"
++ },
++ "source_key": {
++ "type": "string",
++ "pattern": "^\\s*[a-z0-9][a-z0-9-]{1,63}\\s*$"
++ },
++ "source_ref": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 300
++ },
++ "source_item_id": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^(itm_[0-9A-HJKMNP-TV-Z]{26})?$"
++ },
++ "source_url": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^https?://"
++ },
++ "max_age_sec": {
++ "type": [
++ "integer",
++ "string",
++ "null"
++ ],
++ "minimum": 60,
++ "maximum": 315360000
++ }
++ }
++}
ddiff --git a/contracts/trade/observation-result.v1.json b/contracts/trade/observation-result.v1.json
nnew file mode 100644
iindex 00000000..f6cc9cea
---- /dev/null
++++ b/contracts/trade/observation-result.v1.json
@@@ -0,0 +1,19 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/trade/observation-result.v1.json",
++ "title": "TradeObservationResult",
++ "description": "trade.observation-result@1: the answer of POST /api/v1/observations on OpenVibe.Trade: { observation, created } — 201 when recorded (trade.observation.created is announced, alerts are checked), 200 for a repeat of the same statement.",
++ "type": "object",
++ "required": [
++ "observation",
++ "created"
++ ],
++ "properties": {
++ "observation": {
++ "$ref": "observation.v1.json"
++ },
++ "created": {
++ "type": "boolean"
++ }
++ }
++}
ddiff --git a/contracts/trade/observation.v1.json b/contracts/trade/observation.v1.json
nnew file mode 100644
iindex 00000000..fd0b70b7
---- /dev/null
++++ b/contracts/trade/observation.v1.json
@@@ -0,0 +1,122 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/trade/observation.v1.json",
++ "title": "TradeObservation",
++ "description": "trade.observation@1: one market observation on OpenVibe.Trade (server/domain/observations.js dto): a value a source stated (the decimal exactly as stated), when it was true and when it was fetched, where from, and (in reads) whether it is stale by its source's freshness or its own max age. Rows are immutable.",
++ "type": "object",
++ "required": [
++ "id",
++ "metric",
++ "value",
++ "unit",
++ "observed_at",
++ "retrieved_at",
++ "recorded_at",
++ "source"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "metric": {
++ "type": "string"
++ },
++ "value": {
++ "type": "string",
++ "description": "The decimal as the source stated it."
++ },
++ "unit": {
++ "type": "string"
++ },
++ "currency": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "period": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "observed_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "retrieved_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "recorded_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "max_age_sec": {
++ "type": [
++ "integer",
++ "null"
++ ]
++ },
++ "source": {
++ "type": "object",
++ "required": [
++ "key"
++ ],
++ "properties": {
++ "key": {
++ "type": "string"
++ },
++ "item_id": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "url": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "ref": {
++ "type": "string"
++ }
++ }
++ },
++ "freshness": {
++ "type": "object",
++ "properties": {
++ "stale": {
++ "type": "boolean"
++ },
++ "stale_since": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "reason": {
++ "enum": [
++ "value_older_than_max_age",
++ "source_unknown",
++ "source_stale",
++ null
++ ]
++ },
++ "source_status": {},
++ "source_last_success_at": {}
++ }
++ }
++ }
++}
ddiff --git a/contracts/trade/resolve-result.v1.json b/contracts/trade/resolve-result.v1.json
nnew file mode 100644
iindex 00000000..c9cf6177
---- /dev/null
++++ b/contracts/trade/resolve-result.v1.json
@@@ -0,0 +1,53 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/trade/resolve-result.v1.json",
++ "title": "TradeResolveResult",
++ "description": "trade.resolve-result@1: the answer of GET /api/v1/instruments/resolve?q=&kind= on OpenVibe.Trade (trade.instrument.resolve): { query, status: resolved | ambiguous | not_found, match: { kind, value } | null, instrument | null, candidates } (a CIK, symbol or alias matched; candidates when more than one could be meant).",
++ "type": "object",
++ "required": [
++ "status",
++ "match",
++ "instrument",
++ "candidates"
++ ],
++ "properties": {
++ "query": {},
++ "status": {
++ "enum": [
++ "resolved",
++ "ambiguous",
++ "not_found"
++ ]
++ },
++ "match": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "properties": {
++ "kind": {
++ "type": "string"
++ },
++ "value": {
++ "type": "string"
++ }
++ }
++ },
++ "instrument": {
++ "anyOf": [
++ {
++ "$ref": "instrument.v1.json"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "candidates": {
++ "type": "array",
++ "items": {
++ "$ref": "instrument.v1.json"
++ }
++ }
++ }
++}
ddiff --git a/contracts/trade/watchlist-request.v1.json b/contracts/trade/watchlist-request.v1.json
nnew file mode 100644
iindex 00000000..c28251ab
---- /dev/null
++++ b/contracts/trade/watchlist-request.v1.json
@@@ -0,0 +1,33 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/trade/watchlist-request.v1.json",
++ "title": "TradeWatchlistRequest",
++ "description": "trade.watchlist-request@1: bodies of the watchlist writes on OpenVibe.Trade (the acting person). POST /api/v1/watchlists (trade.watchlist.create) and PATCH /api/v1/watchlists/:id (trade.watchlist.update): { name } (a string, at most 80 characters; your names are unique). PUT /api/v1/watchlists/:id/items/:symbol: { note? } (at most 300 characters). DELETE …/items/:symbol takes no body.",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "name"
++ ],
++ "properties": {
++ "name": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 80
++ }
++ }
++ },
++ {
++ "type": "object",
++ "properties": {
++ "note": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 300
++ }
++ }
++ }
++ ]
++}
ddiff --git a/contracts/trade/watchlist-result.v1.json b/contracts/trade/watchlist-result.v1.json
nnew file mode 100644
iindex 00000000..60d67544
---- /dev/null
++++ b/contracts/trade/watchlist-result.v1.json
@@@ -0,0 +1,39 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/trade/watchlist-result.v1.json",
++ "title": "TradeWatchlistResult",
++ "description": "trade.watchlist-result@1: answers of the watchlist routes on OpenVibe.Trade: GET /api/v1/watchlists → { watchlists }; GET /api/v1/watchlists/:id, POST (201) and PATCH → { watchlist }; PUT …/items/:symbol → { added, watchlist }; DELETE …/items/:symbol → { removed, watchlist }.",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "watchlists"
++ ],
++ "properties": {
++ "watchlists": {
++ "type": "array",
++ "items": {
++ "$ref": "watchlist.v1.json"
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "watchlist"
++ ],
++ "properties": {
++ "watchlist": {
++ "$ref": "watchlist.v1.json"
++ },
++ "added": {
++ "type": "boolean"
++ },
++ "removed": {
++ "type": "boolean"
++ }
++ }
++ }
++ ]
++}
ddiff --git a/contracts/trade/watchlist.v1.json b/contracts/trade/watchlist.v1.json
nnew file mode 100644
iindex 00000000..c4d6aaa9
---- /dev/null
++++ b/contracts/trade/watchlist.v1.json
@@@ -0,0 +1,91 @@
++{
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/trade/watchlist.v1.json",
++ "title": "TradeWatchlist",
++ "description": "trade.watchlist@1: one of a person's watchlists on OpenVibe.Trade (server/domain/watchlists.js dto) with its instruments and notes.",
++ "type": "object",
++ "required": [
++ "id",
++ "name"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "name": {
++ "type": "string",
++ "maxLength": 80
++ },
++ "created_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "updated_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "items": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "instrument"
++ ],
++ "properties": {
++ "instrument": {
++ "type": "object",
++ "required": [
++ "id",
++ "symbol"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "symbol": {
++ "type": "string"
++ },
++ "name": {
++ "type": "string"
++ },
++ "kind": {
++ "type": "string"
++ },
++ "exchange": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "status": {
++ "type": "string"
++ },
++ "url": {
++ "type": "string"
++ }
++ }
++ },
++ "note": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "added_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ }
++ }
++ }
++ }
++ }
++}
ddiff --git a/fixtures/deals.flag-request/invalid/duplicate-without-target.json b/fixtures/deals.flag-request/invalid/duplicate-without-target.json
nnew file mode 100644
iindex 00000000..b95f48eb
---- /dev/null
++++ b/fixtures/deals.flag-request/invalid/duplicate-without-target.json
@@@ -0,0 +1 @@
++{ "kind": "duplicate", "reason": "seen it" }
ddiff --git a/fixtures/deals.flag-request/valid/duplicate.json b/fixtures/deals.flag-request/valid/duplicate.json
nnew file mode 100644
iindex 00000000..f1ca157b
---- /dev/null
++++ b/fixtures/deals.flag-request/valid/duplicate.json
@@@ -0,0 +1 @@
++{ "kind": "duplicate", "duplicate_of": "https://openvibe.deals/d/usb-c-capture-card-79" }
ddiff --git a/fixtures/deals.flag-result/invalid/no-flag.json b/fixtures/deals.flag-result/invalid/no-flag.json
nnew file mode 100644
iindex 00000000..ae5f7c88
---- /dev/null
++++ b/fixtures/deals.flag-result/invalid/no-flag.json
@@@ -0,0 +1 @@
++{ "created": true }
ddiff --git a/fixtures/deals.flag-result/valid/created.json b/fixtures/deals.flag-result/valid/created.json
nnew file mode 100644
iindex 00000000..8ec1d4b9
---- /dev/null
++++ b/fixtures/deals.flag-result/valid/created.json
@@@ -0,0 +1 @@
++{ "flag": { "id": "dfl_01JAB2C3D4E5F6G7H8J9K0MNPQ", "kind": "expired", "status": "open" }, "created": true }
ddiff --git a/fixtures/deals.moderation-request/invalid/bad-status.json b/fixtures/deals.moderation-request/invalid/bad-status.json
nnew file mode 100644
iindex 00000000..7a61b1dd
---- /dev/null
++++ b/fixtures/deals.moderation-request/invalid/bad-status.json
@@@ -0,0 +1 @@
++{ "status": "deleted", "reason": 42 }
ddiff --git a/fixtures/deals.moderation-request/valid/dismiss.json b/fixtures/deals.moderation-request/valid/dismiss.json
nnew file mode 100644
iindex 00000000..bb51ffba
---- /dev/null
++++ b/fixtures/deals.moderation-request/valid/dismiss.json
@@@ -0,0 +1 @@
++{ "status": "dismissed", "resolution": "price was right" }
ddiff --git a/fixtures/deals.moderation-result/invalid/bad-flag-status.json b/fixtures/deals.moderation-result/invalid/bad-flag-status.json
nnew file mode 100644
iindex 00000000..c07d3195
---- /dev/null
++++ b/fixtures/deals.moderation-result/invalid/bad-flag-status.json
@@@ -0,0 +1 @@
++{ "flags": [ { "id": "x", "kind": "spam", "origin": "user", "status": "closed" } ] }
ddiff --git a/fixtures/deals.moderation-result/valid/flags.json b/fixtures/deals.moderation-result/valid/flags.json
nnew file mode 100644
iindex 00000000..b48a7382
---- /dev/null
++++ b/fixtures/deals.moderation-result/valid/flags.json
@@@ -0,0 +1 @@
++{ "flags": [ { "id": "dfl_01JAB2C3D4E5F6G7H8J9K0MNPQ", "kind": "price_wrong", "origin": "user", "reporter": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "reason": "now $99", "status": "open", "details": {}, "created_at": "2026-09-25T18:00:00.000Z", "updated_at": "2026-09-25T18:00:00.000Z", "resolved_by": null, "resolved_at": null, "resolution": null, "offer": null } ] }
ddiff --git a/fixtures/deals.offer-action-request/invalid/bad.json b/fixtures/deals.offer-action-request/invalid/bad.json
nnew file mode 100644
iindex 00000000..af8c1318
---- /dev/null
++++ b/fixtures/deals.offer-action-request/invalid/bad.json
@@@ -0,0 +1 @@
++{ "into": "", "reason": 5 }
ddiff --git a/fixtures/deals.offer-action-request/valid/expire.json b/fixtures/deals.offer-action-request/valid/expire.json
nnew file mode 100644
iindex 00000000..cd1ebe3f
---- /dev/null
++++ b/fixtures/deals.offer-action-request/valid/expire.json
@@@ -0,0 +1 @@
++{ "reason": "sold out everywhere" }
ddiff --git a/fixtures/deals.offer-action-request/valid/merge.json b/fixtures/deals.offer-action-request/valid/merge.json
nnew file mode 100644
iindex 00000000..73534ffb
---- /dev/null
++++ b/fixtures/deals.offer-action-request/valid/merge.json
@@@ -0,0 +1 @@
++{ "into": "usb-c-capture-card-79", "reason": "same listing" }
ddiff --git a/fixtures/deals.offer-result/invalid/no-offer.json b/fixtures/deals.offer-result/invalid/no-offer.json
nnew file mode 100644
iindex 00000000..3384c0bd
---- /dev/null
++++ b/fixtures/deals.offer-result/invalid/no-offer.json
@@@ -0,0 +1 @@
++{ "observation": {} }
ddiff --git a/fixtures/deals.offer-result/valid/observed.json b/fixtures/deals.offer-result/valid/observed.json
nnew file mode 100644
iindex 00000000..c164f74d
---- /dev/null
++++ b/fixtures/deals.offer-result/valid/observed.json
@@@ -0,0 +1 @@
++{ "offer": { "id": "dof_01JAB2C3D4E5F6G7H8J9K0MNPR", "slug": "usb-c-capture-card-79", "url": "https://openvibe.deals/d/usb-c-capture-card-79", "title": "USB-C 4K capture card for $79.99", "description": null, "link": "https://shop.example.com/capture-card", "status": "active", "category": "streaming", "origin": "community", "submitted_by": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "text_origin": "human", "review_state": null, "ai_summary": null, "expires_at": null, "expired_at": null, "expired_reason": null, "disabled_reason": null, "created_at": "2026-09-25T18:00:00.000Z", "updated_at": "2026-09-25T18:00:00.000Z", "store": { "id": "dst_1", "domain": "shop.example.com", "name": "Example Shop", "url": "https://openvibe.deals/s/shop.example.com" }, "product": null, "latest_observation": { "id": "dob_01JAB2C3D4E5F6G7H8J9K0MNPQ", "observed_at": "2026-09-25T18:00:00.000Z", "recorded_at": "2026-09-25T18:00:02.000Z", "price": "79.99", "currency": "USD", "shipping": "0", "shipping_note": null, "condition": "new", "availability": "in_stock", "origin": "community", "observed_by": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "listing": "dof_01JAB2C3D4E5F6G7H8J9K0MNPR", "source": { "id": "dsr_01JAB2C3D4E5F6G7H8J9K0MNPS", "kind": "community", "ref": null, "key": null, "url": null } }, "freshness": { "state": "fresh" }, "observations": [ { "id": "dob_01JAB2C3D4E5F6G7H8J9K0MNPQ", "observed_at": "2026-09-25T18:00:00.000Z", "recorded_at": "2026-09-25T18:00:02.000Z", "price": "79.99", "currency": "USD", "shipping": "0", "shipping_note": null, "condition": "new", "availability": "in_stock", "origin": "community", "observed_by": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "listing": "dof_01JAB2C3D4E5F6G7H8J9K0MNPR", "source": { "id": "dsr_01JAB2C3D4E5F6G7H8J9K0MNPS", "kind": "community", "ref": null, "key": null, "url": null } } ], "sources": [], "merged_from": [], "votes": { "up": 12, "down": 1, "up_weight": 11.5, "down_weight": 1 }, "hotness": null, "indexability": { "indexable": true, "robots": "index,follow", "reasons": [] } }, "observation": { "id": "dob_01JAB2C3D4E5F6G7H8J9K0MNPQ", "observed_at": "2026-09-25T18:00:00.000Z", "recorded_at": "2026-09-25T18:00:02.000Z", "price": "79.99", "currency": "USD", "shipping": "0", "shipping_note": null, "condition": "new", "availability": "in_stock", "origin": "community", "observed_by": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "listing": "dof_01JAB2C3D4E5F6G7H8J9K0MNPR", "source": { "id": "dsr_01JAB2C3D4E5F6G7H8J9K0MNPS", "kind": "community", "ref": null, "key": null, "url": null } } }
ddiff --git a/fixtures/deals.offer-submit-request/invalid/not-http.json b/fixtures/deals.offer-submit-request/invalid/not-http.json
nnew file mode 100644
iindex 00000000..3856af77
---- /dev/null
++++ b/fixtures/deals.offer-submit-request/invalid/not-http.json
@@@ -0,0 +1 @@
++{ "url": "ftp://shop.example.com/x", "title": "ok title" }
ddiff --git a/fixtures/deals.offer-submit-request/valid/with-price.json b/fixtures/deals.offer-submit-request/valid/with-price.json
nnew file mode 100644
iindex 00000000..6acc4130
---- /dev/null
++++ b/fixtures/deals.offer-submit-request/valid/with-price.json
@@@ -0,0 +1 @@
++{ "url": "https://shop.example.com/capture-card", "title": "USB-C 4K capture card for $79.99", "category": "streaming", "price": "79.99", "currency": "USD", "shipping": "0", "availability": "in_stock", "condition": "new" }
ddiff --git a/fixtures/deals.offer-update-request/invalid/bad-values.json b/fixtures/deals.offer-update-request/invalid/bad-values.json
nnew file mode 100644
iindex 00000000..ad2b6edb
---- /dev/null
++++ b/fixtures/deals.offer-update-request/invalid/bad-values.json
@@@ -0,0 +1 @@
++{ "title": "x", "condition": "mint" }
ddiff --git a/fixtures/deals.offer-update-request/valid/observation.json b/fixtures/deals.offer-update-request/valid/observation.json
nnew file mode 100644
iindex 00000000..927bf5bf
---- /dev/null
++++ b/fixtures/deals.offer-update-request/valid/observation.json
@@@ -0,0 +1 @@
++{ "price": "74.99", "currency": "USD", "availability": "limited" }
ddiff --git a/fixtures/deals.offer-update-request/valid/patch.json b/fixtures/deals.offer-update-request/valid/patch.json
nnew file mode 100644
iindex 00000000..7e81c22c
---- /dev/null
++++ b/fixtures/deals.offer-update-request/valid/patch.json
@@@ -0,0 +1 @@
++{ "expires_at": "2026-10-01T00:00:00Z", "category": "streaming" }
ddiff --git a/fixtures/deals.offer/invalid/negative-votes.json b/fixtures/deals.offer/invalid/negative-votes.json
nnew file mode 100644
iindex 00000000..c540e4e1
---- /dev/null
++++ b/fixtures/deals.offer/invalid/negative-votes.json
@@@ -0,0 +1 @@
++{ "id": "d", "slug": "s", "url": "u", "title": "t", "link": "l", "status": "active", "origin": "community", "created_at": null, "observations": [], "votes": { "up": -1, "down": 0, "up_weight": 0, "down_weight": 0 }, "indexability": { "indexable": true } }
ddiff --git a/fixtures/deals.offer/valid/active.json b/fixtures/deals.offer/valid/active.json
nnew file mode 100644
iindex 00000000..2fa4c670
---- /dev/null
++++ b/fixtures/deals.offer/valid/active.json
@@@ -0,0 +1 @@
++{ "id": "dof_01JAB2C3D4E5F6G7H8J9K0MNPR", "slug": "usb-c-capture-card-79", "url": "https://openvibe.deals/d/usb-c-capture-card-79", "title": "USB-C 4K capture card for $79.99", "description": null, "link": "https://shop.example.com/capture-card", "status": "active", "category": "streaming", "origin": "community", "submitted_by": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "text_origin": "human", "review_state": null, "ai_summary": null, "expires_at": null, "expired_at": null, "expired_reason": null, "disabled_reason": null, "created_at": "2026-09-25T18:00:00.000Z", "updated_at": "2026-09-25T18:00:00.000Z", "store": { "id": "dst_1", "domain": "shop.example.com", "name": "Example Shop", "url": "https://openvibe.deals/s/shop.example.com" }, "product": null, "latest_observation": { "id": "dob_01JAB2C3D4E5F6G7H8J9K0MNPQ", "observed_at": "2026-09-25T18:00:00.000Z", "recorded_at": "2026-09-25T18:00:02.000Z", "price": "79.99", "currency": "USD", "shipping": "0", "shipping_note": null, "condition": "new", "availability": "in_stock", "origin": "community", "observed_by": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "listing": "dof_01JAB2C3D4E5F6G7H8J9K0MNPR", "source": { "id": "dsr_01JAB2C3D4E5F6G7H8J9K0MNPS", "kind": "community", "ref": null, "key": null, "url": null } }, "freshness": { "state": "fresh" }, "observations": [ { "id": "dob_01JAB2C3D4E5F6G7H8J9K0MNPQ", "observed_at": "2026-09-25T18:00:00.000Z", "recorded_at": "2026-09-25T18:00:02.000Z", "price": "79.99", "currency": "USD", "shipping": "0", "shipping_note": null, "condition": "new", "availability": "in_stock", "origin": "community", "observed_by": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "listing": "dof_01JAB2C3D4E5F6G7H8J9K0MNPR", "source": { "id": "dsr_01JAB2C3D4E5F6G7H8J9K0MNPS", "kind": "community", "ref": null, "key": null, "url": null } } ], "sources": [], "merged_from": [], "votes": { "up": 12, "down": 1, "up_weight": 11.5, "down_weight": 1 }, "hotness": null, "indexability": { "indexable": true, "robots": "index,follow", "reasons": [] } }
ddiff --git a/fixtures/deals.product-resolve-request/invalid/bad-gtin.json b/fixtures/deals.product-resolve-request/invalid/bad-gtin.json
nnew file mode 100644
iindex 00000000..b99b9aa2
---- /dev/null
++++ b/fixtures/deals.product-resolve-request/invalid/bad-gtin.json
@@@ -0,0 +1 @@
++{ "gtin": "12AB" }
ddiff --git a/fixtures/deals.product-resolve-request/valid/gtin.json b/fixtures/deals.product-resolve-request/valid/gtin.json
nnew file mode 100644
iindex 00000000..620ca0a9
---- /dev/null
++++ b/fixtures/deals.product-resolve-request/valid/gtin.json
@@@ -0,0 +1 @@
++{ "name": "Example 4K Capture", "brand": "Example", "gtin": "0123456789012" }
ddiff --git a/fixtures/deals.product-resolve-result/invalid/no-product.json b/fixtures/deals.product-resolve-result/invalid/no-product.json
nnew file mode 100644
iindex 00000000..b214482e
---- /dev/null
++++ b/fixtures/deals.product-resolve-result/invalid/no-product.json
@@@ -0,0 +1 @@
++{ "created": true, "conflicts": [] }
ddiff --git a/fixtures/deals.product-resolve-result/valid/created.json b/fixtures/deals.product-resolve-result/valid/created.json
nnew file mode 100644
iindex 00000000..64aab67c
---- /dev/null
++++ b/fixtures/deals.product-resolve-result/valid/created.json
@@@ -0,0 +1 @@
++{ "product": { "id": "dpr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "slug": "example-4k-capture", "name": "Example 4K Capture", "url": "https://openvibe.deals/p/example-4k-capture" }, "created": true, "conflicts": [] }
ddiff --git a/fixtures/deals.vote-request/invalid/two.json b/fixtures/deals.vote-request/invalid/two.json
nnew file mode 100644
iindex 00000000..5eefaaee
---- /dev/null
++++ b/fixtures/deals.vote-request/invalid/two.json
@@@ -0,0 +1 @@
++{ "value": 2 }
ddiff --git a/fixtures/deals.vote-request/valid/down.json b/fixtures/deals.vote-request/valid/down.json
nnew file mode 100644
iindex 00000000..ca57eb68
---- /dev/null
++++ b/fixtures/deals.vote-request/valid/down.json
@@@ -0,0 +1 @@
++{ "value": -1 }
ddiff --git a/fixtures/deals.vote-result/invalid/bad-value.json b/fixtures/deals.vote-result/invalid/bad-value.json
nnew file mode 100644
iindex 00000000..f6d9e4d8
---- /dev/null
++++ b/fixtures/deals.vote-result/invalid/bad-value.json
@@@ -0,0 +1 @@
++{ "offer_id": "d", "value": 5, "previous": 0, "changed": true, "votes": { "up": 0, "down": 0, "up_weight": 0, "down_weight": 0 } }
ddiff --git a/fixtures/deals.vote-result/valid/up.json b/fixtures/deals.vote-result/valid/up.json
nnew file mode 100644
iindex 00000000..182c3ce1
---- /dev/null
++++ b/fixtures/deals.vote-result/valid/up.json
@@@ -0,0 +1 @@
++{ "offer_id": "dof_01JAB2C3D4E5F6G7H8J9K0MNPR", "value": 1, "previous": 0, "changed": true, "votes": { "up": 13, "down": 1, "up_weight": 12.5, "down_weight": 1 } }
ddiff --git a/fixtures/deals.watch-request/invalid/no-price.json b/fixtures/deals.watch-request/invalid/no-price.json
nnew file mode 100644
iindex 00000000..5c2d14db
---- /dev/null
++++ b/fixtures/deals.watch-request/invalid/no-price.json
@@@ -0,0 +1 @@
++{ "kind": "price_below", "query": "capture card" }
ddiff --git a/fixtures/deals.watch-request/valid/price.json b/fixtures/deals.watch-request/valid/price.json
nnew file mode 100644
iindex 00000000..94f2a748
---- /dev/null
++++ b/fixtures/deals.watch-request/valid/price.json
@@@ -0,0 +1 @@
++{ "kind": "price_below", "query": "capture card", "max_price": "60", "currency": "USD", "label": "cheap capture card" }
ddiff --git a/fixtures/deals.watch-result/invalid/bad-kind.json b/fixtures/deals.watch-result/invalid/bad-kind.json
nnew file mode 100644
iindex 00000000..5a04345d
---- /dev/null
++++ b/fixtures/deals.watch-result/invalid/bad-kind.json
@@@ -0,0 +1 @@
++{ "watches": [ { "id": "w", "kind": "alert", "notify": true, "created_at": null } ] }
ddiff --git a/fixtures/deals.watch-result/valid/deleted.json b/fixtures/deals.watch-result/valid/deleted.json
nnew file mode 100644
iindex 00000000..bff6d2f1
---- /dev/null
++++ b/fixtures/deals.watch-result/valid/deleted.json
@@@ -0,0 +1 @@
++{ "id": "dwt_01JAB2C3D4E5F6G7H8J9K0MNPQ", "deleted": true }
ddiff --git a/fixtures/trade.alert-read-result/invalid/partial-delivery.json b/fixtures/trade.alert-read-result/invalid/partial-delivery.json
nnew file mode 100644
iindex 00000000..ca2dec0c
---- /dev/null
++++ b/fixtures/trade.alert-read-result/invalid/partial-delivery.json
@@@ -0,0 +1 @@
++{ "deliveries": [ { "id": 1 } ] }
ddiff --git a/fixtures/trade.alert-read-result/valid/rules.json b/fixtures/trade.alert-read-result/valid/rules.json
nnew file mode 100644
iindex 00000000..eda4cb39
---- /dev/null
++++ b/fixtures/trade.alert-read-result/valid/rules.json
@@@ -0,0 +1 @@
++{ "rules": [ { "id": "alr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "kind": "threshold", "status": "active", "instrument": { "id": "ins_01JAB2C3D4E5F6G7H8J9K0MNPQ", "symbol": "ACME", "name": "Acme Corporation" }, "metric": "close", "operator": "below", "threshold": "150", "unit": "USD", "currency": "USD", "form_types": null, "armed": true, "created_at": "2026-09-25T10:00:00.000Z", "updated_at": "2026-09-25T10:00:00.000Z" } ] }
ddiff --git a/fixtures/trade.alert-rule-request/invalid/threshold-incomplete.json b/fixtures/trade.alert-rule-request/invalid/threshold-incomplete.json
nnew file mode 100644
iindex 00000000..882e934d
---- /dev/null
++++ b/fixtures/trade.alert-rule-request/invalid/threshold-incomplete.json
@@@ -0,0 +1 @@
++{ "symbol": "ACME", "kind": "threshold", "metric": "close" }
ddiff --git a/fixtures/trade.alert-rule-request/valid/filings.json b/fixtures/trade.alert-rule-request/valid/filings.json
nnew file mode 100644
iindex 00000000..1217183c
---- /dev/null
++++ b/fixtures/trade.alert-rule-request/valid/filings.json
@@@ -0,0 +1 @@
++{ "symbol": "ACME", "kind": "filing_type", "form_types": "10-K, 10-Q, 8-K" }
ddiff --git a/fixtures/trade.alert-rule-result/invalid/partial.json b/fixtures/trade.alert-rule-result/invalid/partial.json
nnew file mode 100644
iindex 00000000..52e76cb3
---- /dev/null
++++ b/fixtures/trade.alert-rule-result/invalid/partial.json
@@@ -0,0 +1 @@
++{ "rule": { "id": "x" } }
ddiff --git a/fixtures/trade.alert-rule-result/valid/rule.json b/fixtures/trade.alert-rule-result/valid/rule.json
nnew file mode 100644
iindex 00000000..9ec55918
---- /dev/null
++++ b/fixtures/trade.alert-rule-result/valid/rule.json
@@@ -0,0 +1 @@
++{ "rule": { "id": "alr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "kind": "threshold", "status": "active", "instrument": { "id": "ins_01JAB2C3D4E5F6G7H8J9K0MNPQ", "symbol": "ACME", "name": "Acme Corporation" }, "metric": "close", "operator": "below", "threshold": "150", "unit": "USD", "currency": "USD", "form_types": null, "armed": true, "created_at": "2026-09-25T10:00:00.000Z", "updated_at": "2026-09-25T10:00:00.000Z" } }
ddiff --git a/fixtures/trade.alert-rule/invalid/bad-operator.json b/fixtures/trade.alert-rule/invalid/bad-operator.json
nnew file mode 100644
iindex 00000000..4e80d507
---- /dev/null
++++ b/fixtures/trade.alert-rule/invalid/bad-operator.json
@@@ -0,0 +1 @@
++{ "id": "alr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "kind": "threshold", "status": "active", "instrument": { "id": "ins_01JAB2C3D4E5F6G7H8J9K0MNPQ", "symbol": "ACME", "name": "Acme Corporation" }, "metric": "close", "operator": "crosses", "threshold": "150", "unit": "USD", "currency": "USD", "form_types": null, "armed": true, "created_at": "2026-09-25T10:00:00.000Z", "updated_at": "2026-09-25T10:00:00.000Z" }
ddiff --git a/fixtures/trade.alert-rule/valid/threshold.json b/fixtures/trade.alert-rule/valid/threshold.json
nnew file mode 100644
iindex 00000000..f222f50e
---- /dev/null
++++ b/fixtures/trade.alert-rule/valid/threshold.json
@@@ -0,0 +1 @@
++{ "id": "alr_01JAB2C3D4E5F6G7H8J9K0MNPQ", "kind": "threshold", "status": "active", "instrument": { "id": "ins_01JAB2C3D4E5F6G7H8J9K0MNPQ", "symbol": "ACME", "name": "Acme Corporation" }, "metric": "close", "operator": "below", "threshold": "150", "unit": "USD", "currency": "USD", "form_types": null, "armed": true, "created_at": "2026-09-25T10:00:00.000Z", "updated_at": "2026-09-25T10:00:00.000Z" }
ddiff --git a/fixtures/trade.context-proposal-result/invalid/no-revision.json b/fixtures/trade.context-proposal-result/invalid/no-revision.json
nnew file mode 100644
iindex 00000000..3fc057ea
---- /dev/null
++++ b/fixtures/trade.context-proposal-result/invalid/no-revision.json
@@@ -0,0 +1 @@
++{ "published": false }
ddiff --git a/fixtures/trade.context-proposal-result/valid/draft.json b/fixtures/trade.context-proposal-result/valid/draft.json
nnew file mode 100644
iindex 00000000..e5335cf1
---- /dev/null
++++ b/fixtures/trade.context-proposal-result/valid/draft.json
@@@ -0,0 +1 @@
++{ "revision": { "revision": 2, "published": true, "published_at": "2026-09-25T12:00:00.000Z", "written_at": "2026-09-25T11:00:00.000Z", "as_of": "2026-09-24T21:05:00.000Z", "origin": "ai", "body_markdown": "Acme closed at 182.40 USD on 2026-09-24 [1].", "body_html": "<p>Acme closed at 182.40 USD on 2026-09-24 [1].</p>", "text": "Acme closed at 182.40 USD on 2026-09-24 [1].", "cites": [ { "kind": "observation", "id": "obs_01JAB2C3D4E5F6G7H8J9K0MNPQ", "observation": { "id": "obs_01JAB2C3D4E5F6G7H8J9K0MNPQ", "metric": "close", "value": "182.40", "unit": "USD", "currency": "USD", "period": "2026-09-24", "observed_at": "2026-09-24T20:00:00.000Z", "retrieved_at": "2026-09-24T21:05:00.000Z", "recorded_at": "2026-09-24T21:05:02.000Z", "max_age_sec": 172800, "source": { "key": "example-quotes", "item_id": null, "url": "https://quotes.example.com/ACME", "ref": "ACME:2026-09-24:close" }, "freshness": { "stale": false, "stale_since": null, "reason": null, "source_status": "healthy", "source_last_success_at": "2026-09-24T21:05:00.000Z" } } } ], "authorship": { "mode": "ai" }, "disclosure": "AI-generated, reviewed by a person", "review": { "decision": "approved", "reviewed_at": "2026-09-25T12:00:00.000Z", "reviewer": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "needs_review": false }, "published": false }
ddiff --git a/fixtures/trade.context-proposal/invalid/wrong-workflow.json b/fixtures/trade.context-proposal/invalid/wrong-workflow.json
nnew file mode 100644
iindex 00000000..c87c529d
---- /dev/null
++++ b/fixtures/trade.context-proposal/invalid/wrong-workflow.json
@@@ -0,0 +1 @@
++{ "workflow": { "id": "wiki.page_refresh", "run_id": "r" }, "output": { "summary": "" } }
ddiff --git a/fixtures/trade.context-proposal/valid/ai.json b/fixtures/trade.context-proposal/valid/ai.json
nnew file mode 100644
iindex 00000000..fde80447
---- /dev/null
++++ b/fixtures/trade.context-proposal/valid/ai.json
@@@ -0,0 +1 @@
++{ "workflow": { "id": "trade.summarize_market_context", "run_id": "run_01JAB2C3D4E5F6G7H8J9K0MNPS", "model": "claude-sonnet-4-5" }, "input_sources": [ { "source_type": "trade.observation", "source_id": "obs_01JAB2C3D4E5F6G7H8J9K0MNPQ" } ], "output": { "summary": "Acme closed at 182.40 USD on 2026-09-24.", "citations": [0], "gaps": ["No filings in the last 30 days."] } }
ddiff --git a/fixtures/trade.context-proposal/valid/editor.json b/fixtures/trade.context-proposal/valid/editor.json
nnew file mode 100644
iindex 00000000..1b90e28e
---- /dev/null
++++ b/fixtures/trade.context-proposal/valid/editor.json
@@@ -0,0 +1 @@
++{ "body": "Acme reported Q3 results on 2026-09-20; see the 10-Q.", "cites": [ { "kind": "document", "id": "doc_01JAB2C3D4E5F6G7H8J9K0MNPQ" } ], "publish": true }
ddiff --git a/fixtures/trade.context-read-result/invalid/no-published.json b/fixtures/trade.context-read-result/invalid/no-published.json
nnew file mode 100644
iindex 00000000..58a4fc93
---- /dev/null
++++ b/fixtures/trade.context-read-result/invalid/no-published.json
@@@ -0,0 +1 @@
++{ "instrument": { "id": "ins_01JAB2C3D4E5F6G7H8J9K0MNPQ", "symbol": "ACME", "name": "Acme Corporation", "kind": "equity", "exchange": "NASDAQ", "cik": "0000123456", "currency": "USD", "status": "active", "url": "https://openvibe.trade/i/ACME", "created_at": "2026-09-20T10:00:00.000Z", "updated_at": "2026-09-20T10:00:00.000Z" }, "disclaimer": "x" }
ddiff --git a/fixtures/trade.context-read-result/valid/published.json b/fixtures/trade.context-read-result/valid/published.json
nnew file mode 100644
iindex 00000000..5144e24e
---- /dev/null
++++ b/fixtures/trade.context-read-result/valid/published.json
@@@ -0,0 +1 @@
++{ "instrument": { "id": "ins_01JAB2C3D4E5F6G7H8J9K0MNPQ", "symbol": "ACME", "name": "Acme Corporation", "kind": "equity", "exchange": "NASDAQ", "cik": "0000123456", "currency": "USD", "status": "active", "url": "https://openvibe.trade/i/ACME", "created_at": "2026-09-20T10:00:00.000Z", "updated_at": "2026-09-20T10:00:00.000Z" }, "published": { "revision": 2, "published": true, "published_at": "2026-09-25T12:00:00.000Z", "written_at": "2026-09-25T11:00:00.000Z", "as_of": "2026-09-24T21:05:00.000Z", "origin": "ai", "body_markdown": "Acme closed at 182.40 USD on 2026-09-24 [1].", "body_html": "<p>Acme closed at 182.40 USD on 2026-09-24 [1].</p>", "text": "Acme closed at 182.40 USD on 2026-09-24 [1].", "cites": [ { "kind": "observation", "id": "obs_01JAB2C3D4E5F6G7H8J9K0MNPQ", "observation": { "id": "obs_01JAB2C3D4E5F6G7H8J9K0MNPQ", "metric": "close", "value": "182.40", "unit": "USD", "currency": "USD", "period": "2026-09-24", "observed_at": "2026-09-24T20:00:00.000Z", "retrieved_at": "2026-09-24T21:05:00.000Z", "recorded_at": "2026-09-24T21:05:02.000Z", "max_age_sec": 172800, "source": { "key": "example-quotes", "item_id": null, "url": "https://quotes.example.com/ACME", "ref": "ACME:2026-09-24:close" }, "freshness": { "stale": false, "stale_since": null, "reason": null, "source_status": "healthy", "source_last_success_at": "2026-09-24T21:05:00.000Z" } } } ], "authorship": { "mode": "ai" }, "disclosure": "AI-generated, reviewed by a person", "review": { "decision": "approved", "reviewed_at": "2026-09-25T12:00:00.000Z", "reviewer": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "needs_review": false }, "disclaimer": "Information only — not investment advice; no trading here." }
ddiff --git a/fixtures/trade.context-revision/invalid/zero.json b/fixtures/trade.context-revision/invalid/zero.json
nnew file mode 100644
iindex 00000000..3dd83188
---- /dev/null
++++ b/fixtures/trade.context-revision/invalid/zero.json
@@@ -0,0 +1 @@
++{ "revision": 0, "published": false, "body_markdown": "", "cites": [], "needs_review": true }
ddiff --git a/fixtures/trade.context-revision/valid/published-ai.json b/fixtures/trade.context-revision/valid/published-ai.json
nnew file mode 100644
iindex 00000000..094334f8
---- /dev/null
++++ b/fixtures/trade.context-revision/valid/published-ai.json
@@@ -0,0 +1 @@
++{ "revision": 2, "published": true, "published_at": "2026-09-25T12:00:00.000Z", "written_at": "2026-09-25T11:00:00.000Z", "as_of": "2026-09-24T21:05:00.000Z", "origin": "ai", "body_markdown": "Acme closed at 182.40 USD on 2026-09-24 [1].", "body_html": "<p>Acme closed at 182.40 USD on 2026-09-24 [1].</p>", "text": "Acme closed at 182.40 USD on 2026-09-24 [1].", "cites": [ { "kind": "observation", "id": "obs_01JAB2C3D4E5F6G7H8J9K0MNPQ", "observation": { "id": "obs_01JAB2C3D4E5F6G7H8J9K0MNPQ", "metric": "close", "value": "182.40", "unit": "USD", "currency": "USD", "period": "2026-09-24", "observed_at": "2026-09-24T20:00:00.000Z", "retrieved_at": "2026-09-24T21:05:00.000Z", "recorded_at": "2026-09-24T21:05:02.000Z", "max_age_sec": 172800, "source": { "key": "example-quotes", "item_id": null, "url": "https://quotes.example.com/ACME", "ref": "ACME:2026-09-24:close" }, "freshness": { "stale": false, "stale_since": null, "reason": null, "source_status": "healthy", "source_last_success_at": "2026-09-24T21:05:00.000Z" } } } ], "authorship": { "mode": "ai" }, "disclosure": "AI-generated, reviewed by a person", "review": { "decision": "approved", "reviewed_at": "2026-09-25T12:00:00.000Z", "reviewer": "usr_01JAB2C3D4E5F6G7H8J9K0MNPQ" }, "needs_review": false }
ddiff --git a/fixtures/trade.deleted-result/invalid/text.json b/fixtures/trade.deleted-result/invalid/text.json
nnew file mode 100644
iindex 00000000..f74be56b
---- /dev/null
++++ b/fixtures/trade.deleted-result/invalid/text.json
@@@ -0,0 +1 @@
++{ "deleted": "yes" }
ddiff --git a/fixtures/trade.deleted-result/valid/deleted.json b/fixtures/trade.deleted-result/valid/deleted.json
nnew file mode 100644
iindex 00000000..4b02a529
---- /dev/null
++++ b/fixtures/trade.deleted-result/valid/deleted.json
@@@ -0,0 +1 @@
++{ "deleted": true }
ddiff --git a/fixtures/trade.instrument-manage-request/invalid/bad.json b/fixtures/trade.instrument-manage-request/invalid/bad.json
nnew file mode 100644
iindex 00000000..f4743f34
---- /dev/null
++++ b/fixtures/trade.instrument-manage-request/invalid/bad.json
@@@ -0,0 +1 @@
++{ "status": "deleted", "kind": "bond" }
ddiff --git a/fixtures/trade.instrument-manage-request/valid/alias.json b/fixtures/trade.instrument-manage-request/valid/alias.json
nnew file mode 100644
iindex 00000000..609397e2
---- /dev/null
++++ b/fixtures/trade.instrument-manage-request/valid/alias.json
@@@ -0,0 +1 @@
++{ "kind": "ticker", "value": "ACME.B" }
ddiff --git a/fixtures/trade.instrument-manage-request/valid/create.json b/fixtures/trade.instrument-manage-request/valid/create.json
nnew file mode 100644
iindex 00000000..0ea1ac91
---- /dev/null
++++ b/fixtures/trade.instrument-manage-request/valid/create.json
@@@ -0,0 +1 @@
++{ "symbol": "ACME", "name": "Acme Corporation", "exchange": "NASDAQ", "cik": "123456", "currency": "usd" }
ddiff --git a/fixtures/trade.instrument-manage-result/invalid/no-instrument.json b/fixtures/trade.instrument-manage-result/invalid/no-instrument.json
nnew file mode 100644
iindex 00000000..e0798443
---- /dev/null
++++ b/fixtures/trade.instrument-manage-result/invalid/no-instrument.json
@@@ -0,0 +1 @@
++{ "aliases": [] }
ddiff --git a/fixtures/trade.instrument-manage-result/valid/created.json b/fixtures/trade.instrument-manage-result/valid/created.json
nnew file mode 100644
iindex 00000000..20b471bf
---- /dev/null
++++ b/fixtures/trade.instrument-manage-result/valid/created.json
@@@ -0,0 +1 @@
++{ "instrument": { "id": "ins_01JAB2C3D4E5F6G7H8J9K0MNPQ", "symbol": "ACME", "name": "Acme Corporation", "kind": "equity", "exchange": "NASDAQ", "cik": "0000123456", "currency": "USD", "status": "active", "url": "https://openvibe.trade/i/ACME", "created_at": "2026-09-20T10:00:00.000Z", "updated_at": "2026-09-20T10:00:00.000Z" }, "aliases": [ { "kind": "ticker", "value": "ACME", "normalized": "ACME" } ] }
ddiff --git a/fixtures/trade.instrument/invalid/bad-kind.json b/fixtures/trade.instrument/invalid/bad-kind.json
nnew file mode 100644
iindex 00000000..7a15b87c
---- /dev/null
++++ b/fixtures/trade.instrument/invalid/bad-kind.json
@@@ -0,0 +1 @@
++{ "id": "ins_01JAB2C3D4E5F6G7H8J9K0MNPQ", "symbol": "ACME", "name": "Acme Corporation", "kind": "bond", "exchange": "NASDAQ", "cik": "0000123456", "currency": "USD", "status": "active", "url": "https://openvibe.trade/i/ACME", "created_at": "2026-09-20T10:00:00.000Z", "updated_at": "2026-09-20T10:00:00.000Z" }
ddiff --git a/fixtures/trade.instrument/valid/equity.json b/fixtures/trade.instrument/valid/equity.json
nnew file mode 100644
iindex 00000000..08163b11
---- /dev/null
++++ b/fixtures/trade.instrument/valid/equity.json
@@@ -0,0 +1 @@
++{ "id": "ins_01JAB2C3D4E5F6G7H8J9K0MNPQ", "symbol": "ACME", "name": "Acme Corporation", "kind": "equity", "exchange": "NASDAQ", "cik": "0000123456", "currency": "USD", "status": "active", "url": "https://openvibe.trade/i/ACME", "created_at": "2026-09-20T10:00:00.000Z", "updated_at": "2026-09-20T10:00:00.000Z" }
ddiff --git a/fixtures/trade.observation-request/invalid/no-source-ref.json b/fixtures/trade.observation-request/invalid/no-source-ref.json
nnew file mode 100644
iindex 00000000..7f4c2d34
---- /dev/null
++++ b/fixtures/trade.observation-request/invalid/no-source-ref.json
@@@ -0,0 +1 @@
++{ "symbol": "ACME", "metric": "close", "value": "182.40", "unit": "USD", "observed_at": "2026-09-24T20:00:00Z", "retrieved_at": "2026-09-24T21:05:00Z", "source_key": "example-quotes" }
ddiff --git a/fixtures/trade.observation-request/valid/close.json b/fixtures/trade.observation-request/valid/close.json
nnew file mode 100644
iindex 00000000..da5d3294
---- /dev/null
++++ b/fixtures/trade.observation-request/valid/close.json
@@@ -0,0 +1 @@
++{ "symbol": "ACME", "metric": "close", "value": "182.40", "unit": "USD", "currency": "USD", "period": "2026-09-24", "observed_at": "2026-09-24T20:00:00Z", "retrieved_at": "2026-09-24T21:05:00Z", "source_key": "example-quotes", "source_ref": "ACME:2026-09-24:close", "source_url": "https://quotes.example.com/ACME", "max_age_sec": 172800 }
ddiff --git a/fixtures/trade.observation-result/invalid/no-observation.json b/fixtures/trade.observation-result/invalid/no-observation.json
nnew file mode 100644
iindex 00000000..ae5f7c88
---- /dev/null
++++ b/fixtures/trade.observation-result/invalid/no-observation.json
@@@ -0,0 +1 @@
++{ "created": true }
ddiff --git a/fixtures/trade.observation-result/valid/created.json b/fixtures/trade.observation-result/valid/created.json
nnew file mode 100644
iindex 00000000..aadd6e54
---- /dev/null
++++ b/fixtures/trade.observation-result/valid/created.json
@@@ -0,0 +1 @@
++{ "observation": { "id": "obs_01JAB2C3D4E5F6G7H8J9K0MNPQ", "metric": "close", "value": "182.40", "unit": "USD", "currency": "USD", "period": "2026-09-24", "observed_at": "2026-09-24T20:00:00.000Z", "retrieved_at": "2026-09-24T21:05:00.000Z", "recorded_at": "2026-09-24T21:05:02.000Z", "max_age_sec": 172800, "source": { "key": "example-quotes", "item_id": null, "url": "https://quotes.example.com/ACME", "ref": "ACME:2026-09-24:close" }, "freshness": { "stale": false, "stale_since": null, "reason": null, "source_status": "healthy", "source_last_success_at": "2026-09-24T21:05:00.000Z" } }, "created": true }
ddiff --git a/fixtures/trade.observation/invalid/number-value.json b/fixtures/trade.observation/invalid/number-value.json
nnew file mode 100644
iindex 00000000..4b9ea334
---- /dev/null
++++ b/fixtures/trade.observation/invalid/number-value.json
@@@ -0,0 +1 @@
++{ "id": "o", "metric": "close", "value": 182.4, "unit": "USD", "observed_at": null, "retrieved_at": null, "recorded_at": null, "source": { "key": "k" } }
ddiff --git a/fixtures/trade.observation/valid/close.json b/fixtures/trade.observation/valid/close.json
nnew file mode 100644
iindex 00000000..e9726d4b
---- /dev/null
++++ b/fixtures/trade.observation/valid/close.json
@@@ -0,0 +1 @@
++{ "id": "obs_01JAB2C3D4E5F6G7H8J9K0MNPQ", "metric": "close", "value": "182.40", "unit": "USD", "currency": "USD", "period": "2026-09-24", "observed_at": "2026-09-24T20:00:00.000Z", "retrieved_at": "2026-09-24T21:05:00.000Z", "recorded_at": "2026-09-24T21:05:02.000Z", "max_age_sec": 172800, "source": { "key": "example-quotes", "item_id": null, "url": "https://quotes.example.com/ACME", "ref": "ACME:2026-09-24:close" }, "freshness": { "stale": false, "stale_since": null, "reason": null, "source_status": "healthy", "source_last_success_at": "2026-09-24T21:05:00.000Z" } }
ddiff --git a/fixtures/trade.resolve-result/invalid/bad-status.json b/fixtures/trade.resolve-result/invalid/bad-status.json
nnew file mode 100644
iindex 00000000..226bd49d
---- /dev/null
++++ b/fixtures/trade.resolve-result/invalid/bad-status.json
@@@ -0,0 +1 @@
++{ "status": "maybe", "match": null, "instrument": null, "candidates": [] }
ddiff --git a/fixtures/trade.resolve-result/valid/resolved.json b/fixtures/trade.resolve-result/valid/resolved.json
nnew file mode 100644
iindex 00000000..003bb197
---- /dev/null
++++ b/fixtures/trade.resolve-result/valid/resolved.json
@@@ -0,0 +1 @@
++{ "query": "acme", "status": "resolved", "match": { "kind": "name", "value": "acme" }, "instrument": { "id": "ins_01JAB2C3D4E5F6G7H8J9K0MNPQ", "symbol": "ACME", "name": "Acme Corporation", "kind": "equity", "exchange": "NASDAQ", "cik": "0000123456", "currency": "USD", "status": "active", "url": "https://openvibe.trade/i/ACME", "created_at": "2026-09-20T10:00:00.000Z", "updated_at": "2026-09-20T10:00:00.000Z" }, "candidates": [] }
ddiff --git a/fixtures/trade.watchlist-request/invalid/not-text.json b/fixtures/trade.watchlist-request/invalid/not-text.json
nnew file mode 100644
iindex 00000000..92e04752
---- /dev/null
++++ b/fixtures/trade.watchlist-request/invalid/not-text.json
@@@ -0,0 +1 @@
++{ "name": 5, "note": 7 }
ddiff --git a/fixtures/trade.watchlist-request/valid/create.json b/fixtures/trade.watchlist-request/valid/create.json
nnew file mode 100644
iindex 00000000..c345e0aa
---- /dev/null
++++ b/fixtures/trade.watchlist-request/valid/create.json
@@@ -0,0 +1 @@
++{ "name": "Semis" }
ddiff --git a/fixtures/trade.watchlist-request/valid/item-note.json b/fixtures/trade.watchlist-request/valid/item-note.json
nnew file mode 100644
iindex 00000000..f01fa1ed
---- /dev/null
++++ b/fixtures/trade.watchlist-request/valid/item-note.json
@@@ -0,0 +1 @@
++{ "note": "earnings in Oct" }
ddiff --git a/fixtures/trade.watchlist-result/invalid/no-watchlist.json b/fixtures/trade.watchlist-result/invalid/no-watchlist.json
nnew file mode 100644
iindex 00000000..9074072b
---- /dev/null
++++ b/fixtures/trade.watchlist-result/invalid/no-watchlist.json
@@@ -0,0 +1 @@
++{ "added": true }
ddiff --git a/fixtures/trade.watchlist-result/valid/added.json b/fixtures/trade.watchlist-result/valid/added.json
nnew file mode 100644
iindex 00000000..048c5463
---- /dev/null
++++ b/fixtures/trade.watchlist-result/valid/added.json
@@@ -0,0 +1 @@
++{ "added": true, "watchlist": { "id": "wl_01JAB2C3D4E5F6G7H8J9K0MNPQ", "name": "Semis", "created_at": "2026-09-25T10:00:00.000Z", "updated_at": "2026-09-25T10:00:00.000Z", "items": [ { "instrument": { "id": "ins_01JAB2C3D4E5F6G7H8J9K0MNPQ", "symbol": "ACME", "name": "Acme Corporation", "kind": "equity", "exchange": "NASDAQ", "status": "active", "url": "https://openvibe.trade/i/ACME" }, "note": "earnings in Oct", "added_at": "2026-09-25T10:00:00.000Z" } ] } }
ddiff --git a/fixtures/trade.watchlist/invalid/no-name.json b/fixtures/trade.watchlist/invalid/no-name.json
nnew file mode 100644
iindex 00000000..588a566a
---- /dev/null
++++ b/fixtures/trade.watchlist/invalid/no-name.json
@@@ -0,0 +1 @@
++{ "id": "w" }
ddiff --git a/fixtures/trade.watchlist/valid/semis.json b/fixtures/trade.watchlist/valid/semis.json
nnew file mode 100644
iindex 00000000..8a129dc7
---- /dev/null
++++ b/fixtures/trade.watchlist/valid/semis.json
@@@ -0,0 +1 @@
++{ "id": "wl_01JAB2C3D4E5F6G7H8J9K0MNPQ", "name": "Semis", "created_at": "2026-09-25T10:00:00.000Z", "updated_at": "2026-09-25T10:00:00.000Z", "items": [ { "instrument": { "id": "ins_01JAB2C3D4E5F6G7H8J9K0MNPQ", "symbol": "ACME", "name": "Acme Corporation", "kind": "equity", "exchange": "NASDAQ", "status": "active", "url": "https://openvibe.trade/i/ACME" }, "note": "earnings in Oct", "added_at": "2026-09-25T10:00:00.000Z" } ] }
ddiff --git a/generated/json-schema/bundle.json b/generated/json-schema/bundle.json
iindex 0a558cd6..24028e0d 100644
---- a/generated/json-schema/bundle.json
++++ b/generated/json-schema/bundle.json
@@@ -28905,337 +28905,3797 @@
}
}
},
++ "deals.flag-request": {
++ "version": "1.0.0",
++ "owner": "deals",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/deals/flag-request.v1.json",
++ "title": "DealsFlagRequest",
++ "description": "deals.flag-request@1: the body of POST /api/v1/offers/:id/flags on OpenVibe.Deals (deals.flag.create; a person): { kind, reason?, duplicate_of? } — duplicate_of (the other deal, id, slug or /d/ link) is required for kind duplicate. An open flag of the same kind by the same person is refreshed, not repeated.",
++ "type": "object",
++ "required": [
++ "kind"
++ ],
++ "properties": {
++ "kind": {
++ "enum": [
++ "expired",
++ "price_wrong",
++ "spam",
++ "duplicate",
++ "other"
++ ]
++ },
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 500
++ },
++ "duplicate_of": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ },
++ "if": {
++ "properties": {
++ "kind": {
++ "const": "duplicate"
++ }
++ }
++ },
++ "then": {
++ "required": [
++ "duplicate_of"
++ ]
++ }
++ }
++ },
++ "deals.flag-result": {
++ "version": "1.0.0",
++ "owner": "deals",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/deals/flag-result.v1.json",
++ "title": "DealsFlagResult",
++ "description": "deals.flag-result@1: the answer of POST /api/v1/offers/:id/flags on OpenVibe.Deals: { flag: { id, kind, status }, created } (201 when created, 200 when an open flag was refreshed).",
++ "type": "object",
++ "required": [
++ "flag",
++ "created"
++ ],
++ "properties": {
++ "flag": {
++ "type": "object",
++ "required": [
++ "id",
++ "kind",
++ "status"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "kind": {
++ "type": "string"
++ },
++ "status": {
++ "type": "string"
++ }
++ }
++ },
++ "created": {
++ "type": "boolean"
++ }
++ }
++ }
++ },
++ "deals.moderation-request": {
++ "version": "1.0.0",
++ "owner": "deals",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/deals/moderation-request.v1.json",
++ "title": "DealsModerationRequest",
++ "description": "deals.moderation-request@1: bodies of deals.offer.moderate on OpenVibe.Deals (moderators). POST /api/v1/offers/:id/disable: { reason } (3-300 characters); POST …/enable: { reason? }; POST …/review (a person): { note? }; POST /api/v1/flags/:id/resolve: { status?: resolved (default) | dismissed, resolution? }. GET /api/v1/flags?status= takes no body.",
++ "anyOf": [
++ {
++ "type": "object",
++ "properties": {
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 300
++ },
++ "note": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 300
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "status"
++ ],
++ "properties": {
++ "status": {
++ "enum": [
++ "resolved",
++ "dismissed"
++ ]
++ },
++ "resolution": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 300
++ }
++ }
++ }
++ ]
++ }
++ },
++ "deals.moderation-result": {
++ "version": "1.0.0",
++ "owner": "deals",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/deals/moderation-result.v1.json",
++ "title": "DealsModerationResult",
++ "description": "deals.moderation-result@1: answers of deals.offer.moderate on OpenVibe.Deals. POST /api/v1/offers/:id/disable|enable|review → { offer }; GET /api/v1/flags?status=open|resolved|dismissed → { flags }; POST /api/v1/flags/:id/resolve → { flag, changed } (changed false when it was no longer open).",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "offer"
++ ],
++ "properties": {
++ "offer": {
++ "$ref": "offer.v1.json"
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "flags"
++ ],
++ "properties": {
++ "flags": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "id",
++ "kind",
++ "origin",
++ "status"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "kind": {
++ "type": "string"
++ },
++ "origin": {
++ "type": "string"
++ },
++ "reporter": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "status": {
++ "enum": [
++ "open",
++ "resolved",
++ "dismissed"
++ ]
++ },
++ "details": {
++ "type": "object"
++ },
++ "created_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "updated_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "resolved_by": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "resolved_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "resolution": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "offer": {
++ "type": [
++ "object",
++ "null"
++ ]
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "flag",
++ "changed"
++ ],
++ "properties": {
++ "flag": {
++ "type": "object",
++ "required": [
++ "id",
++ "kind",
++ "origin",
++ "status"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "kind": {
++ "type": "string"
++ },
++ "origin": {
++ "type": "string"
++ },
++ "reporter": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "status": {
++ "enum": [
++ "open",
++ "resolved",
++ "dismissed"
++ ]
++ },
++ "details": {
++ "type": "object"
++ },
++ "created_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "updated_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "resolved_by": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "resolved_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "resolution": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "offer": {
++ "type": [
++ "object",
++ "null"
++ ]
++ }
++ }
++ },
++ "changed": {
++ "type": "boolean"
++ }
++ }
++ }
++ ]
++ }
++ },
++ "deals.offer-action-request": {
++ "version": "1.0.0",
++ "owner": "deals",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/deals/offer-action-request.v1.json",
++ "title": "DealsOfferActionRequest",
++ "description": "deals.offer-action-request@1: bodies of the deal actions on OpenVibe.Deals. POST /api/v1/offers/:id/expire (deals.offer.expire; the submitter or a moderator): { reason? } (a moderator's reason is recorded). POST …/disable (deals.offer.moderate): { reason } (3-300 characters, required); POST …/enable: { reason? }; POST …/review (a person): { note? }. POST …/merge (deals.offer.merge, moderators): { into, reason? } (the canonical deal, id or slug; no cycles, not an already merged offer); POST …/unmerge: { reason? }.",
++ "anyOf": [
++ {
++ "type": "object",
++ "properties": {
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 300
++ },
++ "note": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 300
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "into"
++ ],
++ "properties": {
++ "into": {
++ "type": "string",
++ "minLength": 1
++ },
++ "reason": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ }
++ ]
++ }
++ },
++ "deals.offer-result": {
++ "version": "1.0.0",
++ "owner": "deals",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/deals/offer-result.v1.json",
++ "title": "DealsOfferResult",
++ "description": "deals.offer-result@1: the answer of the deal writes on OpenVibe.Deals: { offer } (the canonical offer after the change). POST /api/v1/offers (201), PATCH /api/v1/offers/:id, POST …/expire, …/disable, …/enable and …/review answer exactly that; POST …/observations (201) adds the observation recorded; POST …/merge adds merged { id, slug } and the before/after of the change; POST …/unmerge adds from { id, slug } and before/after.",
++ "type": "object",
++ "required": [
++ "offer"
++ ],
++ "properties": {
++ "offer": {
++ "$ref": "offer.v1.json"
++ },
++ "observation": {
++ "type": "object",
++ "required": [
++ "id",
++ "observed_at",
++ "origin",
++ "source"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "observed_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "recorded_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "price": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "currency": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "shipping": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "shipping_note": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "condition": {
++ "enum": [
++ "new",
++ "used",
++ "refurbished",
++ "damaged",
++ null
++ ]
++ },
++ "availability": {
++ "enum": [
++ "in_stock",
++ "out_of_stock",
++ "preorder",
++ "discontinued",
++ "limited",
++ "sold_out",
++ "online_only",
++ "in_store_only",
++ null
++ ]
++ },
++ "origin": {
++ "type": "string"
++ },
++ "observed_by": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "listing": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "source": {
++ "type": "object",
++ "properties": {
++ "id": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "kind": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "ref": {
++ "type": [
++ "object",
++ "null"
++ ]
++ },
++ "key": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "url": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ }
++ }
++ },
++ "merged": {
++ "type": "object",
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "slug": {
++ "type": "string"
++ }
++ }
++ },
++ "from": {
++ "type": "object",
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "slug": {
++ "type": "string"
++ }
++ }
++ },
++ "before": {},
++ "after": {}
++ }
++ }
++ },
++ "deals.offer-submit-request": {
++ "version": "1.0.0",
++ "owner": "deals",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/deals/offer-submit-request.v1.json",
++ "title": "DealsOfferSubmitRequest",
++ "description": "deals.offer-submit-request@1: the body of POST /api/v1/offers on OpenVibe.Deals (deals.offer.submit; a signed-in person, or a service acting for one; X-OV-Origin: ai holds the text for review). url (an http(s) link to the offer; a link already posted is 409 offer.duplicate) and title (3-200 characters) are required; a price or shipping cost needs its currency; expires_at may not be in the past. The first price observation is recorded with it.",
++ "type": "object",
++ "required": [
++ "url",
++ "title"
++ ],
++ "properties": {
++ "url": {
++ "type": "string",
++ "pattern": "^https?://"
++ },
++ "title": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 200
++ },
++ "description": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 4000
++ },
++ "category": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 60
++ },
++ "expires_at": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "store_name": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 120
++ },
++ "product_slug": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "product": {
++ "type": "object",
++ "description": "Product aliases to resolve (or create) the product.",
++ "properties": {
++ "name": {
++ "type": "string"
++ },
++ "brand": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "gtin": {
++ "type": "string",
++ "pattern": "^[0-9]{8,14}$"
++ },
++ "mpn": {
++ "type": "string"
++ },
++ "sku": {
++ "type": "string"
++ },
++ "url": {
++ "type": "string"
++ },
++ "category": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "description": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ },
++ "price": {
++ "type": [
++ "string",
++ "number",
++ "null"
++ ],
++ "description": "A decimal amount; always with its currency."
++ },
++ "currency": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^[A-Za-z]{3}$",
++ "description": "ISO 4217; Deals never assumes one."
++ },
++ "shipping": {
++ "type": [
++ "string",
++ "number",
++ "null"
++ ],
++ "description": "A decimal amount; always with its currency."
++ },
++ "shipping_note": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 200
++ },
++ "condition": {
++ "enum": [
++ "new",
++ "used",
++ "refurbished",
++ "damaged",
++ null
++ ]
++ },
++ "availability": {
++ "enum": [
++ "in_stock",
++ "out_of_stock",
++ "preorder",
++ "discontinued",
++ "limited",
++ "sold_out",
++ "online_only",
++ "in_store_only",
++ null
++ ]
++ },
++ "note": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 300
++ }
++ }
++ }
++ },
++ "deals.offer-update-request": {
++ "version": "1.0.0",
++ "owner": "deals",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/deals/offer-update-request.v1.json",
++ "title": "DealsOfferUpdateRequest",
++ "description": "deals.offer-update-request@1: bodies of deals.offer.update on OpenVibe.Deals. PATCH /api/v1/offers/:id (the submitter or a moderator): { title?, description?, category?, expires_at?, product_slug? | product?, ai_summary? } (ai_summary only from OpenVibe.AI with X-OV-Origin: ai; product null detaches it). POST /api/v1/offers/:id/observations (a person): a price report — at least a price, shipping cost, availability or condition, with the currency of any amount; services may give observed_at (not in the future).",
++ "anyOf": [
++ {
++ "type": "object",
++ "properties": {
++ "title": {
++ "type": "string",
++ "minLength": 3,
++ "maxLength": 200
++ },
++ "description": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 4000
++ },
++ "category": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 60
++ },
++ "expires_at": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "product_slug": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "product": {
++ "anyOf": [
++ {
++ "type": "object",
++ "description": "Product aliases to resolve (or create) the product.",
++ "properties": {
++ "name": {
++ "type": "string"
++ },
++ "brand": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "gtin": {
++ "type": "string",
++ "pattern": "^[0-9]{8,14}$"
++ },
++ "mpn": {
++ "type": "string"
++ },
++ "sku": {
++ "type": "string"
++ },
++ "url": {
++ "type": "string"
++ },
++ "category": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "description": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "ai_summary": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 2000
++ }
++ }
++ },
++ {
++ "type": "object",
++ "anyOf": [
++ {
++ "required": [
++ "price"
++ ]
++ },
++ {
++ "required": [
++ "shipping"
++ ]
++ },
++ {
++ "required": [
++ "availability"
++ ]
++ },
++ {
++ "required": [
++ "condition"
++ ]
++ }
++ ],
++ "properties": {
++ "price": {
++ "type": [
++ "string",
++ "number",
++ "null"
++ ],
++ "description": "A decimal amount; always with its currency."
++ },
++ "currency": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^[A-Za-z]{3}$",
++ "description": "ISO 4217; Deals never assumes one."
++ },
++ "shipping": {
++ "type": [
++ "string",
++ "number",
++ "null"
++ ],
++ "description": "A decimal amount; always with its currency."
++ },
++ "shipping_note": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 200
++ },
++ "condition": {
++ "enum": [
++ "new",
++ "used",
++ "refurbished",
++ "damaged",
++ null
++ ]
++ },
++ "availability": {
++ "enum": [
++ "in_stock",
++ "out_of_stock",
++ "preorder",
++ "discontinued",
++ "limited",
++ "sold_out",
++ "online_only",
++ "in_store_only",
++ null
++ ]
++ },
++ "note": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 300
++ },
++ "observed_at": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ }
++ ]
++ }
++ },
++ "deals.offer": {
++ "version": "1.0.0",
++ "owner": "deals",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/deals/offer.v1.json",
++ "title": "DealsOffer",
++ "description": "deals.offer@1: one deal as OpenVibe.Deals presents it (server/domain/publication.js offerDto; the API and /d/:slug.json): the canonical (root) offer of a merge group with its store, product, price observations (the latest is \"latest\", never \"current\": freshness says whether it still holds), sources, the offers merged into it, votes, hotness and the indexability decision.",
++ "type": "object",
++ "required": [
++ "id",
++ "slug",
++ "url",
++ "title",
++ "link",
++ "status",
++ "origin",
++ "created_at",
++ "observations",
++ "votes",
++ "indexability"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "slug": {
++ "type": "string"
++ },
++ "url": {
++ "type": "string"
++ },
++ "title": {
++ "type": "string"
++ },
++ "description": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "link": {
++ "type": "string",
++ "description": "The offer on the store."
++ },
++ "status": {
++ "type": "string",
++ "description": "active, expired, disabled"
++ },
++ "category": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "origin": {
++ "type": "string"
++ },
++ "submitted_by": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "text_origin": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "review_state": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "ai_summary": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "expires_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "expired_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "expired_reason": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "disabled_reason": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "created_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "updated_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "store": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "domain": {
++ "type": "string"
++ },
++ "name": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "url": {
++ "type": "string"
++ }
++ }
++ },
++ "product": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "slug": {
++ "type": "string"
++ },
++ "name": {
++ "type": "string"
++ },
++ "url": {
++ "type": "string"
++ }
++ }
++ },
++ "latest_observation": {
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "id",
++ "observed_at",
++ "origin",
++ "source"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "observed_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "recorded_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "price": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "currency": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "shipping": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "shipping_note": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "condition": {
++ "enum": [
++ "new",
++ "used",
++ "refurbished",
++ "damaged",
++ null
++ ]
++ },
++ "availability": {
++ "enum": [
++ "in_stock",
++ "out_of_stock",
++ "preorder",
++ "discontinued",
++ "limited",
++ "sold_out",
++ "online_only",
++ "in_store_only",
++ null
++ ]
++ },
++ "origin": {
++ "type": "string"
++ },
++ "observed_by": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "listing": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "source": {
++ "type": "object",
++ "properties": {
++ "id": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "kind": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "ref": {
++ "type": [
++ "object",
++ "null"
++ ]
++ },
++ "key": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "url": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "freshness": {},
++ "observations": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "id",
++ "observed_at",
++ "origin",
++ "source"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "observed_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "recorded_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "price": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "currency": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "shipping": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "shipping_note": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "condition": {
++ "enum": [
++ "new",
++ "used",
++ "refurbished",
++ "damaged",
++ null
++ ]
++ },
++ "availability": {
++ "enum": [
++ "in_stock",
++ "out_of_stock",
++ "preorder",
++ "discontinued",
++ "limited",
++ "sold_out",
++ "online_only",
++ "in_store_only",
++ null
++ ]
++ },
++ "origin": {
++ "type": "string"
++ },
++ "observed_by": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "listing": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "source": {
++ "type": "object",
++ "properties": {
++ "id": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "kind": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "ref": {
++ "type": [
++ "object",
++ "null"
++ ]
++ },
++ "key": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "url": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ }
++ }
++ }
++ },
++ "sources": {
++ "type": "array",
++ "items": {
++ "type": "object"
++ }
++ },
++ "merged_from": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "slug": {
++ "type": "string"
++ },
++ "title": {
++ "type": "string"
++ },
++ "merged_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ }
++ }
++ }
++ },
++ "votes": {
++ "type": "object",
++ "required": [
++ "up",
++ "down",
++ "up_weight",
++ "down_weight"
++ ],
++ "properties": {
++ "up": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "down": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "up_weight": {
++ "type": "number"
++ },
++ "down_weight": {
++ "type": "number"
++ }
++ }
++ },
++ "hotness": {
++ "type": [
++ "object",
++ "null"
++ ]
++ },
++ "indexability": {
++ "type": "object",
++ "required": [
++ "indexable"
++ ],
++ "properties": {
++ "indexable": {
++ "type": "boolean"
++ },
++ "robots": {
++ "type": "string"
++ },
++ "reasons": {
++ "type": "array"
++ }
++ }
++ }
++ }
++ }
++ },
++ "deals.product-resolve-request": {
++ "version": "1.0.0",
++ "owner": "deals",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/deals/product-resolve-request.v1.json",
++ "title": "DealsProductResolveRequest",
++ "description": "deals.product-resolve-request@1: the body of POST /api/v1/products/resolve on OpenVibe.Deals (deals.product.resolve): product aliases (gtin 8-14 digits, mpn, sku, url, name) to find the product by; with create (default true) a product is made when none matches (name then required, 2-200 characters).",
++ "type": "object",
++ "properties": {
++ "name": {
++ "type": "string"
++ },
++ "brand": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "gtin": {
++ "type": "string",
++ "pattern": "^[0-9]{8,14}$"
++ },
++ "mpn": {
++ "type": "string"
++ },
++ "sku": {
++ "type": "string"
++ },
++ "url": {
++ "type": "string"
++ },
++ "category": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "description": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "create": {
++ "type": "boolean"
++ }
++ }
++ }
++ },
++ "deals.product-resolve-result": {
++ "version": "1.0.0",
++ "owner": "deals",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/deals/product-resolve-result.v1.json",
++ "title": "DealsProductResolveResult",
++ "description": "deals.product-resolve-result@1: the answer of POST /api/v1/products/resolve on OpenVibe.Deals: { product, created, conflicts } — 201 when the product was created; conflicts lists aliases that pointed elsewhere. Nothing matching with create false is 404 product.not_found.",
++ "type": "object",
++ "required": [
++ "product",
++ "created",
++ "conflicts"
++ ],
++ "properties": {
++ "product": {
++ "type": "object",
++ "required": [
++ "id",
++ "slug",
++ "name",
++ "url"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "slug": {
++ "type": "string"
++ },
++ "name": {
++ "type": "string"
++ },
++ "url": {
++ "type": "string"
++ }
++ }
++ },
++ "created": {
++ "type": "boolean"
++ },
++ "conflicts": {
++ "type": "array"
++ }
++ }
++ }
++ },
++ "deals.vote-request": {
++ "version": "1.0.0",
++ "owner": "deals",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/deals/vote-request.v1.json",
++ "title": "DealsVoteRequest",
++ "description": "deals.vote-request@1: the body of PUT /api/v1/offers/:id/vote on OpenVibe.Deals (deals.vote.set; a person): { value: 1 | -1 }. DELETE /api/v1/offers/:id/vote (deals.vote.remove) takes no body.",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "value"
++ ],
++ "properties": {
++ "value": {
++ "enum": [
++ 1,
++ -1,
++ "1",
++ "-1"
++ ]
++ }
++ }
++ },
++ {
++ "$ref": "../common/no-body.v1.json"
++ }
++ ]
++ }
++ },
++ "deals.vote-result": {
++ "version": "1.0.0",
++ "owner": "deals",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/deals/vote-result.v1.json",
++ "title": "DealsVoteResult",
++ "description": "deals.vote-result@1: the answer of PUT and DELETE /api/v1/offers/:id/vote on OpenVibe.Deals: { offer_id, value (0 after a removal), previous, changed, votes } — the vote lands on the canonical offer of a merge group.",
++ "type": "object",
++ "required": [
++ "offer_id",
++ "value",
++ "previous",
++ "changed",
++ "votes"
++ ],
++ "properties": {
++ "offer_id": {
++ "type": "string"
++ },
++ "value": {
++ "enum": [
++ 1,
++ -1,
++ 0
++ ]
++ },
++ "previous": {
++ "enum": [
++ 1,
++ -1,
++ 0,
++ null
++ ]
++ },
++ "changed": {
++ "type": "boolean"
++ },
++ "votes": {
++ "type": "object",
++ "required": [
++ "up",
++ "down",
++ "up_weight",
++ "down_weight"
++ ],
++ "properties": {
++ "up": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "down": {
++ "type": "integer",
++ "minimum": 0
++ },
++ "up_weight": {
++ "type": "number"
++ },
++ "down_weight": {
++ "type": "number"
++ }
++ }
++ }
++ }
++ }
++ },
++ "deals.watch-request": {
++ "version": "1.0.0",
++ "owner": "deals",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/deals/watch-request.v1.json",
++ "title": "DealsWatchRequest",
++ "description": "deals.watch-request@1: the body of POST /api/v1/watches on OpenVibe.Deals (deals.watch.create; the acting person): kind keyword or search (query with at least one word; a search is saved without notifications), product (product, a slug or id), or price_below (max_price and currency, with a product or keywords). label is optional (120 characters). There is a per-person limit (429 watch.limit).",
++ "type": "object",
++ "required": [
++ "kind"
++ ],
++ "properties": {
++ "kind": {
++ "enum": [
++ "keyword",
++ "product",
++ "price_below",
++ "search"
++ ]
++ },
++ "query": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 200
++ },
++ "product": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "max_price": {
++ "type": [
++ "string",
++ "number",
++ "null"
++ ],
++ "description": "A decimal amount; always with its currency."
++ },
++ "currency": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^[A-Za-z]{3}$",
++ "description": "ISO 4217; Deals never assumes one."
++ },
++ "label": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 120
++ }
++ },
++ "allOf": [
++ {
++ "if": {
++ "properties": {
++ "kind": {
++ "enum": [
++ "keyword",
++ "search"
++ ]
++ }
++ }
++ },
++ "then": {
++ "required": [
++ "query"
++ ]
++ }
++ },
++ {
++ "if": {
++ "properties": {
++ "kind": {
++ "const": "product"
++ }
++ }
++ },
++ "then": {
++ "required": [
++ "product"
++ ]
++ }
++ },
++ {
++ "if": {
++ "properties": {
++ "kind": {
++ "const": "price_below"
++ }
++ }
++ },
++ "then": {
++ "required": [
++ "max_price",
++ "currency"
++ ]
++ }
++ }
++ ]
++ }
++ },
++ "deals.watch-result": {
++ "version": "1.0.0",
++ "owner": "deals",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/deals/watch-result.v1.json",
++ "title": "DealsWatchResult",
++ "description": "deals.watch-result@1: answers of the watch routes on OpenVibe.Deals: GET /api/v1/watches → { watches } (the acting person's; deals.watch.read); POST /api/v1/watches → 201 { watch } (deals.watch.create); DELETE /api/v1/watches/:id → { id, deleted: true } (deals.watch.delete; someone else's watch is 404).",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "watches"
++ ],
++ "properties": {
++ "watches": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "id",
++ "kind",
++ "notify",
++ "created_at"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "kind": {
++ "enum": [
++ "keyword",
++ "product",
++ "price_below",
++ "search"
++ ]
++ },
++ "query": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "label": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "notify": {
++ "type": "boolean"
++ },
++ "product": {
++ "type": [
++ "object",
++ "null"
++ ]
++ },
++ "max_price": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "currency": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "created_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "notifications": {
++ "type": "integer",
++ "minimum": 0
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "watch"
++ ],
++ "properties": {
++ "watch": {
++ "type": "object",
++ "required": [
++ "id",
++ "kind",
++ "notify",
++ "created_at"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "kind": {
++ "enum": [
++ "keyword",
++ "product",
++ "price_below",
++ "search"
++ ]
++ },
++ "query": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "label": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "notify": {
++ "type": "boolean"
++ },
++ "product": {
++ "type": [
++ "object",
++ "null"
++ ]
++ },
++ "max_price": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "currency": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "created_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "notifications": {
++ "type": "integer",
++ "minimum": 0
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "id",
++ "deleted"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "deleted": {
++ "const": true
++ }
++ }
++ }
++ ]
++ }
++ },
"trade.observation.created": {
"version": "1.0.0",
"owner": "trade",
"status": "active",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
-- "$id": "https://openvibe.network/contracts/events/payloads/trade.observation.created.v1.json",
-- "title": "TradeObservationCreatedPayload",
-- "description": "trade.observation.created v1 (OpenVibe.Trade server/domain/observations.js record). A market observation was recorded: one value a source stated for an instrument (metric, the decimal exactly as stated, unit, currency when monetary, period), when it was true per the source (observed_at), when the source was fetched (retrieved_at) and when Trade recorded it, with the source reference. Recorded by a feed service through POST /api/v1/observations (trade.observation.write) or by the Sources sync (server/domain/sync.js). Only on creation: a replay of the same (source_key, source_ref) sends nothing, and a different value under the same reference is refused. Written in the transaction that inserted the immutable row, together with the source's freshness update (trade.source.recovered|stale), alert evaluation (trade.alert.triggered) and the instrument's Search document. Leaves out the freshness verdict (computed at read time) and recorded_by (the envelope actor). Envelope: subject { type: observation, id }, visibility public, priority low, actor service:<recording service> (service:trade for the Sources sync). Information only, not investment advice.",
++ "$id": "https://openvibe.network/contracts/events/payloads/trade.observation.created.v1.json",
++ "title": "TradeObservationCreatedPayload",
++ "description": "trade.observation.created v1 (OpenVibe.Trade server/domain/observations.js record). A market observation was recorded: one value a source stated for an instrument (metric, the decimal exactly as stated, unit, currency when monetary, period), when it was true per the source (observed_at), when the source was fetched (retrieved_at) and when Trade recorded it, with the source reference. Recorded by a feed service through POST /api/v1/observations (trade.observation.write) or by the Sources sync (server/domain/sync.js). Only on creation: a replay of the same (source_key, source_ref) sends nothing, and a different value under the same reference is refused. Written in the transaction that inserted the immutable row, together with the source's freshness update (trade.source.recovered|stale), alert evaluation (trade.alert.triggered) and the instrument's Search document. Leaves out the freshness verdict (computed at read time) and recorded_by (the envelope actor). Envelope: subject { type: observation, id }, visibility public, priority low, actor service:<recording service> (service:trade for the Sources sync). Information only, not investment advice.",
++ "type": "object",
++ "required": [
++ "instrument",
++ "id",
++ "metric",
++ "value",
++ "unit",
++ "currency",
++ "period",
++ "observed_at",
++ "retrieved_at",
++ "recorded_at",
++ "max_age_sec",
++ "source"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "instrument": {
++ "type": "object",
++ "required": [
++ "id",
++ "symbol"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "id": {
++ "type": "string",
++ "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "symbol": {
++ "type": "string",
++ "pattern": "^[A-Z0-9][A-Z0-9.-]{0,15}$"
++ }
++ }
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^obs_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "metric": {
++ "type": "string",
++ "pattern": "^[A-Za-z][A-Za-z0-9_.:-]{0,79}$",
++ "description": "e.g. price.close, volume, us-gaap:Revenues."
++ },
++ "value": {
++ "type": "string",
++ "pattern": "^-?[0-9]+(\\.[0-9]+)?([eE][-+]?[0-9]+)?$",
++ "maxLength": 64,
++ "description": "The decimal exactly as the source stated it (text, never rounded)."
++ },
++ "unit": {
++ "type": "string",
++ "pattern": "^[A-Za-z][A-Za-z0-9/%._-]{0,19}$",
++ "description": "e.g. USD, shares, USD/shares."
++ },
++ "currency": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^[A-Z]{3}$",
++ "description": "ISO 4217 when the value is monetary; null otherwise."
++ },
++ "period": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "minLength": 1,
++ "maxLength": 40,
++ "description": "The period the value covers when the source states one (e.g. a fiscal quarter)."
++ },
++ "observed_at": {
++ "type": "string",
++ "format": "date-time",
++ "description": "When the value was true, per the source."
++ },
++ "retrieved_at": {
++ "type": "string",
++ "format": "date-time",
++ "description": "When the source was fetched."
++ },
++ "recorded_at": {
++ "type": "string",
++ "format": "date-time",
++ "description": "When Trade recorded the row."
++ },
++ "max_age_sec": {
++ "type": [
++ "integer",
++ "null"
++ ],
++ "minimum": 60,
++ "maximum": 315360000,
++ "description": "The value's own maximum age, when the recorder gave one."
++ },
++ "source": {
++ "type": "object",
++ "required": [
++ "key",
++ "item_id",
++ "url",
++ "ref"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "key": {
++ "type": "string",
++ "pattern": "^[a-z0-9][a-z0-9-]{1,63}$",
++ "description": "OpenVibe.Sources source key."
++ },
++ "item_id": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^itm_[0-9A-HJKMNP-TV-Z]{26}$",
++ "description": "The Sources item the value came from, when there is one."
++ },
++ "url": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^https?://",
++ "description": "The source's page for the value (absolute http(s) URL, normalised)."
++ },
++ "ref": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 300,
++ "description": "The source's own reference for this datum; unique per source_key."
++ }
++ }
++ }
++ }
++ }
++ },
++ "trade.source.stale": {
++ "version": "1.0.0",
++ "owner": "trade",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/trade.source.stale.v1.json",
++ "title": "TradeSourceStalePayload",
++ "description": "trade.source.stale v1 (OpenVibe.Trade server/domain/freshness.js evaluate). A source Trade shows data from went stale by Trade's own clock: its last successful fetch is older than its staleness window (Sources' stale_after_sec, else TRADE_DEFAULT_STALE_AFTER_SEC), or no success is known at all. Sent only on a transition (fresh to stale), or once when Trade first evaluates a source that is already stale; replays and further ticks send nothing. Evaluated when the Sources sync reports a source's health (sync.js reportHealth), when a recorded observation or filing proves a retrieval (noteRetrieval), and by the worker (evaluateAll). Sources' own health status is carried as upstream_status but is not the verdict. Envelope: subject { type: source, id: <source_key> }, visibility public, priority low, actor service:trade.",
++ "type": "object",
++ "required": [
++ "source_key",
++ "stale",
++ "stale_since",
++ "last_success_at",
++ "stale_after_sec",
++ "upstream_status",
++ "evaluated_at"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "source_key": {
++ "type": "string",
++ "pattern": "^[a-z0-9][a-z0-9-]{1,63}$",
++ "description": "OpenVibe.Sources source key."
++ },
++ "stale": {
++ "const": true
++ },
++ "stale_since": {
++ "type": "string",
++ "format": "date-time",
++ "description": "Last success + window, or when Trade first saw the source if it never succeeded."
++ },
++ "last_success_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time",
++ "description": "Last known successful fetch; null when none is known."
++ },
++ "stale_after_sec": {
++ "type": "integer",
++ "description": "The staleness window applied, in seconds."
++ },
++ "upstream_status": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "Sources' own health status as last reported (healthy, stale, failing, never_fetched, disabled, manual); null if never reported."
++ },
++ "evaluated_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ },
++ "trade.source.recovered": {
++ "version": "1.0.0",
++ "owner": "trade",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/trade.source.recovered.v1.json",
++ "title": "TradeSourceRecoveredPayload",
++ "description": "trade.source.recovered v1 (OpenVibe.Trade server/domain/freshness.js evaluate). A stale source is fresh again by Trade's own clock: a successful fetch younger than its staleness window is now known, reported through Sources' health during the sync (sync.js reportHealth) or proved by the retrieved_at of a newly recorded observation or filing (noteRetrieval). Sent only on a stale-to-fresh transition; a source that is already fresh the first time Trade evaluates it is not a recovery and sends nothing. Envelope: subject { type: source, id: <source_key> }, visibility public, priority low, actor service:trade.",
++ "type": "object",
++ "required": [
++ "source_key",
++ "stale",
++ "stale_since",
++ "last_success_at",
++ "stale_after_sec",
++ "upstream_status",
++ "evaluated_at"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "source_key": {
++ "type": "string",
++ "pattern": "^[a-z0-9][a-z0-9-]{1,63}$",
++ "description": "OpenVibe.Sources source key."
++ },
++ "stale": {
++ "const": false
++ },
++ "stale_since": {
++ "type": "null",
++ "description": "Always null: the source is fresh."
++ },
++ "last_success_at": {
++ "type": "string",
++ "format": "date-time",
++ "description": "The successful fetch that made it fresh (or a later one)."
++ },
++ "stale_after_sec": {
++ "type": "integer",
++ "description": "The staleness window applied, in seconds."
++ },
++ "upstream_status": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "description": "Sources' own health status as last reported (healthy, stale, failing, never_fetched, disabled, manual); null if never reported."
++ },
++ "evaluated_at": {
++ "type": "string",
++ "format": "date-time"
++ }
++ }
++ }
++ },
++ "trade.index_document.upserted": {
++ "version": "1.0.0",
++ "owner": "trade",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/trade.index_document.upserted.v1.json",
++ "title": "TradeIndexDocumentUpsertedPayload",
++ "description": "trade.index_document.upserted v1 (OpenVibe.Trade server/domain/indexing.js refresh, via openvibe-publishing/index-hooks indexEvent). The OpenVibe.Search document of an instrument page (/i/<symbol>), the only public indexable object Trade has (watchlists, alert rules and deliveries never reach Search). Sent while the instrument is active and the SEO gate lets the page be listed (financial context is sensitive: the page is hidden until a person-reviewed context is published), whenever the document changed: after a recorded observation, a context publish, review or retraction, an instrument edit, and the worker's refresh pass. The sequencer (trade_index_revisions) gives an unchanged document its old revision and nothing is sent. A page whose newest monetary observation is too old stays as a noindex document (reason stale_price). Title '<symbol> — <name>', language en, facets kind, symbol, exchange; provenance lists Trade's own cited records. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: instrument, id, revision: <document revision> }, visibility internal, priority important, actor service:trade.",
++ "allOf": [
++ {
++ "$ref": "../../search/index-document.v1.json"
++ },
++ {
++ "type": "object",
++ "required": [
++ "owner",
++ "type",
++ "deleted"
++ ],
++ "properties": {
++ "owner": {
++ "const": "trade"
++ },
++ "type": {
++ "const": "instrument"
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "deleted": {
++ "const": false
++ }
++ }
++ }
++ ]
++ }
++ },
++ "trade.index_document.deleted": {
++ "version": "1.0.0",
++ "owner": "trade",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/events/payloads/trade.index_document.deleted.v1.json",
++ "title": "TradeIndexDocumentDeletedPayload",
++ "description": "trade.index_document.deleted v1 (OpenVibe.Trade server/domain/indexing.js refresh, via openvibe-publishing/index-hooks indexEvent). A Search tombstone for an instrument page: the instrument was archived, or the page stopped being listable (e.g. its reviewed context was retracted). Only sent for a page Search was sent before, and only when the tombstone's revision moved. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: instrument, id, revision }, visibility internal, priority important, actor service:trade.",
++ "type": "object",
++ "required": [
++ "type",
++ "id",
++ "revision"
++ ],
++ "additionalProperties": false,
++ "properties": {
++ "type": {
++ "const": "instrument"
++ },
++ "id": {
++ "type": "string",
++ "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"
++ },
++ "revision": {
++ "type": "integer",
++ "minimum": 1,
++ "maximum": 9007199254740991,
++ "description": "Index revision of the tombstone; wins over any document at the same or an older revision."
++ }
++ }
++ }
++ },
++ "trade.alert-read-result": {
++ "version": "1.0.0",
++ "owner": "trade",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/trade/alert-read-result.v1.json",
++ "title": "TradeAlertReadResult",
++ "description": "trade.alert-read-result@1: answers of trade.alert.read on OpenVibe.Trade (the acting person's): GET /api/v1/alerts → { rules }; GET /api/v1/alerts/deliveries?limit= → { deliveries } (newest first, at most 200: which rule fired on what, and the event sent).",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "rules"
++ ],
++ "properties": {
++ "rules": {
++ "type": "array",
++ "items": {
++ "$ref": "alert-rule.v1.json"
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "deliveries"
++ ],
++ "properties": {
++ "deliveries": {
++ "type": "array",
++ "maxItems": 200,
++ "items": {
++ "type": "object",
++ "required": [
++ "id",
++ "rule_id",
++ "trigger"
++ ],
++ "properties": {
++ "id": {
++ "type": [
++ "string",
++ "integer"
++ ]
++ },
++ "rule_id": {
++ "type": "string"
++ },
++ "rule_kind": {
++ "type": "string"
++ },
++ "trigger": {
++ "type": "object",
++ "properties": {
++ "kind": {
++ "type": "string"
++ },
++ "id": {
++ "type": "string"
++ }
++ }
++ },
++ "event_id": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "created_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ }
++ }
++ }
++ }
++ }
++ }
++ ]
++ }
++ },
++ "trade.alert-rule-request": {
++ "version": "1.0.0",
++ "owner": "trade",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/trade/alert-rule-request.v1.json",
++ "title": "TradeAlertRuleRequest",
++ "description": "trade.alert-rule-request@1: the body of POST /api/v1/alerts on OpenVibe.Trade (trade.alert.create; the acting person): an active instrument by symbol and the rule kind. threshold needs metric, operator (above or below), threshold (a finite decimal) and unit, and may name a currency; filing_type needs 1-10 form_types (a list or comma-separated, e.g. 10-K, 8-K); new_document needs nothing more. There is a per-person limit (429 alert.limit).",
++ "type": "object",
++ "required": [
++ "symbol",
++ "kind"
++ ],
++ "properties": {
++ "symbol": {
++ "type": "string"
++ },
++ "kind": {
++ "enum": [
++ "threshold",
++ "filing_type",
++ "new_document"
++ ]
++ },
++ "metric": {
++ "type": "string",
++ "maxLength": 80
++ },
++ "operator": {
++ "enum": [
++ "above",
++ "below"
++ ]
++ },
++ "threshold": {
++ "type": [
++ "string",
++ "number"
++ ]
++ },
++ "unit": {
++ "type": "string",
++ "maxLength": 20
++ },
++ "currency": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^\\s*[A-Za-z]{3}\\s*$|^$",
++ "description": "ISO 4217."
++ },
++ "form_types": {
++ "type": [
++ "array",
++ "string"
++ ],
++ "items": {
++ "type": "string"
++ }
++ }
++ },
++ "allOf": [
++ {
++ "if": {
++ "properties": {
++ "kind": {
++ "const": "threshold"
++ }
++ }
++ },
++ "then": {
++ "required": [
++ "metric",
++ "operator",
++ "threshold",
++ "unit"
++ ]
++ }
++ },
++ {
++ "if": {
++ "properties": {
++ "kind": {
++ "const": "filing_type"
++ }
++ }
++ },
++ "then": {
++ "required": [
++ "form_types"
++ ]
++ }
++ }
++ ]
++ }
++ },
++ "trade.alert-rule-result": {
++ "version": "1.0.0",
++ "owner": "trade",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/trade/alert-rule-result.v1.json",
++ "title": "TradeAlertRuleResult",
++ "description": "trade.alert-rule-result@1: the answer of POST /api/v1/alerts on OpenVibe.Trade: 201 { rule }.",
++ "type": "object",
++ "required": [
++ "rule"
++ ],
++ "properties": {
++ "rule": {
++ "$ref": "alert-rule.v1.json"
++ }
++ }
++ }
++ },
++ "trade.alert-rule": {
++ "version": "1.0.0",
++ "owner": "trade",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/trade/alert-rule.v1.json",
++ "title": "TradeAlertRule",
++ "description": "trade.alert-rule@1: one alert rule of a person on OpenVibe.Trade (server/domain/alerts.js ruleDto): a threshold on a metric, filings of some form types, or any new document for an instrument. Alerts inform; they never trade.",
++ "type": "object",
++ "required": [
++ "id",
++ "kind",
++ "status",
++ "instrument"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "kind": {
++ "enum": [
++ "threshold",
++ "filing_type",
++ "new_document"
++ ]
++ },
++ "status": {
++ "type": "string"
++ },
++ "instrument": {
++ "type": "object",
++ "required": [
++ "id"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "symbol": {
++ "type": "string"
++ },
++ "name": {
++ "type": "string"
++ }
++ }
++ },
++ "metric": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "operator": {
++ "enum": [
++ "above",
++ "below",
++ null
++ ]
++ },
++ "threshold": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "unit": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "currency": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "form_types": {
++ "type": [
++ "array",
++ "null"
++ ],
++ "items": {
++ "type": "string"
++ }
++ },
++ "armed": {
++ "type": [
++ "boolean",
++ "null"
++ ]
++ },
++ "created_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "updated_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ }
++ }
++ }
++ },
++ "trade.context-proposal-result": {
++ "version": "1.0.0",
++ "owner": "trade",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/trade/context-proposal-result.v1.json",
++ "title": "TradeContextProposalResult",
++ "description": "trade.context-proposal-result@1: the answer of POST /api/v1/instruments/:symbol/context on OpenVibe.Trade: 201 { revision, published } (published only for an editor's revision sent with publish).",
++ "type": "object",
++ "required": [
++ "revision",
++ "published"
++ ],
++ "properties": {
++ "revision": {
++ "$ref": "context-revision.v1.json"
++ },
++ "published": {
++ "type": "boolean"
++ }
++ }
++ }
++ },
++ "trade.context-proposal": {
++ "version": "1.0.0",
++ "owner": "trade",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/trade/context-proposal.v1.json",
++ "title": "TradeContextProposal",
++ "description": "trade.context-proposal@1: the body of POST /api/v1/instruments/:symbol/context on OpenVibe.Trade (trade.context.propose). An editor: { body, cites?, expected_revision?, publish?, message? } (Markdown up to 20000 characters). OpenVibe.AI (X-OV-Origin: ai): { workflow: { id: trade.summarize_market_context, run_id, version?, model? }, input_sources: [{ source_type, source_id }], output: { summary, observations?, gaps?, citations? }, stub_provider? } — always a draft that needs a person's review. Context that recommends buying, selling or holding, or gives price targets, is refused (422 context.advice_refused).",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "body"
++ ],
++ "properties": {
++ "body": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 20000
++ },
++ "cites": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "properties": {
++ "kind": {
++ "enum": [
++ "observation",
++ "document"
++ ]
++ },
++ "id": {
++ "type": "string"
++ }
++ }
++ }
++ },
++ "expected_revision": {
++ "type": [
++ "integer",
++ "null"
++ ]
++ },
++ "publish": {
++ "type": [
++ "boolean",
++ "string"
++ ]
++ },
++ "message": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "workflow",
++ "output"
++ ],
++ "properties": {
++ "workflow": {
++ "type": "object",
++ "required": [
++ "id"
++ ],
++ "anyOf": [
++ {
++ "required": [
++ "run_id"
++ ]
++ },
++ {
++ "required": [
++ "runId"
++ ]
++ }
++ ],
++ "properties": {
++ "id": {
++ "const": "trade.summarize_market_context"
++ },
++ "run_id": {
++ "type": "string"
++ },
++ "runId": {
++ "type": "string"
++ },
++ "version": {},
++ "model": {}
++ }
++ },
++ "input_sources": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "source_type",
++ "source_id"
++ ],
++ "properties": {
++ "source_type": {
++ "enum": [
++ "trade.document",
++ "trade.observation"
++ ]
++ },
++ "source_id": {
++ "type": "string"
++ }
++ }
++ }
++ },
++ "output": {
++ "type": "object",
++ "required": [
++ "summary"
++ ],
++ "properties": {
++ "summary": {
++ "type": "string",
++ "minLength": 1
++ },
++ "citations": {
++ "type": "array",
++ "items": {
++ "type": "integer",
++ "minimum": 0
++ }
++ },
++ "observations": {
++ "type": "array",
++ "maxItems": 30,
++ "items": {
++ "type": "object",
++ "properties": {
++ "text": {
++ "type": "string"
++ },
++ "observed_at": {
++ "type": "string"
++ },
++ "citations": {
++ "type": "array",
++ "items": {
++ "type": "integer"
++ }
++ }
++ }
++ }
++ },
++ "gaps": {
++ "type": "array",
++ "maxItems": 20,
++ "items": {
++ "type": "string"
++ }
++ }
++ }
++ },
++ "stub_provider": {
++ "type": "boolean"
++ }
++ }
++ }
++ ]
++ }
++ },
++ "trade.context-read-result": {
++ "version": "1.0.0",
++ "owner": "trade",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/trade/context-read-result.v1.json",
++ "title": "TradeContextReadResult",
++ "description": "trade.context-read-result@1: answers of trade.context.read on OpenVibe.Trade. GET /api/v1/instruments/:symbol/context → { instrument, published, disclaimer } (published is the published context revision or null; ?all=1 adds pending drafts for editors and services); GET …/context/input → { workflow, input, deliver_to } (what OpenVibe.AI's trade.summarize_market_context workflow takes: the instrument, numbered sources with freshness, and where to POST the result).",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "instrument",
++ "published",
++ "disclaimer"
++ ],
++ "properties": {
++ "instrument": {
++ "$ref": "instrument.v1.json"
++ },
++ "published": {
++ "anyOf": [
++ {
++ "$ref": "context-revision.v1.json"
++ },
++ {
++ "type": "null"
++ }
++ ]
++ },
++ "disclaimer": {
++ "type": "string",
++ "description": "Information only — not investment advice; no trading here."
++ },
++ "pending": {
++ "type": "array",
++ "items": {
++ "$ref": "context-revision.v1.json"
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "workflow",
++ "input",
++ "deliver_to"
++ ],
++ "properties": {
++ "workflow": {
++ "type": "string"
++ },
++ "input": {
++ "type": "object",
++ "required": [
++ "instrument",
++ "sources"
++ ],
++ "properties": {
++ "instrument": {
++ "type": "object"
++ },
++ "stale_after_hours": {
++ "type": "integer"
++ },
++ "sources": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "source_type",
++ "source_id"
++ ],
++ "properties": {
++ "source_type": {
++ "enum": [
++ "trade.observation",
++ "trade.document"
++ ]
++ },
++ "source_id": {
++ "type": "string"
++ }
++ }
++ }
++ }
++ }
++ },
++ "deliver_to": {
++ "type": "object",
++ "properties": {
++ "method": {
++ "const": "POST"
++ },
++ "path": {
++ "type": "string"
++ },
++ "headers": {
++ "type": "object"
++ }
++ }
++ }
++ }
++ }
++ ]
++ }
++ },
++ "trade.context-revision": {
++ "version": "1.0.0",
++ "owner": "trade",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/trade/context-revision.v1.json",
++ "title": "TradeContextRevision",
++ "description": "trade.context-revision@1: one revision of an instrument's market context on OpenVibe.Trade (server/domain/context.js view): the Markdown, HTML and text, what it cites (observations and documents; a citation whose source is gone is marked missing), its authorship and disclosure, and whether it still needs a person's review (AI drafts do).",
++ "type": "object",
++ "required": [
++ "revision",
++ "published",
++ "body_markdown",
++ "cites",
++ "needs_review"
++ ],
++ "properties": {
++ "revision": {
++ "type": "integer",
++ "minimum": 1
++ },
++ "published": {
++ "type": "boolean"
++ },
++ "published_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "written_at": {
++ "type": [
++ "string",
++ "integer",
++ "null"
++ ]
++ },
++ "as_of": {},
++ "origin": {
++ "enum": [
++ "editor",
++ "ai",
++ null
++ ]
++ },
++ "body_markdown": {
++ "type": "string"
++ },
++ "body_html": {
++ "type": "string"
++ },
++ "text": {
++ "type": "string"
++ },
++ "cites": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "kind",
++ "id"
++ ],
++ "properties": {
++ "kind": {
++ "enum": [
++ "observation",
++ "document"
++ ]
++ },
++ "id": {
++ "type": "string"
++ },
++ "observation": {
++ "$ref": "observation.v1.json"
++ },
++ "document": {
++ "type": "object"
++ },
++ "missing": {
++ "const": true
++ }
++ }
++ }
++ },
++ "authorship": {},
++ "disclosure": {},
++ "review": {
++ "type": [
++ "object",
++ "null"
++ ],
++ "properties": {
++ "decision": {
++ "type": "string"
++ },
++ "reviewed_at": {},
++ "reviewer": {
++ "type": [
++ "string",
++ "null"
++ ]
++ }
++ }
++ },
++ "needs_review": {
++ "type": "boolean"
++ }
++ }
++ }
++ },
++ "trade.deleted-result": {
++ "version": "1.0.0",
++ "owner": "trade",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/trade/deleted-result.v1.json",
++ "title": "TradeDeletedResult",
++ "description": "trade.deleted-result@1: the answer of DELETE /api/v1/alerts/:id (trade.alert.delete) and DELETE /api/v1/watchlists/:id (trade.watchlist.delete) on OpenVibe.Trade: { deleted }. Someone else's rule or watchlist is 404.",
++ "type": "object",
++ "required": [
++ "deleted"
++ ],
++ "properties": {
++ "deleted": {
++ "type": "boolean"
++ }
++ }
++ }
++ },
++ "trade.instrument-manage-request": {
++ "version": "1.0.0",
++ "owner": "trade",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/trade/instrument-manage-request.v1.json",
++ "title": "TradeInstrumentManageRequest",
++ "description": "trade.instrument-manage-request@1: bodies of trade.instrument.manage on OpenVibe.Trade (editors, or a service with the capability). POST /api/v1/instruments: { symbol, name, kind?, exchange?, cik?, currency? } (kind defaults to equity; aliases for the symbol, CIK and name are added). PATCH /api/v1/instruments/:symbol: any of name, kind, exchange, cik, currency, status (active or archived). POST /api/v1/instruments/:symbol/aliases: { kind: ticker | cik | name, value }. Text fields must be strings; a symbol or CIK another instrument holds is 409.",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "symbol",
++ "name"
++ ],
++ "properties": {
++ "symbol": {
++ "type": "string",
++ "pattern": "^\\s*[A-Za-z0-9][A-Za-z0-9.-]{0,15}\\s*$",
++ "description": "1-16 characters of A-Z, 0-9, . or - (upper-cased)."
++ },
++ "name": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 200
++ },
++ "kind": {
++ "enum": [
++ "equity",
++ "fund",
++ "index",
++ "currency",
++ "commodity",
++ "crypto",
++ "other",
++ null,
++ ""
++ ]
++ },
++ "exchange": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 40
++ },
++ "cik": {
++ "type": [
++ "string",
++ "integer",
++ "null"
++ ],
++ "description": "1-10 digits (zero-padded to 10)."
++ },
++ "currency": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^\\s*[A-Za-z]{3}\\s*$|^$",
++ "description": "ISO 4217."
++ }
++ }
++ },
++ {
++ "type": "object",
++ "properties": {
++ "name": {
++ "type": "string",
++ "maxLength": 200
++ },
++ "kind": {
++ "enum": [
++ "equity",
++ "fund",
++ "index",
++ "currency",
++ "commodity",
++ "crypto",
++ "other"
++ ]
++ },
++ "exchange": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 40
++ },
++ "cik": {
++ "type": [
++ "string",
++ "integer",
++ "null"
++ ]
++ },
++ "currency": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^\\s*[A-Za-z]{3}\\s*$|^$",
++ "description": "ISO 4217."
++ },
++ "status": {
++ "enum": [
++ "active",
++ "archived"
++ ]
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "kind",
++ "value"
++ ],
++ "properties": {
++ "kind": {
++ "enum": [
++ "ticker",
++ "cik",
++ "name"
++ ]
++ },
++ "value": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 200
++ }
++ }
++ }
++ ]
++ }
++ },
++ "trade.instrument-manage-result": {
++ "version": "1.0.0",
++ "owner": "trade",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/trade/instrument-manage-result.v1.json",
++ "title": "TradeInstrumentManageResult",
++ "description": "trade.instrument-manage-result@1: answers of trade.instrument.manage on OpenVibe.Trade: POST /api/v1/instruments → 201 { instrument, aliases }; PATCH → { instrument }; POST …/aliases → 201 { alias, aliases } (alias { kind, value, normalized }).",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "instrument"
++ ],
++ "properties": {
++ "instrument": {
++ "$ref": "instrument.v1.json"
++ },
++ "aliases": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "properties": {
++ "kind": {
++ "type": "string"
++ },
++ "value": {
++ "type": "string"
++ },
++ "normalized": {
++ "type": "string"
++ }
++ }
++ }
++ }
++ }
++ },
++ {
++ "type": "object",
++ "required": [
++ "alias",
++ "aliases"
++ ],
++ "properties": {
++ "alias": {
++ "type": "object",
++ "required": [
++ "kind",
++ "value",
++ "normalized"
++ ],
++ "properties": {
++ "kind": {
++ "enum": [
++ "ticker",
++ "cik",
++ "name"
++ ]
++ },
++ "value": {
++ "type": "string"
++ },
++ "normalized": {
++ "type": "string"
++ }
++ }
++ },
++ "aliases": {
++ "type": "array"
++ }
++ }
++ }
++ ]
++ }
++ },
++ "trade.instrument": {
++ "version": "1.0.0",
++ "owner": "trade",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/trade/instrument.v1.json",
++ "title": "TradeInstrument",
++ "description": "trade.instrument@1: one instrument as OpenVibe.Trade's API shows it (server/domain/reading.js instrumentDto).",
++ "type": "object",
++ "required": [
++ "id",
++ "symbol",
++ "name",
++ "kind",
++ "status",
++ "url"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "symbol": {
++ "type": "string"
++ },
++ "name": {
++ "type": "string"
++ },
++ "kind": {
++ "enum": [
++ "equity",
++ "fund",
++ "index",
++ "currency",
++ "commodity",
++ "crypto",
++ "other"
++ ]
++ },
++ "exchange": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "cik": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^[0-9]{10}$"
++ },
++ "currency": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "status": {
++ "enum": [
++ "active",
++ "archived"
++ ]
++ },
++ "url": {
++ "type": "string"
++ },
++ "created_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "updated_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ }
++ }
++ }
++ },
++ "trade.observation-request": {
++ "version": "1.0.0",
++ "owner": "trade",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/trade/observation-request.v1.json",
++ "title": "TradeObservationRequest",
++ "description": "trade.observation-request@1: the body of POST /api/v1/observations on OpenVibe.Trade (trade.observation.write; services only): one value a source stated for an instrument (symbol or instrument_id). value is the finite decimal as stated; observed_at and retrieved_at are date-times with a zone and not in the future; source_key names the OpenVibe.Sources source and source_ref the statement within it. The same source reference with the same value is answered as it was (200); a different value under the same reference is 409 observation.conflict (a correction needs its own source_ref).",
"type": "object",
"required": [
-- "instrument",
-- "id",
"metric",
"value",
"unit",
-- "currency",
-- "period",
"observed_at",
"retrieved_at",
-- "recorded_at",
-- "max_age_sec",
-- "source"
++ "source_key",
++ "source_ref"
],
-- "additionalProperties": false,
-- "properties": {
-- "instrument": {
-- "type": "object",
++ "anyOf": [
++ {
"required": [
-- "id",
"symbol"
-- ],
-- "additionalProperties": false,
-- "properties": {
-- "id": {
-- "type": "string",
-- "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"
-- },
-- "symbol": {
-- "type": "string",
-- "pattern": "^[A-Z0-9][A-Z0-9.-]{0,15}$"
-- }
-- }
++ ]
},
-- "id": {
-- "type": "string",
-- "pattern": "^obs_[0-9A-HJKMNP-TV-Z]{26}$"
++ {
++ "required": [
++ "instrument_id"
++ ]
++ }
++ ],
++ "properties": {
++ "symbol": {
++ "type": "string"
++ },
++ "instrument_id": {
++ "type": "string"
},
"metric": {
"type": "string",
-- "pattern": "^[A-Za-z][A-Za-z0-9_.:-]{0,79}$",
-- "description": "e.g. price.close, volume, us-gaap:Revenues."
++ "pattern": "^\\s*[A-Za-z][A-Za-z0-9_.:-]{0,79}\\s*$"
},
"value": {
-- "type": "string",
-- "pattern": "^-?[0-9]+(\\.[0-9]+)?([eE][-+]?[0-9]+)?$",
-- "maxLength": 64,
-- "description": "The decimal exactly as the source stated it (text, never rounded)."
++ "type": [
++ "string",
++ "number"
++ ]
},
"unit": {
"type": "string",
-- "pattern": "^[A-Za-z][A-Za-z0-9/%._-]{0,19}$",
-- "description": "e.g. USD, shares, USD/shares."
++ "pattern": "^\\s*[A-Za-z][A-Za-z0-9/%._-]{0,19}\\s*$"
},
"currency": {
"type": [
"string",
"null"
],
-- "pattern": "^[A-Z]{3}$",
-- "description": "ISO 4217 when the value is monetary; null otherwise."
++ "pattern": "^\\s*([A-Z]{3})?\\s*$"
},
"period": {
"type": [
"string",
"null"
],
-- "minLength": 1,
-- "maxLength": 40,
-- "description": "The period the value covers when the source states one (e.g. a fiscal quarter)."
++ "maxLength": 40
},
"observed_at": {
-- "type": "string",
-- "format": "date-time",
-- "description": "When the value was true, per the source."
++ "type": "string"
},
"retrieved_at": {
++ "type": "string"
++ },
++ "source_key": {
"type": "string",
-- "format": "date-time",
-- "description": "When the source was fetched."
++ "pattern": "^\\s*[a-z0-9][a-z0-9-]{1,63}\\s*$"
},
-- "recorded_at": {
++ "source_ref": {
"type": "string",
-- "format": "date-time",
-- "description": "When Trade recorded the row."
++ "minLength": 1,
++ "maxLength": 300
++ },
++ "source_item_id": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^(itm_[0-9A-HJKMNP-TV-Z]{26})?$"
++ },
++ "source_url": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "pattern": "^https?://"
},
"max_age_sec": {
"type": [
"integer",
++ "string",
"null"
],
"minimum": 60,
-- "maximum": 315360000,
-- "description": "The value's own maximum age, when the recorder gave one."
++ "maximum": 315360000
++ }
++ }
++ }
++ },
++ "trade.observation-result": {
++ "version": "1.0.0",
++ "owner": "trade",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/trade/observation-result.v1.json",
++ "title": "TradeObservationResult",
++ "description": "trade.observation-result@1: the answer of POST /api/v1/observations on OpenVibe.Trade: { observation, created } — 201 when recorded (trade.observation.created is announced, alerts are checked), 200 for a repeat of the same statement.",
++ "type": "object",
++ "required": [
++ "observation",
++ "created"
++ ],
++ "properties": {
++ "observation": {
++ "$ref": "observation.v1.json"
++ },
++ "created": {
++ "type": "boolean"
++ }
++ }
++ }
++ },
++ "trade.observation": {
++ "version": "1.0.0",
++ "owner": "trade",
++ "status": "active",
++ "schema": {
++ "$schema": "https://json-schema.org/draft/2020-12/schema",
++ "$id": "https://openvibe.network/contracts/trade/observation.v1.json",
++ "title": "TradeObservation",
++ "description": "trade.observation@1: one market observation on OpenVibe.Trade (server/domain/observations.js dto): a value a source stated (the decimal exactly as stated), when it was true and when it was fetched, where from, and (in reads) whether it is stale by its source's freshness or its own max age. Rows are immutable.",
++ "type": "object",
++ "required": [
++ "id",
++ "metric",
++ "value",
++ "unit",
++ "observed_at",
++ "retrieved_at",
++ "recorded_at",
++ "source"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "metric": {
++ "type": "string"
++ },
++ "value": {
++ "type": "string",
++ "description": "The decimal as the source stated it."
++ },
++ "unit": {
++ "type": "string"
++ },
++ "currency": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "period": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "observed_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "retrieved_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "recorded_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "max_age_sec": {
++ "type": [
++ "integer",
++ "null"
++ ]
},
"source": {
"type": "object",
"required": [
-- "key",
-- "item_id",
-- "url",
-- "ref"
++ "key"
],
-- "additionalProperties": false,
"properties": {
"key": {
-- "type": "string",
-- "pattern": "^[a-z0-9][a-z0-9-]{1,63}$",
-- "description": "OpenVibe.Sources source key."
++ "type": "string"
},
"item_id": {
"type": [
"string",
"null"
-- ],
-- "pattern": "^itm_[0-9A-HJKMNP-TV-Z]{26}$",
-- "description": "The Sources item the value came from, when there is one."
++ ]
},
"url": {
"type": [
"string",
"null"
-- ],
-- "pattern": "^https?://",
-- "description": "The source's page for the value (absolute http(s) URL, normalised)."
++ ]
},
"ref": {
-- "type": "string",
-- "minLength": 1,
-- "maxLength": 300,
-- "description": "The source's own reference for this datum; unique per source_key."
++ "type": "string"
}
}
++ },
++ "freshness": {
++ "type": "object",
++ "properties": {
++ "stale": {
++ "type": "boolean"
++ },
++ "stale_since": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "reason": {
++ "enum": [
++ "value_older_than_max_age",
++ "source_unknown",
++ "source_stale",
++ null
++ ]
++ },
++ "source_status": {},
++ "source_last_success_at": {}
++ }
}
}
}
},
-- "trade.source.stale": {
++ "trade.resolve-result": {
"version": "1.0.0",
"owner": "trade",
"status": "active",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
-- "$id": "https://openvibe.network/contracts/events/payloads/trade.source.stale.v1.json",
-- "title": "TradeSourceStalePayload",
-- "description": "trade.source.stale v1 (OpenVibe.Trade server/domain/freshness.js evaluate). A source Trade shows data from went stale by Trade's own clock: its last successful fetch is older than its staleness window (Sources' stale_after_sec, else TRADE_DEFAULT_STALE_AFTER_SEC), or no success is known at all. Sent only on a transition (fresh to stale), or once when Trade first evaluates a source that is already stale; replays and further ticks send nothing. Evaluated when the Sources sync reports a source's health (sync.js reportHealth), when a recorded observation or filing proves a retrieval (noteRetrieval), and by the worker (evaluateAll). Sources' own health status is carried as upstream_status but is not the verdict. Envelope: subject { type: source, id: <source_key> }, visibility public, priority low, actor service:trade.",
++ "$id": "https://openvibe.network/contracts/trade/resolve-result.v1.json",
++ "title": "TradeResolveResult",
++ "description": "trade.resolve-result@1: the answer of GET /api/v1/instruments/resolve?q=&kind= on OpenVibe.Trade (trade.instrument.resolve): { query, status: resolved | ambiguous | not_found, match: { kind, value } | null, instrument | null, candidates } (a CIK, symbol or alias matched; candidates when more than one could be meant).",
"type": "object",
"required": [
-- "source_key",
-- "stale",
-- "stale_since",
-- "last_success_at",
-- "stale_after_sec",
-- "upstream_status",
-- "evaluated_at"
++ "status",
++ "match",
++ "instrument",
++ "candidates"
],
-- "additionalProperties": false,
"properties": {
-- "source_key": {
-- "type": "string",
-- "pattern": "^[a-z0-9][a-z0-9-]{1,63}$",
-- "description": "OpenVibe.Sources source key."
-- },
-- "stale": {
-- "const": true
-- },
-- "stale_since": {
-- "type": "string",
-- "format": "date-time",
-- "description": "Last success + window, or when Trade first saw the source if it never succeeded."
++ "query": {},
++ "status": {
++ "enum": [
++ "resolved",
++ "ambiguous",
++ "not_found"
++ ]
},
-- "last_success_at": {
++ "match": {
"type": [
-- "string",
++ "object",
"null"
],
-- "format": "date-time",
-- "description": "Last known successful fetch; null when none is known."
-- },
-- "stale_after_sec": {
-- "type": "integer",
-- "description": "The staleness window applied, in seconds."
++ "properties": {
++ "kind": {
++ "type": "string"
++ },
++ "value": {
++ "type": "string"
++ }
++ }
},
-- "upstream_status": {
-- "type": [
-- "string",
-- "null"
-- ],
-- "description": "Sources' own health status as last reported (healthy, stale, failing, never_fetched, disabled, manual); null if never reported."
++ "instrument": {
++ "anyOf": [
++ {
++ "$ref": "instrument.v1.json"
++ },
++ {
++ "type": "null"
++ }
++ ]
},
-- "evaluated_at": {
-- "type": "string",
-- "format": "date-time"
++ "candidates": {
++ "type": "array",
++ "items": {
++ "$ref": "instrument.v1.json"
++ }
}
}
}
},
-- "trade.source.recovered": {
++ "trade.watchlist-request": {
"version": "1.0.0",
"owner": "trade",
"status": "active",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
-- "$id": "https://openvibe.network/contracts/events/payloads/trade.source.recovered.v1.json",
-- "title": "TradeSourceRecoveredPayload",
-- "description": "trade.source.recovered v1 (OpenVibe.Trade server/domain/freshness.js evaluate). A stale source is fresh again by Trade's own clock: a successful fetch younger than its staleness window is now known, reported through Sources' health during the sync (sync.js reportHealth) or proved by the retrieved_at of a newly recorded observation or filing (noteRetrieval). Sent only on a stale-to-fresh transition; a source that is already fresh the first time Trade evaluates it is not a recovery and sends nothing. Envelope: subject { type: source, id: <source_key> }, visibility public, priority low, actor service:trade.",
-- "type": "object",
-- "required": [
-- "source_key",
-- "stale",
-- "stale_since",
-- "last_success_at",
-- "stale_after_sec",
-- "upstream_status",
-- "evaluated_at"
-- ],
-- "additionalProperties": false,
-- "properties": {
-- "source_key": {
-- "type": "string",
-- "pattern": "^[a-z0-9][a-z0-9-]{1,63}$",
-- "description": "OpenVibe.Sources source key."
-- },
-- "stale": {
-- "const": false
-- },
-- "stale_since": {
-- "type": "null",
-- "description": "Always null: the source is fresh."
-- },
-- "last_success_at": {
-- "type": "string",
-- "format": "date-time",
-- "description": "The successful fetch that made it fresh (or a later one)."
-- },
-- "stale_after_sec": {
-- "type": "integer",
-- "description": "The staleness window applied, in seconds."
-- },
-- "upstream_status": {
-- "type": [
-- "string",
-- "null"
++ "$id": "https://openvibe.network/contracts/trade/watchlist-request.v1.json",
++ "title": "TradeWatchlistRequest",
++ "description": "trade.watchlist-request@1: bodies of the watchlist writes on OpenVibe.Trade (the acting person). POST /api/v1/watchlists (trade.watchlist.create) and PATCH /api/v1/watchlists/:id (trade.watchlist.update): { name } (a string, at most 80 characters; your names are unique). PUT /api/v1/watchlists/:id/items/:symbol: { note? } (at most 300 characters). DELETE …/items/:symbol takes no body.",
++ "anyOf": [
++ {
++ "type": "object",
++ "required": [
++ "name"
],
-- "description": "Sources' own health status as last reported (healthy, stale, failing, never_fetched, disabled, manual); null if never reported."
++ "properties": {
++ "name": {
++ "type": "string",
++ "minLength": 1,
++ "maxLength": 80
++ }
++ }
},
-- "evaluated_at": {
-- "type": "string",
-- "format": "date-time"
++ {
++ "type": "object",
++ "properties": {
++ "note": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "maxLength": 300
++ }
++ }
}
-- }
++ ]
}
},
-- "trade.index_document.upserted": {
++ "trade.watchlist-result": {
"version": "1.0.0",
"owner": "trade",
"status": "active",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
-- "$id": "https://openvibe.network/contracts/events/payloads/trade.index_document.upserted.v1.json",
-- "title": "TradeIndexDocumentUpsertedPayload",
-- "description": "trade.index_document.upserted v1 (OpenVibe.Trade server/domain/indexing.js refresh, via openvibe-publishing/index-hooks indexEvent). The OpenVibe.Search document of an instrument page (/i/<symbol>), the only public indexable object Trade has (watchlists, alert rules and deliveries never reach Search). Sent while the instrument is active and the SEO gate lets the page be listed (financial context is sensitive: the page is hidden until a person-reviewed context is published), whenever the document changed: after a recorded observation, a context publish, review or retraction, an instrument edit, and the worker's refresh pass. The sequencer (trade_index_revisions) gives an unchanged document its old revision and nothing is sent. A page whose newest monetary observation is too old stays as a noindex document (reason stale_price). Title '<symbol> — <name>', language en, facets kind, symbol, exchange; provenance lists Trade's own cited records. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: instrument, id, revision: <document revision> }, visibility internal, priority important, actor service:trade.",
-- "allOf": [
++ "$id": "https://openvibe.network/contracts/trade/watchlist-result.v1.json",
++ "title": "TradeWatchlistResult",
++ "description": "trade.watchlist-result@1: answers of the watchlist routes on OpenVibe.Trade: GET /api/v1/watchlists → { watchlists }; GET /api/v1/watchlists/:id, POST (201) and PATCH → { watchlist }; PUT …/items/:symbol → { added, watchlist }; DELETE …/items/:symbol → { removed, watchlist }.",
++ "anyOf": [
{
-- "$ref": "../../search/index-document.v1.json"
++ "type": "object",
++ "required": [
++ "watchlists"
++ ],
++ "properties": {
++ "watchlists": {
++ "type": "array",
++ "items": {
++ "$ref": "watchlist.v1.json"
++ }
++ }
++ }
},
{
"type": "object",
"required": [
-- "owner",
-- "type",
-- "deleted"
++ "watchlist"
],
"properties": {
-- "owner": {
-- "const": "trade"
-- },
-- "type": {
-- "const": "instrument"
++ "watchlist": {
++ "$ref": "watchlist.v1.json"
},
-- "id": {
-- "type": "string",
-- "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"
++ "added": {
++ "type": "boolean"
},
-- "deleted": {
-- "const": false
++ "removed": {
++ "type": "boolean"
}
}
}
]
}
},
-- "trade.index_document.deleted": {
++ "trade.watchlist": {
"version": "1.0.0",
"owner": "trade",
"status": "active",
"schema": {
"$schema": "https://json-schema.org/draft/2020-12/schema",
-- "$id": "https://openvibe.network/contracts/events/payloads/trade.index_document.deleted.v1.json",
-- "title": "TradeIndexDocumentDeletedPayload",
-- "description": "trade.index_document.deleted v1 (OpenVibe.Trade server/domain/indexing.js refresh, via openvibe-publishing/index-hooks indexEvent). A Search tombstone for an instrument page: the instrument was archived, or the page stopped being listable (e.g. its reviewed context was retracted). Only sent for a page Search was sent before, and only when the tombstone's revision moved. Consumed by OpenVibe.Search ('*.index_document.*'). Envelope: subject { type: instrument, id, revision }, visibility internal, priority important, actor service:trade.",
++ "$id": "https://openvibe.network/contracts/trade/watchlist.v1.json",
++ "title": "TradeWatchlist",
++ "description": "trade.watchlist@1: one of a person's watchlists on OpenVibe.Trade (server/domain/watchlists.js dto) with its instruments and notes.",
"type": "object",
"required": [
-- "type",
"id",
-- "revision"
++ "name"
],
-- "additionalProperties": false,
"properties": {
-- "type": {
-- "const": "instrument"
-- },
"id": {
++ "type": "string"
++ },
++ "name": {
"type": "string",
-- "pattern": "^ins_[0-9A-HJKMNP-TV-Z]{26}$"
++ "maxLength": 80
},
-- "revision": {
-- "type": "integer",
-- "minimum": 1,
-- "maximum": 9007199254740991,
-- "description": "Index revision of the tombstone; wins over any document at the same or an older revision."
++ "created_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "updated_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ },
++ "items": {
++ "type": "array",
++ "items": {
++ "type": "object",
++ "required": [
++ "instrument"
++ ],
++ "properties": {
++ "instrument": {
++ "type": "object",
++ "required": [
++ "id",
++ "symbol"
++ ],
++ "properties": {
++ "id": {
++ "type": "string"
++ },
++ "symbol": {
++ "type": "string"
++ },
++ "name": {
++ "type": "string"
++ },
++ "kind": {
++ "type": "string"
++ },
++ "exchange": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "status": {
++ "type": "string"
++ },
++ "url": {
++ "type": "string"
++ }
++ }
++ },
++ "note": {
++ "type": [
++ "string",
++ "null"
++ ]
++ },
++ "added_at": {
++ "type": [
++ "string",
++ "null"
++ ],
++ "format": "date-time"
++ }
++ }
++ }
}
}
}
ddiff --git a/generated/typescript/index.d.ts b/generated/typescript/index.d.ts
iindex 23e32788..dbbfb6e3 100644
---- a/generated/typescript/index.d.ts
++++ b/generated/typescript/index.d.ts
@@@ -26848,6 +26848,469 @@ export interface DealsIndexDocumentDeletedPayload {
revision: number;
}
++/** [email protected] (owner: deals) */
++/**
++ * deals.flag-request@1: the body of POST /api/v1/offers/:id/flags on OpenVibe.Deals (deals.flag.create; a person): { kind, reason?, duplicate_of? } — duplicate_of (the other deal, id, slug or /d/ link) is required for kind duplicate. An open flag of the same kind by the same person is refreshed, not repeated.
++ */
++export interface DealsFlagRequest {
++ kind: "expired" | "price_wrong" | "spam" | "duplicate" | "other";
++ reason?: string | null;
++ duplicate_of?: string | null;
++}
++
++/** [email protected] (owner: deals) */
++/**
++ * deals.flag-result@1: the answer of POST /api/v1/offers/:id/flags on OpenVibe.Deals: { flag: { id, kind, status }, created } (201 when created, 200 when an open flag was refreshed).
++ */
++export interface DealsFlagResult {
++ flag: {
++ id: string;
++ kind: string;
++ status: string;
++ };
++ created: boolean;
++}
++
++/** [email protected] (owner: deals) */
++/**
++ * deals.moderation-request@1: bodies of deals.offer.moderate on OpenVibe.Deals (moderators). POST /api/v1/offers/:id/disable: { reason } (3-300 characters); POST …/enable: { reason? }; POST …/review (a person): { note? }; POST /api/v1/flags/:id/resolve: { status?: resolved (default) | dismissed, resolution? }. GET /api/v1/flags?status= takes no body.
++ */
++export type DealsModerationRequest =
++ | {
++ reason?: string | null;
++ note?: string | null;
++ }
++ | {
++ status: "resolved" | "dismissed";
++ resolution?: string | null;
++ };
++
++/** [email protected] (owner: deals) */
++/**
++ * deals.moderation-result@1: answers of deals.offer.moderate on OpenVibe.Deals. POST /api/v1/offers/:id/disable|enable|review → { offer }; GET /api/v1/flags?status=open|resolved|dismissed → { flags }; POST /api/v1/flags/:id/resolve → { flag, changed } (changed false when it was no longer open).
++ */
++export type DealsModerationResult =
++ | {
++ offer: DealsOffer;
++ }
++ | {
++ flags: {
++ id: string;
++ kind: string;
++ origin: string;
++ reporter?: string | null;
++ reason?: string | null;
++ status: "open" | "resolved" | "dismissed";
++ details?: {};
++ created_at?: string | null;
++ updated_at?: string | null;
++ resolved_by?: string | null;
++ resolved_at?: string | null;
++ resolution?: string | null;
++ offer?: {} | null;
++ }[];
++ }
++ | {
++ flag: {
++ id: string;
++ kind: string;
++ origin: string;
++ reporter?: string | null;
++ reason?: string | null;
++ status: "open" | "resolved" | "dismissed";
++ details?: {};
++ created_at?: string | null;
++ updated_at?: string | null;
++ resolved_by?: string | null;
++ resolved_at?: string | null;
++ resolution?: string | null;
++ offer?: {} | null;
++ };
++ changed: boolean;
++ };
++
++/** [email protected] (owner: deals) */
++/**
++ * deals.offer-action-request@1: bodies of the deal actions on OpenVibe.Deals. POST /api/v1/offers/:id/expire (deals.offer.expire; the submitter or a moderator): { reason? } (a moderator's reason is recorded). POST …/disable (deals.offer.moderate): { reason } (3-300 characters, required); POST …/enable: { reason? }; POST …/review (a person): { note? }. POST …/merge (deals.offer.merge, moderators): { into, reason? } (the canonical deal, id or slug; no cycles, not an already merged offer); POST …/unmerge: { reason? }.
++ */
++export type DealsOfferActionRequest =
++ | {
++ reason?: string | null;
++ note?: string | null;
++ }
++ | {
++ into: string;
++ reason?: string | null;
++ };
++
++/** [email protected] (owner: deals) */
++/**
++ * deals.offer-result@1: the answer of the deal writes on OpenVibe.Deals: { offer } (the canonical offer after the change). POST /api/v1/offers (201), PATCH /api/v1/offers/:id, POST …/expire, …/disable, …/enable and …/review answer exactly that; POST …/observations (201) adds the observation recorded; POST …/merge adds merged { id, slug } and the before/after of the change; POST …/unmerge adds from { id, slug } and before/after.
++ */
++export interface DealsOfferResult {
++ offer: DealsOffer;
++ observation?: {
++ id: string;
++ observed_at: string | null;
++ recorded_at?: string | null;
++ price?: string | null;
++ currency?: string | null;
++ shipping?: string | null;
++ shipping_note?: string | null;
++ condition?: "new" | "used" | "refurbished" | "damaged" | null;
++ availability?:
++ | "in_stock"
++ | "out_of_stock"
++ | "preorder"
++ | "discontinued"
++ | "limited"
++ | "sold_out"
++ | "online_only"
++ | "in_store_only"
++ | null;
++ origin: string;
++ observed_by?: string | null;
++ listing?: string | null;
++ source: {
++ id?: string | null;
++ kind?: string | null;
++ ref?: {} | null;
++ key?: string | null;
++ url?: string | null;
++ };
++ };
++ merged?: {
++ id?: string;
++ slug?: string;
++ };
++ from?: {
++ id?: string;
++ slug?: string;
++ };
++ before?: unknown;
++ after?: unknown;
++}
++
++/** [email protected] (owner: deals) */
++/**
++ * deals.offer-submit-request@1: the body of POST /api/v1/offers on OpenVibe.Deals (deals.offer.submit; a signed-in person, or a service acting for one; X-OV-Origin: ai holds the text for review). url (an http(s) link to the offer; a link already posted is 409 offer.duplicate) and title (3-200 characters) are required; a price or shipping cost needs its currency; expires_at may not be in the past. The first price observation is recorded with it.
++ */
++export interface DealsOfferSubmitRequest {
++ url: string;
++ title: string;
++ description?: string | null;
++ category?: string | null;
++ expires_at?: string | null;
++ store_name?: string | null;
++ product_slug?: string | null;
++ /**
++ * Product aliases to resolve (or create) the product.
++ */
++ product?: {
++ name?: string;
++ brand?: string | null;
++ gtin?: string;
++ mpn?: string;
++ sku?: string;
++ url?: string;
++ category?: string | null;
++ description?: string | null;
++ };
++ /**
++ * A decimal amount; always with its currency.
++ */
++ price?: string | number | null;
++ /**
++ * ISO 4217; Deals never assumes one.
++ */
++ currency?: string | null;
++ /**
++ * A decimal amount; always with its currency.
++ */
++ shipping?: string | number | null;
++ shipping_note?: string | null;
++ condition?: "new" | "used" | "refurbished" | "damaged" | null;
++ availability?:
++ | "in_stock"
++ | "out_of_stock"
++ | "preorder"
++ | "discontinued"
++ | "limited"
++ | "sold_out"
++ | "online_only"
++ | "in_store_only"
++ | null;
++ note?: string | null;
++}
++
++/** [email protected] (owner: deals) */
++/**
++ * deals.offer-update-request@1: bodies of deals.offer.update on OpenVibe.Deals. PATCH /api/v1/offers/:id (the submitter or a moderator): { title?, description?, category?, expires_at?, product_slug? | product?, ai_summary? } (ai_summary only from OpenVibe.AI with X-OV-Origin: ai; product null detaches it). POST /api/v1/offers/:id/observations (a person): a price report — at least a price, shipping cost, availability or condition, with the currency of any amount; services may give observed_at (not in the future).
++ */
++export type DealsOfferUpdateRequest =
++ | {
++ title?: string;
++ description?: string | null;
++ category?: string | null;
++ expires_at?: string | null;
++ product_slug?: string | null;
++ product?: {
++ name?: string;
++ brand?: string | null;
++ gtin?: string;
++ mpn?: string;
++ sku?: string;
++ url?: string;
++ category?: string | null;
++ description?: string | null;
++ } | null;
++ ai_summary?: string | null;
++ }
++ | {
++ [k: string]: unknown | undefined;
++ };
++
++/** [email protected] (owner: deals) */
++/**
++ * deals.offer@1: one deal as OpenVibe.Deals presents it (server/domain/publication.js offerDto; the API and /d/:slug.json): the canonical (root) offer of a merge group with its store, product, price observations (the latest is "latest", never "current": freshness says whether it still holds), sources, the offers merged into it, votes, hotness and the indexability decision.
++ */
++export interface DealsOffer {
++ id: string;
++ slug: string;
++ url: string;
++ title: string;
++ description?: string | null;
++ /**
++ * The offer on the store.
++ */
++ link: string;
++ /**
++ * active, expired, disabled
++ */
++ status: string;
++ category?: string | null;
++ origin: string;
++ submitted_by?: string | null;
++ text_origin?: string | null;
++ review_state?: string | null;
++ ai_summary?: string | null;
++ expires_at?: string | null;
++ expired_at?: string | null;
++ expired_reason?: string | null;
++ disabled_reason?: string | null;
++ created_at: string | null;
++ updated_at?: string | null;
++ store?: {
++ id?: string;
++ domain?: string;
++ name?: string | null;
++ url?: string;
++ } | null;
++ product?: {
++ id?: string;
++ slug?: string;
++ name?: string;
++ url?: string;
++ } | null;
++ latest_observation?: {
++ id: string;
++ observed_at: string | null;
++ recorded_at?: string | null;
++ price?: string | null;
++ currency?: string | null;
++ shipping?: string | null;
++ shipping_note?: string | null;
++ condition?: "new" | "used" | "refurbished" | "damaged" | null;
++ availability?:
++ | "in_stock"
++ | "out_of_stock"
++ | "preorder"
++ | "discontinued"
++ | "limited"
++ | "sold_out"
++ | "online_only"
++ | "in_store_only"
++ | null;
++ origin: string;
++ observed_by?: string | null;
++ listing?: string | null;
++ source: {
++ id?: string | null;
++ kind?: string | null;
++ ref?: {} | null;
++ key?: string | null;
++ url?: string | null;
++ };
++ } | null;
++ freshness?: unknown;
++ observations: {
++ id: string;
++ observed_at: string | null;
++ recorded_at?: string | null;
++ price?: string | null;
++ currency?: string | null;
++ shipping?: string | null;
++ shipping_note?: string | null;
++ condition?: "new" | "used" | "refurbished" | "damaged" | null;
++ availability?:
++ | "in_stock"
++ | "out_of_stock"
++ | "preorder"
++ | "discontinued"
++ | "limited"
++ | "sold_out"
++ | "online_only"
++ | "in_store_only"
++ | null;
++ origin: string;
++ observed_by?: string | null;
++ listing?: string | null;
++ source: {
++ id?: string | null;
++ kind?: string | null;
++ ref?: {} | null;
++ key?: string | null;
++ url?: string | null;
++ };
++ }[];
++ sources?: {}[];
++ merged_from?: {
++ id?: string;
++ slug?: string;
++ title?: string;
++ merged_at?: string | null;
++ }[];
++ votes: {
++ up: number;
++ down: number;
++ up_weight: number;
++ down_weight: number;
++ };
++ hotness?: {} | null;
++ indexability: {
++ indexable: boolean;
++ robots?: string;
++ reasons?: unknown[];
++ };
++}
++
++/** [email protected] (owner: deals) */
++/**
++ * deals.product-resolve-request@1: the body of POST /api/v1/products/resolve on OpenVibe.Deals (deals.product.resolve): product aliases (gtin 8-14 digits, mpn, sku, url, name) to find the product by; with create (default true) a product is made when none matches (name then required, 2-200 characters).
++ */
++export interface DealsProductResolveRequest {
++ name?: string;
++ brand?: string | null;
++ gtin?: string;
++ mpn?: string;
++ sku?: string;
++ url?: string;
++ category?: string | null;
++ description?: string | null;
++ create?: boolean;
++}
++
++/** [email protected] (owner: deals) */
++/**
++ * deals.product-resolve-result@1: the answer of POST /api/v1/products/resolve on OpenVibe.Deals: { product, created, conflicts } — 201 when the product was created; conflicts lists aliases that pointed elsewhere. Nothing matching with create false is 404 product.not_found.
++ */
++export interface DealsProductResolveResult {
++ product: {
++ id: string;
++ slug: string;
++ name: string;
++ url: string;
++ };
++ created: boolean;
++ conflicts: unknown[];
++}
++
++/** [email protected] (owner: deals) */
++/**
++ * deals.vote-request@1: the body of PUT /api/v1/offers/:id/vote on OpenVibe.Deals (deals.vote.set; a person): { value: 1 | -1 }. DELETE /api/v1/offers/:id/vote (deals.vote.remove) takes no body.
++ */
++export type DealsVoteRequest =
++ | {
++ value: 1 | -1 | "1" | "-1";
++ }
++ | NoBody;
++
++/** [email protected] (owner: deals) */
++/**
++ * deals.vote-result@1: the answer of PUT and DELETE /api/v1/offers/:id/vote on OpenVibe.Deals: { offer_id, value (0 after a removal), previous, changed, votes } — the vote lands on the canonical offer of a merge group.
++ */
++export interface DealsVoteResult {
++ offer_id: string;
++ value: 1 | -1 | 0;
++ previous: 1 | -1 | 0 | null;
++ changed: boolean;
++ votes: {
++ up: number;
++ down: number;
++ up_weight: number;
++ down_weight: number;
++ };
++}
++
++/** [email protected] (owner: deals) */
++/**
++ * deals.watch-request@1: the body of POST /api/v1/watches on OpenVibe.Deals (deals.watch.create; the acting person): kind keyword or search (query with at least one word; a search is saved without notifications), product (product, a slug or id), or price_below (max_price and currency, with a product or keywords). label is optional (120 characters). There is a per-person limit (429 watch.limit).
++ */
++export type DealsWatchRequest = {
++ [k: string]: unknown | undefined;
++} & {
++ kind: "keyword" | "product" | "price_below" | "search";
++ query?: string | null;
++ product?: string | null;
++ /**
++ * A decimal amount; always with its currency.
++ */
++ max_price?: string | number | null;
++ /**
++ * ISO 4217; Deals never assumes one.
++ */
++ currency?: string | null;
++ label?: string | null;
++};
++
++/** [email protected] (owner: deals) */
++/**
++ * deals.watch-result@1: answers of the watch routes on OpenVibe.Deals: GET /api/v1/watches → { watches } (the acting person's; deals.watch.read); POST /api/v1/watches → 201 { watch } (deals.watch.create); DELETE /api/v1/watches/:id → { id, deleted: true } (deals.watch.delete; someone else's watch is 404).
++ */
++export type DealsWatchResult =
++ | {
++ watches: {
++ id: string;
++ kind: "keyword" | "product" | "price_below" | "search";
++ query?: string | null;
++ label?: string | null;
++ notify: boolean;
++ product?: {} | null;
++ max_price?: string | null;
++ currency?: string | null;
++ created_at: string | null;
++ notifications?: number;
++ }[];
++ }
++ | {
++ watch: {
++ id: string;
++ kind: "keyword" | "product" | "price_below" | "search";
++ query?: string | null;
++ label?: string | null;
++ notify: boolean;
++ product?: {} | null;
++ max_price?: string | null;
++ currency?: string | null;
++ created_at: string | null;
++ notifications?: number;
++ };
++ }
++ | {
++ id: string;
++ deleted: true;
++ };
++
/** [email protected] (owner: trade) */
/**
* trade.observation.created v1 (OpenVibe.Trade server/domain/observations.js record). A market observation was recorded: one value a source stated for an instrument (metric, the decimal exactly as stated, unit, currency when monetary, period), when it was true per the source (observed_at), when the source was fetched (retrieved_at) and when Trade recorded it, with the source reference. Recorded by a feed service through POST /api/v1/observations (trade.observation.write) or by the Sources sync (server/domain/sync.js). Only on creation: a replay of the same (source_key, source_ref) sends nothing, and a different value under the same reference is refused. Written in the transaction that inserted the immutable row, together with the source's freshness update (trade.source.recovered|stale), alert evaluation (trade.alert.triggered) and the instrument's Search document. Leaves out the freshness verdict (computed at read time) and recorded_by (the envelope actor). Envelope: subject { type: observation, id }, visibility public, priority low, actor service:<recording service> (service:trade for the Sources sync). Information only, not investment advice.
@@@ -26996,6 +27459,559 @@ export interface TradeIndexDocumentDeletedPayload {
revision: number;
}
++/** [email protected] (owner: trade) */
++/**
++ * trade.alert-read-result@1: answers of trade.alert.read on OpenVibe.Trade (the acting person's): GET /api/v1/alerts → { rules }; GET /api/v1/alerts/deliveries?limit= → { deliveries } (newest first, at most 200: which rule fired on what, and the event sent).
++ */
++export type TradeAlertReadResult =
++ | {
++ rules: TradeAlertRule[];
++ }
++ | {
++ /**
++ * @maxItems 200
++ */
++ deliveries: {
++ id: string | number;
++ rule_id: string;
++ rule_kind?: string;
++ trigger: {
++ kind?: string;
++ id?: string;
++ };
++ event_id?: string | null;
++ created_at?: string | null;
++ }[];
++ };
++
++/** [email protected] (owner: trade) */
++/**
++ * trade.alert-rule-request@1: the body of POST /api/v1/alerts on OpenVibe.Trade (trade.alert.create; the acting person): an active instrument by symbol and the rule kind. threshold needs metric, operator (above or below), threshold (a finite decimal) and unit, and may name a currency; filing_type needs 1-10 form_types (a list or comma-separated, e.g. 10-K, 8-K); new_document needs nothing more. There is a per-person limit (429 alert.limit).
++ */
++export type TradeAlertRuleRequest = {
++ [k: string]: unknown | undefined;
++} & {
++ symbol: string;
++ kind: "threshold" | "filing_type" | "new_document";
++ metric?: string;
++ operator?: "above" | "below";
++ threshold?: string | number;
++ unit?: string;
++ /**
++ * ISO 4217.
++ */
++ currency?: string | null;
++ form_types?: string[] | string;
++};
++
++/** [email protected] (owner: trade) */
++/**
++ * trade.alert-rule-result@1: the answer of POST /api/v1/alerts on OpenVibe.Trade: 201 { rule }.
++ */
++export interface TradeAlertRuleResult {
++ rule: TradeAlertRule;
++}
++
++/** [email protected] (owner: trade) */
++/**
++ * trade.alert-rule@1: one alert rule of a person on OpenVibe.Trade (server/domain/alerts.js ruleDto): a threshold on a metric, filings of some form types, or any new document for an instrument. Alerts inform; they never trade.
++ */
++export interface TradeAlertRule {
++ id: string;
++ kind: "threshold" | "filing_type" | "new_document";
++ status: string;
++ instrument: {
++ id: string;
++ symbol?: string;
++ name?: string;
++ };
++ metric?: string | null;
++ operator?: "above" | "below" | null;
++ threshold?: string | null;
++ unit?: string | null;
++ currency?: string | null;
++ form_types?: string[] | null;
++ armed?: boolean | null;
++ created_at?: string | null;
++ updated_at?: string | null;
++}
++
++/** [email protected] (owner: trade) */
++/**
++ * trade.context-proposal-result@1: the answer of POST /api/v1/instruments/:symbol/context on OpenVibe.Trade: 201 { revision, published } (published only for an editor's revision sent with publish).
++ */
++export interface TradeContextProposalResult {
++ revision: TradeContextRevision;
++ published: boolean;
++}
++
++/** [email protected] (owner: trade) */
++/**
++ * trade.context-proposal@1: the body of POST /api/v1/instruments/:symbol/context on OpenVibe.Trade (trade.context.propose). An editor: { body, cites?, expected_revision?, publish?, message? } (Markdown up to 20000 characters). OpenVibe.AI (X-OV-Origin: ai): { workflow: { id: trade.summarize_market_context, run_id, version?, model? }, input_sources: [{ source_type, source_id }], output: { summary, observations?, gaps?, citations? }, stub_provider? } — always a draft that needs a person's review. Context that recommends buying, selling or holding, or gives price targets, is refused (422 context.advice_refused).
++ */
++export type TradeContextProposal =
++ | {
++ body: string;
++ cites?: {
++ kind?: "observation" | "document";
++ id?: string;
++ }[];
++ expected_revision?: number | null;
++ publish?: boolean | string;
++ message?: string | null;
++ }
++ | {
++ workflow: {
++ [k: string]: unknown | undefined;
++ };
++ input_sources?: {
++ source_type: "trade.document" | "trade.observation";
++ source_id: string;
++ }[];
++ output: {
++ summary: string;
++ citations?: number[];
++ /**
++ * @maxItems 30
++ */
++ observations?: {
++ text?: string;
++ observed_at?: string;
++ citations?: number[];
++ }[];
++ /**
++ * @maxItems 20
++ */
++ gaps?:
++ | []
++ | [string]
++ | [string, string]
++ | [string, string, string]
++ | [string, string, string, string]
++ | [string, string, string, string, string]
++ | [string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string, string, string, string]
++ | [string, string, string, string, string, string, string, string, string, string, string, string, string]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ]
++ | [
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string,
++ string
++ ];
++ };
++ stub_provider?: boolean;
++ };
++
++/** [email protected] (owner: trade) */
++/**
++ * trade.context-read-result@1: answers of trade.context.read on OpenVibe.Trade. GET /api/v1/instruments/:symbol/context → { instrument, published, disclaimer } (published is the published context revision or null; ?all=1 adds pending drafts for editors and services); GET …/context/input → { workflow, input, deliver_to } (what OpenVibe.AI's trade.summarize_market_context workflow takes: the instrument, numbered sources with freshness, and where to POST the result).
++ */
++export type TradeContextReadResult =
++ | {
++ instrument: TradeInstrument;
++ published: TradeContextRevision | null;
++ /**
++ * Information only — not investment advice; no trading here.
++ */
++ disclaimer: string;
++ pending?: TradeContextRevision[];
++ }
++ | {
++ workflow: string;
++ input: {
++ instrument: {};
++ stale_after_hours?: number;
++ sources: {
++ source_type: "trade.observation" | "trade.document";
++ source_id: string;
++ }[];
++ };
++ deliver_to: {
++ method?: "POST";
++ path?: string;
++ headers?: {};
++ };
++ };
++
++/** [email protected] (owner: trade) */
++/**
++ * trade.context-revision@1: one revision of an instrument's market context on OpenVibe.Trade (server/domain/context.js view): the Markdown, HTML and text, what it cites (observations and documents; a citation whose source is gone is marked missing), its authorship and disclosure, and whether it still needs a person's review (AI drafts do).
++ */
++export interface TradeContextRevision {
++ revision: number;
++ published: boolean;
++ published_at?: string | null;
++ written_at?: string | number | null;
++ as_of?: unknown;
++ origin?: "editor" | "ai" | null;
++ body_markdown: string;
++ body_html?: string;
++ text?: string;
++ cites: {
++ kind: "observation" | "document";
++ id: string;
++ observation?: TradeObservation;
++ document?: {};
++ missing?: true;
++ }[];
++ authorship?: unknown;
++ disclosure?: unknown;
++ review?: {
++ decision?: string;
++ reviewed_at?: unknown;
++ reviewer?: string | null;
++ } | null;
++ needs_review: boolean;
++}
++
++/** [email protected] (owner: trade) */
++/**
++ * trade.deleted-result@1: the answer of DELETE /api/v1/alerts/:id (trade.alert.delete) and DELETE /api/v1/watchlists/:id (trade.watchlist.delete) on OpenVibe.Trade: { deleted }. Someone else's rule or watchlist is 404.
++ */
++export interface TradeDeletedResult {
++ deleted: boolean;
++}
++
++/** [email protected] (owner: trade) */
++/**
++ * trade.instrument-manage-request@1: bodies of trade.instrument.manage on OpenVibe.Trade (editors, or a service with the capability). POST /api/v1/instruments: { symbol, name, kind?, exchange?, cik?, currency? } (kind defaults to equity; aliases for the symbol, CIK and name are added). PATCH /api/v1/instruments/:symbol: any of name, kind, exchange, cik, currency, status (active or archived). POST /api/v1/instruments/:symbol/aliases: { kind: ticker | cik | name, value }. Text fields must be strings; a symbol or CIK another instrument holds is 409.
++ */
++export type TradeInstrumentManageRequest =
++ | {
++ /**
++ * 1-16 characters of A-Z, 0-9, . or - (upper-cased).
++ */
++ symbol: string;
++ name: string;
++ kind?: "equity" | "fund" | "index" | "currency" | "commodity" | "crypto" | "other" | null | "";
++ exchange?: string | null;
++ /**
++ * 1-10 digits (zero-padded to 10).
++ */
++ cik?: string | number | null;
++ /**
++ * ISO 4217.
++ */
++ currency?: string | null;
++ }
++ | {
++ name?: string;
++ kind?: "equity" | "fund" | "index" | "currency" | "commodity" | "crypto" | "other";
++ exchange?: string | null;
++ cik?: string | number | null;
++ /**
++ * ISO 4217.
++ */
++ currency?: string | null;
++ status?: "active" | "archived";
++ }
++ | {
++ kind: "ticker" | "cik" | "name";
++ value: string;
++ };
++
++/** [email protected] (owner: trade) */
++/**
++ * trade.instrument-manage-result@1: answers of trade.instrument.manage on OpenVibe.Trade: POST /api/v1/instruments → 201 { instrument, aliases }; PATCH → { instrument }; POST …/aliases → 201 { alias, aliases } (alias { kind, value, normalized }).
++ */
++export type TradeInstrumentManageResult =
++ | {
++ instrument: TradeInstrument;
++ aliases?: {
++ kind?: string;
++ value?: string;
++ normalized?: string;
++ }[];
++ }
++ | {
++ alias: {
++ kind: "ticker" | "cik" | "name";
++ value: string;
++ normalized: string;
++ };
++ aliases: unknown[];
++ };
++
++/** [email protected] (owner: trade) */
++/**
++ * trade.instrument@1: one instrument as OpenVibe.Trade's API shows it (server/domain/reading.js instrumentDto).
++ */
++export interface TradeInstrument {
++ id: string;
++ symbol: string;
++ name: string;
++ kind: "equity" | "fund" | "index" | "currency" | "commodity" | "crypto" | "other";
++ exchange?: string | null;
++ cik?: string | null;
++ currency?: string | null;
++ status: "active" | "archived";
++ url: string;
++ created_at?: string | null;
++ updated_at?: string | null;
++}
++
++/** [email protected] (owner: trade) */
++/**
++ * trade.observation-request@1: the body of POST /api/v1/observations on OpenVibe.Trade (trade.observation.write; services only): one value a source stated for an instrument (symbol or instrument_id). value is the finite decimal as stated; observed_at and retrieved_at are date-times with a zone and not in the future; source_key names the OpenVibe.Sources source and source_ref the statement within it. The same source reference with the same value is answered as it was (200); a different value under the same reference is 409 observation.conflict (a correction needs its own source_ref).
++ */
++export type TradeObservationRequest = {
++ [k: string]: unknown | undefined;
++} & {
++ symbol?: string;
++ instrument_id?: string;
++ metric: string;
++ value: string | number;
++ unit: string;
++ currency?: string | null;
++ period?: string | null;
++ observed_at: string;
++ retrieved_at: string;
++ source_key: string;
++ source_ref: string;
++ source_item_id?: string | null;
++ source_url?: string | null;
++ max_age_sec?: number | string | null;
++};
++
++/** [email protected] (owner: trade) */
++/**
++ * trade.observation-result@1: the answer of POST /api/v1/observations on OpenVibe.Trade: { observation, created } — 201 when recorded (trade.observation.created is announced, alerts are checked), 200 for a repeat of the same statement.
++ */
++export interface TradeObservationResult {
++ observation: TradeObservation;
++ created: boolean;
++}
++
++/** [email protected] (owner: trade) */
++/**
++ * trade.observation@1: one market observation on OpenVibe.Trade (server/domain/observations.js dto): a value a source stated (the decimal exactly as stated), when it was true and when it was fetched, where from, and (in reads) whether it is stale by its source's freshness or its own max age. Rows are immutable.
++ */
++export interface TradeObservation {
++ id: string;
++ metric: string;
++ /**
++ * The decimal as the source stated it.
++ */
++ value: string;
++ unit: string;
++ currency?: string | null;
++ period?: string | null;
++ observed_at: string | null;
++ retrieved_at: string | null;
++ recorded_at: string | null;
++ max_age_sec?: number | null;
++ source: {
++ key: string;
++ item_id?: string | null;
++ url?: string | null;
++ ref?: string;
++ };
++ freshness?: {
++ stale?: boolean;
++ stale_since?: string | null;
++ reason?: "value_older_than_max_age" | "source_unknown" | "source_stale" | null;
++ source_status?: unknown;
++ source_last_success_at?: unknown;
++ };
++}
++
++/** [email protected] (owner: trade) */
++/**
++ * trade.resolve-result@1: the answer of GET /api/v1/instruments/resolve?q=&kind= on OpenVibe.Trade (trade.instrument.resolve): { query, status: resolved | ambiguous | not_found, match: { kind, value } | null, instrument | null, candidates } (a CIK, symbol or alias matched; candidates when more than one could be meant).
++ */
++export interface TradeResolveResult {
++ query?: unknown;
++ status: "resolved" | "ambiguous" | "not_found";
++ match: {
++ kind?: string;
++ value?: string;
++ } | null;
++ instrument: TradeInstrument | null;
++ candidates: TradeInstrument[];
++}
++
++/** [email protected] (owner: trade) */
++/**
++ * trade.watchlist-request@1: bodies of the watchlist writes on OpenVibe.Trade (the acting person). POST /api/v1/watchlists (trade.watchlist.create) and PATCH /api/v1/watchlists/:id (trade.watchlist.update): { name } (a string, at most 80 characters; your names are unique). PUT /api/v1/watchlists/:id/items/:symbol: { note? } (at most 300 characters). DELETE …/items/:symbol takes no body.
++ */
++export type TradeWatchlistRequest =
++ | {
++ name: string;
++ }
++ | {
++ note?: string | null;
++ };
++
++/** [email protected] (owner: trade) */
++/**
++ * trade.watchlist-result@1: answers of the watchlist routes on OpenVibe.Trade: GET /api/v1/watchlists → { watchlists }; GET /api/v1/watchlists/:id, POST (201) and PATCH → { watchlist }; PUT …/items/:symbol → { added, watchlist }; DELETE …/items/:symbol → { removed, watchlist }.
++ */
++export type TradeWatchlistResult =
++ | {
++ watchlists: TradeWatchlist[];
++ }
++ | {
++ watchlist: TradeWatchlist;
++ added?: boolean;
++ removed?: boolean;
++ };
++
++/** [email protected] (owner: trade) */
++/**
++ * trade.watchlist@1: one of a person's watchlists on OpenVibe.Trade (server/domain/watchlists.js dto) with its instruments and notes.
++ */
++export interface TradeWatchlist {
++ id: string;
++ name: string;
++ created_at?: string | null;
++ updated_at?: string | null;
++ items?: {
++ instrument: {
++ id: string;
++ symbol: string;
++ name?: string;
++ kind?: string;
++ exchange?: string | null;
++ status?: string;
++ url?: string;
++ };
++ note?: string | null;
++ added_at?: string | null;
++ }[];
++}
++
/** [email protected] (owner: games) */
/**
* games.player.joined v1 (OpenVibe.Games apps/server/src/platform/gameEvents.ts playerJoinedEvent, via GameEventRecorder.recordJoin from game/gameServer.ts handleHello). A character entered the world: written to Games' event_outbox in a transaction of its own once the session exists and the welcome was sent (a new character's row is first written by its next save). restored is true when the character already existed in the database, false for a new one. Only emitted while Events publishing is configured. No position, appearance, inventory, rank or connection data. Envelope: subject { type: player, id: <player.id> }, visibility subject when the player has a Network subject and internal otherwise, priority low, actor user:<usr_...> or guest:<gst_...> from player.subject, else service:games. No consumer yet.
ddiff --git a/manifests/capabilities/deals.flag.create.json b/manifests/capabilities/deals.flag.create.json
iindex fcc58964..8226fe5f 100644
---- a/manifests/capabilities/deals.flag.create.json
++++ b/manifests/capabilities/deals.flag.create.json
@@@ -15,5 +15,7 @@
"events": [],
"implementedBy": [
"POST /api/v1/offers/:id/flags"
-- ]
++ ],
++ "inputSchema": "deals.flag-request@1",
++ "outputSchema": "deals.flag-result@1"
}
ddiff --git a/manifests/capabilities/deals.offer.expire.json b/manifests/capabilities/deals.offer.expire.json
iindex 176c6899..0e4c148d 100644
---- a/manifests/capabilities/deals.offer.expire.json
++++ b/manifests/capabilities/deals.offer.expire.json
@@@ -18,5 +18,7 @@
],
"implementedBy": [
"POST /api/v1/offers/:id/expire"
-- ]
++ ],
++ "inputSchema": "deals.offer-action-request@1",
++ "outputSchema": "deals.offer-result@1"
}
ddiff --git a/manifests/capabilities/deals.offer.merge.json b/manifests/capabilities/deals.offer.merge.json
iindex 427c6ac0..53a235e0 100644
---- a/manifests/capabilities/deals.offer.merge.json
++++ b/manifests/capabilities/deals.offer.merge.json
@@@ -20,5 +20,7 @@
"implementedBy": [
"POST /api/v1/offers/:id/merge",
"POST /api/v1/offers/:id/unmerge"
-- ]
++ ],
++ "inputSchema": "deals.offer-action-request@1",
++ "outputSchema": "deals.offer-result@1"
}
ddiff --git a/manifests/capabilities/deals.offer.moderate.json b/manifests/capabilities/deals.offer.moderate.json
iindex d7d27679..6736370b 100644
---- a/manifests/capabilities/deals.offer.moderate.json
++++ b/manifests/capabilities/deals.offer.moderate.json
@@@ -23,5 +23,7 @@
"POST /api/v1/offers/:id/review",
"GET /api/v1/flags",
"POST /api/v1/flags/:id/resolve"
-- ]
++ ],
++ "inputSchema": "deals.moderation-request@1",
++ "outputSchema": "deals.moderation-result@1"
}
ddiff --git a/manifests/capabilities/deals.offer.submit.json b/manifests/capabilities/deals.offer.submit.json
iindex fc3b3316..5b14fe41 100644
---- a/manifests/capabilities/deals.offer.submit.json
++++ b/manifests/capabilities/deals.offer.submit.json
@@@ -19,5 +19,7 @@
],
"implementedBy": [
"POST /api/v1/offers"
-- ]
++ ],
++ "inputSchema": "deals.offer-submit-request@1",
++ "outputSchema": "deals.offer-result@1"
}
ddiff --git a/manifests/capabilities/deals.offer.update.json b/manifests/capabilities/deals.offer.update.json
iindex 446103d3..bb618648 100644
---- a/manifests/capabilities/deals.offer.update.json
++++ b/manifests/capabilities/deals.offer.update.json
@@@ -20,5 +20,7 @@
"implementedBy": [
"PATCH /api/v1/offers/:id",
"POST /api/v1/offers/:id/observations"
-- ]
++ ],
++ "inputSchema": "deals.offer-update-request@1",
++ "outputSchema": "deals.offer-result@1"
}
ddiff --git a/manifests/capabilities/deals.product.resolve.json b/manifests/capabilities/deals.product.resolve.json
iindex 7f4599d3..8573380d 100644
---- a/manifests/capabilities/deals.product.resolve.json
++++ b/manifests/capabilities/deals.product.resolve.json
@@@ -17,5 +17,7 @@
],
"implementedBy": [
"POST /api/v1/products/resolve"
-- ]
++ ],
++ "inputSchema": "deals.product-resolve-request@1",
++ "outputSchema": "deals.product-resolve-result@1"
}
ddiff --git a/manifests/capabilities/deals.vote.remove.json b/manifests/capabilities/deals.vote.remove.json
iindex f7964120..dd6db60f 100644
---- a/manifests/capabilities/deals.vote.remove.json
++++ b/manifests/capabilities/deals.vote.remove.json
@@@ -17,5 +17,7 @@
],
"implementedBy": [
"DELETE /api/v1/offers/:id/vote"
-- ]
++ ],
++ "inputSchema": "common.no-body@1",
++ "outputSchema": "deals.vote-result@1"
}
ddiff --git a/manifests/capabilities/deals.vote.set.json b/manifests/capabilities/deals.vote.set.json
iindex bed4247d..67c60db2 100644
---- a/manifests/capabilities/deals.vote.set.json
++++ b/manifests/capabilities/deals.vote.set.json
@@@ -17,5 +17,7 @@
],
"implementedBy": [
"PUT /api/v1/offers/:id/vote"
-- ]
++ ],
++ "inputSchema": "deals.vote-request@1",
++ "outputSchema": "deals.vote-result@1"
}
ddiff --git a/manifests/capabilities/deals.watch.create.json b/manifests/capabilities/deals.watch.create.json
iindex 02f995bb..e93fd2b5 100644
---- a/manifests/capabilities/deals.watch.create.json
++++ b/manifests/capabilities/deals.watch.create.json
@@@ -15,5 +15,7 @@
"events": [],
"implementedBy": [
"POST /api/v1/watches"
-- ]
++ ],
++ "inputSchema": "deals.watch-request@1",
++ "outputSchema": "deals.watch-result@1"
}
ddiff --git a/manifests/capabilities/deals.watch.delete.json b/manifests/capabilities/deals.watch.delete.json
iindex fa188d83..d0a5dcbc 100644
---- a/manifests/capabilities/deals.watch.delete.json
++++ b/manifests/capabilities/deals.watch.delete.json
@@@ -15,5 +15,7 @@
"events": [],
"implementedBy": [
"DELETE /api/v1/watches/:id"
-- ]
++ ],
++ "inputSchema": "common.no-body@1",
++ "outputSchema": "deals.watch-result@1"
}
ddiff --git a/manifests/capabilities/deals.watch.read.json b/manifests/capabilities/deals.watch.read.json
iindex cc52b0a0..d8b30abe 100644
---- a/manifests/capabilities/deals.watch.read.json
++++ b/manifests/capabilities/deals.watch.read.json
@@@ -15,5 +15,7 @@
"events": [],
"implementedBy": [
"GET /api/v1/watches"
-- ]
++ ],
++ "inputSchema": "common.no-body@1",
++ "outputSchema": "deals.watch-result@1"
}
ddiff --git a/manifests/capabilities/trade.alert.create.json b/manifests/capabilities/trade.alert.create.json
iindex e58b3bec..d742f14d 100644
---- a/manifests/capabilities/trade.alert.create.json
++++ b/manifests/capabilities/trade.alert.create.json
@@@ -17,5 +17,7 @@
],
"implementedBy": [
"POST /api/v1/alerts"
-- ]
++ ],
++ "inputSchema": "trade.alert-rule-request@1",
++ "outputSchema": "trade.alert-rule-result@1"
}
ddiff --git a/manifests/capabilities/trade.alert.delete.json b/manifests/capabilities/trade.alert.delete.json
iindex 1693fa83..4edcf463 100644
---- a/manifests/capabilities/trade.alert.delete.json
++++ b/manifests/capabilities/trade.alert.delete.json
@@@ -15,5 +15,7 @@
"events": [],
"implementedBy": [
"DELETE /api/v1/alerts/:id"
-- ]
++ ],
++ "inputSchema": "common.no-body@1",
++ "outputSchema": "trade.deleted-result@1"
}
ddiff --git a/manifests/capabilities/trade.alert.read.json b/manifests/capabilities/trade.alert.read.json
iindex 8252eeac..1560f50c 100644
---- a/manifests/capabilities/trade.alert.read.json
++++ b/manifests/capabilities/trade.alert.read.json
@@@ -16,5 +16,7 @@
"implementedBy": [
"GET /api/v1/alerts",
"GET /api/v1/alerts/deliveries"
-- ]
++ ],
++ "inputSchema": "common.no-body@1",
++ "outputSchema": "trade.alert-read-result@1"
}
ddiff --git a/manifests/capabilities/trade.context.propose.json b/manifests/capabilities/trade.context.propose.json
iindex 052b05b3..ca5ca5b7 100644
---- a/manifests/capabilities/trade.context.propose.json
++++ b/manifests/capabilities/trade.context.propose.json
@@@ -15,5 +15,7 @@
"events": [],
"implementedBy": [
"POST /api/v1/instruments/:symbol/context"
-- ]
++ ],
++ "inputSchema": "trade.context-proposal@1",
++ "outputSchema": "trade.context-proposal-result@1"
}
ddiff --git a/manifests/capabilities/trade.context.read.json b/manifests/capabilities/trade.context.read.json
iindex 54c41f27..d42d8f08 100644
---- a/manifests/capabilities/trade.context.read.json
++++ b/manifests/capabilities/trade.context.read.json
@@@ -16,5 +16,7 @@
"implementedBy": [
"GET /api/v1/instruments/:symbol/context",
"GET /api/v1/instruments/:symbol/context/input"
-- ]
++ ],
++ "inputSchema": "common.no-body@1",
++ "outputSchema": "trade.context-read-result@1"
}
ddiff --git a/manifests/capabilities/trade.instrument.manage.json b/manifests/capabilities/trade.instrument.manage.json
iindex 33fb07fd..e6e3e730 100644
---- a/manifests/capabilities/trade.instrument.manage.json
++++ b/manifests/capabilities/trade.instrument.manage.json
@@@ -20,5 +20,7 @@
"POST /api/v1/instruments",
"PATCH /api/v1/instruments/:symbol",
"POST /api/v1/instruments/:symbol/aliases"
-- ]
++ ],
++ "inputSchema": "trade.instrument-manage-request@1",
++ "outputSchema": "trade.instrument-manage-result@1"
}
ddiff --git a/manifests/capabilities/trade.instrument.resolve.json b/manifests/capabilities/trade.instrument.resolve.json
iindex a8540d98..fac655fc 100644
---- a/manifests/capabilities/trade.instrument.resolve.json
++++ b/manifests/capabilities/trade.instrument.resolve.json
@@@ -15,5 +15,7 @@
"events": [],
"implementedBy": [
"GET /api/v1/instruments/resolve"
-- ]
++ ],
++ "inputSchema": "common.no-body@1",
++ "outputSchema": "trade.resolve-result@1"
}
ddiff --git a/manifests/capabilities/trade.observation.write.json b/manifests/capabilities/trade.observation.write.json
iindex 9e8a398a..0bcd7d32 100644
---- a/manifests/capabilities/trade.observation.write.json
++++ b/manifests/capabilities/trade.observation.write.json
@@@ -22,5 +22,7 @@
],
"implementedBy": [
"POST /api/v1/observations"
-- ]
++ ],
++ "inputSchema": "trade.observation-request@1",
++ "outputSchema": "trade.observation-result@1"
}
ddiff --git a/manifests/capabilities/trade.watchlist.create.json b/manifests/capabilities/trade.watchlist.create.json
iindex 488f6220..89e51d56 100644
---- a/manifests/capabilities/trade.watchlist.create.json
++++ b/manifests/capabilities/trade.watchlist.create.json
@@@ -15,5 +15,7 @@
"events": [],
"implementedBy": [
"POST /api/v1/watchlists"
-- ]
++ ],
++ "inputSchema": "trade.watchlist-request@1",
++ "outputSchema": "trade.watchlist-result@1"
}
ddiff --git a/manifests/capabilities/trade.watchlist.delete.json b/manifests/capabilities/trade.watchlist.delete.json
iindex 5db90397..a92b626d 100644
---- a/manifests/capabilities/trade.watchlist.delete.json
++++ b/manifests/capabilities/trade.watchlist.delete.json
@@@ -15,5 +15,7 @@
"events": [],
"implementedBy": [
"DELETE /api/v1/watchlists/:id"
-- ]
++ ],
++ "inputSchema": "common.no-body@1",
++ "outputSchema": "trade.deleted-result@1"
}
ddiff --git a/manifests/capabilities/trade.watchlist.read.json b/manifests/capabilities/trade.watchlist.read.json
iindex b09630b1..26c86f93 100644
---- a/manifests/capabilities/trade.watchlist.read.json
++++ b/manifests/capabilities/trade.watchlist.read.json
@@@ -16,5 +16,7 @@
"implementedBy": [
"GET /api/v1/watchlists",
"GET /api/v1/watchlists/:id"
-- ]
++ ],
++ "inputSchema": "common.no-body@1",
++ "outputSchema": "trade.watchlist-result@1"
}
ddiff --git a/manifests/capabilities/trade.watchlist.update.json b/manifests/capabilities/trade.watchlist.update.json
iindex 57e45395..9dad8950 100644
---- a/manifests/capabilities/trade.watchlist.update.json
++++ b/manifests/capabilities/trade.watchlist.update.json
@@@ -17,5 +17,7 @@
"PATCH /api/v1/watchlists/:id",
"PUT /api/v1/watchlists/:id/items/:symbol",
"DELETE /api/v1/watchlists/:id/items/:symbol"
-- ]
++ ],
++ "inputSchema": "trade.watchlist-request@1",
++ "outputSchema": "trade.watchlist-result@1"
}