Skip to main content
Version: Testnet

Tutorial (Base Sepolia)

In this tutorial, we will be exploring the process of verifying proofs directly on Base Sepolia using the Relayer.

Currently, only Groth16 proofs over BN254 are supported.

Quick Start (Using curl)

If you already have a proof ready and just want to test proof verification directly on Base Sepolia, you can make a direct API call to the Relayer.

Generate an API key from https://relayer-testnet.horizenlabs.io and make a request with "submissionMode": "direct" and "chainId": 84532.

Here is an example request. Proof data is specific to your proof and the other fields should be exactly as is.

Example request
curl -X POST https://relayer-api-testnet.horizenlabs.io/api/v1/submit-proof/<API_KEY> \
-H "Content-Type: application/json" \
-d '{
"proofType": "groth16",
"chainId": 84532,
"submissionMode": "direct",
"vkRegistered": false,
"proofOptions": {
"library": "snarkjs",
"curve": "bn254"
},
"proofData": {
"proof": {
"pi_a": [
"4420052269257603964521765970059465190197504296478730051526403555361572750334",
"1275015612656268002376231456382141930967106678780413667671436026523998775552",
"1"
],
"pi_b": [
[
"11607064673997375736457860321273333509352534161017004314531557355673515813263",
"11478505554692336437939643344391005885947215421348486153593338793656052643302"
],
[
"4958062122395484356380634067656871611831080684969600128423211034631061815968",
"1437185897006356101958796197252755759260680332946169822518079236496799612363"
],
["1", "0"]
],
"pi_c": [
"6626160033948011254336888385645663212035075506864191827227973242651868434509",
"17168089924734544011179603229536569417645686449262898339531288153391209142084",
"1"
],
"protocol": "groth16",
"curve": "bn254"
},
"publicSignals": [
"7713112592372404476342535432037683616424591277138491596200192981572885523208"
],
"vk": {
"protocol": "groth16",
"curve": "bn254",
"nPublic": 1,
"vk_alpha_1": [
"20942142906144515267179725381850804438924571621046706976253286503421986433822",
"9680977157437563010368034719069627443749947549764693978513212184312665470688",
"1"
],
"vk_beta_2": [
[
"16217676226888969690219735041601025084852427983916003102533953692839349242990",
"15645679232287031383735064865107576195135312479750571684638136966783512066986"
],
[
"20044801879021564709130622368889297516821012137991661444872313671032541370254",
"14884690486218043533665503570367172626109391511976084010774043567892292157979"
],
["1", "0"]
],
"vk_gamma_2": [
[
"10857046999023057135944570762232829481370756359578518086990519993285655852781",
"11559732032986387107991004021392285783925812861821192530917403151452391805634"
],
[
"8495653923123431417604973247489272438418190587263600148770280649306958101930",
"4082367875863433681332203403145435568316851327593401208105741076214120093531"
],
["1", "0"]
],
"vk_delta_2": [
[
"16809957133253474739229029892195099076154555898535369996493508512223781677201",
"1024630103201761786084505692366663250469903752194981608555685367041738381534"
],
[
"165235853046638720951189748368360374041485994280569953339158977860635833273",
"8169905769347531841923196983510085592447077089168982572464898358506802524646"
],
["1", "0"]
],
"vk_alphabeta_12": [
[
[
"3519047086240691240444240334405988061824799551425932970208510036292795238167",
"15905769150246362188126455441300829605659570856598816079514359534518991178510"
],
[
"662632515789781217967956417508059228403120573889023591221918843188110872316",
"10552102698544138932322313225403879888853967614750442621675453679651192515736"
],
[
"9505689337794240180920320112769312826334466736047088892474545429041171397668",
"1836826627152142711173666459859880394571989636815151097424263779994518059590"
]
],
[
[
"13424414025091686694562074128196866426603406695370277978924565763645214876896",
"17504463026137961072584988823531288324424168190827629371933668475020799676690"
],
[
"9796356589604743232733734050731974424277201820318493197881360387655742053307",
"9688490986684084850254262810459336414374171307381267716143947497688574395151"
],
[
"2108127091118953822210946419583288095752938830236808750756134529965300882974",
"11654159727836121390777644093536392883585365703915099958414862527652015058986"
]
]
],
"IC": [
[
"16686484712641087123102606730255789399796742399054091813689799051401578414249",
"13747114413450601687552288017944577471844727430547243835474268666172976701503",
"1"
],
[
"18003378217964567771165575855982946773816100030558949570583971000071951872063",
"6656108805491103221214015713698642249076259443837945518170298630752164818667",
"1"
]
]
}
}'

Once submitted, you can check the verification status using the /job-status endpoint:

curl -X GET https://relayer-api-testnet.horizenlabs.io/api/v1/job-status/<jobId>

If you want to integrate this process programmatically or learn how to generate the proof data, continue with the complete tutorial below.


Complete Tutorial: Submitting Proofs via Node.js

info

Similar code examples can be explored here.

note

Before starting the tutorial make sure to update your Node JS to the latest version (v24.1.0) You can check your Node JS version with command node -v

Let's create a new project and install axios for our project. Run the following commands:

Create a new directory:

mkdir proof-submission

Navigate to the project directory:

cd proof-submission

Initialize an NPM project:

npm init -y && npm pkg set type=module

Install axios and dotenv:

npm i axios dotenv

Let's create a .env file to store our API_KEY, which will be used later to send proofs for verification using the Relayer. Use the following code snippet to fill up your .env file. To use the Relayer you need an API Key. Create your own API key by signing up here for testnet.

API_KEY = "generate your API key"

Create a new file named index.js as the entrypoint for our application. Open index.js in your IDE and start with import necessary packages :

import axios from "axios";
import fs from "fs";
import dotenv from "dotenv";
dotenv.config();

After this let's initialize our API URL.

const API_URL = "https://relayer-api-testnet.horizenlabs.io/api/v1";

API Documentation

Swagger docs are available and provide detailed information about each endpoint, expected payloads, and responses. Refer to them when integrating or debugging your API calls: https://relayer-api-testnet.horizenlabs.io/docs


We would also need to import the required files we have generated already in previous tutorials, which are proof, verification key and public inputs.

const proof = JSON.parse(fs.readFileSync("./data/proof.json"));
const publicInputs = JSON.parse(fs.readFileSync("./data/public.json"));
const key = JSON.parse(fs.readFileSync("./data/main.groth16.vkey.json"));
info

Next we will be writing the core logic to send proofs to the Relayer for verification. All the following code snippets should be inserted within async main function.

async function main() {
// Required code
}

main();

We will start the verification process by calling a POST endpoint named submit-proof. We will also need to create a params object with all the necessary information about the proof, set the submission mode to "direct", and specify the chainId, which will be sent in the API call. Find the chain IDs for all the supported networks here.

const params = {
"proofType": "groth16",
"vkRegistered": false,
"proofOptions": {
"library": "snarkjs",
"curve": "bn128"
},
"chainId": 84532,
"submissionMode": "direct",
"proofData": {
"proof": proof,
"publicSignals": publicInputs,
"vk": vk.vkHash || vk.meta.vkHash
}
}

const requestResponse = await axios.post(`${API_URL}/submit-proof/${process.env.API_KEY}`, params)
console.log(requestResponse.data)

After sending the verification request to the Relayer, we can fetch the status of our request using the jobId returned in the response of the previous API call. To get the status, we will be making a GET API call to job-status endpoint. We want to wait until our proof is finalized on Base Sepolia, thus we will run a loop waiting for 5 seconds between multiple API calls.

while (true) {
const jobStatusResponse = await axios.get(`${API_URL}/job-status/${process.env.API_KEY}/${requestResponse.data.jobId}`);
if (jobStatusResponse.data.status === "Finalized") {
console.log("Job finalized successfully");
console.log(jobStatusResponse.data);
break;
} else {
console.log("Job status: ", jobStatusResponse.data.status);
console.log("Waiting for job to finalize...");
await new Promise(resolve => setTimeout(resolve, 5000)); // Wait for 5 seconds before checking again
}
}

Next run this script by running node index.js command. You should get a response similar to the following:

{
jobId: '6d79265c-bfd9-11f0-b4be-a621194d52f0'
}
Job status: Submitted
Waiting for job to finalize...
Job status: IncludedInBlock
Waiting for job to finalize...
Job status: IncludedInBlock
Waiting for job to finalize...
Job finalized successfully
{
"jobId": "6d79265c-bfd9-11f0-b4be-a621194d52f0",
"status": "Finalized",
"statusId": 4,
"proofType": "groth16",
"chainId": 84532,
"createdAt": "2025-11-12T15:08:21.000Z",
"updatedAt": "2025-11-12T15:08:46.000Z",
"proofOptions": {},
"txHash": "0xa215807ab9b38564970ca62b26a6751f97b1904907d0cf457b4c05250a81933b",
"txExplorerUrl": "https://sepolia.basescan.org/tx/0xa215807ab9b38564970ca62b26a6751f97b1904907d0cf457b4c05250a81933b",
"blockHash": "0x7aae663503d45d1ad401f06c7bcb9b4e95e70f81e88fd120efe3d089394a6c58",
"blockExplorerUrl": "https://sepolia.basescan.org/block/0x7aae663503d45d1ad401f06c7bcb9b4e95e70f81e88fd120efe3d089394a6c58"
}

Next Steps

  • Learn how to generate Groth16 proofs here.
  • Explore other submission modes and supported networks here.
  • Review API responses and parameters in the Swagger documentation.

Resources

  1. Submit feedback or an issue: Relayer API: Feedback

  2. Submit a new feature request: Relayer API: New Feature Requests

  3. Reach out to us on Discord or relayer-support@horizenlabs.io if you would like to discuss potential partnerships.