DEV Community

Rotifer Protocol
Rotifer Protocol

Posted on • Originally published at rotifer.dev

Build a Production Hybrid Gene

Most genes are pure functions — they take input, compute, and return output. But real-world agents need to talk to external services: LLM providers, weather APIs, databases, search engines. Rotifer handles this through Hybrid fidelity, which gives a gene controlled network access via a Network Gateway — a sandboxed fetch proxy with domain whitelisting, rate limiting, timeout enforcement, and response size caps.

This tutorial walks through building a production-quality Hybrid gene from scratch.

When to Use Hybrid

Fidelity Network Access Use Case
Wrapped None Pure logic — text transforms, math, formatting
Hybrid Gateway-controlled External API calls — LLMs, weather, search, databases
Native None CPU-bound computation — compiled WASM, crypto, parsing

Choose Hybrid when your gene needs to reach the outside world. The Network Gateway ensures it can only reach domains you explicitly allow, at rates you define.

Step 1: Initialize the Gene

rotifer init weather-gene --fidelity Hybrid
Enter fullscreen mode Exit fullscreen mode

This scaffolds a gene directory with a phenotype.json pre-configured for Hybrid fidelity:

{
  "name": "weather-gene",
  "domain": "utility",
  "description": "",
  "inputSchema": {
    "type": "object",
    "properties": {
      "prompt": { "type": "string" }
    },
    "required": []
  },
  "outputSchema": {
    "type": "object",
    "properties": {
      "result": { "type": "string" }
    }
  },
  "dependencies": [],
  "version": "0.1.0",
  "fidelity": "Hybrid",
  "transparency": "Open",
  "network": {
    "allowedDomains": [],
    "maxTimeoutMs": 30000,
    "maxResponseBytes": 1048576,
    "maxRequestsPerMin": 10
  }
}
Enter fullscreen mode Exit fullscreen mode

The network block is what distinguishes Hybrid from other fidelity levels. Every field matters — the runtime enforces all of them.

Step 2: Configure Network Access

Edit phenotype.json to declare the domains your gene needs:

{
  "network": {
    "allowedDomains": ["wttr.in"],
    "maxTimeoutMs": 10000,
    "maxResponseBytes": 524288,
    "maxRequestsPerMin": 5
  }
}
Enter fullscreen mode Exit fullscreen mode

Domain rules:

  • Exact match"wttr.in" allows only wttr.in, not subdomains.
  • Wildcard"*.supabase.co" matches abc123.supabase.co and supabase.co itself.
  • Forbiddenlocalhost, 127.0.0.1, 192.168.*, 10.*, and other private/loopback addresses are always rejected by the publish pipeline. They pass local tests but will be blocked on publish.

Keep the domain list minimal. Every domain you add is an attack surface your gene exposes. If your gene calls one API, list one domain.

Tuning the limits:

Field Default Guidance
maxTimeoutMs 30000 Set to 2–3× your expected API latency. 10s for fast APIs, 30s for LLMs.
maxResponseBytes 1048576 (1 MiB) Enough for most JSON APIs. Increase for large payloads, decrease for simple endpoints.
maxRequestsPerMin 10 Match your upstream rate limit. Don't set higher than the provider allows.

Step 3: Write the Express Function

Create express.ts in the gene directory. Hybrid genes receive a ctx object with a gatewayFetch function — this is your only network interface. Global fetch is not available inside the gene sandbox.

import type { GatewayFetchOptions, GatewayResponse } from "@rotifer/core";

interface WeatherInput {
  city: string;
  format?: "brief" | "detailed";
}

interface WeatherOutput {
  result: string;
  city: string;
  source: string;
}

export default async function express(
  input: WeatherInput,
  ctx: { gatewayFetch: (url: string, options?: GatewayFetchOptions) => Promise<GatewayResponse> }
): Promise<WeatherOutput> {
  const city = encodeURIComponent(input.city);
  const formatParam = input.format === "detailed" ? "" : "?format=3";
  const url = `https://wttr.in/${city}${formatParam}`;

  const response = await ctx.gatewayFetch(url, {
    method: "GET",
    headers: { "Accept": "text/plain" },
  });

  if (response.status !== 200) {
    throw new Error(`Weather API returned ${response.status}: ${response.body}`);
  }

  return {
    result: response.body.trim(),
    city: input.city,
    source: "wttr.in",
  };
}
Enter fullscreen mode Exit fullscreen mode

Key points:

  • ctx.gatewayFetch, not fetch — this is the gateway-proxied version that respects your domain whitelist, rate limit, and timeout.
  • Input validation happens before the call — encode user-supplied strings, validate formats.
  • Return structured output — your gene's consumers (agents or other genes) expect typed data, not raw API responses.

Step 4: Handle Gateway Errors

The Network Gateway throws NetworkGatewayError with specific codes. Production genes must handle each one gracefully:

import type { GatewayFetchOptions, GatewayResponse } from "@rotifer/core";

interface WeatherInput {
  city: string;
}

interface WeatherOutput {
  result: string;
  city: string;
  source: string;
  degraded: boolean;
}

export default async function express(
  input: WeatherInput,
  ctx: { gatewayFetch: (url: string, options?: GatewayFetchOptions) => Promise<GatewayResponse> }
): Promise<WeatherOutput> {
  const city = encodeURIComponent(input.city);

  try {
    const response = await ctx.gatewayFetch(`https://wttr.in/${city}?format=3`, {
      method: "GET",
      headers: { "Accept": "text/plain" },
    });

    return {
      result: response.body.trim(),
      city: input.city,
      source: "wttr.in",
      degraded: response.truncated,
    };
  } catch (err: any) {
    switch (err.code) {
      case "DOMAIN_BLOCKED":
        throw new Error(`Configuration error: wttr.in not in allowedDomains`);

      case "RATE_LIMITED":
        return {
          result: "Weather service temporarily unavailable (rate limited). Try again shortly.",
          city: input.city,
          source: "fallback",
          degraded: true,
        };

      case "TIMEOUT":
        return {
          result: "Weather service did not respond in time. Try again later.",
          city: input.city,
          source: "fallback",
          degraded: true,
        };

      case "RESPONSE_TOO_LARGE":
        return {
          result: "Weather response exceeded size limit. Try brief format.",
          city: input.city,
          source: "fallback",
          degraded: true,
        };

      case "NETWORK_ERROR":
        return {
          result: `Network error: ${err.message}`,
          city: input.city,
          source: "fallback",
          degraded: true,
        };

      default:
        throw err;
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

The pattern: throw on configuration errors (DOMAIN_BLOCKED means the phenotype is misconfigured), degrade gracefully on transient errors (rate limits, timeouts, network failures). The degraded flag lets downstream consumers know the result may be incomplete.

Step 5: Test and Compile

Run the gene's test suite:

rotifer test weather-gene
Enter fullscreen mode Exit fullscreen mode

Expected output:

  ✓ weather-gene/express — city:"London" → result contains "London"
  ✓ weather-gene/express — city:"東京" → result contains "東京"
  ✓ weather-gene/error  — invalid city → graceful fallback

  3 passed | 0 failed
  Next: rotifer compile weather-gene
Enter fullscreen mode Exit fullscreen mode

Then compile to IR:

rotifer compile weather-gene
Enter fullscreen mode Exit fullscreen mode
  Compiling weather-gene v0.1.0
  Fidelity:  Hybrid
  Network:   wttr.in (1 domain)
  Timeout:   10000ms | Max body: 512 KiB | Rate: 5/min
  Output:    genes/weather-gene/weather-gene.wasm (42 KiB)

  ✓ Compiled successfully
Enter fullscreen mode Exit fullscreen mode

The compiler embeds the network configuration into the WASM custom sections. The runtime reads this at load time — there's no way for the gene to bypass its declared network policy.

Step 6: Publish and Run in an Agent

Publish to the Rotifer Cloud registry:

rotifer publish weather-gene
Enter fullscreen mode Exit fullscreen mode

Now use it in an agent:

rotifer agent create my-weather-agent --genes weather-gene
rotifer agent run my-weather-agent --input '{"city": "Berlin"}'
Enter fullscreen mode Exit fullscreen mode
  Agent: my-weather-agent
  Gene:  weather-gene (Hybrid, gateway: wttr.in)

  Result: Berlin: ⛅ +12°C
Enter fullscreen mode Exit fullscreen mode

The runtime auto-detects Hybrid fidelity, reads the network config from the compiled IR, constructs a NetworkGateway instance with the declared limits, and injects gatewayFetch into the gene's execution context. The gene never touches raw fetch.

Best Practices

Minimal domain surface. List only the domains your gene actually calls. One gene, one API, one domain is the ideal. If you need multiple APIs, consider splitting into separate genes that compose in an agent.

Environment variables for API keys. Never hardcode credentials in gene source. Use ctx.env for API keys that the agent operator provides at deploy time. The gene declares what it needs; the operator supplies values.

Reasonable limits. Set maxTimeoutMs to 2–3× your expected latency, not the maximum 30s. Set maxResponseBytes to the actual payload size you expect, not 1 MiB by default. Tight limits catch regressions early.

LLM provider agnosticism. If your gene wraps an LLM, accept the endpoint URL and model name as input parameters. Don't hardcode api.openai.com — let the operator choose their provider. Declare "allowedDomains": ["*.openai.com", "*.anthropic.com", "*.mistral.ai"] or accept the domain as a runtime parameter.

Test both happy and error paths. Every gatewayFetch call can fail five ways. Your test suite should cover at minimum: successful response, rate-limited response, and timeout. Use the NetworkGateway class directly in tests to simulate each scenario.

Deep Dive: See the full Hybrid Gene Development Guide for Network Gateway reference and RAG pipeline examples.

Top comments (0)