Skip to Content
StacksPackagesx402 Pay-As-You-Go

x402 Pay-As-You-Go

npm 

@stacknet/x402payg is the pay-as-you-go payments package for StackNet. It turns a Solana payment into a permissionless API key with token credit to use on StackNet. No account is needed, no signup, or card. Pay from a wallet, get a key, spend it against any StackNet endpoint until the budget runs out.

PromptMe

Copy the prompt for your setup and give it to your AI coding agent (Claude Code, Cursor, Codex, etc.) to wire pay-as-you-go into your app. Replace STACK_ID with your Stack ID.

## Task Add StackNet pay-as-you-go top-ups to this Next.js app using `@stacknet/x402payg`. Users pay from their Solana wallet (Phantom/Solflare via wallet-adapter), receive a `pg_` API key with a prepaid token budget, and the app uses that key as the Bearer token for StackNet inference calls. No signup or account system — the wallet signature is the only credential. ## Stack details - Stack ID: `STACK_ID` - StackNet URL: `https://stacknet.magma-rpc.com` - Network: Solana MAINNET ## Steps ### 1. Install ```bash npm install @stacknet/x402payg @solana/wallet-adapter-react @solana/wallet-adapter-wallets @solana/web3.js ``` ### 2. Environment variables Add to `.env.local`: ``` NEXT_PUBLIC_STACK_ID=STACK_ID NEXT_PUBLIC_STACKNET_XPC_URL=https://stacknet.magma-rpc.com ``` ### 3. Create the PAYG hook Create `hooks/use-payg.ts`: ```ts 'use client' import { useMemo, useState, useCallback } from 'react' import { useWallet } from '@solana/wallet-adapter-react' import { X402PaygClient, type PaygKeyInfo } from '@stacknet/x402payg' const client = new X402PaygClient({ stacknetBaseUrl: process.env.NEXT_PUBLIC_STACKNET_XPC_URL || 'https://stacknet.magma-rpc.com', }) export function usePayg() { const wallet = useWallet() const [keys, setKeys] = useState<PaygKeyInfo[]>([]) const [busy, setBusy] = useState(false) // wallet-adapter wallets are structurally compatible with PaygWallet const paygWallet = useMemo(() => { if (!wallet.publicKey || !wallet.signMessage) return null return { publicKey: wallet.publicKey, signMessage: wallet.signMessage, sendTransaction: wallet.sendTransaction, } }, [wallet.publicKey, wallet.signMessage, wallet.sendTransaction]) const topUp = useCallback(async (usdcAmount: number) => { if (!paygWallet) throw new Error('connect a wallet first') setBusy(true) try { const created = await client.createKey(paygWallet, { amount: BigInt(Math.round(usdcAmount * 1_000_000)), // USDC 6dp mint: 'USDC', stackId: process.env.NEXT_PUBLIC_STACK_ID || '', }) return created // { apiKey, proofPda, txSignature, ... } } finally { setBusy(false) } }, [paygWallet]) const refreshKeys = useCallback(async () => { if (!paygWallet) return setKeys(await client.getActiveKeys(paygWallet)) }, [paygWallet]) const recoverKey = useCallback(async (proofPda: string) => { if (!paygWallet) throw new Error('connect a wallet first') return client.deriveApiKeyFor(paygWallet, proofPda) }, [paygWallet]) const revoke = useCallback(async (keyId: string) => { if (!paygWallet) throw new Error('connect a wallet first') await client.revokeKey(paygWallet, keyId) await refreshKeys() }, [paygWallet, refreshKeys]) return { topUp, refreshKeys, recoverKey, revoke, keys, busy, connected: !!paygWallet } } ``` ### 4. Build the top-up UI Create a top-up panel with preset amounts ($1 / $5 / $25), a "Top up" button that calls `topUp()`, and a key list showing `tokensRemaining` / `tokenBudget` per key with a progress bar and a revoke button. After a successful top-up, store ONLY the `proofPda` (e.g. localStorage) — NEVER persist the raw `pg_` key; re-derive it with `recoverKey(proofPda)` when needed. ### 5. Use the key for inference ```ts const apiKey = await recoverKey(proofPda) const res = await fetch(`${process.env.NEXT_PUBLIC_STACKNET_XPC_URL}/v1/chat/completions`, { method: 'POST', headers: { authorization: `Bearer ${apiKey}`, 'content-type': 'application/json' }, body: JSON.stringify({ model: 'magma', messages }), }) ``` When a call fails with a "depleted" error, surface the top-up panel again. ## Result After completing these steps you will have: - Wallet-signed pay-as-you-go top-ups (USDC or PAPER on mainnet) with no account system - `pg_` API keys derived from wallet signatures — never persisted anywhere - A key dashboard with live budget bars, recovery, and revoke - StackNet inference calls billed against the prepaid budget

Install

npm install @stacknet/x402payg

Usage

Mint a key (browser wallet)

createKey does the whole loop: builds the payment + proof transaction, has the wallet sign and send it, waits for confirmation, derives the pg_ key from a wallet signature, and registers the proof with StackNet.

import { X402PaygClient } from '@stacknet/x402payg' const client = new X402PaygClient({ stacknetBaseUrl: 'https://stacknet.magma-rpc.com', // rpcUrl optional — defaults to the public mainnet RPC }) // wallet = any wallet-adapter wallet (Phantom, Solflare, ...) const created = await client.createKey(wallet, { amount: 25_000_000n, // 25 USDC (6 decimals) → $25 → 25,000,000 tokens mint: 'USDC', // 'SOL' | 'USDC' | 'PAPER' stackId: 'STACK_ID', }) console.log(created.apiKey) // pg_<payer>.<sig> — use as a Bearer token console.log(created.proofPda) // the on-chain receipt address console.log(created.txSignature) // the funding transaction

Take $25 from my wallet, staple the receipt on-chain, and hand me the prepaid code.

Mint a key (server / CLI)

No browser? Wrap a raw Keypair:

import { Keypair } from '@solana/web3.js' import bs58 from 'bs58' import { X402PaygClient, walletFromKeypair } from '@stacknet/x402payg' const keypair = Keypair.fromSecretKey(bs58.decode(process.env.WALLET_SECRET!)) const wallet = walletFromKeypair(keypair) const client = new X402PaygClient({ stacknetBaseUrl: 'https://stacknet.magma-rpc.com' }) const created = await client.createKey(wallet, { amount: 100_000_000n, // 0.1 SOL in lamports mint: 'SOL', stackId: 'STACK_ID', })

Spend the key

The pg_ key is a standard Bearer token on every StackNet endpoint — chat, messages, tools:

const res = await fetch('https://stacknet.magma-rpc.com/v1/chat/completions', { method: 'POST', headers: { 'authorization': `Bearer ${created.apiKey}`, 'content-type': 'application/json', }, body: JSON.stringify({ model: 'magma', messages: [{ role: 'user', content: 'Hello!' }], }), })

It also drops straight into OpenAI-compatible SDKs:

import OpenAI from 'openai' const openai = new OpenAI({ baseURL: 'https://stacknet.magma-rpc.com/v1', apiKey: created.apiKey, // pg_... })

The code works anywhere a normal API key works. Every call’s cost is subtracted from the card until it’s empty; then calls fail with “depleted” and you mint a fresh key.

List your keys

Ownership is proved with a fresh wallet signature — no login, no password. Raw keys are never returned (the server doesn’t have them).

const keys = await client.listKeys(wallet) // [{ keyId, proofPda, status, tokenBudget, tokensUsed, tokensRemaining, stackId, mint, amount, ... }] const active = await client.getActiveKeys(wallet) // spendable, most-remaining first const best = await client.pickKey(wallet) // the single best key, or null

Recover a lost key

Because the key is a deterministic wallet signature, re-signing reproduces the exact original:

const keys = await client.listKeys(wallet) const apiKey = await client.deriveApiKeyFor(wallet, keys[0].proofPda) // identical to the pg_ key minted originally

The code was never written down anywhere — your wallet just knows how to say it again.

Revoke a leaked key

await client.revokeKey(wallet, keyId) // terminal: the key is dead everywhere, remaining budget is unspendable

Build the transaction yourself

Dapps that simulate before sending, or that own their signing pipeline, can build without sending:

const built = client.buildTopup({ payer: wallet.publicKey, amount: 5_000_000n, mint: 'USDC', stackId: 'STACK_ID', }) // built.transaction — sign & send however you like // built.proofPda — pass to deriveApiKeyFor(wallet, built.proofPda) once it lands

API reference

ExportDescription
X402PaygClientThe client. Config: { stacknetBaseUrl, connection?, rpcUrl?, fetch? }
createKey(wallet, params)Full mint: pay → prove → confirm → derive pg_ key → register
buildTopup(params)Build the payment+proof transaction without sending
listKeys(wallet)List the wallet’s keys (fresh signed challenge; no raw keys returned)
getActiveKeys(wallet)Spendable keys, sorted most-remaining first
pickKey(wallet)The single best key to use, or null
deriveApiKeyFor(wallet, proofPda)Re-derive a raw key by re-signing (deterministic)
revokeKey(wallet, keyId)Kill switch for a leaked key (terminal)
walletFromKeypair(keypair)Wrap a raw Keypair as a wallet (server/CLI)
PAPER_MINT, USDC_MINTOfficial mint addresses (constants)
OFFICIAL_TOPUP_CONTEXTThe pinned vault/verifier/mint set every transaction uses

Key statuses

StatusMeaning
activeSpendable
pending_verifyPayment landed, attestation pending — still spendable
depletedBudget exhausted — mint a new key
revokedKilled by the owner wallet — terminal

Good to know

  • Finality: StackNet credits the budget only after the funding transaction is finalized (~30s). createKey’s register call is retry-safe — re-registering the same proof is idempotent.
  • Amounts are base units: lamports for SOL (1 SOL = 1_000_000_000n), 6-decimal units for USDC ($1 = 1_000_000n).
  • One key per payment: each top-up mints its own key with its own budget. Wallets can hold many; pickKey chooses for you.
Last updated on