New:Microsoft Teams Notifications Are Now Available in Socket.Learn more
Get Started

@aifinpay/gate

Package Overview
Dependencies
Maintainers
2
Versions
10
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@aifinpay/gate

Merchant-side AIFP-1 paywall for your own API — 402 challenge, local receipt verification, billing-unit quota metering.

latest
Source
npmnpm
Version
0.3.4
Version published
Weekly downloads
168
102.41%
Maintainers
2
Weekly downloads
 
Created
Source

@aifinpay/gate

Put your own API behind AIFP-1, in your own process. No proxy, no traffic through us, no round-trip per request.

An AI agent calls your endpoint. It has no account with you and no API key. It gets a 402 that tells it exactly what the call costs and how to buy a batch, pays on-chain from its own wallet, and retries with a signed receipt. Your gate verifies that receipt locally and counts the prepaid units down.

npm install @aifinpay/gate

Requires Node 22+. Express is an optional peer (4 or 5); the core has no framework dependency at all.

1. Sixty seconds

import express from "express";
import { aifpGate } from "@aifinpay/gate";

const app = express();

app.get(
  "/api/search",
  aifpGate({
    merchantId: process.env.AIFP_MERCHANT_ID, // "mrch_…"
    resource: "/api/search",
    tier: "complex", // standard | complex | premium
  }),
  (req, res) => {
    res.json({ results: [], billed_units: req.aifp.weight });
  }
);

app.listen(3000);

That is a working paywall. Before you ship it, read §3 — the default quota store is per-process, and that is the one thing in this package that quietly costs money if you get it wrong.

1b. Next.js, and the two ways it silently earns you nothing

Next.js runs middleware only on paths matched by config.matcher, and the gate charges every request unless you tell it not to. Both defaults are reasonable on their own and together they produce the two failure modes we see most often in real integrations — each of which leaves the dashboard showing paywall_enabled: true while the truth is very different.

aifpGate is Express middleware and does not run in middleware.ts. Use createGate and turn its decision into a NextResponse yourself:

// middleware.ts
import { NextResponse, type NextRequest } from "next/server";
import { createGate, knownAiAgent } from "@aifinpay/gate";

const gate = createGate({
  merchantId: process.env.AIFP_MERCHANT_ID!,
  registry,
  store, // shared, e.g. redisStore(redis) — see §3
  shouldCharge: knownAiAgent, // ← see below
});

export async function middleware(req: NextRequest) {
  const result = await gate({
    path: req.nextUrl.pathname,
    header: (name) => req.headers.get(name) ?? undefined,
  });
  const res = result.ok ? NextResponse.next() : NextResponse.json(result.body, { status: result.status });
  // On BOTH branches. A paid 200 carries AIFP-Quota-Remaining here, and an
  // agent that cannot see it cannot tell a working batch from an empty one.
  for (const [name, value] of Object.entries(result.headers)) res.headers.set(name, value);
  return res;
}

export const config = {
  matcher: [
    "/api/:path*",
    // Every PAGE you registered has to be here too. A page resource that the
    // matcher does not cover is never seen by the gate, and serves free.
    "/",
    "/movies/:path*",
  ],
};

Without the matcher entry, the middleware never runs on that path. The resource is registered, priced, and shown as enabled — and enforcement is zero. Nothing reports this; the endpoint simply answers 200 forever.

Without shouldCharge, every request is charged, including human visitors and Googlebot. On an API that is usually right — agents are the only callers. On a page it is almost never right: you have put a 402 in front of your own readers and your search ranking.

knownAiAgent returns true for the self-identifying AI crawlers — GPTBot, ClaudeBot, PerplexityBot, CCBot, Bytespider, Google-Extended and the rest — plus anything already speaking AIFP. A browser passes through free.

That is a decision about who is asked to pay, not enforcement. The User-Agent is whatever the client says it is: a scraper that sends a browser's User-Agent reads your pages free, exactly like a human. What it cannot do is get a paid API route (no shouldCharge) without a receipt. So put the content you must be paid for behind API routes that charge everyone, and use pages for what you are content to show a browser. If you need pages enforced, the signal has to come from something a client cannot simply claim — your edge's bot verification, a signed agent request — added to the predicate below.

Extend it rather than replacing it when you have your own signal:

shouldCharge: (req) => knownAiAgent(req) || myEdgeStampedAiHeader(req),

A predicate that throws charges the request. Of the two wrong answers a broken detector can give, a 402 to one human is visible and recoverable; a crawler served free is silent and permanent.

robots.txt decides whether any of this runs at all

User-agent: *
Disallow: /

A well-behaved AI crawler reads that and leaves — before it ever reaches your paywall. You cannot forbid a crawler and bill it at the same time; those are opposite instructions to the same client, and robots.txt is the one it reads first.

If you arrived here from "block the AI scrapers", this line is probably already in your site and it guarantees zero revenue. Monetising AI traffic means stop blocking, start charging — a business decision, not a config toggle.

2. Register your endpoints with your API key

Your routes live in code and change in pull requests. Their prices should too.

import { AifpMerchant, ResourceRegistry, aifpGate } from "@aifinpay/gate";

// reads AIFP_MERCHANT_ID and AIFP_MERCHANT_SECRET from the environment
const merchant = new AifpMerchant();

await merchant.ensureResources([
  { route_pattern: "/api/search", type: "api", tier: "complex" },
  { route_pattern: "/api/lookup/*", type: "api", tier: "standard" },
  { route_pattern: "/api/report", type: "api", tier: "premium", name: "Generate report" },
  { route_pattern: "/api/health", type: "api", paywall_enabled: false },
]);

const registry = new ResourceRegistry({ merchant });
registry.start(); // refreshes every 60s

app.use(aifpGate({ merchantId: merchant.merchantId, registry }));

ensureResources is idempotent by route_pattern, so this belongs in your boot script and is safe on every deploy. Re-running it converges instead of piling up duplicates — which matters, because two records for the same path with different weights would charge unpredictably.

These endpoints appear in your AiFinPay dashboard immediately. Not through a sync job — there is nothing to sync. The SDK writes the same endpoint registry the dashboard's Paywall Builder writes and the dashboard reads, so a route registered from a deploy script and a route registered by clicking are the same record, in the same table, on the same Insights charts. Register from code, tune prices in the panel, or both.

One prerequisite: the merchant must be linked to a dashboard account. The simplest path is to create the merchant in the dashboard, copy the secret from there, and give it to the SDK. A merchant created purely over the API has no owner, and the panel will not show you what your key registered under it.

The rest of the management surface, for scripts and internal tooling:

await merchant.listResources();
await merchant.getResource("res_…");
await merchant.createResource({ route_pattern: "/api/new", type: "api" }); // 409 if it exists
await merchant.updateResource("res_…", { tier: "premium" });
await merchant.deleteResource("res_…");
await merchant.merchant(); // name, payout wallets — log this on boot so
await merchant.stats(); //   you can see WHICH merchant you configured
await merchant.activity(50);
await merchant.setWebhook("https://you.example/aifp-webhook");

Errors are typed, so a deploy script can tell a mistake from an outage: AifpValidationError (your input, with the server's own hint), AifpAuthError (wrong or rotated secret), AifpConflictError (duplicate route_pattern, and it carries the existing resource_id). The secret is stored non-enumerably and never appears in an error message, a stack, or JSON.stringify(merchant).

3. Choose a store

The gate meters a prepaid batch by counting billing units against the receipt. The store is where that count lives, and it has exactly two rules:

  • incrBy is atomic and returns the value AFTER the add. The gate never reads a counter and writes it back — it adds the call's weight and compares what comes back. That is what makes overspend arithmetically impossible under concurrency: whichever request receives the value that crosses the limit is the one refused, exactly once.
  • The TTL is set on the first write only. The counter must expire with the receipt. A counter that outlives its receipt refuses paid calls; one that expires early makes the whole batch spendable a second time.

Default — MemoryStore. Correct on one process, and only on one process. It is genuinely atomic there (the read-add-write runs with no await in the middle, so the event loop serializes it), it is capped so a flood of receipt ids cannot exhaust memory, and it needs no infrastructure.

Its two hard limits: counters are lost on restart, and every process gets its own copy. Under pm2 -i 4, node:cluster, or two pods behind a load balancer, a 200-unit batch will serve up to 800 calls, and nothing in the request path can detect it. If you run more than one process, use a shared store.

Shared — redisStore. Pass your own client; this package declares no redis dependency and will not touch your connection policy.

import Redis from "ioredis";
import { redisStore } from "@aifinpay/gate";

const store = redisStore(new Redis(process.env.REDIS_URL));
app.use(aifpGate({ merchantId, registry, store }));

It is one EVAL per call — increment and TTL in a single atomic script, and the script repairs a counter that somehow lost its expiry. Redis errors propagate rather than falling back to local memory: a silent downgrade would reopen the overspend race the counter exists to close. onStoreError decides what happens ("closed"503, the default; "open" → serve and flag), and whatever it decides is visible.

Anything else. DynamoDB, Postgres, Cloudflare KV — implement incrBy and prove it before it meters real money:

import { assertStoreContract } from "@aifinpay/gate/testing";
await assertStoreContract(() => myStore(), { equal: assert.equal, ok: assert.ok });

Six cases, each of which maps to a way a merchant loses revenue. Adapter skeletons for DynamoDB and Postgres are in the source of @aifinpay/gate/stores.

4. What the gate does per request

#ConditionAnswer
1Resolve the resource and its weight — the registry record for this path, or the mount's resource/tier. An unregistered path stays paywalled, never free.
2The resource is registered with paywall_enabled: false200, header AIFP-Paywall: off, no units spent
3No AIFP-Receipt header402 + the full challenge
4Receipt expired402receipt expired — prepay a new batch
4Bad signature, issuer, or audience403receipt verification failed (signature/issuer/audience)
4Our JWKS is unreachable and nothing is cached503 AIFP-503-METER — fails closed
5Receipt is scoped to another path403 — names both the receipt's scope and the path
6Single-use receipt replayed403receipt already spent (single-use)
7used + weight > unit_quota402quota exhausted — prepay the next batch
8Otherwise200, header AIFP-Quota-Remaining, req.aifp populated

Verification is local and stateless: an Ed25519 signature checked against our published JWKS, in your process, pinned to EdDSA. Your latency never depends on ours. Pass jwks: { keys: [...] } to remove even the JWKS fetch (at the cost of a redeploy when we rotate keys).

The 402 an agent sees:

{
  "error": "AIFP-402",
  "detail": "Payment Required — prepay a batch of requests and retry with the AIFP-Receipt header",
  "protocol": "AIFP-1",
  "merchant_id": "mrch_acme",
  "resource": "/api/search",
  "tier": "complex",
  "unit_weight": 4,
  "unit_price_usd": "0.002",
  "min_requests": 50,
  "protocol_fee_bps": 100,
  "no_minimum_fee": true,
  "how_to_pay": [
    "POST https://api.aifinpay.io/v1/quote {\"merchant_id\":\"mrch_acme\",\"resource\":\"/api/search\",\"tier\":\"complex\"}",
    "settle the quoted batch on-chain from your own wallet (order_id = quote_id)",
    "POST https://api.aifinpay.io/v1/pay {quote_id, chain, asset, tx_ref} -> quota receipt",
    "retry this request with header: AIFP-Receipt: <receipt JWT>"
  ]
}

And what your handler gets on a paid call:

req.aifp;
// { agent: "agt_…", receipt_id: "rcpt_…", resource: "/api/search",
//   weight: 4, unit_quota: 200, used: 8, remaining: 192, mode: "paid" }

Pricing

Three fixed settings, per call. The displayed/quoted AIFP-1 price is the gross amount paid by the agent. AiFinPay receives 1% of that gross amount and the merchant receives 99% of gross before external network or settlement costs. The 1% fee is not added on top of the displayed AIFP-1 price.

TierGross price paid by agentMerchant 99%AiFinPay 1%Billing units per call
standard$0.0005$0.000495$0.0000051
complex$0.002$0.00198$0.000024
premium$0.005$0.00495$0.0000510

AIFP-2/x402 is a separate route: the provider receives 100% of its provider-defined price, while any AiFinPay AIFP-2 fee is payer-side/on-top. The current AIFP-2 fee is 0%; a future non-zero fee requires a versioned AIFP-2 settlement profile and must not reduce the provider amount.

During the migration (as of 2026-08-23). The table above is the canonical model and what the v1.3 settlement contract enforces on-chain. Polygon mainnet still runs the previous splitter, whose immutable split is 98.99/1.00/0.01, and the backend grosses the total up from the merchant amount to match it — so today an AIFP-1 merchant is made whole and the agent pays slightly more than the displayed price. When v1.3 is deployed to mainnet, the displayed price becomes exactly what the agent pays and the merchant nets 99% of it. If you are integrating now, that is the one number that moves under you; nothing in this package's API changes with it.

Weight is always price ÷ base price. That equality is what lets one prepaid batch be spent across endpoints of different tiers and still drain at each endpoint's real rate. Override per route with unit_weight when a call is genuinely more expensive than its tier.

Options worth knowing

OptionDefaultWhy you would change it
storeMemoryStoreAnything beyond one process. See §3.
registryPath-matched pricing from your registered endpoints.
onStoreError"closed""open" trades metering for availability, visibly.
onEvent402 / serve / 403 / meter_error for your own metrics.
allowYour own veto, evaluated before any unit is metered, so a refused call costs the agent nothing.
jwksfetchedPin the key set; removes all runtime network dependency on us.
requireAgentMatchfalseCompares AIFP-Agent-Id to the receipt subject. Anti-accident, not anti-theft — the header is not authenticated.
refundOnErrorfalseGive a unit back on a 5xx. Read the warning below first.

refundOnError fires after your response has already gone out. If the agent received a body, the refunded unit is a unit it got served for free — a slow double-spend. Enable it only when your upstream fails before doing any work, and understand that you are trading exact metering for generosity.

5. What this package does not do

  • It does not hold money or keys. No custody, no funds movement. Agents settle on-chain to your own wallet; this package verifies a receipt and counts.
  • It does not enforce merchant policies. Free-unit allowances, per-agent daily caps, blocklists and unlimited-access rules are evaluated by the hosted AiFinPay gateway. A second implementation of a rule you edit in our dashboard would be a second source of truth, and the two would disagree on the day it mattered. Use allow for rules that are genuinely yours.
  • Self-hosted traffic does not populate the panel's funnel, geo and AI-client charts. Those are written by the hosted gateway from traffic that passes through it. Your registered endpoints still appear; the per-request analytics do not. Use onEvent to feed your own metrics.
  • It cannot bind a receipt to a caller. A receipt is a bearer token. Anyone who obtains the JWT can spend the batch. The honest guarantee is bounded loss: a stolen receipt can spend at most the units the payer prepaid, and the post-increment counter makes overspending impossible. It is not non-repudiation, and requireAgentMatch does not make it so.
  • @aifinpay/agent — the other side: an SDK for the agent that pays your paywall.
  • @aifinpay/mcp — MCP server for agents that pay through an MCP host.

MIT © CoinSecurities (SECCO)

Agent instructions and route discovery

Use the payer skill for an agent buying access, or the merchant skill for a site owner integrating the gate. Skills are instructions; they do not install or enable a payment executor.

Starting with 0.3.3, both buildDiscoveryDocument and HTTP 402 challenges include instructions_url, merchant_instructions_url and documentation_url. A partner on an older package must update and redeploy to expose these fields. The skill at a stable linked URL can then be updated independently; installed skill copies need updating in the agent client.

Publish /.well-known/x402.json on each origin that serves gated resources. The helper generates it from the resources array supplied by your app; it does not inspect your router or register that catalog in a central database. Use the same resource configuration for discovery and gates to prevent drift. Keep discovery and an API parameter catalog publicly readable. Link them from /llms.txt using the same origin (or relative URLs), including on staging. A production URL in a staging llms.txt can send agents to a missing catalog.

The discovery file lists resource prices and scopes. A fresh quote supplies settlement terms; discovery is neither a receipt nor proof that a particular client can execute the offered route.

Keywords

aifinpay

FAQs

Package last updated on 22 Sep 2026

Related posts