Engineering a Non-Custodial Bitcoin Paywall for an AI Assistant
We recently completed the first real Bitcoin payment for Zorgax, the AI assistant inside the MyZubster ecosystem.
The goal was not simply to display a wallet address. We wanted a complete production flow that could connect a verified on-chain payment to an authenticated account and automatically activate paid features.
The final lifecycle looks like this:
Authentication
↓
Payment intent
↓
External Bitcoin wallet
↓
Blockchain transaction
↓
Independent verification
↓
Entitlement activation
↓
Backend feature access
Zorgax access levels
Zorgax currently provides three plans:
Free
- Basic AI assistant
- Limited web research
- Maximum 2 sources
Pro — €9.90 monthly equivalent
- Advanced web research
- Maximum 5 sources
- Zorgax workspace
Developer — €29.90 monthly equivalent
- Everything in Pro
- Maximum 8 sources
- Direct API access
- Automation features
The current Bitcoin flow creates time-bounded access. It is not an automatic recurring wallet subscription.
Why a wallet address is not enough
A public Bitcoin address can receive payments, but it cannot answer several application-level questions:
- Which account made the payment?
- Which plan was selected?
- What BTC amount was expected?
- When was the exchange-rate quote generated?
- Has this transaction already been used?
- When should the entitlement expire?
For this reason, Zorgax creates a persistent payment intent before displaying the payment instructions.
{
"intentId": "payment-intent-id",
"ownerId": "authenticated-user",
"plan": "pro",
"asset": "BTC",
"destination": "bitcoin-address",
"expectedAmountSats": 14728,
"status": "PENDING",
"expiresAt": "quote-expiration"
}
The exact amount varies according to the quote generated for the order.
Creating the checkout
The authenticated frontend requests a new payment intent:
const response = await fetch(
"/api/zorgax/assistant/checkout/intent",
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
plan: "pro",
asset: "BTC"
})
}
);
The backend associates the intent with the current user and returns:
- The BTC destination
- The exact amount
- The expiration time
- The payment-intent identifier
The user then pays from an external wallet.
MyZubster never requests the private key and never signs or broadcasts the transaction.
Verifying the transaction
After broadcasting the payment, the user receives a Bitcoin transaction ID.
The TXID is submitted to the verification endpoint:
await fetch(
`/api/zorgax/assistant/checkout/intent/${intentId}/verify`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
Authorization: `Bearer ${token}`
},
body: JSON.stringify({
paymentReference: txid
})
}
);
The server then retrieves the public transaction and checks:
Transaction exists
↓
Correct destination
↓
Sufficient amount
↓
Required confirmations
↓
Intent belongs to the user
↓
Payment has not already been consumed
A simplified verifier looks like this:
const transaction = await getBitcoinTransaction(txid);
const output = transaction.outputs.find(
item => item.address === intent.destination
);
if (!output) {
throw new Error("Unexpected payment destination");
}
if (output.value < intent.expectedAmountSats) {
throw new Error("Insufficient payment amount");
}
if (transaction.confirmations < minimumConfirmations) {
throw new Error("Insufficient blockchain confirmations");
}
Why confirmations matter
During the first live production test, Zorgax detected the transaction while it was still in the mempool.
The application returned:
Payment not activated:
Insufficient blockchain confirmations
This was expected.
Seeing a transaction in the mempool does not mean the system should immediately grant paid access. Zorgax waited until the payment entered a Bitcoin block.
After the required confirmation, the same TXID was verified again:
Payment verified
Access ACTIVE
Plan Pro
No second payment was required.
Activating the entitlement
After successful verification, Zorgax creates or updates the account’s entitlement:
{
"plan": "pro",
"tier": "PRO",
"status": "ACTIVE",
"source": "BITCOIN_PAYMENT",
"startsAt": "activation-date",
"expiresAt": "expiration-date"
}
Every protected request evaluates this access state before running the requested operation.
Enforcing access on the backend
Hiding a button in the frontend is not security.
The backend applies an access policy to every Zorgax request:
const policies = {
guest: {
chat: true,
webResearch: false,
workspace: false,
directApi: false
},
free: {
chat: true,
webResearch: true,
maxWebResults: 2,
workspace: false,
directApi: false
},
pro: {
chat: true,
webResearch: true,
maxWebResults: 5,
workspace: true,
directApi: false
},
developer: {
chat: true,
webResearch: true,
maxWebResults: 8,
workspace: true,
directApi: true
}
};
For example, direct research requires the Developer tier:
router.get(
"/research",
authenticate,
requireZorgaxPlan("developer"),
researchHandler
);
Workspace persistence requires Pro or higher:
router.post(
"/data/commit",
authenticate,
requireZorgaxPlan("pro"),
commitHandler
);
Guest access without silent authentication failures
Zorgax supports guest chat, but an invalid token must never silently become a guest session.
The authentication boundary is:
No token
→ Guest access
Valid token
→ Authenticated access
Invalid token
→ 401 Unauthorized
This distinction prevents expired or manipulated credentials from bypassing protected logic.
Unifying legacy subscriptions and new entitlements
MyZubster already had legacy subscription records. The new payment implementation introduced a more explicit entitlement model.
We created one access service that evaluates both:
Legacy subscriptions ─┐
├── Unified Zorgax access
New entitlements ─────┘
The service selects the highest active plan and returns a single feature policy.
This allows existing accounts to remain compatible while new Bitcoin payments use the improved lifecycle.
A Vercel routing problem
The application also contains a generic Zorgax AI gateway route.
A broad rule such as:
/api/zorgax/*
could intercept requests intended for the monetization backend.
The specific monetization route now takes priority:
{
"source": "/api/zorgax/monetization/(.*)",
"destination": "/api/index.js"
}
Only afterward does the generic AI proxy apply.
This ensures that purchases and entitlements are handled by the backend containing the correct authentication middleware and database connection.
Confirming persistent AI writes
Zorgax can prepare structured data, but it does not silently persist AI-generated content.
The workspace flow requires:
- A generated preview.
- A cryptographic digest of that preview.
- An explicit user confirmation.
- Digest validation before persistence.
const expectedDigest = digestPreview(preview);
if (expectedDigest !== submittedDigest) {
throw new Error("Preview changed");
}
This creates a clear boundary between AI suggestions and authorized writes.
Production verification
Before deployment, the Zorgax test family completed:
58 test suites
206 tests passed
Production checks confirmed that:
Public pricing → 200
Guest chat → 200
Guest web research → disabled
Protected monetization routes → 401 without login
Developer research API → 401 without login
Checkout → 401 without login
The real Bitcoin transaction then proved the final missing part: verified payment-to-entitlement activation.
Security boundaries
The production design follows these rules:
- No private keys are accepted.
- MyZubster does not sign transactions.
- Users pay from external wallets.
- The destination and amount are verified.
- Blockchain confirmations are required.
- Payment intents belong to authenticated users.
- Paid features are enforced server-side.
- Persistent writes require explicit confirmation.
- Invalid tokens never become anonymous sessions.
What comes next
The current system is operational but intentionally conservative.
Possible next improvements include:
- Renewal notifications
- Payment-history dashboards
- Downloadable receipts
- Additional independent Bitcoin providers
- More supported settlement assets
- Entitlement-expiration reminders
- Better transaction monitoring
Every new asset will require its own trusted quote source, verification logic and confirmation policy.
Final result
Zorgax monetization is no longer only a pricing page.
A real production payment successfully completed this sequence:
BTC payment
→ blockchain confirmation
→ independent verification
→ entitlement activation
→ Pro features enabled
The result was:
Access: ACTIVE
Plan: Pro
Project links
Zorgax:
https://www.myzubster.com/zorgax
MyZubster source code:
https://github.com/MyZubster-Ecosystem/myzubster
Production monetization implementation:
https://github.com/MyZubster-Ecosystem/myzubster/commit/d7a579cd89801d29ac094a19af42fb17350691ef