Skip to main content

Build an attested on-chain app

The starter templates show off-chain results and mobile signing. A real on-chain app that moves money usually needs:
  • a chain contract that holds value
  • a way for users to register
  • a Gora-attested decision that names a winner / recipient / outcome
  • a single on-chain settlement transaction that pays out the winner
This page describes the recommended pattern. The reference implementation is in examples/apps/raffle-app.

One operator-signed hop

gora result -> operator tx -> contract records (round, winner, attestation) AND transfers payout
The operator hop is what makes the result on-chain-verifiable: an attestation hash from the off-chain Gora app is written to the same contract storage slot as the winner. The contract pays the winner in the same transaction, so the winner does not have to sign or claim anything — funds land in their wallet automatically. The Gora node stays key-less. The operator key is the only thing that can move funds, and it can only do so by also recording the matching attestation.

Minimum on-chain interface

Whatever chain you target, the contract should expose:
FunctionSignerWhat it does
register()participantadds caller to the participant set
fund()anyonedeposits native token into the treasury
settleWinner(round, winner, attestationHash)operatorstores the result of one round and transfers payoutBps of the treasury to winner in the same call
The operator address is set at deploy time and is held by whoever orchestrates the Gora invoke (typically a script on the dev VM).

Off-chain app shape

The Gora app runs off-chain and returns JSON. For an on-chain app it should return:
{
  "type": "raffle_draw",
  "schema_version": 1,
  "app_id": "my_app",
  "request_id": "round_001",
  "round": 1,
  "winner": { "chain": "base", "address": "0x..." },
  "registry_hash": "<sha256 of sorted participants>",
  "decision": { "status": "approved" },
  "chain": "base",
  "settle_instruction": {
    "schema_version": 1,
    "chain": "base",
    "kind": "auto_settle",
    "contract_ref": "0x...",
    "winner": "0x...",
    "human_summary": "Round 1: 5% of the base treasury will be sent to 0x... automatically."
  }
}
Two things matter:
  • winner is the canonical attested result.
  • settle_instruction is purely informational. The orchestrator script computes a sha256 over the deterministic parts of this output (app_id, request_id, round, winner, registry_hash) and passes that hash to settleWinner. Any committee member can re-run the off-chain app on the same input and reproduce the same hash byte-for-byte.

Operator orchestration

A small script ties everything together:
participants list (from chain or local config)

gora invoke my_app --input round_N.json

attestation_hash = sha256(canonical(result))

chain.settleWinner(round, winner, attestation_hash)   # operator signs

contract transfers payout to winner inside the same tx
The reference is scripts/raffle.mjs draw in examples/apps/raffle-app.

Determinism

Validators reproduce non-deterministic AI outputs by re-running prompts (see MVP §10). For a deterministic app like a raffle, make the selection itself deterministic so a validator committee can verify exact byte-equality:
const seed = `${appName}|round=${round}|registry=${JSON.stringify(sortedParticipants)}`;
const mac  = createHmac("sha256", "domain-tag").update(seed).digest();
const index = bigIntFromBytes(mac.slice(0, 16)) % BigInt(sortedParticipants.length);
Document the domain tag and the sort order so anyone can recompute the result from the published registry.

Trust assumptions to call out in your policy

  • The operator key can lie about the winner. Mitigate by running the off-chain app under a Gora committee quorum so multiple committee members independently sign the attestation before the operator broadcasts the settlement.
  • The contract must not pay out more than the configured share. Cap by payoutBps and never accept an externally-supplied payout amount in settleWinner.
  • Compute the payout from the live treasury balance inside the contract, not from the off-chain output. The contract is the source of truth for how much money is currently held.

What to copy

Open examples/apps/raffle-app and copy:
  • gora.app.json and policy.json for the multi-chain manifest shape
  • src/index.ts for the deterministic winner-picker
  • contracts/{base,solana,algorand}/* for the single-settlement payout pattern
  • scripts/raffle.mjs for the orchestration CLI
  • test-wallets/ for a safe way to keep devnet keys out of git
Next: Devnet quickstart.