Developers

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:

KeyWhere it goes
pk_… project IDBrowser code (public)
sk_… secret keyServer .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}`);
EventData
invoice.paidid, orderId, amountUsd, receivedUsd, feeUsd, netUsd, payCurrency
invoice.held / deposit.heldid, 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.rejectedid (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.reviewid, reference, coin, address, amountUsd, amountCrypto, txHash, status
ptc.completedtransactionId, userId, reward, currencyName, payoutUsd
offer.completed / offer.reversedtransactionId, 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>
AttributeMeaning
data-idYour New tab zone. Omit to show only the network's house ads (no revenue share).
data-intervalMinimum 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

CallAuth
POST /v1/invoices · GET /v1/invoices/:idsecret key
GET /v1/invoices?status=paid · GET /v1/deposits?status=paidsecret key
GET /v1/balancesecret key
POST /v1/payouts · GET /v1/payouts/:idsecret key
POST /v1/auth/introspectsecret key
GET/POST /api/projects · GET/POST /api/projects/:id · POST /api/projects/:id/zonessession or API token

Base URL https://api.aixpa.store. Errors: { "ok": false, "error": "code" } with a matching HTTP status.