A developer-friendly payment gateway API built on Stellar for accepting, verifying, and managing payments in XLM and USDC.
Think Stripe — but powered by the Stellar blockchain instead of banks.
StellarGate abstracts Stellar payments into a simple REST API. Developers can create payment intents, receive a destination address and memo, and get notified when payment is confirmed on-chain.
Client App → POST /payments → get address + memo
User pays via Stellar wallet (e.g. Lobstr)
StellarGate detects transaction on Horizon
Payment marked complete → webhook fired to your app
This project is under active development. The following is implemented:
-
POST /payments— create a payment intent -
GET /payments/:id— query payment status -
GET /payments— list & filter payments (pagination) -
GET /health— health check - SQLite persistence
- Input validation (asset, amount as exact stroops, webhook URL)
- Transaction listener (Horizon SSE streaming + interval polling)
- Payment verification (memo + asset + amount)
- Webhook dispatch (HMAC-SHA256 signed, with retries)
- Multi-merchant support (
merchant_idper payment) - Pending-intent expiry (configurable TTL +
payment.expiredwebhook) - Horizon streaming (currently polled on an interval)
- Dashboard UI
- Language: Rust
- HTTP Framework: axum
- Database: SQLite via sqlx
- Async Runtime: tokio
- Blockchain: Stellar Horizon API
- Rust 1.75+ — install via rustup
git clone https://github.com/StellarGateLabs/StellarGate.git
cd StellarGate
cp .env.example .env
# Edit .env with your Stellar keys| Variable | Description | Default |
|---|---|---|
PORT |
HTTP port | 3000 |
DATABASE_URL |
sqlx connection string | sqlite:stellargate.db |
STELLAR_NETWORK |
testnet or public |
testnet |
STELLAR_HORIZON_URL |
Horizon endpoint | testnet |
STELLAR_GATEWAY_PUBLIC |
Your gateway wallet public key | — |
STELLAR_GATEWAY_SECRET |
Your gateway wallet secret key | — |
ACCEPTED_ASSETS |
Comma-separated assets to accept. Format: CODE for native (e.g. XLM) or CODE:ISSUER for non-native (e.g. USDC:GISSUER). Adding an asset is config-only — no code changes needed. |
XLM,USDC:<testnet-issuer> |
STELLAR_LISTENER_MODE |
stream (SSE + poller reconciler) or poll (interval only) |
stream |
POLL_INTERVAL_SECS |
How often the Horizon poller reconciles | 10 |
PAYMENT_TTL_SECS |
How long a payment intent stays pending before it is expired (from created_at) |
3600 |
WEBHOOK_SECRET |
HMAC signing secret for webhooks | — |
WEBHOOK_RETRY_ATTEMPTS |
Webhook delivery attempts | 3 |
WEBHOOK_RETRY_DELAY_MS |
Delay between webhook retries | 5000 |
CORS_ALLOWED_ORIGINS |
Comma-separated allowed CORS origins (e.g. https://app.example.com). Required on public network; omitting on testnet falls back to permissive with a warning. |
(unset — permissive on testnet) |
RATE_LIMIT_REQUESTS_PER_SEC |
Rate limit for POST /payments (requests per second per IP) |
10 |
DATABASE_URLis a sqlx connection string (sqlite:stellargate.db), not a file path. The Horizon poller stays idle untilSTELLAR_GATEWAY_PUBLICis set. The poller pages forward through payments from a cursor persisted in the database, so it never misses an intent regardless of on-chain volume and resumes from where it left off after a restart.
cargo runThe quickest way to run StellarGate without installing Rust:
cp .env.example .env
# Edit .env with your Stellar keys, then:
docker compose up --buildThe API will be available at http://localhost:3000. The SQLite database is
stored in a named Docker volume (stellargate_data) so it persists across
container restarts. Verify the service is healthy:
curl http://localhost:3000/health
# {"status":"ok"}To stop and remove containers while keeping the database volume:
docker compose downcargo testTests cover amount/stroops handling, Horizon payment verification, webhook signing, and the HTTP API (create, fetch, list/filter, validation).
Create a new payment intent.
Request
{
"amount": "10.00",
"asset": "XLM",
"merchant_id": "your-merchant-id",
"webhook_url": "https://yourapp.com/webhooks/stellar"
}| Field | Type | Required | Values |
|---|---|---|---|
amount |
string | ✅ | Any positive number |
asset |
string | ✅ | XLM or USDC |
merchant_id |
string | ❌ | Any string |
webhook_url |
string | ❌ | Valid HTTPS URL |
Response 201 Created
{
"id": "a1b2c3d4-...",
"destination_address": "GBBD47IF6LWK7P7...",
"memo": "A1B2C3D4",
"amount": "10.00",
"asset": "XLM",
"status": "pending",
"created_at": "2026-04-29T15:00:00",
"expires_at": "2026-04-29T16:00:00"
}The user must send exactly
amountofassettodestination_addresswithmemoset as the transaction memo. The intent expires atexpires_at(default one hour after creation) if unpaid.
Fetch the current status of a payment.
Response 200 OK
{
"id": "a1b2c3d4-...",
"merchant_id": "your-merchant-id",
"destination_address": "GBBD47IF6LWK7P7...",
"memo": "A1B2C3D4",
"amount": "10.00",
"asset": "XLM",
"status": "pending",
"tx_hash": null,
"paid_amount": null,
"created_at": "2026-04-29T15:00:00",
"updated_at": "2026-04-29T15:00:00",
"expires_at": "2026-04-29T16:00:00"
}Status values
| Status | Meaning |
|---|---|
pending |
Awaiting payment |
completed |
Payment confirmed on-chain |
failed |
Partial payment or verification failed |
expired |
TTL elapsed before payment arrived; no longer watched |
List payments, newest first.
Query parameters
| Param | Description | Default |
|---|---|---|
status |
Filter by pending, completed, failed, or expired |
all |
limit |
Page size (1–100) | 20 |
offset |
Rows to skip | 0 |
Response 200 OK
{
"total": 42,
"limit": 20,
"offset": 0,
"payments": [ { "id": "...", "status": "pending", "...": "..." } ]
}Cheap liveness probe. Always returns 200 OK as long as the process is running.
200 OK — { "status": "ok" }Readiness probe. Runs SELECT 1 against the database; returns 503 when unreachable.
200 OK — { "status": "ok" }
503 Unavailable — { "status": "unavailable" }1. Developer calls POST /payments
2. StellarGate returns { destination_address, memo, amount }
3. End user sends payment via any Stellar wallet
4. StellarGate listener detects the transaction on Horizon (SSE stream, ~1s; poller as fallback)
5. Verifies: correct memo + amount + asset
6. Updates payment status and fires a webhook event
Every on-chain payment matched by memo, destination, and asset is resolved as follows:
| Scenario | status |
Webhook event | delta field |
|---|---|---|---|
| Paid exactly the requested amount | completed |
payment.completed |
not present |
| Paid more than requested | completed |
payment.overpaid |
excess amount (should be refunded) |
| Paid less than requested | underpaid |
payment.underpaid |
shortfall still owed |
| Top-up brings cumulative total to exactly expected | completed |
payment.completed |
not present |
| Top-up brings cumulative total above expected | completed |
payment.overpaid |
cumulative excess |
Overpayment: The intent is fulfilled and moves to completed. The payment.overpaid event includes a delta field showing the excess amount the merchant should consider refunding to the sender.
Underpayment: The intent moves to underpaid and remains watchable. StellarGate continues polling for a follow-up payment to the same memo. When the cumulative total meets or exceeds the requested amount, the intent completes normally.
Top-up limitation: Only a single follow-up payment is tracked per underpaid intent. If multiple partial payments are needed, the sender should consolidate them — send the full remaining shortfall (shown in delta) in one transaction.
Post-completion payments: Once an intent reaches completed, any further on-chain payments to the same address and memo are not tracked and will not trigger additional webhooks.
Fired when the cumulative received amount equals the requested amount exactly.
{
"event": "payment.completed",
"payment_id": "a1b2c3d4-...",
"merchant_id": "your-merchant-id",
"tx_hash": "abc123...",
"amount": "10.00",
"paid_amount": "10",
"asset": "XLM",
"status": "completed"
}Fired when the cumulative received amount exceeds the requested amount. delta is the excess the merchant should refund.
{
"event": "payment.overpaid",
"payment_id": "a1b2c3d4-...",
"merchant_id": "your-merchant-id",
"tx_hash": "abc123...",
"amount": "10.00",
"paid_amount": "12.5",
"asset": "XLM",
"status": "completed",
"delta": "2.5"
}Fired when a payment is received but falls short of the requested amount. delta is the remaining shortfall. The intent stays open for a top-up.
{
"event": "payment.underpaid",
"payment_id": "a1b2c3d4-...",
"merchant_id": "your-merchant-id",
"tx_hash": "abc123...",
"amount": "10.00",
"paid_amount": "7",
"asset": "XLM",
"status": "underpaid",
"delta": "3"
}Event types: payment.success (paid in full), payment.failed (underpaid or
verification failed), and payment.expired (the intent's TTL elapsed before
payment arrived). The event field carries the type; status carries the
matching payment status.
Webhooks are signed with X-StellarGate-Signature (HMAC-SHA256) so you can verify authenticity.
src/
├── main.rs # Entry point, server startup, listener/poller spawn, graceful shutdown
├── lib.rs # Shared state and module exports
├── config.rs # Environment configuration
├── db.rs # Database queries (SQLite)
├── money.rs # Stroops-based amount parsing/validation
├── horizon.rs # Horizon polling listener + payment verification
├── expiry.rs # Background sweeper that expires overdue pending intents
├── webhook.rs # HMAC-SHA256 signed webhook dispatch
└── api/
├── mod.rs # Axum router, layers (CORS/trace/body-limit), 404 fallback
└── payments.rs # Payment handlers (create, get, list)
tests/
└── api_tests.rs # Integration tests
This project is open to contributors. See the Wave Program for scoped issues you can pick up.
To contribute:
- Fork the repo
- Create a branch:
git checkout -b feat/your-feature - Make your changes and add tests
- Run
cargo test— all tests must pass - Open a pull request
MIT