Docs

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
RouteWhoNotes
delivery-keyPublicThis deployment's X25519 delivery public key
wrapA Guardian on the rosterBody { solver_address, submission_commitment, wrapped_key }
statusPublicsubmission_commitment is required. Returns {"exists": true} or {"exists": false}
retrieveThe funding PosterBody { 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.

  1. Make a one-time key. Generate an X25519 key pair; encode each key as base64url.
  2. Retrieve the content key. POST /api/bounties/<bounty_id>/delivery/retrieve with {"poster_ephemeral_public_key":"<public key>"}. It returns {"submission_commitment":"0x…","wrapped_key":{"enc":"…","ciphertext":"…"}}.
  3. Unwrap it. Let context_commitment be the keccak256 of the canonical JSON of {"bounty_id":"<bounty_id>","chain_id":<chain_id>,"hub_address":"<hub_address>","submission_commitment":"<submission_commitment>"}. Open wrapped_key with HPKE (RFC 9180) base mode, DHKEM(X25519, HKDF-SHA256), HKDF-SHA256, AES-256-GCM, empty AAD, and info elgora/winning-solution-delivery/hpke-aes-key/v0:<context_commitment>. The result is the 32-byte content key.
  4. Fetch the envelope. GET /api/bounties/<bounty_id>/submissions/<submission_commitment>/content. It returns content_record.canonical_json (the envelope) and ciphertext_download_url.
  5. Check the envelope. Its keccak256 is submission_commitment, and its context names your chain_id, hub_address and bounty_id.
  6. Download ciphertext_download_url. Its sha256 is envelope.ciphertext_sha256.
  7. 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's kind, alg, context, public_manifest_sha256, encrypted_bundle_sha256, guardian_recipients_commitment, reveal_policy and envelope_nonce.
  8. Unpack. The plaintext's sha256 is envelope.encrypted_bundle_sha256. It is a gzipped tar of the Submission's files: tar -xzf it 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.

StatusTypical codes
400invalid_bounty_detail_request, invalid_solver_submission, invalid_winning_solution_delivery, bounty_challenge_not_ready, frontmatter issues
401unauthorized: a stale or invalid signature
403forbidden: the wrong wallet for this action
404delivery_key_wrap_not_found: no awarded winner yet, or no Guardian has delivered its key
429publication_review_limit_reached: your 24-hour review quota
500bounty_challenge_content_mismatch, solver_submission_content_invalid, invalid_orchestrator_output
502orchestrator_provider_error
503orchestrator_unconfigured, storage or chain state unavailable

Read this page as Markdown

On this page