Accept USDT on any website.
One script tag, one button, one server call. Shoppers scan, confirm with their fingerprint and pay with no gas; you get a signed webhook and an on-chain receipt.
1Load the SDK (browser)
Pin an exact version with Subresource Integrity: versioned files (/sdk/v1.2.1/) never change, and if the bytes ever differ the browser refuses to run them. /sdk/v1/checkout.js always serves the latest 1.x for quick prototypes (no SRI).
<script src="https://whitepay.vercel.app/sdk/v1.2.1/checkout.js"
integrity="sha384-mYqN+5ZK/kC03JCTRK5Izc2hJQ5p5vbImdK8RYS8X7Gpj3OUG90OdBzuiHvwdF6g"
crossorigin="anonymous" defer></script>Using a Content-Security-Policy? Allow https://whitepay.vercel.app in script-src (or put your nonce on the tag), connect-src and img-src.
2Add the button
<whitepay-button order-endpoint="/api/create-order"
payload='{"sku":"chai-kit"}'
label="Pay 2.50 USDT"></whitepay-button>
<script>
document.addEventListener("whitepay:paid", (e) => showThanks(e.detail))
</script>order-endpoint | Your same-origin route that signs the order (step 3). Cross-origin endpoints are refused. |
payload | JSON sent to your endpoint (product id, cart id). Never the price. |
label | Button text. |
whitepay:created | { entityKey, payUrl, deadline } the order is live on Arkiv, QR shown |
whitepay:paid | { entityKey, orderId, txHash, explorer, amount } UI hint only: fulfil from the webhook |
whitepay:expired / whitepay:error | QR expired unpaid / something failed ({ message }) |
It is a standard Web Component, so it works in React, Next.js, Vue (isCustomElement), Svelte, Angular (CUSTOM_ELEMENTS_SCHEMA), WordPress, Shopify themes and plain HTML.
3Sign the order (your server)
import { createWhitePayOrder } from "@whitepay/checkout/server"
const CATALOG = { "chai-kit": "2.50" } // prices live on the server
app.post("/api/create-order", async (req, res) => {
const amount = CATALOG[req.body.payload?.sku]
if (!amount) return res.status(400).json({ error: "unknown product" })
res.json(await createWhitePayOrder({
merchantKey: process.env.SHOP_PK, // your registered shop key, never in the browser
amount,
webhook: "https://shop.example/api/webhook",
}))
})Your shop key signs merchant, amount, orderId, deadline (EIP-712). The WhitePay router contract only settles that exact amount, to your payout address, once.
4Fulfil from the webhook
import { verifyWhitePayWebhook, verifyWhitePayPayment } from "@whitepay/checkout/server"
app.post("/api/webhook", express.text({ type: "*/*" }), async (req, res) => {
const auth = await verifyWhitePayWebhook({ rawBody: req.body, headers: new Headers(req.headers) })
if (!auth.ok) return res.status(401).end() // forged, tampered or replayed
if (await db.seen(auth.eventId)) return res.end() // idempotent
const { txHash, orderId, amount } = auth.event
const v = await verifyWhitePayPayment({ txHash, merchant: SHOP, orderId, minAmount: BigInt(amount) })
if (v.ok) await fulfil(orderId) // money confirmed on Whitechain
res.end()
})x-whitepay-signature | EIP-191 signature of whitepay-webhook:v1:<timestamp>:<keccak256(body)> |
x-whitepay-timestamp | Unix seconds; rejected if older than 5 minutes |
x-whitepay-event-id | Unique per payment: de-duplicate on it |
| Signer | 0x27d2F6770F18E1C0Eefc129124747407Ef44aBe1 (also at /api/webhook-key) |
5Subscriptions (Autopay)
Same button, mode="subscribe". The shopper approves once with their fingerprint; WhitePay charges every period with zero gas and sends you a signed webhook per payment.
<whitepay-button mode="subscribe" order-endpoint="/api/create-subscription"
label="Subscribe · 4.99 USDT / month"></whitepay-button>
// server
import { createWhitePaySubscription } from "@whitepay/checkout/server"
app.post("/api/create-subscription", async (_req, res) => res.json(await createWhitePaySubscription({
merchantKey: process.env.SHOP_PK, title: "Pro plan", amount: "4.99", period: "monthly", cycles: 12,
webhook: "https://shop.example/api/webhook", // subscription.started | charged | completed | cancelled | failed
})))- One signature: the shopper signs a single EIP-2612
permitto a tiny contract made only for their subscription. Its address commits to every term, so one wallet prompt approves exactly that shop, amount, period and number of payments. - How it runs: each upcoming payment is an Arkiv entity whose expiry is its countdown. At 0 the keeper calls the contract's
charge(), deletes the entity and creates the next one. - Hard limits on-chain: never early, never late (missed windows are skipped), never more than amount × cycles, never to anyone but you.
- Cancel: one signature switches the subscription's contract off; you can also cancel from your side.
- Verify charges: the contract emits
Charged(cycle, amount); the webhook carries the tx hash.
Security model
- Price tampering: impossible from the browser. The amount is in the shop-signed order and enforced by the contract.
- Fake payments: the browser event is only a hint; fulfilment needs a signed webhook and a
PaymentSettledevent on-chain. - Replay: EIP-3009 nonces are single-use on-chain; webhooks expire after 5 minutes and carry an event id.
- Supply chain: versioned SDK path + SRI hash. The SDK never touches keys or money.
- XSS in the SDK: every network value is regex-validated and written with
textContent; links usenoopener noreferrer. - SSRF: webhooks only go to https:443 hostnames resolving to public IPs, re-checked before every send, redirects not followed.
- Abuse: per-IP rate limits, JSON-only writes, body caps, cross-site POST blocking and per-payer relayer limits on WhitePay.
- Refunds: only from your payout wallet, only to the original payer, capped at the amount paid (on-chain).