Skip to main content

External calls

Real apps often need data from outside the validator process: a price feed, a weather API, a model provider, or a webhook. Gora apps reach external endpoints through a sandboxed fetch-like host function. The committee then has to agree on what came back. This page explains how that works and how to design your app so consensus succeeds.

The host function

Inside your app you call:
const res = await gora.fetch("https://api.example.com/v1/price?symbol=BTC", {
  method: "GET",
  headers: { "X-Source": "gora-app" },
  timeout_ms: 5000,
});

const body = await res.json();
gora.fetch is provided by the Gora runtime, not by your language’s built-in fetch. It enforces:
LimitDefaultWhy
Total timeout5 sA slow upstream cannot stall the round.
Response size256 KBAvoid memory exhaustion.
Concurrent calls per request4Predictable resource use.
Allowed schemeshttps://No raw IP, no http:// for production policy.
AllowlistDeclared in policy.jsonThe app states up front which hosts it talks to.
Apps declare their allowlist:
{
  "network": {
    "allow_hosts": [
      "api.example.com",
      "api.openai.com"
    ]
  }
}
A call to a host not in the allowlist fails fast. The runtime never silently routes around the policy.

How the committee agrees

Validators run your app independently. If they each made their own live HTTP call, they would get slightly different responses (caches, timing, server clocks, rate limits) and never agree. Two strategies keep responses consistent.

Strategy A — Proposer fetches, committee verifies

This is the default for deterministic apps.
Proposer runs the app, including gora.fetch calls.
The proposer's result includes a "fetch transcript":
  { url, method, body_hash, headers_kept, status }
The committee re-runs the app, but its gora.fetch is replayed
from the proposer's transcript instead of hitting the network.
Each committee member confirms the result matches.
The committee never re-calls the upstream. It only checks that, given the proposer’s transcript, the app produces the same output. This makes the round fast and bandwidth-light. The trust assumption is the same as for any deterministic output: a dishonest proposer is caught by the committee’s re-execution, because the re-execution uses the same transcript and so produces the same answer only if the proposer’s app code is honest about what it did with the data.

Strategy B — Quorum fetch (consensus-grade data)

For requests where the upstream value is itself the thing being attested (a price oracle, an event finality check), the app sets consensus: "quorum" on the fetch:
const res = await gora.fetch(url, { consensus: "quorum", quorum_field: "price" });
Every committee member fetches independently. The runtime collects the values, picks the median (or strict-majority match), and exposes that value to the app. The transcript records every validator’s raw response for audit. The round only commits if at least two-thirds of the committee got a usable response. Use quorum mode sparingly: it multiplies upstream load by the committee size and is slower.

Time and the wall clock

Every fetch in proposer-fetch mode captures the proposer’s clock as part of the transcript. Verifiers see the proposer’s fetch_started_at instead of their own Date.now() so re-execution stays deterministic. If your app needs wall-clock time between fetches, read it from req.now_ms, which is the round’s pinned timestamp — not from Date.now().

What to put in the response, what to keep out

A fetch transcript is part of the attestation. Keep it minimal.
KeepAvoid
The URL, method, status, and the body fields you actually useFull HTML pages, tracking cookies, vendor X- headers
A hash of the bodyThe raw body verbatim if it’s larger than 8 KB
Headers you read in the app (e.g. Etag, Retry-After)Bearer tokens, API keys, Authorization headers
The runtime automatically strips known-sensitive headers (Authorization, Cookie, X-Api-Key) from the transcript. Anything else you want stripped, set redact_request_headers: [...] on the fetch call.

Errors and round-fail

Network failures during proposer fetch behave like any other proposer execution error: the proposer broadcasts a round-fail with the reason, and the network retries with a new sequence. In quorum mode, a fetch that does not reach two-thirds usable responses also round-fails. Both cases are recorded in the round log so you can see why a request didn’t land.

Combining VRF with external data

A common pattern: pick a winner from an externally-fetched list, using Gora VRF as the source of randomness.
const list = await (await gora.fetch(roster_url)).json();
const idx = Number(BigInt("0x" + req.vrf.output_hex.slice(0, 16)) % BigInt(list.length));
const winner = list[idx];
Determinism is preserved because both inputs (the fetched body via the proposer’s transcript, the VRF via the round context) are agreed before voting starts.

What’s intentionally not supported

Not supportedWhy
Long-lived sockets / SSERound timing forbids it.
Raw TCP / UDPOut of scope for the host function.
Calls that depend on the validator’s IP geolocationDifferent validators would see different content.
Background fetches after the app returnsThe app result is the round’s output.
Per-validator API keysKeys live in app config and are uniform across the committee.
For workloads that genuinely need long-running off-chain work (multi-minute agents, batch jobs), see the long-running execution surface from MVP 08.5; that path runs outside a single round.