API Documentation
NFT gating, minting, subscriptions, and token-gated access on Solana.
Getting Started
Catalyst is an NFT infrastructure platform on Solana. Verify ownership, check traits, mint NFTs, and manage subscriptions — all through a simple API or embeddable UI.
What are you building?
1. Create an account
Sign in at catalyst.akca.network/login using a Solana wallet or a passkey.
2. Create a project
A project groups your collections, API keys, and analytics. Choose devnet for testing or mainnet for production.
3. Create a collection & add items
A collection maps to an on-chain NFT collection. Add items (products) to it — each item can be one-time (buy once) or recurring (subscription with monthly/quarterly/yearly billing). Add traits to items for role/tier-based gating.
4. Generate an API key
ak_test_* keys are for devnet, ak_live_* keys are for mainnet. Include the key in every request via the X-API-Key header.
curl -X POST https://catalyst.akca.network/api/v1/verify/ownership \
-H "X-API-Key: ak_test_xxx" \
-H "Content-Type: application/json" \
-d '{"wallet": "...", "collection_id": "col_xxx"}'How It Works
When you call the verify endpoint, Catalyst connects to Solana's RPC, retrieves the wallet's token accounts, and checks if any belong to your collection. Results are cached for 30 seconds.
For subscription NFTs, Catalyst additionally checks if the subscription period is still active. Expired NFTs are filtered out — the wallet still owns the NFT on-chain, but hasAccess returns false until the user renews.
One-time NFTs
Mint NFT → own forever
verifyOwnership → always true
verifyTrait → check traits
Subscription NFTs
Mint → subscription starts
Expires → hasAccess: false
Renew SOL → period extended
DAO Membership
Mint to join → role via trait
verifyTrait(Role) → gate features
Optional recurring for tiers
Verify Ownership
Check if a wallet owns any NFT from a specific collection. For subscription NFTs, expired ones are automatically filtered out.
Request Body
{
"wallet": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU",
"collection_id": "col_xxx"
// OR
"collection_address": "Bq42...onchain-address"
}Response
{
"ok": true,
"hasAccess": true,
"nfts": [
{
"mint": "NFT_MINT_ADDRESS",
"name": "Premium Plan #42",
"subscription": {
"status": "active",
"expiresAt": "2026-04-01T00:00:00Z",
"interval": "monthly"
}
}
],
"cached": false
}Pass either collection_id (Catalyst internal ID) or collection_address (Solana on-chain address). The subscription field is only present for recurring NFTs. One-time NFTs always pass verification if the wallet owns them.
Verify Trait
Check if a wallet owns an NFT with specific traits. Expired subscription NFTs are excluded.
{
"wallet": "7xKXtg...",
"collection_id": "col_xxx",
"trait_type": "Role",
"trait_values": ["Admin", "Moderator"]
}Response
{
"ok": true,
"hasAccess": true,
"matchingNfts": [
{ "mint": "...", "name": "...", "matchedTraits": { "Role": "Admin" } }
]
}The trait_values array acts as an OR condition — the wallet needs at least one matching trait value.
Verify Rules
Evaluate a predefined access rule. Rules combine ownership and trait checks with AND/OR logic, configured in the dashboard.
{
"wallet": "7xKXtg...",
"rule_id": "rule_xxx"
}Response
{
"ok": true,
"hasAccess": true,
"ruleType": "trait"
}Mint NFTs
Initiate or confirm an NFT mint transaction. This is a two-step process:
Step 1: Get unsigned transaction
POST /api/v1/mint
{
"wallet": "7xKXtg...",
"collection_id": "col_xxx",
"item_id": "item_xxx" // optional — specific item within collection
}The API returns a serialized transaction for the user to sign in their wallet.
Step 2: Confirm with signature
POST /api/v1/mint
{
"wallet": "7xKXtg...",
"collection_id": "col_xxx",
"item_id": "item_xxx",
"tx_signature": "5Gz..."
}Once confirmed, the NFT appears in the wallet. If the item has recurring billing, a subscription is automatically created with the configured interval.
Subscriptions
Catalyst supports NFT-based subscriptions. Unlike wallet-based subscriptions, the NFT itself carries the subscription state — if the subscription expires, hasAccess returns false even though the wallet still owns the NFT.
Setting up subscriptions
In the dashboard, create an item and set its billing type to Recurring. Choose an interval:
monthly— 30 daysquarterly— 90 daysyearly— 365 days
Set the SOL price per period. When a user mints this item, a subscription record is created and the first period begins immediately.
Check subscription status
POST /api/v1/subscription/status
{
"nft_mint_address": "NFT_MINT_ADDRESS"
}Response
{
"ok": true,
"subscription": {
"status": "active", // "active" or "expired"
"expiresAt": "2026-04-01T00:00:00Z",
"interval": "monthly", // "monthly", "quarterly", "yearly"
"intervalDays": 30,
"collectionId": "col_xxx",
"collectionName": "Premium Access",
"itemId": "item_xxx",
"itemName": "Monthly Plan",
"wallet": "7xKXtg...",
"price": 0.5, // SOL per period
"revenueWallet": "REVENUE_WALLET_ADDRESS"
}
}Renew a subscription
After the user sends a SOL payment to the revenue wallet, confirm the renewal:
POST /api/v1/subscription/renew
{
"nft_mint_address": "NFT_MINT_ADDRESS",
"tx_signature": "5Gz..." // on-chain SOL transfer tx
}Response
{
"ok": true,
"subscription": {
"status": "active",
"expiresAt": "2026-05-01T00:00:00Z", // extended by billing interval
"interval": "monthly"
}
}Renewal logic
- If the subscription is still active, the new period is added to the current expiry date (no overlap lost)
- If the subscription is expired, the new period starts from now
- Duplicate
tx_signaturevalues are rejected to prevent double-counting - The transaction is verified on-chain before the subscription is extended
Expiry handling
Expired subscriptions are detected in real-time during verifyOwnership and verifyTrait calls. When an expired subscription is found, Catalyst automatically updates its status in the database and fires a subscription.expired webhook. No cron job needed.
Dynamic Metadata
Catalyst serves dynamic NFT metadata at /api/metadata/[mintAddress]. For subscription NFTs, traits are computed in real-time:
// GET /api/metadata/NFT_MINT_ADDRESS
{
"name": "Premium Plan #42",
"image": "https://...",
"attributes": [
{ "trait_type": "Role", "value": "Premium" },
{ "trait_type": "Status", "value": "Active" }, // computed
{ "trait_type": "Expires", "value": "2026-04-01" }, // computed
{ "trait_type": "Plan", "value": "Monthly" } // computed
]
}The Status, Expires, and Plan traits update automatically as the subscription state changes. Marketplaces and wallets that read metadata will see the current subscription status.
Embeddable Pages
Drop an iframe into your site — no code needed. Catalyst provides embeddable mint and renewal pages.
Mint embed
Users connect their wallet, pick an item, and mint — all inside the iframe.
<iframe
src="https://catalyst.akca.network/embed/mint/COLLECTION_ID"
width="400" height="600"
style="border: none; border-radius: 12px;"
/>Renewal embed
Shows current subscription status and a "Renew" button. Users pay SOL to extend.
<iframe
src="https://catalyst.akca.network/embed/renew/NFT_MINT_ADDRESS"
width="400" height="500"
style="border: none; border-radius: 12px;"
/>postMessage events
Both embeds communicate via window.postMessage. Listen for events in your parent page:
window.addEventListener('message', (event) => {
if (event.data?.type === 'catalyst:mint:success') {
console.log('Minted!', event.data.mint);
}
if (event.data?.type === 'catalyst:renew:success') {
console.log('Renewed!', event.data.expiresAt);
}
});SDK
npm install akca-catalystWorks with

Server-side (Node.js)
import { AkcaCatalyst } from 'akca-catalyst';
const catalyst = new AkcaCatalyst({
apiKey: process.env.CATALYST_API_KEY,
});
// Verify ownership
const result = await catalyst.verifyOwnership({
wallet: '7xKXtg...',
collectionId: 'col_xxx',
});
console.log(result.hasAccess, result.nfts);
// Get subscription status
const sub = await catalyst.getSubscription({
nftMintAddress: 'NFT_MINT_ADDRESS',
});
console.log(sub.subscription.status, sub.subscription.expiresAt);
// Renew subscription
const renewed = await catalyst.renewSubscription({
nftMintAddress: 'NFT_MINT_ADDRESS',
txSignature: '5Gz...',
});
console.log(renewed.subscription.expiresAt);React — Setup
import { AkcaProvider } from 'akca-catalyst/react';
function App() {
return (
<AkcaProvider config={{ apiKey: 'ak_test_xxx' }}>
<YourApp />
</AkcaProvider>
);
}React — useAkcaGate
Token-gate access to parts of your app.
import { useAkcaGate } from 'akca-catalyst/react';
function ProtectedPage() {
const { hasAccess, isLoading, nfts } = useAkcaGate({
collection: 'col_xxx',
wallet: connectedWallet,
});
if (isLoading) return <Loading />;
if (!hasAccess) return <AccessDenied />;
return <PremiumContent nfts={nfts} />;
}React — useAkcaMint
Mint NFTs from your React app.
import { useAkcaMint } from 'akca-catalyst/react';
function MintButton() {
const { stage, mint, error, startMint, reset } = useAkcaMint({
collectionId: 'col_xxx',
itemId: 'item_xxx', // optional
wallet: connectedWallet,
});
const handleMint = () => startMint(async (transaction) => {
const tx = Transaction.from(Buffer.from(transaction, 'base64'));
const signed = await wallet.signTransaction(tx);
return bs58.encode(signed.signature);
});
return (
<button onClick={handleMint} disabled={stage !== 'idle'}>
{stage === 'idle' ? 'Mint' : stage}
</button>
);
}
// stage: idle → preparing → signing → confirming → success | errorReact — useAkcaSubscription
Monitor and renew NFT subscriptions.
import { useAkcaSubscription } from 'akca-catalyst/react';
function SubscriptionStatus({ nftMint }: { nftMint: string }) {
const {
subscription, // { status, expiresAt, interval, ... } | null
isLoading,
error,
isExpired, // boolean
daysRemaining, // number
renew, // (signTx) => Promise<void>
refetch, // () => void
} = useAkcaSubscription({ nftMintAddress: nftMint });
if (isLoading) return <p>Loading...</p>;
if (!subscription) return <p>No subscription found</p>;
return (
<div>
<p>Status: {subscription.status}</p>
<p>Expires: {subscription.expiresAt}</p>
<p>Days remaining: {daysRemaining}</p>
{isExpired && (
<button onClick={() => renew(async (mint) => {
// Build SOL transfer tx, sign it, return signature
return txSignature;
})}>
Renew for {subscription.price} SOL
</button>
)}
</div>
);
}Next.js Middleware
import { createAkcaMiddleware } from 'akca-catalyst/nextjs';
export default createAkcaMiddleware({
apiKey: process.env.CATALYST_API_KEY!,
protectedPaths: ['/dashboard'],
verify: { mode: 'ownership', collectionId: 'col_xxx' },
getWallet: (req) => req.cookies.get('wallet')?.value || null,
onUnauthorized: (req, reason) =>
NextResponse.redirect(new URL('/login', req.url)),
});Express Middleware
import { akcaAuth } from 'akca-catalyst/express';
app.use('/api/premium', akcaAuth({
apiKey: process.env.CATALYST_API_KEY!,
verify: { mode: 'ownership', collectionId: 'col_xxx' },
getWallet: (req) => req.headers['x-wallet'] || null,
}));
app.get('/api/premium/data', (req, res) => {
const { hasAccess, nfts } = req.catalystGate;
res.json({ hasAccess, nfts });
});Verify Embed Token
const token = await catalyst.verifyToken({ token: 'embed_token_xxx' });
if (token.valid && token.hasAccess) {
console.log('Wallet:', token.wallet);
console.log('NFTs:', token.nfts);
}Webhooks
Receive real-time notifications for verification, mint, and subscription events. Configure webhook URLs in your project settings.
Events
| Event | Trigger |
|---|---|
| verify.success | NFT ownership verified successfully |
| verify.fail | Verification denied — no matching NFT |
| mint.success | NFT minted successfully |
| subscription.created | Subscription starts when recurring NFT is minted |
| subscription.renewed | User pays SOL to extend subscription |
| subscription.expired | Subscription period ends (detected during verify) |
Payload examples
// subscription.created
{
"event": "subscription.created",
"data": {
"subscriptionId": "sub_xxx",
"nftMintAddress": "NFT_MINT",
"wallet": "7xKXtg...",
"collectionId": "col_xxx",
"itemId": "item_xxx",
"interval": "monthly",
"expiresAt": "2026-04-01T00:00:00Z"
},
"timestamp": "2026-03-01T12:00:00Z"
}
// subscription.renewed
{
"event": "subscription.renewed",
"data": {
"subscriptionId": "sub_xxx",
"nftMintAddress": "NFT_MINT",
"wallet": "7xKXtg...",
"expiresAt": "2026-05-01T00:00:00Z"
},
"timestamp": "2026-04-01T12:00:00Z"
}
// subscription.expired
{
"event": "subscription.expired",
"data": {
"subscriptionId": "sub_xxx",
"nftMintAddress": "NFT_MINT",
"wallet": "7xKXtg...",
"expiredAt": "2026-04-01T00:00:00Z"
},
"timestamp": "2026-04-01T12:05:00Z"
}Verifying signatures
Every webhook includes an X-Catalyst-Signature header (HMAC-SHA256). Verify it with the SDK:
import { verifyWebhookSignature, parseWebhookPayload } from 'akca-catalyst/webhook';
app.post('/webhook', express.raw({ type: 'application/json' }), (req, res) => {
const sig = req.headers['x-catalyst-signature'];
if (!verifyWebhookSignature(req.body, sig, process.env.WEBHOOK_SECRET)) {
return res.status(400).send('Invalid signature');
}
const payload = parseWebhookPayload(req.body);
switch (payload.event) {
case 'subscription.expired':
// Revoke access, send renewal email
break;
case 'subscription.renewed':
// Re-enable access
break;
case 'mint.success':
// Welcome new user
break;
}
res.sendStatus(200);
});Error Handling
All error responses follow a consistent format:
{
"ok": false,
"error": {
"code": "INVALID_API_KEY",
"message": "API key is missing or invalid"
}
}| Code | Status | Description |
|---|---|---|
| INVALID_API_KEY | 401 | API key is missing or invalid |
| EXPIRED_API_KEY | 401 | API key has been revoked |
| CORS_REJECTED | 403 | Origin not in allowed CORS list |
| RATE_LIMIT_EXCEEDED | 429 | Too many requests |
| INVALID_WALLET | 400 | Wallet address is not valid |
| INVALID_COLLECTION | 400 | Collection address is not valid |
| COLLECTION_NOT_FOUND | 404 | Collection not found in project |
| VALIDATION_ERROR | 400 | Missing or invalid request parameters |
| INVALID_TOKEN | 400 | Invalid or expired embed token |
| MINT_SOLD_OUT | 400 | Item supply limit reached |
| MINT_TRANSACTION_FAILED | 400 | Mint transaction failed on-chain |
| PROJECT_SUSPENDED | 403 | Project has been suspended |
| RPC_ERROR | 502 | Solana RPC call failed — retry after delay |
SDK error handling
import { AkcaCatalyst, CatalystError } from 'akca-catalyst';
try {
const result = await catalyst.verifyOwnership({ wallet, collectionId });
} catch (err) {
if (err instanceof CatalystError) {
console.log(err.code); // 'INVALID_WALLET'
console.log(err.message); // 'Invalid Solana wallet address'
console.log(err.statusCode); // 400
console.log(err.isRetryable); // false
}
}
// Retryable errors (RPC_ERROR, RATE_LIMIT_EXCEEDED) are auto-retried 2x with exponential backoffRate Limits
| Plan | Verify | Mint |
|---|---|---|
| Free | 100 req/min | 30 req/min |
| Pro | 1,000 req/min | 100 req/min |
| Enterprise | Custom | Custom |
Rate limit headers: X-RateLimit-Remaining, X-RateLimit-Limit, X-RateLimit-Reset