Install
npm install @xpayr/extension-kit
# or copy src/index.js into your extension β
# zero dependencies, no build step, MV3-ready
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.
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.
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.
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.
npm install @xpayr/extension-kit
# or copy src/index.js into your extension β
# zero dependencies, no build step, MV3-ready
{
"manifest_version": 3,
"permissions": ["storage", "alarms"],
"externally_connectable": { "matches": ["https://xpayr.com/*"] }
}
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');
});
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}.
| 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. |
| 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. |
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.
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 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. |
| ExtensionPay | XPayr 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.