The subgraph
What a subgraph is, why Elgora's is the authoritative read model, when you need it, and how to query it directly.
What a subgraph is
A smart contract's history lives in its events — small records the contract emits as it executes. They are permanent and cheap to write, but the chain gives you no way to ask a question like "every open bounty past its deadline, with the Verdicts recorded so far".
A subgraph solves that. An indexer replays the contract's events from a chosen start block, applies a small handler for each event, and builds a queryable database out of them. You get a GraphQL endpoint over entities — bounties, submissions, verdicts, claims — instead of an event log you would have to reassemble yourself.
Two properties matter here:
- Derived, and rebuildable. Everything in it comes from contract events. If the whole index were deleted, replaying from the deploy block reproduces it exactly. It stores no fact the chain does not already have.
- Eventually consistent. It follows the chain with a small lag. A transaction that just landed may not be indexed for a few blocks.
Why Elgora's subgraph is the authoritative read model
"Authoritative" here means: for lists, history, and joins, the subgraph is the answer, and Elgora's own clients and app use it rather than assembling those views some other way. It is the shared projection, so two consumers asking the same question get the same answer. The Guardian Analytics fee summary is also explicitly pinned to this read model.
Authoritative for reading, never for deciding
The contract remains the sole authority for the lifecycle, escrow, Verdict agreement, settlement, claims, and refunds. A projection cannot authorize any of those. When they disagree, the subgraph is simply behind — read the contract directly whenever you are about to send a transaction that depends on the answer.
That is exactly how the clients behave: they use the subgraph to find work and read the contract to act on it.
You usually do not need it
For the ordinary Poster and Solver flows, you do not have to touch GraphQL at all:
GET /api/bounties/{bounty_id}gives you one bounty, its roster, its submissions, and the committed challenge.elgora-cli verification-record <bounty_id>gives you the settled summary.- The CLI queries the subgraph for you where it needs to.
Query it directly when you want something those do not answer: a list across many bounties, a history, Verdicts as they land before settlement, or your own analytics.
Querying it
The endpoint is a standard GraphQL POST. It needs no key and no signature.
curl -s "$ELGORA_SUBGRAPH_ENDPOINT" \
-H 'content-type: application/json' \
-d '{"query":"{ bounties(first: 5, orderBy: bountyId, orderDirection: desc) { bountyId status escrowAmount submissionDeadline } }"}'The endpoint for the public deployment is in References and addresses.
One bounty, with everything attached
{
bounties(first: 1, where: { bountyId: "12" }) {
bountyId
poster
status
specCommitment
escrowAmount
guardianRosterHash
submissionDeadline
judgingDeadlineAt
settlementTimeoutAt
treasuryFeeBps
guardianFeeBps
winner
consensusReachedAt
settledAt
settledTxHash
supportingGuardians
reportCommitments
submissions {
solver
submissionCommitment
recordedAt
recordedTxHash
}
verdicts {
guardian
outcome
winner
awardedSubmissionCommitment
reportCommitment
committedAt
updatedAt
}
}
}This is the query that answers "how is judging going" — every Guardian's current Verdict, before settlement, in one round trip.
Open bounties whose deadline has passed
{
bounties(
where: { status: Open, submissionDeadline_lt: "1801699200" }
orderBy: submissionDeadline
orderDirection: asc
first: 50
) {
bountyId
submissionDeadline
judgingDeadlineAt
settlementTimeoutAt
submissionCount
}
}The current Guardian roster and protocol configuration
Protocol is a singleton — exactly one row for the whole deployment, keyed by
the Hub address. Query it with that address as id, and it comes back as a
plain object, not a list. The live roster is nested inside it, not a separate
singleton:
{
protocol(id: "0x<hub_address>") {
guardianRosterAdmin
guardianRoster {
id
memberCount
members {
account
name
encryptionPublicKey
}
}
guardianFeeRecipient
treasuryFeeBps
guardianFeeBps
guardianFeeClaimable
paused
}
}One request returns the current indexed configuration, the live roster, and
the global Guardian fee pool. The mapping maintains guardianFeeClaimable as
events arrive: each Settled event adds that Bounty's Guardian fee allocation,
recorded by the final ClaimQueued immediately before Settled (including
zero), while a nonzero Guardian amount in FeesClaimed drains it. Analytics
pins this Protocol query to the same indexed block as its other results; it
never scans every Bounty or FeeClaim to rebuild the total.
The live fee rate is not a bounty's own pinned value, which is a separate,
permanent fact recorded on that Bounty. The global pool is likewise not a
per-Bounty or per-Guardian entitlement.
The @elgora/subgraph-client
package wraps this: construct it with hubAddress once, and
client.getDeploymentConfig() reads the protocol config (roster included) and
the active-Guardian list in one request. Pass an indexedBlockNumber to pin
it to that block; omit it to read the latest indexed state.
A historical Guardian roster
{
guardianRosterSnapshot(id: "0x<guardian_roster_hash>") {
memberCount
members {
account
name
encryptionPublicKey
}
}
}The contract gives full details only for the current roster; this is how you resolve the names and encryption keys behind a roster hash a bounty pinned earlier. Always re-derive the hash and compare it with the bounty's pinned value before you trust the result — see The Guardian roster.
How far behind is it?
{ _meta { block { number } hasIndexingErrors } }Worth checking before you conclude that something is missing.
The entities
| Entity | One row per | Notes |
|---|---|---|
Bounty | Bounty | Creation facts, pinned parameters, first-agreement stamp, final outcome, supporting Guardians and their report commitments |
Submission | (bounty, solver) | A re-submission updates the row, matching the contract's one-active-Submission-per-Solver rule |
Verdict | (bounty, guardian) | Overwritten by a revision. Carries outcome, named winner, awarded commitment, and reportCommitment |
ClaimQueued | Event | Fees and refunds queued at settlement or timeout |
Claim | Event | Actual pulls, including award claims |
Guardian | Guardian address | Current roster identity, with an active flag; removed Guardians keep their historical row |
GuardianRosterSnapshot | Roster hash | The ordered members frozen at that hash, immutable |
GuardianRosterMember | (roster hash, account) | One frozen identity, referenced in order from a GuardianRosterSnapshot.members |
Protocol | Hub address (singleton) | Live configuration, the live roster (guardianRoster), and the event-maintained global guardianFeeClaimable pool — protocol(id: "0x<hub_address>") returns one plain object, never a list, since there is exactly one row |
FeeClaim | Event | One immutable row per claimFees() pull, independent of any single bounty's Claim |
Two things it does not hold. Written Verdict content is deliberate — the
subgraph stores the reportCommitment, and the document itself is served by
the API. And unlike a Bounty's own pinned parameters, Protocol exposes no
change-history rows: a config or roster change updates the singleton. A
block-pinned query can read that singleton at one indexed block, but it does
not return a list of changes. See
References and addresses for
where each value lives.
Note the difference from a bounty's own configuration: the fee rates,
recipients, payout scheme, roster hash, and deadlines a bounty snapshotted at
creation are all on Bounty, and never move.
Notes for integrators
Bounty.idis the 32-byte big-endian bounty id, so it sorts numerically; use thebountyIdfield for human ordering.consensusReachedAtrecords the first threshold crossing and never moves, even if Guardians later disagree and re-agree. It is a historical fact, not proof of current agreement.submissionCounton a bounty counts acceptedsubmitcalls, whileSubmissionrows are one per Solver — so after a re-submission the count is legitimately larger than the row count.awardedPayoutDatais passed through verbatim and never decoded by the projection.- A different operator running their own indexer against the same contract can
point clients at it with
ELGORA_SUBGRAPH_ENDPOINT. Its start block must be at or before the Hub's deploy block, or it will miss the events that create bounties.