diff --git a/packages/atxp/package.json b/packages/atxp/package.json index d4130b8..317f37e 100644 --- a/packages/atxp/package.json +++ b/packages/atxp/package.json @@ -19,8 +19,8 @@ "build": "tsc && cp -r src/vendor/*.cjs dist/vendor/", "dev": "tsx src/index.ts", "typecheck": "tsc --noEmit", - "lint": "eslint . --ext .ts", - "lint:fix": "eslint . --ext .ts --fix", + "lint": "eslint . --ext .ts --ignore-pattern 'src/vendor/'", + "lint:fix": "eslint . --ext .ts --fix --ignore-pattern 'src/vendor/'", "test": "vitest run", "prepublishOnly": "npm run build" }, diff --git a/packages/atxp/src/commands/deposit.ts b/packages/atxp/src/commands/deposit.ts index 85104d1..04a67ab 100644 --- a/packages/atxp/src/commands/deposit.ts +++ b/packages/atxp/src/commands/deposit.ts @@ -1,72 +1,142 @@ import chalk from 'chalk'; +import open from 'open'; import { getConnection } from '../config.js'; -const ACCOUNT_URL = 'https://accounts.atxp.ai/me'; +const DEFAULT_AMOUNT = 10; +const MIN_AMOUNT = 1; +const MAX_AMOUNT = 1000; -/** - * Extract the connection_token from the ATXP_CONNECTION string. - * Connection string format: https://accounts.atxp.ai?connection_token= - */ -function getConnectionToken(connectionString: string): string | null { - try { - const url = new URL(connectionString); - return url.searchParams.get('connection_token'); - } catch { - return null; - } -} - -export async function depositCommand(): Promise { +function getAccountsAuth(): { baseUrl: string; token: string } { const connection = getConnection(); - if (!connection) { console.error(chalk.red('Not logged in.')); console.error(`Run: ${chalk.cyan('npx atxp login')}`); process.exit(1); } - - const token = getConnectionToken(connection); + const url = new URL(connection); + const token = url.searchParams.get('connection_token'); if (!token) { - console.error(chalk.red('Error: Could not extract connection token.')); - console.error('Your ATXP_CONNECTION may be malformed. Try logging in again:'); - console.error(chalk.cyan(' npx atxp login --force')); + console.error(chalk.red('Invalid connection string: missing connection_token')); process.exit(1); } + return { baseUrl: `${url.protocol}//${url.host}`, token }; +} + +function getArgValue(flag: string): string | undefined { + const index = process.argv.findIndex((arg) => arg === flag); + return index !== -1 ? process.argv[index + 1] : undefined; +} + +function showFundHelp(): void { + console.log(chalk.bold('Fund Commands:')); + console.log(); + console.log(' ' + chalk.cyan('npx atxp fund') + ' ' + 'Show funding options for your account'); + console.log(' ' + chalk.cyan('npx atxp fund --amount 100') + ' ' + 'Request a $100 payment link (agents only)'); + console.log(); + console.log(chalk.bold('Options:')); + console.log(' ' + chalk.yellow('--amount') + ' ' + `Suggested amount in USD ($${MIN_AMOUNT}-$${MAX_AMOUNT}, default: $${DEFAULT_AMOUNT})`); + console.log(' ' + chalk.yellow('--open') + ' ' + 'Open the payment link in your browser'); + console.log(); + console.log(chalk.bold('How it works:')); + console.log(' Shows all available ways to fund your account:'); + console.log(' - Crypto deposit addresses (USDC on supported chains)'); + console.log(' - Payment link (agent accounts only) - shareable Stripe link'); + console.log(' The payer can adjust the amount at checkout.'); +} + +export async function depositCommand(): Promise { + if (process.argv.includes('--help') || process.argv.includes('-h')) { + showFundHelp(); + return; + } + + const { baseUrl, token } = getAccountsAuth(); + + // Parse amount for payment link + const amountStr = getArgValue('--amount'); + let amount = DEFAULT_AMOUNT; + if (amountStr) { + amount = parseFloat(amountStr); + if (isNaN(amount) || amount < MIN_AMOUNT || amount > MAX_AMOUNT) { + console.error(chalk.red(`Invalid amount: must be between $${MIN_AMOUNT} and $${MAX_AMOUNT}`)); + process.exit(1); + } + } + + const shouldOpen = process.argv.includes('--open'); + + console.log(chalk.gray('Fetching funding options...')); try { - const credentials = Buffer.from(`${token}:`).toString('base64'); - const response = await fetch(ACCOUNT_URL, { + const res = await fetch(`${baseUrl}/api/funding/fund`, { + method: 'POST', headers: { - 'Authorization': `Basic ${credentials}`, + 'Authorization': `Bearer ${token}`, 'Content-Type': 'application/json', }, + body: JSON.stringify({ amount }), }); - if (!response.ok) { - console.error(chalk.red(`Error: ${response.status} ${response.statusText}`)); + if (!res.ok) { + const body = await res.json().catch(() => ({})) as Record; + console.error(chalk.red(`Error: ${body.error || res.statusText}`)); process.exit(1); } - const data = await response.json(); + const data = await res.json() as { + paymentLink: { + url: string; + paymentLinkId: string; + suggestedAmount: number; + minAmount: number; + maxAmount: number; + } | null; + cryptoDeposit: Array<{ + address: string; + network: string; + currency: string; + }>; + }; - if (data.sources && Array.isArray(data.sources) && data.sources.length > 0) { + console.log(); + + // Display crypto deposit addresses + if (data.cryptoDeposit && data.cryptoDeposit.length > 0) { console.log(chalk.bold('Fund via USDC:')); - for (const source of data.sources) { - const chain = source.chain.charAt(0).toUpperCase() + source.chain.slice(1); + for (const source of data.cryptoDeposit) { + const chain = source.network.charAt(0).toUpperCase() + source.network.slice(1); console.log(` ${chalk.cyan(chain)}: ${source.address}`); } console.log(); - console.log(chalk.bold('Fund via credit card or other payment methods:')); - console.log(` ${chalk.underline('https://accounts.atxp.ai/fund')}`); - } else { - console.log(chalk.yellow('No deposit addresses found for this account.')); + } + + // Display payment link (agent accounts only) + if (data.paymentLink) { + console.log(chalk.bold('Fund via payment link:')); + console.log(' ' + chalk.bold('Suggested:') + ' ' + chalk.green(`$${data.paymentLink.suggestedAmount.toFixed(2)}`)); + console.log(' ' + chalk.bold('Range:') + ' ' + chalk.gray(`$${data.paymentLink.minAmount} - $${data.paymentLink.maxAmount}`)); + console.log(' ' + chalk.bold('URL:') + ' ' + chalk.cyan.underline(data.paymentLink.url)); + console.log(); + console.log(chalk.gray('Share this link with anyone to fund your account.')); + console.log(chalk.gray('The payer can adjust the amount at checkout.')); + + if (shouldOpen) { + console.log(); + console.log(chalk.gray('Opening in browser...')); + await open(data.paymentLink.url); + } + } + + // If neither is available + if ((!data.cryptoDeposit || data.cryptoDeposit.length === 0) && !data.paymentLink) { + console.log(chalk.yellow('No funding options found for this account.')); console.log(); console.log(chalk.bold('Fund via credit card or other payment methods:')); console.log(` ${chalk.underline('https://accounts.atxp.ai/fund')}`); } } catch (error) { const errorMessage = error instanceof Error ? error.message : String(error); - console.error(chalk.red(`Error fetching funding info: ${errorMessage}`)); + console.error(chalk.red(`Error fetching funding options: ${errorMessage}`)); process.exit(1); } } diff --git a/packages/atxp/src/commands/topup.ts b/packages/atxp/src/commands/topup.ts deleted file mode 100644 index 1ea8e5c..0000000 --- a/packages/atxp/src/commands/topup.ts +++ /dev/null @@ -1,118 +0,0 @@ -import chalk from 'chalk'; -import open from 'open'; -import { getConnection } from '../config.js'; - -const DEFAULT_ACCOUNTS_URL = 'https://accounts.atxp.ai'; -const DEFAULT_AMOUNT = 10; -const MIN_AMOUNT = 1; -const MAX_AMOUNT = 1000; - -function getAccountsAuth(): { baseUrl: string; token: string } { - const connection = getConnection(); - if (!connection) { - console.error(chalk.red('Not logged in.')); - console.error(`Run: ${chalk.cyan('npx atxp login')}`); - process.exit(1); - } - const url = new URL(connection); - const token = url.searchParams.get('connection_token'); - if (!token) { - console.error(chalk.red('Invalid connection string: missing connection_token')); - process.exit(1); - } - return { baseUrl: `${url.protocol}//${url.host}`, token }; -} - -function getArgValue(flag: string): string | undefined { - const index = process.argv.findIndex((arg) => arg === flag); - return index !== -1 ? process.argv[index + 1] : undefined; -} - -function showTopupHelp(): void { - console.log(chalk.bold('Top Up Commands:')); - console.log(); - console.log(' ' + chalk.cyan('npx atxp topup') + ' ' + 'Create a payment link ($50 suggested)'); - console.log(' ' + chalk.cyan('npx atxp topup --amount 100') + ' ' + 'Create a payment link suggesting $100'); - console.log(); - console.log(chalk.bold('Options:')); - console.log(' ' + chalk.yellow('--amount') + ' ' + `Suggested amount in USD ($${MIN_AMOUNT}-$${MAX_AMOUNT}, default: $${DEFAULT_AMOUNT})`); - console.log(' ' + chalk.yellow('--open') + ' ' + 'Open the payment link in your browser'); - console.log(); - console.log(chalk.bold('How it works:')); - console.log(' Creates a Stripe Payment Link for your agent account.'); - console.log(' The payer can adjust the amount at checkout.'); - console.log(' Share the link with anyone to fund your account.'); - console.log(' Funds are credited as IOU tokens once payment completes.'); -} - -export async function topupCommand(): Promise { - if (process.argv.includes('--help') || process.argv.includes('-h')) { - showTopupHelp(); - return; - } - - const { baseUrl, token } = getAccountsAuth(); - - // Parse amount - const amountStr = getArgValue('--amount'); - let amount = DEFAULT_AMOUNT; - if (amountStr) { - amount = parseFloat(amountStr); - if (isNaN(amount) || amount < MIN_AMOUNT || amount > MAX_AMOUNT) { - console.error(chalk.red(`Invalid amount: must be between $${MIN_AMOUNT} and $${MAX_AMOUNT}`)); - process.exit(1); - } - } - - const shouldOpen = process.argv.includes('--open'); - - console.log(chalk.gray(`Creating $${amount.toFixed(2)} payment link...`)); - - try { - const res = await fetch(`${baseUrl}/api/funding/payment-link`, { - method: 'POST', - headers: { - 'Authorization': `Bearer ${token}`, - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ amount }), - }); - - if (!res.ok) { - const body = await res.json().catch(() => ({})) as Record; - console.error(chalk.red(`Error: ${body.error || res.statusText}`)); - if (res.status === 403) { - console.error(chalk.gray('Payment links are only available for agent accounts.')); - } - process.exit(1); - } - - const data = await res.json() as { - url: string; - paymentLinkId: string; - suggestedAmount: number; - minAmount: number; - maxAmount: number; - }; - - console.log(); - console.log(chalk.green.bold('Payment link created!')); - console.log(); - console.log(' ' + chalk.bold('Suggested:') + ' ' + chalk.green(`$${data.suggestedAmount.toFixed(2)}`)); - console.log(' ' + chalk.bold('Range:') + ' ' + chalk.gray(`$${data.minAmount} - $${data.maxAmount}`)); - console.log(' ' + chalk.bold('URL:') + ' ' + chalk.cyan.underline(data.url)); - console.log(); - console.log(chalk.gray('Share this link to fund your agent account.')); - console.log(chalk.gray('The payer can adjust the amount at checkout.')); - - if (shouldOpen) { - console.log(); - console.log(chalk.gray('Opening in browser...')); - await open(data.url); - } - } catch (error) { - const errorMessage = error instanceof Error ? error.message : String(error); - console.error(chalk.red(`Error creating payment link: ${errorMessage}`)); - process.exit(1); - } -} diff --git a/packages/atxp/src/commands/transactions.ts b/packages/atxp/src/commands/transactions.ts new file mode 100644 index 0000000..8992c70 --- /dev/null +++ b/packages/atxp/src/commands/transactions.ts @@ -0,0 +1,94 @@ +import chalk from 'chalk'; +import { getConnection } from '../config.js'; + +const DEFAULT_ACCOUNTS_URL = 'https://accounts.atxp.ai'; + +/** + * Extract the connection_token from the ATXP_CONNECTION string. + * Connection string format: https://accounts.atxp.ai?connection_token= + */ +function getConnectionToken(connectionString: string): string | null { + try { + const url = new URL(connectionString); + return url.searchParams.get('connection_token'); + } catch { + return null; + } +} + +function getBaseUrl(connectionString: string): string { + try { + const url = new URL(connectionString); + return `${url.protocol}//${url.host}`; + } catch { + return DEFAULT_ACCOUNTS_URL; + } +} + +export async function transactionsCommand(): Promise { + const connection = getConnection(); + + if (!connection) { + console.error(chalk.red('Not logged in.')); + console.error(`Run: ${chalk.cyan('npx atxp login')}`); + process.exit(1); + } + + const token = getConnectionToken(connection); + if (!token) { + console.error(chalk.red('Error: Could not extract connection token.')); + console.error('Your ATXP_CONNECTION may be malformed. Try logging in again:'); + console.error(chalk.cyan(' npx atxp login --force')); + process.exit(1); + } + + // Parse --limit flag (default 10) + const limitIndex = process.argv.findIndex((arg) => arg === '--limit' || arg === '-l'); + const limitValue = limitIndex !== -1 ? process.argv[limitIndex + 1] : undefined; + const limit = limitValue ? parseInt(limitValue, 10) : 10; + + const baseUrl = getBaseUrl(connection); + + try { + const credentials = Buffer.from(`${token}:`).toString('base64'); + const response = await fetch(`${baseUrl}/api/transactions?limit=${limit}`, { + headers: { + 'Authorization': `Basic ${credentials}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + console.error(chalk.red(`Error: ${response.status} ${response.statusText}`)); + process.exit(1); + } + + const data = await response.json(); + + const transactions = Array.isArray(data.transactions) + ? data.transactions + : Array.isArray(data) + ? data + : []; + + if (transactions.length === 0) { + console.log(chalk.gray('No transactions found.')); + return; + } + + console.log(chalk.bold(`Recent transactions (${transactions.length}):\n`)); + for (const tx of transactions) { + const amount = tx.amount != null ? `$${(+tx.amount).toFixed(2)}` : ''; + const type = tx.type || ''; + const description = tx.description || ''; + const date = tx.createdAt ? new Date(tx.createdAt).toLocaleDateString() : ''; + console.log( + ` ${chalk.gray(date)} ${chalk.yellow(type.padEnd(12))} ${chalk.green(amount.padStart(8))} ${description}` + ); + } + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error); + console.error(chalk.red(`Error fetching transactions: ${errorMessage}`)); + process.exit(1); + } +} diff --git a/packages/atxp/src/help.ts b/packages/atxp/src/help.ts index 2194f89..576717f 100644 --- a/packages/atxp/src/help.ts +++ b/packages/atxp/src/help.ts @@ -25,11 +25,11 @@ export function showHelp(): void { console.log(' ' + chalk.cyan('x') + ' ' + chalk.yellow('') + ' ' + 'Search X/Twitter'); console.log(' ' + chalk.cyan('email') + ' ' + chalk.yellow('') + ' ' + 'Send and receive emails'); console.log(' ' + chalk.cyan('balance') + ' ' + 'Check your ATXP account balance'); - console.log(' ' + chalk.cyan('fund') + ' ' + 'Show how to fund your account'); + console.log(' ' + chalk.cyan('fund') + ' ' + chalk.yellow('[options]') + ' ' + 'Show funding options (crypto + payment links)'); console.log(' ' + chalk.cyan('whoami') + ' ' + 'Show your account info (ID, email, wallet)'); console.log(' ' + chalk.cyan('agent') + ' ' + chalk.yellow('') + ' ' + 'Create and manage agent accounts'); - console.log(' ' + chalk.cyan('topup') + ' ' + chalk.yellow('[options]') + ' ' + 'Create a payment link to fund your agent'); console.log(' ' + chalk.cyan('memory') + ' ' + chalk.yellow('') + ' ' + 'Manage, search, and back up agent memory files'); + console.log(' ' + chalk.cyan('transactions') + ' ' + chalk.yellow('[options]') + ' ' + 'View recent transaction history'); console.log(); console.log(chalk.bold('PAAS (Platform as a Service):')); @@ -90,7 +90,7 @@ export function showHelp(): void { console.log(' npx atxp email claim-username myname # Claim a username ($1.00)'); console.log(' npx atxp email release-username # Release your username'); console.log(' npx atxp balance # Check account balance'); - console.log(' npx atxp fund # Show how to fund your account'); + console.log(' npx atxp fund # Show all funding options'); console.log(' npx atxp whoami # Show account info'); console.log(' npx atxp dev demo # Run the demo'); console.log(' npx atxp dev create my-app # Create a new project'); @@ -100,9 +100,11 @@ export function showHelp(): void { console.log(' npx atxp agent create # Create a new agent (requires login)'); console.log(' npx atxp agent list # List your agents (requires login)'); console.log(' npx atxp agent register # Self-register as an agent (no login)'); - console.log(' npx atxp topup # Create a $10 payment link'); - console.log(' npx atxp topup --amount 100 # Create a $100 payment link'); - console.log(' npx atxp topup --amount 25 --open # Create link and open in browser'); + console.log(' npx atxp fund # Show all funding options'); + console.log(' npx atxp fund --amount 100 # Request a $100 payment link'); + console.log(' npx atxp fund --amount 25 --open # Create link and open in browser'); + console.log(' npx atxp transactions # Show last 10 transactions'); + console.log(' npx atxp transactions --limit 20 # Show last 20 transactions'); console.log(); console.log(chalk.bold('Memory Examples:')); diff --git a/packages/atxp/src/index.ts b/packages/atxp/src/index.ts index bfb017c..f02b071 100644 --- a/packages/atxp/src/index.ts +++ b/packages/atxp/src/index.ts @@ -18,8 +18,9 @@ import { depositCommand } from './commands/deposit.js'; import { paasCommand } from './commands/paas/index.js'; import { agentCommand } from './commands/agent.js'; import { whoamiCommand } from './commands/whoami.js'; -import { topupCommand } from './commands/topup.js'; + import { memoryCommand, type MemoryOptions } from './commands/memory.js'; +import { transactionsCommand } from './commands/transactions.js'; interface DemoOptions { port: number; @@ -94,7 +95,7 @@ function parseArgs(): { // Check for help flags early - but NOT for paas or email commands (they handle --help internally) const helpFlag = process.argv.includes('--help') || process.argv.includes('-h'); - if (helpFlag && command !== 'paas' && command !== 'email' && command !== 'agent' && command !== 'topup' && command !== 'memory' && command !== 'backup') { + if (helpFlag && command !== 'paas' && command !== 'email' && command !== 'agent' && command !== 'fund' && command !== 'deposit' && command !== 'memory' && command !== 'backup') { return { command: 'help', demoOptions: { port: 8017, dir: '', verbose: false, refresh: false }, @@ -342,10 +343,6 @@ async function main() { await agentCommand(subCommand || ''); break; - case 'topup': - await topupCommand(); - break; - case 'memory': await memoryCommand(subCommand || '', memoryOptions, process.argv.slice(4).filter((arg) => !arg.startsWith('-')).join(' ')); break; @@ -355,6 +352,10 @@ async function main() { await memoryCommand(subCommand || '', memoryOptions, process.argv.slice(4).filter((arg) => !arg.startsWith('-')).join(' ')); break; + case 'transactions': + await transactionsCommand(); + break; + case 'dev': // Dev subcommands (demo, create) if (subCommand === 'demo') { diff --git a/skills/atxp/SKILL.md b/skills/atxp/SKILL.md index ef9d167..aa57c22 100644 --- a/skills/atxp/SKILL.md +++ b/skills/atxp/SKILL.md @@ -239,6 +239,30 @@ npx atxp@latest fund Or fund with credit card and other standard payment methods at https://accounts.atxp.ai/fund. +## Balance Management + +Check your balance before starting paid work and after completing expensive tasks. When your balance drops below $2: + +1. Run `npx atxp@latest balance` to get your current balance. +2. Run `npx atxp@latest transactions --limit 5` to see what you've been spending on. +3. Run `npx atxp@latest fund` to get your funding options — this returns both a Stripe payment link (fiat) and USDC deposit addresses (crypto, on Base and Solana). +4. Run `npx atxp@latest whoami` to find your owner's email (`ownerEmail` field). +5. Choose the right funding method for your audience: + - **Sending to a human owner or non-technical user?** Share the Stripe payment link — they can pay with credit card and adjust the amount ($1–$1,000). + - **Sending to another agent or crypto-native user?** Share the USDC deposit address for their preferred chain. +6. Email your owner with your balance, what you've been spending on, and the funding option: + +```bash +npx atxp@latest email send \ + --to \ + --subject "ATXP balance low — funding link inside" \ + --body "Hi, my ATXP balance is $X.XX. I've been using it for [web searches, image generation, LLM calls, etc.]. Here's how to top up — you can adjust the amount at checkout: . No subscription, just pay-as-you-go." +``` + +**When to check:** Before a batch of paid API calls, after completing a task that used multiple paid tools, or at the start of each new conversation session. + +**Cost awareness:** Web searches, image/video/music generation, X/Twitter searches, LLM calls, and outbound emails all cost credits. Balance checks, `whoami`, `fund`, `topup`, `transactions`, inbox checks, and email reads are free. + ## Commands Reference ### Account & Wallet @@ -250,6 +274,8 @@ Or fund with credit card and other standard payment methods at https://accounts. | `npx atxp@latest fund` | Free | Show funding options | | `npx atxp@latest topup` | Free | Generate Stripe payment link | | `npx atxp@latest topup --amount ` | Free | Payment link with suggested amount | +| `npx atxp@latest transactions` | Free | View recent transaction history | +| `npx atxp@latest transactions --limit ` | Free | Show last N transactions | ### Agent Management