[Home](https://aixpa.store/) › Integration guide

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](https://aixpa.store/features) and [what it costs](https://aixpa.store/pricing); 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](https://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](https://aixpa.store/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.
