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 days
  • quarterly — 90 days
  • yearly — 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_signature values 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-catalyst

Works with

React
React
Hooks + Provider
Next.js
Next.js
Middleware
Express
Express
Middleware
Node.js
Node.js
Server SDK
TypeScript
TypeScript
Full types
Solana
Solana
On-chain verify
ESM + CJSTree-shakeable0 runtime dependenciesAuto-retry with backoffWebhook signature verification

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 | error

React — 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

EventTrigger
verify.successNFT ownership verified successfully
verify.failVerification denied — no matching NFT
mint.successNFT minted successfully
subscription.createdSubscription starts when recurring NFT is minted
subscription.renewedUser pays SOL to extend subscription
subscription.expiredSubscription 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"
  }
}
CodeStatusDescription
INVALID_API_KEY401API key is missing or invalid
EXPIRED_API_KEY401API key has been revoked
CORS_REJECTED403Origin not in allowed CORS list
RATE_LIMIT_EXCEEDED429Too many requests
INVALID_WALLET400Wallet address is not valid
INVALID_COLLECTION400Collection address is not valid
COLLECTION_NOT_FOUND404Collection not found in project
VALIDATION_ERROR400Missing or invalid request parameters
INVALID_TOKEN400Invalid or expired embed token
MINT_SOLD_OUT400Item supply limit reached
MINT_TRANSACTION_FAILED400Mint transaction failed on-chain
PROJECT_SUSPENDED403Project has been suspended
RPC_ERROR502Solana 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 backoff

Rate Limits

PlanVerifyMint
Free100 req/min30 req/min
Pro1,000 req/min100 req/min
EnterpriseCustomCustom

Rate limit headers: X-RateLimit-Remaining, X-RateLimit-Limit, X-RateLimit-Reset