DEV Community

Cover image for Token-Gated API Access with Wallet Auth: JWT Bearer Token Tutorial
Douglas Borthwick
Douglas Borthwick

Posted on • Originally published at insumermodel.com

Token-Gated API Access with Wallet Auth: JWT Bearer Token Tutorial

You have an API. You want to restrict access to wallets that hold a specific token. OAuth tells you who someone is. Wallet Auth tells you what they hold. This tutorial shows the complete flow: verify token holdings via InsumerAPI, receive a signed JWT, and validate it server-side before granting access. No wallet connection popup, no browser extension required.

The flow

Token-gated API access with JWT bearer tokens works in six steps:

        - Client sends their wallet address and desired conditions to your backend.
        - Your backend calls `POST /v1/attest` with `format: "jwt"`.
        - InsumerAPI verifies holdings on-chain and returns an ECDSA-signed JWT.
        - Your backend returns the JWT to the client.
        - Client includes the JWT as a Bearer token on subsequent API calls.
        - Your API validates the JWT against InsumerAPI's public JWKS.
Enter fullscreen mode Exit fullscreen mode

The JWT expires after 30 minutes, matching the attestation lifetime. During that window, your server validates it locally. No additional API calls required.

Step 1: Get the JWT

Call POST /v1/attest with format: "jwt" included in the request body. This tells the API to return a signed JWT alongside the standard attestation and signature.

curl -X POST https://api.insumermodel.com/v1/attest \
  -H "X-API-Key: insr_live_YOUR_KEY_HERE" \
  -H "Content-Type: application/json" \
  -d '{
    "wallet": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
    "format": "jwt",
    "conditions": [
      {
        "type": "token_balance",
        "contractAddress": "0xA0b86991c6218b36c1d19D4a2e9Eb0cE3606eB48",
        "chainId": 1,
        "threshold": 1000,
        "decimals": 6,
        "label": "USDC >= 1000"
      }
    ]
  }'
Enter fullscreen mode Exit fullscreen mode

When format: "jwt" is set, the response includes a jwt field alongside the standard attestation and sig fields. The JWT contains the same verification result in a standard, portable format that any JWT library can validate.

What the JWT contains

The decoded JWT payload looks like this:

{
  "iss": "https://api.insumermodel.com",
  "sub": "0xd8dA6BF26964aF9D7eEd9e03E53415D37aA96045",
  "jti": "ATST-A7C3E1B2D4F56789",
  "iat": 1741258800,
  "exp": 1741260600,
  "pass": true,
  "results": [
    {
      "condition": 0,
      "label": "USDC >= 1000",
      "type": "token_balance",
      "chainId": 1,
      "met": true,
      "evaluatedCondition": { "type": "token_balance", "operator": "gte", "threshold": 1000 },
      "conditionHash": "0x3a7f...",
      "blockNumber": "0x13a1b40",
      "blockTimestamp": "2026-03-06T10:00:00.000Z"
    }
  ],
  "conditionHash": ["0x3a7f..."],
  "blockNumber": "0x13a1b40",
  "blockTimestamp": "2026-03-06T10:00:00.000Z"
}
Enter fullscreen mode Exit fullscreen mode

Key claims:

        - `sub` is the wallet address that was verified.
        - `pass` indicates whether all conditions were met.
        - `exp` is 30 minutes from issuance. After that, the token is invalid.
        - `results` lists each condition and whether it was met individually.
        - `blockNumber` and `blockTimestamp` pin the verification to a specific on-chain state.
Enter fullscreen mode Exit fullscreen mode

The JWT is signed with ES256 (ECDSA P-256). You verify it against the public key published at the JWKS endpoint.

Step 2: Validate the JWT (Node.js)

Use the jose library to verify the JWT against the InsumerAPI JWKS endpoint. This fetches the public key and validates the signature, issuer, and expiration in one call.

import { createRemoteJWKSet, jwtVerify } from "jose";

const JWKS = createRemoteJWKSet(
  new URL("https://insumermodel.com/.well-known/jwks.json")
);

async function validateWalletAuth(token) {
  const { payload } = await jwtVerify(token, JWKS, {
    issuer: "https://api.insumermodel.com",
  });

  if (!payload.pass) {
    throw new Error("Token conditions not met");
  }

  return payload;
}
Enter fullscreen mode Exit fullscreen mode

The JWKS is cached automatically by the jose library. After the first fetch, subsequent validations happen locally with no network call.

Step 2 (alternative): Validate the JWT (Python)

In Python, use PyJWT with the public key from the JWKS endpoint:

import jwt
import requests

jwks_url = "https://insumermodel.com/.well-known/jwks.json"
jwks = requests.get(jwks_url).json()

public_key = jwt.algorithms.ECAlgorithm.from_jwk(jwks["keys"][0])

payload = jwt.decode(
    token,
    public_key,
    algorithms=["ES256"],
    issuer="https://api.insumermodel.com",
)

if not payload["pass"]:
    raise Exception("Token conditions not met")
Enter fullscreen mode Exit fullscreen mode

Cache the JWKS response in production. The key rotates infrequently, so a 24-hour cache is safe.

Step 3: Protect your endpoints

With the validation function in place, add middleware to your API routes. Here is an Express.js example that gates the /api/premium path on wallet holdings:

app.use("/api/premium", async (req, res, next) => {
  const auth = req.headers.authorization;
  if (!auth || !auth.startsWith("Bearer ")) {
    return res.status(401).json({ error: "Missing token" });
  }

  try {
    const payload = await validateWalletAuth(auth.split(" ")[1]);
    req.wallet = payload.sub;
    next();
  } catch (err) {
    return res.status(403).json({ error: "Invalid or expired wallet auth" });
  }
});
Enter fullscreen mode Exit fullscreen mode

This pattern works with any web framework. The JWT is a standard ES256 token. Flask, Django, Fastify, Go, Rust, any language with a JWT library can validate it. The only requirement is fetching the public key from the JWKS endpoint.

Why not just call the API on every request?

JWT caching. The JWT is valid for 30 minutes. During that window, your server validates it locally against the JWKS (cached). No API call needed per request. This means one verification credit covers 30 minutes of access, regardless of how many API calls the user makes.

This is the same pattern as OAuth access tokens. You trade a single authentication event for a time-limited bearer token that can be validated cheaply on every request.

Works with existing infrastructure

Any API gateway that supports JWT validation works out of the box. Configure the JWKS URL and issuer, and the gateway handles validation with no custom code needed.

        - **Kong:** Use the `jwt` plugin. Point it at the JWKS URL and set the allowed issuer.
        - **Nginx:** Use the `auth_jwt` directive with the JWKS endpoint.
        - **AWS API Gateway:** Create a JWT authorizer with the JWKS URL and issuer.
        - **Cloudflare Access:** Configure a custom JWKS policy with the InsumerAPI issuer.
Enter fullscreen mode Exit fullscreen mode

No SDK, no library, no vendor lock-in. Standard JWT infrastructure, powered by on-chain verification.

, before CTA) -->

    Share this article

        [

            Share on X
        ](#)
        [

            LinkedIn
        ](#)
        [

            Reddit
        ](#)


            Discord



            Copy Link
Enter fullscreen mode Exit fullscreen mode

InsumerAPI is free to start. Get an API key and try it. View API Docs

Top comments (0)