Skip to content
Closed
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
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -226,6 +226,17 @@ The CLI never touches stdout for human chrome when `--json` is passed: every byt

Solana payments route through MPP `solana/charge` (Solana Foundation's `@solana/mpp` package) rather than x402. Tempo is always MPP via `mppx/client`. Base is the only x402 rail pay funds directly.

### Nano (opt-in)

`nano` is an opt-in x402 `exact` rail on `nano:mainnet`, used only when named with `--chain nano`. It is never auto-selected and `wallet create` without `--chain` does not create it. XNO has no USD price inside pay, so `--max-spend` and `limits` cannot bound it: `pay --chain nano` requires `--max-spend-xno` and refuses with `limit_required` without it. Ledger entries carry `price_xno`, never `price_usd`.

```bash
agentscore-pay wallet create --chain nano
agentscore-pay pay POST https://merchant.example/api --chain nano -d '{}' --max-spend-xno 0.05
```

The client publishes a state send block to the 402's `payTo` (`nano_` or legacy `xrb_`) and sends `{ "blockHash": "..." }` as the x402 payload. Proof of work comes from the node's `work_generate`, so `NANO_RPC_URL` must point at a node or proxy that serves it.

## Rail selection

The CLI ships with **no default rail preference**. Algorithm:
Expand Down Expand Up @@ -520,6 +531,7 @@ The npm package itself is published with [npm provenance](https://docs.npmjs.com
| `SOLANA_DEVNET_RPC_URL` | override Solana Devnet RPC endpoint |
| `TEMPO_RPC_URL` | override Tempo mainnet RPC endpoint |
| `TEMPO_TESTNET_RPC_URL` | override Tempo testnet RPC endpoint |
| `NANO_RPC_URL` | Nano node RPC used by `--chain nano` (must serve `work_generate`) |
| `AGENTSCORE_API_KEY` | API key for identity commands (`assess`, `sessions`, `credentials`, `associate-wallet`) |

## Relationship to other AgentScore packages
Expand Down
2 changes: 2 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,8 @@
"dependencies": {
"@agent-score/sdk": "^2.9.1",
"@clack/prompts": "^1.8.1",
"@noble/ed25519": "^3.1.0",
"@noble/hashes": "^2.4.0",
"@scure/bip39": "^2.4.0",
"@solana-program/system": "^0.16.0",
"@solana-program/token": "^0.18.0",
Expand Down
58 changes: 58 additions & 0 deletions src/chains/nano-x402.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
import { CliError } from '../errors';
import { normalizeAddress, send, type NanoRpc } from './nano';
import type { PaymentPayloadResult, PaymentRequirements, SchemeNetworkClient } from '@x402/core/types';

const NANO_DECIMALS = 30;

/**
* x402 `exact` on Nano. Nano has no transfer authorization to hand a facilitator,
* so the client publishes the state send block itself and the payload carries
* the block hash for the merchant to confirm on its own node.
*/
export class ExactNanoScheme implements SchemeNetworkClient {
readonly scheme = 'exact';

constructor(
private readonly opts: {
key: Buffer;
rpc?: NanoRpc;
/**
* Blocks already published in this invocation, keyed by payTo and amount. A retried
* request re-presents the same block instead of paying a second time.
*/
published?: Map<string, string>;
/** Receives errors before x402 wraps them, so the caller can keep the structured code. */
onError?: (err: unknown) => void;
},
) {}

async createPaymentPayload(x402Version: number, requirements: PaymentRequirements): Promise<PaymentPayloadResult> {
try {
const declared = requirements.extra?.decimals;
if (declared !== undefined && declared !== NANO_DECIMALS) {
throw new CliError(
'merchant_spec_violation',
`Merchant 402 declared ${String(declared)} decimals for XNO; Nano raw has ${NANO_DECIMALS}. Refusing to pay.`,
{ extra: { chain: 'nano', decimals: declared } },
);
}
if (!/^\d+$/.test(requirements.amount ?? '')) {
throw new CliError('merchant_spec_violation', `Merchant 402 amount is not integer raw: ${requirements.amount}`, {
extra: { chain: 'nano', amount: requirements.amount },
});
}
const payTo = normalizeAddress(requirements.payTo);
const key = `${payTo}:${requirements.amount}`;
let blockHash = this.opts.published?.get(key);
if (!blockHash) {
const sent = await send({ key: this.opts.key, to: payTo, amountRaw: BigInt(requirements.amount), rpc: this.opts.rpc });
blockHash = sent.hash;
this.opts.published?.set(key, blockHash);
}
return { x402Version, payload: { blockHash } };
} catch (err: unknown) {
this.opts.onError?.(err);
throw err;
}
}
}
Loading