Build a Fact-Checking dApp That Actually Googles Things (Yes, On-Chain)
Part 1: Why GenLayer Changes Everything & How to Get Started
Welcome to a 4-part series and a cheatsheet where we build **TruthPost—a decentralized fact-checker powered by AI. No blockchain experience needed. If you can write Python and basic JavaScript, you're ready to build something impossible on any other chain.
Picture this: a smart contract that Googles something for you.
Not through some sketchy oracle. Not through an API you have to trust. The contract itself opens a browser, reads a Wikipedia page, thinks about what it found, and writes the answer directly to the blockchain.
Sounds impossible? That's because it was impossible. Until now.
Welcome to GenLayer—the first blockchain where smart contracts can access the internet, reason with AI, and reach consensus on subjective outputs.
By the end of this series, you'll have built TruthPost: a fact-checking platform where anyone can submit a claim like "The Eiffel Tower is 330 meters tall," and the blockchain will:
- Fetch Wikipedia (or any source you specify)
- Send the content to an LLM for analysis
- Have multiple validators independently verify the AI's verdict
- Record the result on-chain—permanently, transparently, trustlessly
We're talking about a complete working dApp: a Python smart contract doing things literally impossible on Ethereum, plus a sleek React frontend with wallet integration.
Let's start with why this is possible at all.
Every Other Blockchain Hits the Same Three Walls
If you've ever built on Ethereum, Solana, or really any blockchain, you know the frustrations:
Wall #1: Smart Contracts Are Blind
They can't see the outside world. No HTTP requests. No reading news articles. No checking stock prices. If they need real-world data, they depend on oracles—expensive, slow, centralized middlemen that defeat the entire point of decentralization.
Wall #2: Smart Contracts Can't Think
They execute deterministic logic: math, if/else statements, state updates. But they can't reason. They can't parse natural language, summarize documents, or decide whether a statement is true. They're calculators, not thinkers.
Wall #3: Every Validator Must Agree Exactly
This is the deepest constraint. Blockchain consensus requires determinism—run the same code, get identical output. But AI models are inherently non-deterministic. Ask GPT the same question twice, you get two different phrasings. Traditional blockchains simply can't handle this.
GenLayer breaks all three walls. Not with workarounds—at the protocol level.
What Actually Is GenLayer?
GenLayer is an AI-native blockchain. But "AI-native" gets thrown around a lot in crypto, so let me be specific.
Smart contracts on GenLayer (called Intelligent Contracts) are Python classes that run inside a custom VM called GenVM. Inside GenVM, your contracts have native access to two things no other blockchain offers:
1. The Internet
Contracts can fetch web pages, call APIs, take screenshots—directly, without oracles.
2. Large Language Models
Contracts can run prompts through AI models and get back structured responses.
And GenLayer has a consensus mechanism specifically designed to handle the fact that AI outputs aren't deterministic.
This isn't a layer-2. This isn't a plugin. This is a new blockchain architecture built from the ground up for AI-powered applications.
The Two Breakthrough Concepts You Need to Understand
Everything in GenLayer rests on two ideas. Master these, and the rest of the tutorial will click into place.
Breakthrough #1: Optimistic Democracy
On Ethereum, consensus works through repetition: every validator runs the same code and checks they all got the same answer. That model breaks the moment you introduce AI—two validators will get two different phrasings from an LLM, and the chain rejects the transaction.
GenLayer replaces this with Optimistic Democracy:
- A transaction arrives (e.g., "fact-check this claim")
- A Leader validator processes it first—fetching web data, running the AI, producing a result
- Other validators independently do the same work—each one fetches, each one runs the AI
- Validators compare for equivalent outputs, not identical ones
- If a majority agrees the results are equivalent, the transaction is accepted
- A finality window opens where anyone can appeal the result
- Each appeal brings in more validators (doubling each round), creating escalating security
- Once appeals close, the result is finalized and permanent
The breakthrough: GenLayer doesn't demand every validator produces the string "true". It demands every validator agrees the claim is true.
That's the difference between determinism and equivalence—and it's what makes AI on-chain possible.
This is grounded in Condorcet's Jury Theorem—the mathematical principle that a majority of independent decision-makers is more likely to be correct than any individual. More validators = higher confidence.
Breakthrough #2: The Equivalence Principle
If validators aren't checking for identical outputs, what are they checking?
That's where the Equivalence Principle comes in. As a developer, you define what "equivalent" means for your specific use case.
GenLayer gives you three modes:
| Type | What It Means | When to Use |
|---|---|---|
| Strict Equality | Outputs must be byte-for-byte identical | Structured data: yes/no, categories, JSON with fixed keys |
| Comparative | An LLM judges whether two outputs are "close enough" | Numeric ranges, similar-but-not-identical text, ratings |
| Non-Comparative | An LLM checks whether the leader's output meets criteria | Subjective quality checks, summaries, creative outputs |
For TruthPost, we'll use strict equality. Our AI returns a structured verdict—"true", "false", or "partially_true"—and we want every validator to land on the same label. Because the output space is small and constrained, strict equality works perfectly.
Think of it this way:
Traditional consensus asks: "Did everyone compute 42?"
GenLayer consensus asks: "Does everyone agree this claim is false?"Same security guarantee. Radically more flexibility.
What You Need to Get Started
Before we dive into setup, make sure you have:
- Python 3.11+—for writing intelligent contracts
- Node.js 18+—for the frontend and deployment tools
- Git—for cloning the starter project
- MetaMask browser extension—for wallet connection
- GenLayer Studio—the dev environment (we'll set this up next)
You don't need blockchain experience. No Solidity knowledge. No Web3 background. If you're comfortable with Python and TypeScript/JavaScript, you're ready.
Setting Up Your Environment (The Fun Stuff)
Step 1: Get GenLayer Studio Running
GenLayer Studio is your local blockchain. It spins up a network with validators so you can deploy and test contracts.
Option A (Easiest): Use the hosted version
Just go to studio.genlayer.com. No installation. No Docker. No config files. It just works.
Option B: Run locally
Follow the setup guide at docs.genlayer.com if you prefer running your own instance.
Step 2: Install the GenLayer CLI
The CLI handles network selection and contract deployment:
npm install -g genlayer
Verify it's working:
genlayer --version
Step 3: Clone the Boilerplate Project
We're using the official GenLayer starter template. It includes everything: project structure, deployment scripts, and a complete Next.js frontend.
git clone https://github.com/genlayerlabs/genlayer-project-boilerplate.git truthpost
cd truthpost
Install all dependencies:
# Root dependencies (deployment tools)
npm install
# Frontend dependencies
cd frontend
npm install
cd ..
# Python dependencies (for contract testing)
pip install -r requirements.txt
Step 4: Choose Your Network
GenLayer has three networks to choose from:
| Network | What It's For | RPC URL |
|---|---|---|
| studionet | Development with hosted Studio | https://studio.genlayer.com/api |
| localnet | Your own local Studio instance | http://localhost:8545 |
| testnet (Asimov) | Public testnet | Provided by GenLayer |
For this tutorial, we'll use studionet:
genlayer network
# Select "studionet" from the interactive menu
Step 5: Understand Your Project Structure
Here's what the boilerplate gives you:
truthpost/
├── contracts/ # Python intelligent contracts go here
│ └── football_bets.py # Example contract (we'll replace this)
├── frontend/ # Next.js 15 app
│ ├── app/ # Pages and layouts
│ ├── components/ # React components
│ ├── lib/ # Hooks, utilities, contract interactions
│ └── .env.example # Environment config template
├── deploy/ # TypeScript deployment script
│ └── deployScript.ts # Deploys contracts to GenLayer
├── test/ # Python integration tests
├── package.json # Root dependencies
└── requirements.txt # Python dependencies
The boilerplate ships with a football betting dApp as an example. Over the next two parts, we'll gut the contract and rebuild it as TruthPost.
Step 6: The Moment of Truth (Sanity Check)
Let's make sure everything's wired up correctly by deploying the example contract:
npm run deploy
If you see this, you're golden:
Deploying contract...
Contract deployed at address: 0x...
Troubleshooting if deployment fails:
- Make sure GenLayer Studio is running (or you're connected to studionet)
- Double-check that
genlayer networkshows the right network- Verify
npm installcompleted successfully at the root
The Roadmap: What's Coming Next
Here's the full series at a glance:
| Part | What You'll Build |
|---|---|
| Part 1 (you are here) | GenLayer fundamentals, environment setup, project walkthrough |
| Part 2 | The TruthPost intelligent contract—web access, LLM calls, consensus magic |
| Part 3 | React frontend with genlayer-js SDK, MetaMask integration, TanStack Query |
| Part 4 | Testing with gltest, testnet deployment, extending the app, what to build next |
Key Takeaways (The "You Can Quote Me On This" Section)
Let's lock in what makes GenLayer fundamentally different:
✦ Intelligent Contracts are written in Python—if you know Python, you can build on GenLayer. No Solidity required.
✦ Contracts access the internet natively—no oracles, no off-chain workers, no trusted intermediaries.
✦ Contracts call AI models directly—LLMs are first-class citizens in the execution environment.
✦ Optimistic Democracy enables consensus on non-deterministic outputs—the breakthrough that makes AI on-chain actually possible.
✦ The Equivalence Principle lets you define "agreement"—strict matching, similarity comparison, or criteria-based judgment.
✦ The ecosystem includes Studio (dev env), CLI (deployment), and genlayer-js (frontend SDK)—everything you need is ready to go.
In Part 2, we're writing a Python smart contract that does something no Ethereum contract could ever do: fetch a web page, ask an AI to analyze it, and commit the verdict to the blockchain.
Next up: Part 2—Writing Your First Intelligent Contract
This tutorial is part of the GenLayer Builder Program. Full source code: TruthPost repository
Follow the series:
- Part 1: Introduction & Setup (you are here)
- Part 2: Writing the Intelligent Contract
- Part 3: Building the Frontend
- Part 4: Testing & Deployment
title: "Your First Intelligent Contract: Teaching the Blockchain to Think — Part 2"
published: true
description: "Write a Python smart contract that fetches web pages and runs AI analysis on-chain—doing things literally impossible on Ethereum"
tags: blockchain, ai, python, tutorial
series: "Zero to GenLayer: Build an AI Fact-Checker"
Your First Intelligent Contract: Teaching the Blockchain to Think
Part 2: Writing a Smart Contract That Fetches Web Pages and Runs AI (In Python!)
In Part 1, we set up our environment and learned why GenLayer is different. Now we're writing the actual contract—a Python class that does things literally impossible on any other blockchain.
Let me show you something wild.
Here's what we're about to build: a smart contract that takes a claim like "The Great Wall of China is visible from space," Googles it (well, fetches Wikipedia), sends the content to an LLM, gets back a verdict with an explanation, and writes it to the blockchain.
All of that happens inside the contract itself. No oracles. No backend servers. The blockchain does the thinking.
By the end of this part, you'll have written an intelligent contract that:
- Lets users submit claims to be fact-checked
- Fetches real web sources directly from on-chain code
- Uses an LLM to analyze those sources and produce structured verdicts
- Leverages the Equivalence Principle so all validators reach consensus
- Tracks reputation points for users who contribute verified claims
Ready? Let's write some Python.
Intelligent Contracts 101: The GenLayer Way
Before we dive into TruthPost, let's understand the anatomy of a GenLayer contract. If you've written Solidity before, some of this will feel familiar. Some will feel very different.
# Every contract starts with this import
from genlayer import *
class MyContract(gl.Contract):
# State variables—persisted on-chain
my_data: TreeMap[Address, str]
# Constructor—called once at deployment
def __init__(self):
pass
# Read-only method—anyone can call, no gas cost
@gl.public.view
def get_data(self) -> str:
return "hello"
# Write method—modifies state, costs gas
@gl.public.write
def set_data(self, value: str) -> None:
self.my_data[gl.message.sender_address] = value
Key differences from Solidity:
- Contracts are Python classes that extend
gl.Contract - Only one contract class per file (keep it simple)
- Storage uses GenLayer types (
TreeMap,DynArray) instead of Python'sdict/list - Methods are decorated:
@gl.public.viewfor reads,@gl.public.writefor writes - Access the caller's address with
gl.message.sender_address(likemsg.sender)
Got it? Cool. Let's build TruthPost.
Step 1: Define Your Data Model
Create contracts/truth_post.py and start with the data structures:
# { "Depends": "py-genlayer:test" }
import json
from dataclasses import dataclass
from genlayer import *
@allow_storage
@dataclass
class Claim:
id: str # Unique identifier
text: str # The claim to fact-check
verdict: str # "true", "false", "partially_true", or "pending"
explanation: str # AI-generated explanation
source_url: str # URL used for verification
submitter: str # Address of who submitted
has_been_checked: bool # Whether fact-check has run
What's happening here:
-
@allow_storagetells GenLayer this dataclass can be persisted on-chain. Without this decorator, you can't store custom objects in state. -
@dataclassis standard Python for structured data—nothing fancy. - Each
Claimtracks everything we need: the original text, the AI's verdict, metadata about who submitted it, and whether it's been checked yet.
Quick note on storage types:
Python Type GenLayer Type Why the Change? dictTreeMap[K, V]Blockchain-optimized key-value storage listDynArray[T]Blockchain-optimized arrays intu256,i64Fixed-size integers for deterministic behavior Rule of thumb: Use GenLayer types for state variables. Regular Python types work fine for local variables inside methods.
Step 2: Set Up the Contract Class
Now let's define the actual contract with its state:
class TruthPost(gl.Contract):
claims: TreeMap[str, Claim] # claim_id -> Claim object
reputation: TreeMap[Address, u256] # user address -> reputation points
claim_count: u256 # total claims submitted
def __init__(self):
self.claim_count = 0
Our contract stores three things:
-
claims—Maps claim IDs to fullClaimobjects -
reputation—Tracks reputation scores for each user -
claim_count—A simple counter for generating unique IDs
The __init__ method is the constructor—it runs once when the contract is deployed.
Step 3: Let Users Submit Claims
Time to add our first write method. This is how users submit claims for fact-checking:
@gl.public.write
def submit_claim(self, claim_text: str, source_url: str) -> None:
sender = gl.message.sender_address
# Generate a unique claim ID
self.claim_count += 1
claim_id = f"claim_{self.claim_count}"
# Create the claim in "pending" state
claim = Claim(
id=claim_id,
text=claim_text,
verdict="pending",
explanation="",
source_url=source_url,
submitter=sender.as_hex,
has_been_checked=False,
)
self.claims[claim_id] = claim
Breaking it down:
-
@gl.public.writemarks this as a state-changing method (costs gas to call) -
gl.message.sender_addressgives us the wallet address of whoever called the function (just likemsg.senderin Solidity) - We create a
Claimwithverdict="pending"—the AI hasn't analyzed it yet - The claim gets stored in our
TreeMapwith a unique ID
Simple enough. But here's where things get interesting.
Step 4: The Magic Part—AI-Powered Fact-Checking
This is where GenLayer shows its true power. We're writing a private helper method that:
- Fetches a web page from the internet
- Asks an LLM to analyze it
- Returns a structured verdict
Ready for this?
def _fact_check(self, claim_text: str, source_url: str) -> dict:
def check_claim() -> str:
# Step 1: Fetch real data from the internet
web_data = gl.nondet.web.render(source_url, mode="text")
# Step 2: Ask the LLM to fact-check the claim against the source
prompt = f"""You are a fact-checker. Based on the web content provided,
determine whether the following claim is true, false, or partially true.
CLAIM: {claim_text}
WEB CONTENT:
{web_data}
Respond ONLY with this exact JSON format, nothing else:
{{
"verdict": "<true|false|partially_true>",
"explanation": "<brief 1-2 sentence explanation>"
}}
Rules:
- "true" = the claim is fully supported by the source
- "false" = the claim is contradicted by the source
- "partially_true" = some parts are correct but others are wrong or misleading
- Keep the explanation concise and factual
- Your response must be valid JSON only, no extra text
"""
result = gl.nondet.exec_prompt(prompt, response_format="json")
return json.dumps(result, sort_keys=True)
# Step 3: Use Equivalence Principle for consensus
result_json = json.loads(gl.eq_principle.strict_eq(check_claim))
return result_json
Okay, hold up. Let's unpack every single line of this, because this is the heart of the entire tutorial.
gl.nondet.web.render(url, mode="text")
This line fetches a web page. From inside a smart contract. Let that sink in.
-
mode="text"returns the page content as plain text (HTML tags stripped out) -
mode="html"would give you raw HTML -
mode="screenshot"would actually screenshot the page and return an image
On Ethereum? On Solana? This is literally impossible. Smart contracts are sandboxed. They have zero network access. They can't even ping an IP address.
GenLayer contracts break that barrier at the protocol level. The blockchain itself reaches out to the internet.
gl.nondet.exec_prompt(prompt, response_format="json")
This line runs a prompt through an LLM. Inside a smart contract. On-chain.
The response_format="json" parameter tells the model to return structured JSON instead of free-form text.
Again: impossible on any other blockchain. There's no native AI integration anywhere else. GenLayer validators each have access to LLM providers, making AI a first-class citizen in contract execution.
gl.eq_principle.strict_eq(check_claim)
This is the Equivalence Principle in action. Here's the exact flow:
- The Leader validator executes
check_claim()—fetching the web page, calling the LLM, getting a result - Every other validator independently does the exact same thing
-
strict_eqchecks whether all validators got the same JSON output - If they agree → consensus reached, transaction accepted
- If they disagree → transaction might enter appeal
We use strict_eq because our output is structured: "true", "false", or "partially_true". Since we're asking the LLM to pick from a constrained set of labels, validators are very likely to agree.
When would you use the other equivalence modes?
prompt_comparative: When outputs should be similar but not identical (e.g., "ratings should be within 0.1 of each other")prompt_non_comparative: When you want validators to judge whether the leader's output meets criteria, without redoing the work themselves
Why Is check_claim a Nested Function?
Notice the pattern: we define check_claim() as an inner function and pass it to strict_eq.
This is how GenLayer's non-deterministic execution works. The function gets executed independently by each validator, and the equivalence principle compares their outputs to reach consensus.
Step 5: Trigger the Fact-Check
Now let's add the method that actually runs the AI analysis:
@gl.public.write
def resolve_claim(self, claim_id: str) -> None:
if claim_id not in self.claims:
raise Exception("Claim not found")
claim = self.claims[claim_id]
if claim.has_been_checked:
raise Exception("Claim already fact-checked")
# Run the AI fact-check (this is where the magic happens!)
result = self._fact_check(claim.text, claim.source_url)
# Update the claim with the verdict
claim.verdict = result["verdict"]
claim.explanation = result.get("explanation", "")
claim.has_been_checked = True
# Award reputation to the submitter
submitter_addr = Address(claim.submitter)
if submitter_addr not in self.reputation:
self.reputation[submitter_addr] = 0
self.reputation[submitter_addr] += 1
What happens when someone calls resolve_claim:
- We validate the claim exists and hasn't been checked yet
- We call
_fact_check()—this fetches the web, queries the AI, and gets validator consensus - We update the claim with the verdict and explanation
- We award +1 reputation to whoever submitted the claim (incentivizing contributions)
Step 6: Add Read Methods (View Functions)
Let's add methods so the frontend can read data from the contract:
@gl.public.view
def get_claims(self) -> dict:
return {k: v for k, v in self.claims.items()}
@gl.public.view
def get_claim(self, claim_id: str) -> dict:
if claim_id not in self.claims:
raise Exception("Claim not found")
claim = self.claims[claim_id]
return {
"id": claim.id,
"text": claim.text,
"verdict": claim.verdict,
"explanation": claim.explanation,
"source_url": claim.source_url,
"submitter": claim.submitter,
"has_been_checked": claim.has_been_checked,
}
@gl.public.view
def get_reputation(self) -> dict:
return {k.as_hex: v for k, v in self.reputation.items()}
@gl.public.view
def get_user_reputation(self, user_address: str) -> int:
return self.reputation.get(Address(user_address), 0)
View methods are:
- Read-only—they can't modify state
- Free to call—no gas cost
- Decorated with
@gl.public.view - Return data to whoever's querying
Note how get_reputation converts Address keys to hex strings with .as_hex—this is needed because the frontend works with string addresses, not Address objects.
The Complete Contract (All 95 Lines)
Here's the full contracts/truth_post.py:
# { "Depends": "py-genlayer:test" }
import json
from dataclasses import dataclass
from genlayer import *
@allow_storage
@dataclass
class Claim:
id: str
text: str
verdict: str
explanation: str
source_url: str
submitter: str
has_been_checked: bool
class TruthPost(gl.Contract):
claims: TreeMap[str, Claim]
reputation: TreeMap[Address, u256]
claim_count: u256
def __init__(self):
self.claim_count = 0
def _fact_check(self, claim_text: str, source_url: str) -> dict:
def check_claim() -> str:
web_data = gl.nondet.web.render(source_url, mode="text")
prompt = f"""You are a fact-checker. Based on the web content provided,
determine whether the following claim is true, false, or partially true.
CLAIM: {claim_text}
WEB CONTENT:
{web_data}
Respond ONLY with this exact JSON format, nothing else:
{{
"verdict": "<true|false|partially_true>",
"explanation": "<brief 1-2 sentence explanation>"
}}
Rules:
- "true" = the claim is fully supported by the source
- "false" = the claim is contradicted by the source
- "partially_true" = some parts are correct but others are wrong or misleading
- Keep the explanation concise and factual
- Your response must be valid JSON only, no extra text
"""
result = gl.nondet.exec_prompt(prompt, response_format="json")
return json.dumps(result, sort_keys=True)
result_json = json.loads(gl.eq_principle.strict_eq(check_claim))
return result_json
@gl.public.write
def submit_claim(self, claim_text: str, source_url: str) -> None:
sender = gl.message.sender_address
self.claim_count += 1
claim_id = f"claim_{self.claim_count}"
claim = Claim(
id=claim_id,
text=claim_text,
verdict="pending",
explanation="",
source_url=source_url,
submitter=sender.as_hex,
has_been_checked=False,
)
self.claims[claim_id] = claim
@gl.public.write
def resolve_claim(self, claim_id: str) -> None:
if claim_id not in self.claims:
raise Exception("Claim not found")
claim = self.claims[claim_id]
if claim.has_been_checked:
raise Exception("Claim already fact-checked")
result = self._fact_check(claim.text, claim.source_url)
claim.verdict = result["verdict"]
claim.explanation = result.get("explanation", "")
claim.has_been_checked = True
submitter_addr = Address(claim.submitter)
if submitter_addr not in self.reputation:
self.reputation[submitter_addr] = 0
self.reputation[submitter_addr] += 1
@gl.public.view
def get_claims(self) -> dict:
return {k: v for k, v in self.claims.items()}
@gl.public.view
def get_claim(self, claim_id: str) -> dict:
if claim_id not in self.claims:
raise Exception("Claim not found")
claim = self.claims[claim_id]
return {
"id": claim.id,
"text": claim.text,
"verdict": claim.verdict,
"explanation": claim.explanation,
"source_url": claim.source_url,
"submitter": claim.submitter,
"has_been_checked": claim.has_been_checked,
}
@gl.public.view
def get_reputation(self) -> dict:
return {k.as_hex: v for k, v in self.reputation.items()}
@gl.public.view
def get_user_reputation(self, user_address: str) -> int:
return self.reputation.get(Address(user_address), 0)
That's it. 95 lines of Python that do something no Ethereum contract could ever do.
Tracing the Full Flow
Let's walk through what happens when someone fact-checks a claim:
1. User calls submit_claim("The Eiffel Tower is 330m tall", "https://en.wikipedia.org/wiki/Eiffel_Tower")
↓
Claim stored with verdict="pending"
2. User (or anyone) calls resolve_claim("claim_1")
↓
Contract calls _fact_check()
3. Inside _fact_check():
Leader validator runs check_claim():
- Fetches Wikipedia via gl.nondet.web.render()
- Sends content + claim to LLM via gl.nondet.exec_prompt()
- LLM returns: {"verdict": "true", "explanation": "The Eiffel Tower is 330m tall including its antenna"}
Other validators independently do the same
gl.eq_principle.strict_eq() compares all results
- All validators got "true" → consensus reached ✓
4. Claim updated:
verdict="true"
has_been_checked=True
5. Submitter receives +1 reputation point
Deploy Your Contract
Time to deploy this beast. Update the deployment script to point at our new contract.
Open deploy/deployScript.ts and change the file path:
import path from "path";
import { GenLayerClient } from "genlayer-js";
export default async function main(client: GenLayerClient<any>) {
// Point to our TruthPost contract
const contractFilePath = path.resolve("contracts/truth_post.py");
// ... rest stays the same
}
Then deploy:
npm run deploy
Copy that contract address—you'll need it when we build the frontend in Part 3.
Quick Reference: Core Concepts
| Concept | What It Does | Code Example |
|---|---|---|
| State variables | Persistent blockchain storage | claims: TreeMap[str, Claim] |
| Custom storage types | Store complex objects on-chain | @allow_storage @dataclass class Claim |
| Write methods | Modify state (costs gas) | @gl.public.write |
| View methods | Read state (free) | @gl.public.view |
| Web access | Fetch data from the internet | gl.nondet.web.render(url, mode="text") |
| LLM calls | Run AI prompts on-chain | gl.nondet.exec_prompt(prompt, response_format="json") |
| Equivalence Principle | Validators agree on AI outputs | gl.eq_principle.strict_eq(fn) |
| Sender address | Who called the method | gl.message.sender_address |
Prompt Engineering for On-Chain LLMs (Pro Tips)
Writing prompts for intelligent contracts is different from using ChatGPT. Your prompts need to produce consistent, structured outputs that validators can agree on.
5 Rules for Bulletproof On-Chain Prompts:
-
Always request JSON output—Use
response_format="json"and specify the exact schema - Use constrained labels—Give the LLM specific options instead of free-form answers
- Be explicit about format—Add "Respond ONLY with JSON, no extra text"
-
Sort your keys—Use
json.dumps(result, sort_keys=True)for deterministic ordering - Keep it simple—The simpler the output, the higher the agreement rate
Bad prompt:
"Tell me about this claim and whether it's true"Good prompt:
"Respond ONLY with JSON: {"verdict": "", "explanation": "<1 sentence>"}"
In Part 3, we're building the frontend—a React app that connects to your intelligent contract, lets users submit claims, and triggers AI fact-checks with the click of a button.
Next up: Part 3—Building the Frontend with Next.js & genlayer-js
This tutorial is part of the GenLayer Builder Program. Full source code: TruthPost repository
Follow the series:
- Part 1: Introduction & Setup
- Part 2: Writing the Intelligent Contract (you are here)
- Part 3: Building the Frontend
- Part 4: Testing & Deployment
Build the Frontend: Connecting React to Your AI-Powered Blockchain
Part 3: Next.js, MetaMask, and Making Magic Happen in the Browser
In Parts 1 and 2, we built an intelligent contract that can fetch web pages and run AI analysis on-chain. Now we're building the UI that brings it all to life.
Here's where it gets real.
You've got a smart contract that Googles things and runs AI. Now we need a way for actual humans to interact with it. Not through command line tools or developer consoles — through a clean, fast, modern web interface.
By the end of this part, you'll have a fully functional React app where users can:
- Connect their MetaMask wallet to the GenLayer network
- Submit claims for fact-checking
- Trigger AI fact-checks with a button click and see verdicts appear in real-time
- View a reputation leaderboard of active fact-checkers
The boilerplate already gives us the structure, wallet integration, and UI primitives. We just need to wire everything up for TruthPost.
Let's dive in.
The Frontend Stack (What We're Working With)
The boilerplate uses a modern React stack that'll feel familiar if you've built Next.js apps before:
| Library | What It Does |
|---|---|
| Next.js 15 | React framework with App Router |
| genlayer-js | GenLayer SDK — reads/writes to intelligent contracts |
| wagmi + viem | Wallet management & Ethereum utilities |
| TanStack Query | Server state management (caching, refetching) |
| Tailwind CSS | Utility-first styling |
| Radix UI | Accessible component primitives |
How genlayer-js Works (The Bridge to Your Contract)
Before we write any code, let's understand the SDK. genlayer-js is how your frontend talks to GenLayer:
import { createClient } from "genlayer-js";
import { studionet } from "genlayer-js/chains";
// Create a client connected to GenLayer
const client = createClient({
chain: studionet, // Network: studionet, localnet, or testnetAsimov
account: "0xYourAddress", // Optional: for signing transactions
});
// Read from a contract (free, no gas)
const result = await client.readContract({
address: "0xContractAddress",
functionName: "get_claims",
args: [],
});
// Write to a contract (costs gas, needs wallet)
const txHash = await client.writeContract({
address: "0xContractAddress",
functionName: "submit_claim",
args: ["The sky is blue", "https://example.com"],
value: BigInt(0),
});
// Wait for the transaction to be accepted
const receipt = await client.waitForTransactionReceipt({
hash: txHash,
status: "ACCEPTED",
});
Key patterns to remember:
-
readContractcalls@gl.public.viewmethods — free, no wallet needed -
writeContractcalls@gl.public.writemethods — costs gas, requires a connected wallet -
waitForTransactionReceiptpolls until the transaction reaches consensus - When you provide an
accountaddress, genlayer-js uses MetaMask for signing
Step 1: Configure Your Environment
Copy the environment template and add your deployed contract address:
cd frontend
cp .env.example .env
Edit frontend/.env:
NEXT_PUBLIC_GENLAYER_RPC_URL=https://studio.genlayer.com/api
NEXT_PUBLIC_GENLAYER_CHAIN_ID=61999
NEXT_PUBLIC_GENLAYER_CHAIN_NAME=GenLayer Studio
NEXT_PUBLIC_GENLAYER_SYMBOL=GEN
NEXT_PUBLIC_CONTRACT_ADDRESS=0xYOUR_DEPLOYED_CONTRACT_ADDRESS
Replace 0xYOUR_DEPLOYED_CONTRACT_ADDRESS with the address you got when you deployed in Part 2.
Step 2: Create the Contract Interaction Layer
The boilerplate uses a class-based pattern to wrap genlayer-js calls with TypeScript types. Let's create one for TruthPost.
Define Types
Create frontend/lib/contracts/types.ts:
export interface Claim {
id: string;
text: string;
verdict: string; // "pending" | "true" | "false" | "partially_true"
explanation: string;
source_url: string;
submitter: string; // hex address
has_been_checked: boolean;
}
export interface ReputationEntry {
address: string;
reputation: number;
}
export interface TransactionReceipt {
status: string;
hash: string;
blockNumber?: number;
[key: string]: any;
}
Create the Contract Class
Create frontend/lib/contracts/TruthPost.ts:
import { createClient } from "genlayer-js";
import { studionet } from "genlayer-js/chains";
import type { Claim, ReputationEntry, TransactionReceipt } from "./types";
class TruthPost {
private contractAddress: `0x${string}`;
private client: ReturnType<typeof createClient>;
constructor(
contractAddress: string,
address?: string | null,
studioUrl?: string
) {
this.contractAddress = contractAddress as `0x${string}`;
const config: any = {
chain: studionet,
};
// If a wallet address is provided, the SDK uses MetaMask for signing
if (address) {
config.account = address as `0x${string}`;
}
if (studioUrl) {
config.endpoint = studioUrl;
}
this.client = createClient(config);
}
/**
* Recreate client when user switches wallet accounts
*/
updateAccount(address: string): void {
this.client = createClient({
chain: studionet,
account: address as `0x${string}`,
});
}
// ─── READ METHODS (View Functions) ─────────────────────────
/**
* Fetch all claims from the contract
*/
async getClaims(): Promise<Claim[]> {
const result: any = await this.client.readContract({
address: this.contractAddress,
functionName: "get_claims",
args: [],
});
// genlayer-js returns Maps for TreeMap data — convert to array
if (result instanceof Map) {
return Array.from(result.entries()).map(([id, claimData]: any) => {
// Each claim value is also a Map of field -> value
const obj = claimData instanceof Map
? Object.fromEntries(claimData.entries())
: claimData;
return { id, ...obj } as Claim;
});
}
return [];
}
/**
* Get a specific claim by ID
*/
async getClaim(claimId: string): Promise<Claim> {
const result: any = await this.client.readContract({
address: this.contractAddress,
functionName: "get_claim",
args: [claimId],
});
if (result instanceof Map) {
return Object.fromEntries(result.entries()) as Claim;
}
return result as Claim;
}
/**
* Get reputation for a specific user
*/
async getUserReputation(address: string | null): Promise<number> {
if (!address) return 0;
const result = await this.client.readContract({
address: this.contractAddress,
functionName: "get_user_reputation",
args: [address],
});
return Number(result) || 0;
}
/**
* Get the reputation leaderboard
*/
async getLeaderboard(): Promise<ReputationEntry[]> {
const result: any = await this.client.readContract({
address: this.contractAddress,
functionName: "get_reputation",
args: [],
});
if (result instanceof Map) {
return Array.from(result.entries())
.map(([address, rep]: any) => ({
address,
reputation: Number(rep),
}))
.sort((a, b) => b.reputation - a.reputation);
}
return [];
}
// ─── WRITE METHODS (State-Changing Functions) ──────────────
/**
* Submit a new claim to be fact-checked
*/
async submitClaim(
claimText: string,
sourceUrl: string
): Promise<TransactionReceipt> {
// writeContract sends a transaction signed by MetaMask
const txHash = await this.client.writeContract({
address: this.contractAddress,
functionName: "submit_claim",
args: [claimText, sourceUrl],
value: BigInt(0),
});
// Wait for validators to reach consensus
const receipt = await this.client.waitForTransactionReceipt({
hash: txHash,
status: "ACCEPTED" as any,
retries: 24, // Check up to 24 times
interval: 5000, // Every 5 seconds (2 min total)
});
return receipt as TransactionReceipt;
}
/**
* Trigger AI fact-check for a pending claim
* This is where the magic happens — the contract fetches web data
* and uses AI to analyze the claim!
*/
async resolveClaim(claimId: string): Promise<TransactionReceipt> {
const txHash = await this.client.writeContract({
address: this.contractAddress,
functionName: "resolve_claim",
args: [claimId],
value: BigInt(0),
});
// This can take longer because the AI fact-check runs during consensus
const receipt = await this.client.waitForTransactionReceipt({
hash: txHash,
status: "ACCEPTED" as any,
retries: 24,
interval: 5000,
});
return receipt as TransactionReceipt;
}
}
export default TruthPost;
Important pattern: Map conversion
GenLayer contracts use TreeMap for storage, which genlayer-js returns as JavaScript Map objects. You need to convert these to plain arrays/objects for React:
// TreeMap comes back as Map
if (result instanceof Map) {
return Array.from(result.entries()).map(([key, value]) => ...);
}
Step 3: Create React Hooks with TanStack Query
TanStack Query handles caching, refetching, and loading states. Let's create hooks for our contract.
Create frontend/lib/hooks/useTruthPost.ts:
"use client";
import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query";
import { useMemo, useState } from "react";
import TruthPost from "../contracts/TruthPost";
import { getContractAddress, getStudioUrl } from "../genlayer/client";
import { useWallet } from "../genlayer/wallet";
import type { Claim, ReputationEntry } from "../contracts/types";
/**
* Hook to get the TruthPost contract instance
* Recreated when wallet address changes
*/
export function useTruthPostContract(): TruthPost | null {
const { address } = useWallet();
const contractAddress = getContractAddress();
const studioUrl = getStudioUrl();
return useMemo(() => {
if (!contractAddress) return null;
return new TruthPost(contractAddress, address, studioUrl);
}, [contractAddress, address, studioUrl]);
}
/**
* Hook to fetch all claims
*/
export function useClaims() {
const contract = useTruthPostContract();
return useQuery<Claim[], Error>({
queryKey: ["claims"],
queryFn: () => contract?.getClaims() ?? Promise.resolve([]),
refetchOnWindowFocus: true,
staleTime: 2000,
enabled: !!contract,
});
}
/**
* Hook to fetch user reputation
*/
export function useUserReputation(address: string | null) {
const contract = useTruthPostContract();
return useQuery<number, Error>({
queryKey: ["reputation", address],
queryFn: () => contract?.getUserReputation(address) ?? Promise.resolve(0),
enabled: !!address && !!contract,
staleTime: 2000,
});
}
/**
* Hook to fetch the reputation leaderboard
*/
export function useLeaderboard() {
const contract = useTruthPostContract();
return useQuery<ReputationEntry[], Error>({
queryKey: ["leaderboard"],
queryFn: () => contract?.getLeaderboard() ?? Promise.resolve([]),
refetchOnWindowFocus: true,
staleTime: 2000,
enabled: !!contract,
});
}
/**
* Hook to submit a new claim
*/
export function useSubmitClaim() {
const contract = useTruthPostContract();
const { address } = useWallet();
const queryClient = useQueryClient();
const [isSubmitting, setIsSubmitting] = useState(false);
const mutation = useMutation({
mutationFn: async ({
claimText,
sourceUrl,
}: {
claimText: string;
sourceUrl: string;
}) => {
if (!contract) throw new Error("Contract not configured");
if (!address) throw new Error("Wallet not connected");
setIsSubmitting(true);
return contract.submitClaim(claimText, sourceUrl);
},
onSuccess: () => {
// Refresh all data after submitting a claim
queryClient.invalidateQueries({ queryKey: ["claims"] });
queryClient.invalidateQueries({ queryKey: ["reputation"] });
queryClient.invalidateQueries({ queryKey: ["leaderboard"] });
setIsSubmitting(false);
},
onError: (err: any) => {
console.error("Error submitting claim:", err);
setIsSubmitting(false);
},
});
return {
...mutation,
isSubmitting,
submitClaim: mutation.mutate,
};
}
/**
* Hook to resolve (fact-check) a claim
*/
export function useResolveClaim() {
const contract = useTruthPostContract();
const { address } = useWallet();
const queryClient = useQueryClient();
const [isResolving, setIsResolving] = useState(false);
const [resolvingClaimId, setResolvingClaimId] = useState<string | null>(null);
const mutation = useMutation({
mutationFn: async (claimId: string) => {
if (!contract) throw new Error("Contract not configured");
if (!address) throw new Error("Wallet not connected");
setIsResolving(true);
setResolvingClaimId(claimId);
return contract.resolveClaim(claimId);
},
onSuccess: () => {
queryClient.invalidateQueries({ queryKey: ["claims"] });
queryClient.invalidateQueries({ queryKey: ["reputation"] });
queryClient.invalidateQueries({ queryKey: ["leaderboard"] });
setIsResolving(false);
setResolvingClaimId(null);
},
onError: (err: any) => {
console.error("Error resolving claim:", err);
setIsResolving(false);
setResolvingClaimId(null);
},
});
return {
...mutation,
isResolving,
resolvingClaimId,
resolveClaim: mutation.mutate,
};
}
Why TanStack Query?
Blockchain reads are like API calls — they're async, can fail, and the data goes stale. TanStack Query gives us:
- Automatic caching — don't re-fetch data unnecessarily
-
Loading/error states —
isLoading,isError,errorfor free - Cache invalidation — after a write, we invalidate queries so the UI refreshes
- Refetch on focus — data updates when the user comes back to the tab
Step 4: The Wallet Connection (Already Done!)
The boilerplate includes complete wallet integration with MetaMask. Here's what it provides:
WalletProvider (context) manages:
- Connecting/disconnecting MetaMask
- Listening for account and network changes
- Auto-reconnecting on page refresh
- Network switching to GenLayer
useWallet() hook gives you:
const {
address, // Current wallet address (or null)
isConnected, // Boolean: is wallet connected?
isLoading, // Boolean: connection in progress?
isMetaMaskInstalled, // Boolean: is MetaMask available?
isOnCorrectNetwork, // Boolean: on GenLayer network?
connectWallet, // Function: trigger MetaMask connection
disconnectWallet, // Function: clear wallet state
switchWalletAccount, // Function: show account picker
} = useWallet();
You don't need to modify the wallet code. It works with any GenLayer contract out of the box.
Step 5: Build the Claims List Component
Time to build the UI. Let's create a component that displays all claims:
// frontend/components/ClaimsList.tsx
"use client";
import { useClaims, useResolveClaim } from "../lib/hooks/useTruthPost";
import { useWallet } from "../lib/genlayer/wallet";
function VerdictBadge({ verdict }: { verdict: string }) {
const colors: Record<string, string> = {
true: "bg-green-500/20 text-green-400 border-green-500/30",
false: "bg-red-500/20 text-red-400 border-red-500/30",
partially_true: "bg-yellow-500/20 text-yellow-400 border-yellow-500/30",
pending: "bg-gray-500/20 text-gray-400 border-gray-500/30",
};
const labels: Record<string, string> = {
true: "True",
false: "False",
partially_true: "Partially True",
pending: "Pending",
};
return (
<span className={`px-2 py-1 rounded-full text-xs border ${colors[verdict] || colors.pending}`}>
{labels[verdict] || verdict}
</span>
);
}
export default function ClaimsList() {
const { data: claims, isLoading, error } = useClaims();
const { resolveClaim, isResolving, resolvingClaimId } = useResolveClaim();
const { address, isConnected } = useWallet();
if (isLoading) return <div className="text-center p-8">Loading claims...</div>;
if (error) return <div className="text-red-400 p-8">Error: {error.message}</div>;
if (!claims?.length) return <div className="text-center p-8">No claims yet. Be the first!</div>;
return (
<div className="space-y-4">
<h2 className="text-xl font-bold">Submitted Claims</h2>
{claims.map((claim) => (
<div key={claim.id} className="border border-white/10 rounded-lg p-4 space-y-2">
{/* Claim text */}
<p className="text-lg font-medium">{claim.text}</p>
{/* Metadata row */}
<div className="flex items-center gap-3 text-sm text-gray-400">
<VerdictBadge verdict={claim.verdict} />
<span>Source: {new URL(claim.source_url).hostname}</span>
<span>By: {claim.submitter.slice(0, 6)}...{claim.submitter.slice(-4)}</span>
</div>
{/* AI explanation (shown after fact-check) */}
{claim.has_been_checked && claim.explanation && (
<p className="text-sm text-gray-300 bg-white/5 rounded p-3 mt-2">
AI Analysis: {claim.explanation}
</p>
)}
{/* Resolve button (only for pending claims) */}
{!claim.has_been_checked && isConnected && (
<button
onClick={() => resolveClaim(claim.id)}
disabled={isResolving}
className="mt-2 px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded text-sm disabled:opacity-50"
>
{isResolving && resolvingClaimId === claim.id
? "Fact-checking with AI..."
: "Fact-Check This Claim"
}
</button>
)}
</div>
))}
</div>
);
}
What's happening:
-
useClaims()fetches all claims from the contract -
useResolveClaim()gives us the mutation to trigger a fact-check - Each claim shows its text, verdict badge, source, and submitter
- Pending claims show a "Fact-Check This Claim" button
- When clicked, it triggers the on-chain AI fact-check
- While resolving, the button shows "Fact-checking with AI..." — because GenLayer is actually fetching the web and running an LLM!
Step 6: Build the Submit Claim Form
// frontend/components/SubmitClaimModal.tsx
"use client";
import { useState } from "react";
import { useSubmitClaim } from "../lib/hooks/useTruthPost";
import { useWallet } from "../lib/genlayer/wallet";
export default function SubmitClaimModal() {
const [isOpen, setIsOpen] = useState(false);
const [claimText, setClaimText] = useState("");
const [sourceUrl, setSourceUrl] = useState("");
const { submitClaim, isSubmitting } = useSubmitClaim();
const { isConnected } = useWallet();
const handleSubmit = (e: React.FormEvent) => {
e.preventDefault();
if (!claimText.trim() || !sourceUrl.trim()) return;
submitClaim(
{ claimText: claimText.trim(), sourceUrl: sourceUrl.trim() },
{
onSuccess: () => {
setClaimText("");
setSourceUrl("");
setIsOpen(false);
},
}
);
};
if (!isConnected) return null;
return (
<>
<button
onClick={() => setIsOpen(true)}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded font-medium"
>
Submit a Claim
</button>
{isOpen && (
<div className="fixed inset-0 bg-black/60 flex items-center justify-center z-50">
<div className="bg-gray-900 border border-white/10 rounded-xl p-6 w-full max-w-md">
<h2 className="text-xl font-bold mb-4">Submit a Claim for Fact-Checking</h2>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm text-gray-400 mb-1">Claim</label>
<textarea
value={claimText}
onChange={(e) => setClaimText(e.target.value)}
placeholder='e.g., "The Great Wall of China is visible from space"'
className="w-full bg-white/5 border border-white/10 rounded p-3 text-sm"
rows={3}
required
/>
</div>
<div>
<label className="block text-sm text-gray-400 mb-1">Source URL</label>
<input
type="url"
value={sourceUrl}
onChange={(e) => setSourceUrl(e.target.value)}
placeholder="https://en.wikipedia.org/wiki/Great_Wall_of_China"
className="w-full bg-white/5 border border-white/10 rounded p-3 text-sm"
required
/>
<p className="text-xs text-gray-500 mt-1">
The AI will fetch this URL to verify the claim
</p>
</div>
<div className="flex gap-3 justify-end">
<button
type="button"
onClick={() => setIsOpen(false)}
className="px-4 py-2 text-gray-400 hover:text-white"
>
Cancel
</button>
<button
type="submit"
disabled={isSubmitting || !claimText.trim() || !sourceUrl.trim()}
className="px-4 py-2 bg-blue-600 hover:bg-blue-700 rounded disabled:opacity-50"
>
{isSubmitting ? "Submitting..." : "Submit Claim"}
</button>
</div>
</form>
</div>
</div>
)}
</>
);
}
Step 7: Wire It All Together
Update the main page to use our TruthPost components.
Edit frontend/app/page.tsx:
import ClaimsList from "../components/ClaimsList";
import SubmitClaimModal from "../components/SubmitClaimModal";
export default function Home() {
return (
<main className="min-h-screen p-8 max-w-4xl mx-auto">
<div className="text-center mb-12">
<h1 className="text-4xl font-bold mb-4">TruthPost</h1>
<p className="text-gray-400 text-lg">
Decentralized fact-checking powered by AI on GenLayer
</p>
</div>
{/* Submit button + Claims list */}
<div className="mb-6 flex justify-end">
<SubmitClaimModal />
</div>
<ClaimsList />
{/* How it works */}
<div className="mt-16 border-t border-white/10 pt-8">
<h2 className="text-2xl font-bold mb-6 text-center">How It Works</h2>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div className="text-center p-4">
<div className="text-3xl mb-2">1</div>
<h3 className="font-bold mb-1">Submit a Claim</h3>
<p className="text-sm text-gray-400">
Enter any factual claim and a source URL for verification
</p>
</div>
<div className="text-center p-4">
<div className="text-3xl mb-2">2</div>
<h3 className="font-bold mb-1">AI Fact-Checks It</h3>
<p className="text-sm text-gray-400">
GenLayer fetches the source and uses AI to analyze the claim on-chain
</p>
</div>
<div className="text-center p-4">
<div className="text-3xl mb-2">3</div>
<h3 className="font-bold mb-1">Validators Agree</h3>
<p className="text-sm text-gray-400">
Multiple validators independently verify through Optimistic Democracy
</p>
</div>
</div>
</div>
</main>
);
}
Step 8: Fire It Up!
cd frontend
npm run dev
Open http://localhost:3000 and watch the magic happen.
Try it out:
- Click "Connect Wallet" in the navbar — MetaMask will prompt you to connect and switch to GenLayer
-
Click "Submit a Claim" — try something like "Python was created by Guido van Rossum" with source
https://en.wikipedia.org/wiki/Python_(programming_language) - After the claim appears, click "Fact-Check This Claim"
-
Wait ~30 seconds while GenLayer:
- Fetches the Wikipedia page
- Sends it to an LLM for analysis
- Validators reach consensus on the verdict
- See the verdict appear: True with an AI-generated explanation!
Understanding the Data Flow
Let's trace what happens when a user submits a claim:
User clicks "Submit Claim"
↓
SubmitClaimModal calls submitClaim()
↓
useTruthPost hook calls contract.submitClaim()
↓
genlayer-js calls writeContract()
↓
MetaMask signs the transaction
↓
Transaction sent to GenLayer network
↓
Validators execute submit_claim() in the Python contract
↓
Claim stored on-chain
↓
waitForTransactionReceipt() resolves
↓
TanStack Query invalidates cache
↓
ClaimsList re-renders with new claim
User clicks "Fact-Check This Claim"
↓
ClaimsList calls resolveClaim()
↓
genlayer-js sends resolve_claim transaction
↓
Leader validator runs _fact_check():
↓
gl.nondet.web.render() fetches the URL
↓
gl.nondet.exec_prompt() asks AI to analyze
↓
Result: {"verdict": "true", "explanation": "..."}
↓
Other validators do the same independently
↓
gl.eq_principle.strict_eq() checks consensus
↓
All agree → transaction accepted!
↓
UI updates with verdict and explanation
The Wallet Flow Explained
The boilerplate's wallet integration handles several important flows automatically:
Connecting:
- User clicks "Connect Wallet"
- MetaMask popup asks for permission
- App checks if user is on GenLayer network
- If not, prompts to add/switch to GenLayer
- Connection persisted (auto-reconnects on refresh)
Account switching:
- MetaMask fires
accountsChangedevent - WalletProvider updates state automatically
- Contract hooks recreate the genlayer-js client with new address
- All queries refetch with new account context
Network validation:
- App checks
chainIdmatches GenLayer's (61999) - Shows warning if on wrong network
- Prompts to switch automatically
Key Frontend Patterns Recap
| Pattern | What It Does | File |
|---|---|---|
| Contract class | Typed wrapper around genlayer-js | lib/contracts/TruthPost.ts |
| Map conversion | TreeMap → JavaScript arrays |
getClaims() method |
| TanStack Query hooks | Caching, loading states, auto-refetch | lib/hooks/useTruthPost.ts |
| Cache invalidation | Refresh UI after writes |
onSuccess in mutations |
| Wallet context | Global wallet state for all components | lib/genlayer/WalletProvider.tsx |
| Conditional rendering | Show/hide based on wallet state |
isConnected checks |
In Part 4, we'll test this thing properly with gltest, deploy to the public testnet, and explore what else you can build on GenLayer.
Next up: Part 4 — Testing, Deployment & What to Build Next
This tutorial is part of the GenLayer Builder Program. Full source code: TruthPost repository
Ship It: Testing, Deployment, and What's Next
Part 4: Making Sure It Works, Going Live, and Building the Impossible
We've built TruthPost — an AI-powered fact-checker running on blockchain. Now we're testing it properly, deploying to the public testnet, and exploring what else becomes possible when smart contracts can think.
You did it. You built something that doesn't exist anywhere else in crypto.
A dApp where smart contracts access the internet, run AI models, and reach consensus on subjective outputs. On Ethereum? Impossible. On Solana? Can't be done. On GenLayer? Shipped.
But before you start showing this off to the world, let's make sure it actually works. In this final part, we'll:
- Write proper tests using
gltest(GenLayer's testing framework) - Update the deployment script for production
- Deploy to the GenLayer testnet
- Explore ideas for extending TruthPost
- Talk about what else you can build on GenLayer
Let's finish strong.
Testing Intelligent Contracts with gltest
GenLayer provides gltest — a testing framework that deploys your contract to a local GenLayer instance and runs real transactions against it. This means your tests exercise actual AI and web access, not mocks.
Important: Tests require GenLayer Studio to be running, because
gltestsends real transactions to the local network.
Setting Up Tests
Make sure you have the Python dependencies installed:
pip install -r requirements.txt
Writing Test Fixtures
First, let's define the expected data structures our tests will validate.
Create test/truth_post_fixtures.py:
# Expected claim state after submission (before fact-check)
test_claim_pending = {
"claim_1": {
"id": "claim_1",
"text": "Python was created by Guido van Rossum",
"verdict": "pending",
"explanation": "",
"source_url": "https://en.wikipedia.org/wiki/Python_(programming_language)",
"submitter": None, # Will be set to default_account.address
"has_been_checked": False,
}
}
Writing the Test Suite
Create test/test_truth_post.py:
from gltest import get_contract_factory, default_account
from gltest.helpers import load_fixture
from gltest.assertions import tx_execution_succeeded
def deploy_contract():
"""Deploy a fresh TruthPost contract and verify initial state."""
factory = get_contract_factory("TruthPost")
contract = factory.deploy()
# Verify initial state is empty
all_claims = contract.get_claims(args=[])
assert all_claims == {}
all_reputation = contract.get_reputation(args=[])
assert all_reputation == {}
return contract
def test_submit_claim():
"""Test that a user can submit a claim."""
contract = load_fixture(deploy_contract)
# Submit a claim
result = contract.submit_claim(
args=[
"Python was created by Guido van Rossum",
"https://en.wikipedia.org/wiki/Python_(programming_language)",
]
)
assert tx_execution_succeeded(result)
# Verify claim was stored
claims = contract.get_claims(args=[])
assert "claim_1" in str(claims)
def test_resolve_claim_true():
"""Test that a factually true claim is correctly verified."""
contract = load_fixture(deploy_contract)
# Submit a claim that should be TRUE
submit_result = contract.submit_claim(
args=[
"Python was created by Guido van Rossum",
"https://en.wikipedia.org/wiki/Python_(programming_language)",
]
)
assert tx_execution_succeeded(submit_result)
# Resolve (fact-check) the claim
# This triggers web fetching + AI analysis + validator consensus!
resolve_result = contract.resolve_claim(
args=["claim_1"],
wait_interval=10000, # 10 seconds between checks
wait_retries=15, # Up to 15 retries (2.5 min total)
)
assert tx_execution_succeeded(resolve_result)
# Verify the verdict
claim = contract.get_claim(args=["claim_1"])
assert claim["has_been_checked"] == True
assert claim["verdict"] == "true"
# Verify reputation was awarded
reputation = contract.get_user_reputation(args=[default_account.address])
assert reputation == 1
def test_resolve_claim_false():
"""Test that a factually false claim is correctly identified."""
contract = load_fixture(deploy_contract)
# Submit a claim that should be FALSE
submit_result = contract.submit_claim(
args=[
"The Great Wall of China is visible from space with the naked eye",
"https://en.wikipedia.org/wiki/Great_Wall_of_China",
]
)
assert tx_execution_succeeded(submit_result)
# Resolve the claim
resolve_result = contract.resolve_claim(
args=["claim_1"],
wait_interval=10000,
wait_retries=15,
)
assert tx_execution_succeeded(resolve_result)
# Verify it was marked as false
claim = contract.get_claim(args=["claim_1"])
assert claim["has_been_checked"] == True
assert claim["verdict"] == "false"
def test_cannot_resolve_twice():
"""Test that a claim cannot be fact-checked twice."""
contract = load_fixture(deploy_contract)
# Submit and resolve
contract.submit_claim(
args=[
"Python was created by Guido van Rossum",
"https://en.wikipedia.org/wiki/Python_(programming_language)",
]
)
contract.resolve_claim(
args=["claim_1"],
wait_interval=10000,
wait_retries=15,
)
# Try to resolve again — should fail
try:
contract.resolve_claim(
args=["claim_1"],
wait_interval=10000,
wait_retries=15,
)
assert False, "Should have raised an exception"
except Exception:
pass # Expected — claim already resolved
Running Tests
gltest
Note: Tests involving
resolve_claimwill take longer because GenLayer is actually:
- Fetching a web page
- Running an LLM prompt
- Having validators reach consensus
This is real AI execution, not a mock! Allow ~30-60 seconds per resolve test.
Understanding gltest Patterns
| Pattern | What It Does |
|---|---|
get_contract_factory("Name") |
Gets a factory for deploying a contract by class name |
factory.deploy() |
Deploys a fresh instance and returns a contract proxy |
contract.method_name(args=[...]) |
Calls a contract method |
load_fixture(fn) |
Caches the result of a deploy function across tests |
tx_execution_succeeded(result) |
Asserts the transaction reached consensus |
default_account |
The test account with .address property |
wait_interval / wait_retries
|
For non-deterministic calls that take time |
Updating the Deployment Script
Update deploy/deployScript.ts to deploy TruthPost instead of the example contract:
import { readFileSync } from "fs";
import path from "path";
import {
TransactionHash,
TransactionStatus,
GenLayerClient,
DecodedDeployData,
GenLayerChain,
} from "genlayer-js/types";
import { localnet } from "genlayer-js/chains";
export default async function main(client: GenLayerClient<any>) {
// Point to our TruthPost contract
const filePath = path.resolve(process.cwd(), "contracts/truth_post.py");
try {
const contractCode = new Uint8Array(readFileSync(filePath));
await client.initializeConsensusSmartContract();
const deployTransaction = await client.deployContract({
code: contractCode,
args: [],
});
const receipt = await client.waitForTransactionReceipt({
hash: deployTransaction as TransactionHash,
status: TransactionStatus.ACCEPTED,
retries: 200,
});
if (
receipt.status !== 5 &&
receipt.status !== 6 &&
receipt.statusName !== "ACCEPTED" &&
receipt.statusName !== "FINALIZED"
) {
throw new Error(`Deployment failed. Receipt: ${JSON.stringify(receipt)}`);
}
const deployedContractAddress =
(client.chain as GenLayerChain).id === localnet.id
? receipt.data.contract_address
: (receipt.txDataDecoded as DecodedDeployData)?.contractAddress;
console.log(`Contract deployed at address: ${deployedContractAddress}`);
console.log(`\nNext steps:`);
console.log(`1. Copy the address above`);
console.log(`2. Set NEXT_PUBLIC_CONTRACT_ADDRESS in frontend/.env`);
console.log(`3. Run: cd frontend && npm run dev`);
} catch (error) {
throw new Error(`Error during deployment: ${error}`);
}
}
Deploying to Testnet
So far we've used studionet (the development environment). Let's deploy to the real GenLayer testnet where anyone can interact with your dApp.
Step 1: Switch Network
genlayer network
# Select "testnet" from the menu
Step 2: Get Test Tokens
You'll need test GEN tokens for gas. Visit the GenLayer faucet to get some for your MetaMask address.
Step 3: Deploy
npm run deploy
Step 4: Update Frontend Config
Edit frontend/.env:
NEXT_PUBLIC_GENLAYER_RPC_URL=<testnet RPC URL>
NEXT_PUBLIC_CONTRACT_ADDRESS=0xYOUR_NEW_TESTNET_ADDRESS
Step 5: Run & Share
cd frontend
npm run dev
Your dApp is now running against the public testnet! Anyone can connect their wallet and fact-check claims.
What You've Built — A Reality Check
Let's step back and appreciate what you've accomplished over this tutorial:
You built an AI-powered decentralized fact-checker that:
- Runs on a blockchain (trustless, censorship-resistant)
- Fetches real web data from inside a smart contract (no oracles!)
- Uses AI to analyze claims (on-chain LLM access!)
- Reaches consensus on subjective AI outputs (Equivalence Principle!)
- Has a modern React frontend with wallet integration
This would be impossible on Ethereum, Solana, or any other blockchain. GenLayer is the first platform where this kind of application can exist.
Architecture Diagram (The Full Picture)
┌─────────────────────────────────────────────────────┐
│ Frontend (Next.js) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────────────────┐ │
│ │ Claims │ │ Submit │ │ Wallet Provider │ │
│ │ List │ │ Modal │ │ (MetaMask) │ │
│ └────┬─────┘ └────┬─────┘ └────────┬──────────┘ │
│ │ │ │ │
│ ┌────┴──────────────┴─────────────────┴──────────┐ │
│ │ TanStack Query Hooks │ │
│ │ useClaims() | useSubmitClaim() | useResolve() │ │
│ └────────────────────┬────────────────────────────┘ │
│ │ │
│ ┌────────────────────┴────────────────────────────┐ │
│ │ TruthPost Contract Class │ │
│ │ (genlayer-js wrapper) │ │
│ └────────────────────┬────────────────────────────┘ │
└───────────────────────┼──────────────────────────────┘
│ RPC
┌───────────────────────┼──────────────────────────────┐
│ GenLayer Network │
│ │ │
│ ┌────────────────────┴────────────────────────────┐ │
│ │ TruthPost Contract (Python) │ │
│ │ │ │
│ │ submit_claim() ──→ Store claim on-chain │ │
│ │ resolve_claim() ──→ _fact_check() │ │
│ │ ├─ web.render() → Internet │ │
│ │ ├─ exec_prompt() → LLM │ │
│ │ └─ strict_eq() → Consensus │ │
│ └───────────────────────────────────────────────────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Leader │ │Validator │ │Validator │ ... │
│ │ (runs │ │ (verifies│ │ (verifies│ │
│ │ first) │ │ result) │ │ result) │ │
│ └──────────┘ └──────────┘ └──────────┘ │
└─────────────────────────────────────────────────────────┘
Ideas to Extend TruthPost
Now that you understand the fundamentals, here are ways to make TruthPost more powerful:
1. Add Staking
Make users stake GEN tokens when submitting claims. If the claim turns out to be intentionally misleading, they lose their stake:
@gl.public.write.payable
def submit_claim(self, claim_text: str, source_url: str) -> None:
if gl.message.value < 100:
raise Exception("Minimum stake: 100 GEN")
# ... rest of logic
2. Use Comparative Equivalence
Instead of strict equality, use comparative equivalence for more nuanced verdicts:
result = gl.eq_principle.prompt_comparative(
check_claim,
"Results are equivalent if they agree on the same verdict (true/false/partially_true) "
"and the explanations convey the same core reasoning, even if worded differently."
)
3. Multi-Source Verification
Fetch multiple sources and cross-reference:
def _multi_source_check(self, claim_text, urls):
def check():
results = []
for url in urls:
web_data = gl.nondet.web.render(url, mode="text")
results.append(web_data)
combined = "\n---\n".join(results)
prompt = f"Based on these {len(urls)} sources, fact-check: {claim_text}\n{combined}"
result = gl.nondet.exec_prompt(prompt, response_format="json")
return json.dumps(result, sort_keys=True)
return json.loads(gl.eq_principle.strict_eq(check))
4. Dispute System
Let users challenge verdicts, triggering a re-check with different sources or additional validators.
5. Screenshot Verification
Use gl.nondet.web.render(url, mode="screenshot") to capture visual evidence and analyze it with gl.nondet.exec_prompt() using image support.
What Else Can You Build on GenLayer?
GenLayer opens up entire categories of applications that were impossible before. Here's what becomes possible:
| Category | Example | GenLayer Feature Used |
|---|---|---|
| Dispute Resolution | Arbitration between trustless parties | LLM analyzes evidence, validators vote |
| P2P Betting | Friends bet on subjective outcomes | Web scraping for results, AI determines winners |
| Parametric Insurance | Insurance based on real events | Web access verifies claims automatically |
| Content Moderation | Decentralized content policy enforcement | LLM evaluates content against community rules |
| Prediction Markets | Markets on real-world events | Web + AI settle outcomes objectively |
| Reputation Systems | On-chain skill/trust scoring | AI evaluates contributions and work quality |
| Dynamic NFTs | NFTs that evolve based on real-world data | Web access feeds dynamic metadata |
| Oracle Replacement | Any data from any website |
gl.nondet.web replaces Chainlink for many use cases |
GenLayer vs. Traditional Blockchain (The Final Comparison)
| Feature | Ethereum/Solana | GenLayer |
|---|---|---|
| Smart contract language | Solidity/Rust | Python |
| Internet access | No (need oracles) | Yes, native |
| AI/LLM access | No | Yes, native |
| Non-deterministic logic | Impossible | Yes, via Equivalence Principle |
| Consensus on subjective data | Impossible | Yes, via Optimistic Democracy |
| Cost of web data | High (oracle fees) | Included in gas |
Resources for Your Next Steps
| Resource | Link |
|---|---|
| GenLayer Docs | docs.genlayer.com |
| SDK API Reference | sdk.genlayer.com |
| genlayer-js SDK | docs.genlayer.com/api-references/genlayer-js |
| GenLayer Studio | studio.genlayer.com |
| Contract Examples | docs.genlayer.com/developers/intelligent-contracts/examples |
| Community Discord | Check docs for invite link |
What You've Learned (The Complete Journey)
You went from zero to a working GenLayer dApp in four parts:
Part 1: Understood GenLayer's breakthrough concepts — Optimistic Democracy and the Equivalence Principle — and set up your environment
Part 2: Wrote a Python intelligent contract that fetches web data and uses AI on-chain, doing things impossible on any other blockchain
Part 3: Built a React frontend connected via genlayer-js and MetaMask, with proper state management and wallet integration
Part 4: Tested with gltest, deployed to testnet, and explored what's next
The Bottom Line
GenLayer represents a fundamental shift in what's possible on blockchains.
Smart contracts can now think, read the internet, and make subjective decisions — all while maintaining the security and trustlessness that blockchain promises.
The question isn't "What can I build?" anymore.
It's "What couldn't I build before?"
- Want to build a betting platform that automatically settles based on real-world events? Build it.
- Want to create a dispute resolution system that uses AI to analyze evidence? Build it.
- Want NFTs that evolve based on real-time data from any website? Build it.
- Want insurance that pays out automatically when verifiable conditions are met? Build it.
GenLayer makes all of this possible. Not someday. Not with workarounds. Today.
Welcome to GenLayer. Go build something impossible.
This tutorial is part of the GenLayer Builder Program. Full source code: TruthPost repository
The GenLayer Cheat Sheet: Everything on One Page
Your quick reference for building Intelligent Contracts and dApps on GenLayer. Bookmark this. You'll need it.
CLI Commands (The Essentials)
genlayer network # Switch networks (studionet / localnet / testnet)
npm run deploy # Deploy your contract
npm run dev # Fire up the frontend
gltest # Run tests (needs GenLayer Studio running)
Contract Skeleton (Start Here Every Time)
Every GenLayer contract follows this structure. Copy this, modify it, ship it:
from genlayer import *
@allow_storage # Required for any custom type stored on-chain
@dataclass
class MyData:
name: str
value: u256
class MyContract(gl.Contract):
# State — persisted on-chain between calls
data: TreeMap[Address, MyData]
count: u256
def __init__(self): # Runs once, at deploy time
self.count = 0
@gl.public.view # Read-only — free, no gas
def get_count(self) -> int:
return self.count
@gl.public.write # Modifies state — costs gas
def increment(self) -> None:
self.count += 1
@gl.public.write.payable # Accepts GEN tokens
def pay(self) -> None:
amount = gl.message.value
Storage Types (Python → GenLayer Translation Guide)
| Python Type | GenLayer Equivalent | Why the Change? |
|---|---|---|
dict |
TreeMap[K, V] |
Blockchain-optimized (keys must support <) |
list |
DynArray[T] |
Dynamic arrays that play nice with consensus |
int |
u256, i64, etc. |
Fixed-size integers for determinism |
| custom class | @allow_storage @dataclass |
Needs the decorator to persist on-chain |
TreeMap Operations (Your New Best Friend)
self.data[key] = value # Set
val = self.data[key] # Get (raises KeyError if missing)
val = self.data.get(key, default) # Get with fallback
del self.data[key] # Delete
for k, v in self.data.items(): # Iterate
self.data.get_or_insert_default(key) # Get or create with type default
DynArray Operations (When You Need a List)
self.arr.append(item) # Add to end
self.arr.pop() # Remove last
self.arr[0] # Index access
len(self.arr) # Length
Web Access (The Game Changer)
Your contracts can fetch live data from the internet. No oracles. No middleware. Just code.
# Rendered page as plain text (best for content extraction)
text = gl.nondet.web.render(url, mode="text")
# Raw HTML
html = gl.nondet.web.render(url, mode="html")
# Screenshot (returns Image object — can be passed to LLM)
img = gl.nondet.web.render(url, mode="screenshot")
# HTTP GET with headers
resp = gl.nondet.web.get(url, headers={"Authorization": "Bearer ..."})
# resp.status, resp.headers, resp.body
# HTTP POST
resp = gl.nondet.web.post(url, body="payload", headers={})
LLM / AI Access (The Other Game Changer)
Your contracts can think. Run prompts through AI models and get structured responses.
# Free-form text response
answer = gl.nondet.exec_prompt("Summarize this article: ...")
# Structured JSON response (use this for reliable parsing)
result = gl.nondet.exec_prompt(
"Classify this claim as true or false. "
"Return ONLY JSON: {\"verdict\": \"<true|false>\"}",
response_format="json"
)
# result is a Python dict — e.g., result["verdict"]
# With images (e.g., from a screenshot)
result = gl.nondet.exec_prompt(
"Describe what you see in this image",
images=[screenshot]
)
Equivalence Principle (How Validators Agree on AI)
This is the mechanism that lets validators reach consensus on non-deterministic outputs. Pick your flavor:
Strict Equality (Start Here)
Outputs must be byte-for-byte identical. Perfect for constrained outputs.
def check():
data = gl.nondet.web.render(url, mode="text")
result = gl.nondet.exec_prompt(f"Analyze: {data}", response_format="json")
return json.dumps(result, sort_keys=True) # sort_keys is CRITICAL!
consensus_result = json.loads(gl.eq_principle.strict_eq(check))
Comparative (For Fuzzy Matches)
An LLM judges whether two outputs are "close enough."
result = gl.eq_principle.prompt_comparative(
my_fn,
"Results are equivalent if ratings differ by less than 0.1"
)
Non-Comparative (For Subjective Quality)
An LLM checks the leader's output against criteria. Other validators don't redo the work.
result = gl.eq_principle.prompt_non_comparative(
task="Summarize the article",
criteria="Summary must be accurate, concise, and under 100 words"
)
Which One Should You Use?
| Type | Best For | Validator Cost | Agreement Rate |
|---|---|---|---|
strict_eq |
Yes/no, categories, constrained JSON | Lowest | Highest ✓ |
prompt_comparative |
Numeric ranges, similar text | Medium | High |
prompt_non_comparative |
Subjective quality, creative output | Lowest | Medium |
Rule of thumb: Start with strict_eq. Only reach for the others when your output space is too wide for exact matching.
Common Patterns (Copy-Paste These)
Sender Address
sender = gl.message.sender_address # Address object
sender_hex = gl.message.sender_address.as_hex # "0x..." string
Address Conversion
addr = Address("0x1234...") # From hex string
addr.as_hex # To checksummed hex
addr.as_bytes # To bytes
Contract Balance & Payments
self.balance # Contract's current GEN balance
gl.message.value # GEN sent with this transaction
Reverting a Transaction
raise Exception("Claim has already been resolved") # Reverts all state changes
The Non-Deterministic Block Pattern (Most Important!)
This is the pattern. All web/AI calls must happen inside a function passed to an equivalence principle:
def fetch_and_analyze():
page = gl.nondet.web.render(url, mode="text")
result = gl.nondet.exec_prompt(
f"Analyze this: {page}",
response_format="json"
)
return json.dumps(result, sort_keys=True) # ALWAYS sort keys!
consensus_result = json.loads(gl.eq_principle.strict_eq(fetch_and_analyze))
Why? Because each validator needs to run this independently, and the equivalence principle compares their outputs.
Frontend Integration (genlayer-js)
Client Setup
import { createClient } from "genlayer-js";
import { studionet } from "genlayer-js/chains";
const client = createClient({
chain: studionet, // or localnet, testnetAsimov
account: "0xAddress" as `0x${string}`, // required for write calls
});
Read Contract (Free, No Gas)
const result = await client.readContract({
address: contractAddress,
functionName: "get_claims",
args: [],
});
Write Contract (Costs Gas)
const txHash = await client.writeContract({
address: contractAddress,
functionName: "submit_claim",
args: ["The Earth is round", "https://en.wikipedia.org/wiki/Earth"],
value: BigInt(0),
});
const receipt = await client.waitForTransactionReceipt({
hash: txHash,
status: "ACCEPTED",
retries: 24,
interval: 5000,
});
Deploy Contract
await client.initializeConsensusSmartContract();
const txHash = await client.deployContract({
code: contractCodeAsUint8Array,
args: [],
});
Converting TreeMap Results (Critical Pattern!)
genlayer-js returns Map objects for TreeMap storage. Convert them to plain JavaScript arrays:
if (result instanceof Map) {
const items = Array.from(result.entries()).map(([key, value]) => {
const obj = value instanceof Map
? Object.fromEntries(value.entries())
: value;
return { id: key, ...obj };
});
}
Testing with gltest (Making Sure It Works)
from gltest import get_contract_factory, default_account
from gltest.helpers import load_fixture
from gltest.assertions import tx_execution_succeeded
def deploy():
factory = get_contract_factory("MyContract")
return factory.deploy()
def test_basic_write():
contract = load_fixture(deploy) # Caches deploy across tests
result = contract.my_method(args=["arg1", "arg2"])
assert tx_execution_succeeded(result)
def test_basic_read():
contract = load_fixture(deploy)
data = contract.my_view_method(args=[])
assert data == expected_value
def test_ai_call():
contract = load_fixture(deploy)
# AI/web calls are slow — configure longer polling
result = contract.resolve(
args=["claim_1"],
wait_interval=10000, # 10s between polls
wait_retries=15, # up to 2.5 minutes total
)
assert tx_execution_succeeded(result)
Run tests: gltest (requires GenLayer Studio to be running)
Prompt Engineering for On-Chain AI (5 Rules)
Your prompts need to produce consistent outputs that validators can agree on:
-
Always use
response_format="json"for machine-readable output -
Constrain the output space:
"<true|false|partially_true>"beats free-form text -
Sort JSON keys:
json.dumps(result, sort_keys=True)— absolutely essential forstrict_eq - Be explicit about format: "Respond ONLY with valid JSON, no extra text"
- Fewer fields = higher agreement: The smaller the output, the easier consensus
Good prompt:
"Classify as true/false. Return ONLY: {\"verdict\": \"\", \"reason\": \"\"}"Bad prompt:
"What do you think about this claim?"
Environment Variables (Frontend Config)
# frontend/.env
NEXT_PUBLIC_GENLAYER_RPC_URL=https://studio.genlayer.com/api
NEXT_PUBLIC_GENLAYER_CHAIN_ID=61999
NEXT_PUBLIC_GENLAYER_CHAIN_NAME=GenLayer Studio
NEXT_PUBLIC_GENLAYER_SYMBOL=GEN
NEXT_PUBLIC_CONTRACT_ADDRESS=0xYourDeployedContractAddress
Networks (Where to Deploy)
| Network | Chain ID | RPC URL | When to Use |
|---|---|---|---|
| studionet | 61999 | https://studio.genlayer.com/api |
Development (hosted Studio) |
| localnet | -- | http://localhost:8545 |
Development (local Studio) |
| testnet (Asimov) | -- | See docs | Public testing before mainnet |
Transaction Lifecycle (What's Happening Under the Hood)
Pending → Proposing → Committing → Revealing → Accepted → Finalized
↓
Appeals? (escalating validators)
↓
Finalized (permanent)
Resources (The Official Stuff)
| Resource | URL |
|---|---|
| SDK API Reference (complete) | sdk.genlayer.com |
| Full Documentation | docs.genlayer.com |
| genlayer-js SDK | docs.genlayer.com/api-references/genlayer-js |
| GenLayer Studio | studio.genlayer.com |
| Contract Examples | docs.genlayer.com/.../examples |
Quick Troubleshooting
Contract won't deploy?
- Check that
genlayer networkis set correctly - Verify GenLayer Studio is running (if using localnet/studionet)
- Make sure contract syntax is valid Python
Frontend can't connect?
- Check contract address in
.env - Verify MetaMask is on the right network
- Check browser console for errors
Tests failing?
- GenLayer Studio must be running for
gltest - AI/web tests need longer
wait_intervalandwait_retries - Check that test data matches contract expectations
Validators disagreeing?
- Use
sort_keys=Trueinjson.dumps() - Constrain LLM output space more tightly
- Consider using
prompt_comparativeinstead ofstrict_eq
Link to the truthpost app we built: TruthPost site




Top comments (0)