Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ jobs:
- name: Generated contract code matches the submodule
run: npm run check:contracts

- name: JSON schemas match source definitions
run: npm run check:schemas

- run: npx prisma generate
- run: npx prisma migrate deploy
- name: Verify schema datamodel matches migrations
Expand Down
1 change: 1 addition & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ Forgot `--recurse-submodules`? Run `git submodule update --init`.

```bash
npm run check:contracts # generated contract code matches the submodule
npm run check:schemas # JSON schemas match source definitions
npx prisma migrate deploy # against a scratch database
npx prisma migrate diff --from-migrations prisma/migrations --to-schema-datamodel prisma/schema.prisma --exit-code
npx tsx scripts/audit-constraints.ts --json audit.json
Expand Down
64 changes: 64 additions & 0 deletions docs/api.md
Original file line number Diff line number Diff line change
Expand Up @@ -1619,5 +1619,69 @@ Every call in the frontend's `lib/api/*.ts` and where it lands:
| `unlockTrace` | `POST /traces/:id/unlock` (daily pass) |
| `getSpendingAllowance` / `setSpendingAllowance` | `GET` / `PUT /traces/access/allowance` |
| `getPaymentEvents` | `GET /traces/payments` |
| `getTraceKey` | `GET /traces/:id/key` |
| `getLeaderboard` | `GET /leaderboard` |
| `getWalletStats` | `GET /wallets/:address/stats` |
| `getResolution` | `GET /oracle/markets/:marketId/resolution` |
| per-trace unlock | x402 `GET {X402_URL}/traces/:onChainTraceId` |

---

## Schemas

Public, immutable JSON Schema 2020-12 documents defining verifiable artefacts:

- `GET /api/v1/schemas/trace/v1.json`: Reasoning trace document schema v1.
- `GET /api/v1/schemas/question/v2.json`: Question document schema v2 (with `resolutionSpecHash` and category enums).
- `GET /api/v1/schemas/question/v1.json`: Legacy question document schema v1 for historical verification.

Served with `Content-Type: application/schema+json` and `Cache-Control: public, max-age=31536000, immutable`.

---

## Confidential Trace Key Release

#### `GET /traces/:id/key` (auth, active subscription required)
Returns the per-trace data key for client-side decryption:
```json
{
"kid": "kek-2026-10",
"dataKey": "<base64 32 bytes>",
"plaintextHash": "64-hex"
}
```

#### `POST /internal/traces/:onChainTraceId/key` (shared secret HMAC)
Internal endpoint for x402 services to obtain data keys after verifying on-chain micropayments. Requires `x-signature`, `x-timestamp`, `x-nonce` with replay protection.

---

## Admin Generation Runs (auth, admin)

#### `GET /admin/generation-runs`
Lists saga generation runs with status, inputs, outputs, and lock details. Query: `state`, `page`, `limit`.

#### `GET /admin/generation-runs/:id`
Inspects a specific saga generation run.

#### `POST /admin/generation-runs/:id/cancel`
Cancels an incomplete run in `NEEDS_ATTENTION` before on-chain submission, safely setting associated pending market to `CANCELLED`.

---

## Leaderboard and Wallet Stats

Exact integer accounting in 7-decimal USDC base units (no floating-point rounding drift).

#### Accounting Rules:
- **Average Cost Basis**: Buy collateral (including protocol fee) enters the cost basis. On partial sells, cost is removed proportionally (`cost * sharesIn / shares`).
- **Settlement**: On resolution, winning shares are worth 1.0 USDC each; losing shares are worth 0. Void markets settle combined Yes and No positions with integer division `(yes + no) / 2` matching contract `settle_value`.
- **Incomplete History Flag**: Wallets with trades prior to indexer retention or exceeding known balances are flagged `incomplete: true` and excluded from rankings by default.

#### `GET /leaderboard`
Query params: `window=7d|30d|all` (default `all`), `category`, `metric=pnl|volume|roi` (default `pnl`), `page`, `limit`, `includeIncomplete=true|false`.
Responses are cached in Redis for 30 seconds.

#### `GET /wallets/:address/stats`
Returns totals, category breakdown, open positions with mark-to-market valuations, and last 50 trades from the indexer.

137 changes: 137 additions & 0 deletions docs/trace-format.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
# Pinned Document and Trace Format Specification

OracleDesk pins verifiable artefacts to IPFS:
1. **Question Documents**: committed on-chain through `market_core.agent_create_market(question_hash, meta_uri, ...)`
2. **Reasoning Traces**: committed on-chain through `reasoning_registry.publish_trace(trace_hash, ipfs_cid, ...)`

---

## 1. Versioning Policy

Every pinned document includes explicit metadata:
- `schemaVersion`: an integer version number (e.g. `1` for TraceDocV1, `2` for QuestionDocV2).
- `kind`: document discriminator (e.g. `'oracledesk.trace'`, `'oracledesk.question'`, `'oracledesk.trace.enc'`).

Any semantic change, added required field, or removed field bumps `schemaVersion`.
Legacy unversioned documents are permanently identified as `schemaVersion: 0` in API responses and continue to verify by their exact sha256 byte hash.

---

## 2. Canonical JSON Serialization

To ensure consistent cryptographic hashing across different runtimes and languages, OracleDesk follows strict JSON canonicalization:
1. Object keys are sorted lexicographically (UTF-8 code units) at every level.
2. No unnecessary whitespace is added (`separators=(',', ':')`).
3. Floating point numbers use standard shortest round-trip formatting.
4. Optional fields are explicitly set to `null` rather than omitted, preserving identical key sets across all code paths.
5. Strict checks reject `undefined`, functions, symbols, `Date` objects, and non-finite numbers (`NaN`, `Infinity`).

> [!IMPORTANT]
> Verifiers must check hashes against the **raw fetched bytes** from IPFS, rather than parsing and re-serializing the JSON. Re-serializing in other languages or engines can cause float or unicode differences.

---

## 3. Reasoning Trace Document (v1)

JSON Schema URL: `https://api.oracledesk.io/api/v1/schemas/trace/v1.json`

```json
{
"schemaVersion": 1,
"kind": "oracledesk.trace",
"traceId": "11111111-1111-1111-1111-111111111111",
"agentType": "MARKET_MAKER",
"decisionType": "MARKET_CREATION",
"action": "create_market",
"market": {
"backendId": "22222222-2222-2222-2222-222222222222",
"onChainId": "0"
},
"probabilityEstimate": 0.65,
"marketProbability": 0.5,
"edge": 0.15,
"confidenceInterval": {
"lower": 0.55,
"upper": 0.75
},
"sourcesUsed": [
{
"source": "FRED",
"weight": 0.8,
"signal": "Federal funds futures imply 25bps cut",
"rawValue": 0.75
}
],
"betFraction": null,
"betSizeUsdc": null,
"hedgeConditions": [
"CPI YoY exceeds 3.0%"
],
"agentWallet": null,
"createdAt": "2026-10-17T00:00:00.000Z"
}
```

---

## 4. Confidential Trace Envelope (`oracledesk.trace.enc` v1)

When `TRACE_CONFIDENTIAL=true`, full traces are encrypted with XChaCha20-Poly1305 before pinning.

```json
{
"kind": "oracledesk.trace.enc",
"schemaVersion": 1,
"alg": "XChaCha20-Poly1305",
"kid": "kek-2026-10",
"plaintextHash": "89b51fa16ce70...",
"public": {
"traceId": "11111111-1111-1111-1111-111111111111",
"agentType": "MARKET_MAKER",
"decisionType": "MARKET_CREATION",
"market": {
"backendId": "22222222-2222-2222-2222-222222222222",
"onChainId": "0"
},
"probabilityEstimate": 0.65,
"marketProbability": 0.5,
"edge": 0.15,
"previewSources": [
{
"source": "FRED",
"weight": 0.8,
"signal": "Federal funds futures imply 25bps cut",
"rawValue": 0.75
}
],
"createdAt": "2026-10-17T00:00:00.000Z"
},
"nonce": "<base64 24 bytes>",
"ciphertext": "<base64 ciphertext and 16-byte Poly1305 tag>"
}
```

- **AEAD Associated Data**: Canonical JSON bytes of the envelope excluding `nonce` and `ciphertext`.
- **Public Verifiability**:
1. The on-chain `trace_hash` in `reasoning_registry` equals `sha256(pinned_bytes)` (the canonical envelope bytes).
2. Entitled users unwrap the per-trace data key and decrypt the ciphertext to verify `sha256(plaintext) === envelope.plaintextHash`.

---

## 5. Question Document (v2)

JSON Schema URL: `https://api.oracledesk.io/api/v1/schemas/question/v2.json`

```json
{
"schemaVersion": 2,
"kind": "oracledesk.question",
"question": "Will the Federal Reserve cut the target policy rate by 25bps or more at the Dec 2026 FOMC meeting?",
"category": "FED",
"contractCategory": "Macro",
"closeTime": 1798761600,
"resolutionSource": "FRED",
"resolutionSpecHash": "a1b2c3d4...",
"backendMarketId": "22222222-2222-2222-2222-222222222222"
}
```
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
-- Market generation saga runs table and state enum (Issue #152)
-- Forward-only migration. Does not alter existing table structures.

CREATE TYPE "GenerationRunState" AS ENUM (
'STARTED',
'SIGNALS_READY',
'PROPOSED',
'MARKET_PERSISTED',
'QUESTION_PINNED',
'CHAIN_SUBMITTED',
'CHAIN_CONFIRMED',
'TRACE_PERSISTED',
'TRACE_PINNED',
'TRACE_PUBLISHED',
'COMPLETED',
'SKIPPED',
'NEEDS_ATTENTION',
'CANCELLED'
);

CREATE TABLE "market_generation_runs" (
"id" TEXT NOT NULL,
"trigger" TEXT NOT NULL,
"state" "GenerationRunState" NOT NULL DEFAULT 'STARTED',
"attempts" INTEGER NOT NULL DEFAULT 0,
"nextAttemptAt" TIMESTAMP(3),
"lockedBy" TEXT,
"lockedUntil" TIMESTAMP(3),
"inputs" JSONB,
"outputs" JSONB NOT NULL DEFAULT '{}',
"marketId" TEXT,
"lastError" JSONB,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "market_generation_runs_pkey" PRIMARY KEY ("id"),
CONSTRAINT "market_generation_runs_marketId_key" UNIQUE ("marketId"),
CONSTRAINT "market_generation_runs_marketId_fkey" FOREIGN KEY ("marketId") REFERENCES "markets"("id") ON DELETE SET NULL ON UPDATE CASCADE
);

CREATE INDEX "market_generation_runs_state_nextAttemptAt_idx" ON "market_generation_runs"("state", "nextAttemptAt");
18 changes: 18 additions & 0 deletions prisma/migrations/20261017100001_confidential_traces/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
-- Confidential trace storage and key envelope tables (Issue #154)
-- Forward-only migration. Existing traces remain PLAINTEXT_LEGACY.

CREATE TYPE "TraceContentMode" AS ENUM ('PLAINTEXT_LEGACY', 'ENCRYPTED');

ALTER TABLE "reasoning_traces" ADD COLUMN "contentMode" "TraceContentMode" NOT NULL DEFAULT 'PLAINTEXT_LEGACY';
ALTER TABLE "reasoning_traces" ADD COLUMN "plaintextHash" VARCHAR(64);

CREATE TABLE "trace_keys" (
"traceId" TEXT NOT NULL,
"kid" TEXT NOT NULL,
"wrappedKey" BYTEA NOT NULL,
"plaintextHash" VARCHAR(64) NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "trace_keys_pkey" PRIMARY KEY ("traceId"),
CONSTRAINT "trace_keys_traceId_fkey" FOREIGN KEY ("traceId") REFERENCES "reasoning_traces"("id") ON DELETE CASCADE ON UPDATE CASCADE
);
55 changes: 55 additions & 0 deletions prisma/migrations/20261017100002_wallet_pnl/migration.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
-- Per-wallet trading PnL ledger and leaderboard projections (Issue #155)
-- Forward-only migration.

CREATE TABLE "wallet_positions" (
"wallet" TEXT NOT NULL,
"onChainMarketId" BIGINT NOT NULL,
"outcome" TEXT NOT NULL,
"shares" DECIMAL(39, 0) NOT NULL DEFAULT 0,
"costBasis" DECIMAL(39, 0) NOT NULL DEFAULT 0,
"realizedPnl" DECIMAL(39, 0) NOT NULL DEFAULT 0,
"boughtCollateral" DECIMAL(39, 0) NOT NULL DEFAULT 0,
"soldCollateral" DECIMAL(39, 0) NOT NULL DEFAULT 0,
"feesPaid" DECIMAL(39, 0) NOT NULL DEFAULT 0,
"redeemed" DECIMAL(39, 0) NOT NULL DEFAULT 0,
"tradeCount" INTEGER NOT NULL DEFAULT 0,
"lastEventId" TEXT NOT NULL DEFAULT '',
"settled" BOOLEAN NOT NULL DEFAULT false,
"incomplete" BOOLEAN NOT NULL DEFAULT false,
"updatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "wallet_positions_pkey" PRIMARY KEY ("wallet", "onChainMarketId", "outcome")
);

CREATE INDEX "wallet_positions_onChainMarketId_idx" ON "wallet_positions"("onChainMarketId");

CREATE TABLE "wallet_stats_daily" (
"wallet" TEXT NOT NULL,
"day" DATE NOT NULL,
"category" TEXT NOT NULL,
"volume" DECIMAL(39, 0) NOT NULL DEFAULT 0,
"realizedPnl" DECIMAL(39, 0) NOT NULL DEFAULT 0,
"fees" DECIMAL(39, 0) NOT NULL DEFAULT 0,
"trades" INTEGER NOT NULL DEFAULT 0,
"wins" INTEGER NOT NULL DEFAULT 0,
"losses" INTEGER NOT NULL DEFAULT 0,

CONSTRAINT "wallet_stats_daily_pkey" PRIMARY KEY ("wallet", "day", "category")
);

CREATE INDEX "wallet_stats_daily_day_idx" ON "wallet_stats_daily"("day");

CREATE TABLE "wallet_events_applied" (
"eventId" TEXT NOT NULL,
"appliedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "wallet_events_applied_pkey" PRIMARY KEY ("eventId")
);

CREATE TABLE "wallet_pnl_dirty_markets" (
"onChainMarketId" BIGINT NOT NULL,
"dirtiedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"reason" TEXT,

CONSTRAINT "wallet_pnl_dirty_markets_pkey" PRIMARY KEY ("onChainMarketId")
);
1 change: 1 addition & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -244,6 +244,7 @@ model Market {
updatedAt DateTime @updatedAt

generationJob MarketGenerationJob? @relation("MarketGenerationJobMarket")
generationRun MarketGenerationRun?

trades Trade[]
positions Position[]
Expand Down
Loading