Repository navigation
Publish a versioned JSON Schema for pinned traces and question documents #153
Description
Overview
The documents OracleDesk pins to IPFS are its public, verifiable artefacts. A market's question document is committed on-chain through question_hash and meta_uri. A reasoning trace is committed through reasoning_registry.publish_trace(trace_hash, ipfs_cid). The frontend's fetchAndVerifyTrace, the contracts repo's x402 trace API (x402/trace-api.ts) and any third-party auditor fetch these bytes, check the hash and then parse the JSON. Their shape is defined nowhere except implicitly in backend code, and it already differs from what the contracts repo's agents produce.
questionDocument()insrc/services/market-chain.service.tsreturns{ version: 1, question, category, contractCategory, closeTime, resolutionSource, resolutionHash, backendMarketId }, which is versioned but undocumented.- The trace payload built in
generateReasoningTrace()(src/services/reasoning.service.ts) has no version field. It contains optional fields (betFraction,betSizeUsdc,agentWallet) thatcanonicalJsonsilently drops whenundefined, so whether a key appears depends on the code path.market-maker.agent.tsaddsonChainMarketId, which other callers do not.marketIdis a backend UUID, not the on-chain id. - The contracts repo's
agents/core/trace.tsdefinesReasoningTrace { agent: 'market-maker'|'trader', marketId: number|null, action, reasoning, inputs, timestamp: unix seconds }, which is a completely different shape for the same registry.
Consumers cannot validate content or tell which version they are reading. This issue defines versioned JSON Schemas for both documents, validates every document before it is canonicalised and pinned, serves the schemas at stable URLs, generates the TypeScript types from them, and adds compatibility tests against the contracts repo's trace type. Already-pinned traces must stay verifiable.
Background & current behavior
src/utils/hash.util.ts:canonicalJson(sorted keys, bigint → string,Date→ ISO, dropsundefined),canonicalBytes,sha256Hex,verifyTraceHash. The comment says it mirrorscanonicalizeinagents/core/trace.ts, but the contracts version has noDatecase: aDatehas no own keys, so it serialises as{}there and as an ISO string here. Both dropundefinedobject values only becauseJSON.stringifydoes.src/services/ipfs.service.tspinJson(document, name)canonicalises and pins whatever it receives, with no validation.src/services/trace-publish.service.tspinAndPublishTrace({ payload: Record<string, unknown>, action, ... })takes an untyped payload.trade.service.tspassespayload.tracePayloadforbuy_yes/buy_no.- No
src/schemas/directory exists. The repo has no JSON Schema validator dependency (zod 4 is used, and zod 4 hasz.toJSONSchema()). docs/api.mddescribes the trace API object, not the pinned document.
Goals
src/schemas/trace.schema.jsonandsrc/schemas/question.schema.json(JSON Schema 2020-12), each with$id, a requiredschemaVersionconst, andadditionalProperties: falseat top level.- New documents are validated before canonicalisation. Invalid documents are never pinned.
- TypeScript types generated from (or kept provably in sync with) the schemas and used by
reasoning.service.ts,market-chain.service.tsandtrace-publish.service.ts. - Schemas served by the API at stable, immutable URLs and documented in
docs/trace-format.md. - Explicit
nullinstead of omitted optional fields, so the canonical form is the same on every path. - Compatibility tests against the contracts repo's
agents/core/trace.ts, plus a mapping that can wrap a contracts-style trace in the backend envelope. - Legacy (unversioned) traces stay verifiable and are labelled
schemaVersion: 0in read paths.
Non-goals
- Encrypting trace content (separate confidential-traces issue). The schema should allow an envelope type to be added later.
- Changing the registry contract or the hash algorithm.
- Validating LLM output (the existing zod schemas in
market.service.ts/reasoning.service.tsstay).
Proposed implementation
- Author the schemas with zod as the source of truth. Create
src/schemas/trace.tsandsrc/schemas/question.ts(new) exporting zod 4 schemas, and generate the JSON files with a scriptscripts/gen-schemas.ts(new,npm run gen:schemas) that usesz.toJSONSchema(schema, { target: 'draft-2020-12' }), adds$id/$schema/title, and writessrc/schemas/*.schema.json. A--checkmode fails CI when the committed JSON is stale, likenpm run check:contracts. - Trace document v1.
Numbers must be finite. Document that floats are serialised with
export const TraceDocV1 = z.object({ schemaVersion: z.literal(1), kind: z.literal('oracledesk.trace'), traceId: z.string().uuid(), agentType: z.enum(['MARKET_MAKER', 'TRADER']), decisionType: z.string().min(1).max(64), action: z.string().min(1).max(64), // same string as publish_trace action market: z.object({ backendId: z.string().uuid(), onChainId: z.string().regex(/^\d+$/).nullable() }), probabilityEstimate: z.number().min(0).max(1), marketProbability: z.number().min(0).max(1), edge: z.number().min(-1).max(1), confidenceInterval: z.object({ lower: z.number(), upper: z.number() }).refine(ci => ci.lower <= ci.upper), sourcesUsed: z.array(z.object({ source: z.string(), weight: z.number().min(0).max(1), signal: z.string(), rawValue: z.number().nullable() })).max(50), betFraction: z.number().nullable(), betSizeUsdc: z.number().nullable(), hedgeConditions: z.array(z.string().max(500)).max(20), agentWallet: z.string().nullable(), createdAt: z.string().datetime(), }).strict();
JSON.stringify(shortest round-trip) and that consumers must not re-serialise before hashing. - Question document v2. Keep
version: 1documents valid asQuestionDocV1(the exact current shape, so already-committedquestion_hashvalues still describe valid documents), and addQuestionDocV2withschemaVersion: 2,kind: 'oracledesk.question',resolutionSpecHash(64-hex),resolutionSource: string | null,closeTime(int seconds) andcategory/contractCategoryenums that matchMarketCategoryandContractCategoryTag(src/lib/categories.ts).questionDocument()switches to v2 behindQUESTION_DOC_VERSION(default 2), with v1 retained for reproducibility tests. - Validation before pinning.
- Add
pinDocument<T>(schema: ZodType<T>, doc: T, name: string)toipfs.service.ts. It runsschema.parse(on failure →AppError(500, 'DOCUMENT_SCHEMA_INVALID', …)with zod issues in logs only), then callspinJson. pinAndPublishTracetakes a typedTraceDocV1instead ofRecord<string, unknown>. Itsactionargument must equaldoc.action(assert).createMarketOnChainpins throughpinDocument(QuestionDocV2, …).
- Add
- Build documents explicitly. Replace the ad-hoc
tracePayloadobject ingenerateReasoningTracewithbuildTraceDoc(input, computed): TraceDocV1. Every optional field is set tonull,rawValue ?? null,createdAtis ISO, andmarket.onChainIdis filled from the market row (removing the specialonChainMarketIdspread inmarket-maker.agent.ts). Updatetrade.service.ts/trader.agent.tsto pass the typed document. - Canonicalisation guard. In
hash.util.ts, addcanonicalJsonStrict(value)that throws onundefined, functions, symbols, non-finite numbers andDate(callers convert explicitly), and use it insidepinDocument. KeepcanonicalJsonunchanged for legacy verification. Also add a lightweight check that a document survivesJSON.parse(canonical)→canonicalJsonStrictwith identical bytes. - Legacy handling.
verifyTraceOnChainand the read paths parse fetched JSON withTraceDocAny = z.union([TraceDocV1, LegacyTraceDoc]), whereLegacyTraceDocis a permissivepassthroughschema withoutschemaVersionthat is mapped toschemaVersion: 0in API responses. Hash verification never depends on schema validity. A document can beverified: truewhile also beingschemaValid: false, and the API reports both. - Contracts-repo compatibility. Add
tests/fixtures/contracts-trace.json, built fromagents/core/trace.tsbuildTrace(...)output (generate it once with a small script run in thecontracts/submodule,contracts/oracledesk-stellar/agents/core/trace.ts, and commit the JSON plus its hash). Then:- write
fromContractsTrace(t: ContractsReasoningTrace): TraceDocV1mapping (agent→agentType,inputs→sourcesUsedwhen shaped,timestampseconds →createdAt); - test that
canonicalJsonand the submodule'scanonicalizeproduce identical bytes for every v1 fixture; - add a request to
docs/contract-requests.mdasking the contracts repo to adopt the sameschemaVersion/kindenvelope, or to publish its schema.
- write
- Serving and docs. Add a new route file
src/routes/schemas.routes.tsmounted at/api/v1/schemasthat servesGET /schemas/trace/v1.json,/schemas/question/v1.json,/schemas/question/v2.jsonwithCache-Control: public, max-age=31536000, immutableandContent-Type: application/schema+json. Each$idequals the public URL (from a newPUBLIC_API_BASE_URLenv var). Writedocs/trace-format.md(new): fields, canonicalisation rules, a worked example with bytes and sha256, versioning policy (any change to required fields or semantics bumpsschemaVersion), and manual verification.
Edge cases & pitfalls
- Changing the document shape changes the bytes and therefore the hash of every new trace. That is fine, because old traces keep their pinned bytes. Never "upgrade" a stored document in place, or verification breaks.
z.toJSONSchemacannot express refinements such aslower <= upper; document them indescription.market.onChainIdis null when a trace is pinned before the market is on-chain (dry-run, or the trace-before-market ordering inmarket-maker.agent.ts). The schema must allow it, and the on-chainmarket_idin the registry is authoritative.- Floats such as
0.1 + 0.2serialise as0.30000000000000004. That is deterministic in V8 but may differ in other languages' serialisers. Recommend that third parties verify the hash on raw bytes, never on a re-serialisation. - Upper limits (
max(50)sources, string lengths) must be generous enough that real LLM output never makes the cycle fail. Truncate during building, not during validation.
Testing plan
tests/trace-schema.test.ts(new):buildTraceDoc output validates against TraceDocV1 for market-maker and trader inputstrace doc never contains undefined and canonicalJsonStrict round-tripspinDocument refuses an invalid trace and never calls the transport(fakeIpfsTransport)question v1 documents for existing markets still hash to stored questionHash(fixture from STATUS Phase D / seeded data)question v2 document validates and pinslegacy trace without schemaVersion verifies by hash and reports schemaVersion 0backend canonicalJson matches contracts canonicalize on shared fixturesfromContractsTrace maps the contracts fixture to a valid TraceDocV1committed JSON schemas are up to date(runs the generator in check mode)
tests/http.test.ts:GET /api/v1/schemas/trace/v1.json returns application/schema+json with immutable caching.- Commands:
npm run gen:schemas -- --check,npm run build,npm test,npm run check:contracts.
Acceptance criteria
-
src/schemas/trace.schema.jsonandsrc/schemas/question.schema.jsonexist, are generated from zod sources and fail CI when stale. - Every new trace and question document carries
schemaVersionandkind, uses explicit nulls and validates before pinning. -
pinAndPublishTraceandcreateMarketOnChaintake typed documents, andRecord<string, unknown>payloads are gone from the pin path. - Legacy documents still verify by hash, and read paths report their schema version and validity separately from hash verification.
- A compatibility test proves byte-identical canonicalisation with the contracts repo's
canonicalize, and a mapping from the contractsReasoningTraceexists. - Schemas are served at stable, immutable URLs whose
$idmatches. -
docs/trace-format.mddocuments fields, canonicalisation, versioning and manual verification, anddocs/contract-requests.mdrecords the alignment request. -
npm run build,npm testandnpm run check:contractspass.
Resources
src/services/reasoning.service.ts,src/services/market-chain.service.ts,src/services/trace-publish.service.ts,src/services/ipfs.service.tssrc/utils/hash.util.ts,src/types/index.ts,src/lib/categories.ts,src/agents/market-maker.agent.ts,src/services/trade.service.tsdocs/api.md,docs/architecture.md,docs/contract-requests.md- Contracts repo:
agents/core/trace.ts,x402/trace-verification.ts,x402/trace-api.ts - JSON Schema 2020-12: https://json-schema.org/draft/2020-12/release-notes
- zod 4 JSON Schema: https://zod.dev/json-schema
- RFC 8785 (JSON Canonicalization Scheme) for comparison: https://www.rfc-editor.org/rfc/rfc8785
Important
Raise your pull request against the develop branch, not main.
All contributor work for this issue must be submitted as a PR into develop. Pull requests opened against main will not be merged and are closed automatically. Create your branch from develop, and before opening the PR double-check that the base branch is set to develop.
Generated by Claude Code
Activity
kolawoleoluwaferanmi014-a11y commentedon Oct 17, 2026
@kolawoleoluwaferanmi014-a11y has applied to work on this issue as part of the Stellar Wave Program's 10th wave.
ℹ️ Repo Maintainers: To accept this application, review their application or assign @kolawoleoluwaferanmi014-a11y to this issue.
monarchtk commentedon Oct 17, 2026
@monarchtk has applied to work on this issue as part of the Stellar Wave Program's 10th wave.
ℹ️ Repo Maintainers: To accept this application, review their application or assign @monarchtk to this issue.
lekan305 commentedon Oct 17, 2026
@lekan305 has applied to work on this issue as part of the Stellar Wave Program's 10th wave.
ℹ️ Repo Maintainers: To accept this application, review their application or assign @lekan305 to this issue.
bade2brazy commentedon Oct 17, 2026
@bade2brazy has applied to work on this issue as part of the Stellar Wave Program's 10th wave.
ℹ️ Repo Maintainers: To accept this application, review their application or assign @bade2brazy to this issue.
koladearthur commentedon Oct 17, 2026
@koladearthur has applied to work on this issue as part of the Stellar Wave Program's 10th wave.
ℹ️ Repo Maintainers: To accept this application, review their application or assign @koladearthur to this issue.
drips-wave commentedon Oct 17, 2026
Congratulations, @bade2brazy! 🎉 Your application was accepted by the repo's maintainers, and the issue is due on October 22, 2026.
🧑💻 @bade2brazy: Please resolve the issue such that the repo's maintainers have enough time to review your contribution before the due date. You'll earn Points for completing the issue on-time, which will make you eligible for a share of the Stellar Wave Program's reward pool.
Warning
When opening a PR, please link it to this issue to ensure it gets tracked accurately. Points are awarded when this issue is marked as completed by the maintainer.
🤠 Repo maintainers: Please keep an eye on the contributor's progress and review their work before the due date. You can manage this issue, including adjusting its complexity and points, here.
🌊 Happy Wave 🌊
bade2brazy commentedon Oct 17, 2026
Resolved in PR #161
drips-wave commentedon Oct 25, 2026
This issue has been completed by @bade2brazy as part of the Stellar Wave Program's 10th Wave 🥳
😎 @bade2brazy: You earned 200 Points for completing this issue! Once the Wave's rewards are finalized, you'll be eligible for a percentage of the Wave's reward pool based on the percentage of total points you've earned. Learn more here. You can also Leave a review to share your experience working on this issue.
🧑💻 Repo maintainers: How'd the contributor do? Leave a review to share your experience working with them.