orrery-node 0.0.0-stage → 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Orrery contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,46 @@
1
- # Temporary Holding Version
1
+ # orrery-node
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ Peer-to-peer AI inference on Solana. A router on your computer sends each request straight to a provider's machine and pays it through a Solana payment channel. No server in the middle, and the program takes 0%.
4
+
5
+ ```
6
+ your tool ──► router (127.0.0.1:7171) ──► provider's machine
7
+ │ │
8
+ └── Solana program: channel deposit · redeem ──┘
9
+ ```
10
+
11
+ ## Use the network
12
+
13
+ ```bash
14
+ npm i -g orrery-node
15
+ orrery router --wallet ~/.config/solana/id.json --rpc <your RPC URL>
16
+ ```
17
+
18
+ Point anything that speaks the OpenAI API at `http://127.0.0.1:7171/v1` (any API key). Add one word after the model name to pick the route:
19
+
20
+ | Suffix | Picks |
21
+ |---|---|
22
+ | (none) | a blend of price, first-token time and reputation |
23
+ | `:cheap` | lowest expected cost |
24
+ | `:fast` | fastest first token this router has seen |
25
+ | `:trusted` | most USDC settled on chain, bonded $OREY |
26
+ | `:private` | verified enclaves only, the request sealed |
27
+
28
+ The first request to a provider opens a channel with a small USDC deposit (`--deposit`, default 2). After every answer a session key on your computer signs the new running total; the provider redeems it on Solana. `--daily-cap` (default $5) stops spending for the day. The console at `http://127.0.0.1:7171/` shows every request, the provider it went to and what it cost.
29
+
30
+ ```bash
31
+ orrery models # every model on the network, cheapest price, provider count
32
+ orrery providers # every listed provider and what the chain says about it
33
+ ```
34
+
35
+ ## Run a provider
36
+
37
+ Put a node in front of any OpenAI-compatible server (vLLM, SGLang, Ollama, llama.cpp, or API capacity you hold), set your prices, and list it:
38
+
39
+ ```bash
40
+ orrery provider register --config provider.json --wallet wallet.json
41
+ orrery provider start --config provider.json
42
+ ```
43
+
44
+ Only standard chat fields reach your upstream, so a caller cannot spend your key on models or tools your price list does not cover. Redemptions go straight to your wallet's USDC account.
45
+
46
+ Docs: https://orrerynode.com/docs · Source: https://github.com/orrery-node/orrery (MIT)
package/dist/brand.js ADDED
@@ -0,0 +1,11 @@
1
+ // The project name lives here and nowhere else in the node software. Renaming the project means editing this file
2
+ // (and the web's brand file). On-chain and wire names (crate `orrery`, voucher prefix `orrery/voucher/1`, x-orrery-*
3
+ // headers) are protocol: changing them after the mainnet deploy breaks every deployed node.
4
+ export const NAME = 'Orrery';
5
+ export const CLI = 'orrery';
6
+ export const TICKER = 'OREY';
7
+ export const HOME_DIR = '.orrery';
8
+ export const ROUTER_PORT = 7171;
9
+ export const PROVIDER_PORT = 7272;
10
+ export const PROGRAM_ID = process.env.ORRERY_PROGRAM_ID ?? '7UHevY47LgbJv9hz4iymecR8FURy27jX55KYb1sxytZS';
11
+ export const USDC_MAINNET = 'EPjFWJd5Wm8eUi4wSPdf8qJxQAXnmSeKEr5mVGYLm3CY';
@@ -0,0 +1,146 @@
1
+ // Hand-written client for the Orrery program: PDAs, account layouts and instruction encodings. It mirrors
2
+ // program/programs/orrery/src/lib.rs field for field; test/program.test.ts checks it against the built IDL.
3
+ import { createHash } from 'node:crypto';
4
+ import { PublicKey, SystemProgram, SYSVAR_INSTRUCTIONS_PUBKEY, TransactionInstruction } from '@solana/web3.js';
5
+ import { PROGRAM_ID } from "../brand.js";
6
+ export const TOKEN_PROGRAM = new PublicKey('TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA');
7
+ export const ATA_PROGRAM = new PublicKey('ATokenGPvbdGVxr1b2hvZbsiqW5xWH25efTNsLJA8knL');
8
+ export const UPGRADEABLE_LOADER = new PublicKey('BPFLoaderUpgradeab1e11111111111111111111111');
9
+ export const programId = () => new PublicKey(PROGRAM_ID);
10
+ const disc = (ns, name) => createHash('sha256').update(`${ns}:${name}`).digest().subarray(0, 8);
11
+ export const IX = Object.fromEntries(['initialize', 'set_params', 'set_bond_mint', 'register_provider', 'update_provider', 'attest', 'bond', 'unbond', 'withdraw_bond', 'open_channel', 'top_up', 'redeem', 'request_close', 'close_channel'].map((n) => [n, disc('global', n)]));
12
+ export const ACCOUNT = { Config: disc('account', 'Config'), Provider: disc('account', 'Provider'), Channel: disc('account', 'Channel') };
13
+ export const TEE_KINDS = ['none', 'Intel TDX', 'AMD SEV-SNP', 'Intel TDX + NVIDIA CC'];
14
+ const seed = (s) => Buffer.from(s);
15
+ const u64le = (n) => {
16
+ const b = Buffer.alloc(8);
17
+ b.writeBigUInt64LE(n);
18
+ return b;
19
+ };
20
+ export const pda = {
21
+ config: (pid = programId()) => PublicKey.findProgramAddressSync([seed('config')], pid)[0],
22
+ provider: (authority, pid = programId()) => PublicKey.findProgramAddressSync([seed('provider'), authority.toBuffer()], pid)[0],
23
+ channel: (payer, provider, id, pid = programId()) => PublicKey.findProgramAddressSync([seed('channel'), payer.toBuffer(), provider.toBuffer(), u64le(id)], pid)[0],
24
+ ata: (owner, mint, tokenProgram = TOKEN_PROGRAM) => PublicKey.findProgramAddressSync([owner.toBuffer(), tokenProgram.toBuffer(), mint.toBuffer()], ATA_PROGRAM)[0],
25
+ programData: (pid = programId()) => PublicKey.findProgramAddressSync([pid.toBuffer()], UPGRADEABLE_LOADER)[0],
26
+ };
27
+ class Writer {
28
+ parts = [];
29
+ u8(n) { this.parts.push(Buffer.from([n])); return this; }
30
+ u32(n) { const b = Buffer.alloc(4); b.writeUInt32LE(n); this.parts.push(b); return this; }
31
+ u64(n) { this.parts.push(u64le(n)); return this; }
32
+ i64(n) { const b = Buffer.alloc(8); b.writeBigInt64LE(n); this.parts.push(b); return this; }
33
+ pk(k) { this.parts.push(k.toBuffer()); return this; }
34
+ bytes(b, len) {
35
+ if (b.length !== len)
36
+ throw new Error(`expected ${len} bytes, got ${b.length}`);
37
+ this.parts.push(Buffer.from(b));
38
+ return this;
39
+ }
40
+ str(s) { const b = Buffer.from(s, 'utf8'); this.u32(b.length); this.parts.push(b); return this; }
41
+ opt(v, put) { if (v === undefined)
42
+ this.u8(0);
43
+ else {
44
+ this.u8(1);
45
+ put(v);
46
+ } return this; }
47
+ done() { return Buffer.concat(this.parts); }
48
+ }
49
+ class Reader {
50
+ b;
51
+ at = 8;
52
+ constructor(b) {
53
+ this.b = b;
54
+ }
55
+ u8() { return this.b.readUInt8(this.at++); }
56
+ bool() { return this.u8() === 1; }
57
+ u32() { const v = this.b.readUInt32LE(this.at); this.at += 4; return v; }
58
+ u64() { const v = this.b.readBigUInt64LE(this.at); this.at += 8; return v; }
59
+ i64() { const v = this.b.readBigInt64LE(this.at); this.at += 8; return v; }
60
+ pk() { const v = new PublicKey(this.b.subarray(this.at, this.at + 32)); this.at += 32; return v; }
61
+ bytes(n) { const v = Buffer.from(this.b.subarray(this.at, this.at + n)); this.at += n; return v; }
62
+ str() { const n = this.u32(); const v = this.b.subarray(this.at, this.at + n).toString('utf8'); this.at += n; return v; }
63
+ }
64
+ function expect(data, d, name) {
65
+ if (data.length < 8 || !data.subarray(0, 8).equals(d))
66
+ throw new Error(`not a ${name} account`);
67
+ }
68
+ export function decodeConfig(data) {
69
+ expect(data, ACCOUNT.Config, 'Config');
70
+ const r = new Reader(data);
71
+ return {
72
+ admin: r.pk(), pendingAdmin: r.pk(), settleMint: r.pk(), bondMint: r.pk(),
73
+ closeGraceSecs: r.i64(), unbondSecs: r.i64(), minDeposit: r.u64(), providers: r.u64(), channels: r.u64(), settled: r.u64(),
74
+ };
75
+ }
76
+ export function decodeProvider(data) {
77
+ expect(data, ACCOUNT.Provider, 'Provider');
78
+ const r = new Reader(data);
79
+ return {
80
+ authority: r.pk(), nodeKey: r.pk(), endpoint: r.str(), manifestHash: r.bytes(32), manifestSeq: r.u32(), active: r.bool(),
81
+ teeKind: r.u8(), teeMeasurement: r.bytes(48), teeReportHash: r.bytes(32), teeKey: r.bytes(32), teeExpiresAt: r.i64(),
82
+ bonded: r.u64(), unbonding: r.u64(), unbondAt: r.i64(), settled: r.u64(), redeems: r.u64(), channels: r.u64(),
83
+ registeredAt: r.i64(), lastSettledAt: r.i64(),
84
+ };
85
+ }
86
+ export function decodeChannel(data) {
87
+ expect(data, ACCOUNT.Channel, 'Channel');
88
+ const r = new Reader(data);
89
+ return {
90
+ payer: r.pk(), provider: r.pk(), sessionKey: r.pk(), mint: r.pk(), id: r.u64(),
91
+ deposit: r.u64(), redeemed: r.u64(), usageHash: r.bytes(32), openedAt: r.i64(), closingAt: r.i64(),
92
+ };
93
+ }
94
+ const ix = (data, metas, pid = programId()) => new TransactionInstruction({ programId: pid, keys: metas.map(([pubkey, isSigner, isWritable]) => ({ pubkey, isSigner, isWritable })), data });
95
+ export const ins = {
96
+ initialize(admin, settleMint, p, pid = programId()) {
97
+ const data = new Writer().bytes(IX.initialize, 8).i64(p.closeGraceSecs).i64(p.unbondSecs).u64(p.minDeposit).done();
98
+ return ix(data, [[admin, true, true], [pid, false, false], [pda.programData(pid), false, false], [pda.config(pid), false, true], [settleMint, false, false], [SystemProgram.programId, false, false]], pid);
99
+ },
100
+ registerProvider(authority, nodeKey, endpoint, manifestHash, pid = programId()) {
101
+ const data = new Writer().bytes(IX.register_provider, 8).pk(nodeKey).str(endpoint).bytes(manifestHash, 32).done();
102
+ return ix(data, [[authority, true, true], [pda.config(pid), false, true], [pda.provider(authority, pid), false, true], [SystemProgram.programId, false, false]], pid);
103
+ },
104
+ updateProvider(authority, a, pid = programId()) {
105
+ const w = new Writer().bytes(IX.update_provider, 8);
106
+ w.opt(a.nodeKey, (k) => w.pk(k));
107
+ w.opt(a.endpoint, (s) => w.str(s));
108
+ w.opt(a.manifestHash, (h) => w.bytes(h, 32));
109
+ w.opt(a.active, (v) => w.u8(v ? 1 : 0));
110
+ return ix(w.done(), [[authority, true, false], [pda.provider(authority, pid), false, true]], pid);
111
+ },
112
+ attest(signer, providerAuthority, a, pid = programId()) {
113
+ const data = new Writer().bytes(IX.attest, 8).u8(a.kind).bytes(a.measurement, 48).bytes(a.reportHash, 32).bytes(a.teeKey, 32).i64(a.expiresAt).done();
114
+ return ix(data, [[signer, true, false], [pda.provider(providerAuthority, pid), false, true]], pid);
115
+ },
116
+ openChannel(payer, provider, mint, id, deposit, sessionKey, pid = programId()) {
117
+ const channel = pda.channel(payer, provider, id, pid);
118
+ const data = new Writer().bytes(IX.open_channel, 8).u64(id).u64(deposit).pk(sessionKey).done();
119
+ return ix(data, [
120
+ [payer, true, true], [pda.config(pid), false, true], [provider, false, true], [channel, false, true], [mint, false, false],
121
+ [pda.ata(payer, mint), false, true], [pda.ata(channel, mint), false, true], [TOKEN_PROGRAM, false, false], [ATA_PROGRAM, false, false], [SystemProgram.programId, false, false],
122
+ ], pid);
123
+ },
124
+ topUp(payer, channel, mint, amount, pid = programId()) {
125
+ const data = new Writer().bytes(IX.top_up, 8).u64(amount).done();
126
+ return ix(data, [[payer, true, false], [channel, false, true], [mint, false, false], [pda.ata(payer, mint), false, true], [pda.ata(channel, mint), false, true], [TOKEN_PROGRAM, false, false]], pid);
127
+ },
128
+ redeem(caller, providerAuthority, channel, mint, cumulative, usageHash, pid = programId()) {
129
+ const provider = pda.provider(providerAuthority, pid);
130
+ const data = new Writer().bytes(IX.redeem, 8).u64(cumulative).bytes(usageHash, 32).done();
131
+ return ix(data, [
132
+ [caller, true, true], [pda.config(pid), false, true], [provider, false, true], [channel, false, true], [mint, false, false],
133
+ [pda.ata(channel, mint), false, true], [providerAuthority, false, false], [pda.ata(providerAuthority, mint), false, true],
134
+ [SYSVAR_INSTRUCTIONS_PUBKEY, false, false], [TOKEN_PROGRAM, false, false], [ATA_PROGRAM, false, false], [SystemProgram.programId, false, false],
135
+ ], pid);
136
+ },
137
+ requestClose(payer, channel, pid = programId()) {
138
+ return ix(new Writer().bytes(IX.request_close, 8).done(), [[payer, true, false], [pda.config(pid), false, false], [channel, false, true]], pid);
139
+ },
140
+ closeChannel(closer, providerAuthority, channel, payer, mint, pid = programId()) {
141
+ return ix(new Writer().bytes(IX.close_channel, 8).done(), [
142
+ [closer, true, true], [pda.provider(providerAuthority, pid), false, false], [channel, false, true], [payer, false, true], [mint, false, false],
143
+ [pda.ata(channel, mint), false, true], [pda.ata(payer, mint), false, true], [TOKEN_PROGRAM, false, false], [ATA_PROGRAM, false, false], [SystemProgram.programId, false, false],
144
+ ], pid);
145
+ },
146
+ };
@@ -0,0 +1,38 @@
1
+ // The voucher is the only thing that moves money out of a channel: 88 bytes the router's session key signs, the
2
+ // same bytes program/programs/orrery/src/voucher.rs rebuilds and checks against the runtime's ed25519 check.
3
+ import { Ed25519Program } from '@solana/web3.js';
4
+ import bs58 from 'bs58';
5
+ import { ed25519Verify } from "../keys.js";
6
+ import { ins } from "./program.js";
7
+ export const VOUCHER_PREFIX = Buffer.from('orrery/voucher/1');
8
+ export function voucherMessage(channel, cumulative, usageHash) {
9
+ if (usageHash.length !== 32)
10
+ throw new Error('usage hash must be 32 bytes');
11
+ const amount = Buffer.alloc(8);
12
+ amount.writeBigUInt64LE(cumulative);
13
+ return Buffer.concat([VOUCHER_PREFIX, channel.toBuffer(), amount, Buffer.from(usageHash)]);
14
+ }
15
+ export function signVoucher(sign, channel, cumulative, usageHash) {
16
+ return { cumulative, usageHash: Buffer.from(usageHash), signature: Buffer.from(sign(voucherMessage(channel, cumulative, usageHash))) };
17
+ }
18
+ export function verifyVoucher(sessionKey, channel, v) {
19
+ return ed25519Verify(sessionKey.toBytes(), voucherMessage(channel, v.cumulative, v.usageHash), v.signature);
20
+ }
21
+ export function encodeVoucher(v) {
22
+ return `${v.cumulative}.${v.usageHash.toString('hex')}.${bs58.encode(v.signature)}`;
23
+ }
24
+ export function decodeVoucher(s) {
25
+ const [amount, hash, sig] = s.split('.');
26
+ if (!amount || !/^\d{1,20}$/.test(amount) || !hash || !/^[0-9a-f]{64}$/.test(hash) || !sig)
27
+ throw new Error('malformed voucher');
28
+ const signature = Buffer.from(bs58.decode(sig));
29
+ if (signature.length !== 64)
30
+ throw new Error('malformed voucher signature');
31
+ return { cumulative: BigInt(amount), usageHash: Buffer.from(hash, 'hex'), signature };
32
+ }
33
+ /** The pair a redeem transaction needs: the runtime's signature check, then the program call that relies on it. */
34
+ export function redeemInstructions(o) {
35
+ const message = voucherMessage(o.channel, o.voucher.cumulative, o.voucher.usageHash);
36
+ const check = Ed25519Program.createInstructionWithPublicKey({ publicKey: o.sessionKey.toBytes(), message, signature: o.voucher.signature });
37
+ return [check, ins.redeem(o.caller, o.providerAuthority, o.channel, o.mint, o.voucher.cumulative, o.voucher.usageHash, o.programId)];
38
+ }
package/dist/cli.js ADDED
@@ -0,0 +1,190 @@
1
+ #!/usr/bin/env node
2
+ import { readFileSync } from 'node:fs';
3
+ import { Connection, PublicKey, Transaction, sendAndConfirmTransaction } from '@solana/web3.js';
4
+ import { CLI, NAME, PROVIDER_PORT, ROUTER_PORT, USDC_MAINNET } from "./brand.js";
5
+ import { decodeChannel, decodeConfig, decodeProvider, ins, pda } from "./chain/program.js";
6
+ import { home, loadKeypair, loadOrCreateKeypair } from "./keys.js";
7
+ import { perMillion, usd } from "./pricing.js";
8
+ import { ProviderNode } from "./provider/node.js";
9
+ import { Channels } from "./router/channels.js";
10
+ import { ROUTES } from "./router/policy.js";
11
+ import { Registry } from "./router/registry.js";
12
+ import { Router } from "./router/server.js";
13
+ const argv = process.argv.slice(2);
14
+ const flag = (name, fallback) => {
15
+ const i = argv.indexOf(`--${name}`);
16
+ return i >= 0 ? argv[i + 1] : fallback;
17
+ };
18
+ const has = (name) => argv.includes(`--${name}`);
19
+ const expand = (p) => p.replace(/^~(?=$|\/)/, process.env.HOME ?? '~');
20
+ const rpc = () => flag('rpc', process.env.ORRERY_RPC_URL ?? 'https://api.mainnet-beta.solana.com');
21
+ const mint = () => new PublicKey(flag('mint', process.env.ORRERY_SETTLE_MINT ?? USDC_MAINNET));
22
+ const dollars = (s) => BigInt(Math.round(Number(s) * 1e6));
23
+ const HELP = `${NAME} — peer-to-peer AI inference, settled on Solana
24
+
25
+ ${CLI} router run the router on http://127.0.0.1:${ROUTER_PORT}/v1
26
+ --wallet <keypair.json> wallet that opens channels (signs once per provider)
27
+ --route <${ROUTES.join('|')}> default route (per request: model suffix :cheap :fast :trusted :private)
28
+ --deposit <usd> deposit per new channel (default 2)
29
+ --daily-cap <usd> stop spending after this much per day (default 5)
30
+ --min-reputation <0-1> auto route ignores providers below this (default 0.2)
31
+ --allow-dev-enclaves accept software attestation (local testing only)
32
+ ${CLI} models every model on the network, cheapest price, provider count
33
+ ${CLI} providers every listed provider and what the chain says about it
34
+ ${CLI} channels your router's channels and what the chain says about each
35
+ ${CLI} close <channel> --wallet <keypair.json> start closing (the provider keeps a grace period)
36
+ ${CLI} close <channel> --wallet <keypair.json> --finish after the grace period: the rest comes back to you
37
+ ${CLI} provider start --config provider.json
38
+ ${CLI} provider register --config provider.json --wallet <keypair.json> list or update on chain
39
+ ${CLI} provider attest --config provider.json post the enclave claim (node key signs)
40
+ ${CLI} provider redeem --config provider.json redeem every channel now
41
+
42
+ --rpc <url> Solana RPC (or ORRERY_RPC_URL) --mint <address> settlement mint (default USDC)
43
+ `;
44
+ async function router() {
45
+ const conn = new Connection(rpc(), 'confirmed');
46
+ const walletPath = flag('wallet', process.env.ORRERY_WALLET);
47
+ const wallet = walletPath ? loadKeypair(expand(walletPath)) : null;
48
+ const session = loadOrCreateKeypair(home('router', 'session.json'));
49
+ const route = flag('route', 'auto');
50
+ if (!ROUTES.includes(route))
51
+ throw new Error(`unknown route ${route}`);
52
+ const registry = new Registry(conn, { allowDev: has('allow-dev-enclaves'), measurements: (flag('enclave-images', '') ?? '').split(',').filter(Boolean) });
53
+ const channels = new Channels(conn, wallet, session, mint(), home('router', 'channels.json'), {
54
+ deposit: dollars(flag('deposit', '2')), topUpBelow: dollars(flag('top-up-below', '0.25')), dailyCap: dollars(flag('daily-cap', '5')),
55
+ });
56
+ const port = Number(flag('port', String(ROUTER_PORT)));
57
+ const r = new Router({ port, route, minReputation: Number(flag('min-reputation', '0.2')), registry, channels, conn, refreshSecs: Number(flag('refresh', '60')) });
58
+ await r.start();
59
+ const usable = registry.usable().length;
60
+ console.log(`${NAME} router on http://127.0.0.1:${port}/v1 · ${usable} providers · route ${route} · ${wallet ? `wallet ${wallet.publicKey.toBase58().slice(0, 6)}…` : 'no wallet (read-only)'}`);
61
+ console.log(`session key ${session.publicKey.toBase58()} signs vouchers; console at http://127.0.0.1:${port}/`);
62
+ }
63
+ async function listModels() {
64
+ const registry = new Registry(new Connection(rpc(), 'confirmed'), { allowDev: has('allow-dev-enclaves'), measurements: [] });
65
+ await registry.refresh();
66
+ const rows = registry.catalog();
67
+ if (!rows.length)
68
+ return console.log('No models listed yet.');
69
+ for (const m of rows)
70
+ console.log(`${m.id.padEnd(40)} ${String(m.providers).padStart(3)} providers from ${perMillion(m.cheapest.input)} in · ${perMillion(m.cheapest.output)} out${m.private ? ` · ${m.private} private` : ''}`);
71
+ }
72
+ async function listProviders() {
73
+ const registry = new Registry(new Connection(rpc(), 'confirmed'), { allowDev: has('allow-dev-enclaves'), measurements: [] });
74
+ await registry.refresh();
75
+ for (const e of registry.entries.values()) {
76
+ const a = e.account;
77
+ console.log(`${e.address} ${(e.manifest?.name ?? '?').padEnd(24)} ${a.active ? 'open ' : 'closed'} settled $${usd(a.settled, 2)} in ${a.redeems} redeems ${e.manifest?.models.length ?? 0} models ${e.tee ? `enclave: ${e.tee.reason}` : ''}${e.problem ? ` ✗ ${e.problem}` : ''}`);
78
+ }
79
+ }
80
+ async function listChannels() {
81
+ const conn = new Connection(rpc(), 'confirmed');
82
+ const session = loadOrCreateKeypair(home('router', 'session.json'));
83
+ const local = new Channels(conn, null, session, mint(), home('router', 'channels.json'), { deposit: 0n, topUpBelow: 0n, dailyCap: 0n }).list();
84
+ if (!local.length)
85
+ return console.log('No channels yet. The first request to a provider opens one.');
86
+ for (const c of local) {
87
+ const info = await conn.getAccountInfo(new PublicKey(c.channel));
88
+ if (!info) {
89
+ console.log(`${c.channel} closed`);
90
+ continue;
91
+ }
92
+ const a = decodeChannel(info.data);
93
+ const state = a.closingAt === 0n ? 'open' : `closing, ready after ${new Date(Number(a.closingAt) * 1000).toISOString()}`;
94
+ console.log(`${c.channel} provider ${c.provider.slice(0, 8)}… deposit $${usd(a.deposit, 2)} you signed $${usd(c.cumulative)} redeemed $${usd(a.redeemed)} ${state}`);
95
+ }
96
+ }
97
+ async function provider(sub) {
98
+ const cfgPath = flag('config');
99
+ if (!cfgPath)
100
+ throw new Error('--config <provider.json> is required');
101
+ const cfg = JSON.parse(readFileSync(expand(cfgPath), 'utf8'));
102
+ cfg.rpcUrl = flag('rpc', cfg.rpcUrl) ?? cfg.rpcUrl;
103
+ cfg.listen = Number(flag('port', process.env.PORT ?? '')) || cfg.listen || PROVIDER_PORT;
104
+ const node = new ProviderNode(cfg);
105
+ if (sub === 'start') {
106
+ await node.listen();
107
+ console.log(`${NAME} provider "${cfg.name}" on :${cfg.listen} · listing ${node.pda.toBase58()} · ${cfg.models.length} models · manifest ${node.manifestHash.toString('hex').slice(0, 12)}…`);
108
+ return;
109
+ }
110
+ if (sub === 'register') {
111
+ const w = loadKeypair(expand(flag('wallet', cfg.wallet ?? '')));
112
+ if (!w.publicKey.equals(node.authority))
113
+ throw new Error('the wallet does not match the config authority');
114
+ const exists = await node.conn.getAccountInfo(node.pda);
115
+ const ix = exists ? node.updateInstruction(w.publicKey) : node.registerInstruction(w.publicKey);
116
+ const sig = await sendAndConfirmTransaction(node.conn, new Transaction().add(ix), [w], { commitment: 'confirmed' });
117
+ console.log(`${exists ? 'updated' : 'listed'} ${node.pda.toBase58()} tx ${sig}`);
118
+ return;
119
+ }
120
+ if (sub === 'attest') {
121
+ const sig = await sendAndConfirmTransaction(node.conn, new Transaction().add(node.attestInstruction()), [node.node], { commitment: 'confirmed' });
122
+ console.log(`enclave claim posted tx ${sig}`);
123
+ return;
124
+ }
125
+ if (sub === 'redeem') {
126
+ const done = await node.redeemDue(1n);
127
+ console.log(done.length ? done.map((d) => `$${usd(d.amount)} from ${d.channel}`).join('\n') : 'nothing to redeem');
128
+ return;
129
+ }
130
+ if (sub === 'show') {
131
+ const info = await node.conn.getAccountInfo(node.pda);
132
+ if (!info)
133
+ return console.log('not listed');
134
+ const p = decodeProvider(info.data);
135
+ console.log({ ...p, authority: p.authority.toBase58(), nodeKey: p.nodeKey.toBase58() });
136
+ return;
137
+ }
138
+ throw new Error(`unknown provider command ${sub}`);
139
+ }
140
+ async function main() {
141
+ const [cmd, sub] = argv;
142
+ if (!cmd || cmd === 'help' || has('help'))
143
+ return console.log(HELP);
144
+ if (cmd === 'router')
145
+ return router();
146
+ if (cmd === 'models')
147
+ return listModels();
148
+ if (cmd === 'providers')
149
+ return listProviders();
150
+ if (cmd === 'provider')
151
+ return provider(sub ?? 'start');
152
+ if (cmd === 'channels')
153
+ return listChannels();
154
+ if (cmd === 'close') {
155
+ const walletPath = flag('wallet', process.env.ORRERY_WALLET);
156
+ if (!sub || !walletPath)
157
+ throw new Error(`usage: ${CLI} close <channel> --wallet <keypair.json> [--finish]`);
158
+ const w = loadKeypair(expand(walletPath));
159
+ const ch = new PublicKey(sub);
160
+ const conn = new Connection(rpc(), 'confirmed');
161
+ const info = await conn.getAccountInfo(ch);
162
+ if (!info)
163
+ return console.log(`${ch.toBase58()} is already closed`);
164
+ const c = decodeChannel(info.data);
165
+ if (!c.payer.equals(w.publicKey))
166
+ throw new Error('this wallet did not open that channel');
167
+ if (!has('finish')) {
168
+ if (c.closingAt !== 0n)
169
+ return console.log(`already closing; the rest can come back after ${new Date(Number(c.closingAt) * 1000).toISOString()} with --finish`);
170
+ await sendAndConfirmTransaction(conn, new Transaction().add(ins.requestClose(w.publicKey, ch)), [w]);
171
+ const cfg = decodeConfig((await conn.getAccountInfo(pda.config())).data);
172
+ return console.log(`closing ${ch.toBase58()}; the provider has ${cfg.closeGraceSecs}s to redeem, then run: ${CLI} close ${ch.toBase58()} --wallet ${walletPath} --finish`);
173
+ }
174
+ if (c.closingAt === 0n)
175
+ throw new Error(`start the close first: ${CLI} close ${ch.toBase58()} --wallet ${walletPath}`);
176
+ const now = BigInt(Math.floor(Date.now() / 1000));
177
+ if (now < c.closingAt)
178
+ throw new Error(`the grace period ends at ${new Date(Number(c.closingAt) * 1000).toISOString()}`);
179
+ const prov = decodeProvider((await conn.getAccountInfo(c.provider)).data);
180
+ const sig = await sendAndConfirmTransaction(conn, new Transaction().add(ins.closeChannel(w.publicKey, prov.authority, ch, w.publicKey, c.mint)), [w]);
181
+ return console.log(`closed ${ch.toBase58()}; $${usd(c.deposit - c.redeemed)} and the account rent are back in your wallet tx ${sig}`);
182
+ }
183
+ console.log(HELP);
184
+ process.exitCode = 1;
185
+ }
186
+ void pda;
187
+ main().catch((e) => {
188
+ console.error(e.message);
189
+ process.exit(1);
190
+ });
package/dist/keys.js ADDED
@@ -0,0 +1,35 @@
1
+ import { createPrivateKey, createPublicKey, sign, verify } from 'node:crypto';
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
3
+ import { dirname, join } from 'node:path';
4
+ import { homedir } from 'node:os';
5
+ import { Keypair } from '@solana/web3.js';
6
+ import { HOME_DIR } from "./brand.js";
7
+ const PKCS8_ED25519 = Buffer.from('302e020100300506032b657004220420', 'hex');
8
+ const SPKI_ED25519 = Buffer.from('302a300506032b6570032100', 'hex');
9
+ export function home(...parts) {
10
+ return join(process.env.ORRERY_HOME ?? join(homedir(), HOME_DIR), ...parts);
11
+ }
12
+ export function writePrivate(path, body) {
13
+ mkdirSync(dirname(path), { recursive: true, mode: 0o700 });
14
+ writeFileSync(path, body, { mode: 0o600 });
15
+ }
16
+ export function loadKeypair(path) {
17
+ return Keypair.fromSecretKey(Uint8Array.from(JSON.parse(readFileSync(path, 'utf8'))));
18
+ }
19
+ export function loadOrCreateKeypair(path) {
20
+ if (existsSync(path))
21
+ return loadKeypair(path);
22
+ const kp = Keypair.generate();
23
+ writePrivate(path, JSON.stringify(Array.from(kp.secretKey)));
24
+ return kp;
25
+ }
26
+ export function signerOf(kp) {
27
+ const key = createPrivateKey({ key: Buffer.concat([PKCS8_ED25519, Buffer.from(kp.secretKey.subarray(0, 32))]), format: 'der', type: 'pkcs8' });
28
+ return (message) => new Uint8Array(sign(null, message, key));
29
+ }
30
+ export function ed25519Verify(publicKey, message, signature) {
31
+ if (publicKey.length !== 32 || signature.length !== 64)
32
+ return false;
33
+ const key = createPublicKey({ key: Buffer.concat([SPKI_ED25519, Buffer.from(publicKey)]), format: 'der', type: 'spki' });
34
+ return verify(null, message, key, signature);
35
+ }
@@ -0,0 +1,34 @@
1
+ // A provider's price list. The node serves it at /v1/manifest; the sha256 of its canonical bytes is on chain, so a
2
+ // router can tell whether the list it fetched is the one the provider's wallet signed for.
3
+ import { createHash } from 'node:crypto';
4
+ function sortKeys(v) {
5
+ if (Array.isArray(v))
6
+ return v.map(sortKeys);
7
+ if (v && typeof v === 'object') {
8
+ return Object.fromEntries(Object.keys(v).sort().filter((k) => v[k] !== undefined).map((k) => [k, sortKeys(v[k])]));
9
+ }
10
+ return v;
11
+ }
12
+ export function canonical(m) {
13
+ return Buffer.from(JSON.stringify(sortKeys(m)), 'utf8');
14
+ }
15
+ export function manifestHash(m) {
16
+ return createHash('sha256').update(Buffer.isBuffer(m) ? m : canonical(m)).digest();
17
+ }
18
+ const MODEL_ID = /^[a-z0-9][a-z0-9._\-/]{0,95}$/;
19
+ export function checkManifest(m) {
20
+ const x = m;
21
+ if (!x || x.v !== 1 || typeof x.name !== 'string' || !Array.isArray(x.models))
22
+ throw new Error('manifest: bad shape');
23
+ if (x.models.length > 512)
24
+ throw new Error('manifest: too many models');
25
+ for (const o of x.models) {
26
+ if (!MODEL_ID.test(o.id))
27
+ throw new Error(`manifest: bad model id ${o.id}`);
28
+ if (!Number.isSafeInteger(o.price?.input) || !Number.isSafeInteger(o.price?.output) || o.price.input < 0 || o.price.output < 0)
29
+ throw new Error(`manifest: bad price for ${o.id}`);
30
+ if (!Number.isSafeInteger(o.context) || !Number.isSafeInteger(o.maxOutput) || o.maxOutput <= 0)
31
+ throw new Error(`manifest: bad limits for ${o.id}`);
32
+ }
33
+ return x;
34
+ }
@@ -0,0 +1,22 @@
1
+ const MILLION = 1000000n;
2
+ export function costOf(p, inputTokens, outputTokens) {
3
+ const raw = BigInt(Math.max(0, Math.floor(inputTokens))) * BigInt(p.input) + BigInt(Math.max(0, Math.floor(outputTokens))) * BigInt(p.output);
4
+ return (raw + MILLION - 1n) / MILLION;
5
+ }
6
+ /** A rough upper bound for a request's prompt before it runs: 4 characters per token over everything the model
7
+ * reads (messages, tool definitions, response schema). Router and provider compute it the same way. */
8
+ export function estimateInputTokens(body) {
9
+ const b = body && typeof body === 'object' && !Array.isArray(body) ? body : null;
10
+ const read = b && ('messages' in b || 'tools' in b) ? { m: b.messages, t: b.tools, r: b.response_format } : body;
11
+ return Math.ceil(JSON.stringify(read ?? '').length / 4);
12
+ }
13
+ export function usd(units, digits = 6) {
14
+ const neg = units < 0n;
15
+ const v = neg ? -units : units;
16
+ const whole = v / 1000000n;
17
+ const frac = (v % 1000000n).toString().padStart(6, '0').slice(0, digits);
18
+ return `${neg ? '-' : ''}${whole}.${frac}`;
19
+ }
20
+ export function perMillion(units) {
21
+ return `$${(units / 1_000_000).toFixed(units < 100_000 ? 3 : 2)}/M`;
22
+ }