Skip to main content
SEP-31 defines a standard for direct cross-border payments and remittances. This protocol enables sending institutions to initiate payments on behalf of users through receiving anchors.

Overview

While PayOnProof’s source code doesn’t include a dedicated SEP-31 implementation file, the platform detects SEP-31 support through anchor discovery and includes it in capability resolution.

Use cases

  • Remittances: Send money across borders to recipients
  • B2B payments: Business-to-business transactions
  • Programmatic transfers: API-driven payment flows
  • Cross-border settlement: International payment settlement

SEP-31 detection

PayOnProof discovers SEP-31 support through the DIRECT_PAYMENT_SERVER field in stellar.toml:
services/api/lib/stellar/sep1.ts
export async function discoverAnchorFromDomain(
  input: Sep1DiscoveryInput
): Promise<Sep1DiscoveryResult> {
  // Fetch stellar.toml
  const text = await response.text();
  const parsed = parseTomlFlat(text);
  
  return {
    domain,
    stellarTomlUrl,
    signingKey: parsed.SIGNING_KEY,
    webAuthEndpoint: parsed.WEB_AUTH_ENDPOINT,
    transferServerSep24: parsed.TRANSFER_SERVER_SEP0024,
    transferServerSep6: parsed.TRANSFER_SERVER,
    directPaymentServer: parsed.DIRECT_PAYMENT_SERVER, // SEP-31
    kycServer: parsed.KYC_SERVER,
    raw: parsed,
  };
}
See the SEP-1 discovery implementation at services/api/lib/stellar/sep1.ts:70

Capability resolution

SEP-31 capabilities are included in the anchor capability resolution:
services/api/lib/stellar/capabilities.ts
export async function resolveAnchorCapabilities(input: {
  domain: string;
  assetCode: string;
}): Promise<ResolvedAnchorCapabilities> {
  const sep1 = await discoverAnchorFromDomain({ domain: input.domain });
  
  const sep = {
    sep24: Boolean(sep1.transferServerSep24),
    sep6: Boolean(sep1.transferServerSep6),
    sep31: Boolean(sep1.directPaymentServer), // SEP-31 support flag
    sep10: Boolean(sep1.webAuthEndpoint),
  };
  
  return {
    domain: sep1.domain,
    sep,
    endpoints: {
      webAuthEndpoint: sep1.webAuthEndpoint,
      transferServerSep24: sep1.transferServerSep24,
      transferServerSep6: sep1.transferServerSep6,
      directPaymentServer: sep1.directPaymentServer, // SEP-31 endpoint
      kycServer: sep1.kycServer,
    },
    fees,
    diagnostics,
    raw: { signingKey: sep1.signingKey, sep24Info, sep6Info },
  };
}
The resolved capabilities object includes the directPaymentServer endpoint URL when the anchor supports SEP-31.

Trust evaluation

When evaluating anchor trustworthiness, PayOnProof checks for at least one transfer protocol:
services/api/lib/stellar/trust.ts
export function evaluateAnchorTrust(input: AnchorTrustInput): AnchorTrustResult {
  const reasons: string[] = [];
  
  if (!c.sep.sep24 && !c.sep.sep6 && !c.sep.sep31) {
    reasons.push("Anchor missing transfer protocol capability (SEP-24/6/31)");
  }
  
  if (requireSep24OrSep31 && !c.sep.sep24 && !c.sep.sep31) {
    reasons.push("Anchor missing SEP-24/SEP-31 (SEP-6 only is not allowed)");
  }
  
  if (c.sep.sep31 && !isHttpsUrl(c.endpoints.directPaymentServer)) {
    reasons.push("Invalid DIRECT_PAYMENT_SERVER (must be HTTPS)");
  }
  
  return { trusted: reasons.length === 0, reasons };
}
PayOnProof requires anchors to support at least SEP-24 or SEP-31. SEP-6-only anchors may be rejected depending on configuration.

Security requirements

For SEP-31 endpoints to be considered trusted:
  1. HTTPS required: The DIRECT_PAYMENT_SERVER must use HTTPS
  2. SEP-10 authentication: SEP-31 flows require SEP-10 auth tokens
  3. Signing key: The anchor must provide a SIGNING_KEY in stellar.toml

HTTPS validation

services/api/lib/stellar/trust.ts
function isHttpsUrl(value: string | undefined): boolean {
  if (!value) return false;
  try {
    const url = new URL(value);
    return url.protocol === "https:";
  } catch {
    return false;
  }
}

SEP-31 in anchor directory

When discovering anchors from Horizon, PayOnProof checks for SEP-31 support:
services/api/lib/stellar/horizon.ts
const sep1 = await discoverAnchorFromDomain({ domain, timeoutMs });

// Check for SEP-31 capability
let sep24Info: unknown;
let sep6Info: unknown;

if (sep1.transferServerSep24) {
  try {
    sep24Info = (await fetchSep24Info({ ... })).info;
  } catch { }
}

if (sep1.transferServerSep6) {
  try {
    sep6Info = (await fetchSep6Info({ ... })).info;
  } catch { }
}

// Extract supported operations from all protocols
const extracted = [
  ...extractTypesAndCurrencies(sep24Info),
  ...extractTypesAndCurrencies(sep6Info),
];
While SEP-31 is detected and included in capabilities, the current implementation focuses on SEP-24 and SEP-6 for extracting supported currencies and fee information.

Diagnostic information

The capability resolver provides diagnostics when SEP-31 is missing:
services/api/lib/stellar/capabilities.ts
if (!sep.sep31) {
  diagnostics.push("SEP-31 endpoint missing in stellar.toml");
}
These diagnostics help identify which protocols an anchor supports and potential configuration issues. SEP-31 works in conjunction with other Stellar standards:
  • SEP-10: Required for authentication before initiating payments
  • SEP-12: KYC information collection for sender and receiver
  • SEP-38: Quote negotiation for cross-asset payments

Next steps

SEP-10 authentication

Authentication required for SEP-31 flows

SEP-24 flows

Alternative hosted deposit/withdrawal

Anchor trust

Security validation for anchors

Capability resolution

How capabilities are determined

Build docs developers (and LLMs) love