# ChainChoice SDK

A single-file TypeScript client for the v1 partner API: `POST /api/v1/recommend`,
`GET /api/v1/receipts/{id}`, the GDPR Article 22 explanation, the deletion
endpoint, and the public signed documents and receipt verifier. No build step
and no dependencies; it runs wherever `fetch` is global (Node 18+, Bun, Deno,
browsers). Beside it, `verify.mjs` checks a signed receipt offline.

Its types are the schemas in [/docs/openapi.json](https://chainchoice.io/docs/openapi.json).
A test in the ChainChoice repository holds each type to its schema and each
schema to the handler that answers, so a field the client names is a field the
API returns.

License: Apache-2.0.

## Install

```bash
mkdir -p src/vendor && curl -o src/vendor/chainchoice.ts https://chainchoice.io/docs/sdk/chainchoice.ts
```

## Use

```ts
import { ChainChoiceClient } from './vendor/chainchoice'

const client = new ChainChoiceClient({ apiKey: process.env.CHAINCHOICE_API_KEY! })

const rec = await client.recommend({
  query: 'which exchange should I use to buy BTC?',
  country: 'DE',
})

// The pick is the highest row of chainchoice.io's published order for the
// category the question maps to that Germany keeps; null when ChainChoice
// publishes no ranking for the question.
console.log(rec.pick?.providerName, rec.pick?.publishedRank, rec.pick?.publishedScore)
console.log(rec.order?.category, rec.order?.excludedCount) // the order that answered, and what the country removed
console.log(rec.receipt?.url)                              // https://chainchoice.io/r/<uuid>

// What the receipt recorded, later.
if (rec.receipt) {
  const receipt = await client.getReceipt(rec.receipt.id)
  console.log(receipt.integrity.contentHash) // SHA-256 over the receipt's content
}
```

Only `query` and `country` change the answer. `jurisdiction` is read as a country
when neither the body nor the question names one; `amountUsd`, `region`, `asset`
and `riskTolerance` are accepted and not read.

## Verify a signed receipt

The Impressum, the affiliate disclosure and ChainChoice's decision receipts are
Ed25519-signed. `client.verifyReceipt(receipt)` sends one to
`POST /api/v1/verify`, which needs no key. Send the receipt as
`GET /api/terminal?route=receipt&id=<receipt id>` serves it, or as an issuer
envelope. Without a key set, the endpoint checks it against the published key
for its key id: ChainChoice's own (`/api/terminal?route=receipt-keys`), then a
registered issuer's.

```ts
const imprint = await client.getImprint()
const res = await fetch(`https://chainchoice.io/api/terminal?route=receipt&id=${imprint.receiptId}`)
const verdict = await client.verifyReceipt(await res.json())
console.log(verdict.verified, verdict.reason)
```

To check without trusting the endpoint, run the one-file verifier,
[verify.mjs](https://chainchoice.io/docs/sdk/verify.mjs). It needs Node 18 or
later and nothing else, and its header states the recipe: the content hash is
the SHA-256 of the receipt's fields as canonical JSON, the receipt id derives
from that hash, and the signature is Ed25519 over `<receipt id>:<contentHash>`.

```bash
curl -O https://chainchoice.io/docs/sdk/verify.mjs
node verify.mjs --document https://chainchoice.io/api/v1/imprint
```

Recommendation receipts (`/api/v1/receipts/{id}`) are hashed, not signed; they
carry `integrity.contentHash` and are not checked here.

## Errors

A non-2xx response throws `ChainChoiceError` with the HTTP `status`, the body's
`error` as its message, and, where the API sends them, a `code`, a
`retryAfterSeconds` and the `issues` a request failed on.

## Generating a client instead

The spec feeds straight into a generator:

- `npx openapi-typescript https://chainchoice.io/docs/openapi.json --output ./api.d.ts`
- `openapi-generator generate -i https://chainchoice.io/docs/openapi.json -g typescript-fetch -o ./src/api`

## Versioning

The path segment `v1` is the contract: fields and endpoints may be added within
it, and a breaking change ships as `v2`, running beside `v1` for at least six
months. Once, on 2026-09-14, the values of existing v1 fields changed: the pick
became the published order's pick and provider ids became the ids in
`/data/rankings.json`. The reason and the full list are in the changes section
of the API documentation.
