Introduction

Stargate is a USDC payment gateway built on Stellar — invoices, hosted checkout, an embeddable widget, and webhook delivery, backed by non-custodial escrow and multi-sig settlement. Think Stripe, but the ledger is Stellar and the asset is USDC.

Merchants create an invoice through the API or dashboard, the payer settles it directly on-chain from their own wallet, and a reconciler confirms the payment and fires a webhook. Stargate never custodies funds mid-flow — payments go straight from payer to a merchant-specific muxed Stellar address.

Every payer address is screened against an on-chain compliance oracle before a transaction is even built, and results are cached for an hour to keep checkout fast.

How it Works

An invoice moves through five stages, each owned by a different service:

StageOwnerWhat happens
createAPIMerchant creates an invoice; a muxed Stellar address is derived for it
payWalletPayer fetches unsigned XDR, signs with Freighter/Albedo, submits to Horizon
reconcileReconcilerRust sidecar streams Horizon SSE, matches the payment by muxed ID
notifyWebhooksSigned HTTP callback fires to the merchant endpoint
settleTreasury2-of-3 multi-sig contract disburses batched settlement

End-to-end sequence

flow
Merchant  ──POST /invoices──▶  API  ──▶  Postgres (invoice row + muxed address)
Payer     ──GET  pay/:id────▶  Checkout ──▶ API (prepare-tx) ──▶ Horizon (sign + submit)
Horizon   ──SSE payments────▶  Reconciler ──▶ Postgres (paid) + Redis (publish)
Redis     ──message─────────▶  API (SSE)  ──▶ Checkout (success screen)
Reconciler ──insert delivery─▶ Webhook worker ──▶ Merchant endpoint (HMAC signed)

Creating Invoices

Authenticate with a bearer token, then create an invoice with an amount and expiry:

bash
curl -X POST https://api.stargate.finance/invoices \
  -H "Authorization: Bearer <access_token>" \
  -H "Content-Type: application/json" \
  -d '{
    "amount_usdc": "10.00",
    "memo": "Order #1001",
    "expires_in_seconds": 900
  }'
json
{
  "id": "inv_abc123",
  "stellar_address": "M...",
  "amount_usdc": "10.00",
  "status": "pending",
  "payment_url": "https://pay.stargate.finance/pay/inv_abc123",
  "expires_at": "2026-07-06T00:15:00Z"
}

Send payment_url to your customer, or mount the embeddable widget directly on your own checkout page.

Hosted Checkout & Wallets

/pay/[id] is a public hosted checkout page. It fetches the public invoice, screens the payer address, requests payer-specific unsigned XDR from the API, and hands it to the payer's wallet — Stargate never signs or holds funds on the payer's behalf.

Supported wallets

Freighter and Albedo sign the XDR directly in-browser. Mobile wallets (LOBSTR, Solar, Vibrant) scan a SEP-0007 QR code rendered on the checkout page, which encodes aweb+stellar:pay deep link.

Confirmation

Once submitted to Horizon, the checkout page opens a server-sent events stream against /payments/:id/stream. The reconciler publishes to Redis on confirmation, and the API relays that over SSE — no polling required.

Embeddable Widget

Drop a single script tag on your own site to mount a sandboxed checkout iframe, or mount it programmatically for more control.

html
<script src="https://pay.stargate.finance/widget/stargate-widget.js"
        data-invoice-id="inv_abc123"
        data-container="#pay-here"></script>
js
window.Stargate.mount({
  invoiceId: 'inv_abc123',
  container: document.getElementById('checkout'),
  origin: 'https://pay.stargate.finance',
  onSuccess: ({ invoiceId, txHash }) => { /* order fulfilled */ },
  onError: ({ code, message }) => { /* show error */ },
});

The iframe emits STARGATE_LOADED, STARGATE_PAID, and STARGATE_ERROR events via postMessage. Always validate event.origin before trusting a message.

Webhooks

Subscribe to only the event types you need:

EventFired when
invoice.paidOn-chain payment confirmed by the reconciler
invoice.expiredInvoice passes expires_at unpaid
invoice.cancelledMerchant cancels via API
settlement.completedBatch disbursed to merchant treasury
merchant.kyc.approvedKYC review approved
merchant.kyc.rejectedKYC review rejected

Every delivery carries X-Stargate-Signature: sha256=<hex>. Verify it before processing the body:

js
const digest = crypto
  .createHmac('sha256', webhookSecret)
  .update(rawBody)
  .digest('hex');

if (digest !== signatureFromHeader) {
  throw new Error('invalid webhook signature');
}

Deliveries are queued on Redis Streams with a consumer group, so retries use exponential backoff with jitter and are delivered at least once — build your handler to be idempotent on invoice.id.

Compliance & Settlement

Before any payment XDR is built, the payer address is checked against an on-chain ComplianceContract. A blocked address gets a rejected transaction before it ever reaches Horizon — funds are never in flight to a screened-out address.

Settlement batches merchant balances and disburses them through a TreasuryContract requiring 2-of-3 multi-sig approval, signed via KMS in production.

Security

The hosted checkout and widget enforce strict boundaries so an embedding page can never intercept payment data:

http
Content-Security-Policy: frame-ancestors https://*.stargate.finance
X-Frame-Options: DENY   # on /dashboard/*
X-Content-Type-Options: nosniff
Strict-Transport-Security: max-age=63072000; includeSubDomains; preload
Webhook URLs are re-resolved on every retry and rejected if they resolve to a private or loopback IP, to close off SSRF via DNS rebinding on redirect.

Contracts

Three Soroban contracts back the protocol: invoice (escrow state machine), treasury (multi-sig settlement), and compliance (allow/block oracle). ABI metadata is versioned alongside the backend.

Mainnet contract IDs are published after the signing ceremony documented in MAINNET_DEPLOYMENT.md — check the dashboard's developer settings for the current network's addresses.

FAQ

Does Stargate ever custody funds?

No. Payments go directly from the payer's wallet to a merchant-specific muxed Stellar address. Stargate builds the unsigned transaction and confirms it after the fact — it never holds a signing key for customer funds.

What happens if a webhook delivery fails?

It retries with exponential backoff via a Redis Streams consumer group. Delivery is at-least-once, so handlers should be idempotent.

Can I self-host the widget?

The widget script itself can be self-hosted, but the iframe it mounts always points back at the Stargate checkout origin — that's what keeps signing and compliance screening out of your page's trust boundary.