Skip to content

Instantly share code, notes, and snippets.

@MastaP
Created June 17, 2026 10:06
Show Gist options
  • Select an option

  • Save MastaP/a880d41614b69038b76ceb5df58972ec to your computer and use it in GitHub Desktop.

Select an option

Save MastaP/a880d41614b69038b76ceb5df58972ec to your computer and use it in GitHub Desktop.
External crypto payment rail — research, comparison & plan (aggregator-subscription gateway)

External crypto payment rail — research, comparison & plan

Working notes (uncommitted). Goal: let a brand-new user who already holds common crypto pay to bootstrap their first API key, breaking the Unicity chicken-and-egg. Research → analysis → plan only; no implementation yet.

Sourced from a multi-agent web-research pass (6 finder dimensions, 32 options, ~95 primary sources fetched) + a codebase-fit analysis. As of mid-2026.


TL;DR recommendation

Rank Approach Why
Primary Self-hosted HD-wallet acceptance of USDC on an L2 (Base/Arbitrum), detected via an Alchemy/QuickNode address-activity webhook, credited through one new Jetty Handler.Abstract endpoint Genuinely non-custodial (funds land in operator-derived addresses; the provider only watches), reuses the existing payment_sessions design almost 1:1, stablecoin = trivial fixed $1–50 price, L2 = sub-cent payer fee, zero prior relationship needed. Backend code ≈ an HMAC-verified JSON webhook + a couple of REST calls.
Runner-up (purest) Same HD-wallet design but poll eth_getLogs with web3j instead of a webhook No third party in the payment path at all, no public endpoint to secure (outbound-only). Cost: you own a poller (confirmation depth, reorg, last-scanned-block persistence).
Most elegant bet x402 (HTTP-402 + USDC on Base) via a facilitator Conceptually the cleanest fit for an API gateway — answer the bootstrap request with 402, verify+settle synchronously, mint the key. No webhook, no watcher. But the Java SDK is 0.1.0-SNAPSHOT and servlet-filter-shaped (not Jetty), and it leans on a Coinbase facilitator in the settle path. Great strategic direction, slightly early to bet the only purchase path on.
Hosted fallback NOWPayments (email-only signup, no merchant KYC for crypto-only, REST + HMAC-SHA512 IPN, 200+ assets) If you'd rather someone else watch every chain. Honest cost: hybrid-custodial (funds transit their wallet), which departs from this project's non-custodial ethos.
Do NOT adopt Coinbase Commerce Its non-custodial product shut down March 31 2026 outside US/Singapore. A cautionary tale for betting the bootstrap path on one hosted vendor's roadmap.

Reframing that raises the stakes: the native Unicity completePayment() is currently disabled (/api/payment/complete → 501) pending the SDK-v2 migration. So an external rail isn't merely a bootstrap on-ramp right now — it's effectively the only working purchase path. Design it to stand alone, not just to hand off into the native flow.


Why this fits this codebase (the real reason it's "simple")

The current payment_sessions + PaymentService design already implements ~every pattern a crypto rail needs. An external USDC rail mostly reuses it:

Pattern the research says you need Already in this repo What changes for a USDC rail
Unique receive address per session payment_sessions derives a masked address from serverSecret + receiverNonce Derive an EVM address per session instead (web3j BIP-32 from the same seed). EVM has no memo field, so per-session address is the right matching strategy.
Exactly-once activation / double-credit guard unique request_id index + activation in one DB tx Swap the dedup key to UNIQUE(chain, tx_hash, log_index); keep the insert in the same tx as the key mint.
Serialize concurrent completers FOR UPDATE NOWAIT, lock order api_key → session Keep verbatim.
New-key purchase should_create_key, api_key nullable, ApiKeyService mints sk_…, ApiKeyUtils sets 30-day expiry Reuse the entire mint/activate path unchanged — it's the back half of the disabled completePayment().
Quote/expiry window 15-min session expiry Keep (for USDC it's an anti-stale-address measure, not FX protection — stablecoin drift ≈ 0).
Amount validation validateIncomingCoins rejects over and under ⚠️ Must relax — see pitfalls. Exchange withdrawals deduct network fees from the sent amount, so strict-exact will hard-fail nearly every first payment.
Manual reconciliation AdminHandler payment-session views Extend with the new rail's sessions.

Net new pieces: (1) EVM address derivation (web3j, one-time per session), (2) a detection mechanism (webhook handler or poller), (3) a per-(chain,asset) confirmation-depth state machine (PENDING → CONFIRMED), (4) a tolerance band on amount matching, (5) USDC→treasury sweep ops.


Comparison matrix (all serious options)

Option Custody Assets/Chains Integration Java/Jetty fit Ops Fees Settle latency KYC (op / payer) Bootstrap Maturity 2026
HD-wallet + Alchemy webhook Non-custodial USDC/USDT/ETH on Base/Arb/OP/Polygon Webhook push + REST addr-reg High (JSON+HMAC; web3j for derivation only) Low 0% processor; payer sub-cent gas Real-time, ~secs Dev acct (email) / none Excellent Mature
HD-wallet + eth_getLogs poll (web3j) Non-custodial USDC/ETH on L2s Poller (outbound only) Strong (web3j; no public endpoint) Medium 0%; payer gas ~secs + poll lag none / none Excellent Mature pattern
x402 (CDP facilitator) Non-custodial USDC on Base/Polygon/Arb/Solana, EURC HTTP-402 sync verify+settle Medium (Java SDK SNAPSHOT, not Jetty) Low Free ≤1k tx/mo, then $0.001/tx; gasless payer sub-sec–secs Free CDP acct / none Excellent Strong; Java SDK early
x402 self-hosted facilitator Non-custodial USDC EVM L2 HTTP-402 + own relayer Heavier (web3j + relayer key/gas) High no fee; you fund gas as above none / none Excellent Bespoke in Java
NOWPayments Hybrid 200–300+ incl USDC Base/Solana/Tron Webhook (IPN) + REST High (REST + HMAC-SHA512) Low 0.5% same-coin on-chain (~secs–1min L2) email only / none Excellent Mature
Helio (MoonPay Commerce) Non-custodial USDC/SOL/ETH/BTC (Solana-centric) Webhook + Pay Link High Low 2% (1% w/ HelioX) near-instant Solana light / none Excellent Mature (rebrand flux)
Stripe Stablecoin (Bridge) Custodial USDC Eth/Sol/Polygon/Base Webhook + hosted redirect Best — official stripe-java v33, Java 21 Low flat 1.5% secs–mins Stripe KYB / none Strong (if user holds USDC) GA US-only, EU beta
Circle Programmable Wallets Hybrid USDC 19+ chains, EURC Webhook + REST (Circle watches) Medium (no Java SDK, REST) Medium usage-tiered, sales-gated on-chain finality Circle KYB / none Good Mature
BTCPay Server Non-custodial BTC on-chain + Lightning; altcoins via plugin Webhook (BTCPay-Sig) + own node High (thin REST + HMAC) High 0% (network only) LN ~secs / on-chain ~10 min none / none Excellent (BTC) Very mature
OpenNode Custodial BTC + Lightning Webhook + REST High Low 1% LN instant / chain ~10 min full KYB / none Good (payer) Established
CoinGate / BitPay Custodial 70–100+, fiat settle Webhook + REST High (BitPay Java lib) Med/High ~1%+ stacked chain + fiat next-day full KYC/KYB / none Fine (payer) Mature, regulated
Transak / MoonPay on-ramp Non-custodial delivery card/bank → 100+ assets Hosted widget + webhook Medium Medium ~1–4.5% card + min mins + KYC offloaded / user KYC Solves no-crypto user only Mature

Shortlist, in depth

1. HD-wallet + provider address-activity webhook (USDC on Base) — recommended default Best balance of non-custodial purity and minimal code on this stack. The provider (Alchemy/QuickNode) only watches addresses you register; USDC lands directly in operator-derived addresses. Integration ≈ a single Handler.Abstract webhook that constant-time-verifies X-Alchemy-Signature + timestamp, dedups on txHash:logIndex, and calls the existing mint path. Watch-outs: the public webhook is a free-key forgery vector if signature/timestamp/replay checks are sloppy; vendor delivery hiccups can silently stall payments (many teams add a polling reconciliation sweep — which erodes the simplicity edge slightly).

2. HD-wallet + eth_getLogs polling (web3j) — purest, zero-vendor The closest match to the existing philosophy (server secret → derived address → verify on-chain). Outbound-only, no public endpoint, no processor account. web3j gives first-class Java HD derivation + ERC-20 Transfer log filtering. Watch-outs: you write and operate the poller — confirmation depth, reorg handling, last-scanned-block persistence across restarts, few-second detection lag. (A free RPC tier is fine.)

3. x402 (USDC on Base) — most elegant, slightly early The HTTP-402 + retry shape is the natural way for an API gateway to charge: emit 402 with the required USDC amount, the client signs a gasless EIP-3009 authorization, you verify+settle via a facilitator (synchronous — no webhook, no watcher, no reconciliation table), then mint the key. Truly non-custodial; gasless for a brand-new payer (needs only USDC, no ETH). 2026 adoption is strong (Base/Polygon/Arbitrum/Solana; AWS exposes it at CloudFront/WAF). Watch-outs: the official Java SDK is 0.1.0-SNAPSHOT and servlet-filter oriented — on Jetty 12 you'd write a thin REST integration to the facilitator, not drop-in. Hosted facilitator = a trusted third party in settlement.

4. NOWPayments — pragmatic hosted fallback Fastest go-live: email-only signup, no merchant KYC for crypto-only, plain REST + HMAC-SHA512 IPN that mirrors the existing PaymentHandler, 200+ assets so the payer brings whatever they hold. Watch-outs: hybrid-custodial (funds touch their wallet before forwarding) and the IPN signature requires exact alphabetical JSON key-sorting (easy to get subtly wrong).

5. BTCPay Server — BTC-first non-custodial gold standard Funds go straight to the operator's xpub, 0% platform fee, no KYC, signed webhooks. Native on-chain BTC means a new user with BTC anywhere can pay. Watch-outs: highest ops burden (run a Bitcoin full node ± Lightning), ~10-min on-chain confirmation hurts one-shot bootstrap UX, and BTC volatility forces per-invoice re-quotes against a fixed USD price.


Pitfalls to honor (most are already solved in this repo)

  1. Relax strict-exact-amount → tolerance band. Current validateIncomingCoins rejects over and under. Exchange withdrawals deduct the network fee from the sent amount, so "exact" rarely arrives — strict matching would hard-fail most first payments and defeat the whole goal. Accept ≥ required − ε; decide an overpayment policy (credit-forward vs manual refund).
  2. Confirmation depth vs reorg. Don't credit on 0-conf. Use a per-(chain,asset) PENDING → CONFIRMED threshold. Circle's production thresholds: Ethereum 12 blocks (~3 min), Base/OP 12 (~3–9 min), Arbitrum 12 (~4–6 min), Polygon 2–3 (~8 s), Solana 1 (~0.4 s). Optimistic-rollup true finality is 7 days; crediting on L2 soft-confirm for a $1–50 key is the accepted, documented compromise.
  3. Idempotency. Keep the existing shape: deterministic dedup key (txHash:logIndex) under a UNIQUE constraint, inserted in the same DB tx as the mint; webhooks/watchers are at-least-once.
  4. Webhook security (if not polling): HMAC over raw bytes before JSON parse, constant-time compare (MessageDigest.isEqual — already used in CryptoUtils), signed-timestamp freshness (~5 min), persisted event-id replay guard.
  5. Irreversibility / refunds. Crypto is final — no chargebacks (a feature), but state "payments are irreversible, this one-off key is non-refundable" before pay. Any refund is a new outbound, possibly-regulated, irreversible transfer.
  6. Sweep/treasury. USDC can't pay its own gas; you must fund native gas to dust deposit addresses to sweep to treasury (or use account abstraction). Decide who funds it and on what schedule.
  7. Sanctions/AML. Cheapest posture: accept only mainstream USDC-on-L2 to minimize exposure; optionally screen source addresses at credit time (Chainalysis/TRM/Elliptic/AMLBot). Money-transmitter/Travel-Rule scope is jurisdiction-specific — legal review, not an engineering call.

Decisions to make before any implementation

  1. Asset/chain — commit to one first (recommendation: USDC on Base). This determines web3j-vs-LN, the provider, and the confirmation table.
  2. Custody stance — is hybrid (NOWPayments) or custodial (Stripe) acceptable as a fallback, or is strictly non-custodial a hard requirement (rules out all hosted processors)?
  3. Operator jurisdiction & legal posture — drives money-transmission/VASP/Travel-Rule exposure and whether US-only rails (Stripe Stablecoin, Coinbase Business) are even available.
  4. Amount-matching policy — tolerance band ε, and overpayment handling (credit-forward vs refund-owed ledger).
  5. Confirmation depth per chain — credit at L2 soft-confirm (~secs) or wait deeper?
  6. Detection — webhook (least code, public attack surface, vendor in loop) vs polling (outbound-only, you own the watcher) vs both (webhook + reconciliation sweep).
  7. Persistent key vs per-request metering — mint a key on first payment (matches current model) or also support x402-style per-call gating later?
  8. No-crypto users — if a meaningful share hold no crypto, none of these serve them; you'd add a card on-ramp (Transak/MoonPay, user KYC, poor small-ticket economics) separately.
  9. Native-rail timing — since /complete is 501 today, should the external rail be self-standing (the only purchase path) rather than a hand-off into the native flow?

Phased plan (when you're ready — not started)

  • Phase 0 — Decide. Resolve the decisions above, especially asset/chain, custody, jurisdiction. Without these the schema and provider choice are premature.
  • Phase 1 — Model. Extend payment_sessions (or a sibling table) with rail/chain/asset, the derived EVM deposit_address, a PENDING/CONFIRMED/FAILED state, and a processed_chain_events UNIQUE(chain, tx_hash, log_index) dedup table. New Flyway V2x__…. Reuse should_create_key + the nullable api_key.
  • Phase 2 — Address derivation. web3j BIP-32 from the existing server seed → one deterministic EVM address per session (mirrors receiverNonce). Pure unit-testable, no network.
  • Phase 3 — Detection. Implement one of: (a) a new Handler.Abstract webhook (HMAC + timestamp + replay, raw-bytes verify) in the combined chain, or (b) a web3j poller with persisted last-scanned block. Spike both behind an interface; pick per the Phase-0 decision.
  • Phase 4 — Activation. On CONFIRMED, run the existing mint/activate path (the back half of completePayment()) inside one tx with the dedup insert and the api_key → session lock order. Apply the tolerance band.
  • Phase 5 — Admin & ops. Extend AdminHandler views for the new rail; build the USDC→treasury sweep job and gas funding; add reconciliation (and, if webhook-primary, a polling safety sweep).
  • Phase 6 — Harden. Sanctions screening (if in scope), metrics (gateway_payment_*), alerting on stuck PENDING, runbook for stuck/over/under/wrong-chain deposits, and the irreversibility/refund policy copy.

Key sources (primary)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment