Most developers think about crypto payments as a checkout problem.
Create an invoice. Show a payment link. Wait for a callback. Mark the order as paid.
That is enough for a demo.
It is not enough for a real merchant.
Once a business starts receiving meaningful crypto payment volume, the hard question is no longer only:
Did the customer pay?
The harder questions are:
Which order does this payment belong to?
Was the amount exact, underpaid, late, duplicated, manually accepted, refunded, or still waiting?
Did the order system, support team, finance team, and payment provider all agree on the same state?
Can the merchant prove what happened when a customer opens a support ticket?
That is the opportunity.
A developer can build a crypto payment reconciliation tool for merchants.
Not another crypto checkout.
Not another wallet dashboard.
A tool that connects payment events, invoices, orders, customers, transactions, support records, reports, and exceptions into one operational system.
In this article, I will use OxaPay as the example payment infrastructure because its documentation exposes the primitives a developer needs for this kind of product: invoice generation, payment webhooks, payment information lookup, payment history search, static addresses, SDKs, and automation integrations.
The goal is not to build a toy script. The goal is to understand how a developer can turn payment reconciliation into a real merchant-facing product or service.
The business opportunity
Merchants do not usually wake up thinking they need a reconciliation tool.
They discover the need when payment volume creates operational friction.
At low volume, a merchant can manually check payments. At higher volume, manual checking starts to break down.
A customer says they paid, but the order is still pending.
An invoice expires, but the blockchain transaction arrives late.
A customer pays less than expected.
A support agent sees an order ID, but the payment provider stores a track ID.
The finance team exports payment history, but the ecommerce platform has a different number of paid orders.
The developer who solves this problem is not selling “API integration.”
They are selling operational confidence.
A reconciliation product helps merchants answer:
- Which orders are fully paid?
- Which paid orders were not fulfilled?
- Which payments are unresolved?
- Which invoices are expired but have transaction activity?
- Which payments are underpaid or manually accepted?
- Which customers need support follow-up?
- Which payment records should be exported for finance?
- Which webhook events were received, ignored, duplicated, or failed?
That is much more valuable than simply adding a payment button.
Who would pay for this?
A crypto payment reconciliation tool is not for every merchant.
A small seller with five payments per month probably does not need it.
The best customers are merchants where payment confusion has a real cost.
Good targets include:
- Digital product stores
- SaaS platforms
- Hosting companies
- VPN and reseller panels
- Online course platforms
- Telegram or Discord paid communities
- Software license sellers
- Cross-border service businesses
- Agencies that manage multiple merchant clients
- Marketplaces or creator platforms that receive many payments
The common pattern is simple: they receive payments, deliver something after payment, and need proof that payment state and business state match.
This product becomes more valuable when the merchant has:
- many orders
- multiple payment methods
- manual support tickets
- paid but undelivered orders
- expired invoice disputes
- multiple currencies or networks
- finance reporting needs
- operational staff who are not developers
The first version does not need to be a huge SaaS platform. It can start as a private dashboard, a managed reconciliation service, or a plugin-like internal tool for one niche.
What you are building
A crypto payment reconciliation tool sits between the merchant’s business system and the payment provider.
It receives payment events, stores normalized payment records, compares those records with merchant orders, detects mismatches, and gives humans a clear queue of exceptions.
The architecture looks like this:
Merchant store / SaaS / bot / CRM
↓
Order created
↓
OxaPay invoice / white-label / static address
↓
Customer payment
↓
Webhook callback
↓
Payment event store
↓
Reconciliation engine
↓
Matched / unresolved / risky / needs review
↓
Dashboard + alerts + finance export + support timeline
The product has four main responsibilities:
- Ingest payment events from webhooks.
- Backfill and verify payment records using payment information and payment history APIs.
-
Match payments to merchant orders using identifiers such as
order_id,track_id, amount, currency, email, address, and time window. - Surface exceptions that require action.
The most important idea is this:
Reconciliation is not just storing payment status. Reconciliation is proving that payment state, order state, fulfillment state, and finance state agree.
Why OxaPay has the right primitives for this
A reconciliation tool needs more than a checkout URL.
It needs persistent identifiers, payment status, callbacks, transaction metadata, history endpoints, and the ability to query records again when the webhook stream is incomplete.
OxaPay provides several useful primitives for this.
Generate Invoice
The Generate Invoice endpoint creates a payment invoice and returns a payment URL. The request can include fields such as amount, currency, lifetime, callback URL, return URL, email, and order_id.
That order_id is important. It lets the developer connect the payment session to the merchant’s internal order record from the beginning.
Reference: OxaPay Generate Invoice
Webhook
OxaPay webhooks send JSON notifications to a merchant-defined callback_url when payment status changes. The docs explain that the merchant should wait for the final paid state rather than treating the earlier paying status as final.
The webhook guide also documents HMAC SHA-512 callback validation using the merchant API key as the shared secret. It also explains the retry behavior: OxaPay expects an HTTP 200 response with ok, and retries webhook delivery up to five times with increasing delays.
Reference: OxaPay Webhook
Payment Information
The Payment Information endpoint lets a developer retrieve details for a specific payment using its track_id.
This is useful for support investigation, webhook recovery, and periodic verification.
Reference: OxaPay Payment Information
Payment History
The Payment History endpoint returns account payment records and supports filtering by criteria such as track_id, type, status, currency, network, address, date window, amount range, sorting, page, and size.
This is essential for reconciliation because a robust tool should not depend only on webhooks. It should also run scheduled jobs that compare the merchant’s internal records with provider-side history.
Reference: OxaPay Payment History
Payment Status Table
OxaPay documents payment statuses such as new, waiting, paying, paid, manual_accept, underpaid, refunding, refunded, and expired.
A reconciliation tool should map these provider statuses to internal operational states.
Reference: OxaPay Payment Status Table
Static Address
For some merchants, invoice-based payments are not the only model. OxaPay also supports static addresses linked to a unique track_id, with callback support for payments made to that address.
This can matter for recurring deposits, account top-ups, wallets, or customer-specific payment addresses.
Reference: OxaPay Generate Static Address
The core product: from payment records to reconciliation cases
The tool should not show merchants a raw table of transactions and call it a day.
The product should turn payment data into cases.
A case is a business question that needs resolution.
Examples:
Order paid but not fulfilled
Invoice expired but payment activity exists
Payment underpaid
Payment confirmed but order missing
Webhook received but signature invalid
Duplicate callback received
Provider status differs from local status
Payment refunded but order still active
Static address payment received without a matching customer action
This is where the product becomes valuable.
A raw payment dashboard is easy to ignore.
A prioritized exception queue is useful.
MVP scope
Do not start by building a full finance platform.
Start with a focused MVP.
A strong first version could include:
- Merchant account connection
- Invoice creation wrapper
- Webhook receiver
- HMAC validation
- Payment event log
- Order-to-payment matching
- Reconciliation status engine
- Exception queue
- Manual resolution notes
- CSV export
- Daily summary email or Telegram alert
The MVP should answer five questions well:
- Which orders are paid?
- Which paid orders are not fulfilled?
- Which payments need review?
- Which webhook events were missed or duplicated?
- What should support or finance do next?
That is enough to sell to an early merchant.
Data model
A reconciliation tool needs its own normalized database.
Do not rely only on the ecommerce platform database.
Do not rely only on the payment provider dashboard.
You need a middle layer that can compare both sides.
Here is a practical schema.
CREATE TABLE merchants (
id UUID PRIMARY KEY,
name TEXT NOT NULL,
oxapay_merchant_api_key_encrypted TEXT NOT NULL,
webhook_secret_hint TEXT,
created_at TIMESTAMP NOT NULL DEFAULT NOW()
);
CREATE TABLE merchant_orders (
id UUID PRIMARY KEY,
merchant_id UUID NOT NULL REFERENCES merchants(id),
external_order_id TEXT NOT NULL,
customer_email TEXT,
expected_amount NUMERIC(18, 8) NOT NULL,
expected_currency TEXT NOT NULL,
order_status TEXT NOT NULL,
fulfillment_status TEXT NOT NULL DEFAULT 'not_fulfilled',
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE (merchant_id, external_order_id)
);
CREATE TABLE payment_sessions (
id UUID PRIMARY KEY,
merchant_id UUID NOT NULL REFERENCES merchants(id),
order_id UUID REFERENCES merchant_orders(id),
provider TEXT NOT NULL DEFAULT 'oxapay',
provider_track_id TEXT NOT NULL,
provider_type TEXT NOT NULL,
requested_amount NUMERIC(18, 8),
requested_currency TEXT,
provider_status TEXT NOT NULL,
internal_status TEXT NOT NULL,
invoice_url TEXT,
expires_at TIMESTAMP,
raw_provider_payload JSONB,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
updated_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE (merchant_id, provider, provider_track_id)
);
CREATE TABLE payment_transactions (
id UUID PRIMARY KEY,
payment_session_id UUID NOT NULL REFERENCES payment_sessions(id),
tx_hash TEXT,
amount NUMERIC(18, 8),
currency TEXT,
network TEXT,
address TEXT,
tx_status TEXT,
confirmations INTEGER,
raw_tx_payload JSONB,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
UNIQUE (payment_session_id, tx_hash)
);
CREATE TABLE webhook_events (
id UUID PRIMARY KEY,
merchant_id UUID NOT NULL REFERENCES merchants(id),
provider TEXT NOT NULL DEFAULT 'oxapay',
provider_track_id TEXT,
event_status TEXT,
hmac_valid BOOLEAN NOT NULL,
payload_hash TEXT NOT NULL,
raw_payload JSONB NOT NULL,
received_at TIMESTAMP NOT NULL DEFAULT NOW(),
processed_at TIMESTAMP,
processing_status TEXT NOT NULL DEFAULT 'pending',
UNIQUE (merchant_id, payload_hash)
);
CREATE TABLE reconciliation_cases (
id UUID PRIMARY KEY,
merchant_id UUID NOT NULL REFERENCES merchants(id),
order_id UUID REFERENCES merchant_orders(id),
payment_session_id UUID REFERENCES payment_sessions(id),
case_type TEXT NOT NULL,
severity TEXT NOT NULL,
status TEXT NOT NULL DEFAULT 'open',
summary TEXT NOT NULL,
recommended_action TEXT,
assigned_to TEXT,
resolved_by TEXT,
resolution_note TEXT,
created_at TIMESTAMP NOT NULL DEFAULT NOW(),
resolved_at TIMESTAMP
);
This schema is intentionally operational.
It is not only for storing payments.
It is for investigating mismatches.
Internal payment state machine
Provider statuses are not always the same as business statuses.
A merchant does not only care whether a payment is paid.
They care whether the order is safe to deliver, whether support should intervene, and whether finance should include it in reporting.
You can map OxaPay statuses into internal states.
OxaPay status Internal state Business meaning
--------------------------------------------------------------------------
new created Invoice exists, no payer action yet
waiting awaiting_payment Payer selected currency, waiting for transfer
paying confirming Payment attempt seen, not final yet
paid paid_confirmed Payment can trigger fulfillment
manual_accept manually_accepted Merchant accepted manually; needs audit note
underpaid needs_review_underpaid Partial payment; support/finance review
expired expired_no_payment_or_late Invoice expired; check for late activity
refunding refund_in_progress Refund started
refunded refunded Refund completed
Do not trigger fulfillment on paying.
In OxaPay’s webhook documentation, paying is an earlier status and paid is the status to wait for before treating the payment as confirmed.
The reconciliation tool should make that distinction very visible.
Webhook receiver with HMAC validation
Webhook ingestion is the first technical building block.
This example uses Node.js and Express.
The important part is that HMAC validation must use the raw request body, not a parsed and re-serialized JSON object.
import express from "express";
import crypto from "crypto";
const app = express();
// Keep raw body for HMAC verification.
app.use("/webhooks/oxapay", express.raw({ type: "application/json" }));
function verifyOxaPayHmac(rawBody, receivedHmac, merchantApiKey) {
const calculated = crypto
.createHmac("sha512", merchantApiKey)
.update(rawBody)
.digest("hex");
const a = Buffer.from(calculated, "hex");
const b = Buffer.from(receivedHmac || "", "hex");
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}
app.post("/webhooks/oxapay", async (req, res) => {
const rawBody = req.body;
const receivedHmac = req.header("HMAC");
// In a multi-merchant product, resolve the merchant carefully.
// You might map by callback URL token, track_id lookup, or tenant route.
const merchant = await findMerchantFromWebhook(rawBody);
if (!merchant) {
return res.status(400).send("unknown merchant");
}
const hmacValid = verifyOxaPayHmac(
rawBody,
receivedHmac,
merchant.oxapayMerchantApiKey
);
const payloadText = rawBody.toString("utf8");
const payloadHash = crypto
.createHash("sha256")
.update(payloadText)
.digest("hex");
const payload = JSON.parse(payloadText);
await saveWebhookEvent({
merchantId: merchant.id,
provider: "oxapay",
providerTrackId: payload.track_id,
eventStatus: payload.status,
hmacValid,
payloadHash,
rawPayload: payload
});
if (!hmacValid) {
// Store the event for audit, but do not process it.
return res.status(401).send("invalid signature");
}
await enqueuePaymentEventProcessing({
merchantId: merchant.id,
providerTrackId: payload.track_id,
payloadHash
});
// OxaPay expects HTTP 200 with body "ok" for successful webhook delivery.
return res.status(200).send("ok");
});
A production implementation should process the webhook asynchronously.
The endpoint should validate, store, enqueue, and return quickly.
Do not run long reconciliation jobs inside the webhook request.
Idempotency and duplicate callbacks
Webhook retries are normal.
Network failures happen.
Your endpoint may receive the same event more than once.
Your reconciliation tool must be idempotent.
Use a unique constraint on a stable event hash:
async function saveWebhookEvent(event) {
try {
return await db.webhookEvents.create({ data: event });
} catch (err) {
if (isUniqueViolation(err)) {
return await db.webhookEvents.findUnique({
where: {
merchantId_payloadHash: {
merchantId: event.merchantId,
payloadHash: event.payloadHash
}
}
});
}
throw err;
}
}
Also make payment state transitions idempotent.
If the local session is already paid_confirmed, another paid webhook should not trigger fulfillment twice.
async function applyPaymentStatus(session, providerStatus) {
const nextInternalStatus = mapProviderStatus(providerStatus);
if (session.internalStatus === nextInternalStatus) {
return { changed: false, status: session.internalStatus };
}
if (session.internalStatus === "paid_confirmed") {
return { changed: false, status: session.internalStatus };
}
await db.paymentSessions.update({
where: { id: session.id },
data: {
providerStatus,
internalStatus: nextInternalStatus,
updatedAt: new Date()
}
});
return { changed: true, status: nextInternalStatus };
}
This is not just a code quality issue.
It protects the merchant from duplicate fulfillment.
Backfill with Payment History
A serious reconciliation tool should not trust webhooks as the only source of truth.
Webhooks can fail.
Servers can be down.
Firewalls can block callbacks.
A bug can process an event incorrectly.
That is why you also need periodic provider-side backfill.
OxaPay’s Payment History endpoint supports date filtering, status filtering, payment type filtering, pagination, sorting, and amount filtering.
A scheduled job can pull recent records and compare them with local state.
async function fetchOxaPayPaymentHistory({ merchantApiKey, fromDate, toDate, page = 1 }) {
const params = new URLSearchParams({
from_date: String(fromDate),
to_date: String(toDate),
size: "200",
page: String(page),
sort_by: "pay_date",
sort_type: "desc"
});
const response = await fetch(`https://api.oxapay.com/v1/payment?${params}`, {
method: "GET",
headers: {
"merchant_api_key": merchantApiKey,
"Content-Type": "application/json"
}
});
if (!response.ok) {
throw new Error(`OxaPay history request failed: ${response.status}`);
}
return response.json();
}
A simple backfill strategy:
Every 15 minutes:
Pull payments from the last 2 hours.
Upsert payment sessions by track_id.
Compare provider status with local status.
Create case if provider is paid but local order is unpaid.
Every 24 hours:
Pull payments from the previous day.
Compare total paid amount with merchant order totals.
Generate daily reconciliation report.
This is where your tool becomes more robust than a basic webhook integration.
Payment Information lookup for support
The Payment Information endpoint is useful when support needs to investigate one specific payment.
For example, a customer writes:
I paid, but my order is still pending.
The support agent can search by order ID, customer email, track ID, or transaction hash.
If the tool has the OxaPay track_id, it can fetch the latest payment details.
async function fetchPaymentInformation({ merchantApiKey, trackId }) {
const response = await fetch(`https://api.oxapay.com/v1/payment/${trackId}`, {
method: "GET",
headers: {
"merchant_api_key": merchantApiKey,
"Content-Type": "application/json"
}
});
if (!response.ok) {
throw new Error(`Payment lookup failed: ${response.status}`);
}
return response.json();
}
The result can update the support timeline:
Order created
Invoice generated
Customer selected currency
Payment status changed to paying
Transaction confirmation detected
Payment status changed to paid
Fulfillment job failed
Support case opened
This is the kind of view support teams actually need.
Matching logic
Reconciliation is mostly matching.
You are matching provider-side records to merchant-side records.
The strongest matching key is order_id.
When creating invoices, always send the merchant’s internal order ID as order_id. This gives your reconciliation engine a clean join key.
But real systems need fallback logic.
Recommended matching priority:
1. Exact provider track_id already linked to order
2. Exact order_id from provider payload
3. Exact external_order_id stored in local metadata
4. Customer email + amount + narrow time window
5. Static address track_id assigned to customer account
6. Transaction hash manually attached by support
7. Manual review
Do not over-automate weak matches.
If a match is not strong enough, create a reconciliation case instead of silently marking an order paid.
Example matching function:
async function findMatchingOrder({ merchantId, payment }) {
if (payment.order_id) {
const order = await db.merchantOrders.findUnique({
where: {
merchantId_externalOrderId: {
merchantId,
externalOrderId: payment.order_id
}
}
});
if (order) return { order, confidence: "high", reason: "order_id" };
}
if (payment.email && payment.amount && payment.currency) {
const candidates = await db.merchantOrders.findMany({
where: {
merchantId,
customerEmail: payment.email,
expectedAmount: payment.amount,
expectedCurrency: payment.currency,
createdAt: {
gte: new Date(Date.now() - 3 * 60 * 60 * 1000)
}
}
});
if (candidates.length === 1) {
return { order: candidates[0], confidence: "medium", reason: "email_amount_time" };
}
}
return { order: null, confidence: "none", reason: "no_safe_match" };
}
Weak matching should require human approval.
That is a product feature, not a limitation.
Reconciliation cases
The product should create clear, actionable cases.
Here are useful case types.
1. Paid but not fulfilled
Provider says payment is paid, but merchant order has not been fulfilled.
Recommended action:
Check fulfillment job logs. If payment is confirmed and order is valid, trigger fulfillment manually or re-run automation.
2. Fulfilled but not paid
Merchant system says the order was fulfilled, but provider status is not paid.
Recommended action:
Review automation logs. Possible duplicate fulfillment, manual override, or incorrect order state.
3. Underpaid payment
Provider status indicates underpayment or the received amount is below expected amount.
Recommended action:
Ask customer to complete remaining payment, manually accept if policy allows, or cancel order.
4. Expired invoice with customer claim
Invoice expired, but the customer says they paid.
Recommended action:
Look up payment information by track_id, address, tx_hash, amount, and time window.
5. Unknown payment
A payment exists in provider history, but no local order matches it.
Recommended action:
Review order_id, email, amount, static address assignment, and support tickets.
6. Duplicate webhook
Same event was received multiple times.
Recommended action:
No merchant action needed if idempotency prevented duplicate fulfillment.
7. Invalid webhook signature
Webhook failed HMAC validation.
Recommended action:
Do not process payment state. Investigate source, endpoint exposure, and secret configuration.
8. Status mismatch
Provider and local database disagree.
Recommended action:
Refresh payment information by track_id and update local state only if provider response is verified.
These cases are the product.
They turn raw payment data into operational work.
Reconciliation engine example
Here is a simplified reconciliation worker.
async function reconcilePayment({ merchantId, providerTrackId }) {
const session = await db.paymentSessions.findUnique({
where: {
merchantId_provider_providerTrackId: {
merchantId,
provider: "oxapay",
providerTrackId
}
},
include: { order: true }
});
if (!session) {
await createCase({
merchantId,
caseType: "payment_session_missing",
severity: "high",
summary: `No local payment session found for track_id ${providerTrackId}`,
recommendedAction: "Fetch payment information from provider and attempt safe matching."
});
return;
}
const order = session.order;
if (session.internalStatus === "paid_confirmed" && !order) {
await createCase({
merchantId,
paymentSessionId: session.id,
caseType: "paid_payment_without_order",
severity: "high",
summary: "Payment is confirmed but no local order is linked.",
recommendedAction: "Review order_id, customer email, amount, and time window."
});
return;
}
if (session.internalStatus === "paid_confirmed" && order.fulfillmentStatus !== "fulfilled") {
await createCase({
merchantId,
orderId: order.id,
paymentSessionId: session.id,
caseType: "paid_not_fulfilled",
severity: "high",
summary: `Order ${order.externalOrderId} is paid but not fulfilled.`,
recommendedAction: "Re-run fulfillment job or review fulfillment logs."
});
}
if (session.internalStatus === "needs_review_underpaid") {
await createCase({
merchantId,
orderId: order?.id,
paymentSessionId: session.id,
caseType: "underpaid_payment",
severity: "medium",
summary: "Payment appears underpaid and needs merchant review.",
recommendedAction: "Apply merchant policy: request remaining amount, manually accept, or cancel."
});
}
if (order?.fulfillmentStatus === "fulfilled" && session.internalStatus !== "paid_confirmed") {
await createCase({
merchantId,
orderId: order.id,
paymentSessionId: session.id,
caseType: "fulfilled_not_paid",
severity: "critical",
summary: `Order ${order.externalOrderId} was fulfilled before confirmed payment.`,
recommendedAction: "Review automation logic and prevent future fulfillment before paid status."
});
}
}
In production, each case type should have deduplication rules.
You do not want to create the same case every time a job runs.
Use a unique case key such as:
merchant_id + case_type + order_id + payment_session_id + open_status
The dashboard
The dashboard should be built for operations, not developers.
A merchant support agent should understand the screen in seconds.
Useful dashboard sections:
Overview
Total paid today
Orders paid but not fulfilled
Open reconciliation cases
Underpaid payments
Expired invoices with activity
Webhook failures
Daily payment total
Provider/local mismatch count
Exception queue
Columns:
Severity
Case type
Order ID
Track ID
Customer
Amount
Currency
Provider status
Order status
Fulfillment status
Age
Assigned agent
Recommended action
Payment timeline
For each order:
Order created
Invoice generated
Webhook received: waiting
Webhook received: paying
Provider lookup: paid
Fulfillment job started
Fulfillment completed
Support note added
Case resolved
Finance export
Export fields:
Date
Order ID
Track ID
Payment type
Payment status
Order status
Fulfillment status
Requested amount
Paid amount
Currency
Network
Transaction hash
Customer email
Resolution status
Support note
Webhook health
Show:
received events
valid HMAC count
invalid HMAC count
duplicate events
processing failures
last webhook timestamp
retry-sensitive endpoints
This is especially valuable because many merchants only notice webhook problems when customers complain.
Alerts
Do not send alerts for everything.
Alert fatigue kills operational tools.
Good alerts:
Paid order not fulfilled for more than 5 minutes
Invalid webhook signature received
Provider shows paid but local order is unpaid
High number of expired invoices in last hour
Backfill found paid payment missing locally
Underpaid payment above threshold
Webhook processing queue delayed
Bad alerts:
Every invoice created
Every waiting payment
Every duplicate webhook
Every normal paid payment
A reconciliation tool should reduce noise, not create more.
Static address reconciliation
Static addresses create a different reconciliation problem.
With invoice payments, each payment session usually has a specific expected amount and order ID.
With static addresses, the same address may be associated with a customer, account, or deposit flow. Payments can arrive at different times and amounts.
OxaPay’s static address endpoint links an address to a track_id and supports callback notifications when payments are made to that address.
For reconciliation, static address flows need extra rules:
Assign static address to customer or account
Store track_id and address mapping
Ingest payment callback
Match payment to account balance or deposit record
Detect unknown amount
Detect duplicate tx_hash
Detect stale address assignment
Handle revoked/inactive addresses
Static address reconciliation is useful for:
- account top-ups
- wallet-like merchant balances
- customer deposit accounts
- recurring customer deposits
- B2B clients with repeated payments
But it needs stricter tracking.
Do not use static addresses as a shortcut if the merchant really needs order-level invoice reconciliation.
Revenue model for developers
This can be a real business, but the monetization depends on positioning.
Do not sell it as “I built a crypto dashboard.”
Sell it as:
I reduce payment support problems, fulfillment mistakes, and finance reporting gaps for merchants accepting crypto payments.
Possible revenue models:
1. Setup fee
A one-time fee to connect the merchant’s store, OxaPay account, webhook endpoint, order database, and dashboard.
Best for freelancers and agencies.
2. Monthly monitoring retainer
The developer monitors webhook health, open cases, daily mismatch reports, and operational alerts.
Best for service businesses.
3. SaaS subscription
The developer sells the tool as a self-serve or semi-managed product.
Best if the product is focused on a niche, such as WooCommerce digital stores, hosting providers, or paid communities.
4. Finance/reporting add-on
The developer charges extra for CSV exports, daily reports, monthly summaries, and custom accounting formats.
Best for merchants with operations or finance teams.
5. Support operations package
The developer sells support workflows, case queues, customer response templates, and payment investigation tools.
Best for merchants with many customer tickets.
Revenue should be framed carefully.
There is no guaranteed income.
But the value can be strong because the product touches real merchant pain: lost orders, confused customers, delayed fulfillment, support load, and finance mismatch.
Pricing examples
These are not promises. They are product packaging examples.
Starter package:
- One merchant integration
- Webhook receiver
- Basic payment/order matching
- Daily CSV export
- Manual support
Possible pricing: setup fee + small monthly retainer
Operations package:
- Everything in Starter
- Exception queue
- Payment timeline
- Backfill jobs
- Alerts
- Support notes
Possible pricing: higher setup fee + monthly monitoring
Advanced package:
- Multi-store support
- Static address reconciliation
- Custom fulfillment checks
- Finance exports
- Role-based dashboard
- SLA monitoring
Possible pricing: premium setup + recurring subscription/retainer
If you are a solo developer, start with a service package before building full SaaS.
You will learn the real edge cases faster.
Technical risks
A reconciliation product deals with financial operations.
Be careful.
Do not process unsigned callbacks
Store invalid callbacks for audit, but do not update payment state from them.
Do not trigger fulfillment twice
Every fulfillment action needs idempotency.
Do not treat paying as final
Wait for confirmed paid state before fulfillment.
Do not hide underpayments
Underpayments should be visible to support or finance.
Do not rely only on webhooks
Use scheduled backfill from provider history.
Do not overwrite manual decisions silently
If a merchant manually accepts or resolves a case, preserve the audit trail.
Do not expose API keys
Encrypt stored API keys. Use strict access control. Avoid logging secrets.
Do not become the merchant of record accidentally
If you are building software for merchants, make it clear that the merchant owns the payment relationship, customer relationship, tax obligations, refund policy, and compliance responsibilities.
This is especially important if your tool touches payouts, refunds, account balances, or multi-party flows.
Security checklist
A production-grade reconciliation tool should include:
[ ] HMAC validation for every webhook
[ ] Raw body preservation for signature verification
[ ] Timing-safe signature comparison
[ ] Encrypted API key storage
[ ] No secrets in logs
[ ] Idempotent webhook processing
[ ] Unique transaction hash constraints
[ ] Role-based dashboard access
[ ] Audit trail for manual resolution
[ ] Provider/local state mismatch alerts
[ ] Backfill jobs for missed webhooks
[ ] Queue-based processing
[ ] Dead-letter queue for failed jobs
[ ] Secure admin actions
[ ] Export access logging
This checklist can become part of your paid offering.
Merchants do not only pay for code. They pay for operational safety.
Build plan: 21 days
Here is a realistic build plan for a first usable version.
Days 1-3: Merchant and order model
Build:
- merchant tenant model
- encrypted API key storage
- local order import
- external order ID mapping
- basic dashboard shell
Days 4-6: Invoice and payment session layer
Build:
- invoice creation wrapper
- storage of track ID
- order-to-payment link
- payment session table
- basic payment status page
Days 7-9: Webhook ingestion
Build:
- webhook endpoint
- raw body capture
- HMAC validation
- webhook event log
- async processing queue
- idempotency logic
Days 10-12: Reconciliation engine
Build:
- provider status mapping
- paid-not-fulfilled detection
- fulfilled-not-paid detection
- underpaid case detection
- expired invoice detection
- duplicate webhook detection
Days 13-15: Backfill jobs
Build:
- Payment History pull
- recent window sync
- daily reconciliation report
- provider/local mismatch detection
Days 16-18: Dashboard and case queue
Build:
- exception queue
- severity labels
- case assignment
- resolution notes
- payment timeline
- CSV export
Days 19-21: Alerts and production hardening
Build:
- email/Telegram/Slack alerts
- retry handling
- dead-letter queue
- admin audit log
- security review
- onboarding checklist
At the end of this sprint, you do not have a perfect SaaS.
You have a productized tool that can solve a real merchant problem.
The best niche to start with
Do not start too broad.
A reconciliation tool for “all crypto merchants” is hard to sell.
Start with one niche.
Good first niches:
Digital product stores
They need automatic delivery and support investigation.
Hosting or VPN sellers
They need plan activation, renewal checks, and payment status clarity.
Course platforms
They need access control and finance reporting.
Telegram paid communities
They need membership state, expiry, and support lookup.
Agencies managing merchant clients
They need one dashboard across multiple stores.
The narrower the niche, the easier it is to write copy, design workflows, and price the product.
What makes this more than a dashboard?
A dashboard shows data.
A reconciliation product closes operational gaps.
The difference is action.
Weak product:
Here are your payments.
Strong product:
These 7 paid orders were not fulfilled.
These 3 invoices are underpaid.
These 2 payments exist in provider history but not in your store.
This webhook endpoint failed 4 times.
Here is the recommended action for each case.
That is what merchants will pay for.
Advanced features
Once the MVP works, useful advanced features include:
- multi-merchant agency dashboard
- per-niche reconciliation rules
- customer-facing payment status page
- Slack/Telegram alerting
- accounting exports
- static address deposit reconciliation
- payout reconciliation
- automatic support response drafts
- anomaly detection
- monthly finance summary
- webhook health score
- API integration with helpdesk tools
- role-based approval for manual resolution
Do not build these first.
Use them to upsell after the core reconciliation workflow proves value.
How this becomes a developer business
This is the important part.
A developer does not need to compete with payment gateways.
The developer builds the layer around the gateway.
OxaPay handles the crypto payment infrastructure.
The developer builds the merchant-specific operational system:
- order matching
- case management
- support visibility
- fulfillment checks
- reporting
- alerts
- audit trail
- niche-specific workflows
That is where customization lives.
That is where merchants need help.
That is where a developer can charge.
Final takeaway
Crypto payment reconciliation is one of the strongest product opportunities for developers building around payment infrastructure.
It is not the flashiest idea.
It is not as obvious as building a checkout page.
But it solves a real operational problem.
When payment volume grows, merchants need more than invoice links and callbacks. They need to know whether every payment, order, fulfillment action, support case, and finance report agrees.
That is the product.
If you build it well, you are not just integrating OxaPay.
You are building a merchant payment operations layer on top of it.
And that is a much stronger business than selling one-off API setup.
Top comments (0)