# REST API Reference

Base URLs:

- Hosted Base Sepolia API: `https://cortex-api.projectaegis.ai`
- Local development API: `http://localhost:3001` (configurable via `API_PORT`)

The hosted API currently indexes the live Base Sepolia deployment. Local development uses the same routes against your local Postgres/indexer stack.

## Authentication

Read endpoints (all `GET` routes) are public — they expose indexed protocol data and require no credentials.

Mutating endpoints (`POST`/`PUT` that persist hosted documents) are gated by an API key passed in the `X-Cortex-Api-Key` header. Stateless compute endpoints are exempt because they persist nothing: `POST /x402/normalize`, `POST /x402/verify`, and `POST /preflight`.

Enforcement is controlled by the `API_KEY_REQUIRED` environment variable:

- `API_KEY_REQUIRED=true` (production): a valid key is required on every mutating route. Requests without one get `401`.
- `API_KEY_REQUIRED=false` (local/dev only): a missing key is allowed, but any key that *is* supplied is still validated and counted against its daily quota.

Each key has a daily quota. Responses:

- `401 { "error": "API key required" }` — required but absent.
- `401 { "error": "Invalid API key" }` — unknown key.
- `401 { "error": "API key revoked" }` — revoked key.
- `429 { "error": "Daily quota exceeded" }` — quota reached for the day.

Only the SHA-256 hash of a key is stored. External testers request keys through the signature-gated self-service endpoint below. Operators can still mint a key directly against the database when needed:

```bash
cd api && npm run mint-key -- --label "acme-prod" --owner 0xYourAddress --quota 5000
```

### Request an API Key

```
POST /keys/request
```

Mints a testnet API key for a wallet address. This route is exempt from `X-Cortex-Api-Key` because it is the key bootstrap path, but it requires a wallet signature over a fresh message and is still write-rate-limited by IP.

Message to sign:

```text
Cortex API key request
address: 0xyouraddresslowercase
issued_at: <unix-seconds>
```

Request:

```json
{
  "address": "0xYourAddress",
  "issued_at": 1781104042,
  "signature": "0x...",
  "label": "demo laptop"
}
```

Response `201`:

```json
{
  "api_key": "ck_...",
  "owner_address": "0xyouraddress",
  "tier": "testnet",
  "daily_quota": 2000,
  "created_at": "2026-06-10T15:07:22.702Z",
  "note": "Store this key now - it is shown once and cannot be recovered. Send it as the X-Cortex-Api-Key header on write requests."
}
```

The `issued_at` value must be a Unix timestamp in seconds within 10 minutes of server time. Each wallet can hold up to five active self-service keys. Store the returned key locally and send it on hosted-document writes:

```bash
curl -X POST "$API_URL/catalogs" \
  -H "content-type: application/json" \
  -H "X-Cortex-Api-Key: $CORTEX_API_KEY" \
  -d '{"catalog_json":"{\"schema\":\"cortex.serviceCatalog.v1\",\"merchant\":{},\"services\":[]}"}'
```

## Rate Limits

Fixed-window (60s) per-process limits, configurable via env:

| Scope | Env | Default | Key |
|-------|-----|---------|-----|
| Reads (`GET`) | `RATE_LIMIT_READ_PER_MIN` | 100/min | per IP |
| Writes (mutating) | `RATE_LIMIT_WRITE_PER_MIN` | 20/min | per API key (IP fallback) |

Over-limit requests get `429 { "error": "Rate limit exceeded" }` with `RateLimit-*` headers. Set `RATE_LIMIT_ENABLED=false` to disable (auto-disabled under `NODE_ENV=test`). The limiter store is in-memory per process, so with N API replicas the effective ceiling is N × the configured limit; a shared (Redis) store is the follow-up for cluster-wide accuracy.

Other request controls: CORS is pinned to `CORS_ORIGINS` (comma-separated; empty allows any origin), `helmet` sets standard security headers, and request bodies are capped at `MAX_BODY_BYTES` (default 256KB — above the 128KB per-document caps plus JSON overhead) returning `413` when exceeded.

## Endpoints

### Health Check

```
GET /health
```

Response:
```json
{ "status": "ok" }
```

---

### Catalog Documents

#### Publish Catalog JSON

```
POST /catalogs
```

Canonicalizes catalog JSON, stores the canonical bytes by `keccak256` hash, and returns the canonical URI/hash to use as the service metadata URI/hash when registering a service.

Request:
```json
{
  "catalog_json": "{\n  \"merchant\": {...},\n  \"services\": [...]\n}",
  "expected_hash": "0x...",
  "merchant_id": "1",
  "service_id": "enrich-company-v1"
}
```

`expected_hash`, `merchant_id`, and `service_id` are optional. If `expected_hash` is provided, the API rejects mismatches. `expected_hash` must be computed over canonical JSON bytes, not pretty-printed input.

Response `201`:
```json
{
  "catalog_hash": "0x...",
  "merchant_id": "1",
  "service_id": "enrich-company-v1",
  "size_bytes": 2048,
  "uri": "https://cortex-api.projectaegis.ai/catalogs/0x...",
  "canonical_json": "{\"merchant\":{},\"services\":[]}"
}
```

#### Fetch Catalog JSON

```
GET /catalogs/:hash
```

Returns the canonical JSON text as `application/json`.

#### Fetch Catalog Metadata

```
GET /catalogs/:hash/metadata
```

Returns hash, merchant id, service id, byte size, timestamps, and URI.

---

### Quote Documents

#### Publish Quote Request

```
POST /quote-requests
```

Canonicalizes the agent quote request JSON, stores the canonical bytes by `keccak256` hash, and returns a stable URI for merchant retrieval.

Request:
```json
{
  "quote_request_json": "{\n  \"request_id\": \"req-001\",\n  \"merchant_id\": \"1\"\n}",
  "expected_hash": "0x...",
  "request_id": "req-001",
  "merchant_id": "1",
  "service_numeric_id": "1",
  "service_id": "enrich-company-v1",
  "agent": "0x..."
}
```

Response `201`:
```json
{
  "request_hash": "0x...",
  "request_id": "req-001",
  "merchant_id": "1",
  "service_numeric_id": "1",
  "service_id": "enrich-company-v1",
  "agent": "0x...",
  "size_bytes": 1024,
  "uri": "https://cortex-api.projectaegis.ai/quote-requests/0x...",
  "canonical_json": "{\"merchant_id\":\"1\",\"request_id\":\"req-001\"}"
}
```

Fetch canonical JSON at `GET /quote-requests/:hash`. Metadata is available at `GET /quote-requests/:hash/metadata`.

### Settlement Plans

#### Publish Settlement Plan

```
POST /settlement-plans
```

Canonicalizes a `cortex.settlement-plan.v1` document, validates the quote fields and settlement lines, verifies the line total against the quote gross amount, stores the canonical bytes, and returns the hash to bind into quote `termsHash`.

Request:

```json
{
  "settlement_plan_json": "{\n  \"schema\": \"cortex.settlement-plan.v1\",\n  \"quote\": {...},\n  \"lines\": [...]\n}",
  "expected_hash": "0x..."
}
```

Response `201`:

```json
{
  "plan_hash": "0x...",
  "merchant_id": "1",
  "service_numeric_id": "1",
  "service_id": "enrich-company-v1",
  "agent": "0x...",
  "token": "0x...",
  "payment_rail": "transfer",
  "gross_amount": "1000000",
  "line_total": "1000000",
  "matches_quote_amount": true,
  "uri": "https://cortex-api.projectaegis.ai/settlement-plans/0x...",
  "canonical_json": "{\"lines\":[...],\"quote\":{...},\"schema\":\"cortex.settlement-plan.v1\"}"
}
```

Fetch canonical JSON at `GET /settlement-plans/:hash`. Metadata is available at `GET /settlement-plans/:hash/metadata`.

## License Documents

### `POST /licenses`

Canonicalizes a `cortex.license.v1` JSON document, stores the canonical bytes by `keccak256` hash, and returns the hash/URI that can be bound into quote terms, receipts, or attestations.

```json
{
  "license_json": "{\n  \"schema\": \"cortex.license.v1\",\n  \"issuer\": \"0x...\",\n  \"holder\": \"0x...\",\n  \"rights\": [\"read\", \"analyze\"]\n}",
  "expected_hash": "0x..."
}
```

### `GET /licenses`

Lists licenses with optional filters: `holder`, `issuer`, `merchant_id`, `service_numeric_id`, `asset_id`, `active`, `limit`, and `offset`.

### `GET /licenses/:hash`

Returns the canonical license JSON. Metadata is available at `GET /licenses/:hash/metadata`.

### `POST /licenses/check`

Returns a deterministic allow/deny decision for a proposed agent use.

```json
{
  "license_hash": "0x...",
  "agent": "0x...",
  "action": "commercial_output",
  "quote_hash": "0x...",
  "receipt_id": "1",
  "context": {
    "commercial": true,
    "model_training": false,
    "redistribution": false,
    "derivative": true
  }
}
```

Checks include active status, holder match, requested action, expiration, quote/receipt binding, and common usage constraints.

#### Publish Quote Response

```
POST /quote-responses
```

Canonicalizes the merchant quote response JSON, stores the canonical bytes by `keccak256` hash, and can link it to a hosted quote request hash.

Request:
```json
{
  "quote_response_json": "{\n  \"request_id\": \"req-001\",\n  \"quote\": {...}\n}",
  "expected_hash": "0x...",
  "request_hash": "0x...",
  "request_id": "req-001",
  "merchant_id": "1",
  "service_numeric_id": "1",
  "agent": "0x..."
}
```

Response `201`:
```json
{
  "response_hash": "0x...",
  "request_hash": "0x...",
  "request_id": "req-001",
  "merchant_id": "1",
  "service_numeric_id": "1",
  "agent": "0x...",
  "size_bytes": 1024,
  "uri": "https://cortex-api.projectaegis.ai/quote-responses/0x...",
  "canonical_json": "{\"quote\":{},\"request_id\":\"req-001\"}"
}
```

Fetch canonical JSON at `GET /quote-responses/:hash`. Metadata is available at `GET /quote-responses/:hash/metadata`.

#### Normalize x402 Payment Requirement

```
POST /x402/normalize
```

Normalizes a facilitator payment requirement into the Cortex canonical x402 shape, hashes the canonical JSON bytes, and optionally compares the result with the quote-bound `x402PayloadHash`.

Request:
```json
{
  "payment_requirement_json": {
    "accepts": [
      {
        "scheme": "exact",
        "network": "base-sepolia",
        "payTo": "0x...",
        "asset": "0x...",
        "maxAmountRequired": "1000000",
        "resource": "https://merchant.example/api/report",
        "method": "POST",
        "facilitator": { "url": "https://facilitator.example" },
        "nonce": "quote-001"
      }
    ]
  },
  "expected_hash": "0x...",
  "quote": {
    "x402_payload_hash": "0x..."
  }
}
```

Response `200`:
```json
{
  "normalized": {
    "schema": "cortex.x402-payment-requirement.v1",
    "scheme": "exact",
    "network": "base-sepolia",
    "pay_to": "0x...",
    "asset": "0x...",
    "amount": "1000000"
  },
  "canonical_json": "{\"amount\":\"1000000\",\"asset\":\"0x...\"}",
  "x402_payload_hash": "0x...",
  "matches_expected_hash": true,
  "matches_quote_hash": true,
  "warnings": []
}
```

If `expected_hash` is present and does not match the normalized payload, the API returns `409` with the computed hash and canonical JSON. Agents should sign only after this hash matches the quote's `x402PayloadHash` and local policy checks pass.

---

#### Verify x402 Payment Authorization

```
POST /x402/verify
```

Recovers the EIP-712 signer of a signed payment authorization and checks the authorized terms against the payment requirement. Stateless (no chain calls, no persistence). Supports `eip3009` (`TransferWithAuthorization`/`ReceiveWithAuthorization`, signed over the token's domain) and `permit2` (`PermitTransferFrom`, signed over the canonical Permit2 domain). See `docs/x402-policy.md` for the full check list.

Request (EIP-3009):
```json
{
  "scheme": "eip3009",
  "expected": { "chain_id": 84532, "asset": "0x...", "pay_to": "0x...", "amount": "1000000" },
  "authorization": {
    "from": "0x...",
    "to": "0x...",
    "value": "1000000",
    "validAfter": "0",
    "validBefore": "10000000000",
    "nonce": "0x...",
    "signature": "0x...",
    "domain": { "name": "USD Coin", "version": "2", "chainId": 84532, "verifyingContract": "0x..." }
  }
}
```

Instead of `expected`, you may pass a full `payment_requirement_json` (normalized internally) plus `quote.x402_payload_hash` to additionally assert the requirement is the one bound to the quote. For Permit2, the `authorization` carries `owner`, `permitted: { token, amount }`, `spender`, `nonce`, `deadline`, `chainId`, and `signature`. Pass `now` (unix seconds) to pin the validity-window/deadline check; it defaults to the current time.

Response:
```json
{
  "scheme": "eip3009",
  "valid": true,
  "signer": "0x...",
  "checks": [{ "name": "signer_is_payer", "passed": true, "detail": "..." }],
  "expected": { "chain_id": 84532, "asset": "0x...", "pay_to": "0x...", "amount": "1000000" },
  "quote_hash_match": true
}
```

`valid` is true only when every check passes. A failing authorization still returns `200` with `valid: false`; inspect `checks[]` to see which constraint failed. Malformed input (unknown scheme, missing required fields) returns `400`.

---

#### Publish Encrypted Fulfillment Payload

```
POST /fulfillment-payloads
```

Canonicalizes an encrypted fulfillment payload envelope and stores it by `keccak256` hash. The payload should contain ciphertext and encryption metadata, not plaintext shipping data.

Request:
```json
{
  "fulfillment_payload_json": "{\n  \"schema\": \"cortex.encrypted-fulfillment.v1\",\n  \"ciphertext\": \"base64:...\"\n}",
  "expected_hash": "0x...",
  "merchant_id": "1",
  "agent": "0x...",
  "quote_hash": "0x...",
  "encryption": "x25519-xsalsa20-poly1305",
  "merchant_key_id": "did:key:z6MkMerchantFulfillmentKey"
}
```

Response `201`:
```json
{
  "payload_hash": "0x...",
  "merchant_id": "1",
  "agent": "0x...",
  "quote_hash": "0x...",
  "encryption": "x25519-xsalsa20-poly1305",
  "merchant_key_id": "did:key:z6MkMerchantFulfillmentKey",
  "size_bytes": 1024,
  "uri": "https://cortex-api.projectaegis.ai/fulfillment-payloads/0x...",
  "canonical_json": "{\"ciphertext\":\"base64:...\",\"schema\":\"cortex.encrypted-fulfillment.v1\"}"
}
```

Fetch canonical ciphertext envelope JSON at `GET /fulfillment-payloads/:hash`. Metadata is available at `GET /fulfillment-payloads/:hash/metadata`.

#### Publish Fulfillment Evidence

```
POST /fulfillment-evidence
```

Canonicalizes a fulfillment evidence document and stores it by `keccak256` hash. Use this hash as the `fulfillmentHash` when calling `CommerceRegistry.recordFulfillment(receiptId, fulfillmentHash)`.

Request:
```json
{
  "fulfillment_evidence_json": "{\n  \"schema\": \"cortex.fulfillment-evidence.v1\",\n  \"evidence_type\": \"shipment\"\n}",
  "expected_hash": "0x...",
  "receipt_id": "1",
  "quote_hash": "0x...",
  "payload_hash": "0x...",
  "evidence_type": "shipment"
}
```

Response `201`:
```json
{
  "evidence_hash": "0x...",
  "receipt_id": "1",
  "quote_hash": "0x...",
  "payload_hash": "0x...",
  "evidence_type": "shipment",
  "size_bytes": 1024,
  "uri": "https://cortex-api.projectaegis.ai/fulfillment-evidence/0x...",
  "canonical_json": "{\"evidence_type\":\"shipment\",\"schema\":\"cortex.fulfillment-evidence.v1\"}"
}
```

Fetch canonical evidence JSON at `GET /fulfillment-evidence/:hash`. Metadata is available at `GET /fulfillment-evidence/:hash/metadata`.

---

### Agents

#### Get Agent by ID

```
GET /agents/:agentId
```

Response `200`:
```json
{
  "agent_id": "1",
  "owner": "0x3c44cdddb6a900fa2b585dd299e03d12fa4293bc",
  "metadata_uri": "ipfs://agent-meta",
  "pubkey": "0xaabb",
  "capabilities_hash": "0x...",
  "revoked": false,
  "block_number": "10"
}
```

Errors: `400` (invalid ID), `404` (not found)

#### List Agents by Owner

```
GET /agents?owner=0x...&limit=50&offset=0
```

The `owner` parameter is required.

Response `200`:
```json
{
  "agents": [...],
  "pagination": { "limit": 50, "offset": 0, "count": 1 }
}
```

Errors: `400` (missing or invalid owner)

---

### Intents

#### Get Intent by ID

```
GET /intents/:id
```

Response `200`:
```json
{
  "intent_id": "1",
  "owner": "0x...",
  "intent_type": "SWAP_EXACT_IN_MAX_SLIPPAGE",
  "input_token": "0x...",
  "output_token": "0x...",
  "amount_in_max": "1000000000000000000000",
  "amount_out_min": "900000000000000000000",
  "deadline": "1738965600",
  "slippage_bps": "100",
  "nonce": "42",
  "status": "FILLED",
  "block_number": "15",
  "fill": {
    "solver": "0x...",
    "amount_in": "950000000000000000000",
    "amount_out": "900000000000000000000",
    "tx_hash": "0x...",
    "block_number": "18"
  }
}
```

If the intent is not filled, `fill` is `null`.

Errors: `400` (invalid ID), `404` (not found)

#### List Intents

```
GET /intents?status=open&limit=50&offset=0
```

The `status` filter is optional. Valid values: `open`, `filled`, `cancelled`.

Response `200`:
```json
{
  "intents": [...],
  "pagination": { "limit": 50, "offset": 0, "count": 5 }
}
```

Errors: `400` (invalid status)

---

#### Set Intent Metadata

```
PUT /intents/:id/metadata
```

Stores offchain execution/provenance metadata for an indexed intent.

Request:
```json
{
  "execution_target": "0x...",
  "execution_data": "0x...",
  "required_attestation_subject": "0x...",
  "required_attestation_schema": "0x..."
}
```

All fields are optional. The solver uses this metadata when `API_URL` is configured.

---

#### Reserve Intent Metadata

```
POST /intents/metadata
```

Stores execution metadata before the onchain submit is indexed. The SDK computes `intent_hash`, reserves metadata, submits the signed intent, and the indexer links the reserved metadata to the final `intent_id`.

Request:
```json
{
  "intent_hash": "0x...",
  "owner": "0x...",
  "execution_target": "0x...",
  "execution_data": "0x...",
  "required_attestation_subject": "0x...",
  "required_attestation_schema": "0x..."
}
```

Response `201`: pending metadata row.

---

### Policies

#### Get Account Policies

```
GET /accounts/:address/policies?limit=50&offset=0
```

Response `200`:
```json
{
  "account": "0x...",
  "policies": [
    {
      "policy_type": "SPEND_LIMIT",
      "token": "0x1111111111111111111111111111111111111111",
      "max_per_day": "10000000000000000000000",
      "block_number": "12"
    },
    {
      "policy_type": "TARGET_ALLOWLIST",
      "target": "0xe7f1725e7734ce288f8367e1bb143e90bb3f0512",
      "allowed": true,
      "block_number": "13"
    }
  ],
  "pagination": { "limit": 50, "offset": 0, "count": 2 }
}
```

Errors: `400` (invalid address)

---

### Policy Preflight

```
POST /preflight
```

Checks indexed account policies before an agent signs or submits a transaction.

Request:
```json
{
  "account": "0x...",
  "target": "0x...",
  "value": "0",
  "data": "0x"
}
```

Response:
```json
{
  "allowed": true,
  "action": "ERC20 transfer from 0x...",
  "reasons": [],
  "missing_policies": [],
  "spend_checks": [],
  "required_policy_updates": []
}
```

```
POST /preflight/purchase
```

Checks signed-payment policy for a commerce purchase before an agent signs a payment authorization. Pass either `merchant_id` or `merchant` payout address.

Request:
```json
{
  "account": "0x...",
  "merchant_id": "1",
  "token": "0x0000000000000000000000000000000000000000",
  "facilitator": "0x0000000000000000000000000000000000000000",
  "amount": "5000000000000000"
}
```

Response:
```json
{
  "allowed": true,
  "merchant": "0x...",
  "policy_scope": "any_merchant",
  "matched_policy": {
    "target": "0x0000000000000000000000000000000000000000",
    "max_per_payment": "10000000000000000",
    "max_per_day": "40000000000000000",
    "allowed": true
  },
  "reasons": [],
  "missing_policies": [],
  "required_policy_updates": []
}
```

Policy scope is `merchant`, `any_merchant`, or `null`. Exact merchant policies override any-merchant policies.

---

### Solver Bids

Bid commitment and selection are onchain through `IntentBook.submitBid` and `IntentBook.selectBid`. The API mirrors indexed bid events so agents can inspect the market without scanning logs themselves.

#### Create Bid

```
POST /intents/:id/bids
```

Legacy/development helper for inserting an offchain bid row. Production solver bids should use the onchain `IntentBook.submitBid` function.

Request:
```json
{
  "solver": "0x...",
  "amount_in": "1000",
  "amount_out": "950",
  "fee": "5",
  "valid_until": "1779633720",
  "execution_plan": { "route": "demo" }
}
```

#### List Bids

```
GET /intents/:id/bids?status=open
```

Bids are ordered best-first by output amount, input amount, fee, then creation order.

#### Select Bid

```
POST /bids/:bidId/select
```

Legacy/development helper for selecting an offchain bid row. Production selection should use the onchain `IntentBook.selectBid(intentId, chainBidId)` function. When a bid is selected onchain, `IntentBook.fillIntent` enforces the selected solver and exact bid amounts.

---

### Attestation Schemas

```
GET /attestations/schemas
GET /attestations/schemas/:schemaHash
POST /attestations/schemas
```

Built-in schemas include `solver_reputation`, `tool_capability`, `model_provider`, and `safety_review`.

---

### Solvers

#### Get Solver

```
GET /solvers/:id
```

Returns solver operator, metadata, capabilities hash, bond, active status, fill counters, average latency blocks, and average output surplus.

#### List Solvers

```
GET /solvers?active=true&operator=0x...&limit=50&offset=0
```

Both filters are optional.

---

### Attestors

#### Get Attestor

```
GET /attestors/:id
```

Returns attestor operator, metadata, schema hash, active status, and indexed attestation counters.

#### List Attestors

```
GET /attestors?active=true&operator=0x...&limit=50&offset=0
```

Both filters are optional.

---

### Commerce

#### List Merchants

```
GET /merchants?owner=0x...&active=true&limit=50&offset=0
```

Filters are optional. Returns registered merchant owners, payout addresses, metadata URIs/hashes, active status, and block metadata.

#### Get Merchant

```
GET /merchants/:id
```

Errors: `400` (invalid ID), `404` (not found)

#### List Services

```
GET /services?merchant_id=1&capability_hash=0x...&active=true&limit=50&offset=0
```

Filters are optional. Services include merchant ID, service ID, metadata URI/hash, capability hash, and active status.

#### Get Service

```
GET /services/:id
```

Errors: `400` (invalid ID), `404` (not found)

#### List Facilitators

```
GET /facilitators?active=true&limit=50&offset=0
```

Returns facilitator address, metadata URI/hash, active status, and block metadata.

#### Get Quote

```
GET /quotes/:quoteHash
```

Response `200`:
```json
{
  "quote_hash": "0x...",
  "merchant_id": "1",
  "service_numeric_id": "1",
  "agent": "0x...",
  "token": "0x...",
  "facilitator": "0x...",
  "amount": "1000000000000000000",
  "payment_rail": 3,
  "protocol_fee_bps": 0,
  "protocol_fee_amount": "0",
  "expires_at": "1779652362",
  "payment_nonce": "1",
  "resource_hash": "0x...",
  "terms_hash": "0x...",
  "x402_payload_hash": "0x...",
  "settled": true
}
```

Payment rails are `0=transfer`, `1=swap`, `2=facilitator`, and `3=x402`. The `x402_payload_hash` is used when the payment rail is x402. For basic transfers or swaps, quote terms can bind the payment data through the resource/terms hashes and normal policy checks.

#### List Receipts

```
GET /receipts?agent=0x...&merchant_id=1&limit=50&offset=0
```

Returns settled commerce receipts with amount, token, payment rail, facilitator, result hash, resource hash, fulfillment hash, and zero-fee protocol instrumentation.

#### Get Merchant Reputation

```
GET /merchants/:id/reputation
```

Returns merchant details plus receipt count, settled volume, fulfilled receipt count, dispute counts, and trust signal counts grouped by kind.

#### List Trust Signals

```
GET /trust-signals?subject_type=0&subject_id=1&kind=0&reporter=0x...&limit=50&offset=0
```

Trust signal subject types are `0=merchant`, `1=service`, `2=facilitator`, and `3=agent`. Kinds are `0=verification`, `1=risk`, `2=compliance`, and `3=fulfillment`.

#### List Disputes

```
GET /disputes?receipt_id=1&limit=50&offset=0
```

Returns receipt-linked dispute status, opener, reason hash, resolution hash, and block metadata.

---

### Commerce Analytics

```
GET /analytics/commerce
```

Returns dashboard-ready protocol metrics.

Response `200`:
```json
{
  "summary": {
    "merchants": "1",
    "active_merchants": "1",
    "services": "1",
    "active_services": "1",
    "facilitators": "1",
    "active_facilitators": "1",
    "quotes": "1",
    "settled_quotes": "1",
    "quoted_volume": "1000000000000000000",
    "quoted_protocol_fees": "0",
    "receipts": "1",
    "settled_volume": "1000000000000000000",
    "settled_protocol_fees": "0",
    "disputes": "1",
    "open_disputes": "0",
    "resolved_disputes": "1",
    "rejected_disputes": "0"
  },
  "volume_by_token": [],
  "top_merchants": [],
  "top_services": [],
  "facilitator_volume": [],
  "volume_by_payment_rail": [],
  "trust_signals_by_kind": []
}
```

Protocol fee fields are currently instrumented but set to zero by `CommerceRegistry.PROTOCOL_FEE_BPS`.

---

### Transaction Explain

#### Explain Transaction

```
GET /tx/:hash/explain
```

Returns a human-readable and machine-readable summary of a transaction's events.

Response `200`:
```json
{
  "tx_hash": "0x...",
  "block_number": "15",
  "summary": "Transaction contained 1 event(s)",
  "events": [
    {
      "eventName": "IntentSubmitted",
      "args": { "intentId": "1", "owner": "0x...", "nonce": "42" },
      "description": "Intent #1 submitted by 0x..."
    }
  ]
}
```

Errors: `400` (invalid hash), `404` (not found)

---

## Common Parameters

| Parameter | Default | Max | Description |
|-----------|---------|-----|-------------|
| `limit` | 50 | 100 | Number of results per page |
| `offset` | 0 | — | Number of results to skip |

## Notes

- All addresses are normalized to lowercase.
- NUMERIC/BIGINT values are returned as strings for BigInt safety.
- All error responses follow the format `{ "error": "message" }`.
