Skill: shawnmuggle/rozo-intents-skills (version 1.0.2)

--- name: rozo-intents description: > Cross-chain crypto payments and bridging via Rozo. Send USDC/USDT across Ethereum, Base, BNB Chain, Solana, and Stellar. Use when user says "pay", "send", "transfer", "payout", "check balance", "payment status", or shares a QR code screenshot. Also triggers on wallet addresses (0x, base58, G/C stellar), transaction hashes. Auto-detects wallet type, auto-selects token (USDC preferred), checks balances, gets fees, and confirms before sending. Do NOT use for general blockchain questions or non-payment tasks. metadata: author: rozo version: 1.0.2 runtime: node --- # Rozo Cross-Chain Payments / Bridging Send cross-chain crypto payments and bridging via Rozo. Send USDC/USDT across Ethereum, Base, BNB Chain, Solana, and Stellar. ## Routing Determine the user's intent and load the matching sub-skill: | Intent | Sub-skill | Triggers | |--------|-----------|----------| | Send a payment | `skills/send-payment/SKILL.md` | "pay", "send", "transfer", "payout", shares a QR code, provides an amount + address | | Check wallet balance | `skills/check-balance/SKILL.md` | "check balance", "how much do I have", "show my balance", "wallet balance" | | Parse a QR code | `skills/parse-qr/SKILL.md` | "scan QR", "parse QR", "read this QR", shares a QR image without mentioning payment | | Check payment status | `skills/payment-status/SKILL.md` | "check payment", "payment status", "where is my payment", "track payment", provides a payment UUID or tx hash | **Rules:** 1. If the user mentions sending/paying → route to `send-payment` (it handles QR parsing internally) 2. If the user shares a QR code WITHOUT mentioning payment → route to `parse-qr` first, then offer to send 3. If the user asks about balance before sending → route to `check-balance`, then continue to `send-payment` if they want to pay 4. If ambiguous, ask the user what they'd like to do ## Supported Chains ### Pay-Out (sending to) | Chain | USDC | USDT | |-------|------|------| | Ethereum | Yes | Yes | | Arbitrum | Yes | Yes | | Base | Yes | Yes | | BSC | Yes | Yes | | Polygon | Yes | Yes | | Solana | Yes | No | | Stellar | Yes | No | ### Pay-In (paying from) | Chain | USDC | USDT | |-------|------|------| | Ethereum | Yes | Yes | | Arbitrum | Yes | Yes | | Base | Yes | No | | BSC | Yes | Yes | | Polygon | Yes | Yes | | Solana | Yes | Yes | | Stellar | Yes | No | ## Runtime Requires **Node.js** (ES modules). All scripts in `scripts/dist/` are run with `node`. ## Authentication & Rate Limiting The Rozo APIs are **public and rate-limited** — no API keys or authentication tokens are required. | Endpoint | Host | Auth | Notes | |----------|------|------|-------| | Payment API (create, get, check) | `intentapiv4.rozo.ai` | None (rate-limited) | Main Rozo payment API | | Balance API (check balance) | `api-balance.rozo-deeplink.workers.dev` | None (rate-limited) | Rozo balance service (Cloudflare Workers) | Both hosts are operated by Rozo. The balance endpoint uses a separate Cloudflare Workers deployment for performance. ## Quick Reference - **Amount limits:** $0.01 minimum, $10,000 maximum per transaction - **Token selection:** Auto — fetch balance, prefer USDC, fall back to USDT - **Payment type:** `exactOut` by default — recipient gets exact amount, fee added on top ## Scripts Shared Node.js scripts in `scripts/dist/` (run with plain `node`): | Script | Purpose | |--------|---------| | `check-balance.js` | Fetch wallet balances via Rozo balance API | | `check-stellar-trustline.js` | Verify asset trustline (USDC/EURC) on Stellar G-wallets | | `create-payment.js` | Create a payment (or dryrun for fee estimate) via Rozo API | | `get-payment.js` | Get payment status by ID, tx hash, or address+memo | | `parse-qr.js` | Parse payment QR code URIs (EIP-681, Solana Pay, Stellar URI) | | `chains.js` | Shared chain/token config (imported by other scripts) | ## Shared Resources - `references/supported-chains.md` — chain IDs, token addresses, decimals - `references/api-reference.md` — Rozo API endpoints and schemas - `references/wallet-detection.md` — address format detection rules

path: LICENSE.md
content: MIT No Attribution

Copyright 2026 Rozo

Permission is hereby granted, free of charge, to any person obtaining a copy of this
software and associated documentation files (the "Software"), to deal in the Software
without restriction, including without limitation the rights to use, copy, modify,
merge, publish, distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A
PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.


path: scripts/package.json
content: {
  "name": "rozo-intents-scripts",
  "version": "0.1.0",
  "private": true,
  "type": "module",
  "scripts": {
    "build": "tsc",
    "check-balance": "node dist/check-balance.js",
    "create-payment": "node dist/create-payment.js",
    "get-payment": "node dist/get-payment.js",
    "test": "vitest run",
    "test:watch": "vitest"
  },
  "devDependencies": {
    "typescript": "^5.4.0",
    "vitest": "^3.0.0"
  }
}


path: scripts/tsconfig.json
content: {
  "compilerOptions": {
    "target": "ES2022",
    "module": "ESNext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "outDir": "dist",
    "rootDir": "src",
    "declaration": true,
    "skipLibCheck": true
  },
  "include": ["src"],
  "exclude": ["node_modules", "dist", "**/*.test.ts"]
}


path: scripts/src/create-payment.ts
content: import {
  getTokenAddress,
  getChainName,
  isPayoutSupported,
  type TokenSymbol,
} from "./chains.js";
import { randomUUID } from "crypto";

const API_BASE = "https://intentapiv4.rozo.ai/functions/v1/payment-api";
const MIN_AMOUNT = 0.01;
const MAX_AMOUNT = 10_000;

export interface CreatePaymentParams {
  appId?: string;
  type?: "exactIn" | "exactOut";
  sourceChain: number;
  sourceToken: TokenSymbol;
  destChain: number;
  destAddress: string;
  destToken: TokenSymbol;
  destAmount: string;
  destMemo?: string;
  orderId?: string;
  sourceAmount?: string;
  dryrun?: boolean;
}

export interface PaymentResponse {
  success: boolean;
  payment?: Record<string, unknown>;
  statusCode?: number;
  error?: unknown;
}

export async function createPayment(params: CreatePaymentParams): Promise<PaymentResponse> {
  const {
    appId = "rozoAgent",
    type = "exactOut",
    sourceChain,
    sourceToken,
    destChain,
    destAddress,
    destToken,
    destAmount,
    destMemo,
    orderId,
    sourceAmount,
    dryrun = false,
  } = params;

  // Validate amount limits
  const amount = parseFloat(destAmount);
  if (isNaN(amount) || amount < MIN_AMOUNT || amount > MAX_AMOUNT) {
    return {
      success: false,
      error: `Amount must be between $${MIN_AMOUNT} and $${MAX_AMOUNT.toLocaleString()}. Got: ${destAmount}`,
    };
  }

  if (!isPayoutSupported(destChain, destToken)) {
    return {
      success: false,
      error: `Payout not supported: ${destToken} on ${getChainName(destChain)}`,
    };
  }

  const sourceTokenInfo = getTokenAddress(sourceChain, sourceToken, "payin");
  if (!sourceTokenInfo) {
    return {
      success: false,
      error: `Pay-in not supported: ${sourceToken} on ${getChainName(sourceChain)}`,
    };
  }

  const destTokenInfo = getTokenAddress(destChain, destToken, "payout");
  if (!destTokenInfo) {
    return {
      success: false,
      error: `Payout not supported: ${destToken} on ${getChainName(destChain)}`,
    };
  }

  const payload: Record<string, unknown> = {
    appId,
    type,
    orderId: orderId ?? randomUUID(),
    display: {
      title: "Payment",
      currency: "USD",
    },
    source: {
      chainId: sourceChain,
      tokenSymbol: sourceToken,
      tokenAddress: sourceTokenInfo.address,
      ...(type === "exactIn" && { amount: sourceAmount ?? destAmount }),
    },
    destination: {
      chainId: destChain,
      receiverAddress: destAddress,
      tokenSymbol: destToken,
      tokenAddress: destTokenInfo.address,
      ...(type === "exactOut" && { amount: destAmount }),
      ...(destMemo && { receiverMemo: destMemo }),
    },
  };

  let response: Response;
  try {
    const url = dryrun ? `${API_BASE}?dryrun=true` : API_BASE;
    response = await fetch(url, {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify(payload),
    });
  } catch (err) {
    return { success: false, error: `Network error: ${(err as Error).message}` };
  }

  let body: Record<string, unknown>;
  try {
    body = await response.json();
  } catch {
    return { success: false, statusCode: response.status, error: "Invalid JSON response from Rozo API" };
  }

  if (!response.ok) {
    return { success: false, statusCode: response.status, error: body };
  }

  return { success: true, payment: body };
}

// CLI entry point
if (import.meta.url === `file://${process.argv[1]}`) {
  const args = process.argv.slice(2);
  const get = (flag: string): string | undefined => {
    const idx = args.indexOf(flag);
    return idx !== -1 ? args[idx + 1] : undefined;
  };

  const sourceChain = get("--source-chain");
  const sourceToken = get("--source-token");
  const destChain = get("--dest-chain");
  const destAddress = get("--dest-address");
  const destToken = get("--dest-token");
  const destAmount = get("--dest-amount");

  if (!sourceChain || !sourceToken || !destChain || !destAddress || !destToken || !destAmount) {
    console.error(
      "Usage: --source-chain <id> --source-token <USDC|USDT> --dest-chain <id> --dest-address <addr> --dest-token <USDC|USDT> --dest-amount <amount>"
    );
    process.exit(1);
  }

  const result = await createPayment({
    appId: get("--app-id") ?? "rozoAgent",
    type: (get("--type") as "exactIn" | "exactOut") ?? "exactOut",
    sourceChain: Number(sourceChain),
    sourceToken: sourceToken.toUpperCase() as TokenSymbol,
    destChain: Number(destChain),
    destAddress,
    destToken: destToken.toUpperCase() as TokenSymbol,
    destAmount,
    destMemo: get("--dest-memo"),
    orderId: get("--order-id"),
    sourceAmount: get("--source-amount"),
    dryrun: args.includes("--dryrun"),
  });

  console.log(JSON.stringify(result, null, 2));
  if (!result.success) process.exit(1);
}


path: scripts/src/check-balance.ts
content: /**
 * Check wallet balances via the Rozo balance API.
 *
 * Single call returns all USDC/USDT balances across all supported chains
 * for the given address type (evm, solana, stellar).
 */

const BALANCE_API = "https://api-balance.rozo-deeplink.workers.dev/balance";

export interface TokenBalance {
  [REDACTED_SECRET];
  chain: string;
  balance: string;
  decimals: number;
}

export interface BalanceResponse {
  address: string;
  chain: string;
  balances: TokenBalance[];
  error?: string;
}

type ChainParam = "evm" | "solana" | "stellar";

function detectChainParam(address: string): ChainParam | null {
  if (address.startsWith("0x") && address.length === 42) return "evm";
  if (address.startsWith("G") && address.length === 56) return "stellar";
  if (address.startsWith("C") && address.length === 56) return "stellar";
  if (/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(address)) return "solana";
  return null;
}

export async function checkBalance(
  address: string,
  chain?: ChainParam
): Promise<BalanceResponse> {
  const chainParam = chain ?? detectChainParam(address);
  if (!chainParam) {
    return { address, chain: "unknown", balances: [], error: "Cannot detect chain from address format" };
  }

  const url = `${BALANCE_API}/${address}?chain=${chainParam}`;

  let response: Response;
  try {
    response = await fetch(url);
  } catch (err) {
    return { address, chain: chainParam, balances: [], error: `Network error: ${(err as Error).message}` };
  }

  if (!response.ok) {
    return { address, chain: chainParam, balances: [], error: `Balance API error: ${response.status}` };
  }

  try {
    return await response.json() as BalanceResponse;
  } catch {
    return { address, chain: chainParam, balances: [], error: "Invalid JSON response from balance API" };
  }
}

// CLI entry point
if (import.meta.url === `file://${process.argv[1]}`) {
  const args = process.argv.slice(2);
  const addrIdx = args.indexOf("--address");
  const chainIdx = args.indexOf("--chain");

  if (addrIdx === -1) {
    console.error("Usage: --address <wallet_address> [--chain <evm|solana|stellar>]");
    process.exit(1);
  }

  const chain = chainIdx !== -1 ? (args[chainIdx + 1] as ChainParam) : undefined;
  const result = await checkBalance(args[addrIdx + 1], chain);
  console.log(JSON.stringify(result, null, 2));
  if (result.error) process.exit(1);
}


path: scripts/src/check-stellar-trustline.ts
content: /**
 * Check if a Stellar G-wallet has a trustline for a given asset via the Horizon API.
 * Supports USDC (default) and EURC.
 */

const HORIZON_URL = "https://horizon.stellar.org";

export type StellarAsset = "USDC" | "EURC";

const ASSET_ISSUERS: Record<StellarAsset, string> = {
  USDC: "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN",
  EURC: "GDHU6WRG4IEQXM5NZ4BMPKOXHW76MZM4Y2IEMFDVXBSDP6SJY4ITNPP2",
};

export interface TrustlineResult {
  address: string;
  asset: StellarAsset;
  hasTrustline: boolean;
  balance?: string;
  error?: string;
}

export async function checkStellarTrustline(
  address: string,
  asset: StellarAsset = "USDC"
): Promise<TrustlineResult> {
  if (!address.startsWith("G") || address.length !== 56) {
    return { address, asset, hasTrustline: false, error: "Not a valid Stellar G-wallet address" };
  }

  const issuer = ASSET_ISSUERS[asset];
  if (!issuer) {
    return { address, asset, hasTrustline: false, error: `Unsupported asset: ${asset}` };
  }

  let response: Response;
  try {
    response = await fetch(`${HORIZON_URL}/accounts/${address}`);
  } catch (err) {
    return { address, asset, hasTrustline: false, error: `Network error: ${(err as Error).message}` };
  }

  if (!response.ok) {
    if (response.status === 404) {
      return { address, asset, hasTrustline: false, error: "Account not found on Stellar network" };
    }
    return { address, asset, hasTrustline: false, error: `Horizon API error: ${response.status}` };
  }

  let account: {
    balances: Array<{
      asset_type: string;
      asset_code?: string;
      asset_issuer?: string;
      balance: string;
    }>;
  };
  try {
    account = await response.json();
  } catch {
    return { address, asset, hasTrustline: false, error: "Invalid JSON response from Horizon API" };
  }

  const found = account.balances.find(
    (b) => b.asset_code === asset && b.asset_issuer === issuer
  );

  if (found) {
    return { address, asset, hasTrustline: true, balance: found.balance };
  }

  return { address, asset, hasTrustline: false };
}

// CLI entry point
if (import.meta.url === `file://${process.argv[1]}`) {
  const args = process.argv.slice(2);
  const addrIdx = args.indexOf("--address");
  const assetIdx = args.indexOf("--asset");

  if (addrIdx === -1) {
    console.error("Usage: --address <stellar_g_wallet> [--asset <USDC|EURC>]");
    process.exit(1);
  }

  const asset = assetIdx !== -1 ? (args[assetIdx + 1] as StellarAsset) : "USDC";
  const result = await checkStellarTrustline(args[addrIdx + 1], asset);
  console.log(JSON.stringify(result, null, 2));
  if (!result.hasTrustline) process.exit(1);
}


path: scripts/src/get-payment.ts
content: const API_BASE = "https://intentapiv4.rozo.ai/functions/v1/payment-api";

export interface GetPaymentResponse {
  success: boolean;
  payment?: Record<string, unknown>;
  statusCode?: number;
  error?: unknown;
}

/**
 * Get payment by payment ID.
 */
export async function getPayment(paymentId: string): Promise<GetPaymentResponse> {
  return fetchPayment(`${API_BASE}/payments/${paymentId}`);
}

/**
 * Check payment by source transaction hash.
 * Looks back 7 days, returns the most recent matching payment.
 */
export async function checkPaymentByTxHash(txHash: string): Promise<GetPaymentResponse> {
  return fetchPayment(`${API_BASE}/payments/check?txHash=${encodeURIComponent(txHash)}`);
}

/**
 * Check payment by deposit address + memo.
 * Looks back 7 days, returns the most recent matching payment.
 */
export async function checkPaymentByAddressMemo(
  receiverAddress: string,
  receiverMemo: string
): Promise<GetPaymentResponse> {
  const params = new URLSearchParams({ receiverAddress, receiverMemo });
  return fetchPayment(`${API_BASE}/payments/check?${params}`);
}

async function fetchPayment(url: string): Promise<GetPaymentResponse> {
  let response: Response;
  try {
    response = await fetch(url);
  } catch (err) {
    return { success: false, error: `Network error: ${(err as Error).message}` };
  }

  let body: Record<string, unknown>;
  try {
    body = await response.json();
  } catch {
    return { success: false, statusCode: response.status, error: "Invalid JSON response from Rozo API" };
  }

  if (!response.ok) {
    return { success: false, statusCode: response.status, error: body };
  }

  return { success: true, payment: body };
}

// CLI entry point
if (import.meta.url === `file://${process.argv[1]}`) {
  const args = process.argv.slice(2);
  const get = (flag: string): string | undefined => {
    const idx = args.indexOf(flag);
    return idx !== -1 ? args[idx + 1] : undefined;
  };

  const paymentId = get("--payment-id");
  const txHash = get("--tx-hash");
  const receiverAddress = get("--receiver-address");
  const receiverMemo = get("--receiver-memo");

  let result: GetPaymentResponse;

  if (paymentId) {
    result = await getPayment(paymentId);
  } else if (txHash) {
    result = await checkPaymentByTxHash(txHash);
  } else if (receiverAddress && receiverMemo) {
    result = await checkPaymentByAddressMemo(receiverAddress, receiverMemo);
  } else {
    console.error("Usage: --payment-id <uuid> | --tx-hash <hash> | --receiver-address <addr> --receiver-memo <memo>");
    process.exit(1);
  }

  console.log(JSON.stringify(result, null, 2));
  if (!result.success) process.exit(1);
}


path: scripts/src/parse-qr.ts
content: /**
 * Parse payment QR code content.
 *
 * Supports:
 * - EIP-681: ethereum:<address>[@chainId][/transfer?address=<to>&uint256=<amount>]
 * - Solana Pay: solana:<address>?amount=<amount>[&spl-[REDACTED_SECRET]
 * - Stellar URI: web+stellar:pay?destination=<address>&amount=<amount>[&asset_code=USDC&asset_issuer=...]
 * - Plain addresses: 0x..., base58, G..., C...
 */

export interface ParsedQR {
  type: "eip681" | "solana-pay" | "stellar-uri" | "plain-address";
  address: string;
  chainId?: number;
  token?: string;
  tokenAddress?: string;
  amount?: string;
  memo?: string;
  label?: string;
  message?: string;
  raw: string;
}

// EIP-681 chain ID to Rozo chain ID mapping
const EIP681_CHAIN_MAP: Record<number, number> = {
  1: 1,        // Ethereum
  42161: 42161, // Arbitrum
  8453: 8453,  // Base
  56: 56,      // BSC
  137: 137,    // Polygon
};

// Known ERC-20 token contract → symbol mapping (lowercase)
const EVM_TOKEN_CONTRACTS: Record<string, { symbol: string; chainId: number }> = {
  // USDC
  [REDACTED_SECRET]: { symbol: "USDC", chainId: 1 },
  [REDACTED_SECRET]: { symbol: "USDC", chainId: 42161 },
  [REDACTED_SECRET]: { symbol: "USDC", chainId: 8453 },
  [REDACTED_SECRET]: { symbol: "USDC", chainId: 56 },
  [REDACTED_SECRET]: { symbol: "USDC", chainId: 137 },
  // USDT
  [REDACTED_SECRET]: { symbol: "USDT", chainId: 1 },
  [REDACTED_SECRET]: { symbol: "USDT", chainId: 42161 },
  [REDACTED_SECRET]: { symbol: "USDT", chainId: 56 },
  [REDACTED_SECRET]: { symbol: "USDT", chainId: 137 },
};

// Solana SPL token mint → symbol mapping
const SOLANA_TOKEN_MINTS: Record<string, string> = {
  [REDACTED_SECRET]: "USDC",
  [REDACTED_SECRET]: "USDT",
};

function parseEIP681(uri: string): ParsedQR | null {
  // ethereum:<contract_or_address>[@chainId][/transfer?address=<to>&uint256=<amount>]
  // or ethereum:<address>[@chainId][?value=<wei_amount>]
  const match = uri.match(/^ethereum:(0x[0-9a-fA-F]{40})(?:@(\d+))?(?:\/(\w+))?(?:\?(.+))?$/);
  if (!match) return null;

  const [, addressOrContract, chainIdStr, method, queryStr] = match;
  const params = new URLSearchParams(queryStr ?? "");
  const chainId = chainIdStr ? EIP681_CHAIN_MAP[Number(chainIdStr)] ?? Number(chainIdStr) : undefined;

  // ERC-20 transfer: contract is the token, address param is the recipient
  if (method === "transfer") {
    const recipient = params.get("address");
    const rawAmount = params.get("uint256");
    if (!recipient) return null;

    const contractLower = addressOrContract.toLowerCase();
    const tokenInfo = EVM_TOKEN_CONTRACTS[contractLower];

    return {
      type: "eip681",
      address: recipient,
      chainId: chainId ?? tokenInfo?.chainId,
      [REDACTED_SECRET],
      tokenAddress: addressOrContract,
      amount: rawAmount ?? undefined,
      raw: uri,
    };
  }

  // Simple ETH/native transfer with value (we note it but it's not a stablecoin transfer)
  return {
    type: "eip681",
    address: addressOrContract,
    chainId,
    amount: params.get("value") ?? undefined,
    raw: uri,
  };
}

function parseSolanaPay(uri: string): ParsedQR | null {
  // solana:<address>?amount=<amount>[&spl-[REDACTED_SECRET]
  const match = uri.match(/^solana:([1-9A-HJ-NP-Za-km-z]{32,44})(?:\?(.+))?$/);
  if (!match) return null;

  const [, address, queryStr] = match;
  const params = new URLSearchParams(queryStr ?? "");
  const splToken = params.get("spl-token");

  return {
    type: "solana-pay",
    address,
    chainId: 900,
    [REDACTED_SECRET] ? SOLANA_TOKEN_MINTS[splToken] : undefined,
    tokenAddress: splToken ?? undefined,
    amount: params.get("amount") ?? undefined,
    memo: params.get("memo") ?? undefined,
    label: params.get("label") ?? undefined,
    message: params.get("message") ?? undefined,
    raw: uri,
  };
}

function parseStellarURI(uri: string): ParsedQR | null {
  // web+stellar:pay?destination=<address>&amount=<amount>[&asset_code=USDC&asset_issuer=...]
  const match = uri.match(/^web\+stellar:pay\?(.+)$/);
  if (!match) return null;

  const params = new URLSearchParams(match[1]);
  const destination = params.get("destination");
  if (!destination) return null;

  const assetCode = params.get("asset_code");

  return {
    type: "stellar-uri",
    address: destination,
    chainId: 1500,
    [REDACTED_SECRET] ?? undefined,
    amount: params.get("amount") ?? undefined,
    memo: params.get("memo") ?? undefined,
    raw: uri,
  };
}

function parsePlainAddress(text: string): ParsedQR | null {
  const trimmed = text.trim();

  // EVM
  if (/^0x[0-9a-fA-F]{40}$/.test(trimmed)) {
    return { type: "plain-address", address: trimmed, raw: trimmed };
  }
  // Stellar G-wallet
  if (/^G[A-Z2-7]{55}$/.test(trimmed)) {
    return { type: "plain-address", address: trimmed, chainId: 1500, raw: trimmed };
  }
  // Stellar C-wallet
  if (/^C[A-Z2-7]{55}$/.test(trimmed)) {
    return { type: "plain-address", address: trimmed, chainId: 1500, raw: trimmed };
  }
  // Solana
  if (/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(trimmed)) {
    return { type: "plain-address", address: trimmed, chainId: 900, raw: trimmed };
  }

  return null;
}

export function parseQR(content: string): ParsedQR | null {
  const trimmed = content.trim();

  if (trimmed.startsWith("ethereum:")) return parseEIP681(trimmed);
  if (trimmed.startsWith("solana:")) return parseSolanaPay(trimmed);
  if (trimmed.startsWith("web+stellar:")) return parseStellarURI(trimmed);

  return parsePlainAddress(trimmed);
}

// CLI entry point
if (import.meta.url === `file://${process.argv[1]}`) {
  const input = process.argv.slice(2).join(" ").trim();
  if (!input) {
    console.error("Usage: node parse-qr.js <qr_content>");
    process.exit(1);
  }

  const result = parseQR(input);
  if (result) {
    console.log(JSON.stringify(result, null, 2));
  } else {
    console.error(JSON.stringify({ error: "Could not parse QR content", raw: input }));
    process.exit(1);
  }
}


path: scripts/src/chains.ts
content: export type ChainType = "evm" | "solana" | "stellar";
export type TokenSymbol = "USDC" | "USDT";
export type Direction = "payin" | "payout";

export interface ChainInfo {
  name: string;
  type: ChainType;
}

export interface TokenInfo {
  address: string;
  decimals: number;
}

export const CHAINS: Record<number, ChainInfo> = {
  1: { name: "Ethereum", type: "evm" },
  42161: { name: "Arbitrum", type: "evm" },
  8453: { name: "Base", type: "evm" },
  56: { name: "BSC", type: "evm" },
  137: { name: "Polygon", type: "evm" },
  900: { name: "Solana", type: "solana" },
  1500: { name: "Stellar", type: "stellar" },
};

export const CHAIN_NAME_TO_ID: Record<string, number> = Object.fromEntries(
  Object.entries(CHAINS).map(([id, info]) => [info.name.toLowerCase(), Number(id)])
);

export const PAYIN_TOKENS: Record<TokenSymbol, Record<number, TokenInfo>> = {
  USDC: {
    1: { address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", decimals: 6 },
    42161: { address: "0xaf88d065e77c8cc2239327c5edb3a432268e5831", decimals: 6 },
    8453: { address: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", decimals: 6 },
    56: { address: "0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d", decimals: 18 },
    137: { address: "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", decimals: 6 },
    900: { address: [REDACTED_SECRET], decimals: 6 },
    1500: { address: "USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", decimals: 7 },
  },
  USDT: {
    1: { address: "0xdac17f958d2ee523a2206206994597c13d831ec7", decimals: 6 },
    42161: { address: "0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9", decimals: 6 },
    56: { address: "0x55d398326f99059ff775485246999027b3197955", decimals: 18 },
    137: { address: "0xc2132d05d31c914a87c6611c10748aeb04b58e8f", decimals: 6 },
    900: { address: [REDACTED_SECRET], decimals: 6 },
  },
};

export const PAYOUT_TOKENS: Record<TokenSymbol, Record<number, TokenInfo>> = {
  USDC: {
    1: { address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", decimals: 6 },
    42161: { address: "0xaf88d065e77c8cc2239327c5edb3a432268e5831", decimals: 6 },
    8453: { address: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", decimals: 6 },
    56: { address: "0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d", decimals: 18 },
    137: { address: "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", decimals: 6 },
    900: { address: [REDACTED_SECRET], decimals: 6 },
    1500: { address: "USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", decimals: 7 },
  },
  USDT: {
    1: { address: "0xdac17f958d2ee523a2206206994597c13d831ec7", decimals: 6 },
    42161: { address: "0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9", decimals: 6 },
    8453: { address: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", decimals: 6 },
    56: { address: "0x55d398326f99059ff775485246999027b3197955", decimals: 18 },
    137: { address: "0xc2132d05d31c914a87c6611c10748aeb04b58e8f", decimals: 6 },
  },
};

export function getTokenAddress(
  chainId: number,
  [REDACTED_SECRET],
  direction: Direction = "payin"
): TokenInfo | null {
  const tokens = direction === "payout" ? PAYOUT_TOKENS : PAYIN_TOKENS;
  return tokens[token]?.[chainId] ?? null;
}

export function getChainName(chainId: number): string {
  return CHAINS[chainId]?.name ?? `Unknown (${chainId})`;
}

export function isPayoutSupported(chainId: number, [REDACTED_SECRET]): boolean {
  return PAYOUT_TOKENS[token]?.[chainId] !== undefined;
}


path: scripts/tests/check-stellar-trustline.test.ts
content: import { describe, it, expect, vi, beforeEach } from "vitest";
import { checkStellarTrustline } from "../src/check-stellar-trustline.js";
import { TEST_EVM_ADDRESS_2, TEST_STELLAR_G_ADDRESS } from "./fixtures.js";

const mockFetch = vi.fn();
vi.stubGlobal("fetch", mockFetch);

beforeEach(() => {
  mockFetch.mockReset();
});

describe("checkStellarTrustline", () => {
  it("returns hasTrustline=true when USDC trustline exists", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      json: async () => ({
        balances: [
          { asset_type: "native", balance: "100.0" },
          {
            asset_type: "credit_alphanum4",
            asset_code: "USDC",
            asset_issuer: "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN",
            balance: "250.50",
          },
        ],
      }),
    });

    const result = await checkStellarTrustline(TEST_STELLAR_G_ADDRESS);
    expect(result.hasTrustline).toBe(true);
    expect(result.balance).toBe("250.50");
    expect(result.asset).toBe("USDC");
    expect(result.error).toBeUndefined();
  });

  it("defaults to USDC when no asset specified", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      json: async () => ({ balances: [] }),
    });

    const result = await checkStellarTrustline(TEST_STELLAR_G_ADDRESS);
    expect(result.asset).toBe("USDC");
  });

  it("checks EURC trustline when specified", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      json: async () => ({
        balances: [
          {
            asset_type: "credit_alphanum4",
            asset_code: "EURC",
            asset_issuer: "GDHU6WRG4IEQXM5NZ4BMPKOXHW76MZM4Y2IEMFDVXBSDP6SJY4ITNPP2",
            balance: "75.00",
          },
        ],
      }),
    });

    const result = await checkStellarTrustline(TEST_STELLAR_G_ADDRESS, "EURC");
    expect(result.hasTrustline).toBe(true);
    expect(result.balance).toBe("75.00");
    expect(result.asset).toBe("EURC");
  });

  it("returns hasTrustline=false when no matching trustline", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      json: async () => ({
        balances: [{ asset_type: "native", balance: "100.0" }],
      }),
    });

    const result = await checkStellarTrustline(TEST_STELLAR_G_ADDRESS);
    expect(result.hasTrustline).toBe(false);
    expect(result.balance).toBeUndefined();
  });

  it("ignores USDC from wrong issuer", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      json: async () => ({
        balances: [
          {
            asset_type: "credit_alphanum4",
            asset_code: "USDC",
            asset_issuer: "GDIFFERENTISSUERADDRESSXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
            balance: "100.0",
          },
        ],
      }),
    });

    const result = await checkStellarTrustline(TEST_STELLAR_G_ADDRESS);
    expect(result.hasTrustline).toBe(false);
  });

  it("returns error for 404 (account not found)", async () => {
    mockFetch.mockResolvedValueOnce({ ok: false, status: 404 });

    const result = await checkStellarTrustline(TEST_STELLAR_G_ADDRESS);
    expect(result.hasTrustline).toBe(false);
    expect(result.error).toContain("Account not found");
  });

  it("returns error for other HTTP failures", async () => {
    mockFetch.mockResolvedValueOnce({ ok: false, status: 500 });

    const result = await checkStellarTrustline(TEST_STELLAR_G_ADDRESS);
    expect(result.hasTrustline).toBe(false);
    expect(result.error).toContain("Horizon API error: 500");
  });

  it("rejects invalid address format", async () => {
    const result = await checkStellarTrustline(TEST_EVM_ADDRESS_2);
    expect(result.hasTrustline).toBe(false);
    expect(result.error).toContain("Not a valid Stellar G-wallet");
    expect(mockFetch).not.toHaveBeenCalled();
  });

  it("rejects C-wallet address", async () => {
    const result = await checkStellarTrustline(
      "CABCDEFGHIJKLMNOPQRSTUVWXYZ234567ABCDEFGHIJKLMNOPQRSTUV23"
    );
    expect(result.hasTrustline).toBe(false);
    expect(result.error).toContain("Not a valid Stellar G-wallet");
  });

  it("calls correct Horizon URL", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      json: async () => ({ balances: [] }),
    });

    await checkStellarTrustline(TEST_STELLAR_G_ADDRESS);
    expect(mockFetch).toHaveBeenCalledWith(
      `https://horizon.stellar.org/accounts/${TEST_STELLAR_G_ADDRESS}`
    );
  });

  it("handles network error (fetch throws)", async () => {
    mockFetch.mockRejectedValueOnce(new Error("ETIMEDOUT"));
    const result = await checkStellarTrustline(TEST_STELLAR_G_ADDRESS);
    expect(result.hasTrustline).toBe(false);
    expect(result.error).toContain("Network error");
  });

  it("handles non-JSON response", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      json: async () => { throw new SyntaxError("Unexpected token"); },
    });
    const result = await checkStellarTrustline(TEST_STELLAR_G_ADDRESS);
    expect(result.hasTrustline).toBe(false);
    expect(result.error).toContain("Invalid JSON");
  });
});


path: scripts/tests/fixtures.ts
content: import { readFileSync } from "fs";
import { resolve } from "path";

function loadEnvDev(): Record<string, string> {
  const envPath = resolve(import.meta.dirname, "../.env.dev");
  const content = readFileSync(envPath, "utf-8");
  const vars: Record<string, string> = {};
  for (const line of content.split("\n")) {
    const trimmed = line.trim();
    if (!trimmed || trimmed.startsWith("#")) continue;
    const eqIdx = trimmed.indexOf("=");
    if (eqIdx === -1) continue;
    vars[trimmed.slice(0, eqIdx)] = trimmed.slice(eqIdx + 1);
  }
  return vars;
}

const env = loadEnvDev();

export const TEST_EVM_ADDRESS = env.TEST_EVM_ADDRESS!;
export const TEST_EVM_ADDRESS_2 = env.TEST_EVM_ADDRESS_2!;
export const TEST_SOLANA_ADDRESS = env.TEST_SOLANA_ADDRESS!;
export const TEST_SOLANA_ADDRESS_2 = env.TEST_SOLANA_ADDRESS_2!;
export const TEST_STELLAR_G_ADDRESS = env.TEST_STELLAR_G_ADDRESS!;
export const TEST_STELLAR_C_ADDRESS = env.TEST_STELLAR_C_ADDRESS!;


path: scripts/tests/parse-qr.test.ts
content: import { describe, it, expect } from "vitest";
import { parseQR } from "../src/parse-qr.js";
import {
  TEST_EVM_ADDRESS,
  TEST_EVM_ADDRESS_2,
  TEST_SOLANA_ADDRESS_2,
  TEST_STELLAR_G_ADDRESS,
  TEST_STELLAR_C_ADDRESS,
} from "./fixtures.js";

describe("parseQR", () => {
  // --- Plain addresses ---
  describe("plain addresses", () => {
    it("parses EVM address", () => {
      const r = parseQR(TEST_EVM_ADDRESS);
      expect(r).not.toBeNull();
      expect(r!.type).toBe("plain-address");
      expect(r!.address).toBe(TEST_EVM_ADDRESS);
      expect(r!.chainId).toBeUndefined();
    });

    it("parses Solana address", () => {
      const r = parseQR(TEST_SOLANA_ADDRESS_2);
      expect(r!.type).toBe("plain-address");
      expect(r!.chainId).toBe(900);
    });

    it("parses Stellar G-wallet", () => {
      const r = parseQR(TEST_STELLAR_G_ADDRESS);
      expect(r!.type).toBe("plain-address");
      expect(r!.chainId).toBe(1500);
    });

    it("parses Stellar C-wallet", () => {
      const r = parseQR(TEST_STELLAR_C_ADDRESS);
      expect(r!.type).toBe("plain-address");
      expect(r!.chainId).toBe(1500);
    });

    it("returns null for invalid content", () => {
      expect(parseQR("hello world")).toBeNull();
      expect(parseQR("0xinvalid")).toBeNull();
    });
  });

  // --- EIP-681 ---
  describe("EIP-681", () => {
    it("parses simple address", () => {
      const r = parseQR(`ethereum:${TEST_EVM_ADDRESS_2}`);
      expect(r!.type).toBe("eip681");
      expect(r!.address).toBe(TEST_EVM_ADDRESS_2);
      expect(r!.chainId).toBeUndefined();
    });

    it("parses with value (native ETH transfer)", () => {
      const r = parseQR(`ethereum:${TEST_EVM_ADDRESS_2}?value=1e18`);
      expect(r!.type).toBe("eip681");
      expect(r!.amount).toBe("1e18");
    });

    it("parses with chain ID", () => {
      const r = parseQR(`ethereum:${TEST_EVM_ADDRESS_2}@1?value=1.5e18`);
      expect(r!.chainId).toBe(1);
      expect(r!.amount).toBe("1.5e18");
    });

    it("parses ERC-20 transfer with known USDC contract (Base)", () => {
      const r = parseQR(
        `ethereum:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913/transfer?address=${TEST_EVM_ADDRESS}&uint256=1000000`
      );
      expect(r!.type).toBe("eip681");
      expect(r!.address).toBe(TEST_EVM_ADDRESS);
      expect(r!.token).toBe("USDC");
      expect(r!.tokenAddress).toBe([REDACTED_SECRET]);
      expect(r!.chainId).toBe(8453);
      expect(r!.amount).toBe("1000000");
    });

    it("parses ERC-20 transfer with explicit chain ID override", () => {
      const r = parseQR(
        `ethereum:0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913@8453/transfer?address=${TEST_EVM_ADDRESS}&uint256=5000000`
      );
      expect(r!.chainId).toBe(8453);
      expect(r!.token).toBe("USDC");
      expect(r!.amount).toBe("5000000");
    });

    it("parses ERC-20 transfer with known USDT contract (Ethereum)", () => {
      const r = parseQR(
        `ethereum:0xdAC17F958D2ee523a2206206994597C13D831ec7/transfer?address=${TEST_EVM_ADDRESS_2}&uint256=50000000`
      );
      expect(r!.token).toBe("USDT");
      expect(r!.chainId).toBe(1);
    });

    it("parses ERC-20 with unknown token contract", () => {
      const r = parseQR(
        `ethereum:0x6B175474E89094C44Da98b954EedeAC495271d0F@10/transfer?address=${TEST_EVM_ADDRESS_2}&uint256=42`
      );
      expect(r!.type).toBe("eip681");
      expect(r!.token).toBeUndefined();
      expect(r!.chainId).toBe(10);
      expect(r!.tokenAddress).toBe([REDACTED_SECRET]);
    });
  });

  // --- Solana Pay ---
  describe("Solana Pay", () => {
    it("parses plain solana URI", () => {
      const r = parseQR(`solana:${TEST_SOLANA_ADDRESS_2}`);
      expect(r!.type).toBe("solana-pay");
      expect(r!.address).toBe(TEST_SOLANA_ADDRESS_2);
      expect(r!.chainId).toBe(900);
    });

    it("parses with amount (SOL transfer)", () => {
      const r = parseQR(`solana:${TEST_SOLANA_ADDRESS_2}?amount=2.5`);
      expect(r!.amount).toBe("2.5");
    });

    it("parses SPL token transfer with known USDC mint", () => {
      const r = parseQR(
        `solana:${TEST_SOLANA_ADDRESS_2}?amount=10.25&spl-[REDACTED_SECRET]`
      );
      expect(r!.token).toBe("USDC");
      expect(r!.amount).toBe("10.25");
      expect(r!.tokenAddress).toBe([REDACTED_SECRET]);
    });

    it("parses SPL token transfer with known USDT mint", () => {
      const r = parseQR(
        `solana:${TEST_SOLANA_ADDRESS_2}?amount=5&spl-[REDACTED_SECRET]`
      );
      expect(r!.token).toBe("USDT");
    });

    it("parses with label, message, and memo", () => {
      const r = parseQR(
        `solana:${TEST_SOLANA_ADDRESS_2}?amount=5&label=Charity&message=Donation&memo=ThankYou`
      );
      expect(r!.amount).toBe("5");
      expect(r!.label).toBe("Charity");
      expect(r!.message).toBe("Donation");
      expect(r!.memo).toBe("ThankYou");
    });

    it("parses with unknown SPL token", () => {
      const r = parseQR(
        `solana:${TEST_SOLANA_ADDRESS_2}?amount=1&spl-[REDACTED_SECRET]`
      );
      expect(r!.token).toBeUndefined();
      expect(r!.tokenAddress).toBe([REDACTED_SECRET]);
    });
  });

  // --- Stellar URI ---
  describe("Stellar URI", () => {
    it("parses basic pay request", () => {
      const r = parseQR(
        `web+stellar:pay?destination=${TEST_STELLAR_G_ADDRESS}&amount=100&asset_code=USDC`
      );
      expect(r!.type).toBe("stellar-uri");
      expect(r!.address).toBe(TEST_STELLAR_G_ADDRESS);
      expect(r!.chainId).toBe(1500);
      expect(r!.token).toBe("USDC");
      expect(r!.amount).toBe("100");
    });

    it("parses with memo", () => {
      const r = parseQR(
        `web+stellar:pay?destination=${TEST_STELLAR_G_ADDRESS}&amount=50&asset_code=USDC&memo=invoice-123`
      );
      expect(r!.memo).toBe("invoice-123");
    });

    it("parses without asset_code (native XLM)", () => {
      const r = parseQR(
        `web+stellar:pay?destination=${TEST_STELLAR_G_ADDRESS}&amount=10`
      );
      expect(r!.token).toBeUndefined();
      expect(r!.amount).toBe("10");
    });

    it("returns null for missing destination", () => {
      const r = parseQR("web+stellar:pay?amount=10&asset_code=USDC");
      expect(r).toBeNull();
    });
  });

  // --- Edge cases ---
  describe("edge cases", () => {
    it("handles whitespace around content", () => {
      const r = parseQR(`  ${TEST_EVM_ADDRESS}  `);
      expect(r).not.toBeNull();
      expect(r!.type).toBe("plain-address");
    });

    it("returns null for empty string", () => {
      expect(parseQR("")).toBeNull();
    });
  });
});


path: scripts/tests/get-payment.test.ts
content: import { describe, it, expect, vi, beforeEach } from "vitest";
import { getPayment, checkPaymentByTxHash, checkPaymentByAddressMemo } from "../src/get-payment.js";

const mockFetch = vi.fn();
vi.stubGlobal("fetch", mockFetch);

beforeEach(() => {
  mockFetch.mockReset();
});

describe("getPayment", () => {
  it("returns payment on success", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      status: 200,
      json: async () => ({
        id: "550e8400-e29b-41d4-a716-446655440000",
        status: "payment_completed",
        type: "exactOut",
      }),
    });

    const result = await getPayment("550e8400-e29b-41d4-a716-446655440000");
    expect(result.success).toBe(true);
    expect(result.payment?.status).toBe("payment_completed");
    expect(mockFetch).toHaveBeenCalledWith(
      "https://intentapiv4.rozo.ai/functions/v1/payment-api/payments/550e8400-e29b-41d4-a716-446655440000"
    );
  });

  it("handles 404 not found", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: false,
      status: 404,
      json: async () => ({ error: { code: "NOT_FOUND", message: "Payment not found" } }),
    });

    const result = await getPayment("nonexistent-id");
    expect(result.success).toBe(false);
    expect(result.statusCode).toBe(404);
  });

  it("returns all payment statuses correctly", async () => {
    const statuses = [
      "payment_unpaid",
      "payment_started",
      "payment_payin_completed",
      "payment_payout_completed",
      "payment_completed",
      "payment_bounced",
      "payment_expired",
      "payment_refunded",
    ];

    for (const status of statuses) {
      mockFetch.mockResolvedValueOnce({
        ok: true,
        status: 200,
        json: async () => ({ id: "test", status }),
      });

      const result = await getPayment("test");
      expect(result.success).toBe(true);
      expect(result.payment?.status).toBe(status);
    }
  });

  it("handles network error (fetch throws)", async () => {
    mockFetch.mockRejectedValueOnce(new Error("ENOTFOUND"));
    const result = await getPayment("some-id");
    expect(result.success).toBe(false);
    expect(result.error).toContain("Network error");
  });

  it("handles non-JSON response", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      status: 200,
      json: async () => { throw new SyntaxError("Unexpected token"); },
    });
    const result = await getPayment("some-id");
    expect(result.success).toBe(false);
    expect(result.error).toContain("Invalid JSON");
  });
});

describe("checkPaymentByTxHash", () => {
  it("looks up by Stellar tx hash", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      status: 200,
      json: async () => ({ id: "found-by-hash", status: "payment_completed" }),
    });

    const result = await checkPaymentByTxHash("817089164daae43566dade1f7e83028430308a96662a556f1abcb131b10bbe59");
    expect(result.success).toBe(true);
    expect(result.payment?.id).toBe("found-by-hash");
    expect(mockFetch).toHaveBeenCalledWith(
      expect.stringContaining("/payments/check?txHash=817089164daae43566dade1f7e83028430308a96662a556f1abcb131b10bbe59")
    );
  });

  it("looks up by EVM tx hash", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      status: 200,
      json: async () => ({ id: "evm-hash-result", status: "payment_payin_completed" }),
    });

    const result = await checkPaymentByTxHash("0x767f62ca11fe708e7d1267f7f89f6e4b84e347e1aba29a8b855bbbbec9d645e3");
    expect(result.success).toBe(true);
    expect(result.payment?.status).toBe("payment_payin_completed");
  });

  it("returns 404 when no match in last 7 days", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: false,
      status: 404,
      json: async () => ({ error: { code: "paymentNotFound" } }),
    });

    const result = await checkPaymentByTxHash("0xnonexistent");
    expect(result.success).toBe(false);
    expect(result.statusCode).toBe(404);
  });

  it("handles network error", async () => {
    mockFetch.mockRejectedValueOnce(new Error("ECONNREFUSED"));
    const result = await checkPaymentByTxHash("0xabc");
    expect(result.success).toBe(false);
    expect(result.error).toContain("Network error");
  });
});

describe("checkPaymentByAddressMemo", () => {
  it("looks up by Stellar address + memo", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      status: 200,
      json: async () => ({ id: "found-by-memo", status: "payment_completed" }),
    });

    const result = await checkPaymentByAddressMemo(
      "GB4CLV3UMXDPFP5OQJQKUCWPRJXPXPJSHTUKZEJLAIZFZR7UHYAQ6EB4",
      "83589538"
    );
    expect(result.success).toBe(true);
    expect(result.payment?.id).toBe("found-by-memo");
    expect(mockFetch).toHaveBeenCalledWith(
      expect.stringContaining([REDACTED_SECRET])
    );
    expect(mockFetch).toHaveBeenCalledWith(
      expect.stringContaining("receiverMemo=83589538")
    );
  });

  it("looks up by Solana address + memo", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      status: 200,
      json: async () => ({ id: "solana-memo-result", status: "payment_started" }),
    });

    const result = await checkPaymentByAddressMemo(
      [REDACTED_SECRET],
      "68253860"
    );
    expect(result.success).toBe(true);
  });

  it("returns 404 when no match", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: false,
      status: 404,
      json: async () => ({ error: { code: "paymentNotFound" } }),
    });

    const result = await checkPaymentByAddressMemo("GB4CLV3U...", "99999999");
    expect(result.success).toBe(false);
    expect(result.statusCode).toBe(404);
  });

  it("handles network error", async () => {
    mockFetch.mockRejectedValueOnce(new Error("ETIMEDOUT"));
    const result = await checkPaymentByAddressMemo("addr", "memo");
    expect(result.success).toBe(false);
    expect(result.error).toContain("Network error");
  });
});


path: scripts/tests/create-payment.test.ts
content: import { describe, it, expect, vi, beforeEach } from "vitest";
import { createPayment, type CreatePaymentParams } from "../src/create-payment.js";
import { TEST_EVM_ADDRESS_2, TEST_STELLAR_G_ADDRESS } from "./fixtures.js";

const mockFetch = vi.fn();
vi.stubGlobal("fetch", mockFetch);

beforeEach(() => {
  mockFetch.mockReset();
});

const validParams: CreatePaymentParams = {
  sourceChain: 8453,
  sourceToken: "USDC",
  destChain: 137,
  destAddress: TEST_EVM_ADDRESS_2,
  destToken: "USDC",
  destAmount: "10.00",
};

describe("createPayment", () => {
  it("rejects amount below minimum ($0.01)", async () => {
    const result = await createPayment({ ...validParams, destAmount: "0.001" });
    expect(result.success).toBe(false);
    expect(result.error).toContain("Amount must be between");
  });

  it("rejects amount above maximum ($10,000)", async () => {
    const result = await createPayment({ ...validParams, destAmount: "10001" });
    expect(result.success).toBe(false);
    expect(result.error).toContain("Amount must be between");
  });

  it("rejects non-numeric amount", async () => {
    const result = await createPayment({ ...validParams, destAmount: "abc" });
    expect(result.success).toBe(false);
    expect(result.error).toContain("Amount must be between");
  });

  it("accepts minimum amount ($0.01)", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true, status: 201,
      json: async () => ({ id: "min-uuid" }),
    });
    const result = await createPayment({ ...validParams, destAmount: "0.01" });
    expect(result.success).toBe(true);
  });

  it("accepts maximum amount ($10,000)", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true, status: 201,
      json: async () => ({ id: "max-uuid" }),
    });
    const result = await createPayment({ ...validParams, destAmount: "10000" });
    expect(result.success).toBe(true);
  });

  it("rejects unsupported payout chain", async () => {
    const result = await createPayment({
      ...validParams,
      destChain: 9999,
      destToken: "USDC",
    });
    expect(result.success).toBe(false);
    expect(result.error).toContain("Payout not supported");
  });

  it("rejects unsupported source token on chain", async () => {
    const result = await createPayment({
      ...validParams,
      sourceChain: 8453,
      sourceToken: "USDT",
    });
    expect(result.success).toBe(false);
    expect(result.error).toContain("Pay-in not supported");
  });

  it("rejects USDT payout to Solana", async () => {
    const result = await createPayment({
      ...validParams,
      destChain: 900,
      destToken: "USDT",
    });
    expect(result.success).toBe(false);
    expect(result.error).toContain("Payout not supported");
  });

  it("rejects USDT payout to Stellar", async () => {
    const result = await createPayment({
      ...validParams,
      destChain: 1500,
      destToken: "USDT",
    });
    expect(result.success).toBe(false);
    expect(result.error).toContain("Payout not supported");
  });

  it("sends correct payload for exactOut", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      status: 201,
      json: async () => ({ id: "test-uuid", status: "payment_unpaid" }),
    });

    const result = await createPayment(validParams);
    expect(result.success).toBe(true);
    expect(result.payment).toEqual({ id: "test-uuid", status: "payment_unpaid" });

    const call = mockFetch.mock.calls[0];
    expect(call[0]).toBe("https://intentapiv4.rozo.ai/functions/v1/payment-api");

    const body = JSON.parse(call[1].body);
    expect(body.appId).toBe("rozoAgent");
    expect(body.type).toBe("exactOut");
    expect(body.source.chainId).toBe(8453);
    expect(body.source.tokenSymbol).toBe("USDC");
    expect(body.destination.amount).toBe("10.00");
    expect(body.destination.receiverAddress).toBe(TEST_EVM_ADDRESS_2);
    expect(body.source.amount).toBeUndefined();
  });

  it("sends correct payload for exactIn", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      status: 201,
      json: async () => ({ id: "test-uuid-2" }),
    });

    await createPayment({
      ...validParams,
      type: "exactIn",
      sourceAmount: "11.00",
    });

    const body = JSON.parse(mockFetch.mock.calls[0][1].body);
    expect(body.type).toBe("exactIn");
    expect(body.source.amount).toBe("11.00");
    expect(body.destination.amount).toBeUndefined();
  });

  it("includes memo for Stellar C-wallet", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      status: 201,
      json: async () => ({ id: "stellar-uuid" }),
    });

    await createPayment({
      ...validParams,
      destChain: 1500,
      destToken: "USDC",
      destAddress: TEST_STELLAR_G_ADDRESS,
      destMemo: "contract-memo-123",
    });

    const body = JSON.parse(mockFetch.mock.calls[0][1].body);
    expect(body.destination.receiverMemo).toBe("contract-memo-123");
  });

  it("does not include memo when not provided", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      status: 201,
      json: async () => ({ id: "no-memo-uuid" }),
    });

    await createPayment(validParams);

    const body = JSON.parse(mockFetch.mock.calls[0][1].body);
    expect(body.destination.receiverMemo).toBeUndefined();
  });

  it("generates orderId if not provided", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      status: 201,
      json: async () => ({ id: "uuid" }),
    });

    await createPayment(validParams);

    const body = JSON.parse(mockFetch.mock.calls[0][1].body);
    expect(body.orderId).toBeDefined();
    expect(body.orderId.length).toBeGreaterThan(0);
  });

  it("uses provided orderId", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      status: 201,
      json: async () => ({ id: "uuid" }),
    });

    await createPayment({ ...validParams, orderId: "my-order-123" });

    const body = JSON.parse(mockFetch.mock.calls[0][1].body);
    expect(body.orderId).toBe("my-order-123");
  });

  it("handles API error response", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: false,
      status: 400,
      json: async () => ({ error: { code: "INVALID", message: "Bad request" } }),
    });

    const result = await createPayment(validParams);
    expect(result.success).toBe(false);
    expect(result.statusCode).toBe(400);
  });

  it("handles 409 conflict", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: false,
      status: 409,
      json: async () => ({ error: { code: "CONFLICT", message: "Duplicate orderId" } }),
    });

    const result = await createPayment({ ...validParams, orderId: "dup-id" });
    expect(result.success).toBe(false);
    expect(result.statusCode).toBe(409);
  });

  it("allows BSC USDT payout", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      status: 201,
      json: async () => ({ id: "bsc-uuid" }),
    });

    const result = await createPayment({
      sourceChain: 56,
      sourceToken: "USDT",
      destChain: 56,
      destAddress: TEST_EVM_ADDRESS_2,
      destToken: "USDT",
      destAmount: "50.00",
    });

    expect(result.success).toBe(true);
    const body = JSON.parse(mockFetch.mock.calls[0][1].body);
    expect(body.source.tokenSymbol).toBe("USDT");
    expect(body.destination.tokenSymbol).toBe("USDT");
  });

  it("handles network error (fetch throws)", async () => {
    mockFetch.mockRejectedValueOnce(new Error("ECONNREFUSED"));
    const result = await createPayment(validParams);
    expect(result.success).toBe(false);
    expect(result.error).toContain("Network error");
  });

  it("handles non-JSON response", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      status: 201,
      json: async () => { throw new SyntaxError("Unexpected token"); },
    });
    const result = await createPayment(validParams);
    expect(result.success).toBe(false);
    expect(result.error).toContain("Invalid JSON");
  });
});


path: scripts/tests/chains.test.ts
content: import { describe, it, expect } from "vitest";
import {
  CHAINS,
  CHAIN_NAME_TO_ID,
  PAYIN_TOKENS,
  PAYOUT_TOKENS,
  getTokenAddress,
  getChainName,
  isPayoutSupported,
} from "../src/chains.js";

describe("CHAINS", () => {
  it("has all 7 supported chains", () => {
    expect(Object.keys(CHAINS)).toHaveLength(7);
  });

  it("maps chain IDs to correct names", () => {
    expect(CHAINS[1].name).toBe("Ethereum");
    expect(CHAINS[8453].name).toBe("Base");
    expect(CHAINS[56].name).toBe("BSC");
    expect(CHAINS[900].name).toBe("Solana");
    expect(CHAINS[1500].name).toBe("Stellar");
  });

  it("maps chain IDs to correct types", () => {
    expect(CHAINS[1].type).toBe("evm");
    expect(CHAINS[42161].type).toBe("evm");
    expect(CHAINS[900].type).toBe("solana");
    expect(CHAINS[1500].type).toBe("stellar");
  });
});

describe("CHAIN_NAME_TO_ID", () => {
  it("maps lowercase chain names back to IDs", () => {
    expect(CHAIN_NAME_TO_ID["ethereum"]).toBe(1);
    expect(CHAIN_NAME_TO_ID["base"]).toBe(8453);
    expect(CHAIN_NAME_TO_ID["solana"]).toBe(900);
    expect(CHAIN_NAME_TO_ID["stellar"]).toBe(1500);
  });
});

describe("PAYIN_TOKENS", () => {
  it("has USDC on all 7 chains", () => {
    expect(Object.keys(PAYIN_TOKENS.USDC)).toHaveLength(7);
  });

  it("has USDT on 5 chains (no Base, no Stellar)", () => {
    expect(Object.keys(PAYIN_TOKENS.USDT)).toHaveLength(5);
    expect(PAYIN_TOKENS.USDT[8453]).toBeUndefined();
    expect(PAYIN_TOKENS.USDT[1500]).toBeUndefined();
  });

  it("BSC uses 18 decimals", () => {
    expect(PAYIN_TOKENS.USDC[56].decimals).toBe(18);
    expect(PAYIN_TOKENS.USDT[56].decimals).toBe(18);
  });

  it("Stellar uses 7 decimals", () => {
    expect(PAYIN_TOKENS.USDC[1500].decimals).toBe(7);
  });
});

describe("PAYOUT_TOKENS", () => {
  it("has USDC on 7 payout chains (including Arbitrum)", () => {
    expect(Object.keys(PAYOUT_TOKENS.USDC)).toHaveLength(7);
    expect(PAYOUT_TOKENS.USDC[42161]).toBeDefined();
  });

  it("has USDT on 5 EVM payout chains (including Arbitrum)", () => {
    expect(Object.keys(PAYOUT_TOKENS.USDT)).toHaveLength(5);
    expect(PAYOUT_TOKENS.USDT[1]).toBeDefined();
    expect(PAYOUT_TOKENS.USDT[42161]).toBeDefined();
    expect(PAYOUT_TOKENS.USDT[8453]).toBeDefined();
    expect(PAYOUT_TOKENS.USDT[56]).toBeDefined();
    expect(PAYOUT_TOKENS.USDT[137]).toBeDefined();
  });

  it("has no USDT payout on Solana or Stellar", () => {
    expect(PAYOUT_TOKENS.USDT[900]).toBeUndefined();
    expect(PAYOUT_TOKENS.USDT[1500]).toBeUndefined();
  });
});

describe("getTokenAddress", () => {
  it("returns token info for valid payin combo", () => {
    const info = getTokenAddress(1, "USDC", "payin");
    expect(info).not.toBeNull();
    expect(info!.address).toBe("0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48");
    expect(info!.decimals).toBe(6);
  });

  it("returns token info for valid payout combo", () => {
    const info = getTokenAddress(8453, "USDC", "payout");
    expect(info).not.toBeNull();
    expect(info!.address).toBe("0x833589fcd6edb6e08f4c7c32d4f71b54bda02913");
  });

  it("returns token info for Arbitrum payout", () => {
    expect(getTokenAddress(42161, "USDC", "payout")).not.toBeNull();
    expect(getTokenAddress(42161, "USDT", "payout")).not.toBeNull();
  });

  it("returns null for unsupported combo", () => {
    expect(getTokenAddress(900, "USDT", "payout")).toBeNull();
    expect(getTokenAddress(9999, "USDC", "payin")).toBeNull();
  });

  it("defaults to payin direction", () => {
    const info = getTokenAddress(42161, "USDC");
    expect(info).not.toBeNull();
  });
});

describe("getChainName", () => {
  it("returns correct name for known chains", () => {
    expect(getChainName(1)).toBe("Ethereum");
    expect(getChainName(900)).toBe("Solana");
  });

  it("returns fallback for unknown chain", () => {
    expect(getChainName(9999)).toBe("Unknown (9999)");
  });
});

describe("isPayoutSupported", () => {
  it("returns true for supported combos", () => {
    expect(isPayoutSupported(8453, "USDC")).toBe(true);
    expect(isPayoutSupported(1, "USDT")).toBe(true);
    expect(isPayoutSupported(56, "USDC")).toBe(true);
    expect(isPayoutSupported(56, "USDT")).toBe(true);
    expect(isPayoutSupported(42161, "USDC")).toBe(true);
    expect(isPayoutSupported(42161, "USDT")).toBe(true);
  });

  it("returns false for unsupported combos", () => {
    expect(isPayoutSupported(900, "USDT")).toBe(false);
    expect(isPayoutSupported(1500, "USDT")).toBe(false);
  });
});


path: scripts/tests/check-balance.test.ts
content: import { describe, it, expect, vi, beforeEach } from "vitest";
import { checkBalance } from "../src/check-balance.js";
import {
  TEST_EVM_ADDRESS,
  TEST_SOLANA_ADDRESS,
  TEST_STELLAR_G_ADDRESS,
  TEST_STELLAR_C_ADDRESS,
} from "./fixtures.js";

const mockFetch = vi.fn();
vi.stubGlobal("fetch", mockFetch);

beforeEach(() => {
  mockFetch.mockReset();
});

describe("checkBalance", () => {
  it("auto-detects EVM and calls correct URL", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      json: async () => ({
        address: TEST_EVM_ADDRESS,
        chain: "evm",
        balances: [
          { token: "USDC", chain: "ethereum", balance: "1155.834397", decimals: 6 },
          { token: "USDT", chain: "ethereum", balance: "53.630000", decimals: 6 },
        ],
      }),
    });

    const result = await checkBalance(TEST_EVM_ADDRESS);
    expect(result.chain).toBe("evm");
    expect(result.balances).toHaveLength(2);
    expect(result.balances[0].token).toBe("USDC");
    expect(mockFetch).toHaveBeenCalledWith(
      `https://api-balance.rozo-deeplink.workers.dev/balance/${TEST_EVM_ADDRESS}?chain=evm`
    );
  });

  it("auto-detects Solana", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      json: async () => ({
        address: TEST_SOLANA_ADDRESS,
        chain: "solana",
        balances: [
          { token: "USDC", chain: "solana", balance: "552.942981", decimals: 6 },
        ],
      }),
    });

    const result = await checkBalance(TEST_SOLANA_ADDRESS);
    expect(result.chain).toBe("solana");
    expect(mockFetch).toHaveBeenCalledWith(
      expect.stringContaining("?chain=solana")
    );
  });

  it("auto-detects Stellar G-wallet", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      json: async () => ({
        address: TEST_STELLAR_G_ADDRESS,
        chain: "stellar",
        balances: [
          { token: "USDC", chain: "stellar", balance: "216.0158044", decimals: 7 },
        ],
      }),
    });

    const result = await checkBalance(TEST_STELLAR_G_ADDRESS);
    expect(result.chain).toBe("stellar");
    expect(result.balances[0].balance).toBe("216.0158044");
  });

  it("auto-detects Stellar C-wallet", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      json: async () => ({
        address: TEST_STELLAR_C_ADDRESS,
        chain: "stellar",
        balances: [
          { token: "USDC", chain: "stellar", balance: "5.0799517", decimals: 7 },
        ],
      }),
    });

    const result = await checkBalance(TEST_STELLAR_C_ADDRESS);
    expect(result.chain).toBe("stellar");
  });

  it("accepts explicit chain override", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      json: async () => ({
        address: TEST_EVM_ADDRESS,
        chain: "evm",
        balances: [],
      }),
    });

    await checkBalance(TEST_EVM_ADDRESS, "evm");
    expect(mockFetch).toHaveBeenCalledWith(
      expect.stringContaining("?chain=evm")
    );
  });

  it("returns error for unrecognized address format", async () => {
    const result = await checkBalance("invalid-address");
    expect(result.error).toContain("Cannot detect chain");
    expect(result.balances).toHaveLength(0);
    expect(mockFetch).not.toHaveBeenCalled();
  });

  it("handles API error response", async () => {
    mockFetch.mockResolvedValueOnce({ ok: false, status: 500 });

    const result = await checkBalance(TEST_EVM_ADDRESS);
    expect(result.error).toContain("Balance API error: 500");
    expect(result.balances).toHaveLength(0);
  });

  it("returns all balances across chains for EVM", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      json: async () => ({
        address: TEST_EVM_ADDRESS,
        chain: "evm",
        balances: [
          { token: "USDT", chain: "ethereum", balance: "53.63", decimals: 6 },
          { token: "USDC", chain: "ethereum", balance: "1155.83", decimals: 6 },
          { token: "USDC", chain: "base", balance: "100.81", decimals: 6 },
          { token: "USDT", chain: "bsc", balance: "22.74", decimals: 18 },
          { token: "USDC", chain: "bsc", balance: "1.61", decimals: 18 },
        ],
      }),
    });

    const result = await checkBalance(TEST_EVM_ADDRESS);
    expect(result.balances).toHaveLength(5);
    const bscUsdt = result.balances.find(b => b.token === "USDT" && b.chain === "bsc");
    expect(bscUsdt).toBeDefined();
    expect(bscUsdt!.decimals).toBe(18);
  });

  it("handles network error (fetch throws)", async () => {
    mockFetch.mockRejectedValueOnce(new Error("ECONNREFUSED"));
    const result = await checkBalance(TEST_EVM_ADDRESS);
    expect(result.error).toContain("Network error");
    expect(result.balances).toHaveLength(0);
  });

  it("handles non-JSON response", async () => {
    mockFetch.mockResolvedValueOnce({
      ok: true,
      json: async () => { throw new SyntaxError("Unexpected token"); },
    });
    const result = await checkBalance(TEST_EVM_ADDRESS);
    expect(result.error).toContain("Invalid JSON");
    expect(result.balances).toHaveLength(0);
  });
});


path: scripts/dist/check-stellar-trustline.js
content: /**
 * Check if a Stellar G-wallet has a trustline for a given asset via the Horizon API.
 * Supports USDC (default) and EURC.
 */
const HORIZON_URL = "https://horizon.stellar.org";
const ASSET_ISSUERS = {
    USDC: "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN",
    EURC: "GDHU6WRG4IEQXM5NZ4BMPKOXHW76MZM4Y2IEMFDVXBSDP6SJY4ITNPP2",
};
export async function checkStellarTrustline(address, asset = "USDC") {
    if (!address.startsWith("G") || address.length !== 56) {
        return { address, asset, hasTrustline: false, error: "Not a valid Stellar G-wallet address" };
    }
    const issuer = ASSET_ISSUERS[asset];
    if (!issuer) {
        return { address, asset, hasTrustline: false, error: `Unsupported asset: ${asset}` };
    }
    let response;
    try {
        response = await fetch(`${HORIZON_URL}/accounts/${address}`);
    }
    catch (err) {
        return { address, asset, hasTrustline: false, error: `Network error: ${err.message}` };
    }
    if (!response.ok) {
        if (response.status === 404) {
            return { address, asset, hasTrustline: false, error: "Account not found on Stellar network" };
        }
        return { address, asset, hasTrustline: false, error: `Horizon API error: ${response.status}` };
    }
    let account;
    try {
        account = await response.json();
    }
    catch {
        return { address, asset, hasTrustline: false, error: "Invalid JSON response from Horizon API" };
    }
    const found = account.balances.find((b) => b.asset_code === asset && b.asset_issuer === issuer);
    if (found) {
        return { address, asset, hasTrustline: true, balance: found.balance };
    }
    return { address, asset, hasTrustline: false };
}
// CLI entry point
if (import.meta.url === `file://${process.argv[1]}`) {
    const args = process.argv.slice(2);
    const addrIdx = args.indexOf("--address");
    const assetIdx = args.indexOf("--asset");
    if (addrIdx === -1) {
        console.error("Usage: --address <stellar_g_wallet> [--asset <USDC|EURC>]");
        process.exit(1);
    }
    const asset = assetIdx !== -1 ? args[assetIdx + 1] : "USDC";
    const result = await checkStellarTrustline(args[addrIdx + 1], asset);
    console.log(JSON.stringify(result, null, 2));
    if (!result.hasTrustline)
        process.exit(1);
}


path: scripts/dist/get-payment.js
content: const API_BASE = "https://intentapiv4.rozo.ai/functions/v1/payment-api";
/**
 * Get payment by payment ID.
 */
export async function getPayment(paymentId) {
    return fetchPayment(`${API_BASE}/payments/${paymentId}`);
}
/**
 * Check payment by source transaction hash.
 * Looks back 7 days, returns the most recent matching payment.
 */
export async function checkPaymentByTxHash(txHash) {
    return fetchPayment(`${API_BASE}/payments/check?txHash=${encodeURIComponent(txHash)}`);
}
/**
 * Check payment by deposit address + memo.
 * Looks back 7 days, returns the most recent matching payment.
 */
export async function checkPaymentByAddressMemo(receiverAddress, receiverMemo) {
    const params = new URLSearchParams({ receiverAddress, receiverMemo });
    return fetchPayment(`${API_BASE}/payments/check?${params}`);
}
async function fetchPayment(url) {
    let response;
    try {
        response = await fetch(url);
    }
    catch (err) {
        return { success: false, error: `Network error: ${err.message}` };
    }
    let body;
    try {
        body = await response.json();
    }
    catch {
        return { success: false, statusCode: response.status, error: "Invalid JSON response from Rozo API" };
    }
    if (!response.ok) {
        return { success: false, statusCode: response.status, error: body };
    }
    return { success: true, payment: body };
}
// CLI entry point
if (import.meta.url === `file://${process.argv[1]}`) {
    const args = process.argv.slice(2);
    const get = (flag) => {
        const idx = args.indexOf(flag);
        return idx !== -1 ? args[idx + 1] : undefined;
    };
    const paymentId = get("--payment-id");
    const txHash = get("--tx-hash");
    const receiverAddress = get("--receiver-address");
    const receiverMemo = get("--receiver-memo");
    let result;
    if (paymentId) {
        result = await getPayment(paymentId);
    }
    else if (txHash) {
        result = await checkPaymentByTxHash(txHash);
    }
    else if (receiverAddress && receiverMemo) {
        result = await checkPaymentByAddressMemo(receiverAddress, receiverMemo);
    }
    else {
        console.error("Usage: --payment-id <uuid> | --tx-hash <hash> | --receiver-address <addr> --receiver-memo <memo>");
        process.exit(1);
    }
    console.log(JSON.stringify(result, null, 2));
    if (!result.success)
        process.exit(1);
}


path: scripts/dist/check-balance.js
content: /**
 * Check wallet balances via the Rozo balance API.
 *
 * Single call returns all USDC/USDT balances across all supported chains
 * for the given address type (evm, solana, stellar).
 */
const BALANCE_API = "https://api-balance.rozo-deeplink.workers.dev/balance";
function detectChainParam(address) {
    if (address.startsWith("0x") && address.length === 42)
        return "evm";
    if (address.startsWith("G") && address.length === 56)
        return "stellar";
    if (address.startsWith("C") && address.length === 56)
        return "stellar";
    if (/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(address))
        return "solana";
    return null;
}
export async function checkBalance(address, chain) {
    const chainParam = chain ?? detectChainParam(address);
    if (!chainParam) {
        return { address, chain: "unknown", balances: [], error: "Cannot detect chain from address format" };
    }
    const url = `${BALANCE_API}/${address}?chain=${chainParam}`;
    let response;
    try {
        response = await fetch(url);
    }
    catch (err) {
        return { address, chain: chainParam, balances: [], error: `Network error: ${err.message}` };
    }
    if (!response.ok) {
        return { address, chain: chainParam, balances: [], error: `Balance API error: ${response.status}` };
    }
    try {
        return await response.json();
    }
    catch {
        return { address, chain: chainParam, balances: [], error: "Invalid JSON response from balance API" };
    }
}
// CLI entry point
if (import.meta.url === `file://${process.argv[1]}`) {
    const args = process.argv.slice(2);
    const addrIdx = args.indexOf("--address");
    const chainIdx = args.indexOf("--chain");
    if (addrIdx === -1) {
        console.error("Usage: --address <wallet_address> [--chain <evm|solana|stellar>]");
        process.exit(1);
    }
    const chain = chainIdx !== -1 ? args[chainIdx + 1] : undefined;
    const result = await checkBalance(args[addrIdx + 1], chain);
    console.log(JSON.stringify(result, null, 2));
    if (result.error)
        process.exit(1);
}


path: scripts/dist/chains.js
content: export const CHAINS = {
    1: { name: "Ethereum", type: "evm" },
    42161: { name: "Arbitrum", type: "evm" },
    8453: { name: "Base", type: "evm" },
    56: { name: "BSC", type: "evm" },
    137: { name: "Polygon", type: "evm" },
    900: { name: "Solana", type: "solana" },
    1500: { name: "Stellar", type: "stellar" },
};
export const CHAIN_NAME_TO_ID = Object.fromEntries(Object.entries(CHAINS).map(([id, info]) => [info.name.toLowerCase(), Number(id)]));
export const PAYIN_TOKENS = {
    USDC: {
        1: { address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", decimals: 6 },
        42161: { address: "0xaf88d065e77c8cc2239327c5edb3a432268e5831", decimals: 6 },
        8453: { address: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", decimals: 6 },
        56: { address: "0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d", decimals: 18 },
        137: { address: "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", decimals: 6 },
        900: { address: [REDACTED_SECRET], decimals: 6 },
        1500: { address: "USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", decimals: 7 },
    },
    USDT: {
        1: { address: "0xdac17f958d2ee523a2206206994597c13d831ec7", decimals: 6 },
        42161: { address: "0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9", decimals: 6 },
        56: { address: "0x55d398326f99059ff775485246999027b3197955", decimals: 18 },
        137: { address: "0xc2132d05d31c914a87c6611c10748aeb04b58e8f", decimals: 6 },
        900: { address: [REDACTED_SECRET], decimals: 6 },
    },
};
export const PAYOUT_TOKENS = {
    USDC: {
        1: { address: "0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48", decimals: 6 },
        42161: { address: "0xaf88d065e77c8cc2239327c5edb3a432268e5831", decimals: 6 },
        8453: { address: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", decimals: 6 },
        56: { address: "0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d", decimals: 18 },
        137: { address: "0x3c499c542cef5e3811e1192ce70d8cc03d5c3359", decimals: 6 },
        900: { address: [REDACTED_SECRET], decimals: 6 },
        1500: { address: "USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN", decimals: 7 },
    },
    USDT: {
        1: { address: "0xdac17f958d2ee523a2206206994597c13d831ec7", decimals: 6 },
        42161: { address: "0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9", decimals: 6 },
        8453: { address: "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913", decimals: 6 },
        56: { address: "0x55d398326f99059ff775485246999027b3197955", decimals: 18 },
        137: { address: "0xc2132d05d31c914a87c6611c10748aeb04b58e8f", decimals: 6 },
    },
};
export function getTokenAddress(chainId, token, direction = "payin") {
    const tokens = direction === "payout" ? PAYOUT_TOKENS : PAYIN_TOKENS;
    return tokens[token]?.[chainId] ?? null;
}
export function getChainName(chainId) {
    return CHAINS[chainId]?.name ?? `Unknown (${chainId})`;
}
export function isPayoutSupported(chainId, token) {
    return PAYOUT_TOKENS[token]?.[chainId] !== undefined;
}


path: scripts/dist/parse-qr.js
content: /**
 * Parse payment QR code content.
 *
 * Supports:
 * - EIP-681: ethereum:<address>[@chainId][/transfer?address=<to>&uint256=<amount>]
 * - Solana Pay: solana:<address>?amount=<amount>[&spl-[REDACTED_SECRET]
 * - Stellar URI: web+stellar:pay?destination=<address>&amount=<amount>[&asset_code=USDC&asset_issuer=...]
 * - Plain addresses: 0x..., base58, G..., C...
 */
// EIP-681 chain ID to Rozo chain ID mapping
const EIP681_CHAIN_MAP = {
    1: 1, // Ethereum
    42161: 42161, // Arbitrum
    8453: 8453, // Base
    56: 56, // BSC
    137: 137, // Polygon
};
// Known ERC-20 token contract → symbol mapping (lowercase)
const EVM_TOKEN_CONTRACTS = {
    // USDC
    [REDACTED_SECRET]: { symbol: "USDC", chainId: 1 },
    [REDACTED_SECRET]: { symbol: "USDC", chainId: 42161 },
    [REDACTED_SECRET]: { symbol: "USDC", chainId: 8453 },
    [REDACTED_SECRET]: { symbol: "USDC", chainId: 56 },
    [REDACTED_SECRET]: { symbol: "USDC", chainId: 137 },
    // USDT
    [REDACTED_SECRET]: { symbol: "USDT", chainId: 1 },
    [REDACTED_SECRET]: { symbol: "USDT", chainId: 42161 },
    [REDACTED_SECRET]: { symbol: "USDT", chainId: 56 },
    [REDACTED_SECRET]: { symbol: "USDT", chainId: 137 },
};
// Solana SPL token mint → symbol mapping
const SOLANA_TOKEN_MINTS = {
    [REDACTED_SECRET]: "USDC",
    [REDACTED_SECRET]: "USDT",
};
function parseEIP681(uri) {
    // ethereum:<contract_or_address>[@chainId][/transfer?address=<to>&uint256=<amount>]
    // or ethereum:<address>[@chainId][?value=<wei_amount>]
    const match = uri.match(/^ethereum:(0x[0-9a-fA-F]{40})(?:@(\d+))?(?:\/(\w+))?(?:\?(.+))?$/);
    if (!match)
        return null;
    const [, addressOrContract, chainIdStr, method, queryStr] = match;
    const params = new URLSearchParams(queryStr ?? "");
    const chainId = chainIdStr ? EIP681_CHAIN_MAP[Number(chainIdStr)] ?? Number(chainIdStr) : undefined;
    // ERC-20 transfer: contract is the token, address param is the recipient
    if (method === "transfer") {
        const recipient = params.get("address");
        const rawAmount = params.get("uint256");
        if (!recipient)
            return null;
        const contractLower = addressOrContract.toLowerCase();
        const tokenInfo = EVM_TOKEN_CONTRACTS[contractLower];
        return {
            type: "eip681",
            address: recipient,
            chainId: chainId ?? tokenInfo?.chainId,
            [REDACTED_SECRET],
            tokenAddress: addressOrContract,
            amount: rawAmount ?? undefined,
            raw: uri,
        };
    }
    // Simple ETH/native transfer with value (we note it but it's not a stablecoin transfer)
    return {
        type: "eip681",
        address: addressOrContract,
        chainId,
        amount: params.get("value") ?? undefined,
        raw: uri,
    };
}
function parseSolanaPay(uri) {
    // solana:<address>?amount=<amount>[&spl-[REDACTED_SECRET]
    const match = uri.match(/^solana:([1-9A-HJ-NP-Za-km-z]{32,44})(?:\?(.+))?$/);
    if (!match)
        return null;
    const [, address, queryStr] = match;
    const params = new URLSearchParams(queryStr ?? "");
    const splToken = params.get("spl-token");
    return {
        type: "solana-pay",
        address,
        chainId: 900,
        [REDACTED_SECRET] ? SOLANA_TOKEN_MINTS[splToken] : undefined,
        tokenAddress: splToken ?? undefined,
        amount: params.get("amount") ?? undefined,
        memo: params.get("memo") ?? undefined,
        label: params.get("label") ?? undefined,
        message: params.get("message") ?? undefined,
        raw: uri,
    };
}
function parseStellarURI(uri) {
    // web+stellar:pay?destination=<address>&amount=<amount>[&asset_code=USDC&asset_issuer=...]
    const match = uri.match(/^web\+stellar:pay\?(.+)$/);
    if (!match)
        return null;
    const params = new URLSearchParams(match[1]);
    const destination = params.get("destination");
    if (!destination)
        return null;
    const assetCode = params.get("asset_code");
    return {
        type: "stellar-uri",
        address: destination,
        chainId: 1500,
        [REDACTED_SECRET] ?? undefined,
        amount: params.get("amount") ?? undefined,
        memo: params.get("memo") ?? undefined,
        raw: uri,
    };
}
function parsePlainAddress(text) {
    const trimmed = text.trim();
    // EVM
    if (/^0x[0-9a-fA-F]{40}$/.test(trimmed)) {
        return { type: "plain-address", address: trimmed, raw: trimmed };
    }
    // Stellar G-wallet
    if (/^G[A-Z2-7]{55}$/.test(trimmed)) {
        return { type: "plain-address", address: trimmed, chainId: 1500, raw: trimmed };
    }
    // Stellar C-wallet
    if (/^C[A-Z2-7]{55}$/.test(trimmed)) {
        return { type: "plain-address", address: trimmed, chainId: 1500, raw: trimmed };
    }
    // Solana
    if (/^[1-9A-HJ-NP-Za-km-z]{32,44}$/.test(trimmed)) {
        return { type: "plain-address", address: trimmed, chainId: 900, raw: trimmed };
    }
    return null;
}
export function parseQR(content) {
    const trimmed = content.trim();
    if (trimmed.startsWith("ethereum:"))
        return parseEIP681(trimmed);
    if (trimmed.startsWith("solana:"))
        return parseSolanaPay(trimmed);
    if (trimmed.startsWith("web+stellar:"))
        return parseStellarURI(trimmed);
    return parsePlainAddress(trimmed);
}
// CLI entry point
if (import.meta.url === `file://${process.argv[1]}`) {
    const input = process.argv.slice(2).join(" ").trim();
    if (!input) {
        console.error("Usage: node parse-qr.js <qr_content>");
        process.exit(1);
    }
    const result = parseQR(input);
    if (result) {
        console.log(JSON.stringify(result, null, 2));
    }
    else {
        console.error(JSON.stringify({ error: "Could not parse QR content", raw: input }));
        process.exit(1);
    }
}


path: scripts/dist/create-payment.js
content: import { getTokenAddress, getChainName, isPayoutSupported, } from "./chains.js";
import { randomUUID } from "crypto";
const API_BASE = "https://intentapiv4.rozo.ai/functions/v1/payment-api";
const MIN_AMOUNT = 0.01;
const MAX_AMOUNT = 10_000;
export async function createPayment(params) {
    const { appId = "rozoAgent", type = "exactOut", sourceChain, sourceToken, destChain, destAddress, destToken, destAmount, destMemo, orderId, sourceAmount, dryrun = false, } = params;
    // Validate amount limits
    const amount = parseFloat(destAmount);
    if (isNaN(amount) || amount < MIN_AMOUNT || amount > MAX_AMOUNT) {
        return {
            success: false,
            error: `Amount must be between $${MIN_AMOUNT} and $${MAX_AMOUNT.toLocaleString()}. Got: ${destAmount}`,
        };
    }
    if (!isPayoutSupported(destChain, destToken)) {
        return {
            success: false,
            error: `Payout not supported: ${destToken} on ${getChainName(destChain)}`,
        };
    }
    const sourceTokenInfo = getTokenAddress(sourceChain, sourceToken, "payin");
    if (!sourceTokenInfo) {
        return {
            success: false,
            error: `Pay-in not supported: ${sourceToken} on ${getChainName(sourceChain)}`,
        };
    }
    const destTokenInfo = getTokenAddress(destChain, destToken, "payout");
    if (!destTokenInfo) {
        return {
            success: false,
            error: `Payout not supported: ${destToken} on ${getChainName(destChain)}`,
        };
    }
    const payload = {
        appId,
        type,
        orderId: orderId ?? randomUUID(),
        display: {
            title: "Payment",
            currency: "USD",
        },
        source: {
            chainId: sourceChain,
            tokenSymbol: sourceToken,
            tokenAddress: sourceTokenInfo.address,
            ...(type === "exactIn" && { amount: sourceAmount ?? destAmount }),
        },
        destination: {
            chainId: destChain,
            receiverAddress: destAddress,
            tokenSymbol: destToken,
            tokenAddress: destTokenInfo.address,
            ...(type === "exactOut" && { amount: destAmount }),
            ...(destMemo && { receiverMemo: destMemo }),
        },
    };
    let response;
    try {
        const url = dryrun ? `${API_BASE}?dryrun=true` : API_BASE;
        response = await fetch(url, {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(payload),
        });
    }
    catch (err) {
        return { success: false, error: `Network error: ${err.message}` };
    }
    let body;
    try {
        body = await response.json();
    }
    catch {
        return { success: false, statusCode: response.status, error: "Invalid JSON response from Rozo API" };
    }
    if (!response.ok) {
        return { success: false, statusCode: response.status, error: body };
    }
    return { success: true, payment: body };
}
// CLI entry point
if (import.meta.url === `file://${process.argv[1]}`) {
    const args = process.argv.slice(2);
    const get = (flag) => {
        const idx = args.indexOf(flag);
        return idx !== -1 ? args[idx + 1] : undefined;
    };
    const sourceChain = get("--source-chain");
    const sourceToken = get("--source-token");
    const destChain = get("--dest-chain");
    const destAddress = get("--dest-address");
    const destToken = get("--dest-token");
    const destAmount = get("--dest-amount");
    if (!sourceChain || !sourceToken || !destChain || !destAddress || !destToken || !destAmount) {
        console.error("Usage: --source-chain <id> --source-token <USDC|USDT> --dest-chain <id> --dest-address <addr> --dest-token <USDC|USDT> --dest-amount <amount>");
        process.exit(1);
    }
    const result = await createPayment({
        appId: get("--app-id") ?? "rozoAgent",
        type: get("--type") ?? "exactOut",
        sourceChain: Number(sourceChain),
        sourceToken: sourceToken.toUpperCase(),
        destChain: Number(destChain),
        destAddress,
        destToken: destToken.toUpperCase(),
        destAmount,
        destMemo: get("--dest-memo"),
        orderId: get("--order-id"),
        sourceAmount: get("--source-amount"),
        dryrun: args.includes("--dryrun"),
    });
    console.log(JSON.stringify(result, null, 2));
    if (!result.success)
        process.exit(1);
}


path: references/wallet-detection.md
content: # Wallet Address Detection Rules

## Detection Patterns

### EVM Addresses
- **Pattern**: `0x` followed by exactly 40 hexadecimal characters (0-9, a-f, A-F)
- **Regex**: `^0x[0-9a-fA-F]{40}$`
- **Chains**: Ethereum (1), Arbitrum (42161), Base (8453), BSC (56), Polygon (137)
- **Action**: ALWAYS ask which chain — never assume from address alone
- **Payout chains**: Ethereum, Arbitrum, Base, BSC, Polygon

### Solana Addresses
- **Pattern**: Base58 encoded, 32-44 characters, does NOT start with `0x`
- **Regex**: `^[1-9A-HJ-NP-Za-km-z]{32,44}$`
- **Chain ID**: 900
- **Action**: Auto-detect as Solana

### Stellar G-Wallet (Private Key Wallet)
- **Pattern**: Starts with `G`, followed by 55 uppercase alphanumeric characters (Base32)
- **Regex**: `^G[A-Z2-7]{55}$`
- **Chain ID**: 1500
- **Action**: Auto-detect as Stellar
- **Trustline check**: Run `node scripts/dist/check-stellar-trustline.js --address <G_wallet>` (defaults to USDC, use `--asset EURC` for EURC). Block payment if trustline is missing.

### Stellar C-Wallet (Smart Contract Wallet)
- **Pattern**: Starts with `C`, followed by 55 uppercase alphanumeric characters (Base32)
- **Regex**: `^C[A-Z2-7]{55}$`
- **Chain ID**: 1500
- **Action**: Auto-detect as Stellar, use `stellar_payin_contracts` intent
- **Flow**:
  1. API returns Soroban contract address (`receiverAddressContract`) and memo (`receiverMemoContract`)
  2. User invokes the contract's `pay()` function with the amount and memo
  3. System monitors and triggers payout upon detection

## Decision Tree

```
Address starts with "0x" and length is 42?
├── Yes → EVM address
│   └── Ask user which chain (Ethereum, Arbitrum, Base, BSC, Polygon for payouts)
│
Address is Base58, 32-44 chars?
├── Yes → Solana (chain 900)
│
Address starts with "G" and length is 56?
├── Yes → Stellar G-wallet (chain 1500)
│   └── Warn about USDC trustline if sending USDC
│
Address starts with "C" and length is 56?
├── Yes → Stellar C-wallet (chain 1500)
│   └── Use stellar_payin_contracts intent
│
└── None matched → Ask user to verify the address format
```


path: references/supported-chains.md
content: # Supported Tokens and Chains

## Pay-In Tokens

### USDC Pay-In

| Chain | Chain ID | Token Address | Decimals |
|-------|----------|--------------|----------|
| Ethereum | 1 | 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 | 6 |
| Arbitrum | 42161 | 0xaf88d065e77c8cc2239327c5edb3a432268e5831 | 6 |
| Base | 8453 | 0x833589fcd6edb6e08f4c7c32d4f71b54bda02913 | 6 |
| BSC | 56 | 0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d | 18 |
| Polygon | 137 | 0x3c499c542cef5e3811e1192ce70d8cc03d5c3359 | 6 |
| Solana | 900 | EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v | 6 |
| Stellar | 1500 | USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN | 7 |

### USDT Pay-In

| Chain | Chain ID | Token Address | Decimals |
|-------|----------|--------------|----------|
| Ethereum | 1 | 0xdac17f958d2ee523a2206206994597c13d831ec7 | 6 |
| Arbitrum | 42161 | 0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9 | 6 |
| BSC | 56 | 0x55d398326f99059ff775485246999027b3197955 | 18 |
| Polygon | 137 | 0xc2132d05d31c914a87c6611c10748aeb04b58e8f | 6 |
| Solana | 900 | Es9vMFrzaCERmJfrF4H2FYD4KCoNkY11McCe8BenwNYB | 6 |

## Pay-Out Tokens

### USDC Pay-Out

| Chain | Chain ID | Token Address | Decimals |
|-------|----------|--------------|----------|
| Ethereum | 1 | 0xa0b86991c6218b36c1d19d4a2e9eb0ce3606eb48 | 6 |
| Arbitrum | 42161 | 0xaf88d065e77c8cc2239327c5edb3a432268e5831 | 6 |
| Base | 8453 | 0x833589fcd6edb6e08f4c7c32d4f71b54bda02913 | 6 |
| BSC | 56 | 0x8ac76a51cc950d9822d68b83fe1ad97b32cd580d | 18 |
| Polygon | 137 | 0x3c499c542cef5e3811e1192ce70d8cc03d5c3359 | 6 |
| Solana | 900 | EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v | 6 |
| Stellar | 1500 | USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN | 7 |

### USDT Pay-Out (EVM chains only)

| Chain | Chain ID | Token Address | Decimals |
|-------|----------|--------------|----------|
| Ethereum | 1 | 0xdac17f958d2ee523a2206206994597c13d831ec7 | 6 |
| Arbitrum | 42161 | 0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9 | 6 |
| Base | 8453 | 0x833589fcd6edb6e08f4c7c32d4f71b54bda02913 | 6 |
| BSC | 56 | 0x55d398326f99059ff775485246999027b3197955 | 18 |
| Polygon | 137 | 0xc2132d05d31c914a87c6611c10748aeb04b58e8f | 6 |

## Important Notes

- **USDT payout supported on EVM chains** — Ethereum, Arbitrum, Base, BSC, Polygon
- **USDT payout NOT supported on Solana or Stellar**
- **BSC uses 18 decimals** for both USDC and USDT (different from other chains)
- **Stellar uses 7 decimals** for USDC


path: references/api-reference.md
content: # Rozo API Reference

Base URL: `https://intentapiv4.rozo.ai/functions/v1/payment-api`

**Authentication:** None required. All endpoints are public and rate-limited.

## Create Payment

**POST** `/payment-api/`

### Request Body

```json
{
  "appId": "rozoAgent",
  "orderId": "optional-unique-id",
  "type": "exactOut",
  "display": {
    "title": "Payment",
    "description": "optional description",
    "currency": "USD"
  },
  "source": {
    "chainId": 8453,
    "tokenSymbol": "USDC",
    "tokenAddress": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
    "amount": "10.00"
  },
  "destination": {
    "chainId": 1500,
    "receiverAddress": "GC56BX...",
    "receiverMemo": "optional-for-stellar",
    "tokenSymbol": "USDC",
    "tokenAddress": "USDC:GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN",
    "amount": "10.00"
  },
  "webhookUrl": "optional-callback-url",
  "metadata": {}
}
```

### Fields

| Field | Type | Required | Description |
|-------|------|----------|-------------|
| appId | string | Yes | Always `rozoAgent` |
| orderId | string | No | Unique order reference for idempotency |
| type | enum | No | `exactIn` (fee from input) or `exactOut` (fee added to input, default) |
| display.title | string | Yes | Payment title |
| display.description | string | No | Payment description |
| display.currency | string | Yes | Display currency, use `USD` |
| source.chainId | integer | Yes | Source chain ID |
| source.tokenSymbol | enum | Yes | `USDC` or `USDT` |
| source.tokenAddress | string | No | Token contract address |
| source.amount | string | No | Required for `exactIn` type |
| destination.chainId | integer | Yes | Destination chain ID |
| destination.receiverAddress | string | Yes | Recipient wallet address |
| destination.receiverMemo | string | No | Required for Stellar memo-based payments |
| destination.tokenSymbol | enum | Yes | `USDC` or `USDT` |
| destination.tokenAddress | string | No | Token contract address |
| destination.amount | string | No | Required for `exactOut` type |

### Payment Types

- **exactOut** (default): Recipient receives the exact amount. Fee is added to the source amount.
- **exactIn**: Source sends exact amount. Fee is deducted, recipient gets less.

### Response (201 Created)

```json
{
  "id": "550e8400-e29b-41d4-a716-446655440000",
  "appId": "rozoAgent",
  "status": "payment_unpaid",
  "type": "exactOut",
  "createdAt": "2024-01-01T00:00:00Z",
  "expiresAt": "2024-01-01T01:00:00Z",
  "source": {
    "chainId": 8453,
    "tokenSymbol": "USDC",
    "amount": "10.50",
    "receiverAddress": "0x_DEPOSIT_ADDRESS_HERE",
    "fee": "0.50"
  },
  "destination": {
    "chainId": 1500,
    "receiverAddress": "GC56BX...",
    "tokenSymbol": "USDC",
    "amount": "10.00"
  }
}
```

Key response fields:
- `source.receiverAddress` — the deposit address where the user sends funds
- `source.receiverMemo` — memo if required (Stellar)
- `source.fee` — the fee amount
- `expiresAt` — payment must be funded before this time

### Errors

- **400** — Invalid request (bad parameters)
- **409** — Duplicate orderId conflict

## Get Payment

**GET** `/payments/{paymentId}`

### Path Parameters

| Parameter | Type | Required |
|-----------|------|----------|
| paymentId | string (UUID) | Yes |

### Response (200)

Same schema as Create Payment response.

### Payment Statuses

| Status | Description |
|--------|-------------|
| payment_unpaid | Created, awaiting funding |
| payment_started | Funding detected, processing |
| payment_payin_completed | Source funds received |
| payment_payout_completed | Destination funds sent |
| payment_completed | Fully complete |
| payment_bounced | Payout failed |
| payment_expired | Not funded in time |
| payment_refunded | Funds returned to sender |

## Check Payment by Source

**GET** `/payments/check`

Lookup a payment by source transaction hash or deposit address+memo instead of payment ID. Searches the last 7 days and returns the most recent match.

### Query Parameters

Provide **either** `txHash` **or** both `receiverAddress` + `receiverMemo`:

| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
| txHash | string | Either this... | Source chain transaction hash (Stellar, EVM, Solana) |
| receiverAddress | string | ...or both of these | Deposit address |
| receiverMemo | string | | Memo associated with the deposit address |

### Response (200)

Same schema as Get Payment response.

### Errors

- **400** — Missing required params (need `txHash` or both `receiverAddress` + `receiverMemo`)
- **404** — No matching payment in last 7 days

## Stellar Contract Payments

For Stellar C-wallet destinations, the API uses `stellar_payin_contracts` intent:

1. Create payment as normal with C-wallet as destination
2. Response includes `receiverAddressContract` and `receiverMemoContract`
3. User invokes the Soroban contract's `pay()` function with amount and memo
4. System monitors the contract for payment
5. Cross-chain payout triggers once payment is detected


Security scan verdicts — ClawScan: clean | static analysis: clean | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)