Integration guide
One project per website. Aixpa handles sign-in, payments, payouts and ads; your site keeps its own users and data.
New here? See what each module does and what it costs; the steps below open one at a time.
1. Create a project
Dashboard → Projects → Connect a new website. You get:
| Key | Where it goes |
|---|---|
pk_… project ID | Browser code (public) |
sk_… secret key | Server .env only. Signs webhooks and login tokens. Rotate it from the dashboard if it leaks. |
Automating it? Create a personal API token (dashboard → API tokens) and call POST /api/projects
with Authorization: Bearer axp_…. Tokens can create and configure projects but can never withdraw.
2. Login (wallet, Google, email)
One button, every method: the Aixpa sign-in window offers Google, a 6-digit email code, browser wallets (MetaMask, Trust, OKX, Phantom, Solflare) and WalletConnect. Google and email run on Aixpa's own accounts — your site needs no Google Cloud project, no mail server and no extra keys.
<script src="https://aixpa.store/sdk/v1.js"></script>
<script>
Aixpa.init({ project: 'pk_…' });
const { token } = await Aixpa.login(); // popup: Google, email code, wallets
await fetch('/api/login', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ token }) });
// Mobile wallets may come back with #aixpa_token=… → Aixpa.redirectToken()
</script>
The token is a JWT (HS256, key = your secret key, valid 1 hour) and is only ever delivered to the origin of your registered site URL. Verify it on your server, then create your own session:
import { Aixpa } from './aixpa-node.mjs'; // https://aixpa.store/sdk/aixpa-node.mjs — one file, no deps
const aixpa = new Aixpa(process.env.AIXPA_SECRET_KEY);
const claims = aixpa.verifyLogin(token); // null if forged/expired
// claims.sub = "evm:0xabc…" | "solana:…" | "google:…" | "email:you@x.com" → unique key of the user in YOUR database
// claims.address = wallet address or email (for display), claims.method = "wallet" | "google" | "email"
No Node? Call POST https://api.aixpa.store/v1/auth/introspect with the secret key and
{ "token": "…" } → { "active": true, "sub": … }.
3. Top-ups (crypto invoices)
// server
const { invoice } = await aixpa.createInvoice({ amountUsd: 10, payCurrency: 'USDT-TRC20', orderId: `topup:${userId}:${uniqueId}` });
// browser
await Aixpa.pay(invoice.payUrl); // hosted checkout: QR, address, countdown, live status
Coins: USDT-TRC20 USDT-BEP20 BTC ETH LTC DOGE TRX BNB SOL, or FAUCETPAY (payer pays from their FaucetPay balance in any coin, ideal for small top-ups). Minimum $1 (some coins higher).
orderId is unique per project. Fee: 0.5% of the amount received.
Two clocks: expiresAt is the 60-minute price quote, payableUntil how long the address stays
this invoice's (up to 90 days for BTC / LTC). A transaction reported before expiresAt is credited at the
quoted price (up to the quoted amount) when it confirms; anything later — also a payment that replaced it — at the
price when it arrives. An invoice made without the new
fields (no account, the payer pays the fee) reads one clock, as before: unpaid past expiresAt
it answers status: "expired" — a late payment still turns it paid and sends
invoice.paid. With account or feePaidBy: "merchant" it stays
pending until payableUntil.
4. Webhooks — the only place to credit
Set the webhook URL in project settings (public https). Every event is a POST with JSON
{ event, createdAt, data } and header x-aixpa-signature: t=<ms>,v1=<hex HMAC-SHA256(secret, t + "." + rawBody)>.
Answer 2xx; failures retry for about 2 days. Events can arrive twice — credit with a unique reference.
const event = aixpa.verifyWebhook(rawBody, req.headers['x-aixpa-signature']);
if (!event) return res.status(401).end();
if (event.event === 'invoice.paid') credit(userFrom(event.data.orderId), event.data.netUsd, `invoice:${event.data.id}`);
| Event | Data |
|---|---|
invoice.paid | id, orderId, amountUsd, receivedUsd, feeUsd, netUsd, payCurrency |
invoice.held / deposit.held | id, account, reasonCode (e.g. shared_address, partial_payment, other_coin, unknown_amount, extra_payment), reason; orderId on an invoice, invoiceId + orderId on a deposit that belongs to one — checked by hand, nothing to credit yet |
deposit.paid / deposit.rejected | id (dep_… a payment to a permanent address; fp_… / rev_… a second or other-coin payment on an invoice, released by hand), account, invoiceId + orderId when it belongs to an invoice, receivedUsd, feeUsd, netUsd, paidAt — credit netUsd once per id; rejected: the held payment was closed, nothing to credit |
payout.sent / payout.failed / payout.review | id, reference, coin, address, amountUsd, amountCrypto, txHash, status |
ptc.completed | transactionId, userId, reward, currencyName, payoutUsd |
offer.completed / offer.reversed | transactionId, network, userId, reward, payoutUsd |
A webhook that never reached you (your server was down longer than the retries, or answered 2xx without crediting): read what was paid and credit it with the same unique reference — a payment credited before is skipped by your own ledger. Run it once a day, or after an outage.
// every paid invoice after your stored cursor, oldest paid first — each row is what its invoice.paid carried
for (;;) {
const { invoices, next } = await aixpa.paidInvoices({ after: cursor, limit: 200 });
for (const inv of invoices) {
if (inv.netUsd !== undefined) credit(userFrom(inv.orderId), inv.netUsd, `invoice:${inv.id}`);
cursor = inv.id; // store it: the next run starts here
}
if (!next) break;
}
GET /v1/invoices?status=paid&after=<invoice id>&limit=<1–200> →
{ invoices, next }; after must be a paid invoice of your project (else
400 invalid_cursor); an invoice paid in the last second shows on the next read. One invoice:
aixpa.getInvoice(id) — once status is "paid" it carries the same
receivedUsd, feeUsd, netUsd as its webhook (an invoice paid before these were
stored has none: skip it, as the loop does). Everything sent as deposit.paid is listed the same way:
aixpa.deposits({ status: 'paid', after, limit }) → { deposits, next }, ids
dep_…, fp_…, rev_…, each row equal to its webhook.
5. Payouts to your users
await aixpa.payout({ amountUsd: 1.5, coin: 'LTC', address: 'ltc1…', reference: `wd:${withdrawalId}` }); // on-chain
await aixpa.payout({ amountUsd: 0.2, coin: 'DOGE', address: 'player@mail.com', reference: `wd:${id}` }); // FaucetPay
Paid from the project balance. Reusing a reference returns the first payout, so retries never pay twice.
A LTC address goes on-chain; a FaucetPay email or FaucetPay-linked address (BTC ETH LTC DOGE BCH DASH DGB TRX BNB SOL XRP ADA
TON USDT ZEC XLM) is paid instantly through FaucetPay — ideal for small faucet withdrawals. Force a rail with
method: 'onchain' | 'faucetpay'. Status of every rail: aixpa.store/status.
6. Ads and rewards
Create a zone in project settings, then Aixpa.ad(document.getElementById('slot'), 'zn_…'). Offerwall and PTC rewards
reach your webhook as ptc.completed / offer.completed with the userId you passed
(letters, digits, _ . @ -).
6b. Offerwalls and surveys (one embed)
Every offerwall and survey network connected to Aixpa (BitcoTasks, CPX Research, BitLabs, TimeWall, Monlix…) plus Aixpa PTC, in one page. Put it where a signed-in user earns, with your own user id (the same id your webhook credits):
<iframe src="https://aixpa.store/earn?p=pk_…&u=USER_ID" style="width:100%;height:900px;border:0"></iframe>
Completed offers reach your webhook as offer.completed (and offer.reversed on a
chargeback) with userId = the u you passed, the reward in your currency and your USD share. No
account at any network is needed: Aixpa holds them all.
7. Ads: new tab, banner and text
One line anywhere in your pages. On a visitor's click (any link, button or text), the next promoted page opens in a new tab of the same browser and takes focus; the page they clicked still works normally. Campaigns rotate per visitor (A → B → C → A). Create a New tab zone in project settings to earn your share.
<script async src="https://aixpa.store/static/js/main.js" data-id="zn_…"></script>
| Attribute | Meaning |
|---|---|
data-id | Your New tab zone. Omit to show only the network's house ads (no revenue share). |
data-interval | Minimum seconds between two tabs for one visitor. Default 30; 0 = every click. |
Banner + text slots (same script)
With a Banner + text zone, put <div data-slot></div> anywhere: header, sidebar, between posts,
mobile footer. Each slot measures its own width and shows a banner that fits (728x90, 468x60, 300x250, 320x50) or a text ad
when no banner fits. Slots on one page get different ads, and ads rotate evenly on every view.
<script async src="https://aixpa.store/static/js/main.js" data-id="zn_…"></script> <div data-slot></div> <!-- any width --> <div data-slot data-height="60"></div> <!-- limit the height (e.g. mobile bar) -->
data-refresh="60" on the script tag rotates slots every 60 s while the page is visible
(minimum 15). Slots added later (infinite scroll, SPA pages) are filled automatically.
Popup blockers allow it because the ad link is fetched before the click and opened synchronously inside the visitor's own click. File and API paths look like ordinary site assets, so generic filter rules do not match them; a visitor's ad blocker can still block any third-party script. Try it live on /tab-test.
Starter project
A complete example (Node, SQLite, no npm): wallet login, top-up, webhook crediting with an idempotent coin ledger.
In the Aixpa repo: node ops/dev/new-project.mjs --name "My Game" --site https://mygame.com --dir ../mygame
creates the project and the folder with .env already filled.
Server API reference
| Call | Auth |
|---|---|
POST /v1/invoices · GET /v1/invoices/:id | secret key |
GET /v1/invoices?status=paid · GET /v1/deposits?status=paid | secret key |
GET /v1/balance | secret key |
POST /v1/payouts · GET /v1/payouts/:id | secret key |
POST /v1/auth/introspect | secret key |
GET/POST /api/projects · GET/POST /api/projects/:id · POST /api/projects/:id/zones | session or API token |
Base URL https://api.aixpa.store. Errors: { "ok": false, "error": "code" } with a matching HTTP status.