How to Handle Async Contract Operations in GenLayer: A Complete Guide

GenLayer contracts cannot use Python's native async/await primitives; instead, use a two-step state-machine pattern with gl.nondet.* functions and equivalence-principle validators to handle asynchronous-like workflows.

The GenLayer platform enables intelligent smart contracts that interact with external data sources, but its deterministic execution model requires a different approach to handling operations that would traditionally be asynchronous. In genlayerlabs/genlayer-project-boilerplate, the reference implementation demonstrates how to structure contracts that fetch remote data or invoke LLM prompts across multiple transactions.

Why GenLayer Prohibits Native Async/Await

GenLayer smart contracts execute inside the GenVM, a deterministic virtual machine that guarantees consensus across all validators. This design has a critical constraint: contracts must run to completion in a single transaction without yielding control or suspending execution.

Native Python async/await introduces nondeterminism through coroutine scheduling, which would break validator consensus. The commented line in contracts/football_bets.py illustrates this limitation:


# Line 65 – This would be ideal but is NOT allowed in GenLayer:

# match_status = await self._check_match(bet.resolution_url, bet.team1, bet.team2)

Instead, all nondeterministic operations are performed synchronously through the GenLayer SDK, with results validated via equivalence principles.

The Two-Step Pattern for Async-Like Operations

To model workflows that span multiple transactions, split your logic into distinct phases:

  1. Initiation – Store request parameters in contract state; emit events but do not call gl.nondet.* functions.
  2. Resolution – In a separate transaction, execute the nondeterministic call and apply the validated result.

This pattern is fully implemented in the FootballBets contract at contracts/football_bets.py.

Step 1: Initiate Without Nondeterministic Calls

The create_bet method (lines 70–87) demonstrates proper initiation:

@gl.public.write
def create_bet(
    self,
    game_date: str,
    team1: str,
    team2: str,
    predicted_winner: str
) -> str:
    # Generate deterministic data for later resolution

    match_resolution_url = (
        f"https://www.bbc.com/sport/football/scores-fixtures/{game_date}"
    )
    
    bet_id = f"{gl.message.sender_address}_{team1}_{team2}_{game_date}"
    
    # Store state WITHOUT making any nondet calls

    self.bets[gl.message.sender_address][bet_id] = Bet(
        game_date=game_date,
        team1=team1,
        team2=team2,
        predicted_winner=predicted_winner,
        resolution_url=match_resolution_url,
        has_resolved=False
    )
    
    return bet_id

Key characteristics of the initiation phase:

  • All operations are deterministic and pure
  • External URLs or prompts are stored, not executed
  • Contract state tracks the pending request

Step 2: Resolve With Synchronous Nondeterministic Calls

The resolve_bet method (lines 90–103) performs the actual fetch:

@gl.public.write
def resolve_bet(self, bet_id: str):
    # Guard clauses ensure proper state

    if bet_id not in self.bets[gl.message.sender_address]:
        raise Exception("Invalid bet ID")
    
    bet = self.bets[gl.message.sender_address][bet_id]
    
    if bet.has_resolved:
        raise Exception("Bet already resolved")
    
    # SYNCHRONOUS nondeterministic call (no await)

    match_status = self._check_match(
        bet.resolution_url,
        bet.team1,
        bet.team2
    )
    
    # Apply result deterministically

    bet.has_resolved = True
    # ... determine winner, update balances ...

The _check_match helper (lines 30–55) encapsulates the LLM prompt and validation:

def _check_match(self, url: str, team1: str, team2: str) -> dict:
    # Fetch remote HTML synchronously

    page_contents = gl.nondet.web.render(url, mode="text")
    
    # Build prompt for LLM extraction

    prompt = f"""
    Based on the following content from {url},
    what was the result of the match between {team1} and {team2}?
    Return a JSON with keys: home_score, away_score.
    Content: {page_contents}
    """
    
    # Execute LLM prompt synchronously

    raw_response = gl.nondet.exec_prompt(prompt)
    
    # Equivalence-principle validator ensures consensus

    def parse_response() -> dict:
        return json.loads(raw_response)
    
    # All validators must agree on this exact output

    validated_result = gl.eq_principle.strict_eq(parse_response)
    return validated_result

Core GenLayer SDK Components

Component Purpose Example Usage
gl.nondet.web.render Fetch remote web content gl.nondet.web.render(url, mode="text"
gl.nondet.exec_prompt Execute LLM prompts gl.nondet.exec_prompt(prompt_string)
gl.eq_principle.strict_eq Exact-match validator for simple outputs gl.eq_principle.strict_eq(lambda: result)
gl.eq_principle.prompt_comparative LLM-based semantic comparison validator For fuzzy or structured data matching
gl.public.write Decorator for state-modifying functions Marks transaction entry points

Complete Example: Generic Async Pattern

Apply this template to your own contracts:

import json

class AsyncOperationContract:
    def __init__(self):
        # Track pending operations by caller

        self.pending_requests: dict[str, str] = {}
        self.results: dict[str, str] = {}
    
    @gl.public.write
    def start_operation(self, resource_url: str) -> str:
        """
        Phase 1: Store the request parameters.
        No nondeterministic calls allowed here.
        """
        request_id = f"{gl.message.sender_address}_{gl.block.number}"
        self.pending_requests[gl.message.sender_address] = resource_url
        return request_id
    
    @gl.public.write
    def complete_operation(self) -> str:
        """
        Phase 2: Execute fetch and validate result.
        This runs in a separate transaction initiated later.
        """
        url = self.pending_requests.get(gl.message.sender_address)
        if not url:
            raise Exception("No pending request for caller")
        
        # Nondeterministic fetch (synchronous)

        raw_content = gl.nondet.web.render(url, mode="text")
        
        # Define validator function

        def extract_data() -> str:
            # Deterministic processing of fetched content

            return raw_content[:1000]  # Truncate for example

        
        # Consensus validation

        validated = gl.eq_principle.strict_eq(extract_data)
        
        # Cleanup and store

        del self.pending_requests[gl.message.sender_address]
        self.results[gl.message.sender_address] = validated
        
        return validated

Error Handling Best Practices

The resolve_bet implementation demonstrates defensive patterns:

  • State validation before nondet calls: Check has_resolved, existence of records, and authorization before consuming validator resources.
  • Deterministic exceptions: Use standard raise Exception(...) for control flow—never catch nondeterministic errors.
  • Idempotent resolution design: Ensure multiple valid calls produce consistent final state.

From contracts/football_bets.py lines 91–99:

if bet_id not in self.bets[gl.message.sender_address]:
    raise Exception("Invalid bet ID")

bet = self.bets[gl.message.sender_address][bet_id]

if bet.has_resolved:
    raise Exception("Bet already resolved")  # Prevent double-resolution

Testing Async Contract Patterns

The repository includes tests/direct/test_resolve_bet.py for validating the two-step flow:


# Typical test sequence

def test_full_bet_lifecycle(contract_client):
    # Phase 1: Create (deterministic)

    bet_id = contract_client.create_bet(
        game_date="2024-01-15",
        team1="Arsenal",
        team2="Chelsea",
        predicted_winner="Arsenal"
    )
    
    # Phase 2: Resolve (nondeterministic, validated)

    result = contract_client.resolve_bet(bet_id)
    assert result["has_resolved"] is True

Run tests with the commands documented in CLAUDE.md:


# Lint contract code

python -m py_contracts lint contracts/

# Run direct tests

pytest tests/direct/test_resolve_bet.py -v

Summary

  • No async/await in GenLayer: The GenVM requires single-transaction determinism.
  • Split into initiate + resolve: Store parameters first, execute gl.nondet.* calls later.
  • Always validate with equivalence principles: Use gl.eq_principle.strict_eq or gl.eq_principle.prompt_comparative to ensure validator consensus.
  • Reference implementation: Study contracts/football_bets.py for production patterns.

Frequently Asked Questions

Can I use Python's asyncio library in GenLayer contracts?

No. The GenVM executes contracts deterministically across all validators, and asyncio's event loop introduces scheduling nondeterminism. All I/O operations must use synchronous gl.nondet.* primitives wrapped in equivalence-principle validators, as implemented in contracts/football_bets.py.

How do I handle long-running external API calls?

Store the API parameters in contract state during an initial transaction. Trigger the actual call in a subsequent transaction when needed. The external latency is absorbed between transactions, not within one. The FootballBets contract defers match result fetching from create_bet to resolve_bet for this reason.

What happens if validators disagree on a nondeterministic result?

The equivalence-principle validator rejects the transaction. Contracts must handle this by either retrying with adjusted parameters or leaving the request in pending state for future resolution. gl.eq_principle.strict_eq requires byte-identical outputs; use gl.eq_principle.prompt_comparative for semantically equivalent but textually different responses.

Can multiple pending requests exist per user?

Yes. The storage pattern maps user addresses to nested structures (e.g., self.bets[address][bet_id]). Design your initiation function to generate unique IDs—typically combining sender address, block number, and content hash—to enable concurrent pending operations.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →