Crypto-native licensing

The license is the wallet.

Sell a browser extension, plugin or desktop app for crypto without an account system. The buyer pays from their wallet, and that wallet is the licence β€” they unlock any other device by signing a free message. No email, no licence keys to lose, no chargebacks.

Why this exists

Chrome Web Store payments shut down in February 2021, so every extension developer needs an external payment layer. The leading option requires a Stripe account, which excludes developers in Turkey, Pakistan, Nigeria, Bangladesh, Indonesia, Egypt and many other countries outright.

Crypto payment gateways stop at "payment received". They hand you a webhook, not a licence: no entitlement store, no SDK, no cross-device verification. You end up building the hard half yourself. This kit is that half.

Comparison

                        ExtensionPay   Paddle / LS   Crypto gateways   XPayr Kit
fee                     5% + Stripe    5% + 50c      0.5-1%            2.9% flat
works without Stripe    no             partly        yes               yes
licensing layer         yes            partial       no                yes
chargebacks             yes            yes           none              none
merchant of record      no             yes           no                no

We are not a merchant of record: you file your own taxes. That is what Paddle and Lemon Squeezy's higher fee buys you, and it is a real trade-off worth understanding before you choose.

Quickstart

Create a product in your merchant dashboard to get a product code, then wire the SDK. No host_permissions are needed, so installing your extension shows no data-access warning.

Install

npm install @xpayr/extension-kit

# or copy src/index.js into your extension β€”
# zero dependencies, no build step, MV3-ready

manifest.json

{
  "manifest_version": 3,
  "permissions": ["storage", "alarms"],
  "externally_connectable": { "matches": ["https://xpayr.com/*"] }
}

background.js

import XPayrKit from '@xpayr/extension-kit';

const xpayr = XPayrKit('prod_my_extension');
xpayr.startBackground();          // required: token delivery + daily refresh

xpayr.onLicenseChanged.addListener(user => {
  console.log(user.licensed ? 'unlocked' : 'locked');
});

Gating a feature

const user = await xpayr.getUser();

if (user.licensed) {
  enableProFeatures();
} else {
  showUpgradeButton(() => xpayr.openCheckout());
}

That is the whole integration. The buyer flow β€” pick a network, pay, sign, unlock β€” is hosted by us at xpayr.com/buy/{product_code}.

The user object

Field Type Meaning
licensed boolean Gate your features on this. Answers from a locally verified token, so it is instant and works offline.
plan 'lifetime' | 'prepaid_year' | null The plan the buyer purchased.
wallet string | null The wallet the license is bound to β€” the one that paid.
paidAt Date | null When the license was issued.
expiresAt Date | null End date for time-limited plans; null for lifetime.
source 'cache' | 'network' Where this answer came from.
staleSince Date | null Set while running on an aged token inside the 30-day offline grace window.

API

Method Description
startBackground() Call once in your service worker. Wires token delivery and the daily refresh alarm.
getUser() Offline-first license check. Verifies the cached ES256 token locally β€” no network round trip on the hot path.
openCheckout() Opens the hosted checkout in a new tab. The buyer picks any network and token you have enabled.
openRestore() "I already bought this" β€” unlocks another device with a wallet signature. No email, no license key.
refresh() Forces online revalidation. Picks up refunds and revocations immediately.
getPlans() Public product info: price, plan type, allowed networks.
onLicenseChanged Fires whenever the license state changes.

How verification works

  1. The buyer pays from their wallet on the hosted checkout.
  2. XPayr records the entitlement against the wallet address that paid.
  3. The buyer signs a free, single-use message proving they control that wallet. No funds move.
  4. XPayr returns an ES256-signed token, scoped to your product and valid for 7 days.
  5. Your extension verifies that token locally with WebCrypto on every getUser(), and silently renews it once a day.

Offline for a while? The licence keeps working for 30 days past the token's expiry, then soft-locks with a prompt to reconnect β€” nobody gets locked out mid-flight. Refunded a customer? Revoke in the dashboard: renewal fails immediately and the token dies within 7 days.

Pricing

Your first $300 of licence sales carry no fee at all. After that it is 2.9% flat, split off in the same on-chain transaction as the payment β€” there is no invoice and nothing to collect later. No fixed fee, no monthly fee, no chargebacks. Network gas is separate and paid by the buyer.

$14 licence, inside the free tier  ->  you receive $14.00$14 licence, after the free tier  ->  you receive $13.59  (fee $0.41)

Webhooks

Webhooks are optional β€” the Kit issues and verifies licences without any backend on your side. If you do want server-side events, they use the same signature scheme as every other XPayr webhook (see the signature guide) and are retried with backoff. Use the event_id for idempotency.

Event When
license.issued A payment produced a license. Carries the wallet, product code, plan and transaction hash.
license.revoked You revoked a license from the dashboard, typically after a refund.
payment.pending A checkout session was created.
payment.processing The payment is on chain and waiting for confirmations.
payment.completed Payment verified on chain.
payment.failed The submitted transaction did not verify against the session.
payment.expired The session expired unpaid.

Migrating from ExtensionPay

ExtensionPayXPayr Kit
ExtPay('extension-id') XPayrKit('prod_code')
extpay.startBackground() xpayr.startBackground()
user.paid user.licensed
user.email user.wallet
openPaymentPage() openCheckout()
openLoginPage() openRestore()
onPaid.addListener onLicenseChanged.addListener
getPlans() getPlans()

The one structural difference is identity: ExtensionPay identifies a customer by email, the Kit identifies them by wallet. That removes the magic-link round trip, and it means a licence cannot be shared without sharing a wallet.

Honest limitations

  • Client-side licensing is a deterrent, not DRM. A determined user can patch a local check β€” true of every library in this category, ours included.
  • We are not a merchant of record. You are responsible for your own tax reporting.
  • The buyer needs a wallet. For crypto-native audiences that is a non-issue; for general consumers, run a fiat rail alongside this one.
  • No pull-based subscriptions. Crypto has no card-on-file, so v1 supports lifetime and prepaid annual licences.

Start