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
118 changes: 118 additions & 0 deletions packages/atxp/src/commands/topup.ts
Original file line number Diff line number Diff line change
@@ -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';

Check warning on line 5 in packages/atxp/src/commands/topup.ts

View workflow job for this annotation

GitHub Actions / test

'DEFAULT_ACCOUNTS_URL' is assigned a value but never used
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<void> {
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<string, string>;
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);
}
}
8 changes: 8 additions & 0 deletions packages/atxp/src/commands/whoami.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,8 @@ export async function whoamiCommand(): Promise<void> {
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
Expand All @@ -84,6 +86,12 @@ export async function whoamiCommand(): Promise<void> {
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})`));
}
Expand Down
4 changes: 4 additions & 0 deletions packages/atxp/src/help.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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('<command>') + ' ' + '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):'));
Expand Down Expand Up @@ -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:'));
Expand Down
7 changes: 6 additions & 1 deletion packages/atxp/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -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') {
Expand Down
Loading