Skip to Content
React SDKPayments
View .md

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:

  1. Frontend creates an invoice on your backend, then opens the Alien payment UI
  2. User approves the payment; the transaction is broadcast
  3. Frontend immediately receives the result (paid, cancelled, or broadcast failed)
  4. Backend later receives a webhook when the transaction is confirmed on-chain, and fulfills the order

Prerequisites

  • @alien_org/react installed and AlienProvider wrapping your app
  • A webhook registered in the Developer Portal
  • A backend endpoint to receive and verify webhook callbacks

Supported Tokens

TokenNetworkDecimalsRecipient
USDCsolana6Your Solana wallet address
SOLsolana9Your Solana wallet address
ALIENalien9Your 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

OptionTypeDefaultDescription
timeoutnumber120000Payment 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

ValueTypeDescription
pay(params) => Promise<PaymentResult>Initiates a payment
statusPaymentStatus'idle', 'loading', 'paid', etc.
isLoadingbooleanPayment is in progress
isPaidbooleanPayment completed successfully
isCancelledbooleanUser cancelled the payment
isFailedbooleanPayment failed
txHashstring | undefinedTransaction hash (when paid)
errorCodePaymentErrorCode | undefinedError code (when failed)
errorError | undefinedError object (when failed)
reset() => voidReset state to idle
supportedbooleanWhether payments are supported

Payment Parameters

ParameterTypeRequiredDescription
recipientstringYesWallet address to receive payment
amountstringYesAmount in token’s smallest unit
tokenstringYes'USDC', 'SOL', 'ALIEN', or identifier
networkstringYes'solana' or 'alien'
invoicestringYesUnique payment identifier
itemobjectNoDisplay info for approval screen
item.titlestringYes*Item title
item.iconUrlstringYes*Item icon URL
item.quantitynumberYes*Item quantity
testPaymentTestScenarioNoTest 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

CodeDescription
insufficient_balanceUser does not have enough tokens
network_errorBlockchain network issue
unknownUnexpected error occurred

Webhook Setup

Before accepting payments, you must register a webhook in the Alien Dev Portal:

  1. Go to the Webhooks page

  2. Click Create webhook

  3. Select your Mini App from the dropdown

  4. Set the Webhook URL to your payment endpoint (any URL that can receive POST requests):

    https://<your-domain>/your-webhook-path

    If you’re using the Next.js boilerplate, this is https://<your-domain>/api/webhooks/payment.

  5. Optionally add your Solana pubkey if you want to receive USDC and SOL payments

  6. 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

  1. Read the raw request body as a UTF-8 string
  2. Get the x-webhook-signature header value (hex-encoded Ed25519 signature)
  3. Import your WEBHOOK_PUBLIC_KEY (hex-encoded Ed25519 public key)
  4. 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

StatusDescriptionAction
finalizedTransaction confirmed on-chainFulfill order
failedTransaction failed on-chainMark 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

ScenarioClient seesWebhook sentWebhook status
'paid'paidYesfinalized
'paid:failed'paidYesfailed
'cancelled'cancelledNo-
'error:insufficient_balance'failedNo-
'error:network_error'failedNo-
'error:unknown'failedNo-
  • 'paid' — Happy path. Client shows success, webhook reports finalized.
  • 'paid:failed' — Client shows success, but the webhook reports failed. 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

Last updated on