Imagine a smart contract that reads a news article, decides whether a prediction market resolves in your favor, and records that verdict on-chain — without trusting any third party to feed it the data. No oracle. No intermediary. The contract itself fetches the page, reasons about it, and commits the result.
That's not a concept. That's GenLayer, and it's running on testnet right now.
This guide will teach you what GenLayer is, why it represents something genuinely new in the blockchain space, and how to build a production-grade REST API that connects your frontend to a real on-chain AI contract. We'll go step by step, explain every decision, and document the non-obvious problems you'll hit along the way.
By the end you'll have a working API you can deploy in minutes.
Part 1: The Problem With Every Blockchain You Know
To understand why GenLayer matters, you need to understand the constraint that every existing blockchain shares — a constraint so fundamental that most developers accept it without question.
Blockchains require determinism
For a network of thousands of independent validators to agree on the state of a ledger, every validator must run the same computation and get the exact same result. This is the foundation of blockchain consensus. It's what makes trustless agreement possible at all.
The rule sounds reasonable until you think about what it eliminates:
These aren't edge cases. They're the most interesting things software does. And they're all off-limits in a standard smart contract environment.
The oracle workaround — and why it's not enough
The standard solution is an oracle: a trusted off-chain service that fetches external data and pushes it into the contract. Chainlink is the most famous example. The oracle watches the real world and reports what it sees.
But here's the problem. An oracle is centralized. You're trusting the oracle operator to report data accurately, to stay online, to not get hacked, and to not collude with anyone. Every oracle is a single point of trust in a system that was built to eliminate trust.
It's also reactive by nature. The oracle only provides what someone thought to pipe into the contract. If the contract needs to reason about something new — evaluate a novel claim, interpret an unusual situation, make a judgment that wasn't anticipated — the oracle can't help.
GenLayer changes the rule
GenLayer's fundamental insight is that determinism isn't actually the goal. Agreement is the goal. And agreement doesn't require identical outputs — it requires equivalent outputs.
Two validators can arrive at the same conclusion through different paths. Two LLM calls can produce different words that mean the same thing. What matters is whether the validators agree that their results are equivalent. And that's a solvable problem.
Part 2: How GenLayer Actually Works
Optimistic Democracy
GenLayer's consensus mechanism is called Optimistic Democracy. It's inspired by Condorcet's Jury Theorem — a 1785 mathematical proof that a group of independent agents, each with better-than-random judgment, is more likely to reach the correct answer than any individual member. The more agents, the higher the probability.
Here's the full lifecycle of a GenLayer transaction:
You call evaluate_claim("The price of ETH exceeded $4000 on Jan 15")
│
▼
┌─────────────────────────────────────┐
│ LEADER VALIDATOR selected │
│ │
│ 1. Executes your Python contract │
│ 2. Fetches the webpage you told it │
│ 3. Calls the LLM with your prompt │
│ 4. Gets a result: {"verdict":true} │
│ 5. Proposes this to the network │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ OTHER VALIDATORS (4+ nodes) │
│ │
│ Each independently: │
│ • Fetches the same webpage │
│ • Calls their own LLM │
│ • Compares their result to leader's│
│ • Votes: equivalent or not │
└─────────────────────────────────────┘
│
▼
┌─────────────────────────────────────┐
│ EQUIVALENCE CHECK │
│ │
│ Leader: "The price exceeded $4000" │
│ Val. 2: "ETH was above $4k" ✓ │
│ Val. 3: "Confirmed, true" ✓ │
│ Val. 4: "Threshold reached" ✓ │
│ Val. 5: "Yes, verified" ✓ │
│ │
│ Majority agrees → ACCEPTED │
└─────────────────────────────────────┘
│
▼
Appeal window passes → FINALIZED
Result stored permanently on-chain
FINALIZED
Result stored permanently on-chain
The key distinction: validators aren't checking that every character matches. They're checking whether the meaning is equivalent. GenLayer provides different Equivalence Principles for this purpose:
-
gl.eq_principle.strict_eq(fn)— byte-for-byte identical (used for JSON, numbers, structured data) -
gl.eq_principle.prompt_comparative(fn, criteria)— semantic equivalence judged by another LLM call
For AI outputs, you almost always want strict_eq on a normalized JSON string, which produces a reliable deterministic representation.
The GenVM
The contract execution environment is GenVM — a WebAssembly-based virtual machine with a Python interpreter built in. Inside GenVM, Python contracts have access to primitives you won't find on any other blockchain:
| Primitive | What it does |
|---|---|
gl.nondet.exec_prompt(task) |
Calls the LLM with your prompt string |
gl.eq_principle.strict_eq(fn) |
Coordinates consensus on a function's return value |
gl.nondet.web.get_webpage(url) |
Fetches live web content from inside the contract |
gl.message.sender_address |
The wallet that signed this transaction |
TreeMap[K, V] |
Persistent key-value storage on-chain |
DynArray[T] |
Persistent dynamic array |
Transaction lifecycle
Every GenLayer transaction goes through distinct states. Understanding these prevents confusion when integrating:
| Status | What's happening |
|---|---|
PENDING |
In the queue, not yet picked up |
PROPOSING |
Leader validator is executing your contract |
COMMITTING |
Validators are submitting encrypted votes |
REVEALING |
Validators are revealing their votes |
ACCEPTED |
Majority agreed — in the 30-second appeal window |
FINALIZED |
Permanent. Irreversible. The truth. |
UNDETERMINED |
Consensus failed — rare, usually retried |
For any operation that involves an LLM call, plan for 30–120 seconds from submission to FINALIZED. For simple reads (view methods), the response is immediate and free.
Part 3: Why GenLayer? Real Benefits, Real Use Cases
Let me be concrete about what this unlocks, because the implications are broader than they first appear.
Benefit 1: AI calls inside the contract, not beside it
On every other platform, "AI + blockchain" means an oracle calls an AI and pipes the result into a contract. The AI result is trusted because the oracle is trusted. The oracle is trusted because... someone decided to trust it.
On GenLayer, the LLM call is part of the contract execution itself. The result is trusted because multiple independent validators each ran the same model and agreed. No single party controls the AI output.
# This runs INSIDE the transaction
# Verified by multiple validators with their own LLM calls
# No intermediary to trust
def analyze() -> str:
result = gl.nondet.exec_prompt(my_prompt)
return json.dumps(json.loads(result), sort_keys=True)
verified_result = gl.eq_principle.strict_eq(analyze)
Benefit 2: Subjective decisions become on-chain facts
Traditional smart contracts can compute. They can check if a number exceeds a threshold, if an address is on an allowlist, if a deadline has passed. What they cannot do is judge.
GenLayer contracts can evaluate quality, assess whether content meets a standard, determine if a proposal satisfies requirements written in natural language, or verify a claim against evidence. These judgments are made by the LLM, verified by multiple validators, and stored on-chain as permanent, queryable facts.
Benefit 3: Live web data, natively
# Fetch live data from any URL directly inside the contract
# Verified by all validators — each fetches independently
page = gl.nondet.web.get_webpage("https://api.coinbase.com/v2/prices/ETH-USD/spot")
price_data = json.loads(page)
This enables contracts that self-resolve based on real-world data without trusting anyone to provide that data. The contract reads the source directly.
Benefit 4: Python — no new language
Ethereum requires Solidity. Solana requires Rust. GenLayer requires Python. The time from "I want to deploy this" to "it's on-chain" is hours, not weeks. If you write backend code, you can write a GenLayer contract.
Use case gallery
Content moderation API — A social platform stores content submissions on-chain. Before a post goes live, a GenLayer contract evaluates it against community guidelines. Multiple validators each call their own LLM. The moderation decision is stored on-chain and cannot be altered by the platform retroactively. Users can verify why their content was moderated. Appeals go back on-chain.
Prediction markets that self-resolve — A market asks "Will the Fed raise rates in Q1?" The contract checks a live financial data source when the market closes, uses LLM reasoning to interpret the data, and resolves the market automatically. No admin needed.
Compliance oracle — A DeFi protocol routes transactions through a GenLayer contract that checks whether the sender address appears on a live sanctions list. The check happens on-chain. No centralized API key, no admin who could be pressured to look the other way.
Freelance escrow — A client deposits funds into a contract. When the freelancer submits work, the contract evaluates whether the delivery meets the brief — written in plain English, stored in the contract. Validators each judge the submission. The decision is made by consensus, not by either party.
AI resume screener — A company's hiring pipeline posts job requirements on-chain. Applicants submit. A GenLayer contract evaluates each application against the requirements. Results are auditable. Bias claims can be investigated on-chain.
Part 4: The Intelligent Contract
Let's build something real. We'll create a content validation contract: users submit text, the AI analyzes it, the result is stored on-chain.
The header — mandatory and exact
Every GenLayer contract must start with this exact line:
# { "Depends": "py-genlayer:test" }
This tells GenVM which SDK version to load. Without it, the VM refuses to execute the contract. "py-genlayer:test" always resolves to the current studionet-compatible runtime — use it for testnet development.
Storage type rules
GenLayer has a stricter type system than Python. These rules apply to all persistent storage fields:
class MyContract(gl.Contract):
# ✅ Allowed
name: str
counter: str # use str for numeric IDs — int is forbidden
active: bool
amount: u64 # sized integers: u8, u16, u32, u64
big: bigint # for arbitrary precision math
data: TreeMap[str, str] # persistent key-value map
items: DynArray[str] # persistent dynamic array
# ❌ These will throw TypeError at deployment
# bad: int → use bigint or u64
# bad: float → not supported, use str("0.85")
# bad: dict[str, str] → use TreeMap[str, str]
# bad: list[str] → use DynArray[str]
The three rules inside the LLM block
Before looking at the full contract, internalize these three rules. They explain 90% of the errors people hit:
Rule 1: Read storage before the nondet block, not inside it.
Storage is inaccessible from within a non-deterministic function. Read what you need into a local variable first.
Rule 2: Return a deterministic string from the nondet block.
json.dumps(parsed, sort_keys=True) produces the same bytes regardless of key insertion order. strict_eq compares byte-for-byte — a plain dict is unreliable.
Rule 3: Float is not supported in calldata.
If your prompt might return a number like 0.91 as a float, the calldata encoder will crash. Always prompt for quoted strings ("confidence": "0.91") and always cast: str(parsed.get("confidence", "0")).
The full contract
# { "Depends": "py-genlayer:test" }
from genlayer import *
import json
import typing
class ContentValidator(gl.Contract):
"""
Stores text submissions on-chain.
Analyzes them via LLM with multi-validator consensus.
Stores the verified AI verdict permanently.
"""
# Persistent storage — survives between transactions
items: TreeMap[str, str] # id → submitted text
results: TreeMap[str, str] # id → AI analysis (JSON string)
statuses: TreeMap[str, str] # id → "pending" | "analyzed"
authors: TreeMap[str, str] # id → submitter wallet address
next_id: str # counter (str — int is forbidden)
def __init__(self):
self.next_id = "0"
@gl.public.write
def submit(self, text: str) -> typing.Any:
"""
Store a text submission on-chain.
Returns the assigned ID.
Use typing.Any as return type — prevents genlayer-js type validation errors.
"""
assert len(text) > 0, "Text cannot be empty"
assert len(text) <= 1000, "Max 1000 characters"
item_id = str(self.next_id) # always convert storage proxy to str
self.items[item_id] = text
self.results[item_id] = ""
self.statuses[item_id] = "pending"
self.authors[item_id] = gl.message.sender_address.as_hex
self.next_id = str(int(self.next_id) + 1)
return item_id
@gl.public.write
def analyze(self, item_id: str) -> typing.Any:
"""
Run LLM analysis through multi-validator consensus.
This is the core of a GenLayer Intelligent Contract.
"""
assert item_id in self.items, "Invalid ID"
assert self.statuses[item_id] == "pending", "Already analyzed"
# ─── Rule 1: Read storage BEFORE the nondet block ───────────────────
# Storage proxies are inaccessible inside def get_analysis().
# Read everything you need into local variables first.
text = str(self.items[item_id])
def get_analysis() -> str:
"""
This function runs inside each validator independently.
Each validator calls their own LLM and compares results.
"""
task = f"""You are a content analyst. Analyze the following text.
Text: "{text}"
Respond with ONLY valid JSON matching this exact schema:
{{
"summary": "one sentence describing the main point",
"sentiment": "positive or negative or neutral or mixed",
"quality": "high or medium or low",
"verdict": "one sentence recommendation",
"confidence": "0.85"
}}
Critical rules:
- "confidence" MUST be a quoted string "0.85" — never a bare number like 0.85
- All values must be strings
- Return ONLY the JSON. No markdown code blocks. No explanation."""
raw = (
gl.nondet.exec_prompt(task)
.replace("```
json", "")
.replace("
```", "")
.strip()
)
parsed = json.loads(raw)
# ─── Rule 3: Always cast numeric fields to str ───────────────────
# The calldata encoder does not support float. Cast everything.
parsed["confidence"] = str(parsed.get("confidence", "0"))
for key in ["summary", "sentiment", "quality", "verdict"]:
if key in parsed and not isinstance(parsed[key], str):
parsed[key] = str(parsed[key])
# ─── Rule 2: Return deterministic string ─────────────────────────
# sort_keys=True ensures consistent byte output regardless of
# Python dict ordering — strict_eq compares byte-for-byte
return json.dumps(parsed, sort_keys=True)
# Run the nondet function through consensus
consensus_str = gl.eq_principle.strict_eq(get_analysis)
# Deserialize OUTSIDE the nondet block
consensus_json = json.loads(consensus_str)
self.results[item_id] = json.dumps(consensus_json)
self.statuses[item_id] = "analyzed"
return consensus_json
@gl.public.view
def get_item(self, item_id: str) -> typing.Any:
"""
Read a single item. Free — no wallet, no transaction.
Always wrap storage proxy values in str().
Without str(), genlayer-js throws "Value must be an instance of str"
even though the field is annotated as str.
"""
assert item_id in self.items, "Invalid ID"
return {
"id": str(item_id),
"text": str(self.items[item_id]),
"result": str(self.results[item_id]),
"status": str(self.statuses[item_id]),
"author": str(self.authors[item_id]),
}
@gl.public.view
def get_count(self) -> typing.Any:
return str(self.next_id)
@gl.public.view
def get_all(self) -> typing.Any:
return {str(k): str(v) for k, v in self.items.items()}
How to deploy: Go to studio.genlayer.com → New Contract → paste the code → Deploy. Copy the contract address that appears after deployment — you'll need it in the next section.
Part 5: Building the REST API
Why a server-side API layer?
Before writing any code, understand why we're not calling GenLayer directly from the browser:
CORS — The studionet RPC at studio.genlayer.com:8443 blocks direct browser requests. There's no workaround on the client side.
Private key safety — Write transactions need a signing key. That key cannot live in client-side JavaScript — anyone who opens devtools gets it.
ESM compatibility — genlayer-js is an ESM-only package that doesn't load with require() in a standard Node.js server file. There's a specific pattern to make it work.
Port restrictions — Some hosting environments block outbound connections to port 8443. There's a Node-specific fix.
The server-side API solves all four. Your frontend calls POST /api/items — it never knows a blockchain exists.
Project structure
my-api/
├── api/
│ └── index.js ← all route logic
├── contract/
│ └── validator.py ← the contract we just wrote
├── server.js ← Express entry for Railway/Heroku
├── package.json
├── vercel.json ← for Vercel deployment
├── Procfile ← for Railway/Heroku
└── .env.example
Problem 1: genlayer-js is ESM-only
genlayer-js has "type": "module" in its own package.json. If your server file uses CommonJS, then require('genlayer-js') throws at runtime:
Error [ERR_REQUIRE_ESM]: require() of ES Module not supported.
The fix: dynamic import(). This works inside any async function in any Node.js module, CommonJS or ESM, since Node 12:
// ✅ Works everywhere
let _sdk = null;
async function getSDK() {
if (_sdk) return _sdk; // import once, reuse for every request
const [gl, chains] = await Promise.all([
import('genlayer-js'),
import('genlayer-js/chains'),
]);
_sdk = { gl, chains };
return _sdk;
}
Cache the result in _sdk. Dynamic imports are expensive — you only want to pay that cost once when the process starts, not on every API call.
Problem 2: The Cloudflare port 8443 problem
Here's the map of what lives where on GenLayer's servers:
studio.genlayer.com:443 → Studio web interface (returns HTML)
studio.genlayer.com:8443 → JSON-RPC API (returns JSON) ✓
When your server runs on Vercel or Railway, it has a datacenter IP address. Cloudflare's edge network sometimes blocks outbound TCP connections to port 8443 from such IPs — returning a 522 Connection Timed Out HTML error page.
When viem (the library genlayer-js uses internally) receives that HTML instead of JSON, it throws:
Unexpected token '<', "<!DOCTYPE "... is not valid JSON
The fix for your own reads: use Node's built-in https module with explicit TCP timeout:
const https = require('https');
function rpcPost(bodyObject) {
const buf = Buffer.from(JSON.stringify(bodyObject), 'utf8');
return new Promise((resolve, reject) => {
const req = https.request({
hostname: 'studio.genlayer.com',
port: 8443,
path: '/api',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Content-Length': buf.length,
'Accept': 'application/json',
},
}, (res) => {
const chunks = [];
res.on('data', c => chunks.push(c));
res.on('end', () => {
const text = Buffer.concat(chunks).toString('utf8');
try { resolve(JSON.parse(text)); }
catch { reject(new Error('Non-JSON from studionet: ' + text.slice(0, 100))); }
});
res.on('error', reject);
});
req.on('error', reject);
req.setTimeout(28000, () => req.destroy(new Error('RPC timeout')));
req.write(buf);
req.end();
});
}
The fix for genlayer-js writes: viem also calls globalThis.fetch internally. That fetch hits the same Cloudflare wall. We intercept it before the SDK loads:
let _fetchPatched = false;
function patchGlobalFetch() {
if (_fetchPatched) return;
_fetchPatched = true;
const originalFetch = globalThis.fetch;
globalThis.fetch = async function (input, init) {
const url = String(input?.url || input);
// Only intercept calls to studionet — pass everything else through
if (!url.includes('studio.genlayer.com')) {
return originalFetch.call(this, input, init);
}
const bodyStr = init?.body
? (typeof init.body === 'string' ? init.body : JSON.stringify(init.body))
: '{}';
const buf = Buffer.from(bodyStr, 'utf8');
const text = await new Promise((resolve, reject) => {
const req = https.request({
hostname: 'studio.genlayer.com', port: 8443, path: '/api', method: 'POST',
headers: { 'Content-Type': 'application/json', 'Content-Length': buf.length },
}, (res) => {
const chunks = [];
res.on('data', c => chunks.push(c));
res.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
res.on('error', reject);
});
req.setTimeout(28000, () => req.destroy());
req.on('error', reject);
req.write(buf);
req.end();
});
// Return a Response-compatible object that viem can consume
return {
ok: true,
status: 200,
statusText: 'OK',
headers: { get: () => 'application/json' },
text: async () => text,
json: async () => JSON.parse(text),
clone: function () { return this; },
};
};
}
// Call this BEFORE await import('genlayer-js')
async function getSDK() {
if (_sdk) return _sdk;
patchGlobalFetch(); // ← patch first, then load
const [gl, chains] = await Promise.all([
import('genlayer-js'),
import('genlayer-js/chains'),
]);
_sdk = { gl, chains };
return _sdk;
}
Reading from the contract
Reads use gen_call directly — no wallet needed, no gas, instant:
const CONTRACT = process.env.CONTRACT_ADDRESS;
const ZERO = '0x0000000000000000000000000000000000000000';
async function contractRead(method, args = []) {
const json = await rpcPost({
jsonrpc: '2.0',
id: Date.now(),
method: 'gen_call',
params: [{
from: ZERO,
to: CONTRACT,
type: 'read',
data: { function: method, args },
status: 'accepted',
}],
});
if (json.error) throw new Error(json.error.message || JSON.stringify(json.error));
return json.result;
}
Writing to the contract
async function contractWrite(method, args = [], privateKey) {
const { gl, chains } = await getSDK();
const key = privateKey || process.env.PRIVATE_KEY;
const account = key
? gl.createAccount(key.startsWith('0x') ? key : '0x' + key)
: gl.createAccount(); // auto-generates a funded studionet account
const client = gl.createClient({
chain: chains.studionet,
account,
endpoint: 'https://studio.genlayer.com:8443/api',
});
const txHash = await client.writeContract({
address: CONTRACT,
functionName: method,
args,
value: BigInt(0),
});
// Wait for full consensus — FINALIZED means permanent
const receipt = await client.waitForTransactionReceipt({
hash: txHash,
status: 'FINALIZED',
interval: 3000, // poll every 3 seconds
retries: 120, // up to 6 minutes total
});
return {
txHash,
result: receipt?.result ?? receipt?.return_value ?? null,
};
}
The route handler
async function handleRequest(method, path, body, headers) {
body = body || {};
headers = headers || {};
const pk = headers['x-private-key'] || headers['X-Private-Key'];
if (method === 'GET' && path === '/') {
return { name: 'ContentValidator API', version: '1.0.0', contract: CONTRACT };
}
if (method === 'GET' && path === '/items/count') {
const raw = await contractRead('get_count', []);
return { total: parseInt(String(raw), 10) || 0 };
}
if (method === 'GET' && path === '/items') {
const all = await contractRead('get_all', []);
return { items: all, count: Object.keys(all || {}).length };
}
const mId = path.match(/^\/items\/([^/]+)$/);
if (mId && method === 'GET') {
const item = await contractRead('get_item', [mId[1]]);
if (item && typeof item === 'object') {
Object.keys(item).forEach(k => {
if (item[k] != null && typeof item[k] !== 'string') {
item[k] = JSON.stringify(item[k]);
}
});
}
if (item?.result) {
try { item.result_parsed = JSON.parse(item.result); } catch (_) {}
}
return item;
}
if (method === 'POST' && path === '/items') {
const { text } = body;
if (!text || !text.trim())
throw Object.assign(new Error('body.text is required'), { status: 400 });
if (text.length > 1000)
throw Object.assign(new Error('body.text max 1000 characters'), { status: 400 });
const out = await contractWrite('submit', [text], pk);
return { id: String(out.result), txHash: out.txHash, status: 'pending' };
}
const mAnalyze = path.match(/^\/items\/([^/]+)\/analyze$/);
if (mAnalyze && method === 'POST') {
const id = mAnalyze[1];
const out = await contractWrite('analyze', [id], pk);
let result = out.result;
if (typeof result === 'string') {
try { result = JSON.parse(result); } catch (_) {}
}
return { id, txHash: out.txHash, status: 'analyzed', result };
}
throw Object.assign(new Error(`Not found: ${method} ${path}`), { status: 404 });
}
The Express server
// server.js
'use strict';
const express = require('express');
const handler = require('./api/index.js');
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.json({ limit: '1mb' }));
app.use('/', handler.router);
app.listen(PORT, () => {
console.log(`\n◈ ContentValidator API`);
console.log(` Running at: http://localhost:${PORT}`);
console.log(` Contract: ${process.env.CONTRACT_ADDRESS || '(set CONTRACT_ADDRESS)'}\n`);
});
Part 6: Deploy and Test
Environment variables
# Required
CONTRACT_ADDRESS=0xYourContractAddress
# Optional — a fresh studionet key is auto-generated if not set
PRIVATE_KEY=0xYourPrivateKey
# Set automatically by Railway/Heroku
# PORT=3000
Deploy to Railway
- Push your code to GitHub
- Go to railway.app → New Project → Deploy from GitHub repo
- In Variables, add
CONTRACT_ADDRESSandPRIVATE_KEY - Railway reads your
Procfileand runsnode server.jsweb: node server.js
Deploy to Vercel
npm install -g vercel
vercel env add CONTRACT_ADDRESS
vercel env add PRIVATE_KEY
vercel --prod
Testing the API
# Check it's running
curl https://your-api.railway.app/
# Submit text
curl -X POST https://your-api.railway.app/items \
-H "Content-Type: application/json" \
-H "X-Private-Key: 0xyourkey" \
-d '{"text": "Decentralized AI consensus is the future of trust"}'
# → {"ok":true,"id":"0","txHash":"0x...","status":"pending"}
# Trigger AI analysis (30–120 seconds)
curl -X POST https://your-api.railway.app/items/0/analyze \
-H "X-Private-Key: 0xyourkey"
# → {"ok":true,"status":"analyzed","result":{"sentiment":"positive","confidence":"0.91",...}}
# Read back — free, no key needed
curl https://your-api.railway.app/items/0
curl https://your-api.railway.app/items/count
Calling from your frontend
const API = 'https://your-api.railway.app';
async function submitAndAnalyze(text) {
// Submit
const sub = await fetch(`${API}/items`, {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-Private-Key': KEY },
body: JSON.stringify({ text }),
});
const { id } = await sub.json();
// Analyze — show a loading state, this takes time
const ana = await fetch(`${API}/items/${id}/analyze`, {
method: 'POST',
headers: { 'X-Private-Key': KEY },
});
const { result } = await ana.json();
console.log(result.sentiment); // "positive"
console.log(result.confidence); // "0.91"
}
Quick reference: the errors you will hit
| Error | Cause | Fix |
|---|---|---|
invalid_contract absent_runner_comment |
Wrong or missing first line | Line 1 must be exactly # { "Depends": "py-genlayer:test" }
|
Value must be an instance of str |
Returning storage proxy without str()
|
Wrap every return value: str(self.field)
|
Unexpected token '<' |
HTML 522 page instead of JSON | Use Node https module + patchGlobalFetch()
|
ERR_REQUIRE_ESM |
require('genlayer-js') in CJS file |
Use await import('genlayer-js') in async function |
float is not supported in calldata |
LLM returned a number, not a string | Cast: str(parsed.get("confidence", "0"))
|
use bigint or sized integers |
int type in a storage field |
Use str, u64, u32, or bigint
|
| Transaction never finalizes | AI consensus taking longer than expected | Increase retries — it usually resolves |
What to build next
The pattern you've learned is a template. Swap the contract logic and you have:
- A content moderation API where every moderation decision is verifiable on-chain
- A prediction market resolver that self-resolves using live web data
- A governance system where proposals are evaluated against rules written in plain English
- A fact-checking service where claims are verified against multiple sources on-chain
- A reputation system where AI evaluates track records and stores scores permanently
The source code for the project this guide is based on is at https://github.com/Said1235/Educational-project--for-implementing-an-API--in-GenLayer.git.
Built with GenLayer Studionet · Node.js 24 · Railway

Top comments (0)