Skip to main content
PayOnProof includes utilities for managing Stellar assets, including validation, formatting, and integration with anchor discovery. Assets are the fundamental unit of value transfer on the Stellar network.

Asset structure

While the source code doesn’t include a dedicated asset.ts implementation with full asset management, asset handling is integrated throughout the Stellar modules.

Asset identification

Stellar assets are identified by:
  • Asset code: 1-12 alphanumeric characters (e.g., “USDC”, “BTC”)
  • Issuer: Stellar account that issued the asset (public key)
  • Native: XLM (Stellar’s native asset) has no issuer

Asset code validation

PayOnProof validates asset codes using regex patterns:
services/api/lib/stellar/anchor-directory.ts
function normalizeCurrencyCode(value: string): string {
  const code = value.trim().toUpperCase();
  return /^[A-Z0-9]{2,12}$/.test(code) ? code : "";
}
Valid asset codes:
  • Minimum 2 characters
  • Maximum 12 characters
  • Alphanumeric only (A-Z, 0-9)
  • Case-insensitive (normalized to uppercase)
The validation pattern ^[A-Z0-9]{2,12}$ ensures compliance with Stellar’s asset code requirements.

Asset discovery from Horizon

PayOnProof discovers assets by querying the Horizon API:
services/api/lib/stellar/horizon.ts
interface HorizonAssetRecord {
  asset_type: string;
  asset_code?: string;
  asset_issuer?: string;
}

async function getAssetIssuersFromHorizon(input: {
  horizonUrl: string;
  assetPages: number;
  assetsPerPage: number;
  timeoutMs: number;
}): Promise<Set<string>> {
  const issuers = new Set<string>();
  let nextUrl =
    `${normalizeBaseUrl(input.horizonUrl)}/assets?limit=${input.assetsPerPage}&order=desc`;
  
  for (let page = 0; page < input.assetPages; page += 1) {
    const payload = await fetchJsonWithTimeout(nextUrl, input.timeoutMs);
    const records = payload?._embedded?.records ?? [];
    
    for (const record of records) {
      if (record.asset_type === "native") continue;
      if (record.asset_issuer) issuers.add(record.asset_issuer);
    }
    
    const href = payload?._links?.next?.href;
    if (!href) break;
    nextUrl = href;
  }
  
  return issuers;
}
See implementation at services/api/lib/stellar/horizon.ts:206

Asset types

Horizon returns three asset types:
  • native: The native XLM asset
  • credit_alphanum4: 1-4 character asset codes
  • credit_alphanum12: 5-12 character asset codes
PayOnProof filters out native assets when discovering issuers.

Asset fee extraction

Fee information is extracted from anchor info endpoints:
services/api/lib/stellar/capabilities.ts
function extractFeeFromInfo(
  info: unknown,
  assetCode: string
): { fixed?: number; percent?: number } | undefined {
  if (!info || typeof info !== "object") return undefined;
  const root = info as Record<string, unknown>;
  const deposit = root.deposit as Record<string, unknown> | undefined;
  const withdraw = root.withdraw as Record<string, unknown> | undefined;
  
  const candidates = [
    deposit?.[assetCode],
    withdraw?.[assetCode],
    deposit?.[`${assetCode}:*`],
    withdraw?.[`${assetCode}:*`],
  ];
  
  const found = candidates.find((v) => v && typeof v === "object") as
    | Record<string, unknown>
    | undefined;
    
  if (!found) return undefined;
  
  return {
    fixed: toNumber(found.fee_fixed),
    percent: toNumber(found.fee_percent),
  };
}
Anchors can specify fees for specific assets or use wildcard patterns like USDC:* to apply fees across all issuers of that asset code.

Asset code parsing

When processing anchor responses, asset codes are extracted from composite strings:
services/api/lib/stellar/horizon.ts
for (const code of Object.keys(deposit ?? {})) {
  const assetCode = code.split(":")[0]?.trim().toUpperCase();
  if (assetCode && /^[A-Z0-9]{2,12}$/.test(assetCode)) {
    rows.push({
      type: "on-ramp",
      currency: assetCode,
      countries: extractCountryCodesFromAssetConfig(deposit[code]),
    });
  }
}
This handles formats like:
  • USDC - Simple asset code
  • USDC:GABC... - Asset with issuer
  • USDC:* - Wildcard issuer

Number conversion utilities

Asset amounts and fees are normalized using safe conversion:
function toNumber(value: unknown): number | undefined {
  if (typeof value === "number" && Number.isFinite(value)) return value;
  if (typeof value === "string" && value.trim()) {
    const parsed = Number(value);
    if (Number.isFinite(parsed)) return parsed;
  }
  return undefined;
}
This ensures:
  • Handles both number and string inputs
  • Rejects NaN, Infinity, and -Infinity
  • Returns undefined for invalid values

Asset catalog integration

Discovered assets are included in the anchor catalog:
export interface AnchorCatalogImportRow {
  id: string;
  name: string;
  domain: string;
  country: string;
  currency: string; // Asset code
  type: "on-ramp" | "off-ramp";
  active: boolean;
}
Each row represents an anchor’s support for a specific asset in a specific country.

Asset in capability resolution

Asset codes are central to capability resolution:
services/api/lib/stellar/capabilities.ts
export async function resolveAnchorCapabilities(input: {
  domain: string;
  assetCode: string; // Asset to query
}): Promise<ResolvedAnchorCapabilities> {
  const sep1 = await discoverAnchorFromDomain({ domain: input.domain });
  
  let fees: { fixed?: number; percent?: number; source: "sep24" | "sep6" | "default" } = {
    source: "default",
  };
  
  if (sep1.transferServerSep24) {
    const r = await fetchSep24Info({ transferServerSep24: sep1.transferServerSep24 });
    sep24Info = r.info;
    const extracted = extractFeeFromInfo(r.info, input.assetCode);
    if (extracted?.fixed !== undefined || extracted?.percent !== undefined) {
      fees = { ...extracted, source: "sep24" };
    }
  }
  
  return { domain: sep1.domain, sep, endpoints, fees, diagnostics };
}
Capability resolution queries fees for a specific asset code, as different assets may have different fee structures.

Home domain for assets

Asset issuers set home domains on their accounts:
services/api/lib/stellar/horizon.ts
interface HorizonAccountRecord {
  id: string;
  home_domain?: string;
}

async function getHomeDomainForIssuer(input: {
  horizonUrl: string;
  issuer: string;
  timeoutMs: number;
}): Promise<string | undefined> {
  const url = `${normalizeBaseUrl(input.horizonUrl)}/accounts/${input.issuer}`;
  const payload = await fetchJsonWithTimeout(url, input.timeoutMs) as HorizonAccountRecord;
  const homeDomain = payload.home_domain?.trim().toLowerCase();
  return homeDomain || undefined;
}
The home domain links an asset issuer to their anchor services.

ID generation

Unique IDs are generated for asset-country-anchor combinations:
function toId(value: string): string {
  return value
    .toLowerCase()
    .replace(/[^a-z0-9]+/g, "-")
    .replace(/^-+|-+$/g, "")
    .slice(0, 80);
}

const id = toId(
  `anchor-${domain}-${countryCode}-${currency}-${type}`
);
Example: anchor-example-com-us-usdc-on-ramp

Asset name extraction

Anchor names are extracted from info responses:
function pickAnchorName(domain: string, info: unknown): string {
  if (info && typeof info === "object") {
    const root = info as Record<string, unknown>;
    const orgName = root.org_name;
    const name = root.name;
    if (typeof orgName === "string" && orgName.trim()) return orgName.trim();
    if (typeof name === "string" && name.trim()) return name.trim();
  }
  return domain;
}
This provides user-friendly names for anchors supporting the asset.

Asset configuration fields

Anchors provide asset-specific configuration:
{
  "deposit": {
    "USDC": {
      "enabled": true,
      "fee_fixed": 0.1,
      "fee_percent": 0.5,
      "min_amount": 10,
      "max_amount": 50000,
      "fields": {
        "email_address": {
          "description": "Email for notifications",
          "optional": true
        }
      }
    }
  }
}
PayOnProof extracts fee, limit, and field information from these configurations.

Next steps

Horizon integration

Query assets from the network

Capability resolution

Discover asset support

SEP-24 flows

Asset deposit and withdrawal

Anchor discovery

Find anchors for assets

Build docs developers (and LLMs) love