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