If their wallet meets the condition, they see the content. If it doesn't, they don't.
Drop a wallet gate into any Next.js app. Bring your own wallet stack: wagmi, RainbowKit, ConnectKit, Privy, whatever you already use. @skyemeta/skyegate handles the gating layer. Four condition types, 34 blockchains, server-side JWT validation, same SKYE license key as the WordPress plugin.
The SDK is free on npm. Calls need a Skye License key: $49 a month or $350 a year, emailed to you the moment checkout finishes. Prefer WordPress? Same key works there.
Powered by The Insumer Model™ condition-based access API.
Checked on your server, the wallet proves it's theirs, and every answer comes back signed. 34 chains, four condition types, three steps.
npm install @skyemeta/skyegate
Peer deps: react >=18 (only if you import the React entry) and @noble/post-quantum (optional, to verify the post-quantum companion). Runtime dep: jose. Zero wallet libs. You use your own.
# .env.local
NEXT_PUBLIC_SKYE_LICENSE_KEY=SKYE-XXXX-XXXX-XXXX
No key yet? Get one here ($49/mo or $350/yr). Pay on Stripe and the key arrives by email from noreply@skyemeta.com a moment later.
Your SKYE license key is safe to expose. The proxy auto-binds it to your production domain on first use; other apexes are rejected.
<GatedContent />Wrap whatever you want gated. Prove wallet ownership once when the wallet connects (a free signature: EIP-191 for EVM; Solana, Sui and Tron wallets sign with their own message signature; the proxy requires it for licensed calls), then the component handles the verify call, status transitions, and conditional rendering. Server-side validation goes in a route handler.
'use client';
import { useEffect, useState } from 'react';
import { useAccount } from 'wagmi';
import { proveWalletOwnership } from '@skyemeta/skyegate';
import { GatedContent } from '@skyemeta/skyegate/react';
export default function Page() {
const { address } = useAccount();
// One free signature proves the visitor controls the
// address; the token covers the whole visit.
const [proof, setProof] = useState<string>();
useEffect(() => {
setProof(undefined);
if (!address) return;
proveWalletOwnership({ address, provider: window.ethereum })
.then((r) => setProof(r.proofToken ?? undefined));
}, [address]);
return (
<GatedContent
address={address}
walletProof={proof}
enabled={!!proof}
conditions={[{ type: 'farcaster_id' }]}
licenseKey={process.env.NEXT_PUBLIC_SKYE_LICENSE_KEY!}
loading={<p>Verifying…</p>}
fallback={<p>Connect a Farcaster-linked wallet.</p>}
onPass={async (jwt, pqJwt) => {
const res = await fetch('/api/gated', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ jwt, pqJwt }),
});
// render res.json().secret …
}}
>
<p>Welcome, Farcaster user.</p>
</GatedContent>
);
}
import { validateContentToken } from '@skyemeta/skyegate';
export const runtime = 'nodejs';
export async function POST(req: Request) {
const { jwt, pqJwt } = await req.json();
const result = await validateContentToken(jwt, {
pqJwt, // post-quantum companion; reported as result.pq
expectedConditions: [{ type: 'farcaster_id' }],
});
if (!result.pass) {
return Response.json(
{ error: result.error },
{ status: 403 },
);
}
return Response.json({
secret: 'Real gated content here.',
});
}
Same vocabulary as the WordPress plugin. Stack up to 10 per gate; pass=true requires all of them to be met.
ERC-20, SPL, native ETH/SOL/MATIC, plus Tron & Sui. 34 chains.
{
type: 'token_balance',
contractAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
chainId: 1,
threshold: '100', // decimal string: keeps full precision; decimals are read from the chain
}
ERC-721 and Solana NFTs. Match a whole collection: its contract on EVM chains, its verified collection address on Solana.
{
type: 'nft_ownership',
contractAddress: '0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85',
chainId: 1, // ENS .eth
}
Ethereum Attestation Service templates: Coinbase Verified, Gitcoin Passport, more.
{
type: 'eas_attestation',
template: 'coinbase_verified_account',
}
Wallet linked to a Farcaster account (FID present). New in Pro 1.4.0.
{
type: 'farcaster_id',
}
Match the same conditions in validateContentToken's expectedConditions on the server side. That's how a JWT earned for one route can't unlock another.
proveWalletOwnership(params)Promise<{ proofToken, expiresInSec?, error? }>
Proves the person present controls the wallet, not just that an address was supplied. Requests a one-time challenge, has the wallet sign it (EIP-191 personal_sign: free, gasless, no transaction; smart wallets verify on-chain via EIP-1271/6492; Solana, Sui and Tron wallets sign with their own message signature), and returns a session-scoped proof token. The signature goes from the visitor's browser to the proof endpoint directly and never touches your server.
Required params: address, plus provider (e.g. window.ethereum) or a signMessage callback (wagmi's signMessageAsync, viem wallet clients, Privy). Prove once per visit; pass the token as walletProof below. EVM, Solana, Sui and Tron wallets. For a non-EVM wallet, pass walletType and a signMessage callback that returns the wallet's own signature.
verifyConditions(params)Promise<{ pass, jwt, pqJwt, raw, error? }>
Low-level imperative call. The React hook + component use this internally; reach for it in non-React contexts (server actions, API routes, Node scripts).
Required params: address, conditions[], licenseKey, and walletProof (from proveWalletOwnership; the proxy rejects licensed calls without it). Optional: walletType ('evm' | 'solana' | 'sui' | 'tron'), endpoint, domain.
validateContentToken(jwt, options?)Promise<{ valid, pass, payload?, pq?, error? }>
Server-side JWT validation. Verifies the ECDSA P-256 signature against InsumerAPI's JWKS, checks issuer + expiry via jose, and (if you pass expectedConditions) confirms the signed conditions match what the route requires.
Since September 2026 InsumerAPI returns an ML-DSA-65 post-quantum companion (pqJwt) beside the ES256 jwt. Pass it as options.pqJwt and the result carries pq: verified, refuted, absent, or unverifiable, on every outcome once the JWT verified. A refuted companion always rejects; an absent or unverifiable one rejects only past your own pqRequiredFrom cutoff. The companion is bound to the JWT by the full claim set, every claim compared as parsed JSON, so verified vouches for the results that expectedConditions is matched against, not only for pass. Install the optional peer @noble/post-quantum to verify companions.
Trust the math, not a company, including us. Every result is independently verifiable by any third party using the public JWKS.
Nothing here caches a verdict: every call re-verifies the signature, so a route that follows the examples above is checking cryptographic proof on every request. Caching that away is a reasonable optimisation, and it is where this goes wrong.
Key any cache on something only the person who passed could produce : the JWT itself, or a session you issued them. Never on the wallet address, and never on a content or product id. Both are public. A cached “yes” filed under a public value can be claimed by anyone who knows it, and because a cache hit returns before your validation runs, the signature you were relying on is never checked.
A wallet address feels like an identifier for a person. It is not a secret, it is a name anyone can read off a block explorer, and anyone can send you one.
useSkyeGate(options){ status, pass, jwt, pqJwt, error, refetch }
React hook. Same options as verifyConditions (including walletProof), plus enabled to gate the call. status cycles through 'idle' → 'verifying' → 'pass' | 'fail' | 'error'.
<GatedContent />JSX.Element
Declarative wrapper. Renders children on pass, fallback otherwise, loading while verifying. Optional onPass(jwt, pqJwt?) callback fires once when the gate first passes; forward both tokens to your server. Use it to fetch gated content from your server.
your Next.js app
↓ proveWalletOwnership(address, provider)
↓
skyemeta.com/api/wallet-proof ← one-time challenge; the wallet signs
↓ (EIP-191, free) and a session-scoped
↓ proof token comes back
↓ verifyConditions(address, conditions, licenseKey, walletProof)
↓
skyemeta.com/api/verify ← SkyeMeta proxy validates SKYE key + domain
↓ + the ownership proof for the address
↓
api.insumermodel.com ← InsumerAPI returns a signed boolean
↓ (no balances exposed)
JWT signed with ECDSA P-256
↓ POSTed to your server
↓
validateContentToken(jwt, { pqJwt })
↓ ← jose + JWKS, signature + issuer + expiry +
↓ condition match + post-quantum companion (pq)
gated content delivered
The wallet's actual balances never reach your server or your customers. Only the signed yes-or-no on whether the condition was met. Trust the math, not a company, including us.
Your license has no domain at first. The first call from a real apex (e.g. myapp.com) auto-binds the license to that apex. Subsequent calls from myapp.com and any subdomain (staging.myapp.com, preview.myapp.com, app.myapp.com) are accepted.
Calls from a different apex are rejected with License bound to myapp.com.
localhost, 127.0.0.1, *.vercel.app, and *.local verify normally but never bind the license. Vercel preview deploys, local dev, and .local hostnames don't burn your production bind.
Treat your license key as you would any per-domain credential. To move a key to a different production apex, use the self-serve Move License to New Domain flow at skyemeta.com/account.
| WordPress plugin | Vercel SDK (this page) | |
|---|---|---|
| Distribution | Direct .zip from skyemeta.com | npm install @skyemeta/skyegate |
| Stack | PHP + bundled JS | TypeScript / React |
| Wallet connect | Bundled, multi-wallet | Bring your own (wagmi, RainbowKit, ConnectKit, Privy) |
| License key | Same SKYE key | Same SKYE key |
| Conditions | All 4 types, up to 10 stacked | All 4 types, up to 10 stacked |
| Chains | 34 | 34 |
| Source | Closed (proprietary plugin) | Open (MIT, GitHub) |
One Pro license. Pick the channel that matches your stack, or use both, on the same key, no extra cost.
SkyeGate Pro is $49/mo or $350/yr per domain. Same key works on the WordPress plugin and this SDK.
How it works: 1. Tick the box and pay on Stripe. 2. Your SKYE- key is emailed to you from noreply@skyemeta.com. 3. Paste it into .env.local as NEXT_PUBLIC_SKYE_LICENSE_KEY and deploy.
$49 a month or $350 a year. Renews automatically until you cancel. Cancel anytime from your account page; you keep access to the end of the period you paid for.
Comparison, FAQ, and the WordPress option at skyemeta.com/skyegate.
Gate WooCommerce products and apply wallet-verified discounts at checkout. No coupon codes. Discounts tied to blockchain-verified holdings.
Wallet-qualified sessions for AI agents. Both agents verify wallet conditions before exchanging data. Cryptographically verified. Crypto native.