---
title: "Solana NFT Gates"
description: "Metaplex Core gate NFTs, KEYS collection metadata, RING sale currency, GateEscrow staking, and tradeable vendor gate listings"
locale: "en"
---
# Solana NFT Gates

> **Info**
> Use **Founder** / **Developer** tabs in the docs sidebar to filter this page. `audience` frontmatter controls in-page blocks; sidebar visibility is curated in `lib/docs/audience-curated-docs.ts`.

MVP-A ships **Solana Metaplex Core** gate NFTs for membership and vendor unlocks. Buyers pay primary sale with the clone **native token** (RING on this tree), stake into **GateEscrow** (not `NATIVE_NFT_APR`), and `GateResolver.hasFeature` unlocks product surfaces. Tradeable vendor gates can now list in the NFT Exhibition Marketplace, but a bought gate must still be **staked** before features activate.

| Concern | Truth |
|---------|--------|
| Mint SSOT | Metaplex Core (`createCollection` / `create` / `fetchAsset`) |
| Stake SSOT | GateEscrow DB + optional program id — **not** Ring staking APR |
| Primary sale | RING `transferChecked` then mint (`features/nft-gates/purchase.ts`) |
| User surface | `/nft/gates` |
| Secondary surface | `/nft/market`, `/nft/market/[listingId]`, `/nft/market/sell` |
| Admin | `/admin/nft/templates`, `/admin/nft/mint` |
| Subscription provider | `nft_gate` → `nftGateSubscriptionProvider` |
| Schema | `data/migrations/029_nft_gates_schema.sql` |
| Marketplace schema | `data/migrations/031_nft_exhibition_market_schema.sql` |

### For founders

## Why this matters for your clone

Sell access as an on-chain certificate instead of only card or credit rails. Membership gates are **soulbound**; vendor deed / DAGI / license gates are **tradeable** in the NFT Exhibition Marketplace. Price changes mint a **new** active template asset — sold assets are never mutated.

### Operator journey

1. Create the Metaplex Core **collection** once (admin mint console).
2. Set `nft.collectionMint` in `ring-config.json` (devnet example already set on this tree).
3. Open **Mint / activate + art** → pick one of four ImageConductor previews → mint into the collection.
4. Members buy on `/nft/gates`; membership slugs auto-stake and unlock member features.
5. Tradeable vendor gates can be listed on `/nft/market/sell` only when they are owned, verified, non-soulbound, and not staked.
6. Before mainnet: move collection **update authority** and marketplace authorities to Squads (see `solana/SECURITY.md`).

### Explorer Symbol: KEYS (not RING)

Solana Explorer **Symbol** comes from off-chain collection metadata JSON at the collection `uri` — Metaplex Core has no on-chain symbol. Ring Platform uses **`KEYS`** for the gate NFT family (`Ringdom Keys Collection`). **RING** remains the SPL **sale currency** only.

Live family JSON (devnet on-chain uri): gist `collection.json` with `"symbol":"KEYS"`. App SSOT: `public/nft/gates/collection.json`. After deploy, stable path `/nft/gates/collection.json` rewrites to `/api/nft/gates/collection`.

### Typical scenarios

- Lifetime membership NFT as a permanent member seal (stake required).
- Vendor DAGI key staked before agent activation (`hasFeature('vendor.dagi')`).
- Store deed / DAGI / annual / quarterly vendor gates listed for RING resale when unstaked.
- Bought vendor gate staked after purchase before product access activates.

  
- **[SubscriptionConductor](/docs/features/subscriptions.md)** — How `nft_gate` sits beside credit, WayForPay, Stripe, and native token.

  
- **[Wallet](/docs/features/wallet.md)** — Custodial Solana wallets and RING transfers that fund primary sale.

  
- **[RingFileBase](/docs/integrations/ring-filebase.md)** — Where generated NFT art lands after ImageConductor.

  
- **[NFT Marketplace](/docs/features/nft-market.md)** — Shipped Exhibition Marketplace for eligible vendor KEYS gates; audited on-chain GateMarket remains future work.

> **Warning**
> A marketplace purchase transfers ownership only. It does **not** grant features until the buyer stakes the gate. An active listing means `hasFeature:false`; an active stake means `canList:false`.

### For developers

## Architecture

```mermaid
sequenceDiagram
  participant User
  participant Action as nft-gates actions
  participant Pay as purchaseGateNft
  participant Core as Metaplex Core
  participant Escrow as GateEscrow
  participant Resolve as GateResolver
  participant Market as NFT Market

  User->>Action: purchaseGateAction(slug)
  Action->>Pay: transferChecked RING
  Pay->>Core: mintGateAsset
  Core-->>Pay: asset + signature
  Pay->>Escrow: auto-stake membership slugs
  User->>Resolve: hasFeature(feature)
  Resolve->>Resolve: cache + stakes + fetchAsset
  User->>Market: list unstaked tradeable vendor gate
```

### Module map

| Path | Role |
|------|------|
| `features/nft-gates/umi-client.ts` | Sponsor umi from `SOLANA_FEE_PAYER_PRIVATE_KEY` |
| `features/nft-gates/metaplex-core-onchain.ts` | `createCollection`, mint, `fetchAsset`, soulbound freeze |
| `features/nft-gates/metaplex-core.ts` | On-chain vs ledger-dev adapter |
| `features/nft-gates/purchase.ts` | RING pay → mint → ownership |
| `features/nft-gates/gate-escrow.ts` | Stake / unstake + entitlement invalidation |
| `features/nft-gates/gate-resolver.ts` | `hasFeature` (cache → stakes → RPC) |
| `features/nft-gates/admin-mint.ts` | Collection create + template activate |
| `features/nft-gates/art-prompt.ts` | Prompt vars + favicon data URI |
| `features/nft-market/listing-policy.ts` | Tradeable SKU, soulbound, stake, duplicate listing checks |
| `features/nft-market/services/listing-service.ts` | NFT Exhibition listing lifecycle |
| `features/nft-market/services/solana-market-client.ts` | Ledger-dev marketplace adapter; Anchor branch blocked |
| `features/admin/nft/gate-art-preview-modal.tsx` | 4-up preview fs-modal |
| `app/_actions/nft-gates.ts` | Server actions |
| `app/_actions/nft-market.ts` | List, cancel, purchase marketplace actions |
| `lib/payments/subscription/providers/nft-gate-subscription.ts` | `nft_gate` provider |

### Config (`ring-config.json` → `nft`)

Verified keys: `enabled`, `collectionMint`, `collectionUri`, `collectionSymbol` (`KEYS`), `sponsorFeePayer`, `marketplaceEnabled`, `marketplaceFeeRecipient`, `marketplaceFeeBps`, `gateEscrowProgramId`, `gateMarketProgramId`, `templates[]`.

Off-chain family metadata: `public/nft/gates/collection.json` (`symbol: KEYS`). Admin **Point collection URI → KEYS JSON** runs Metaplex Core `updateCollection` when the sponsor still holds update authority.

Seven templates (slugs): `one-month-membership`, `annual-membership`, `lifetime-membership`, `vendor-store-deed`, `vendor-dagi-key`, `vendor-annual-store-license`, `vendor-quarterly-store-license`.

`payment.gateways.nft_gate.enabled` is **true** on this tree. `nft_gate` remains listed under `payment.futureMethods` (not yet in `supportedMethods`), so membership PaymentModal may still tag it as coming-soon while `/nft/gates` and the provider module are live.

Tradeable marketplace slugs are intentionally narrower: `vendor-store-deed`, `vendor-dagi-key`, `vendor-annual-store-license`, and `vendor-quarterly-store-license`. Membership slugs remain soulbound and unlistable.

### Server actions

| Action | Purpose |
|--------|---------|
| `purchaseGateAction` | Buy gate NFT with RING |
| `stakeGateAction` / `unstakeGateAction` | GateEscrow |
| `hasGateFeatureAction` | Entitlement check |
| `previewGateArtAction` | ImageConductor `n: 4` + favicon reference |
| `adminActivateGateTemplateAction` | Mint/activate with optional `imageUri` |
| `adminCreateGateCollectionAction` | Create Core collection |

### Admin art preview flow

Open `/admin/nft/templates` or `/admin/nft/mint` and click **Mint / activate + art**.

`previewGateArtAction` interpolates `$projectName`, `$activeColor`, `$secondaryColor`, `$projectColor1`, `$projectColor2`, `$organization`, then calls `ImageConductor.generate` with `n: 4` and favicon `referenceImages`.

Fullscreen modal shows a 2×2 grid, collapsible **Generation prompt** textarea, and **Regenerate** (session-only prompt edits — not written to ring-config).

**Mint with selected art** calls `adminActivateGateTemplateAction({ imageUri, regenerateArt: false })` → Metaplex Core mint → `nft_gates` edition row + **db() active-template pointer** (`nft_active_{slug}`). Runtime SSOT is `listNftGateTemplatesResolved()` (ring-config defaults + DB overlay). `ring-config.json` is never rewritten in k8s.

### Image + storage wiring

| Piece | Path / env |
|-------|------------|
| Conductor | `lib/images/conductor/image-conductor.ts` |
| xAI provider | `lib/images/providers/xai.provider.ts` — `/images/generations` or `/images/edits` |
| Favicon PNG | `public/images/favicon.png` → `data:image/png;base64,…` |
| Persist | `file().upload` → `RingBaseAdapter` when `NEXT_PUBLIC_STORAGE_PROVIDER=ring_filebase` |
| Local→prod API | Port-forward `ring-filebase-api` to `127.0.0.1:18080` (see [RingFileBase](/docs/integrations/ring-filebase.md)) |

{`SOLANA_RPC_URL=https://api.devnet.solana.com
SOLANA_FEE_PAYER_PRIVATE_KEY=
XAI_API_KEY=
IMAGE_GEN_PROVIDER=xai
NEXT_PUBLIC_STORAGE_PROVIDER=ring_filebase
RINGBASE_API_URL=http://127.0.0.1:18080
NEXT_PUBLIC_RINGBASE_API_URL=http://127.0.0.1:18080
RINGBASE_PUBLIC_URL=https://cdn.ring-platform.org
RINGBASE_API_TOKEN=
# Optional ledger-dev mint without collectionMint:
# ALLOW_LEDGER_NFT_MINT=1`}

> **Warning**
> Do **not** set `RINGBASE_API_URL=https://api.ring-platform.org` — that host is the Next.js app ingress, not `ring-filebase-api`. Uploads return HTML and surface as HTTP 500 in `RingBaseAdapter`.

### Persistence collections

Migration `029_nft_gates_schema.sql` creates JSONB collections: `nft_gates`, `nft_stakes`, `nft_entitlement_cache`, `nft_ownership` (plus related indexes). Cron: `GET /api/cron/nft-gate-expiry` → `runNftGateExpiry` (daily 4am schedule in SubscriptionConductor docs).

Migration `031_nft_exhibition_market_schema.sql` adds secondary-market persistence: `nft_listings`, `nft_market_sales`, and `nft_market_collections`.

### Ledger-dev vs on-chain

If `nft.collectionMint` is empty, `metaplex-core.ts` issues ledger-dev asset ids when not production, or when `ALLOW_LEDGER_NFT_MINT=1`. Production refuses ledger mint without that flag.

If `nft.gateMarketProgramId` is empty, `SolanaMarketClient` uses the shipped ledger-dev market adapter. If a program id is set in this build, list/cancel/buy throw because audited Anchor GateMarket CPI is not implemented yet.

Anchor scaffolds exist at `solana/programs/{gate-escrow,gate-market}`. `solana/SECURITY.md` keeps both programs out of production until audit, Squads authority control, pinned IDs/IDLs, and invariant tests are complete.

## Backlog (not shipped)

## Related documentation

  
- **[SubscriptionConductor](/docs/features/subscriptions.md)** — Provider registry including `nft_gate`.

  
- **[Generative images](/docs/development/generative-images.md)** — ImageConductor providers and ring-filebase persistence.

  
- **[RingFileBase](/docs/integrations/ring-filebase.md)** — Upload API vs CDN read path.

  
- **[Staking](/docs/features/staking.md)** — APR pools — do not confuse with GateEscrow.

  
- **[NFT Exhibition Marketplace](/docs/features/nft-market.md)** — Secondary trading for eligible vendor KEYS gates.
