Skip to content

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 and authStatusHandler 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:

  1. Download the circuit verification keys from the official Iden3 documentation, Pivadoid Verifier example.
  2. 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:

server.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:

  1. /auth-request: SDK sends a POST request to start a new proof session.
  2. Redirect: The user scans the generated QR code, which encodes the proof request.
  3. /callback: Privado Wallet sends the ZK proof to this endpoint.
  4. /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.