From 5eb81fa43e71f1be8b855e97951e0e7f7bc3db44 Mon Sep 17 00:00:00 2001 From: lightchainaidev Date: Sat, 10 Oct 2026 03:46:16 +0200 Subject: [PATCH 1/2] Nightly keyless x402 acceptance from a fresh wallet acceptance:x402 needs a wallet that never authorized a delegate, and the nightly wallet authorizes the API's for acceptance:pool. scripts/fresh-wallet.ts runs an npm script with a wallet made for that run: the funder sends it LIGHTCHAIN_FUND_WEI, and afterwards, pass or fail, it withdraws its prepaid balance and sends everything back. acceptance.yml runs it after pool, from the same secret. The x402 payment cap defaults to 0.05 LCAI, in acceptance-x402.ts and in the README example: testnet charges 0.02 a job, past the old 0.01 default. --- .github/workflows/acceptance.yml | 29 ++++++++++++++-- README.md | 5 +-- package.json | 3 +- scripts/acceptance-x402.ts | 4 +-- scripts/fresh-wallet.ts | 57 ++++++++++++++++++++++++++++++++ 5 files changed, 91 insertions(+), 7 deletions(-) create mode 100644 scripts/fresh-wallet.ts diff --git a/.github/workflows/acceptance.yml b/.github/workflows/acceptance.yml index e1e5afc..35fefa7 100644 --- a/.github/workflows/acceptance.yml +++ b/.github/workflows/acceptance.yml @@ -1,5 +1,6 @@ -# The pool acceptance flow against testnet: mints a key, tops up on the first 402, then a streaming completion and two -# interleaved conversations through the OpenAI SDK. The wallet in TESTNET_WALLET_PRIVATE_KEY needs testnet LCAI. +# Testnet acceptance, from the wallet in TESTNET_WALLET_PRIVATE_KEY (it needs testnet LCAI). +# pool: mints a key, tops up on the first 402, then a streaming completion and two interleaved conversations through +# the OpenAI SDK. x402: keyless pay-per-call, from a fresh wallet that wallet funds and that sends the rest back. name: Testnet acceptance on: @@ -32,3 +33,27 @@ jobs: with: name: acceptance-pool-${{ github.run_id }} path: acceptance-pool.log + + # After pool, not beside it: both send from the same wallet, and two jobs at once would race for its nonce. + x402: + needs: pool + if: ${{ !cancelled() }} + runs-on: ubuntu-latest + timeout-minutes: 20 + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v7 + with: + node-version: '24' + cache: npm + - run: npm ci + - run: npm run acceptance:x402:fresh 2>&1 | tee acceptance-x402.log + env: + FUNDER_PRIVATE_KEY: ${{ secrets.TESTNET_WALLET_PRIVATE_KEY }} + LIGHTCHAIN_NETWORK: testnet + LIGHTCHAIN_MODEL: gemma4:e2b + - if: always() + uses: actions/upload-artifact@v7 + with: + name: acceptance-x402-${{ github.run_id }} + path: acceptance-x402.log diff --git a/README.md b/README.md index 6d5555c..100a75f 100644 --- a/README.md +++ b/README.md @@ -67,7 +67,7 @@ const lc = new Lightchain({ network: "testnet", account: privateKeyToAccount(process.env.WALLET_PRIVATE_KEY as `0x${string}`), payment: "x402", - maxPaymentWei: 10n ** 16n, // required: the most one call may pay (0.01 LCAI) + maxPaymentWei: 5n * 10n ** 16n, // required: the most one call may pay (0.05 LCAI; testnet charges 0.02 a job) keyless: true, // or apiKey: process.env.LIGHTCHAIN_API_KEY onPayment: (p) => console.log(`settled in ${p.hash}, ${p.amount} wei debited`), }); @@ -134,9 +134,10 @@ npm run abi # regenerate src/abi.ts from pkg/chain/abis (inside the orch npm run openapi # regenerate test/fixtures/openapi.json from consumer-api's source (inside the orchestrator's sdk/) 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. -CI (`.github/workflows/ci.yml`) typechecks, builds and tests every pull request and every push to `main`. `acceptance.yml` runs `acceptance:pool` 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. Each run's log is kept as an artifact. +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. diff --git a/package.json b/package.json index 5420b4a..4dfbe18 100644 --- a/package.json +++ b/package.json @@ -27,7 +27,8 @@ "openapi": "../consumer-api/node_modules/.bin/tsx scripts/openapi.ts", "acceptance": "node scripts/acceptance.ts", "acceptance:x402": "node scripts/acceptance-x402.ts", - "acceptance:pool": "node scripts/acceptance-pool.ts" + "acceptance:pool": "node scripts/acceptance-pool.ts", + "acceptance:x402:fresh": "node scripts/fresh-wallet.ts acceptance:x402" }, "dependencies": { "viem": "^2.47.6" diff --git a/scripts/acceptance-x402.ts b/scripts/acceptance-x402.ts index f6d0264..12d607b 100644 --- a/scripts/acceptance-x402.ts +++ b/scripts/acceptance-x402.ts @@ -10,7 +10,7 @@ // LIGHTCHAIN_CHAIN_ID and LIGHTCHAIN_JOB_REGISTRY instead of LIGHTCHAIN_NETWORK. // LIGHTCHAIN_MODEL is required: with no key there is no /v1/models to ask. // Optional: LIGHTCHAIN_DEPOSIT_WEI (default 0.1 LCAI), LIGHTCHAIN_MAX_PAYMENT_WEI -// (default 0.01 LCAI), LIGHTCHAIN_PAYER_CONCURRENCY (the server's per-payer +// (default 0.05 LCAI), LIGHTCHAIN_PAYER_CONCURRENCY (the server's per-payer // limit, default 1). import assert from 'node:assert/strict'; import OpenAI from 'openai'; @@ -35,7 +35,7 @@ const lc = new Lightchain({ network, account: privateKeyToAccount(env.WALLET_PRIVATE_KEY as Hex), payment: 'x402', - maxPaymentWei: BigInt(env.LIGHTCHAIN_MAX_PAYMENT_WEI ?? 10n ** 16n), + maxPaymentWei: BigInt(env.LIGHTCHAIN_MAX_PAYMENT_WEI ?? 5n * 10n ** 16n), keyless: true, onPayment: (p) => { payments.push(p); diff --git a/scripts/fresh-wallet.ts b/scripts/fresh-wallet.ts new file mode 100644 index 0000000..2cf698d --- /dev/null +++ b/scripts/fresh-wallet.ts @@ -0,0 +1,57 @@ +// Runs an npm script with a wallet made for this run alone, for a test that needs a wallet with no history (one +// that never authorized a delegate) and must still run every night. FUNDER_PRIVATE_KEY's wallet funds it. After the +// script, pass or fail, the fresh wallet withdraws its prepaid balance and sends all it holds back to the funder. +// +// FUNDER_PRIVATE_KEY=0x... LIGHTCHAIN_MODEL=gemma4:e2b node scripts/fresh-wallet.ts acceptance:x402 +// +// The fresh key exists only in this process and the script's environment. LIGHTCHAIN_FUND_WEI (default 0.11 LCAI: +// acceptance:x402's 0.1 deposit, plus gas) is what a run killed before the sweep leaves behind. LIGHTCHAIN_NETWORK +// picks the network (default testnet). +import { spawnSync } from 'node:child_process'; +import { createPublicClient, createWalletClient, defineChain, formatEther, type Hex, http, parseAbi } from 'viem'; +import { generatePrivateKey, privateKeyToAccount } from 'viem/accounts'; +import { networks } from '../src/index.ts'; + +const { FUNDER_PRIVATE_KEY, ...env } = process.env; +const script = process.argv[2]; +if (!script || !FUNDER_PRIVATE_KEY) throw new Error('usage: FUNDER_PRIVATE_KEY=0x... node scripts/fresh-wallet.ts '); + +const network = networks[(env.LIGHTCHAIN_NETWORK ?? 'testnet') as 'mainnet' | 'testnet']; +const chain = defineChain({ + id: network.chainId, + name: `LightChain ${network.chainId}`, + nativeCurrency: { name: 'LightChain AI', symbol: 'LCAI', decimals: 18 }, + rpcUrls: { default: { http: [network.rpcUrl] } }, +}); +const transport = http(network.rpcUrl); +const chainClient = createPublicClient({ chain, transport, pollingInterval: 1_000 }); +const funder = createWalletClient({ account: privateKeyToAccount(FUNDER_PRIVATE_KEY as Hex), chain, transport }); +const key = generatePrivateKey(); +const fresh = createWalletClient({ account: privateKeyToAccount(key), chain, transport }); +const mined = async (hash: Hex) => { + const receipt = await chainClient.waitForTransactionReceipt({ hash }); + if (receipt.status !== 'success') throw new Error(`transaction ${hash} reverted`); + return hash; +}; + +const fund = BigInt(env.LIGHTCHAIN_FUND_WEI ?? 11n * 10n ** 16n); +const funded = await mined(await funder.sendTransaction({ to: fresh.account.address, value: fund })); +console.log(`fresh wallet ${fresh.account.address}: ${formatEther(fund)} LCAI from ${funder.account.address} in ${funded}`); + +const run = spawnSync('npm', ['run', '-s', script], { stdio: 'inherit', env: { ...env, WALLET_PRIVATE_KEY: key } }); + +const abi = parseAbi(['function prepaidBalanceOf(address) view returns (uint256)', 'function withdrawBalance(uint256)']); +const address = network.jobRegistry; +const prepaid = await chainClient.readContract({ address, abi, functionName: 'prepaidBalanceOf', args: [fresh.account.address] }); +if (prepaid > 0n) { + const hash = await mined(await fresh.writeContract({ address, abi, functionName: 'withdrawBalance', args: [prepaid] })); + console.log(`withdrew the prepaid ${formatEther(prepaid)} LCAI in ${hash}`); +} +// A legacy transfer at a fixed gas price costs exactly 21000 * gasPrice, so the rest can go back to the last wei. +const gasPrice = await chainClient.getGasPrice(); +const rest = (await chainClient.getBalance({ address: fresh.account.address })) - 21_000n * gasPrice; +if (rest > 0n) { + const hash = await mined(await fresh.sendTransaction({ to: funder.account.address, value: rest, gas: 21_000n, gasPrice })); + console.log(`returned ${formatEther(rest)} LCAI to ${funder.account.address} in ${hash}`); +} +process.exit(run.status ?? 1); From 6c554b62f6db539d325a5baec98faeb8fca6c739 Mon Sep 17 00:00:00 2001 From: lightchainaidev Date: Sat, 10 Oct 2026 03:46:36 +0200 Subject: [PATCH 2/2] Ignore node_modules --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 849ddff..1eae0cf 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ dist/ +node_modules/