Backend Setup for Eligibility SDK¶
The Eligibility SDK integrates with an off-chain verifier backend that implements the Iden3 auth protocol. This backend verifies zero-knowledge proofs submitted by users through compatible wallets like Privado.
⚠️ Important: To use the EligibilitySDK, you’ll need to provide custom
queryHandler
andauthStatusHandler
functions.
🔐 Example API Endpoints¶
Route | Method | Description |
---|---|---|
/auth-request |
POST | Initializes a new proof session |
/callback |
POST | Accepts ZK proof token from the wallet |
/status/:id |
GET | Returns proof status and any resolved data |
These endpoints must be hosted at the same URL as the config.endpoint
passed to the SDK.
🔑 Public Verification Keys¶
To verify ZK proofs, you must add public keys generated from the Iden3 trusted setup:
- Download the circuit verification keys from the official Iden3 documentation, Pivadoid Verifier example.
- Place them inside a folder named
keys/
in your backend project root.
Registering Receptor DID Method with iden3/js-iden3-auth¶
To enable the backend server to function with the Redbelly Mainnet and Testnet, it's essential to register the custom "receptor" DID method with the iden3/js-iden3-auth SDK. This registration must occur before initializing the backend server.
const { core } = require("@iden3/js-iden3-auth");
// Register the "receptor" DID method for Redbelly Testnet
core.registerDidMethodNetwork({
method: "receptor",
methodByte: 0b10000011,
blockchain: "redbelly",
network: "testnet",
networkFlag: 0b10000011,
chainId: 153,
});
// Register the "receptor" DID method for Redbelly Mainnet
core.registerDidMethodNetwork({
method: "receptor",
methodByte: 0b01010111,
blockchain: "redbelly",
network: "mainnet",
networkFlag: 0b01010111,
chainId: 151,
});
Adding State Resolvers for Proof Verification¶
Before verifying submitted zero-knowledge proofs, you must configure state resolvers for the iden3/js-iden3-auth verifier instance. State resolvers are crucial for the verifier to interact with the blockchain and resolve DID states.
The resolvers are provided as an object where each key represents a DID method (e.g., "redbelly:mainnet"), and its corresponding value is an EthStateResolver instance. To create an EthStateResolver, you need the RPC URL of the blockchain and the address of the deployed state contract on that network.
const { auth, resolver } = require("@iden3/js-iden3-auth");
// Define state resolvers for different Redbelly networks
const resolvers = {
"redbelly:mainnet": new resolver.EthStateResolver(
"https://governors.mainnet.redbelly.network", // RPC URL for Redbelly Mainnet
"0x1cc7261e1777D69505Cb6413a91bb27ca9eb1456" // State contract address on Redbelly Mainnet
),
"redbelly:testnet": new resolver.EthStateResolver(
"https://governors.testnet.redbelly.network", // RPC URL for Redbelly Testnet
"0x69376715FB5E2B924a33e9C27302F52DEa178CDC" // State contract address on Redbelly Testnet
),
};
// Initialize the Verifier with the defined state resolvers and other configurations
const verifier = await auth.Verifier.newVerifier({
stateResolver: resolvers, // Pass the configured state resolvers
circuitsDir: KEY_DIR, // Directory containing circuit verification keys
ipfsGatewayURL: "https://ipfs.io", // IPFS gateway for content resolution
});
✅ Example Verifier Backend (Express.js)¶
Below is a full example of a verifier backend implemented using Express.js:
const express = require("express");
const { auth, resolver, protocol, core } = require("@iden3/js-iden3-auth");
const getRawBody = require("raw-body");
// const cors = require("cors");
const path = require("path");
const uuidv4 = require("uuid").v4;
const app = express();
const port = 8081;
const HOST_URL = "https://verfier-backend.vercel.app"; // provide hosted url of backend so wallet can call it
const KEY_DIR = path.join(__dirname, "./keys");
const CALLBACK_URL = "/callback";
// app.use(cors());
app.use(express.json());
const requestMap = new Map();
const statusMap = new Map();
core.registerDidMethodNetwork({
method: "receptor",
methodByte: 0b10000011,
blockchain: "redbelly",
network: "testnet",
networkFlag: 0b10000011,
chainId: 153,
});
core.registerDidMethodNetwork({
method: "receptor",
methodByte: 0b01010111,
blockchain: "redbelly",
network: "mainnet",
networkFlag: 0b01010111,
chainId: 151,
});
app.post("/auth-request", async (req, res) => {
const { flowName = "Eligibility Check", scope = [] } = req.body;
const sessionId = uuidv4();
const callbackUri = `${HOST_URL}${CALLBACK_URL}?sessionId=${sessionId}`;
const audience =
"did:receptor:redbelly:testnet:31Jz2omB1fL33eGkuwi8vXKuxfS3XTck7X58XWuovE";
// basic auth request iden3
const request = auth.createAuthorizationRequest(
flowName,
audience,
callbackUri
);
// pass query to the request for specific proof eg: "age over 18"
request.body.scope = scope;
// set request to the map with sessionId
requestMap.set(sessionId, request);
return res
.status(200)
.set("Content-Type", "application/json")
.send({ request, sessionId });
});
app.post("/callback", async (req, res) => {
const sessionId = req.query.sessionId;
const raw = await getRawBody(req);
const tokenStr = raw.toString().trim();
statusMap.set(sessionId, { status: "verifying" });
const authRequest = requestMap.get(sessionId);
if (!authRequest) {
statusMap.set(sessionId, { status: "failed", error: "Invalid session ID" });
return res.status(400).send("Invalid session ID");
}
const resolvers = {
"privado:main": new resolver.EthStateResolver(
"https://rpc-mainnet.privado.id",
"0x3C9acB2205Aa72A05F6D77d708b5Cf85FCa3a896"
),
"redbelly:mainnet": new resolver.EthStateResolver(
"https://rbn9bb267fa.staging.redbelly.network/rpc",
"0xEd5604a53971cB4e7f51A351b5d93Eef50517a7e"
),
"redbelly:testnet": new resolver.EthStateResolver(
"https://governors.testnet.redbelly.network",
"0x69376715FB5E2B924a33e9C27302F52DEa178CDC"
),
};
const verifier = await auth.Verifier.newVerifier({
stateResolver: resolvers,
circuitsDir: KEY_DIR,
ipfsGatewayURL: "https://ipfs.io",
// didDocumentResolver: new resolver.DidDocumentResolver(
// "https://resolver.privado.id"
// ),
});
try {
const opts = {
AcceptedStateTransitionDelay: 5 * 60 * 1000, // 5 minute
};
const authResponse = await verifier.fullVerify(tokenStr, authRequest, opts);
statusMap.set(sessionId, { status: "success", proof: authResponse });
return res
.status(200)
.set("Content-Type", "application/json")
.send(authResponse);
} catch (err) {
console.error("Error verifying token", err);
statusMap.set(sessionId, { status: "failed", error: err.message });
return res.status(500).send(err);
}
});
app.get("/status/:sessionId", (req, res) => {
const result = statusMap.get(req.params.sessionId);
if (!result)
return res
.status(200)
.set("Content-Type", "application/json")
.send({ status: "idle" });
return res.status(200).set("Content-Type", "application/json").send(result);
});
app.listen(port, () => {
console.log(`✅ Verifier backend running at ${HOST_URL}`);
});
📦 Internal SDK Behavior¶
These backend routes are triggered by the SDK internally — developers integrating the SDK don't call them directly.
Here's a breakdown of what happens inside the SDK:
/auth-request
: SDK sends a POST request to start a new proof session.- Redirect: The user scans the generated QR code, which encodes the proof request.
/callback
: Privado Wallet sends the ZK proof to this endpoint./status/:sessionId
: SDK polls this endpoint until it receives a success or failure status.
Pseudocode (Internal Logic)¶
// Internal SDK flow reference (not exported)
axios.post(`${config.endpoint}/auth-request`, { scope, flowName, scope });
axios.get(`${config.endpoint}/status/${sessionId}`);
These endpoints form the required backend contract. Although the SDK's internal hooks aren't exposed, your backend must support these exact routes and behaviors for the widget to work properly.