Payments
Accept payments in your mini app using the Alien React SDK. Users can pay with USDC, SOL (on Solana) or ALIEN tokens directly from their wallet.
Overview
The payment flow involves both frontend and backend:
- Frontend creates an invoice on your backend, then opens the Alien payment UI
- User approves the payment; the transaction is broadcast
- Frontend immediately receives the result (paid, cancelled, or broadcast failed)
- Backend later receives a webhook when the transaction is confirmed on-chain, and fulfills the order
Prerequisites
@alien_org/reactinstalled andAlienProviderwrapping your app- A webhook registered in the Developer Portal
- A backend endpoint to receive and verify webhook callbacks
Supported Tokens
| Token | Network | Decimals | Recipient |
|---|---|---|---|
| USDC | solana | 6 | Your Solana wallet address |
| SOL | solana | 9 | Your Solana wallet address |
| ALIEN | alien | 9 | Your provider address from the Dev Portal |
Using the usePayment Hook
The usePayment hook provides full state management for payments.
function CheckoutButton() {
const { authToken } = useAlien();
const {
pay,
status,
isLoading,
isPaid,
isCancelled,
isFailed,
txHash,
errorCode,
error,
reset,
supported,
} = usePayment({
onPaid: (txHash) => {
console.log('Payment successful:', txHash);
},
onCancelled: () => {
console.log('Payment cancelled by user');
},
onFailed: (errorCode, error) => {
console.error('Payment failed:', errorCode, error);
},
});
const handlePayment = async () => {
// Step 1: Create invoice on your backend
const res = await fetch('/api/invoices', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authToken}`,
},
body: JSON.stringify({
recipientAddress: 'YOUR_WALLET_ADDRESS',
amount: '10000',
token: 'USDC',
network: 'solana',
productId: 'my-product',
}),
});
const { invoice } = await res.json();
// Step 2: Open Alien payment UI
await pay({
recipient: 'YOUR_WALLET_ADDRESS',
amount: '10000',
token: 'USDC',
network: 'solana',
invoice,
item: {
title: 'Premium Plan',
iconUrl: 'https://example.com/icon.png',
quantity: 1,
},
});
};
if (!supported) {
return <p>Payments not supported in this environment</p>;
}
return (
<button onClick={handlePayment} disabled={isLoading}>
{isLoading ? 'Processing...' : 'Pay Now'}
</button>
);
}Hook Options
| Option | Type | Default | Description |
|---|---|---|---|
timeout | number | 120000 | Payment timeout in ms (2 min) |
onPaid | (txHash: string) => void | - | Called on success |
onCancelled | () => void | - | Called on user cancel |
onFailed | (code, error?) => void | - | Called on failure |
onStatusChange | (status) => void | - | Called on any change |
Hook Return Values
| Value | Type | Description |
|---|---|---|
pay | (params) => Promise<PaymentResult> | Initiates a payment |
status | PaymentStatus | 'idle', 'loading', 'paid', etc. |
isLoading | boolean | Payment is in progress |
isPaid | boolean | Payment completed successfully |
isCancelled | boolean | User cancelled the payment |
isFailed | boolean | Payment failed |
txHash | string | undefined | Transaction hash (when paid) |
errorCode | PaymentErrorCode | undefined | Error code (when failed) |
error | Error | undefined | Error object (when failed) |
reset | () => void | Reset state to idle |
supported | boolean | Whether payments are supported |
Payment Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
recipient | string | Yes | Wallet address to receive payment |
amount | string | Yes | Amount in token’s smallest unit |
token | string | Yes | 'USDC', 'SOL', 'ALIEN', or identifier |
network | string | Yes | 'solana' or 'alien' |
invoice | string | Yes | Unique payment identifier |
item | object | No | Display info for approval screen |
item.title | string | Yes* | Item title |
item.iconUrl | string | Yes* | Item icon URL |
item.quantity | number | Yes* | Item quantity |
test | PaymentTestScenario | No | Test scenario string |
*Required if item is provided.
Invoice
The invoice field is a memo string attached to the on-chain
transaction. It serves as a unique identifier that links the payment
to an order in your backend.
You can use any format for the invoice value — UUID, SHA-256 hash, Nano ID, or any custom string — as long as it is unique per payment and your backend can look it up when the webhook arrives.
Size limit: The invoice is written to the transaction memo and must be strictly less than 64 bytes (512 bits). SHA-256 hashes (32 bytes raw), UUIDs (36 chars), and Nano IDs (21 chars) all fit comfortably within this limit.
Examples of valid invoice values:
// UUID (36 bytes)
const invoice = crypto.randomUUID();
// SHA-256 as base64url (43 bytes) — fits well under 64
const invoice = createHash('sha256')
.update(orderId)
.digest('base64url');
// Nano ID (21 bytes)
const invoice = nanoid();
// Custom short ID
const invoice = `ord_${Date.now()}`;Error Codes
| Code | Description |
|---|---|
insufficient_balance | User does not have enough tokens |
network_error | Blockchain network issue |
unknown | Unexpected error occurred |
Webhook Setup
Before accepting payments, you must register a webhook in the Alien Dev Portal :
-
Go to the Webhooks page
-
Click Create webhook
-
Select your Mini App from the dropdown
-
Set the Webhook URL to your payment endpoint (any URL that can receive POST requests):
https://<your-domain>/your-webhook-pathIf you’re using the Next.js boilerplate , this is
https://<your-domain>/api/webhooks/payment. -
Optionally add your Solana pubkey if you want to receive USDC and SOL payments
-
Click Create
After creation, you will be shown your webhook public key
(Ed25519, hex-encoded). Copy it immediately — this is your
WEBHOOK_PUBLIC_KEY environment variable. It is only shown once.
If you need to rotate the public key, open your webhook in the Dev
Portal and click Rotate key. A new key will be generated without
recreating the webhook — just update WEBHOOK_PUBLIC_KEY on your
server.
Webhook Payload
The Alien platform sends a POST request to your webhook URL with
the following JSON body:
interface WebhookPayload {
invoice: string;
recipient: string;
status: 'finalized' | 'failed';
txHash?: string;
amount?: string;
token?: string;
network?: string;
test?: boolean;
}Webhook Signature Verification
Security: Always verify the webhook signature. Without verification, any external party could send fake webhook requests to your endpoint and trick your backend into fulfilling orders that were never paid.
Every webhook request includes an x-webhook-signature header
containing an Ed25519 signature of the raw request body. You must
verify this signature before processing the payload — reject the
request if the signature is missing or invalid.
The public key used to verify signatures is shown once when you
create your webhook in the Dev Portal. Store it as
the WEBHOOK_PUBLIC_KEY environment variable on your server.
Verification Algorithm
- Read the raw request body as a UTF-8 string
- Get the
x-webhook-signatureheader value (hex-encoded Ed25519 signature) - Import your
WEBHOOK_PUBLIC_KEY(hex-encoded Ed25519 public key) - Verify the signature against the raw body using Ed25519
Implementation (Web Crypto API)
async function verifySignature(
publicKeyHex: string,
signatureHex: string,
body: string,
): Promise<boolean> {
// Import the Ed25519 public key from hex
const publicKey = await crypto.subtle.importKey(
'raw',
Buffer.from(publicKeyHex, 'hex'),
{ name: 'Ed25519' },
false,
['verify'],
);
// Verify the signature
return crypto.subtle.verify(
'Ed25519',
publicKey,
Buffer.from(signatureHex, 'hex'),
Buffer.from(body),
);
}Complete Webhook Handler
Here is a complete webhook handler example based on the Next.js boilerplate . Adapt the route path and database logic to your own backend:
async function verifySignature(
publicKeyHex: string,
signatureHex: string,
body: string,
): Promise<boolean> {
const publicKey = await crypto.subtle.importKey(
'raw',
Buffer.from(publicKeyHex, 'hex'),
{ name: 'Ed25519' },
false,
['verify'],
);
return crypto.subtle.verify(
'Ed25519',
publicKey,
Buffer.from(signatureHex, 'hex'),
Buffer.from(body),
);
}
export async function POST(request: Request) {
// 1. Read the raw body (must be read before parsing JSON)
const rawBody = await request.text();
// 2. Get the signature from headers
const signatureHex =
request.headers.get('x-webhook-signature') ?? '';
if (!signatureHex) {
return NextResponse.json(
{ error: 'Missing webhook signature' },
{ status: 401 },
);
}
try {
// 3. Verify the Ed25519 signature
const isValid = await verifySignature(
process.env.WEBHOOK_PUBLIC_KEY!,
signatureHex,
rawBody,
);
if (!isValid) {
return NextResponse.json(
{ error: 'Invalid signature' },
{ status: 401 },
);
}
// 4. Parse and process the payload
const payload = JSON.parse(rawBody);
// 5. Find the order by invoice and update status
if (payload.status === 'finalized') {
// Transaction confirmed on-chain — fulfill the order
await fulfillOrder(payload.invoice, payload.txHash);
} else if (payload.status === 'failed') {
// Transaction failed — handle accordingly
await markOrderFailed(payload.invoice);
}
return NextResponse.json({ success: true });
} catch (error) {
console.error('Webhook processing error:', error);
return NextResponse.json(
{ error: 'Failed to process webhook' },
{ status: 500 },
);
}
}Webhook Statuses
| Status | Description | Action |
|---|---|---|
finalized | Transaction confirmed on-chain | Fulfill order |
failed | Transaction failed on-chain | Mark order as failed |
Test Mode
Test payments without transferring real tokens by passing a test
scenario string. The test parameter accepts a PaymentTestScenario
string that simulates different outcomes:
await pay({
recipient: 'YOUR_WALLET_ADDRESS',
amount: '10000',
token: 'USDC',
network: 'solana',
invoice: 'test-order-123',
test: 'paid', // Simulate successful payment
});Test Scenarios
| Scenario | Client sees | Webhook sent | Webhook status |
|---|---|---|---|
'paid' | paid | Yes | finalized |
'paid:failed' | paid | Yes | failed |
'cancelled' | cancelled | No | - |
'error:insufficient_balance' | failed | No | - |
'error:network_error' | failed | No | - |
'error:unknown' | failed | No | - |
'paid'— Happy path. Client shows success, webhook reportsfinalized.'paid:failed'— Client shows success, but the webhook reportsfailed. Useful for testing rollback logic.'cancelled'— Simulates user cancellation. No webhook is sent.'error:*'— Pre-broadcast failures. No transaction is created and no webhook is sent.
Test transactions are marked with test: true in the webhook payload.
Handling Payment States
function PaymentStatus() {
const {
status, isPaid, isFailed, isCancelled,
txHash, errorCode, reset,
} = usePayment();
if (isPaid) {
return (
<div>
<h2>Payment Successful</h2>
<p>Transaction: {txHash}</p>
<button onClick={reset}>Make Another Payment</button>
</div>
);
}
if (isFailed) {
return (
<div>
<h2>Payment Failed</h2>
<p>Error: {errorCode}</p>
<button onClick={reset}>Try Again</button>
</div>
);
}
if (isCancelled) {
return (
<div>
<h2>Payment Cancelled</h2>
<button onClick={reset}>Try Again</button>
</div>
);
}
return <p>Status: {status}</p>;
}Complete Example
A full purchase flow with invoice creation, payment, and webhook handling.
Frontend (React)
function BuyButton({ product }) {
const { authToken } = useAlien();
const payment = usePayment({
onPaid: (txHash) => console.log('Paid!', txHash),
onCancelled: () => console.log('Cancelled'),
onFailed: (code) => console.log('Failed:', code),
});
const handlePurchase = useCallback(async () => {
if (!authToken) return;
// 1. Create invoice on your backend
const res = await fetch('/api/invoices', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
Authorization: `Bearer ${authToken}`,
},
body: JSON.stringify({
recipientAddress: product.recipientAddress,
amount: product.amount,
token: product.token,
network: product.network,
productId: product.id,
}),
});
const { invoice } = await res.json();
// 2. Open payment UI
await payment.pay({
recipient: product.recipientAddress,
amount: product.amount,
token: product.token,
network: product.network,
invoice,
item: {
title: product.name,
iconUrl: product.iconUrl,
quantity: product.quantity,
},
});
}, [authToken, payment, product]);
if (!payment.supported) {
return <p>Payments not available</p>;
}
return (
<button onClick={handlePurchase} disabled={payment.isLoading}>
{payment.isLoading ? 'Processing...' : `Buy for ${product.price}`}
</button>
);
}Backend Invoice Route
Example using Next.js App Router (adapt to your framework). This route authenticates the user, validates the request body, verifies the product parameters match your catalog, and creates a payment intent:
const authClient = createAuthClient();
function extractBearerToken(
header: string | null,
): string | null {
return header?.startsWith('Bearer ')
? header.slice(7)
: null;
}
export async function POST(request: Request) {
try {
const token = extractBearerToken(
request.headers.get('Authorization'),
);
if (!token) {
return NextResponse.json(
{ error: 'Missing authorization token' },
{ status: 401 },
);
}
// Verify JWT — `sub` is the user's Alien ID
const { sub } = await authClient.verifyToken(token);
const body = await request.json();
const {
recipientAddress, amount,
token: paymentToken, network, productId,
} = body;
// Validate that the product parameters match your catalog
// to prevent clients from sending arbitrary amounts
const product = PRODUCTS.find((p) => p.id === productId);
if (
!product ||
product.amount !== amount ||
product.token !== paymentToken ||
product.network !== network ||
product.recipientAddress !== recipientAddress
) {
return NextResponse.json(
{ error: 'Invalid product parameters' },
{ status: 400 },
);
}
// Any unique string under 64 bytes
const invoice = randomUUID();
// Save payment intent to your database
await db.paymentIntents.create({
invoice,
senderAlienId: sub,
recipientAddress,
amount,
token: paymentToken,
network,
productId,
status: 'pending',
});
return NextResponse.json({ invoice });
} catch (error) {
if (error instanceof JwtErrors.JWTExpired) {
return NextResponse.json(
{ error: 'Token expired' },
{ status: 401 },
);
}
if (error instanceof JwtErrors.JOSEError) {
return NextResponse.json(
{ error: 'Invalid token' },
{ status: 401 },
);
}
console.error('Failed to create invoice:', error);
return NextResponse.json(
{ error: 'Failed to create invoice' },
{ status: 500 },
);
}
}Important: Always validate that the product parameters (amount, token, network, recipient) match your server-side product catalog. Never trust client-submitted amounts directly.
Next Steps
- Install Boilerplate — Pre-configured template
- Authentication — Authenticate users before checkout
- Developer Portal — Configure webhooks