GenLayer Intelligent Contracts
GenLayer enables Intelligent Contracts - Python smart contracts that can call LLMs, fetch web data, and handle non-deterministic operations while maintaining blockchain consensus.
Quick Start
Minimal Contract
# v0.1.0
# { "Depends": "py-genlayer:latest" }
from genlayer import *
class MyContract(gl.Contract):
value: str
def __init__(self, initial: str):
self.value = initial
@gl.public.view
def get_value(self) -> str:
return self.value
@gl.public.write
def set_value(self, new_value: str) -> None:
self.value = new_value
Contract with LLM
# v0.1.0
# { "Depends": "py-genlayer:latest" }
from genlayer import *
import json
class AIContract(gl.Contract):
result: str
def __init__(self):
self.result = ""
@gl.public.write
def analyze(self, text: str) -> None:
prompt = f"Classify sentiment as positive, negative, or neutral. Text: {text}"
def leader():
raw = gl.nondet.exec_prompt(prompt)
return _parse_llm_json(raw)
def validator(leader_result):
raw = gl.nondet.exec_prompt(prompt)
my_result = _parse_llm_json(raw)
# Validators independently evaluate — compare classification
return leader_result.get("sentiment") == my_result.get("sentiment")
# Leader and validator both execute independently — the 90% default
self.result = json.dumps(gl.vm.run_nondet(leader=leader, validator=validator))
@gl.public.view
def get_result(self) -> str:
return self.result
Contract with Web Access
# v0.1.0
# { "Depends": "py-genlayer:latest" }
from genlayer import *
class WebContract(gl.Contract):
content: str
def __init__(self):
self.content = ""
@gl.public.write
def fetch(self, url: str) -> None:
url_copy = url # Capture for closure
def leader():
return gl.nondet.web.render(url_copy, mode="text")
def validator(leader_result):
# Each validator independently fetches and compares
my_content = gl.nondet.web.render(url_copy, mode="text")
return leader_result == my_content
# Leader and validators both execute independently — the 90% default
self.content = gl.vm.run_nondet(leader=leader, validator=validator)
@gl.public.view
def get_content(self) -> str:
return self.content
Core Concepts
Contract Structure
- Version header:
# v0.1.0(required) - Dependencies:
# { "Depends": "py-genlayer:latest" } - Import:
from genlayer import * - Class: Extend
gl.Contract(only ONE per file) - State: Class-level typed attributes
- Constructor:
__init__(not public) - Methods: Decorated with
@gl.public.viewor@gl.public.write
Method Decorators
| Decorator | Purpose | Can Modify State |
|---|---|---|
@gl.public.view |
Read-only queries | No |
@gl.public.write |
State mutations | Yes |
@gl.public.write.payable |
Receive value + mutate | Yes |
Storage Types
Replace standard Python types with GenVM storage-compatible types:
| Python Type | GenVM Type | Usage |
|---|---|---|
int |
u32, u64, u256, i32, i64, etc. |
Sized integers |
int (unbounded) |
bigint |
Arbitrary precision (avoid) |
float |
float |
8-byte double (works directly) |
datetime |
datetime.datetime |
With timezone support |
list[T] |
DynArray[T] |
Dynamic arrays |
| fixed array | Array[T, Literal[N]] |
Fixed-size arrays |
dict[K,V] |
TreeMap[K,V] |
Ordered maps |
str |
str |
Strings (unchanged) |
bool |
bool |
Booleans (unchanged) |
⚠️ int is NOT supported! Always use sized integers. In production, int may cause opaque exit_code 1 crashes.
Address Type
# Creating addresses
addr = Address("0x03FB09251eC05ee9Ca36c98644070B89111D4b3F")
# Get sender
sender = gl.message.sender_address
# Conversions
hex_str = addr.as_hex # "0x03FB..."
bytes_val = addr.as_bytes # bytes
Custom Data Types
from dataclasses import dataclass
@allow_storage
@dataclass
class UserData:
name: str
balance: u256
active: bool
class MyContract(gl.Contract):
users: TreeMap[Address, UserData]
Non-Deterministic Operations
The Problem
LLMs and web fetches produce different results across validators. GenLayer solves this with the Equivalence Principle.
Equivalence Principles
1. Custom Leader/Validator (gl.vm.run_nondet) — THE DEFAULT (~90% of use cases)
Leader fetches+evaluates, validator independently fetches+evaluates, then compares. Used by Rally, MergeProof, Molly.fun. This is how GenLayer is designed to work.
def leader():
data = gl.nondet.web.render(url, mode="text")
return parse_price(data)
def validator(leader_result):
# Independently fetch and evaluate
my_data = gl.nondet.web.render(url, mode="text")
my_price = parse_price(my_data)
# Allow 5% tolerance for timing differences
return abs(leader_result - my_price) / my_price < 0.05
result = gl.vm.run_nondet(leader=leader, validator=validator)
Best for: Everything with web fetches or LLM calls — which is almost every real contract.
2. Strict Equality (strict_eq)
Shortcut: all validators must produce identical results. NOT for LLM text outputs.
def check_flag():
content = gl.nondet.web.render(url, mode="text")
return "maintenance" in content.lower() # boolean → deterministic
result = gl.eq_principle.strict_eq(check_flag)
Best for: Deterministic-only results — booleans, exact parsed values, checksums.
Warning: LLMs rarely produce identical text across validators. Use run_nondet instead.
3. Prompt Comparative (prompt_comparative)
Shortcut: LLM compares leader's result against validators' results. Extra cost.
def get_analysis():
return gl.nondet.exec_prompt(prompt)
result = gl.eq_principle.prompt_comparative(
get_analysis,
"The sentiment classification must match"
)
Best for: Simple LLM classifications where you want LLM-based semantic comparison.
4. Prompt Non-Comparative (prompt_non_comparative)
ONLY for pure NLP tasks with NO web fetching. Validators don't re-execute — they
only check whether the leader's output looks reasonable. Using this with web.get or
web.render means validators blindly trust the leader's fetch — this defeats the
entire purpose of multi-validator consensus.
result = gl.eq_principle.prompt_non_comparative(
lambda: input_data, # What to process
task="Summarize the key points",
criteria="Summary must be under 100 words and factually accurate"
)
Best for: Pure text NLP with no external data. Example: summarize a string passed as argument.
Non-Deterministic Functions
| Function | Purpose |
|---|---|
gl.nondet.exec_prompt(prompt, images=[], response_format='text') |
LLM prompt (text/json, optional images) |
gl.nondet.web.get(url) |
HTTP GET (returns Response with .status, .body) |
gl.nondet.web.post(url, body=) |
HTTP POST |
gl.nondet.web.render(url, mode=, wait_after_loaded=) |
Render webpage ("text", "html", "screenshot") |
⚠️ Rules:
- Must be called inside equivalence principle functions
- Cannot access storage directly
- Copy storage data to memory first with
gl.storage.copy_to_memory()
Contract Interactions
Call Other Contracts
# Dynamic typing
other = gl.get_contract_at(Address("0x..."))
result = other.view().some_method()
# Static typing (better IDE support)
@gl.contract_interface
class TokenInterface:
class View:
def balance_of(self, owner: Address) -> u256: ...
class Write:
def transfer(self, to: Address, amount: u256) -> bool: ...
token = TokenInterface(Address("0x..."))
balance = token.view().balance_of(my_address)
Emit Messages (Async Calls)
other = gl.get_contract_at(addr)
other.emit(on='accepted').update_status("active")
other.emit(on='finalized').confirm_transaction()
Deploy Contracts
child_addr = gl.deploy_contract(code=contract_code, salt=u256(1))
EVM Interop
@gl.evm.contract_interface
class ERC20:
class View:
def balance_of(self, owner: Address) -> u256: ...
class Write:
def transfer(self, to: Address, amount: u256) -> bool: ...
token = ERC20(evm_address)
balance = token.view().balance_of(addr)
token.emit().transfer(recipient, u256(100)) # Messages only on finality
CLI Commands
Setup
npm install -g genlayer
genlayer init # Download components
genlayer up # Start local network
Deployment
# Direct deploy
genlayer deploy --contract my_contract.py
# With constructor args
genlayer deploy --contract my_contract.py --args "Hello" 42
# To testnet
genlayer network set testnet-asimov
genlayer deploy --contract my_contract.py
Interaction
# Read (view methods)
genlayer call --address 0x... --function get_value
# Write
genlayer write --address 0x... --function set_value --args "new_value"
# Get schema
genlayer schema --address 0x...
# Check transaction
genlayer receipt --tx-hash 0x...
Networks
genlayer network # Show current
genlayer network list # Available networks
genlayer network set localnet # Local dev
genlayer network set studionet # Hosted dev
genlayer network set testnet-asimov # Testnet
Best Practices
Prompt Engineering
prompt = f"""
Analyze this text and classify the sentiment.
Text: {text}
Respond using ONLY this JSON format:
{{"sentiment": "positive" | "negative" | "neutral", "confidence": float}}
Output ONLY valid JSON, no other text.
"""
Security: Prompt Injection
- Restrict inputs: Minimize user-controlled text in prompts
- Restrict outputs: Define exact output formats
- Validate: Check parsed results match expected schema
- Simplify logic: Clear contract flow reduces attack surface
Error Handling
Use gl.vm.UserError (not from genlayer import UserError). Use error prefixes for
easier debugging and monitoring:
@gl.public.write
def safe_operation(self, value: u256, url: str) -> None:
# [EXPECTED] — business logic validation
if value == u256(0):
raise gl.vm.UserError("[EXPECTED] Value must be positive")
def leader():
resp = gl.nondet.web.get(url)
# [EXTERNAL] — API/web fetch failures
if resp.status != 200:
raise gl.vm.UserError(f"[EXTERNAL] Fetch failed: {resp.status}")
# [TRANSIENT] — 5xx, timeouts (caller should retry)
if resp.status >= 500:
raise gl.vm.UserError(f"[TRANSIENT] Server error: {resp.status}")
return resp.body
def validator(leader_result):
try:
my_resp = gl.nondet.web.get(url)
return leader_result == my_resp.body
except Exception:
# [LLM_ERROR] — LLM failures
raise gl.vm.UserError("[LLM_ERROR] Validation fetch failed")
self.result = gl.vm.run_nondet(leader=leader, validator=validator)
LLM Response Parsing (_parse_llm_json)
exec_prompt can return dict or str depending on the GenVM runtime and LLM backend.
Using bare json.loads(str(raw)) fails when GenVM returns a dict (Python repr uses single quotes,
not valid JSON). Always use this helper:
import json
def _parse_llm_json(raw):
"""Parse LLM response — handles both string and dict returns from exec_prompt."""
if isinstance(raw, dict):
return raw
s = str(raw).strip()
s = s.replace("```json", "").replace("```", "").strip()
start = s.find("{")
end = s.rfind("}") + 1
if start >= 0 and end > start:
s = s[start:end]
s = s.replace("'", '"')
return json.loads(s)
This was the root cause of 7 consecutive UNDETERMINED failures in ERC-8183 bounty contracts.
Memory Management
# Copy storage to memory for non-det blocks
data_copy = gl.storage.copy_to_memory(self.some_data)
def process():
return gl.nondet.exec_prompt(f"Process: {data_copy}")
result = gl.eq_principle.strict_eq(process)
Common Patterns
Token with AI Transfer Validation
See references/examples.md → LLM ERC20
Prediction Market
See references/examples.md → Football Prediction Market
Vector Search / Embeddings
See references/examples.md → Log Indexer
Tooling
GenVM Linter (mandatory pre-deploy)
Run before every deploy — catches common mistakes that cause silent crashes on GenVM:
pip install genvm-linter # or: pipx install genvm-linter
genvm-lint check contract.py # lint + validate (recommended)
genvm-lint lint contract.py # fast AST checks only (~50ms)
Common catches:
ValueError→ should begl.vm.UserError- Unreachable nondet blocks
- Forbidden imports:
random,os,time(non-deterministic) - Incorrect storage type usage
Direct Mode Testing
Test locally without Studionet — catches logic bugs fast (~0.4s vs ~100s):
pip install genlayer-test
# Write pytest tests, run locally against mocked GenVM
# See: https://github.com/genlayerlabs/genlayer-testing-suite
Local tests catch Python/logic bugs. Studionet catches GenVM + consensus bugs. Always test on Studionet before declaring production-ready.
Debugging
- GenLayer Studio: Use
genlayer upfor local testing - Logs: Filter by transaction hash, debug level
gl.trace(): Trace output visible in validator logsgl.trace_time_micro(): Measure execution time
Reference Files
references/sdk-api.md- Complete SDK API referencereferences/equivalence-principles.md- Consensus patterns in depthreferences/examples.md- Full annotated contract examples (incl. production oracle)references/deployment.md- CLI, networks, deployment workflowreferences/genvm-internals.md- VM architecture, storage, ABI detailsreferences/production-learnings.md- Hard-won lessons: storage gotchas, multimodal prompting, evidence design for AI jury, local vs studionet gaps, bridge relay tips
Links
- Docs: https://docs.genlayer.com
- SDK: https://sdk.genlayer.com
- Studio: https://studio.genlayer.com
- GitHub: https://github.com/genlayerlabs