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
8 changes: 4 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ The constructor throws on any other combination, and on an option the mode would
| `mainnet` | 9200 | `https://chat-api.mainnet.lightchain.ai` (provisional) | `https://rpc.mainnet.lightchain.ai` | `0xfB15F90298e4CcD7106E76fFB5e520315cC42B0b` |
| `testnet` | 8200 | `https://chat-api.testnet.lightchain.ai` | `https://rpc.testnet.lightchain.ai` | `0x531b3A87c5D785441B9cF55b98169F20FD9056a7` |

The mainnet Developer API hostname is not decided yet. `chat-api.mainnet.lightchain.ai` is the production consumer API's name, but today it serves an older consumer API without the key routes and `/v1`: mainnet calls fail there until the Developer API is deployed behind it.
The mainnet Developer API is not live yet: mainnet calls fail until it is, and its URL may change.

## Development

Expand All @@ -130,14 +130,14 @@ npm install
npm test # unit tests against recorded devnet answers (Node >= 22.18)
npm run typecheck
npm run build # dist/
npm run abi # regenerate src/abi.ts from pkg/chain/abis (inside the orchestrator's sdk/, after `make bindings`)
npm run openapi # regenerate test/fixtures/openapi.json from consumer-api's source (inside the orchestrator's sdk/)
npm run abi -- IJobRegistry.json # regenerate src/abi.ts from the JobRegistry's ABI, after the contracts change
npm run openapi # refresh test/fixtures/openapi.json from the API testnet serves
WALLET_PRIVATE_KEY=0x... npm run acceptance # a fresh wallet: the key alone gets its 402, then account pays it; see scripts/acceptance.ts
WALLET_PRIVATE_KEY=0x... LIGHTCHAIN_MODEL=gemma4:e2b npm run acceptance:x402 # keyless x402 mode; see scripts/acceptance-x402.ts
FUNDER_PRIVATE_KEY=0x... LIGHTCHAIN_MODEL=gemma4:e2b npm run acceptance:x402:fresh # the same, from a fresh wallet FUNDER_PRIVATE_KEY funds
WALLET_PRIVATE_KEY=0x... npm run acceptance:pool # streaming, and two interleaved conversations in one pooled session
```

The fixtures in `test/fixtures` were recorded from a local devnet (`make devnet-full`, chain 48221) with Foundry's publicly known test account 7. The x402 tests sign the scheme's published test vectors (`test/fixtures/x402-vectors.json`, a copy of the orchestrator's `scripts/x402-vectors/vectors.json`) byte for byte, with Foundry's test account 3. Never use either key on a real network.
The fixtures in `test/fixtures` were recorded from a local devnet with Foundry's publicly known test account 7. The x402 tests sign the scheme's test vectors (`test/fixtures/x402-vectors.json`) byte for byte, with Foundry's test account 3. Never use either key on a real network.

CI (`.github/workflows/ci.yml`) typechecks, builds and tests every pull request and every push to `main`. `acceptance.yml` runs `acceptance:pool`, then `acceptance:x402:fresh`, against testnet every night at 03:17 UTC, and on demand from the Actions tab. It pays from the wallet in the `TESTNET_WALLET_PRIVATE_KEY` secret, which needs testnet LCAI. `acceptance:x402:fresh` (`scripts/fresh-wallet.ts`) runs `acceptance:x402` with a wallet made for that run, which the secret's wallet funds and which sends what it has left back: `acceptance:x402` needs a wallet that never authorized a delegate. Each run's log is kept as an artifact.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
"typecheck": "tsc --noEmit",
"test": "node --test 'test/*.test.ts'",
"abi": "node scripts/abi.ts",
"openapi": "../consumer-api/node_modules/.bin/tsx scripts/openapi.ts",
"openapi": "node scripts/openapi.ts",
"acceptance": "node scripts/acceptance.ts",
"acceptance:x402": "node scripts/acceptance-x402.ts",
"acceptance:pool": "node scripts/acceptance-pool.ts",
Expand Down
12 changes: 7 additions & 5 deletions scripts/abi.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
// Writes src/abi.ts: the JobRegistry functions the SDK calls, cut from the ABI
// that `make bindings` extracts from the compiled contracts. Run from sdk/ in
// the orchestrator checkout, after `make bindings` if the contracts moved.
// Writes src/abi.ts: the JobRegistry functions the SDK calls, cut from the
// JobRegistry's ABI (a JSON array) when the contracts change.
//
// npm run abi -- path/to/IJobRegistry.json
import { readFileSync, writeFileSync } from 'node:fs';

const SOURCE = '../pkg/chain/abis/IJobRegistry.json';
const SOURCE = process.argv[2];
if (!SOURCE) throw new Error('usage: npm run abi -- path/to/IJobRegistry.json');
const FUNCTIONS = ['deposit', 'depositAndAuthorize', 'prepaidBalanceOf', 'isDelegateAuthorized', 'delegateAllowance'];

const abi: { type: string; name?: string }[] = JSON.parse(readFileSync(SOURCE, 'utf8'));
Expand All @@ -15,6 +17,6 @@ const picked = FUNCTIONS.map((name) => {

writeFileSync(
'src/abi.ts',
`// Generated by scripts/abi.ts from ${SOURCE.slice(3)}. Do not edit.\n` +
'// Generated by scripts/abi.ts from the JobRegistry ABI. Do not edit.\n' +
`export const jobRegistryAbi = ${JSON.stringify(picked, null, 2)} as const;\n`,
);
28 changes: 8 additions & 20 deletions scripts/openapi.ts
Original file line number Diff line number Diff line change
@@ -1,26 +1,14 @@
// Writes test/fixtures/openapi.json: the /v1/chat/completions part of the
// consumer API's /docs/json, built from the consumer-api submodule's source
// and read by injection, no server listening. Run from sdk/ with consumer-api's
// dependencies installed: `npm run openapi` (it runs under consumer-api's tsx).
// Developer API's OpenAPI document, as the network's API serves it.
//
// LIGHTCHAIN_NETWORK=testnet npm run openapi
import { writeFileSync } from 'node:fs';
import { createRequire } from 'node:module';
import { networks } from '../src/index.ts';

const API = new URL('../../consumer-api/', import.meta.url);
// The env schema exits without these; nothing the document says depends on them.
process.env.DATABASE_URL ??= 'postgres://unused/unused';
process.env.DISPATCHER_INTERNAL_AUTH_SECRET ??= 'unused';
process.env.DISPATCHER_JWT_PUBLIC_KEY_HEX ??= `02${'00'.repeat(32)}`;
process.env.CHAIN_ID ??= '1';

// Loaded from consumer-api, by computed paths, so this package's tsc stays out of its source.
const load = async (path: string) => (await import(new URL(path, API).href)).default;
const Fastify = createRequire(API)('fastify');
const app = Fastify({ logger: false });
app.decorate('redis', {});
await app.register(await load('src/plugins/swagger.plugin.ts'));
await app.register(await load('src/routes/v1/index.ts'));
const { openapi, info, paths } = (await app.inject({ method: 'GET', url: '/docs/json' })).json();
await app.close();
const { apiUrl } = networks[(process.env.LIGHTCHAIN_NETWORK ?? 'testnet') as keyof typeof networks];
const res = await fetch(`${apiUrl}/docs/json`);
if (!res.ok) throw new Error(`${apiUrl}/docs/json: ${res.status}`);
const { openapi, info, paths } = await res.json();

const path = '/v1/chat/completions';
const fixture = { openapi, info, paths: { [path]: paths[path] } };
Expand Down
2 changes: 1 addition & 1 deletion src/abi.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Generated by scripts/abi.ts from pkg/chain/abis/IJobRegistry.json. Do not edit.
// Generated by scripts/abi.ts from the JobRegistry ABI. Do not edit.
export const jobRegistryAbi = [
{
"type": "function",
Expand Down
4 changes: 2 additions & 2 deletions src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ type X402Accept = {
/** A /v1 402 body: OpenAI's error, with the ways to pay in `accepts`. */
type PaymentRequired = { error?: { message?: string; code?: string; accepts?: (DelegateAccept | X402Accept)[] } };

/** LightChain's x402 scheme (docs/x402/lightchain-scheme.md in the orchestrator): its EIP-712 domain and type. */
/** LightChain's x402 scheme, prepaid-debit: its EIP-712 domain and type. */
const X402_SCHEME = 'prepaid-debit';
const DEBIT_DOMAIN = { name: 'LightChain JobRegistry', version: '1' } as const;
const DEBIT_AUTHORIZATION = {
Expand Down Expand Up @@ -307,7 +307,7 @@ export class Lightchain {
headers.delete('content-length');
return new Response(JSON.stringify(refused), { status: 402, headers });
}
// ponytail: sent again once. A 402 on that retry (the fee rose meanwhile)
// Sent again once. A 402 on that retry (the fee rose meanwhile)
// comes back to the caller; loop, with a bound, if that shows up.
if (!x402) return this.#fetch(again, init);
const paidHeaders = new Headers(headers);
Expand Down
7 changes: 1 addition & 6 deletions src/networks.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,13 @@ export type Network = {
};

export const networks = {
// JobRegistry from scripts/mainnet/chat/manifest.mainnet.json (verified live 2026-09-09).
// ponytail: the mainnet Developer API hostname is not decided; this is the
// production consumer-api name, which serves an older API without the key
// routes and /v1 today. Change it here once one is chosen.
// The mainnet Developer API is not live yet, and its URL may change.
mainnet: {
chainId: 9200,
apiUrl: 'https://chat-api.mainnet.lightchain.ai',
rpcUrl: 'https://rpc.mainnet.lightchain.ai',
jobRegistry: '0xfB15F90298e4CcD7106E76fFB5e520315cC42B0b',
},
// JobRegistry from terraform/environments/testnet/terraform.tfvars; API from
// consumer-api/docs/developer-api/quickstart.md.
testnet: {
chainId: 8200,
apiUrl: 'https://chat-api.testnet.lightchain.ai',
Expand Down
4 changes: 2 additions & 2 deletions test/openapi.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import vectors from './fixtures/x402-vectors.json' with { type: 'json' };
// The consumer API's published document; `npm run openapi` writes it from ../consumer-api.
// The Developer API's OpenAPI document, as testnet serves it; `npm run openapi` refreshes it.
import spec from './fixtures/openapi.json' with { type: 'json' };
import pay from './fixtures/pay-402.json' with { type: 'json' };

Expand All @@ -10,7 +10,7 @@ type Schema = { required?: string[]; properties?: Record<string, Schema>; items?
/**
* The fields `schema` requires that `value` lacks, as paths. A oneOf takes
* the branch that misses least.
* ponytail: required fields only, no types or $ref; the published schemas are
* Required fields only, no types or $ref; the published schemas are
* inline. Use a JSON Schema validator if they grow those.
*/
function missing(schema: Schema, value: unknown, at = '$'): string[] {
Expand Down
2 changes: 1 addition & 1 deletion test/payment.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ function withAccepts(accepts: object[]): Exchange {
const withOffer = (change: (accept: Accept) => object) => withAccepts([change(offer)]);

// The x402 prepaid-debit requirements an API that takes x402 payments lists
// after the delegate entry, shaped as consumer-api's own tests build them.
// after the delegate entry.
const prepaidDebit = {
scheme: 'prepaid-debit',
network: `eip155:${network.chainId}`,
Expand Down
2 changes: 1 addition & 1 deletion test/x402.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import vectors from './fixtures/x402-vectors.json' with { type: 'json' };
import pay from './fixtures/pay-402.json' with { type: 'json' };
import { type Exchange, replayHttp, replayRpc } from './replay.ts';

// The published vectors of the prepaid-debit scheme (docs/x402/lightchain-scheme.md):
// The prepaid-debit scheme's test vectors:
// their payer is Foundry's public test account 3, never a key for a real network.
type Vector = (typeof vectors.vectors)[number];
const payerKey = vectors.accounts.find((a) => a.role === 'payer')!.privateKey as Hex;
Expand Down
Loading