Documentation menu

Custom API rule

A Custom API rule lets your own server decide whether a member qualifies. NXT sends the member's wallets to your endpoint and grants the role when your endpoint answers true.

Settings

Field Description
Endpoint URL A public https:// URL. Private, local, and plain http:// addresses are rejected.
Secret Optional but recommended. Used to sign every request so you can verify it came from NXT.
Role The Discord role to grant

Request

NXT sends a POST request with a JSON body.

For a member with one wallet:

{
  "wallet": "0x8f3a5c0d2e4b6a7f9c1d3e5f7a9b1c3d5e7f19c2",
  "wallets": ["0x8f3a5c0d2e4b6a7f9c1d3e5f7a9b1c3d5e7f19c2"]
}

For a member with several wallets:

{
  "wallets": [
    "0x8f3a5c0d2e4b6a7f9c1d3e5f7a9b1c3d5e7f19c2",
    "0x21bd7e0c9a8b6d5f4e3c2b1a0f9e8d7c6b5aa07e"
  ]
}

Addresses are lowercase. Headers:

Header Value
content-type application/json
user-agent NXT-Verify/1.0
x-nxt-signature Hex-encoded HMAC-SHA256 of the raw request body, using your secret. Sent only when a secret is set.

Response

Your response NXT treats it as
200 with {"success": true} Pass
200 with {"success": false} Fail
404 Fail. Use this when the wallet is unknown to you.
Any other status Unknown
No response within 8 seconds Unknown
Body that is not valid JSON, or has no boolean success Unknown

Unknown results never remove a role. NXT retries on the next check. Redirects are not followed.

Verifying the signature

Compute the HMAC over the exact bytes you received, before parsing the JSON, and compare it in constant time.

import { createHmac, timingSafeEqual } from 'node:crypto';

export async function POST(req) {
  const raw = await req.text();
  const expected = createHmac('sha256', process.env.NXT_SECRET).update(raw).digest('hex');
  const got = req.headers.get('x-nxt-signature') ?? '';
  const ok = got.length === expected.length && timingSafeEqual(Buffer.from(got), Buffer.from(expected));
  if (!ok) return new Response('bad signature', { status: 401 });

  const { wallets } = JSON.parse(raw);
  const allowed = await isEligible(wallets); // your logic
  return Response.json({ success: allowed });
}

A 401 from your endpoint is treated as unknown, not as a failure, so a misconfigured secret will not strip roles. Run the live holdings check on a member's profile in the member directory to see the result if roles are not being granted.

When it is called

Your endpoint is called whenever NXT checks a member: when they verify, when their wallets change, when a gated NFT transfer re-checks them, and on every hourly sweep for each verified member. Responses are not cached between sweeps, so changes on your side take effect on the next check. Keep the endpoint fast and able to handle a burst of requests during sweeps.

Something unclear or out of date? Tell us at support@nxtnft.app.