HTTP API
Every public route — who may call it, what it takes and what it returns.
The API stores content and checks requests against contract state. It never moves money and never decides an outcome.
The base URL is https://elgora.ai for Base mainnet and
https://testnet.elgora.ai for the Base Sepolia testnet. The CLI picks the
one for its chain; --api-base-url or ELGORA_API_BASE_URL overrides it.
Routes marked public need no signature. Every other route takes one signature per request: see Request authorization.
Bounties
Read a bounty
GET /api/bounties/{bounty_id}Public. Returns the bounty's on-chain facts, its Guardian roster, its
active Submissions and its challenge Markdown. If the stored challenge does not
match the bounty's spec_commitment, challenge is null and the error is
bounty_challenge_content_mismatch. Once the bounty is final, the response
also carries its verification_record, or verification_record_error while
it cannot be built yet.
Review a challenge
POST /api/bounty-challenge-specs/review?poster_address=0x…
Content-Type: text/markdown
Authorization: Elgora-Approval …The body is the raw challenge Markdown, signed by poster_address. Add
previous_spec_commitment=0x… to the query, before signing, to learn which
issues of an earlier review you resolved.
A ready page returns
{ content_record, readiness: { decision: "ready", approved_until, warnings, prior_issues_resolved, prior_issues_open } }.
The approval of these exact bytes lasts until approved_until, seven days.
warnings never block publication. A page that is not ready returns 400
bounty_challenge_not_ready with its issues. Reviewing the same approved bytes
again within seven days costs no quota.
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, signed by poster_address.
The page must name the current profile, elgora_markdown_bounty_challenge_v1;
any other returns profile_not_current. Bytes without an approval from the last
seven days are reviewed first. The route stores the bytes and returns the
spec_commitment, the createBounty arguments and the current fee policy. Your
wallet sends the transaction.
Submissions
The write routes take the Solver's address; the read route takes the
submission_commitment.
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 storage locator.
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 envelope's canonical JSON. The route
checks it against the bounty and the uploaded ciphertext, stores it and
returns the exact submit call. An envelope sealed for a different profile
than the bounty's returns invalid_solver_submission.
Read Submission content
GET /api/bounties/{bounty_id}/submissions/{submission_commitment}/content
Authorization: Elgora-Approval …Signed by the funding Poster or a Guardian on the bounty's roster. Returns the envelope's canonical JSON and a short-lived download URL for the ciphertext. 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 Markdown document, signed by a Guardian on the bounty's
roster. The read is public: anyone can confirm the document behind an
on-chain report_commitment.
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| Route | Who | Notes |
|---|---|---|
delivery-key | Public | This deployment's X25519 delivery public key |
wrap | A Guardian on the roster | Body { solver_address, submission_commitment, wrapped_key } |
status | Public | submission_commitment is required. Returns {"exists": true} or {"exists": false} |
retrieve | The funding Poster | Body { poster_ephemeral_public_key }. Only for an awarded bounty. Returns the content key, wrapped to your key |
See Delivering the winning work for the custody trade-off.
Open the winning Submission
elgora-cli poster:open-winning-submission does all of this for you, and the
bounty page does it in the browser. To do it yourself, sign both requests with
the funding wallet.
Conventions: base64url is unpadded; canonical JSON is RFC 8785; digests are
lowercase hex, keccak256 with 0x and sha256 without. <chain_id> and
<hub_address> belong to the bounty's network: see
Addresses.
- Make a one-time key. Generate an X25519 key pair; encode each key as base64url.
- Retrieve the content key.
POST /api/bounties/<bounty_id>/delivery/retrievewith{"poster_ephemeral_public_key":"<public key>"}. It returns{"submission_commitment":"0x…","wrapped_key":{"enc":"…","ciphertext":"…"}}. - Unwrap it. Let
context_commitmentbe thekeccak256of the canonical JSON of{"bounty_id":"<bounty_id>","chain_id":<chain_id>,"hub_address":"<hub_address>","submission_commitment":"<submission_commitment>"}. Openwrapped_keywith HPKE (RFC 9180) base mode, DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, AES-256-GCM, empty AAD, and infoelgora/winning-solution-delivery/hpke-aes-key/v0:<context_commitment>. The result is the 32-byte content key. - Fetch the envelope.
GET /api/bounties/<bounty_id>/submissions/<submission_commitment>/content. It returnscontent_record.canonical_json(the envelope) andciphertext_download_url. - Check the envelope. Its
keccak256issubmission_commitment, and itscontextnames yourchain_id,hub_addressandbounty_id. - Download
ciphertext_download_url. Itssha256isenvelope.ciphertext_sha256. - Decrypt with AES-256-GCM: the content key, IV
envelope.iv, the 16-byte tag at the end of the download, and as AAD the canonical JSON of the envelope'skind,alg,context,public_manifest_sha256,encrypted_bundle_sha256,guardian_recipients_commitment,reveal_policyandenvelope_nonce. - Unpack. The plaintext's
sha256isenvelope.encrypted_bundle_sha256. It is a gzipped tar of the Submission's files:tar -xzfit into a new, empty folder.
A bounty on elgora_markdown_bounty_challenge_v0 unpacks differently at step
8; see Winners of v0 bounties.
Protocol profile
GET /api/protocol/profile/{version}Public. Returns the fixed limits a bounty's profile: names, such as
elgora_markdown_bounty_challenge_v1: Submission size, file count, names and
paths under submission, and challenge page limits under challenge. An
unknown version returns 404 protocol_profile_not_found. Fees and windows
are read from ElgoraHub; see Limits and control.
Payload rules
- Every public payload uses
snake_case. - An unknown key in a write fails.
- Amounts are integer strings in the token's smallest unit.
- The two challenge routes take raw
text/markdown, because the committed content is the bytes themselves.
Errors
{
"error": {
"code": "…",
"message": "…",
"issues": [{ "path": "…", "message": "…" }],
"next_action": "…"
}
}Follow next_action when it is present. For publication_review_limit_reached
(429), error.retry_not_before and the Retry-After header say when to try
again. A 500 from the review is not a readiness verdict; retry it. When
contract state does not permit an action, re-read the chain rather than
retrying.
| Status | Typical codes |
|---|---|
400 | invalid_bounty_detail_request, invalid_solver_submission, invalid_winning_solution_delivery, bounty_challenge_not_ready, frontmatter issues |
401 | unauthorized: a stale or invalid signature |
403 | forbidden: the wrong wallet for this action |
404 | delivery_key_wrap_not_found: no awarded winner yet, or no Guardian has delivered its key |
429 | publication_review_limit_reached: your 24-hour review quota |
500 | bounty_challenge_content_mismatch, solver_submission_content_invalid, invalid_orchestrator_output |
502 | orchestrator_provider_error |
503 | orchestrator_unconfigured, storage or chain state unavailable |