diff --git a/packages/atxp/src/commands/topup.ts b/packages/atxp/src/commands/topup.ts new file mode 100644 index 0000000..1ea8e5c --- /dev/null +++ b/packages/atxp/src/commands/topup.ts @@ -0,0 +1,118 @@ +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/whoami.ts b/packages/atxp/src/commands/whoami.ts index d6e6068..9b68c60 100644 --- a/packages/atxp/src/commands/whoami.ts +++ b/packages/atxp/src/commands/whoami.ts @@ -66,6 +66,8 @@ export async function whoamiCommand(): Promise { displayName?: string; sources?: Array<{ chain: string; address: string }>; team?: { id: string; name: string; role: string }; + ownerEmail?: string; + isOrphan?: boolean; }; // Find the primary wallet address from sources @@ -84,6 +86,12 @@ export async function whoamiCommand(): Promise { if (wallet) { console.log(' ' + chalk.bold('Wallet:') + ' ' + wallet.address + chalk.gray(` (${wallet.chain})`)); } + if (data.ownerEmail) { + console.log(' ' + chalk.bold('Owner:') + ' ' + chalk.cyan(data.ownerEmail)); + } + if (data.isOrphan) { + console.log(' ' + chalk.bold('Owner:') + ' ' + chalk.gray('self-registered (no owner)')); + } if (data.team) { console.log(' ' + chalk.bold('Team:') + ' ' + data.team.name + chalk.gray(` (${data.team.role})`)); } diff --git a/packages/atxp/src/help.ts b/packages/atxp/src/help.ts index 640ac40..e8b2c44 100644 --- a/packages/atxp/src/help.ts +++ b/packages/atxp/src/help.ts @@ -28,6 +28,7 @@ export function showHelp(): void { console.log(' ' + chalk.cyan('fund') + ' ' + 'Show how to fund your account'); 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(); console.log(chalk.bold('PAAS (Platform as a Service):')); @@ -98,6 +99,9 @@ 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(); console.log(chalk.bold('PAAS Examples:')); diff --git a/packages/atxp/src/index.ts b/packages/atxp/src/index.ts index 48f3002..3bae424 100644 --- a/packages/atxp/src/index.ts +++ b/packages/atxp/src/index.ts @@ -18,6 +18,7 @@ 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'; interface DemoOptions { port: number; @@ -91,7 +92,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') { + if (helpFlag && command !== 'paas' && command !== 'email' && command !== 'agent' && command !== 'topup') { return { command: 'help', demoOptions: { port: 8017, dir: '', verbose: false, refresh: false }, @@ -331,6 +332,10 @@ async function main() { await agentCommand(subCommand || ''); break; + case 'topup': + await topupCommand(); + break; + case 'dev': // Dev subcommands (demo, create) if (subCommand === 'demo') {