Skip to content
Open
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
4 changes: 2 additions & 2 deletions .github/workflows/ts-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -69,5 +69,5 @@ jobs:
- name: Build npm bundle
run: npm run build

- name: Validate npm package contents
run: npm pack --dry-run
- name: Verify independently installed npm package
run: npm run verify:package
9 changes: 5 additions & 4 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,13 +48,14 @@ stdout/stderr discipline).

Key points:
- **Ports live in `application/ports/`** (e.g. `wallet-repository`, `tron-gateway`, `ledger-device`,
`price-provider`); outbound adapters implement them (dependency inversion).
`price-provider`); outbound adapters implement them (dependency inversion). Shared transaction input types live in
`application/contracts/`; ports must not import use cases or services (`ports-are-innermost`).
- **Chain-family differences** are isolated per family — `application/use-cases/<family>/`,
`adapters/outbound/chain/<family>/`, and the family plugin under `bootstrap/families/`. Both
`tron` and `evm` are registered unconditionally (`bootstrap/composition.ts`) and reachable: the
builtin networks and aliases cover ETH, Sepolia, BSC and BSC testnet alongside the TRON three.
There is no family-level feature gate. EVM simply binds a narrower command set (~22 bindings:
account, block, tx, token, contract, message/typed-data signing) against TRON's ~53, which adds
builtin networks and aliases cover ETH, Sepolia, BSC, BSC testnet, Base and Base Sepolia alongside the TRON three.
There is no family-level feature gate. EVM simply binds a narrower command set (29 bindings:
account, block, chain, tx, token, contract, message/typed-data signing and eight ERC-8004 identity commands) against TRON's 78, which adds
stake, permission, proposal, asset, GasFree and TronLink multisig.
- **A single Zod schema per command** drives validation, yargs arity, help text, and JSON Schema.
- **Secrets** (private keys, mnemonics, BIP39 passphrases) are encrypted at rest and never accepted
Expand Down
6 changes: 6 additions & 0 deletions ts/.dependency-cruiser.cjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,12 @@
*/
module.exports = {
forbidden: [
{
name: "ports-are-innermost",
severity: "error",
from: { path: "^src/application/ports/" },
to: { path: "^src/application/(use-cases|services)/" },
},
{
name: "no-circular",
severity: "error",
Expand Down
145 changes: 94 additions & 51 deletions ts/README.md

Large diffs are not rendered by default.

4 changes: 3 additions & 1 deletion ts/docs/commands/networks.md
Original file line number Diff line number Diff line change
Expand Up @@ -36,14 +36,16 @@ wallet-cli networks
| eip155:11155111 | sepolia | evm | 11155111 | evm-gas | ethereum-sepolia-rpc.publicnode.com |
| eip155:56 | bsc | evm | 56 | evm-gas | bsc-dataseed.bnbchain.org |
| eip155:97 | bsc-testnet | evm | 97 | evm-gas | bsc-testnet-dataseed.bnbchain.org |
| eip155:8453 | base | evm | 8453 | evm-gas | mainnet.base.org |
| eip155:84532 | base-sepolia | evm | 84532 | evm-gas | sepolia.base.org |
```

```bash
wallet-cli networks -o json
```

```json
{"schema":"wallet-cli.result.v1","success":true,"command":"networks","data":[{"id":"tron:728126428","alias":"tron","family":"tron","chainId":"728126428","feeModel":"tron-resource","endpoint":"api.trongrid.io"},{"id":"tron:3448148188","alias":"nile","family":"tron","chainId":"3448148188","feeModel":"tron-resource","endpoint":"nile.trongrid.io"},{"id":"tron:2494104990","alias":"shasta","family":"tron","chainId":"2494104990","feeModel":"tron-resource","endpoint":"api.shasta.trongrid.io"},{"id":"eip155:1","alias":"ethereum","family":"evm","chainId":"1","feeModel":"evm-gas","endpoint":"ethereum-rpc.publicnode.com"},{"id":"eip155:11155111","alias":"sepolia","family":"evm","chainId":"11155111","feeModel":"evm-gas","endpoint":"ethereum-sepolia-rpc.publicnode.com"},{"id":"eip155:56","alias":"bsc","family":"evm","chainId":"56","feeModel":"evm-gas","endpoint":"bsc-dataseed.bnbchain.org"},{"id":"eip155:97","alias":"bsc-testnet","family":"evm","chainId":"97","feeModel":"evm-gas","endpoint":"bsc-testnet-dataseed.bnbchain.org"}],"meta":{"durationMs":2,"warnings":[]}}
{"schema":"wallet-cli.result.v1","success":true,"command":"networks","data":[{"id":"tron:728126428","alias":"tron","family":"tron","chainId":"728126428","feeModel":"tron-resource","endpoint":"api.trongrid.io"},{"id":"tron:3448148188","alias":"nile","family":"tron","chainId":"3448148188","feeModel":"tron-resource","endpoint":"nile.trongrid.io"},{"id":"tron:2494104990","alias":"shasta","family":"tron","chainId":"2494104990","feeModel":"tron-resource","endpoint":"api.shasta.trongrid.io"},{"id":"eip155:1","alias":"ethereum","family":"evm","chainId":"1","feeModel":"evm-gas","endpoint":"ethereum-rpc.publicnode.com"},{"id":"eip155:11155111","alias":"sepolia","family":"evm","chainId":"11155111","feeModel":"evm-gas","endpoint":"ethereum-sepolia-rpc.publicnode.com"},{"id":"eip155:56","alias":"bsc","family":"evm","chainId":"56","feeModel":"evm-gas","endpoint":"bsc-dataseed.bnbchain.org"},{"id":"eip155:97","alias":"bsc-testnet","family":"evm","chainId":"97","feeModel":"evm-gas","endpoint":"bsc-testnet-dataseed.bnbchain.org"},{"id":"eip155:8453","alias":"base","family":"evm","chainId":"8453","feeModel":"evm-gas","endpoint":"mainnet.base.org"},{"id":"eip155:84532","alias":"base-sepolia","family":"evm","chainId":"84532","feeModel":"evm-gas","endpoint":"sepolia.base.org"}],"meta":{"durationMs":2,"warnings":[]}}
```

## Output
Expand Down
4 changes: 4 additions & 0 deletions ts/docs/concepts/networks.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ wallet-cli networks
| eip155:11155111 | sepolia | evm | 11155111 | evm-gas | ethereum-sepolia-rpc.publicnode.com |
| eip155:56 | bsc | evm | 56 | evm-gas | bsc-dataseed.bnbchain.org |
| eip155:97 | bsc-testnet | evm | 97 | evm-gas | bsc-testnet-dataseed.bnbchain.org |
| eip155:8453 | base | evm | 8453 | evm-gas | mainnet.base.org |
| eip155:84532 | base-sepolia | evm | 84532 | evm-gas | sepolia.base.org |
```

| Id | Alias | What it is | Native coin value |
Expand All @@ -27,6 +29,8 @@ wallet-cli networks
| `eip155:11155111` | `sepolia` | Ethereum test network | none |
| `eip155:56` | `bsc` | BNB Smart Chain | **Real money** |
| `eip155:97` | `bsc-testnet` | BNB Smart Chain test network | none |
| `eip155:8453` | `base` | Base mainnet | **Real money** |
| `eip155:84532` | `base-sepolia` | Base test network | none |

An **alias** is a short name you may type instead of the id. It resolves once, at selection, and nothing downstream ever sees it — `chain.network` in the JSON envelope always reports the canonical id. Aliases live in config and can be re-pointed, so scripts should pass canonical ids.

Expand Down
7 changes: 7 additions & 0 deletions ts/docs/concepts/provider-catalog.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
# Provider catalog queries and snapshots

`wallet-cli x402 provider-list`, `provider-show` and `provider-endpoints` query the online catalog. They do not read the local snapshot or silently fall back to stale data.

`wallet-cli x402 provider-update` downloads a catalog snapshot for inspection or external tooling. Its result includes the `cache` file path. Updating this snapshot does not change subsequent online queries. Offline catalog lookup is not currently implemented.

Catalog requests honor the CLI `--timeout` value and limit response bodies to 10 MiB, including responses without a Content-Length header. Facilitator requests use the same configured per-request timeout and a 1 MiB response limit. The timeout does not stop the lifetime of a running `x402 serve` process.
178 changes: 178 additions & 0 deletions ts/docs/development/bai-recharge.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,178 @@
# B.AI recharge

B.AI now authenticates recharge operations with each user's personal API key.
The CLI calls B.AI directly to resolve the credit recipient, create a preorder,
and report the payment transaction. The selected wallet signs the payment.

## Payment flow

1. Check the local confirmation for the API key, payer wallet and mainnet.
2. Validate the amount and trusted platform destination. Resolve `--to` when it
identifies another B.AI user.
3. Create the preorder with the personal API key.
4. Call the existing `X402Service.roundtrip()` with the platform destination,
token and exact amount. It starts a temporary endpoint on `127.0.0.1` using
an automatically allocated port, pays through `X402PaymentClient`, and closes
the endpoint in `finally`.
5. The local endpoint calls the facilitator's `/verify` and `/settle` endpoints.
Wallet account selection and signing use the existing x402 signer bridge.
6. Validate the successful settlement, network, transaction hash and payer.
Call `order.reportTxHash` with the original chain, amount and credit target.

The personal API key is sent only to B.AI business APIs. Neither the local payment
endpoint nor the facilitator receives it. `--to` selects the account receiving
credits; the on-chain recipient is always the platform address.

The CLI no longer calls the old recharge MCP or its merchant credit endpoint.
That removes a second credit-reporting path and a second set of recharge-server
configuration. The existing x402 SDK, facilitator and `roundtrip` remain in use.
Retiring the deployed recharge server is a separate operation.

## Wallet binding and signed message

Binding uses the personal API key to identify the B.AI user. Before signing,
construct the message using the recharge binding template from the updated API
specification. Arbitrary test text is rejected with `WalletInvalidSignature`,
even when the signature recovers the correct wallet address locally.

```javascript
const message = [
"Welcome to BAI !",
`${origin} wants you to confirm wallet binding for recharge:`,
address,
"",
`Chain ID: ${chainId}`,
`Expiration Time: ${expirationTime}`,
`Nonce: ${nonce}`,
].join("\n");
```

For production, `origin` is `https://chat.bankofai.io`; the specification's
`https://chat-dev.b.ai` is the development example. Use the origin of the target
B.AI deployment. Mainnet chain IDs are `728126428` (TRON), `8453` (Base), and
`56` (BNB Chain). The live test used an ISO 8601 UTC expiration five minutes ahead
and a fresh 16-byte random nonce encoded as 32 hexadecimal characters; these are
verified client choices, not documented server limits or a server-issued challenge.

Select the wallet explicitly when signing:

```bash
wallet-cli message sign --account <wallet> --network <tron|base|bsc> \
--message "$message" --password-stdin -o json
```

Pass the master password through stdin. Send the returned `address`, unchanged
`message`, and `signature` to `POST /trpc/lambda/wallet.bindRechargeWallet`, inside
`{"json":{...}}`, with the personal API key as Bearer authentication. Set `chain`
to `tron`, `base`, or `bnb`; `version: 2` selects TRON V2 signing and was also
accepted on both EVM chains. Never trim, reformat, or rebuild the message after
signing. Binding signatures authorize account association; this step sends no
payment transaction.

The backend canonicalizes EVM binding responses: `chain` becomes `eth`, and the
address is lowercase. The adapter accepts that family alias for `bnb`/`base`/`eth`
and compares EVM addresses without case sensitivity, while still rejecting another
address or unrelated chain. TRON addresses remain case-sensitive. The adapter
returns the server's canonical binding; subsequent network-specific checks still
use the original `base` or `bnb` request chain.

On 2026-09-09, three different wallets were signed with Wallet CLI and bound using
one personal API key. Every successful binding returned the same user ID. After
each binding, all three original chain/address pairs were queried through
`wallet.isRechargeBound` using that same key:

| After binding | TRON wallet | Base wallet | BNB Chain wallet |
| --- | --- | --- | --- |
| TRON | true | false | false |
| Base | true | true | false |
| BNB Chain | true | true | true |

This verifies those three bindings coexist on the server; it does not establish an
unlimited wallet count or prove recharge settlement. No funds were transferred.
The local `bai-binding.json` still stores only the last confirmed API-key/chain/address
fingerprint. Switching wallet or network requires configuring the same key again
for that selection to refresh local confirmation; this does not remove server
bindings. CLI credential setup checks existing bindings, rather than creating one.
`BaiRechargeClient.bind()` accepts an already signed message; there is no automatic
binding or new binding command in this change.

## Networks and payment requirements

| Network | Token | B.AI payment scheme |
| --- | --- | --- |
| TRON mainnet | USDT, USDD | exact / Permit2 |
| BNB Chain mainnet | USDT | exact / Permit2 |
| Base mainnet | USDC | exact / EIP-3009 |

B.AI uses `exact`; it does not select GasFree automatically. Generic x402 commands
continue to support TRON `exact_gasfree`. The local server owns token metadata,
including Base USDC's six decimals and EIP-712 domain version `2`.

Platform addresses live in `adapters/outbound/config/bai-builtins.ts`. Only TRON,
BNB Chain and Base are retained. The allowlist cannot be overridden by user
configuration; changing it requires a CLI release. Minimum recharge rules remain
in `domain/bai/recharge-policy.ts`.

Roundtrip enforces token, scheme, destination and exact amount before signing.
The explicit maximum equals the requested amount. On TRON, the SDK checks Permit2
allowance and automatically signs, broadcasts and waits for an approval when it is
insufficient and the server has not declared approval resource sponsoring. The SDK
approves the maximum uint256 amount. A failed approval stops payment; tokens that
require resetting an existing allowance to zero may still require manual handling.
When the server declares approval resource sponsoring, the signed approval is sent
in the extension instead. EVM self-funded approval fallback is not implemented.
For EVM approval sponsoring, the x402 signer bridge maps the SDK transaction
`gas` field to wallet `gasLimit`, preserving an explicit `gasLimit` when supplied.

## Failure and verification

The roundtrip port validates the token and decimal precision before target resolution
and preorder creation, using the same adapter rules as server startup. Classified
payment errors retain their codes and any settlement evidence through the recharge
flow. Preorder failure stops payment. An uncertain payment is never retried automatically.
Only a successful settlement with a valid hash and matching network can be reported.
Reporting failure preserves the hash, original target and `retryPayment: false`.
`bai recharge-report <txHash> --chain tron|bnb|base [--amount <original amount>]`
retries reporting without creating an order, resolving a recipient, signing or
paying. It requires the original personal API key but no local wallet. For another
recipient, supply both `--to <original identifier>` and `--target-id <original ID>`
from `rechargeTarget`; omit both only for self recharge. Backend verification remains
authoritative. A persistent recovery log is not implemented; retain the JSON result.

Settlement validation failures preserve a syntactically valid hash as
`details.candidateTxHash`, with a fixed `reason`, `paymentStatus: unknown`,
`settled: false` and `retryPayment: false`. Original chain, amount and recipient are
retained by the recharge flow. A candidate is evidence for reconciliation, not a
confirmed payment: verify it before using the report-only command.

Tests cover the local HTTP roundtrip with the installed SDK on BSC and Base,
settlement validation, endpoint cleanup, self/recipient CLI orchestration and
reporting failure. The facilitator and B.AI backend are mocked. Real settlement,
credit attribution, repeated reporting and Ledger operation still need live
integration verification.

## API failure diagnostics

Both B.AI API adapters decode bounded HTTP error bodies and tRPC error envelopes.
Recognized business failures return `bai_rejected` with a fixed explanatory message
and `details.reason`, `procedure`, `httpStatus`, and `retryPayment: false`.
The recognized reasons are `WalletInvalidSignature`, `UNSUPPORTED_CHAIN`,
`TX_NOT_FOUND_OR_INVALID`, `UNSUPPORTED_TOKEN`, `PAYER_MISMATCH`, `WALLET_NOT_BOUND`,
`RECHARGE_TX_TOO_OLD`, `TX_TIMESTAMP_UNAVAILABLE`, `PRICE_UNAVAILABLE`,
`RECHARGE_AMOUNT_TOO_SMALL`, and `SELF_RECHARGE_TARGET` (the documented Chinese
self-recipient error). Signature rejection explains the required message fields
and wallet selection rather than blaming the signer.

HTTP 401/403 retain `bai_auth_failed`; 429 retains `provider_rate_limited` without
waiting for an error body. Timeouts, oversized responses, malformed JSON and
connection failures remain distinguishable. Unknown server messages are never
copied into output; callers receive the operation, HTTP status and a safe message.
Invalid local recharge request fields return `invalid_value` before HTTP.

A report result with `success: false` retains its existing business `code` and adds
a locally defined explanation. The recharge/recharge-report result keeps the hash,
original target, `creditStatus: unconfirmed` and `retryPayment: false`. Thrown API
errors also retain their structured error envelope inside that result. These are
credit failures after payment, not permission to repeat the payment. A failure to
retrieve a price or timestamp suggests retrying reporting only. Unknown report
codes retain the bounded code and a generic reconciliation instruction.
Loading