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, 37 blockchains, server-side JWT validation, same SKYE license key as the WordPress plugin.
Powered by The Insumer Model™ condition-based access API.
Three steps. No configuration. No magic.
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
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 EIP-191 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, XRPL trust line tokens, plus Bitcoin, Tron, Stellar & Sui. 37 chains.
{
type: 'token_balance',
contractAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
chainId: 1,
threshold: '100', // decimal string — keeps full precision
decimals: 6, // USDC
}
ERC-721, ERC-1155, Solana SPL NFTs. Match by collection contract.
{
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), 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 wallets in this wave.
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 EVM calls without it). Optional: walletType ('evm' | 'solana'), 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. 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 | 37 | 37 |
| 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.
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.