PayOnProof uses the Stellar Network as settlement infrastructure and integrates with Stellar anchors (regulated fiat on/off-ramps) through standardized SEP protocols.
Why Stellar?
PayOnProof chose Stellar for cross-border remittances because:
Low transaction costs : ~$0.00001 per transaction
Fast settlement : 3-5 second finality
Built-in multi-currency support : Native asset issuance and pathfinding
Anchor ecosystem : Regulated fiat gateways in 100+ countries
SEP standards : Interoperable protocols for deposits, withdrawals, and auth
Regulatory compatibility : Anchors handle KYC/AML compliance
Stellar is purpose-built for cross-border payments. Unlike general-purpose blockchains, it optimizes for speed, low cost, and regulatory compliance.
Stellar architecture in PayOnProof
User initiates transfer through PayOnProof UI
API discovers anchors via SEP-1 (stellar.toml)
API validates anchor capabilities via SEP-24/SEP-6 endpoints
User authenticates with anchor via SEP-10 (Stellar Web Auth)
User initiates deposit/withdrawal via SEP-24 (interactive flow)
Transaction settles on Stellar Network
API queries Horizon for confirmation
Proof of payment generated with transaction hash
Stellar Ecosystem Protocols (SEPs)
PayOnProof implements multiple SEP standards:
SEP-1: Stellar Info File (stellar.toml)
Purpose : Discover anchor metadata and service endpoints.
Implementation : lib/stellar/sep1.ts
export async function discoverAnchorFromDomain (
input : Sep1DiscoveryInput
) : Promise < Sep1DiscoveryResult > {
const stellarTomlUrl = `https:// ${ domain } /.well-known/stellar.toml` ;
const response = await fetchWithTimeout ( stellarTomlUrl , timeout );
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 ,
kycServer: parsed . KYC_SERVER ,
};
}
Example stellar.toml:
VERSION = "2.0.0"
NETWORK_PASSPHRASE = "Public Global Stellar Network ; September 2015"
[[ CURRENCIES ]]
code = "USDC"
issuer = "GA5ZSEJYB37JRC5AVCIA5MOP4RHTM335X2KGX3IHOJAPP5RE34K4KZVN"
SIGNING_KEY = "GCXYZ..."
WEB_AUTH_ENDPOINT = "https://anchor.example.com/auth"
TRANSFER_SERVER_SEP0024 = "https://anchor.example.com/sep24"
DIRECT_PAYMENT_SERVER = "https://anchor.example.com/sep31"
SEP-1 is the entry point for all anchor discovery. Every operational anchor must host a stellar.toml file at /.well-known/stellar.toml.
SEP-10: Stellar Web Authentication
Purpose : Authenticate users with anchors using Stellar keypairs.
Flow:
Client requests challenge from anchor’s WEB_AUTH_ENDPOINT
Anchor returns unsigned transaction challenge
User signs challenge with Stellar keypair (via Freighter wallet)
Client submits signed challenge
Anchor validates signature and returns JWT token
Implementation : lib/stellar/sep10.ts
export async function getSep10Challenge ( input : {
webAuthEndpoint : string ;
accountId : string ;
homeDomain : string ;
}) : Promise <{ transaction : string ; networkPassphrase : string }> {
const url = ` ${ input . webAuthEndpoint } ?account= ${ input . accountId } &home_domain= ${ input . homeDomain } ` ;
const response = await fetch ( url );
const data = await response . json ();
return {
transaction: data . transaction ,
networkPassphrase: data . network_passphrase ,
};
}
export async function submitSep10Challenge ( input : {
webAuthEndpoint : string ;
signedTransaction : string ;
}) : Promise <{ token : string }> {
const response = await fetch ( input . webAuthEndpoint , {
method: "POST" ,
headers: { "Content-Type" : "application/json" },
body: JSON . stringify ({ transaction: input . signedTransaction }),
});
return response . json ();
}
JWT token usage:
// All subsequent anchor API calls use the JWT
const headers = {
"Authorization" : `Bearer ${ jwtToken } ` ,
"Content-Type" : "application/json" ,
};
SEP-24: Hosted Deposit and Withdrawal
Purpose : Interactive deposit/withdrawal flows with KYC/AML compliance.
Implementation : lib/stellar/sep24.ts
export async function fetchSep24Info ( input : Sep24InfoInput ) {
const infoUrl = ` ${ input . transferServerSep24 } /info` ;
const response = await fetchWithTimeout ( infoUrl , timeout );
const info = await response . json ();
return {
infoUrl ,
transferServerSep24: input . transferServerSep24 ,
info , // Contains supported assets, fees, limits
};
}
Example /info response:
{
"deposit" : {
"USDC" : {
"enabled" : true ,
"fee_fixed" : 0 ,
"fee_percent" : 0.1 ,
"min_amount" : 10 ,
"max_amount" : 100000
}
},
"withdraw" : {
"USDC" : {
"enabled" : true ,
"fee_fixed" : 5 ,
"fee_percent" : 0.5 ,
"min_amount" : 10 ,
"max_amount" : 50000
}
}
}
Interactive deposit flow:
// 1. Get deposit URL
const depositResponse = await fetch ( ` ${ transferServer } /transactions/deposit/interactive` , {
method: "POST" ,
headers: { Authorization: `Bearer ${ jwtToken } ` },
body: JSON . stringify ({
asset_code: "USDC" ,
account: userStellarAddress ,
amount: "1000" ,
}),
});
const { url , id } = await depositResponse . json ();
// 2. User completes KYC/payment in popup
window . open ( url , "_blank" , "width=500,height=700" );
// 3. Poll transaction status
const statusResponse = await fetch ( ` ${ transferServer } /transaction?id= ${ id } ` , {
headers: { Authorization: `Bearer ${ jwtToken } ` },
});
const { status } = await statusResponse . json ();
// status: "pending_user_transfer_start" | "completed" | "error"
SEP-6: Deposit and Withdrawal API
Purpose : Programmatic deposits/withdrawals without interactive UI.
Implementation : lib/stellar/sep6.ts
export async function fetchSep6Info ( input : Sep6InfoInput ) {
const infoUrl = ` ${ input . transferServerSep6 } /info` ;
const response = await fetchWithTimeout ( infoUrl , timeout );
const info = await response . json ();
return { infoUrl , transferServerSep6: input . transferServerSep6 , info };
}
Difference from SEP-24:
SEP-24: Interactive (popup/iframe for KYC)
SEP-6: Programmatic (API-only, pre-verified users)
PayOnProof prefers SEP-24 for MVP because it handles KYC flows automatically.
SEP-31: Cross-Border Payments API
Purpose : Direct payment protocol for remittances.
Flow:
Sender’s anchor receives fiat
Sender’s anchor sends USDC to recipient’s anchor
Recipient’s anchor disburses local fiat
Discovery:
const sep1 = await discoverAnchorFromDomain ({ domain: "anchor.example.com" });
if ( sep1 . directPaymentServer ) {
// Anchor supports SEP-31 direct payments
console . log ( "SEP-31 endpoint:" , sep1 . directPaymentServer );
}
PayOnProof uses SEP-31 as a scoring signal (routes with SEP-31 support get higher scores) but doesn’t implement the full protocol in MVP.
Stellar Network configuration
Network selection
PayOnProof supports both Testnet (staging) and Mainnet (production):
// lib/stellar.ts
export function getStellarConfig () {
const popEnv = getPopEnv ();
const defaultHorizonUrl = popEnv === "production"
? "https://horizon.stellar.org"
: "https://horizon-testnet.stellar.org" ;
const defaultPassphrase = popEnv === "production"
? "Public Global Stellar Network ; September 2015"
: "Test SDF Network ; September 2015" ;
return {
popEnv ,
horizonUrl: process . env . STELLAR_HORIZON_URL ?? defaultHorizonUrl ,
networkPassphrase: process . env . STELLAR_NETWORK_PASSPHRASE ?? defaultPassphrase ,
};
}
Environment detection:
export function getPopEnv () : PopEnv {
// Explicit override
if ( process . env . POP_ENV === "production" ) return "production" ;
if ( process . env . POP_ENV === "staging" ) return "staging" ;
// Infer from network passphrase
if ( process . env . STELLAR_NETWORK_PASSPHRASE === "Test SDF Network ; September 2015" ) {
return "staging" ;
}
return "production" ;
}
Horizon API client
import { Horizon } from "@stellar/stellar-sdk" ;
export function getHorizonServer () {
const { horizonUrl } = getStellarConfig ();
return new Horizon . Server ( horizonUrl );
}
export async function getLatestLedgerSequence () {
const server = getHorizonServer ();
const ledgers = await server . ledgers (). order ( "desc" ). limit ( 1 ). call ();
return Number ( ledgers . records [ 0 ]. sequence );
}
Anchor catalog management
Discovery and import
PayOnProof maintains a catalog of Stellar anchors in Supabase:
Automatic sync (cron):
# Runs every 15 minutes via Vercel Cron
GET /api/cron/anchors-sync
Discovery modes:
Horizon discovery (recommended):
Query Horizon for asset issuers with home_domain
Validate each domain’s stellar.toml
Extract SEP endpoints and capabilities
Insert/update anchors_catalog table
Manual seed import :
npm run anchors:seed:import -- --file ./anchor-seeds.json --apply
Seed file format:
{
"anchors" : [
{
"name" : "MoneyGram Access" ,
"domain" : "moneygram.stellar.org" ,
"countries" : [ "US" , "MX" , "PH" ],
"currencies" : [ "USDC" ],
"type" : "on-ramp" ,
"active" : true
}
]
}
Capability refresh
Anchors are periodically validated:
const CAPABILITY_REFRESH_MS = 10 * 60 * 1000 ; // 10 minutes
async function resolveAnchorRuntime ( anchor : AnchorCatalogEntry ) {
const shouldRefresh = Date . now () - lastCheckedAtMs > CAPABILITY_REFRESH_MS ;
if ( ! shouldRefresh ) {
return anchor . capabilities ; // Use cache
}
// Refresh from stellar.toml and SEP endpoints
const resolved = await resolveAnchorCapabilities ({
domain: anchor . domain ,
assetCode: anchor . currency ,
});
// Update database
await updateAnchorCapabilities ({
id: anchor . id ,
... resolved . sep ,
... resolved . endpoints ,
operational: isOperational ( resolved ),
lastCheckedAt: new Date (). toISOString (),
});
return resolved ;
}
Operational status criteria:
const operational = Boolean (
resolved . sep . sep10 && // Has auth endpoint
resolved . sep . sep24 && // Has SEP-24 server
resolved . endpoints . webAuthEndpoint &&
resolved . endpoints . transferServerSep24 &&
sep24AssetSupported // Asset is in /info response
);
Transaction flow example
End-to-end flow for a US → Mexico remittance:
1. Route discovery
POST /api/compare-routes
{
"origin" : "US",
"destination" : "MX",
"amount" : 1000
}
Backend logic:
// Get anchors from catalog
const anchors = await getAnchorsForCorridor ({ origin: "US" , destination: "MX" });
// Resolve capabilities from stellar.toml + SEP endpoints
const runtimes = await Promise . all ( anchors . map ( resolveAnchorRuntime ));
// Filter operational anchors
const originAnchors = runtimes . filter ( r => r . catalog . type === "on-ramp" && r . operational );
const destinationAnchors = runtimes . filter ( r => r . catalog . type === "off-ramp" && r . operational );
// Build route matrix
const routes = [];
for ( const origin of originAnchors ) {
for ( const dest of destinationAnchors ) {
routes . push ( buildRoute ( input , origin , dest , exchangeRate ));
}
}
return scoreRoutes ( routes );
2. User selects route
Frontend displays routes, user clicks “Select” on best route.
3. SEP-10 authentication
// Request challenge
const { transaction , networkPassphrase } = await getSep10Challenge ({
webAuthEndpoint: route . originAnchor . webAuthEndpoint ,
accountId: userPublicKey ,
homeDomain: route . originAnchor . domain ,
});
// User signs with Freighter wallet
const signedTx = await window . freighterApi . signTransaction ( transaction , {
networkPassphrase ,
});
// Submit signed challenge
const { token } = await submitSep10Challenge ({
webAuthEndpoint: route . originAnchor . webAuthEndpoint ,
signedTransaction: signedTx ,
});
4. SEP-24 deposit
// Get interactive deposit URL
const response = await fetch ( ` ${ transferServer } /transactions/deposit/interactive` , {
method: "POST" ,
headers: { Authorization: `Bearer ${ token } ` },
body: JSON . stringify ({
asset_code: "USDC" ,
account: userPublicKey ,
amount: "1000" ,
}),
});
const { url , id } = await response . json ();
// Open popup for KYC/payment
const popup = window . open ( url , "sep24" , "width=500,height=700" );
// Poll for completion
const interval = setInterval ( async () => {
const status = await fetch ( ` ${ transferServer } /transaction?id= ${ id } ` , {
headers: { Authorization: `Bearer ${ token } ` },
}). then ( r => r . json ());
if ( status . status === "completed" ) {
clearInterval ( interval );
popup . close ();
showProofOfPayment ( status . stellar_transaction_id );
}
}, 2000 );
5. Proof of payment
const stellarExpertUrl = `https://stellar.expert/explorer/public/tx/ ${ stellarTxHash } ` ;
< a href = { stellarExpertUrl } target = "_blank" >
View transaction on StellarExpert
</ a >
Fee structure
On-chain fees (Stellar Network)
Base fee : 100 stroops (~$0.00001 USD)
Network congestion : Can increase to 1000 stroops (~$0.0001)
PayOnProof pays network fees from operational account.
Anchor fees
Extracted from SEP-24/SEP-6 /info endpoints:
function extractFeeFromInfo ( info : unknown , assetCode : string ) {
const deposit = info . deposit [ assetCode ];
return {
fixed: deposit . fee_fixed , // Fixed fee in asset units
percent: deposit . fee_percent , // Percentage fee (e.g., 0.5 = 0.5%)
};
}
Fee calculation:
const onRampFee = originAnchor . fees . percent ; // e.g., 0.2%
const offRampFee = destinationAnchor . fees . percent ; // e.g., 1.0%
const bridgeFee = 0.2 ; // PayOnProof fee
const totalFeePercent = onRampFee + bridgeFee + offRampFee ; // 1.4%
const feeAmount = amount * ( totalFeePercent / 100 );
const receivedAmount = ( amount - feeAmount ) * exchangeRate ;
Security considerations
Private key management
PayOnProof never stores user private keys. All transaction signing occurs in the user’s browser via Freighter wallet.
Backend signing secret:
# Only used for operational transactions (anchor catalog refresh, etc.)
STELLAR_SIGNING_SECRET = S...
Stored in Vercel environment variables, never exposed to frontend.
Anchor validation
Before calling any anchor endpoint:
Verify stellar.toml HTTPS : Must be served over HTTPS
Validate TOML signature : Check SIGNING_KEY matches expected value
Allowlist domains : Only interact with anchors in catalog
Rate limiting : Prevent abuse of anchor APIs
Timeout enforcement : 8-second timeout on all anchor requests
Transaction verification
After transaction submission:
const server = getHorizonServer ();
const tx = await server . transactions (). transaction ( txHash ). call ();
if ( ! tx . successful ) {
throw new Error ( "Transaction failed on Stellar" );
}
// Verify memo matches expected value
if ( tx . memo !== expectedMemo ) {
throw new Error ( "Memo mismatch" );
}
Production checklist
Next steps
Architecture overview Review the full system architecture
API reference Explore API endpoints and request/response formats