# 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.

<Callout type="warn" title="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.
</Callout>

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.

```sh
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](/docs/reference/addresses).

### One bounty, with everything attached

```graphql
{
  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

```graphql
{
  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:

```graphql
{
  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`](https://github.com/moleculeprotocol/elgora-v0/tree/main/packages/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

```graphql
{
  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](/docs/how-it-works/guardian-roster).

### How far behind is it?

```graphql
{ _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](/docs/reference/addresses#values-that-change) 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.id` is the 32-byte big-endian bounty id, so it sorts numerically; use
  the `bountyId` field for human ordering.
* `consensusReachedAt` records 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.
* `submissionCount` on a bounty counts accepted `submit` **calls**, while
  `Submission` rows are one per Solver — so after a re-submission the count is
  legitimately larger than the row count.
* `awardedPayoutData` is 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.
