diff --git a/.claude/CLAUDE.md b/.claude/CLAUDE.md index 3c78a09..2ad8f03 100644 --- a/.claude/CLAUDE.md +++ b/.claude/CLAUDE.md @@ -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 ` — 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 ` settle leg | diff --git a/README.md b/README.md index ed67a3f..4fc329d 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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 [-X method] [-d body] [-H header]...` | Probe a 402-gated URL and report how many calls your balance covers + top-up suggestion | | `check [-X method] [-d body] [-H header]...` | Probe 402 response; show accepted rails without paying | @@ -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 --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 diff --git a/src/cli.ts b/src/cli.ts index 0713375..173ac78 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -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' }, ], @@ -338,8 +338,8 @@ 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, @@ -347,7 +347,7 @@ export function buildCli() { 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) { diff --git a/src/commands/agent-guide.ts b/src/commands/agent-guide.ts index c519c58..d05510b 100644 --- a/src/commands/agent-guide.ts +++ b/src/commands/agent-guide.ts @@ -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 ` for an onramp link or testnet faucet.', + 'If a chain is empty, run `agentscore-pay fund --chain ` for a receive QR (mainnet) or testnet faucet/programmatic mint.', ], }, { @@ -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 ` to compute "how many calls does my current balance cover for this merchant" — useful before deciding whether to top up.', ], }, diff --git a/src/commands/fund.ts b/src/commands/fund.ts index 089b93b..dc4b7c7 100644 --- a/src/commands/fund.ts +++ b/src/commands/fund.ts @@ -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'; @@ -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; @@ -86,7 +85,6 @@ export async function fund(input: FundInput): Promise { }; } - 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; @@ -101,7 +99,6 @@ export async function fund(input: FundInput): Promise { 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), @@ -116,7 +113,6 @@ export async function fund(input: FundInput): Promise { 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), diff --git a/src/commands/wallet.ts b/src/commands/wallet.ts index 13635cd..e5cc87e 100644 --- a/src/commands/wallet.ts +++ b/src/commands/wallet.ts @@ -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'; @@ -25,7 +25,6 @@ export interface CreateResult { created: boolean; reason?: string; qr_uri?: string; - onramp_url?: string | null; } export interface WalletCreateInput { @@ -99,7 +98,6 @@ export async function walletCreate(input: WalletCreateInput = {}): Promise keystore: keystorePath(c), created: true, qr_uri: getQrUri(wallet), - onramp_url: onrampUrl(c, wallet.address), }); } await saveMnemonic(phrase, passphrase, chains); @@ -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); diff --git a/src/constants.ts b/src/constants.ts index 6935145..bfb48c8 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -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; diff --git a/tests/constants.test.ts b/tests/constants.test.ts index 9a49485..da6d8b2 100644 --- a/tests/constants.test.ts +++ b/tests/constants.test.ts @@ -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', () => { @@ -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);