Batch proposing
Prepare, submit, and track up to 100 independent proposals per request.
The Proposer API supports batch proposing at https://api.risklabs.co. Use POST endpoints to submit independently signed proposals and poll their candidate statuses, with an optional helper to fetch signing context. Each request accepts 1–100 items, subject to the shared rate-limit budget. This is a request-size cap, not a throughput quota.
| Step | Endpoint | Request body |
|---|---|---|
| Prepare (optional) | /propose/batch/signing-data | { "identifiers": ["question-id-or-slug"] } |
| Submit | /propose/batch | { "proposals": [signedProposal, ...] } |
| Track | /propose/batch/status | { "candidate_ids": ["candidate-uuid"] } |
Send Content-Type: application/json. Use these exact paths, without a trailing slash. All three are available through the same gateway as the single-proposal v1 endpoints.
Preparation is strictly optional. If you already have the current request details, bond requirements, and trusted signing configuration, build and sign each Permit2 witness locally and go straight to submission. The API does not require a prior /propose/batch/signing-data call or any preparation token.
How a batch behaves
Each proposal has its own authorization, candidate ID, and outcome. The API processes signing-context and admission items with bounded concurrency and returns results in input order. One item can succeed while another fails; there is no rollback of successful items when a sibling fails.
A completed batch call returns HTTP 200, even when some or all items failed. Read every item's status_code and error. Request-level failures, such as malformed input, more than 100 items, rate limiting, or unavailable service, can instead return a non-200 response without per-item results. Schema validation applies to the whole request before items are processed.
Batching combines HTTP requests. It does not create one shared signature, a durable batch ID, an atomic group of candidates, or a guarantee that candidates will execute in the same Polygon transaction. Admission is asynchronous: acceptance does not mean the proposed answer is correct, selected, or on-chain.
Prepare signing context
POST https://api.risklabs.co/propose/batch/signing-data
{
"identifiers": ["example-market-a", "example-market-b"]
}Use real canonical question IDs or market slugs in place of the examples. For each successful item, results[i] contains the original identifier, status_code: 200, and signing_data; a failed item contains an error instead. The signing context provides:
| Field | Purpose |
|---|---|
question_id, request | Canonical question, oracle, requester, oracle identifier, timestamp, and ancillary bytes |
price.allowed_values | Allowed numerical proposal prices |
bond.token, bond.required_bond | Bond token and required amount in raw token units |
contracts.permit2, contracts.signed_proposer | Signing domain contract and authorized spender |
signing | Chain ID, maximum payment, and witness type definitions |
Verify the request and contracts against trusted configuration. Choose an allowed price, ensure enough bond balance and Permit2 allowance, and allocate a distinct unused Permit2 nonce for every authorization. Reserve funds across all proposals that could execute; preparation does not reserve balance, allowance, or nonces.
Sign each proposal
The optional batch preparation helper returns signing context, not the complete v1 typed_data response. Construct the Permit2 witness structure for each proposal. The fields below refer to the helper response; if you skip it, supply the equivalent current values from your own request data and trusted configuration:
- Domain:
Permit2, chainsigning.chain_id(Polygon137), verifying contractcontracts.permit2, with no domain version. - Message: token
bond.token, your chosen permit amount, unused nonce and deadline, and spendercontracts.signed_proposer. - Witness: the returned request's oracle, requester,
identifier.raw, timestamp, and ancillary bytes, plus your chosenproposedPriceandmaxPayment: 0.
The witness's oracle identifier is distinct from the question ID. Use bigint for Solidity integers in your signer, preserve exact ancillary bytes, and sign each message independently. Check wallet eligibility before submitting.
Submit signed proposals
POST https://api.risklabs.co/propose/batch
The following illustrates one item; add up to 100 to proposals. Replace the example identifier, wallet, signature, nonce, amount, and deadline with verified values. The timestamp is illustrative; choose a short deadline at least five minutes beyond admission.
{
"proposals": [
{
"identifier": "example-market-a",
"client_ref": "job-1",
"proposer": "0x1111111111111111111111111111111111111111",
"proposed_price": "1000000000000000000",
"max_payment": "0",
"permit": {
"amount": "10500000",
"nonce": "7",
"deadline": 1800000000
},
"signature": "0x..."
}
]
}Save each exact signed payload before submitting. client_ref is an optional caller-supplied correlation string, echoed in the result; it is not an idempotency key. Use the returned canonical question ID as identifier where possible.
Batch admission uses the existing compatibility format: proposed_price is a numerical price string, permit.deadline is a JSON integer in Unix seconds, and there is no version object or outcome field. permit.amount and permit.nonce remain decimal strings. Do not send a v1 admission payload unchanged.
The response has results and summary (accepted, replayed, rejected). Each result includes client_ref, status_code, and either candidate or error:
| Item status | Meaning | Next step |
|---|---|---|
202 | New candidate durably accepted for asynchronous validation | Save candidate.candidate_id and poll |
200 | Exact signed-payload replay within the current request round; existing candidate returned | Follow that candidate |
| Other | This item failed; inspect error.code and error.detail | Handle that item's error before retrying |
A successful candidate contains candidate_id, status, received_at, and status_url. For gateway batch clients, poll the batch status endpoint below. The compatibility status_url points to /propose/{question_id}/status/{candidate_id}, a single-item route that is not exposed through this gateway.
Track candidates
POST https://api.risklabs.co/propose/batch/status
{
"candidate_ids": ["11111111-1111-4111-8111-111111111111"]
}Replace the example UUID with a returned candidate ID. Each result contains candidate_id, status_code, and either candidate or error. Unknown candidates produce per-item 404 results without failing other lookups.
Follow the candidate's status and its separate, nullable submission. A selected candidate has been chosen for execution; a submission marked confirmed or indexed has reached chain. Continue tracking oracle liveness, disputes, and settlement separately. Batch statuses include nullable failure details (code and detail), but omit v1's richer recovery fields such as next_action. If you need that guidance, use the exposed GET /v1/proposals/{question_id}/status/{candidate_id} route with the matching question ID.
Limits and retries
Split more than 100 items into separate requests. Preparation, submission, and each polling pass all charge by item count against the same budget; eligibility and single-proposal calls also share that budget. Preparing 100 questions, submitting 100 candidates, and polling them once costs 300 items, not 100. Skipping the optional preparation call saves its 100-item charge; submission plus one polling pass still costs 200 items and remains subject to the shared budget.
The backend defaults are 120 items per IP per 60-second window and 1,000 items globally per window; operator configuration can differ. The gateway does not forward a trusted end-user IP, so clients seen under the same proxy identity share the upstream IP budget. Do not assume an independent allowance for each wallet or API caller.
With those defaults, preparing 100 items and immediately submitting 100 exceeds the IP quota and the submission returns 429 before admission. Pace phases across quota windows and budget for every polling pass. Splitting a batch or adding concurrency does not reduce its total item cost. Rejected rate-limited attempts also charge the counters, so avoid tight retry loops. Honor Retry-After when supplied or the retry delay in the error detail, and use backoff with jitter. Coordinate higher-throughput capacity with the integration team; do not assume the 100-item cap reserves quota or guarantees completion in one window.
Keep concurrent calls bounded and reserve collateral across requests. Request and response size limits can also require smaller batches, especially with large signatures or ancillary data; a gateway 413 means the request body is too large.
An HTTP 200 only means you received per-item outcomes. Handle known item errors individually. For transient failures, retry the affected items with their original signed payloads after backoff. If a response is lost, times out, or returns a gateway 502/504, some candidates may already have been stored. Within the current request round, replay the exact saved items to recover their acknowledgements. Do not generate new nonces merely because a response was uncertain.
Replay lookup does not recover candidates from an earlier oracle request round. If the indexed round has advanced, stop replaying the old signature and reconcile the earlier candidate before preparing a new authorization. Poll its saved candidate ID if available; if the lost response left you without one, contact the integration team with the question ID, client_ref, and submission time, never the signature or private key. A new round requires fresh signing context. See status and retries for the shared lifecycle and recovery principles.