DEV Community

Daniel Ioni
Daniel Ioni

Posted on

title: "Improving Zorgax Bitcoin Payments: Automatic Verification, Renewals and Receipts"

Improving Zorgax Bitcoin Payments: Automatic Verification, Renewals and Receipts

Zorgax recently completed its first real Bitcoin payment and successfully activated a Pro account in production.

The original flow already supported:

Authentication
→ Payment intent
→ External Bitcoin wallet
→ TXID submission
→ Blockchain verification
→ Pro activation
Enter fullscreen mode Exit fullscreen mode

The payment worked, but the live test revealed several usability improvements we needed to make.

When the transaction was initially unconfirmed, the user had to wait and manually press “Verify payment” again.

We have now developed the next version of the Zorgax monetization lifecycle.

At the time of writing, these improvements are implemented and tested locally. Publication to main and production deployment are the next steps.

What we added

The new implementation introduces:

  • Persistent transaction IDs
  • Automatic confirmation checks
  • Payment recovery after reopening Zorgax
  • Subscription renewal stacking
  • Authenticated payment history
  • Downloadable technical receipts
  • Improved replay protection
  • Safer concurrent verification

Persisting unconfirmed transactions

Previously, the payment reference was only saved after successful verification.

This meant that when the blockchain returned:

Insufficient blockchain confirmations
Enter fullscreen mode Exit fullscreen mode

the server could not automatically retry because the TXID had not been persisted.

The new flow binds the TXID to its payment intent as soon as the authenticated user submits it.

intent.settlement.paymentReference = txid;
intent.settlement.submittedAt = new Date();
intent.settlement.nextCheckAt = new Date();
Enter fullscreen mode Exit fullscreen mode

The payment remains in the PENDING state until the required confirmation is available.

A simplified tracking state looks like this:

{
  "status": "PENDING",
  "paymentReference": "bitcoin-transaction-id",
  "submittedAt": "submission-date",
  "lastCheckedAt": "last-verification-date",
  "nextCheckAt": "next-verification-date",
  "checkAttempts": 2,
  "lastError": "Insufficient blockchain confirmations"
}
Enter fullscreen mode Exit fullscreen mode

Automatic confirmation monitoring

After the TXID is registered, the Zorgax interface checks its status automatically every 15 seconds.

monitorTimer = setInterval(
  pollPaymentIntent,
  15000
);
Enter fullscreen mode Exit fullscreen mode

The browser calls a protected refresh endpoint:

POST /api/zorgax/assistant/checkout/intent/:intentId/refresh
Enter fullscreen mode Exit fullscreen mode

The endpoint never accepts payment coordinates from the client. It reloads the original intent from MongoDB and uses its persisted values:

await verifySettlement({
  asset: intent.asset,
  paymentReference: intent.settlement.paymentReference,
  destination: intent.destination,
  cryptoAmount: intent.quote.cryptoAmount
});
Enter fullscreen mode Exit fullscreen mode

When confirmation becomes available, the backend activates the subscription and the interface updates automatically.

Payment verified automatically
Access ACTIVE
Plan Pro
Enter fullscreen mode Exit fullscreen mode

The user no longer needs to keep pressing the verification button.

Recovering a pending payment

Payment tracking is also restored when an authenticated user returns to Zorgax.

The frontend loads the payment history and searches for a pending intent containing a submitted TXID:

const pending = intents.find(
  intent =>
    intent.settlementStatus === "PENDING" &&
    intent.paymentReference
);
Enter fullscreen mode Exit fullscreen mode

If one is found, Zorgax:

  1. Restores the payment card.
  2. Displays the original amount and destination.
  3. Restores the submitted TXID.
  4. Restarts automatic confirmation monitoring.

This makes the process resilient to closing or reopening the page.

Handling quote expiration correctly

Payment quotes have an expiration time because the BTC/EUR exchange rate can change.

However, a valid transaction may be submitted before the quote expires and receive its first confirmation afterward.

The updated logic distinguishes between:

TXID submitted after quote expiration
→ Reject the payment intent as expired

TXID submitted before quote expiration
→ Continue monitoring after expiration
Enter fullscreen mode Exit fullscreen mode

Conceptually:

function submissionWasInTime(intent) {
  return (
    intent.settlement.submittedAt &&
    intent.settlement.submittedAt <= intent.expiresAt
  );
}
Enter fullscreen mode Exit fullscreen mode

A user who paid in time is therefore not penalized because a Bitcoin block arrived after the quote expired.

Subscription renewal stacking

Zorgax paid access currently lasts 30 days.

The new checkout can detect an active subscription owned by the authenticated account.

const activeSubscription =
  await ZorgaxSubscription.findOne({
    ownerId,
    plan,
    "access.status": "ACTIVE",
    "access.expiresAt": {
      $gt: new Date()
    }
  });
Enter fullscreen mode Exit fullscreen mode

When the payment represents a renewal, the new access period begins at the current subscription’s expiration time.

Current Pro access
31 August → 30 September

Renewal
30 September → 30 October
Enter fullscreen mode Exit fullscreen mode

The remaining paid time is not discarded.

The backend resolves the active subscription itself. The frontend never sends an arbitrary subscription identifier to the verification endpoint.

Authenticated payment history

Zorgax now provides an owner-scoped payment-history endpoint:

GET /api/zorgax/assistant/checkout/history
Enter fullscreen mode Exit fullscreen mode

The API only returns payment intents belonging to the authenticated user.

The public history representation includes:

  • Selected plan
  • Payment asset
  • Quoted amount
  • Settlement status
  • TXID submission time
  • Confirmation count
  • Verification attempts
  • Renewal status
  • Receipt availability

Internal ownership information is not included in the client response.

Downloadable payment receipts

Verified payments can now generate a technical JSON receipt.

GET /api/zorgax/assistant/checkout/intent/:intentId/receipt
Enter fullscreen mode Exit fullscreen mode

The receipt is created exclusively from verified server-side records:

{
  "documentType": "PAYMENT_RECEIPT",
  "fiscalInvoice": false,
  "entity": "ZORGAX-001",
  "plan": "pro",
  "payment": {
    "asset": "BTC",
    "cryptoAmount": "verified-amount",
    "paymentReference": "verified-txid",
    "confirmations": 1,
    "verifier": "blockstream-esplora"
  },
  "access": {
    "status": "ACTIVE",
    "startsAt": "activation-date",
    "expiresAt": "expiration-date"
  }
}
Enter fullscreen mode Exit fullscreen mode

The document is explicitly identified as a technical payment receipt, not a fiscal invoice.

Users can download it directly from the Zorgax payment-history panel.

Replay and concurrency protection

A Bitcoin transaction must never activate multiple subscriptions.

Zorgax already used a unique payment-reference index. The updated flow also handles concurrent retries safely.

If two verification requests for the same intent complete almost simultaneously:

  1. Both use the same persisted payment intent.
  2. The payment reference remains unique.
  3. Only one subscription is created.
  4. A repeated request by the same owner returns the existing activation.
  5. A different owner can never reuse the transaction.
const existing =
  await ZorgaxSubscription.findOne({
    paymentReference
  });

if (existing) {
  if (existing.ownerId !== ownerId) {
    throw new Error("Payment already used");
  }

  return existing;
}
Enter fullscreen mode Exit fullscreen mode

This makes automatic polling idempotent.

Retryable and fatal verification failures

Not every verification error should be treated in the same way.

Retryable conditions include:

Insufficient confirmations
Transaction not found yet
Temporary verifier unavailability
Enter fullscreen mode Exit fullscreen mode

These keep the intent in the PENDING state.

Fatal validation problems include:

Wrong destination
Insufficient amount
Invalid transaction reference
Enter fullscreen mode Exit fullscreen mode

These remove the rejected reference from the intent so the user can submit the correct TXID without creating a second order.

Security boundaries

The new version preserves the original non-custodial design:

  • MyZubster never requests private keys.
  • Zorgax never signs transactions.
  • Funds are sent through external wallets.
  • Checkout requires authentication.
  • The expected destination comes from the persisted intent.
  • The expected amount comes from the persisted quote.
  • Blockchain confirmations are required.
  • Payment history is owner-scoped.
  • Receipts are generated only for verified payments.
  • Renewal identifiers are resolved by the backend.
  • Paid features remain enforced server-side.

Testing

The updated implementation currently has:

61 Zorgax test suites passed
191 tests passed
Enter fullscreen mode Exit fullscreen mode

The tests cover:

  • TXID persistence
  • Automatic retry state
  • Activation after confirmation
  • Confirmation after quote expiration
  • Renewal resolution
  • Payment-history privacy
  • Receipt generation
  • Concurrent replay handling
  • Cross-owner replay rejection
  • UI payment recovery
  • Protected billing endpoints

Four existing MongoDB integration suites remain blocked in the local environment by a mongodb-memory-server driver-handshake incompatibility. The failure occurs while starting the temporary database, before application logic is executed.

All non-MongoDB Zorgax suites and the new payment tests pass.

The improved payment lifecycle

The new complete flow is:

Authenticated checkout
        ↓
Persistent payment intent
        ↓
External BTC payment
        ↓
TXID persisted
        ↓
Automatic confirmation checks
        ↓
Independent blockchain verification
        ↓
Idempotent entitlement activation
        ↓
Payment history and receipt
        ↓
Optional stacked renewal
Enter fullscreen mode Exit fullscreen mode

What remains

These improvements do not introduce automatic recurring withdrawals.

The user still explicitly creates and pays every renewal through an external wallet.

Future additions may include:

  • Always-on server-side transaction monitoring
  • Expiration notifications
  • Email renewal reminders
  • Administrative revenue analytics
  • Fiscal invoice integrations
  • Card-payment providers
  • Additional settlement assets

Each external payment provider will require its own credentials, verification rules and security review.

Project links

Try the current production version of Zorgax:

https://www.myzubster.com/zorgax

Explore MyZubster:

https://github.com/MyZubster-Ecosystem/myzubster

View the production foundation for paid access:

https://github.com/MyZubster-Ecosystem/myzubster/commit/d7a579cd89801d29ac094a19af42fb17350691ef

Top comments (0)