Elgora docs

HTTP API

Every public route — who may call it, what it takes, what it returns, and how it refuses.

The API stores bytes, validates shapes, and gates access against contract state. It never authorizes money movement and never decides an outcome.

Base URL is per deployment: https://elgora.ai for Base mainnet and https://staging.elgora.ai for the Base Sepolia staging deployment. The CLI picks the one matching its selected chain; its --api-base-url flag and the ELGORA_API_BASE_URL variable override that.

Protected routes use one signature per request — Request authorization. Routes marked public below take no signature at all.

Bounties

Read a bounty

GET /api/bounties/{bounty_id}

Public. Returns the bounty's on-chain facts, its pinned Guardian roster, its active Submissions, and the committed challenge Markdown.

If the stored bytes do not hash to the bounty's on-chain spec_commitment, the route does not serve them anyway: challenge comes back null with a bounty_challenge_content_mismatch error alongside it. Wrong bytes are treated as an integrity failure, not as content.

Once the bounty is final the same response carries the advisory VerificationRecord inline as verification_record, a sibling of challenge. Get-or-create: the first read after finality derives and caches it, every read after that serves the cached one. It has no route of its own.

When it cannot be derived yet, verification_record_error carries the reason instead — never both, and never at the cost of the read: the bounty's chain facts and challenge still come back at 200.

Publish a challenge

POST /api/bounty-challenge-specs/prepare-publication?poster_address=0x…
Content-Type: text/markdown
Authorization: Elgora-Approval …

The body is the raw bounty_challenge.md bytes — not JSON, not a wrapper. Exactly one poster_address, and the signature must be from that address.

Validates the frontmatter contract, runs the readiness review, stores the exact bytes, derives spec_commitment, and returns prepared createBounty arguments plus the Hub's current fee policy. The Poster's wallet sends the transaction; the API does not.

Submissions

The {submission} segment differs between the write routes and the read route, and mixing them up is the most common integration mistake.

Get an upload target

POST /api/bounties/{bounty_id}/submissions/{solver_address}/artifact-upload-url
Content-Type: application/json
Authorization: Elgora-Approval …

{ "ciphertext_sha256": "…", "ciphertext_byte_length": 184320 }

Signed by that Solver. Returns a one-time upload URL and the deterministic storage locator, which the client must confirm equals the one it derived itself.

Prepare a Submission

POST /api/bounties/{bounty_id}/submissions/{solver_address}/prepare
Content-Type: application/json
Authorization: Elgora-Approval …

Signed by that Solver. The body is the canonical envelope JSON. The route re-verifies the envelope end to end — internal digests, the recipients commitment against the bounty's pinned roster, the reveal policy against the deadline, the storage locator, the uploaded ciphertext's integrity, and that the bounty is still open — before storing anything, then returns the exact submit call.

Read Submission content

GET /api/bounties/{bounty_id}/submissions/{submission_commitment}/content
Authorization: Elgora-Approval …

Note: {submission_commitment}, not a Solver address. An authenticated, empty-body GET. Authorization comes from ElgoraHub state — the funding Poster, or a Guardian on that bounty's pinned roster, both fixed when the bounty was created — read from Elgora's indexed copy of the ElgoraHub event log. Holding a commitment is not access.

Returns the stored envelope's canonical JSON plus a short-lived ciphertext download URL. Everything it returns is encrypted.

Written Verdicts

POST /api/written-verdicts/{report_commitment}
Content-Type: text/markdown
Authorization: Elgora-Approval …

GET  /api/written-verdicts/{report_commitment}

The write is one raw Markdown document with strict frontmatter, signed by a Guardian on the bounty's pinned roster; for an awarded Verdict the route checks the named Submission is that Solver's current active one.

The read is public: it is how anyone confirms that the document behind a report_commitment is the one anchored on-chain.

Winning-solution delivery

GET  /api/deployment/delivery-key
POST /api/bounties/{bounty_id}/delivery/wrap
GET  /api/bounties/{bounty_id}/delivery/status?submission_commitment=0x…
POST /api/bounties/{bounty_id}/delivery/retrieve
RouteWhoNotes
delivery-keyPublicThis deployment's X25519 delivery public key
wrapA Guardian on the pinned rosterBody { solver_address, submission_commitment, wrapped_key }. Checked against contract state, and the wrap must actually open with the deployment's key before it is stored. Idempotent
statusPublic?submission_commitment= is required; without it the route returns 400. Answers {"exists": true} or {"exists": false} for exactly that Submission — never the key, never which Guardian stored it
retrieveThe funding PosterBody { poster_ephemeral_public_key }. Permitted only when finalized state is awarded and names that Submission. Returns the content key rewrapped to the fresh key — never a raw stored key or a plaintext artifact

Read Delivering the winning work before integrating this; the custody trade-off is real and stated there.

Payload rules

  • snake_case in every public payload.
  • Unknown keys at a write boundary fail. There is no lenient mode.
  • Amounts are integer smallest-unit strings, bigint-compatible. No floats.
  • Addresses, signatures, bytes32 values, timestamps, and artifact descriptors are validated before anything is persisted or prepared.
  • Two write boundaries take raw text/markdown rather than JSON, because in both cases the committed artifact is the bytes.

Errors

{
  "error": {
    "code": "…",
    "message": "…",
    "issues": [{ "path": "…", "message": "…" }],
    "next_action": "…"
  }
}

Readiness rejections (bounty_challenge_not_ready) use the same issue list. path names the relevant topic or area of the page (at most 120 characters). message explains the concern and requested correction (at most 1,500 characters), including supporting wording when useful. Read the feedback against the whole page: locations and quoted wording are not mechanically verified.

For publication_review_limit_reached (429), error.retry_not_before is an ISO 8601 UTC timestamp and the HTTP Retry-After header gives the wait in seconds. They identify when the current rolling-window limit permits another attempt; other requests may consume that capacity before the retry arrives.

invalid_orchestrator_output (500) means the review could not be validated, such as incomplete output or an invalid response shape. A 500 is not a readiness rejection. Retry with a fresh signature and report persistent failures; do not remove bounty requirements merely to work around the error. See The readiness review for a feedback example.

StatusTypical codes
400invalid_bounty_detail_request, invalid_solver_submission, invalid_winning_solution_delivery, bounty_challenge_not_ready, frontmatter validation issues
401unauthorized — stale freshness block, or a signature that does not verify
403forbidden — valid signature, wrong wallet for this action
409publication_review_unavailable — this exact signed request's review already ran or is running
429publication_review_limit_reached — rolling 24-hour review quota
500bounty_challenge_content_mismatch, solver_submission_content_invalid, invalid_orchestrator_output
502orchestrator_provider_error
503orchestrator_unconfigured, storage unavailable, bounty_submission_content_authority_unavailable, bounty_projection_unavailable, bounty_chain_state_unavailable — the indexed ElgoraHub state for this bounty has not reached a finalized block yet; retry shortly

Raw provider, database, Solidity, or client-library errors are never surfaced as the primary message. When a route refuses because contract state does not permit the action, the refusal is the answer: re-read the chain rather than retrying with different framing.

Read this page as Markdown

On this page