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
4 changes: 2 additions & 2 deletions .claude/CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,14 @@ Single-package TypeScript CLI published to npm. Runnable via `npx @agent-score/p
| `src/keystore.ts` | AES-256-GCM + scrypt encrypted keystore (generic, chain-agnostic) |
| `src/wallets.ts` | Wallet factory dispatching to chain modules |
| `src/prompts.ts` | Passphrase input (respects `AGENTSCORE_PAY_PASSPHRASE` env) |
| `src/constants.ts` | Chain network IDs, USDC addresses, RPC URLs, Coinbase Onramp builder |
| `src/constants.ts` | Chain network IDs, USDC addresses, RPC URLs |
| `src/chains/base.ts` | EVM adapter (x402): viem Account, USDC balance, EIP-681 QR URI |
| `src/chains/solana.ts` | SVM adapter (x402): `@solana/kit` KeyPairSigner, SPL balance, `solana:` URI |
| `src/chains/tempo.ts` | EVM adapter (MPP): viem Account on chain 4217, USDC.e balance, EIP-681 QR URI |
| `src/commands/wallet.ts` | `wallet create/import/address/list/remove/export/show-mnemonic` |
| `src/commands/balance.ts` | `balance` across chains |
| `src/commands/qr.ts` | `qr` with optional amount |
| `src/commands/fund.ts` | `fund` — onramp link + QR + balance polling (Tempo testnet uses programmatic mint) |
| `src/commands/fund.ts` | `fund` — receive QR + balance polling (Tempo testnet uses programmatic mint via tempo_fundAddress) |
| `src/commands/pay.ts` | `pay <METHOD> <URL>` — routes to `@x402/fetch` (base/solana) or `mppx/client` (tempo) |
| `src/commands/identity.ts` | `reputation`, `assess`, `sessions create/get`, `credentials create/list/revoke`, `associate-wallet` (wraps `@agent-score/sdk`) |
| `src/commands/passport.ts` | `passport login/status/logout` — AgentScore Passport (buyer-side identity); stores opc_ at `~/.agentscore/passport.json`, auto-attached on `pay <url>` settle leg |
Expand Down
10 changes: 5 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ Requires Node 20+ for the npm path. Native single-file binaries (no Node require
# 1. One-shot init — encrypted keystores on every chain, derived from a single BIP-39 mnemonic
agentscore-pay init

# 2. Fund one of them (prints Coinbase Onramp URL + QR + polls balance)
# 2. Fund one of them (prints receive QR + polls balance)
agentscore-pay fund --chain base --amount 10

# 3. Pay — rail auto-selected from the single funded wallet
Expand Down Expand Up @@ -267,7 +267,7 @@ Verbose mode (`-v`) logs rail selection + balances to stderr.
| `wallet show-mnemonic --danger [--skip-confirm]` | Decrypt + print the stored BIP-39 mnemonic |
| `balance [--chain c] [--network n]` | USDC balance across chains (mainnet default; `--network testnet` for Base Sepolia / Solana Devnet / Tempo testnet) |
| `qr --chain c [--amount N] [--network n]` | ASCII QR or EIP-681 / `solana:` URI |
| `fund --chain c [--amount N] [--network n]` | Onramp URL + QR + balance poll. Default amount is `10` USD (~50-200 typical agent calls). |
| `fund --chain c [--amount N] [--network n]` | Receive QR + balance poll. Default amount is `10` USD (~50-200 typical agent calls). Send USDC from any wallet, exchange, or fiat onramp; `fund` polls until it lands. |
| `faucet --chain c` | Print testnet faucet URL(s) for the chain + copy your address to clipboard |
| `fund-estimate <url> [-X method] [-d body] [-H header]...` | Probe a 402-gated URL and report how many calls your balance covers + top-up suggestion |
| `check <url> [-X method] [-d body] [-H header]...` | Probe 402 response; show accepted rails without paying |
Expand Down Expand Up @@ -425,9 +425,9 @@ The wallet holds USDC only — no ETH or SOL required. x402 (EIP-3009) and MPP T

### Mainnet

- **Base, Solana** — `agentscore-pay fund --chain base --amount 10` prints a [Coinbase Onramp](https://www.coinbase.com/onramp) URL (card → USDC on your chain) and an ASCII QR. Click the URL, or scan the QR from any mobile wallet with USDC to send yourself a transfer. `fund` polls balance and confirms when the deposit lands. Default amount is `$10` (~50-200 typical agent calls).
- **Tempo** — Coinbase Onramp does not cover Tempo. Fund by transferring USDC.e (chain 4217) from another Tempo wallet.
- **From an existing wallet (no onramp)** — `agentscore-pay wallet address --chain base` prints the address; send USDC on Base to it from MetaMask, Rabby, Coinbase Wallet, Phantom, or a CEX withdrawal. Same pattern on Solana + Tempo.
- **All chains** — `agentscore-pay fund --chain <chain> --amount 10` prints an ASCII QR and your wallet address, then polls balance until the deposit lands. Send USDC from any source: another wallet (MetaMask, Rabby, Coinbase Wallet, Phantom), a CEX withdrawal, or any third-party fiat onramp that supports the destination chain (Coinbase Pay, MoonPay, Transak, Onramper, Stripe Crypto Onramp, etc.). Default amount is `$10` (~50-200 typical agent calls).
- **Tempo specifics** — most fiat-onramp partners don't cover Tempo (chain 4217) yet. Fund by transferring USDC.e from another Tempo wallet, or via a bridge (LayerZero / Squid / Relay) from Base.
- **No-frills receive** — `agentscore-pay wallet address --chain base` prints just the address if you already know your funding source.

### Testnet

Expand Down
8 changes: 4 additions & 4 deletions src/cli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export function buildCli() {
description: 'Next steps:',
commands: [
{ command: 'passport login', description: 'Verify identity once — required for AgentScore-gated merchants. Skipped automatically for unregulated ones. ~30 seconds in browser, no money needed.' },
{ command: 'fund', options: { chain: true }, description: 'Top up a wallet via Coinbase Onramp + QR' },
{ command: 'fund', options: { chain: true }, description: 'Print a receive QR + poll for the deposit' },
{ command: 'balance', description: 'Confirm wallet balances' },
{ command: 'discover', description: 'Browse paid services in the x402 + MPP ecosystem' },
],
Expand Down Expand Up @@ -338,16 +338,16 @@ export function buildCli() {
// ── fund ────────────────────────────────────────────────────────────────────
cli.command('fund', {
description:
'Fund the wallet. Base/Solana mainnet: Coinbase Onramp URL + receive QR + balance polling. Tempo testnet: programmatic mint via tempo_fundAddress (free, no signup). Tempo mainnet: receive QR + balance polling.',
hint: 'Tempo testnet funds instantly via JSON-RPC — no browser required. Other networks open Coinbase Onramp in your browser.',
'Fund the wallet. Mainnet networks: receive QR + balance polling. Tempo testnet: programmatic mint via tempo_fundAddress (free, no signup). Base/Solana testnets: see the `faucet` command.',
hint: 'Tempo testnet funds instantly via JSON-RPC. Mainnet networks print a receive QR — send USDC from another wallet, exchange, or fiat onramp; pay polls until it lands.',
options: z.object({
chain: chainSchema,
network: networkSchema,
name: walletNameSchema,
amount: z.coerce.number().optional().describe('Target amount in USD (default 10)'),
}),
examples: [
{ options: { chain: 'base', amount: 10 }, description: 'Fund $10 USDC on Base via Coinbase Onramp' },
{ options: { chain: 'base', amount: 10 }, description: 'Print receive QR for $10 USDC on Base and poll for the deposit' },
{ options: { chain: 'tempo', network: 'testnet' }, description: 'Programmatically mint Tempo testnet stablecoins (free)' },
],
run(c) {
Expand Down
7 changes: 3 additions & 4 deletions src/commands/agent-guide.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@ const GUIDE: AgentGuide = {
command_example: 'agentscore-pay balance --json',
notes: [
'Pass --network testnet to check testnet balances (Base Sepolia, Solana devnet, Tempo testnet).',
'If a chain is empty, run `agentscore-pay fund --chain <chain>` for an onramp link or testnet faucet.',
'If a chain is empty, run `agentscore-pay fund --chain <chain>` for a receive QR (mainnet) or testnet faucet/programmatic mint.',
],
},
{
Expand Down Expand Up @@ -130,12 +130,11 @@ const GUIDE: AgentGuide = {
},
{
step: 'Get MAINNET USDC with `fund`',
why: '`fund` walks the user through Coinbase Onramp (Base + Solana mainnet) or prints a receive QR + balance polling (Tempo mainnet has no public onramp).',
why: '`fund` prints a receive QR for the wallet address and polls balance until USDC lands. The user funds from any source they prefer (CEX withdrawal, another wallet, fiat onramp).',
command_example: 'agentscore-pay fund --chain base --json',
notes: [
'Tempo TESTNET via `fund` calls the same programmatic mint as `faucet` — free, immediate, no browser. `fund --chain tempo --network testnet` works without prompts.',
'Tempo MAINNET via `fund` prints a receive QR; the user has to acquire USDC.e on Tempo via a CEX or bridge themselves (no onramp partner today).',
'Base/Solana mainnet `fund` opens Coinbase Onramp — the user completes a card or bank purchase, pay polls until USDC lands in the wallet.',
'All mainnet networks behave the same: receive QR + balance poll. The user picks the funding source — CEX, another wallet, or any third-party onramp that supports the destination chain.',
'Use `fund-estimate <URL>` to compute "how many calls does my current balance cover for this merchant" — useful before deciding whether to top up.',
],
},
Expand Down
6 changes: 1 addition & 5 deletions src/commands/fund.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { setTimeout as sleep } from 'timers/promises';
import * as baseChain from '../chains/base';
import * as solanaChain from '../chains/solana';
import * as tempoChain from '../chains/tempo';
import { onrampUrl, type Chain, type Network } from '../constants';
import { type Chain, type Network } from '../constants';
import { loadKeystore } from '../keystore';
import { DEFAULT_WALLET_NAME } from '../paths';

Expand All @@ -24,7 +24,6 @@ export interface FundResult {
address: string;
amount_usd: number | null;
status: 'deposit_detected' | 'tempo_testnet_minted' | 'tempo_testnet_mint_pending' | 'timeout';
onramp_url?: string | null;
qr_uri?: string;
initial_usdc?: string;
final_usdc?: string;
Expand Down Expand Up @@ -86,7 +85,6 @@ export async function fund(input: FundInput): Promise<FundResult> {
};
}

const onramp = network === 'mainnet' ? onrampUrl(input.chain, ks.address, input.amountUsd) : null;
const uri = buildQrUri(input.chain, ks.address, input.amountUsd, network);
const initial = await readBalance(input.chain, ks.address, network);
const deadline = Date.now() + DEFAULT_TIMEOUT_MS;
Expand All @@ -101,7 +99,6 @@ export async function fund(input: FundInput): Promise<FundResult> {
address: ks.address,
amount_usd: input.amountUsd ?? null,
status: 'deposit_detected',
onramp_url: onramp,
qr_uri: uri,
initial_usdc: formatBalance(input.chain, initial),
final_usdc: formatBalance(input.chain, current),
Expand All @@ -116,7 +113,6 @@ export async function fund(input: FundInput): Promise<FundResult> {
address: ks.address,
amount_usd: input.amountUsd ?? null,
status: 'timeout',
onramp_url: onramp,
qr_uri: uri,
initial_usdc: formatBalance(input.chain, initial),
final_usdc: formatBalance(input.chain, current),
Expand Down
6 changes: 1 addition & 5 deletions src/commands/wallet.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { onrampUrl, SUPPORTED_CHAINS, type Chain } from '../constants';
import { SUPPORTED_CHAINS, type Chain } from '../constants';
import { CliError } from '../errors';
import { decryptSecret, deleteKeystore, keystoreExists, listWallets, loadKeystore } from '../keystore';
import { deriveKey, generatePhrase, validatePhrase } from '../mnemonic';
Expand All @@ -25,7 +25,6 @@ export interface CreateResult {
created: boolean;
reason?: string;
qr_uri?: string;
onramp_url?: string | null;
}

export interface WalletCreateInput {
Expand Down Expand Up @@ -99,7 +98,6 @@ export async function walletCreate(input: WalletCreateInput = {}): Promise<Walle
keystore: keystorePath(c, name),
created: true,
qr_uri: getQrUri(wallet),
onramp_url: onrampUrl(c, wallet.address),
});
}
return { created, skipped: existing };
Expand Down Expand Up @@ -140,7 +138,6 @@ async function walletCreateMnemonic(chain?: Chain): Promise<WalletCreateResult>
keystore: keystorePath(c),
created: true,
qr_uri: getQrUri(wallet),
onramp_url: onrampUrl(c, wallet.address),
});
}
await saveMnemonic(phrase, passphrase, chains);
Expand Down Expand Up @@ -238,7 +235,6 @@ export async function walletImportMnemonic(input: WalletImportMnemonicInput): Pr
keystore: keystorePath(c),
created: true,
qr_uri: getQrUri(wallet),
onramp_url: onrampUrl(c, wallet.address),
});
}
await saveMnemonic(normalized, passphrase, chains);
Expand Down
14 changes: 0 additions & 14 deletions src/constants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,20 +58,6 @@ export const USDC = {
},
} as const;

export function onrampUrl(chain: Chain, address: string, amountUsd?: number): string | null {
if (chain === 'tempo') return null;
const network = chain === 'base' ? 'base' : 'solana';
const asset = 'USDC';
const base = 'https://pay.coinbase.com/buy/select-asset';
const params = new URLSearchParams({
defaultAsset: asset,
defaultNetwork: network,
addresses: JSON.stringify({ [address]: [network] }),
});
if (amountUsd && amountUsd > 0) params.set('presetFiatAmount', String(amountUsd));
return `${base}?${params.toString()}`;
}

export type EvmConfig = {
address: `0x${string}`;
decimals: number;
Expand Down
23 changes: 1 addition & 22 deletions tests/constants.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest';
import { isKnownUSDC, onrampUrl, SUPPORTED_CHAINS, USDC } from '../src/constants';
import { isKnownUSDC, SUPPORTED_CHAINS, USDC } from '../src/constants';

describe('constants', () => {
it('supports base, solana, tempo', () => {
Expand All @@ -13,27 +13,6 @@ describe('constants', () => {
expect(USDC.tempo.mainnet.address.toLowerCase()).toBe('0x20c000000000000000000000b9537d11c60e8b50');
});

it('onrampUrl returns Coinbase Pay URL for base', () => {
const url = onrampUrl('base', '0xABC');
expect(url).toContain('pay.coinbase.com');
expect(url).toContain('defaultNetwork=base');
expect(url).toContain(encodeURIComponent('0xABC'));
});

it('onrampUrl returns Coinbase Pay URL for solana', () => {
const url = onrampUrl('solana', '4Nd1mBQtrMJVYVfKf2PJy9NZUZdTAsp7D4xWLs4gDB4T');
expect(url).toContain('defaultNetwork=solana');
});

it('onrampUrl returns null for tempo', () => {
expect(onrampUrl('tempo', '0xABC')).toBeNull();
});

it('onrampUrl includes amount when provided', () => {
const url = onrampUrl('base', '0xABC', 50);
expect(url).toContain('presetFiatAmount=50');
});

describe('isKnownUSDC', () => {
it('matches base USDC mainnet + sepolia case-insensitively', () => {
expect(isKnownUSDC(USDC.base.mainnet.address, 'base')).toBe(true);
Expand Down
Loading