x402 Pay-As-You-Go
@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.
Next.js (browser wallet)
## 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 budgetInstall
npm install @stacknet/x402paygUsage
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 transactionTake $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 nullRecover 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 originallyThe 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 unspendableBuild 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 landsAPI reference
| Export | Description |
|---|---|
X402PaygClient | The 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_MINT | Official mint addresses (constants) |
OFFICIAL_TOPUP_CONTEXT | The pinned vault/verifier/mint set every transaction uses |
Key statuses
| Status | Meaning |
|---|---|
active | Spendable |
pending_verify | Payment landed, attestation pending — still spendable |
depleted | Budget exhausted — mint a new key |
revoked | Killed 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;
pickKeychooses for you.