From 3b2694559100504e36cd28f545ee821126960286 Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Mon, 26 Jan 2026 16:39:57 -0800 Subject: [PATCH 1/2] Add Grid CLI commands Commands for interacting with the Grid API: - config: Get/update platform configuration - customers: List, get, create, update, delete customers and generate KYC links - accounts: List internal accounts (balances), list/create external accounts - quotes: List, get, create, execute cross-currency transfer quotes - transactions: List, get, approve, reject transactions - transfers: Same-currency transfer-in and transfer-out - receiver: Look up UMA addresses and external accounts - sandbox: Fund accounts, simulate send/receive in sandbox mode --- cli/package-lock.json | 251 +++++++++++++++++++++++++++++++ cli/src/client.ts | 6 +- cli/src/commands/accounts.ts | 212 ++++++++++++++++++++++++++ cli/src/commands/config.ts | 55 +++++++ cli/src/commands/customers.ts | 189 +++++++++++++++++++++++ cli/src/commands/quotes.ts | 165 ++++++++++++++++++++ cli/src/commands/receiver.ts | 66 ++++++++ cli/src/commands/sandbox.ts | 73 +++++++++ cli/src/commands/transactions.ts | 114 ++++++++++++++ cli/src/commands/transfers.ts | 71 +++++++++ cli/src/index.ts | 29 +++- 11 files changed, 1229 insertions(+), 2 deletions(-) create mode 100644 cli/package-lock.json create mode 100644 cli/src/commands/accounts.ts create mode 100644 cli/src/commands/config.ts create mode 100644 cli/src/commands/customers.ts create mode 100644 cli/src/commands/quotes.ts create mode 100644 cli/src/commands/receiver.ts create mode 100644 cli/src/commands/sandbox.ts create mode 100644 cli/src/commands/transactions.ts create mode 100644 cli/src/commands/transfers.ts diff --git a/cli/package-lock.json b/cli/package-lock.json new file mode 100644 index 0000000..0ec237b --- /dev/null +++ b/cli/package-lock.json @@ -0,0 +1,251 @@ +{ + "name": "grid-cli", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "grid-cli", + "version": "1.0.0", + "license": "Apache-2.0", + "dependencies": { + "commander": "^12.1.0" + }, + "bin": { + "grid": "dist/index.js" + }, + "devDependencies": { + "@types/node": "^20.0.0", + "ts-node": "^10.9.2", + "typescript": "^5.4.0" + } + }, + "node_modules/@cspotcode/source-map-support": { + "version": "0.8.1", + "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", + "integrity": "sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "0.3.9" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.9.tgz", + "integrity": "sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.0.3", + "@jridgewell/sourcemap-codec": "^1.4.10" + } + }, + "node_modules/@tsconfig/node10": { + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@tsconfig/node10/-/node10-1.0.12.tgz", + "integrity": "sha512-UCYBaeFvM11aU2y3YPZ//O5Rhj+xKyzy7mvcIoAjASbigy8mHMryP5cK7dgjlz2hWxh1g5pLw084E0a/wlUSFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node12": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@tsconfig/node12/-/node12-1.0.11.tgz", + "integrity": "sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node14": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@tsconfig/node14/-/node14-1.0.3.tgz", + "integrity": "sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tsconfig/node16": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/@tsconfig/node16/-/node16-1.0.4.tgz", + "integrity": "sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.30", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.30.tgz", + "integrity": "sha512-WJtwWJu7UdlvzEAUm484QNg5eAoq5QR08KDNx7g45Usrs2NtOPiX8ugDqmKdXkyL03rBqU5dYNYVQetEpBHq2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/acorn": { + "version": "8.15.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", + "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-walk": { + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", + "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.11.0" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/arg": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", + "integrity": "sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "12.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-12.1.0.tgz", + "integrity": "sha512-Vw8qHK3bZM9y/P10u3Vib8o/DdkvA2OtPtZvD871QKjy74Wj1WSKFILMPRPSdUSx5RFK1arlJzEtA4PkFgnbuA==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/create-require": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", + "integrity": "sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/diff": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-4.0.4.tgz", + "integrity": "sha512-X07nttJQkwkfKfvTPG/KSnE2OMdcUCao6+eXF3wmnIQRn2aPAHH3VxDbDOdegkd6JbPsXqShpvEOHfAT+nCNwQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/make-error": { + "version": "1.3.6", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", + "integrity": "sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==", + "dev": true, + "license": "ISC" + }, + "node_modules/ts-node": { + "version": "10.9.2", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.2.tgz", + "integrity": "sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cspotcode/source-map-support": "^0.8.0", + "@tsconfig/node10": "^1.0.7", + "@tsconfig/node12": "^1.0.7", + "@tsconfig/node14": "^1.0.0", + "@tsconfig/node16": "^1.0.2", + "acorn": "^8.4.1", + "acorn-walk": "^8.1.1", + "arg": "^4.1.0", + "create-require": "^1.1.0", + "diff": "^4.0.1", + "make-error": "^1.1.1", + "v8-compile-cache-lib": "^3.0.1", + "yn": "3.1.1" + }, + "bin": { + "ts-node": "dist/bin.js", + "ts-node-cwd": "dist/bin-cwd.js", + "ts-node-esm": "dist/bin-esm.js", + "ts-node-script": "dist/bin-script.js", + "ts-node-transpile-only": "dist/bin-transpile.js", + "ts-script": "dist/bin-script-deprecated.js" + }, + "peerDependencies": { + "@swc/core": ">=1.2.50", + "@swc/wasm": ">=1.2.50", + "@types/node": "*", + "typescript": ">=2.7" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "@swc/wasm": { + "optional": true + } + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/v8-compile-cache-lib": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz", + "integrity": "sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==", + "dev": true, + "license": "MIT" + }, + "node_modules/yn": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yn/-/yn-3.1.1.tgz", + "integrity": "sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + } + } +} diff --git a/cli/src/client.ts b/cli/src/client.ts index 30acc86..d97bc81 100644 --- a/cli/src/client.ts +++ b/cli/src/client.ts @@ -34,7 +34,11 @@ export class GridClient { path: string, params?: Record ): string { - const url = new URL(path, this.config.baseUrl); + const baseUrl = this.config.baseUrl.endsWith("/") + ? this.config.baseUrl + : this.config.baseUrl + "/"; + const fullPath = path.startsWith("/") ? path.slice(1) : path; + const url = new URL(fullPath, baseUrl); if (params) { Object.entries(params).forEach(([key, value]) => { if (value !== undefined) { diff --git a/cli/src/commands/accounts.ts b/cli/src/commands/accounts.ts new file mode 100644 index 0000000..6a0d720 --- /dev/null +++ b/cli/src/commands/accounts.ts @@ -0,0 +1,212 @@ +import { Command } from "commander"; +import { GridClient, PaginatedResponse } from "../client"; +import { outputResponse } from "../output"; +import { GlobalOptions } from "../index"; + +interface InternalAccount { + id: string; + customerId?: string; + currency: string; + balance: number; + availableBalance: number; + status: string; + paymentInstructions?: unknown; + createdAt: string; + updatedAt: string; +} + +interface ExternalAccount { + id: string; + customerId: string; + currency: string; + accountInfo: { + accountType: string; + [key: string]: unknown; + }; + status: string; + createdAt: string; + updatedAt: string; +} + +export function registerAccountsCommand( + program: Command, + getClient: (opts: GlobalOptions) => GridClient | null +): void { + const accountsCmd = program + .command("accounts") + .description("Account management commands"); + + const internalCmd = accountsCmd + .command("internal") + .description("Internal account commands"); + + internalCmd + .command("list") + .description("List internal accounts (balances)") + .option("-l, --limit ", "Maximum results (default 20, max 100)", "20") + .option("--cursor ", "Pagination cursor") + .option("--customer-id ", "Filter by customer ID") + .option("--currency ", "Filter by currency code") + .option("--platform", "List platform internal accounts instead of customer accounts") + .action(async (options) => { + const opts = program.opts(); + const client = getClient(opts); + if (!client) return; + + const params: Record = { + limit: parseInt(options.limit), + cursor: options.cursor, + customerId: options.customerId, + currency: options.currency, + }; + + const endpoint = options.platform + ? "/platform/internal-accounts" + : "/customers/internal-accounts"; + + const response = await client.get>( + endpoint, + params + ); + outputResponse(response); + }); + + const externalCmd = accountsCmd + .command("external") + .description("External account commands"); + + externalCmd + .command("list") + .description("List external accounts") + .option("-l, --limit ", "Maximum results (default 20, max 100)", "20") + .option("--cursor ", "Pagination cursor") + .option("--customer-id ", "Filter by customer ID") + .option("--currency ", "Filter by currency code") + .option("--platform", "List platform external accounts instead of customer accounts") + .action(async (options) => { + const opts = program.opts(); + const client = getClient(opts); + if (!client) return; + + const params: Record = { + limit: parseInt(options.limit), + cursor: options.cursor, + customerId: options.customerId, + currency: options.currency, + }; + + const endpoint = options.platform + ? "/platform/external-accounts" + : "/customers/external-accounts"; + + const response = await client.get>( + endpoint, + params + ); + outputResponse(response); + }); + + externalCmd + .command("create") + .description("Create an external account") + .requiredOption("--customer-id ", "Customer ID") + .requiredOption("--currency ", "Currency code (USD, MXN, BRL, EUR, etc.)") + .requiredOption("--account-type ", "Account type (US_ACCOUNT, CLABE, PIX, IBAN, UPI, NGN_ACCOUNT, SPARK_WALLET, etc.)") + .option("--account-number ", "Account number (for US_ACCOUNT, NGN_ACCOUNT)") + .option("--routing-number ", "Routing number (for US_ACCOUNT)") + .option("--account-category ", "Account category: CHECKING or SAVINGS (for US_ACCOUNT)") + .option("--clabe ", "CLABE number (for Mexico)") + .option("--pix-key ", "PIX key (for Brazil)") + .option("--iban ", "IBAN (for Europe)") + .option("--upi-id ", "UPI ID (for India)") + .option("--bank-name ", "Bank name (for NGN_ACCOUNT)") + .option("--purpose ", "Purpose of payment (for NGN_ACCOUNT): GIFT, SELF, GOODS_OR_SERVICES, EDUCATION, etc.") + .option("--address ", "Wallet address (for SPARK_WALLET, SOLANA_WALLET, etc.)") + .option("--beneficiary-type ", "Beneficiary type: INDIVIDUAL or BUSINESS") + .option("--beneficiary-name ", "Beneficiary full name (individual) or legal name (business)") + .option("--beneficiary-birth-date ", "Beneficiary birth date YYYY-MM-DD (individual)") + .option("--beneficiary-nationality ", "Beneficiary nationality country code (individual)") + .option("--beneficiary-address-line1 ", "Beneficiary address line 1") + .option("--beneficiary-address-city ", "Beneficiary city") + .option("--beneficiary-address-state ", "Beneficiary state") + .option("--beneficiary-address-postal ", "Beneficiary postal code") + .option("--beneficiary-address-country ", "Beneficiary country code") + .action(async (options) => { + const opts = program.opts(); + const client = getClient(opts); + if (!client) return; + + const accountInfo: Record = { + accountType: options.accountType, + }; + + switch (options.accountType) { + case "US_ACCOUNT": + if (options.accountNumber) accountInfo.accountNumber = options.accountNumber; + if (options.routingNumber) accountInfo.routingNumber = options.routingNumber; + if (options.accountCategory) accountInfo.accountCategory = options.accountCategory; + break; + case "CLABE": + if (options.clabe) accountInfo.clabeNumber = options.clabe; + break; + case "PIX": + if (options.pixKey) accountInfo.pixKey = options.pixKey; + break; + case "IBAN": + if (options.iban) accountInfo.iban = options.iban; + break; + case "UPI": + if (options.upiId) accountInfo.vpa = options.upiId; + break; + case "NGN_ACCOUNT": + if (options.accountNumber) accountInfo.accountNumber = options.accountNumber; + if (options.bankName) accountInfo.bankName = options.bankName; + if (options.purpose) accountInfo.purposeOfPayment = options.purpose; + break; + case "SPARK_WALLET": + case "SOLANA_WALLET": + case "TRON_WALLET": + case "POLYGON_WALLET": + case "BASE_WALLET": + if (options.address) accountInfo.address = options.address; + break; + } + + if (options.beneficiaryType || options.beneficiaryName) { + const beneficiary: Record = {}; + if (options.beneficiaryType) beneficiary.beneficiaryType = options.beneficiaryType; + + if (options.beneficiaryType === "INDIVIDUAL") { + if (options.beneficiaryName) beneficiary.fullName = options.beneficiaryName; + if (options.beneficiaryBirthDate) beneficiary.birthDate = options.beneficiaryBirthDate; + if (options.beneficiaryNationality) beneficiary.nationality = options.beneficiaryNationality; + } else if (options.beneficiaryType === "BUSINESS") { + if (options.beneficiaryName) beneficiary.legalName = options.beneficiaryName; + } + + if (options.beneficiaryAddressLine1 || options.beneficiaryAddressCity) { + beneficiary.address = { + line1: options.beneficiaryAddressLine1, + city: options.beneficiaryAddressCity, + state: options.beneficiaryAddressState, + postalCode: options.beneficiaryAddressPostal, + country: options.beneficiaryAddressCountry, + }; + } + + accountInfo.beneficiary = beneficiary; + } + + const body = { + customerId: options.customerId, + currency: options.currency, + accountInfo, + }; + + const response = await client.post( + "/customers/external-accounts", + body + ); + outputResponse(response); + }); +} diff --git a/cli/src/commands/config.ts b/cli/src/commands/config.ts new file mode 100644 index 0000000..8638f20 --- /dev/null +++ b/cli/src/commands/config.ts @@ -0,0 +1,55 @@ +import { Command } from "commander"; +import { GridClient } from "../client"; +import { outputResponse } from "../output"; +import { GlobalOptions } from "../index"; + +interface PlatformConfig { + id: string; + umaDomain?: string; + webhookEndpoint?: string; + supportedCurrencies: Array<{ + currencyCode: string; + minAmount?: number; + maxAmount?: number; + enabledTransactionTypes?: string[]; + }>; +} + +export function registerConfigCommand( + program: Command, + getClient: (opts: GlobalOptions) => GridClient | null +): void { + const configCmd = program + .command("config") + .description("Platform configuration commands"); + + configCmd + .command("get") + .description("Get platform configuration (currencies, limits, webhook)") + .action(async () => { + const opts = program.opts(); + const client = getClient(opts); + if (!client) return; + + const response = await client.get("/config"); + outputResponse(response); + }); + + configCmd + .command("update") + .description("Update platform configuration") + .option("--uma-domain ", "UMA domain") + .option("--webhook-endpoint ", "Webhook endpoint URL") + .action(async (options) => { + const opts = program.opts(); + const client = getClient(opts); + if (!client) return; + + const body: Record = {}; + if (options.umaDomain) body.umaDomain = options.umaDomain; + if (options.webhookEndpoint) body.webhookEndpoint = options.webhookEndpoint; + + const response = await client.patch("/config", body); + outputResponse(response); + }); +} diff --git a/cli/src/commands/customers.ts b/cli/src/commands/customers.ts new file mode 100644 index 0000000..dfe5195 --- /dev/null +++ b/cli/src/commands/customers.ts @@ -0,0 +1,189 @@ +import { Command } from "commander"; +import { GridClient, PaginatedResponse } from "../client"; +import { outputResponse, formatError, output } from "../output"; +import { GlobalOptions } from "../index"; + +interface Customer { + id: string; + platformCustomerId: string; + customerType: "INDIVIDUAL" | "BUSINESS"; + umaAddress?: string; + fullName?: string; + birthDate?: string; + kycStatus?: string; + createdAt: string; + updatedAt: string; +} + +export function registerCustomersCommand( + program: Command, + getClient: (opts: GlobalOptions) => GridClient | null +): void { + const customersCmd = program + .command("customers") + .description("Customer management commands"); + + customersCmd + .command("list") + .description("List customers") + .option("-l, --limit ", "Maximum results (default 20, max 100)", "20") + .option("--cursor ", "Pagination cursor") + .option("--platform-id ", "Filter by platform customer ID") + .option("--type ", "Filter by type (INDIVIDUAL or BUSINESS)") + .option("--uma-address
", "Filter by UMA address") + .action(async (options) => { + const opts = program.opts(); + const client = getClient(opts); + if (!client) return; + + const params: Record = { + limit: parseInt(options.limit), + cursor: options.cursor, + platformCustomerId: options.platformId, + customerType: options.type, + umaAddress: options.umaAddress, + }; + + const response = await client.get>( + "/customers", + params + ); + outputResponse(response); + }); + + customersCmd + .command("get ") + .description("Get customer details") + .action(async (customerId: string) => { + const opts = program.opts(); + const client = getClient(opts); + if (!client) return; + + const response = await client.get(`/customers/${customerId}`); + outputResponse(response); + }); + + customersCmd + .command("create") + .description("Create a new customer") + .requiredOption("--platform-id ", "Platform-specific customer ID") + .option("--type ", "Customer type (INDIVIDUAL or BUSINESS)", "INDIVIDUAL") + .option("--uma-address
", "UMA address (optional, generated if not provided)") + .option("--full-name ", "Full name (for individuals)") + .option("--birth-date ", "Birth date YYYY-MM-DD (for individuals)") + .option("--legal-name ", "Legal name (for businesses)") + .option("--registration-number ", "Registration number (for businesses)") + .option("--tax-id ", "Tax ID (for businesses)") + .option("--address-line1 ", "Address line 1") + .option("--address-city ", "City") + .option("--address-state ", "State/Province") + .option("--address-postal ", "Postal code") + .option("--address-country ", "Country code (e.g., US)") + .action(async (options) => { + const opts = program.opts(); + const client = getClient(opts); + if (!client) return; + + const body: Record = { + platformCustomerId: options.platformId, + customerType: options.type, + }; + + if (options.umaAddress) body.umaAddress = options.umaAddress; + + if (options.type === "INDIVIDUAL") { + if (options.fullName) body.fullName = options.fullName; + if (options.birthDate) body.birthDate = options.birthDate; + } else if (options.type === "BUSINESS") { + const businessInfo: Record = {}; + if (options.legalName) businessInfo.legalName = options.legalName; + if (options.registrationNumber) + businessInfo.registrationNumber = options.registrationNumber; + if (options.taxId) businessInfo.taxId = options.taxId; + body.businessInfo = businessInfo; + } + + if (options.addressLine1 || options.addressCity) { + body.address = { + line1: options.addressLine1, + city: options.addressCity, + state: options.addressState, + postalCode: options.addressPostal, + country: options.addressCountry, + }; + } + + const response = await client.post("/customers", body); + outputResponse(response); + }); + + customersCmd + .command("update ") + .description("Update a customer") + .option("--full-name ", "Full name") + .option("--birth-date ", "Birth date YYYY-MM-DD") + .option("--address-line1 ", "Address line 1") + .option("--address-city ", "City") + .option("--address-state ", "State/Province") + .option("--address-postal ", "Postal code") + .option("--address-country ", "Country code") + .action(async (customerId: string, options) => { + const opts = program.opts(); + const client = getClient(opts); + if (!client) return; + + const body: Record = {}; + if (options.fullName) body.fullName = options.fullName; + if (options.birthDate) body.birthDate = options.birthDate; + + if (options.addressLine1 || options.addressCity) { + body.address = { + line1: options.addressLine1, + city: options.addressCity, + state: options.addressState, + postalCode: options.addressPostal, + country: options.addressCountry, + }; + } + + const response = await client.patch( + `/customers/${customerId}`, + body + ); + outputResponse(response); + }); + + customersCmd + .command("delete ") + .description("Delete a customer") + .action(async (customerId: string) => { + const opts = program.opts(); + const client = getClient(opts); + if (!client) return; + + const response = await client.delete(`/customers/${customerId}`); + outputResponse(response); + }); + + customersCmd + .command("kyc-link") + .description("Generate a KYC link for a customer") + .requiredOption("--customer-id ", "Customer ID") + .requiredOption("--redirect-url ", "Redirect URL after KYC completion") + .action(async (options) => { + const opts = program.opts(); + const client = getClient(opts); + if (!client) return; + + const body = { + customerId: options.customerId, + redirectUrl: options.redirectUrl, + }; + + const response = await client.post<{ kycUrl: string }>( + "/customers/kyc-link", + body + ); + outputResponse(response); + }); +} diff --git a/cli/src/commands/quotes.ts b/cli/src/commands/quotes.ts new file mode 100644 index 0000000..152e235 --- /dev/null +++ b/cli/src/commands/quotes.ts @@ -0,0 +1,165 @@ +import { Command } from "commander"; +import { GridClient, PaginatedResponse } from "../client"; +import { outputResponse } from "../output"; +import { GlobalOptions } from "../index"; + +interface Quote { + id: string; + status: "PENDING" | "PROCESSING" | "COMPLETED" | "FAILED" | "EXPIRED"; + source: { + accountId?: string; + customerId?: string; + currency: string; + }; + destination: { + accountId?: string; + umaAddress?: string; + currency: string; + }; + lockedCurrencySide: "SENDING" | "RECEIVING"; + lockedCurrencyAmount: number; + sendingAmount: number; + sendingCurrency: string; + receivingAmount: number; + receivingCurrency: string; + exchangeRate: number; + fees?: Array<{ + type: string; + amount: number; + currency: string; + }>; + expiresAt: string; + createdAt: string; + updatedAt: string; +} + +export function registerQuotesCommand( + program: Command, + getClient: (opts: GlobalOptions) => GridClient | null +): void { + const quotesCmd = program + .command("quotes") + .description("Quote management commands"); + + quotesCmd + .command("list") + .description("List transfer quotes") + .option("-l, --limit ", "Maximum results (default 20, max 100)", "20") + .option("--cursor ", "Pagination cursor") + .option("--customer-id ", "Filter by sending customer ID") + .option("--sending-account ", "Filter by sending account ID") + .option("--receiving-account ", "Filter by receiving account ID") + .option("--sending-uma
", "Filter by sending UMA address") + .option("--receiving-uma
", "Filter by receiving UMA address") + .option("--status ", "Filter by status (PENDING, PROCESSING, COMPLETED, FAILED, EXPIRED)") + .action(async (options) => { + const opts = program.opts(); + const client = getClient(opts); + if (!client) return; + + const params: Record = { + limit: parseInt(options.limit), + cursor: options.cursor, + customerId: options.customerId, + sendingAccountId: options.sendingAccount, + receivingAccountId: options.receivingAccount, + sendingUmaAddress: options.sendingUma, + receivingUmaAddress: options.receivingUma, + status: options.status, + }; + + const response = await client.get>( + "/quotes", + params + ); + outputResponse(response); + }); + + quotesCmd + .command("get ") + .description("Get quote details") + .action(async (quoteId: string) => { + const opts = program.opts(); + const client = getClient(opts); + if (!client) return; + + const response = await client.get(`/quotes/${quoteId}`); + outputResponse(response); + }); + + quotesCmd + .command("create") + .description("Create a transfer quote") + .requiredOption("--amount ", "Amount in smallest currency unit (e.g., cents)") + .requiredOption("--lock-side ", "Lock SENDING or RECEIVING amount") + .option("--source-account ", "Source account ID (InternalAccount:...)") + .option("--source-customer ", "Source customer ID (for customer-funded quotes)") + .option("--source-currency ", "Source currency (required with --source-customer)") + .option("--dest-account ", "Destination account ID") + .option("--dest-uma
", "Destination UMA address") + .option("--dest-currency ", "Destination currency") + .option("--description ", "Transfer description") + .option("--lookup-id ", "Lookup request ID (from receiver lookup)") + .option("--immediate", "Execute the quote immediately after creation") + .option("--sender-name ", "Sender full name (for UMA destinations)") + .option("--sender-birth-date ", "Sender birth date YYYY-MM-DD (for UMA destinations)") + .option("--sender-nationality ", "Sender nationality country code (for UMA destinations)") + .action(async (options) => { + const opts = program.opts(); + const client = getClient(opts); + if (!client) return; + + const body: Record = { + lockedCurrencyAmount: parseInt(options.amount), + lockedCurrencySide: options.lockSide, + }; + + if (options.sourceAccount) { + body.source = { accountId: options.sourceAccount }; + } else if (options.sourceCustomer) { + body.source = { + customerId: options.sourceCustomer, + currency: options.sourceCurrency, + }; + } + + if (options.destAccount) { + body.destination = { + accountId: options.destAccount, + currency: options.destCurrency, + }; + } else if (options.destUma) { + body.destination = { + umaAddress: options.destUma, + currency: options.destCurrency, + }; + } + + if (options.description) body.description = options.description; + if (options.lookupId) body.lookupId = options.lookupId; + if (options.immediate) body.immediatelyExecute = true; + + const senderInfo: Record = {}; + if (options.senderName) senderInfo.FULL_NAME = options.senderName; + if (options.senderBirthDate) senderInfo.BIRTH_DATE = options.senderBirthDate; + if (options.senderNationality) senderInfo.NATIONALITY = options.senderNationality; + if (Object.keys(senderInfo).length > 0) { + body.senderCustomerInfo = senderInfo; + } + + const response = await client.post("/quotes", body); + outputResponse(response); + }); + + quotesCmd + .command("execute ") + .description("Execute a pending quote") + .action(async (quoteId: string) => { + const opts = program.opts(); + const client = getClient(opts); + if (!client) return; + + const response = await client.post(`/quotes/${quoteId}/execute`); + outputResponse(response); + }); +} diff --git a/cli/src/commands/receiver.ts b/cli/src/commands/receiver.ts new file mode 100644 index 0000000..ced420c --- /dev/null +++ b/cli/src/commands/receiver.ts @@ -0,0 +1,66 @@ +import { Command } from "commander"; +import { GridClient } from "../client"; +import { outputResponse } from "../output"; +import { GlobalOptions } from "../index"; + +interface ReceiverLookup { + id: string; + umaAddress?: string; + accountId?: string; + currencies: Array<{ + code: string; + name: string; + symbol: string; + minAmount?: number; + maxAmount?: number; + }>; + requiredPayerData?: Array<{ + name: string; + mandatory: boolean; + }>; +} + +export function registerReceiverCommand( + program: Command, + getClient: (opts: GlobalOptions) => GridClient | null +): void { + const receiverCmd = program + .command("receiver") + .description("Receiver lookup commands"); + + receiverCmd + .command("lookup-uma ") + .description("Look up a UMA address to get payment capabilities") + .option("--customer-id ", "Sender customer ID") + .option("--sender-uma
", "Sender UMA address") + .action(async (umaAddress: string, options) => { + const opts = program.opts(); + const client = getClient(opts); + if (!client) return; + + const params: Record = { + customerId: options.customerId, + senderUmaAddress: options.senderUma, + }; + + const response = await client.get( + `/receiver/uma/${encodeURIComponent(umaAddress)}`, + params + ); + outputResponse(response); + }); + + receiverCmd + .command("lookup-account ") + .description("Look up an external account to get payment capabilities") + .action(async (accountId: string) => { + const opts = program.opts(); + const client = getClient(opts); + if (!client) return; + + const response = await client.get( + `/receiver/external-account/${encodeURIComponent(accountId)}` + ); + outputResponse(response); + }); +} diff --git a/cli/src/commands/sandbox.ts b/cli/src/commands/sandbox.ts new file mode 100644 index 0000000..8e4a2c2 --- /dev/null +++ b/cli/src/commands/sandbox.ts @@ -0,0 +1,73 @@ +import { Command } from "commander"; +import { GridClient } from "../client"; +import { outputResponse } from "../output"; +import { GlobalOptions } from "../index"; + +export function registerSandboxCommand( + program: Command, + getClient: (opts: GlobalOptions) => GridClient | null +): void { + const sandboxCmd = program + .command("sandbox") + .description("Sandbox testing commands"); + + sandboxCmd + .command("send") + .description("Simulate sending a payment in sandbox") + .requiredOption("--quote-id ", "Quote ID to simulate sending") + .requiredOption("--currency ", "Currency code for the funds to send") + .option("--amount ", "Amount in smallest unit (derived from quote if not provided)") + .action(async (options) => { + const opts = program.opts(); + const client = getClient(opts); + if (!client) return; + + const body: Record = { + quoteId: options.quoteId, + currencyCode: options.currency, + }; + if (options.amount) body.currencyAmount = parseInt(options.amount); + const response = await client.post("/sandbox/send", body); + outputResponse(response); + }); + + sandboxCmd + .command("receive") + .description("Simulate receiving a UMA payment in sandbox") + .requiredOption("--uma-address
", "Receiver UMA address") + .requiredOption("--amount ", "Amount in smallest unit") + .requiredOption("--currency ", "Currency code") + .option("--sender-uma
", "Sender UMA address") + .action(async (options) => { + const opts = program.opts(); + const client = getClient(opts); + if (!client) return; + + const body: Record = { + receiverUmaAddress: options.umaAddress, + amount: parseInt(options.amount), + currency: options.currency, + }; + if (options.senderUma) body.senderUmaAddress = options.senderUma; + + const response = await client.post("/sandbox/uma/receive", body); + outputResponse(response); + }); + + sandboxCmd + .command("fund ") + .description("Fund an internal account in sandbox") + .requiredOption("--amount ", "Amount in smallest unit") + .action(async (accountId: string, options) => { + const opts = program.opts(); + const client = getClient(opts); + if (!client) return; + + const body = { amount: parseInt(options.amount) }; + const response = await client.post( + `/sandbox/internal-accounts/${accountId}/fund`, + body + ); + outputResponse(response); + }); +} diff --git a/cli/src/commands/transactions.ts b/cli/src/commands/transactions.ts new file mode 100644 index 0000000..48f9155 --- /dev/null +++ b/cli/src/commands/transactions.ts @@ -0,0 +1,114 @@ +import { Command } from "commander"; +import { GridClient, PaginatedResponse } from "../client"; +import { outputResponse } from "../output"; +import { GlobalOptions } from "../index"; + +interface Transaction { + id: string; + type: "INCOMING" | "OUTGOING"; + status: string; + amount: number; + currency: string; + senderAccountIdentifier?: string; + receiverAccountIdentifier?: string; + reference?: string; + description?: string; + createdAt: string; + updatedAt: string; +} + +export function registerTransactionsCommand( + program: Command, + getClient: (opts: GlobalOptions) => GridClient | null +): void { + const transactionsCmd = program + .command("transactions") + .description("Transaction management commands"); + + transactionsCmd + .command("list") + .description("List transactions") + .option("-l, --limit ", "Maximum results (default 20, max 100)", "20") + .option("--cursor ", "Pagination cursor") + .option("--customer-id ", "Filter by customer ID") + .option("--platform-customer-id ", "Filter by platform customer ID") + .option("--sender ", "Filter by sender account identifier") + .option("--receiver ", "Filter by receiver account identifier") + .option("--status ", "Filter by status") + .option("--type ", "Filter by type (INCOMING or OUTGOING)") + .option("--reference ", "Filter by reference") + .option("--start-date ", "Filter by start date (ISO 8601)") + .option("--end-date ", "Filter by end date (ISO 8601)") + .option("--sort ", "Sort order: asc or desc (default: desc)") + .action(async (options) => { + const opts = program.opts(); + const client = getClient(opts); + if (!client) return; + + const params: Record = { + limit: parseInt(options.limit), + cursor: options.cursor, + customerId: options.customerId, + platformCustomerId: options.platformCustomerId, + senderAccountIdentifier: options.sender, + receiverAccountIdentifier: options.receiver, + status: options.status, + type: options.type, + reference: options.reference, + startDate: options.startDate, + endDate: options.endDate, + sortOrder: options.sort, + }; + + const response = await client.get>( + "/transactions", + params + ); + outputResponse(response); + }); + + transactionsCmd + .command("get ") + .description("Get transaction details") + .action(async (transactionId: string) => { + const opts = program.opts(); + const client = getClient(opts); + if (!client) return; + + const response = await client.get( + `/transactions/${transactionId}` + ); + outputResponse(response); + }); + + transactionsCmd + .command("approve ") + .description("Approve an incoming payment transaction") + .action(async (transactionId: string) => { + const opts = program.opts(); + const client = getClient(opts); + if (!client) return; + + const response = await client.post( + `/transactions/${transactionId}/approve` + ); + outputResponse(response); + }); + + transactionsCmd + .command("reject ") + .description("Reject an incoming payment transaction") + .option("--reason ", "Rejection reason") + .action(async (transactionId: string, options) => { + const opts = program.opts(); + const client = getClient(opts); + if (!client) return; + + const body = options.reason ? { reason: options.reason } : undefined; + const response = await client.post( + `/transactions/${transactionId}/reject`, + body + ); + outputResponse(response); + }); +} diff --git a/cli/src/commands/transfers.ts b/cli/src/commands/transfers.ts new file mode 100644 index 0000000..2c76034 --- /dev/null +++ b/cli/src/commands/transfers.ts @@ -0,0 +1,71 @@ +import { Command } from "commander"; +import { GridClient } from "../client"; +import { outputResponse } from "../output"; +import { GlobalOptions } from "../index"; + +interface Transaction { + id: string; + type: "INCOMING" | "OUTGOING"; + status: string; + amount: number; + currency: string; + createdAt: string; + updatedAt: string; +} + +export function registerTransfersCommand( + program: Command, + getClient: (opts: GlobalOptions) => GridClient | null +): void { + const transfersCmd = program + .command("transfers") + .description("Same-currency transfer commands"); + + transfersCmd + .command("in") + .description("Transfer from external account to internal account (same currency)") + .requiredOption("--source ", "Source external account ID (ExternalAccount:...)") + .requiredOption("--dest ", "Destination internal account ID (InternalAccount:...)") + .option("--amount ", "Amount in smallest currency unit (optional for full balance)") + .action(async (options) => { + const opts = program.opts(); + const client = getClient(opts); + if (!client) return; + + const body: Record = { + source: { accountId: options.source }, + destination: { accountId: options.dest }, + }; + + if (options.amount) { + body.amount = parseInt(options.amount); + } + + const response = await client.post("/transfer-in", body); + outputResponse(response); + }); + + transfersCmd + .command("out") + .description("Transfer from internal account to external account (same currency)") + .requiredOption("--source ", "Source internal account ID (InternalAccount:...)") + .requiredOption("--dest ", "Destination external account ID (ExternalAccount:...)") + .option("--amount ", "Amount in smallest currency unit (optional for full balance)") + .action(async (options) => { + const opts = program.opts(); + const client = getClient(opts); + if (!client) return; + + const body: Record = { + source: { accountId: options.source }, + destination: { accountId: options.dest }, + }; + + if (options.amount) { + body.amount = parseInt(options.amount); + } + + const response = await client.post("/transfer-out", body); + outputResponse(response); + }); +} diff --git a/cli/src/index.ts b/cli/src/index.ts index 860c550..91b82d2 100644 --- a/cli/src/index.ts +++ b/cli/src/index.ts @@ -39,4 +39,31 @@ function getClient(options: GlobalOptions): GridClient | null { export { program, getClient, GridClient, GridConfig }; -program.parse(process.argv); +async function main() { + const { registerConfigCommand } = await import("./commands/config"); + const { registerCustomersCommand } = await import("./commands/customers"); + const { registerAccountsCommand } = await import("./commands/accounts"); + const { registerQuotesCommand } = await import("./commands/quotes"); + const { registerTransactionsCommand } = await import( + "./commands/transactions" + ); + const { registerTransfersCommand } = await import("./commands/transfers"); + const { registerSandboxCommand } = await import("./commands/sandbox"); + const { registerReceiverCommand } = await import("./commands/receiver"); + + registerConfigCommand(program, getClient); + registerCustomersCommand(program, getClient); + registerAccountsCommand(program, getClient); + registerQuotesCommand(program, getClient); + registerTransactionsCommand(program, getClient); + registerTransfersCommand(program, getClient); + registerSandboxCommand(program, getClient); + registerReceiverCommand(program, getClient); + + await program.parseAsync(process.argv); +} + +main().catch((err) => { + output(formatError(err.message)); + process.exitCode = 1; +}); From 294da993d3187f29d39f771c91d224bbce4255e9 Mon Sep 17 00:00:00 2001 From: Jeremy Klein Date: Tue, 27 Jan 2026 11:42:12 -0800 Subject: [PATCH 2/2] Add readme --- cli/README.md | 353 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 353 insertions(+) create mode 100644 cli/README.md diff --git a/cli/README.md b/cli/README.md new file mode 100644 index 0000000..5176be8 --- /dev/null +++ b/cli/README.md @@ -0,0 +1,353 @@ +# Grid CLI + +A command-line interface for the Grid API, enabling global payments across fiat, stablecoins, and Bitcoin. + +## Installation + +```bash +cd cli +npm install +npm run build +``` + +## Configuration + +The CLI requires API credentials via environment variables: + +```bash +export GRID_API_TOKEN_ID="your-token-id" +export GRID_API_CLIENT_SECRET="your-client-secret" +export GRID_BASE_URL="https://api.lightspark.com/grid/2025-10-13" # optional +``` + +## Usage + +Run commands from the repository root: + +```bash +node cli/dist/index.js [options] +``` + +### Global Options + +| Option | Description | +|--------|-------------| +| `-c, --config ` | Path to credentials file | +| `-u, --base-url ` | Override API base URL | +| `-V, --version` | Show version | +| `-h, --help` | Show help | + +## Commands + +### Platform Configuration + +```bash +# Get platform config (currencies, limits, webhook) +node cli/dist/index.js config get + +# Update webhook endpoint +node cli/dist/index.js config update --webhook-endpoint https://example.com/webhooks +``` + +### Customers + +```bash +# List customers +node cli/dist/index.js customers list [--limit 20] [--type INDIVIDUAL|BUSINESS] + +# Get customer details +node cli/dist/index.js customers get + +# Create individual customer +node cli/dist/index.js customers create \ + --platform-id "your-id" \ + --type INDIVIDUAL \ + --full-name "John Doe" \ + --birth-date "1990-01-15" \ + --address-line1 "123 Main St" \ + --address-city "Seattle" \ + --address-state "WA" \ + --address-postal "98101" \ + --address-country "US" + +# Create business customer +node cli/dist/index.js customers create \ + --platform-id "biz-123" \ + --type BUSINESS \ + --legal-name "Acme Inc" \ + --tax-id "12-3456789" + +# Generate KYC link +node cli/dist/index.js customers kyc-link \ + --customer-id \ + --redirect-url https://example.com/kyc-complete + +# Update customer +node cli/dist/index.js customers update --full-name "Jane Doe" + +# Delete customer +node cli/dist/index.js customers delete +``` + +### Accounts + +#### Internal Accounts (Grid-managed balances) + +```bash +# List customer internal accounts +node cli/dist/index.js accounts internal list [--customer-id ] [--currency USD] + +# List platform internal accounts +node cli/dist/index.js accounts internal list --platform +``` + +#### External Accounts (Bank accounts, wallets) + +```bash +# List external accounts +node cli/dist/index.js accounts external list [--customer-id ] + +# Create US bank account +node cli/dist/index.js accounts external create \ + --customer-id \ + --currency USD \ + --account-type US_ACCOUNT \ + --account-number "123456789" \ + --routing-number "021000021" \ + --account-category CHECKING \ + --beneficiary-type INDIVIDUAL \ + --beneficiary-name "John Doe" + +# Create Mexico CLABE account +node cli/dist/index.js accounts external create \ + --customer-id \ + --currency MXN \ + --account-type CLABE \ + --clabe "012345678901234567" \ + --beneficiary-type INDIVIDUAL \ + --beneficiary-name "Carlos Garcia" \ + --beneficiary-birth-date "1988-03-20" \ + --beneficiary-nationality MX + +# Create India UPI account +node cli/dist/index.js accounts external create \ + --customer-id \ + --currency INR \ + --account-type UPI \ + --upi-id "name@okaxis" \ + --beneficiary-type INDIVIDUAL \ + --beneficiary-name "Rajesh Kumar" \ + --beneficiary-birth-date "1985-06-15" \ + --beneficiary-nationality IN + +# Create Brazil PIX account +node cli/dist/index.js accounts external create \ + --customer-id \ + --currency BRL \ + --account-type PIX \ + --pix-key "12345678901" \ + --beneficiary-type INDIVIDUAL \ + --beneficiary-name "Maria Silva" \ + --beneficiary-birth-date "1990-05-10" \ + --beneficiary-nationality BR + +# Create Nigeria account +node cli/dist/index.js accounts external create \ + --customer-id \ + --currency NGN \ + --account-type NGN_ACCOUNT \ + --account-number "1234567890" \ + --bank-name "First Bank" \ + --purpose GOODS_OR_SERVICES \ + --beneficiary-type INDIVIDUAL \ + --beneficiary-name "Chidi Okonkwo" \ + --beneficiary-birth-date "1992-08-20" \ + --beneficiary-nationality NG + +# Create Europe IBAN account +node cli/dist/index.js accounts external create \ + --customer-id \ + --currency EUR \ + --account-type IBAN \ + --iban "DE89370400440532013000" \ + --beneficiary-type INDIVIDUAL \ + --beneficiary-name "Hans Mueller" + +# Create crypto wallet (Solana USDC) +node cli/dist/index.js accounts external create \ + --customer-id \ + --currency USDC \ + --account-type SOLANA_WALLET \ + --address "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU" +``` + +### Quotes (Cross-Currency Transfers) + +```bash +# List quotes +node cli/dist/index.js quotes list [--status PENDING] [--customer-id ] + +# Get quote details +node cli/dist/index.js quotes get + +# Create quote from internal account (prefunded) +node cli/dist/index.js quotes create \ + --source-account \ + --dest-uma '$user@domain.com' \ + --amount 10000 \ + --lock-side SENDING + +# Create quote with JIT funding (real-time) +node cli/dist/index.js quotes create \ + --source-customer \ + --source-currency USDC \ + --dest-account \ + --dest-currency MXN \ + --amount 100000 \ + --lock-side RECEIVING + +# Execute a pending quote +node cli/dist/index.js quotes execute +``` + +### Same-Currency Transfers + +```bash +# Transfer in (external → internal) +node cli/dist/index.js transfers in \ + --source \ + --dest \ + --amount 10000 + +# Transfer out (internal → external) +node cli/dist/index.js transfers out \ + --source \ + --dest \ + --amount 10000 +``` + +### Transactions + +```bash +# List transactions +node cli/dist/index.js transactions list \ + [--customer-id ] \ + [--status PENDING|PROCESSING|COMPLETED|FAILED] \ + [--type INCOMING|OUTGOING] \ + [--start-date 2024-01-01] \ + [--end-date 2024-12-31] + +# Get transaction details +node cli/dist/index.js transactions get + +# Approve incoming payment +node cli/dist/index.js transactions approve + +# Reject incoming payment +node cli/dist/index.js transactions reject --reason "Invalid sender" +``` + +### Receiver Lookup + +```bash +# Look up UMA address +node cli/dist/index.js receiver lookup-uma '$user@domain.com' + +# Look up external account +node cli/dist/index.js receiver lookup-account +``` + +### Sandbox Testing + +```bash +# Fund an internal account +node cli/dist/index.js sandbox fund --amount 100000 + +# Simulate sending funds to a JIT quote +node cli/dist/index.js sandbox send --quote-id --currency USDC + +# Simulate receiving a UMA payment +node cli/dist/index.js sandbox receive \ + --uma-address '$user@domain.com' \ + --amount 1000 \ + --currency USD +``` + +## Output Format + +All commands output JSON: + +```json +{ + "success": true, + "data": { ... } +} +``` + +On error: + +```json +{ + "success": false, + "error": { + "code": "ERROR_CODE", + "message": "Human readable message" + } +} +``` + +## Common Workflows + +### Send USDC to Mexico (JIT Funding) + +```bash +# 1. Create external account +node cli/dist/index.js accounts external create \ + --customer-id \ + --currency MXN \ + --account-type CLABE \ + --clabe "012345678901234567" \ + --beneficiary-type INDIVIDUAL \ + --beneficiary-name "Carlos Garcia" \ + --beneficiary-birth-date "1988-03-20" \ + --beneficiary-nationality MX + +# 2. Create quote (returns payment instructions) +node cli/dist/index.js quotes create \ + --source-customer \ + --source-currency USDC \ + --dest-account \ + --dest-currency MXN \ + --amount 100000 \ + --lock-side RECEIVING + +# 3. In sandbox, simulate the USDC deposit +node cli/dist/index.js sandbox send --quote-id --currency USDC + +# 4. Check transaction status +node cli/dist/index.js transactions get +``` + +### Send to UMA Address + +```bash +# 1. Look up the receiver +node cli/dist/index.js receiver lookup-uma '$alice@example.com' + +# 2. Create and execute quote +node cli/dist/index.js quotes create \ + --source-account \ + --dest-uma '$alice@example.com' \ + --amount 5000 \ + --lock-side SENDING + +# 3. Execute the quote +node cli/dist/index.js quotes execute +``` + +## Notes + +- All amounts are in the **smallest currency unit** (cents for USD, satoshis for BTC) +- Quotes expire in 1-5 minutes +- JIT quotes auto-execute when funds are received (no manual execute needed) +- Use `--lock-side SENDING` to fix the send amount, `RECEIVING` to fix the receive amount