<!-- Markdown copy of https://skyemeta.com/skyegate/vercel/ for agents. -->

> npm install @skyemeta/skyegate to show content only to qualifying wallets in Next.js, checked on the server. 34 chains, one Skye License for both stacks.
>
> Page: https://skyemeta.com/skyegate/vercel/ · Index for agents: https://skyemeta.com/llms.txt · Build on the API underneath: https://insumermodel.com/developers/

If their wallet meets the condition, they see the content. If it doesn't, they don't.

# SkyeGate Pro: token gating for *Next.js and Vercel.*

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**.

npm install @skyemeta/skyegate

[npm](https://www.npmjs.com/package/@skyemeta/skyegate) [GitHub (MIT)](https://github.com/skyemeta/skyegate-sdk) [Next.js starter](https://github.com/skyemeta/skyegate-nextjs-starter)

Quick start Get a license key [See it live](https://skyemeta.com/skyegate/demo/)

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](https://skyemeta.com/skyegate/).

Powered by [The Insumer Model™](https://insumermodel.com) condition-based access API.

## Five Minutes to a Gate a Copied Address Can't Open

Checked on your server, the wallet proves it's theirs, and every answer comes back signed. 34 chains, four condition types, three steps.

### 1. Install

```bash
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.

### 2. Set your license key

```
# .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.

### 3. Drop in `<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.

app/page.tsx

```
'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>
  );
}
```

app/api/gated/route.ts

```javascript
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.',
  });
}
```

[Clone the full starter app →](https://github.com/skyemeta/skyegate-nextjs-starter)

## Four Condition Types

Same vocabulary as the WordPress plugin. Stack up to 10 per gate; `pass=true` requires all of them to be met.

### Token balance

ERC-20, SPL, native ETH/SOL/MATIC, plus Tron & Sui. 34 chains.

```json
{
  type: 'token_balance',
  contractAddress: '0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48',
  chainId: 1,
  threshold: '100',    // decimal string: keeps full precision; decimals are read from the chain
}
```

### NFT ownership

ERC-721 and Solana NFTs. Match a whole collection: its contract on EVM chains, its verified collection address on Solana.

```json
{
  type: 'nft_ownership',
  contractAddress: '0x57f1887a8BF19b14fC0dF6Fd9B2acc9Af147eA85',
  chainId: 1,          // ENS .eth
}
```

### EAS attestation

Ethereum Attestation Service templates: Coinbase Verified, Gitcoin Passport, more.

```json
{
  type: 'eas_attestation',
  template: 'coinbase_verified_account',
}
```

### Farcaster identity

Wallet linked to a Farcaster account (FID present). New in Pro 1.4.0.

```json
{
  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.

## API at a Glance

### `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.

### If you cache the result, key it on the JWT

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.

[Full API reference on GitHub →](https://github.com/skyemeta/skyegate-sdk#api)

## How It Works

```
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.**

## One License, One Site, with Sensible Defaults

### Auto-bind on first production use

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`.

### Dev / preview hosts skip the bind

`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](https://skyemeta.com/account/).

## WordPress Plugin vs Vercel SDK

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

## Get a Key

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.

[$350/yr, Save 40%](https://buy.stripe.com/8x26oA9F6eWAeAC7S804805) [or subscribe monthly](https://buy.stripe.com/eVqbIU18A6q43VY7S804800)

$49 a month or $350 a year. Renews automatically until you cancel. Cancel anytime from your [account page](https://skyemeta.com/account/); you keep access to the end of the period you paid for.

I agree to the [Terms of Service](https://skyemeta.com/terms/).

Comparison, FAQ, and the WordPress option at [skyemeta.com/skyegate](https://skyemeta.com/skyegate/).

## Same Engine. Different Use Cases.

### SkyeWoo

Gate WooCommerce products and apply wallet-verified discounts at checkout. No coupon codes. Discounts tied to blockchain-verified holdings.

[SkyeWoo](https://skyemeta.com/skyewoo/)

### SkyeSkiff

Wallet-qualified sessions for AI agents. Both agents verify wallet conditions before exchanging data. Cryptographically verified. Crypto native.

[SkyeSkiff](https://skyemeta.com/skyeskiff/)

## Ready to gate some content?

Bring your own wallet stack. We bring the gating layer.

Get a SkyeGate Pro license [Clone the starter](https://github.com/skyemeta/skyegate-nextjs-starter)
