Job Orders
SolverQuip.solve() proposes a job, waits for it and decodes the answer
in one call. create_order() splits that call into steps: it returns a
draft JobOrder that you can inspect, change, price and submit, and a
submitted order leaves a receipt that follows it on chain from any
session. Every option is checked against the chain’s limits before
anything is signed, so a refused option costs nothing.
This page covers the order surface. Quip Network
covers installation, configuration, encoding, cost and the error
classes, and Your First Quip Job walks one order
through end to end. The examples import from xquad.sa.quip;
xqsa.quip exports the same names. Every output on this page was
captured on aglais at specVersion 119 on 2026-10-15, from the
16-variable TSP model the tutorial builds, unless it says otherwise.
Orders
from xquad.sa.quip import SolverQuip
solver = SolverQuip.for_network("aglais", topology="native")
order = solver.create_order(model, deadline_blocks=200)
print(order)
print(order.quote())
order.submit().wait()
print(order.receipt())
create_order(model, **options) places the model and returns a draft.
It takes ten options. The first six default to the client’s values (see
Installation and
Configuration);
mapping and the three quality floors exist per order only:
| Option | Default | Meaning |
|---|---|---|
reward | The client’s reward, else QUIP_REWARD, else MinReward | Planck reserved at proposal and paid to the winners |
access | AccessOpen() | Who may answer; see Access |
resolution | "SingleBest" | How the reward is split; see Payout rules |
deadline_blocks | 100 | Blocks the order stays open, from 10 to MaxDeadlineBlocks |
block_wait | 10 | Blocks the order keeps accepting answers after the first, from 0 to MaxBlockWait |
topology | The client’s topology | A registered topology hash, or "native" |
mapping | None | An explicit variable-to-node placement; not allowed with "native" |
max_energy, min_solutions, min_diversity | None | See Quality floors |
Any other keyword raises TypeError, including mode= (deprecated) and
delivery= (client-only):
TypeError: unknown order option 'mode'; expected one of access, block_wait, deadline_blocks, mapping, max_energy, min_diversity, min_solutions, resolution, reward, topology
reward, deadline_blocks and block_wait must be integers
(TypeError: reward must be an int, not str). A raw chain dict for
access or resolution raises
TypeError: access: raw chain values are not accepted.
States
An order is in one of five states. status()["state"] reports it, and
the order box shows it in its title.
| State | Meaning |
|---|---|
draft | Built by create_order(); nothing is on chain. Only a draft accepts set(), quote() and submit() |
submitted | Proposed and included; the order has an id and a receipt |
finalized | The chain head reached the order’s effective expiry (see Job Lifecycle); wait() and status() move a submitted order here |
failed | propose_job was included but its dispatch failed: the fee was paid and no order was placed |
unconfirmed | propose_job was sent, but its outcome or order id is unknown. The order may be on chain, so it is never resent |
Calling set(), quote() or submit() on anything but a draft raises
QuipSubmissionError:
QuipSubmissionError: order 20 is finalized; set() works only on a draft order
Changing a draft
set(**options) takes the same options as create_order(). It re-runs
every check over the whole configuration and leaves the order unchanged
if any check refuses, so it also repeats the warnings for options you
did not touch. It places the model again only when topology or
mapping changes. Any change drops the cached signature, so the next
quote() or submit() signs afresh. options() returns all ten
options as a dict, and each is also a read-only property
(order.reward, order.access, order.max_energy and so on).
Quote and submit
quote() signs the order’s propose_job extrinsic once and caches it,
then prices it: the fee from the chain’s payment_queryInfo, the
account balance and the head block, all re-read on every call. Nothing
is sent and no nonce is consumed. It returns a JobQuote; Cost
Model lists its fields.
submit() sends the cached extrinsic, signing first if the order was
never quoted. Before sending, it quotes again and runs the autoconfirm
and autofund gates, as solve() does. It ends in one of three states:
submitted: the order is on chain, andsubmit()returns the order.failed: the dispatch failed, andsubmit()raisesQuipSubmissionError("propose_job 0x... failed on chain in block 0x...: <error>. The fee was paid and no order was placed.").unconfirmed: the outcome is unknown, andsubmit()raisesQuipUnconfirmedError, which carriesextrinsic_hashandblock_hash.
A signature carries the account’s nonce. When another order from the
same account is sent between quote() and submit(), the nonce has
moved, so submit() signs again. Two drafts quoted together and then
submitted show it:
a = solver.create_order(model, resolution=ResolutionTopNEqual(2))
b = solver.create_order(model, access=AccessAllow(accounts=[me]), deadline_blocks=10, block_wait=0)
a.quote(), b.quote()
a.submit()
b.submit()
proposed Quip job: order_id=21 spec_id=0x8f46...67f0 reward=1000000000000 planck (block 0x2114...c0fa)
account nonce moved from 1 to 2 since signing; signing again
proposed Quip job: order_id=22 spec_id=0x8f46...67f0 reward=1000000000000 planck (block 0x2a8b...1752)
(Log lines from the xqsa.quip logger at INFO, hashes shortened.) If
the nonce moves again while submit() signs, it refuses rather than
race the other client:
QuipSubmissionError: account nonce moved twice while submitting; another client is sending from this account. Nothing was sent.
submit() logs the quote box at INFO on the xqsa.quip logger, and
also prints it to stderr when stderr is a terminal.
Checks before signing
create_order() and set() check each option against the chain’s
limits, which the client reads once when it connects. A refusal raises
QuipOrderOptionError, a QuipSubmissionError whose option attribute
names the option. A warning is a UserWarning; the order is still
built. The limits on aglais are MinReward 1 AGLS,
MaxDeadlineBlocks 1000 and MaxBlockWait 100.
| Option | Refused | Warns |
|---|---|---|
reward | reward=999999999999 planck is below the chain's MinReward of 1000000000000 | |
deadline_blocks | deadline_blocks=1001 must be at least 10 and at most MaxDeadlineBlocks (1000) | deadline_blocks=50: orders shorter than 100 blocks often go unanswered |
block_wait | block_wait=101 must be at least 0 and at most MaxBlockWait (100) | block_wait=100 is not below deadline_blocks=100; the deadline ends the order before the wait for better answers can |
The chain itself accepts a deadline of 0, which expires on arrival. The
short-deadline warning reflects how often aglais answered: an order
with a 1-block deadline was answered 0 times out of 11, 30 blocks 28
times out of 38, and 100 blocks 108 times out of 108. The other options’
checks are in their sections below.
The order box
print(order) renders a 72-column box. Its sections are the network;
the model and where it was placed; the terms (payout, access, floors,
deadline, block wait and reward); and a summary of the latest quote and
the receipt:
╭─ Job order (draft) ──────────────────────────────────────────────────╮
│ Network aglais │
├──────────────────────────────────────────────────────────────────────┤
│ Model 16 variables, 96 couplings, binary │
│ Placed native, 16 spins │
├──────────────────────────────────────────────────────────────────────┤
│ Payout single best │
│ Access open │
│ Floors none │
│ Deadline 100 blocks │
│ Block wait 10 blocks │
│ Reward 1.000000000000 AGLS │
├──────────────────────────────────────────────────────────────────────┤
│ Quote not quoted │
│ Receipt not submitted │
╰──────────────────────────────────────────────────────────────────────╯
On a draft, the Quote row shows the last quote taken and whether the
account can afford it. Once the order has an id, a Paid row replaces
it: the fee the chain charged, read from the order’s
TransactionFeePaid event, the reward, and the quote’s total beside
them for comparison. The quoted fee is an estimate at the head block, so
the two can differ. When the fee cannot be read, the row says
fee unknown. A failed order paid the fee but placed nothing, so its
Paid row shows the fee and the quote with no reward. An unconfirmed
order’s outcome is unknown, so its Quoted row shows the quote alone.
Payout rules
The resolution option decides how the reward is split:
"SingleBest"orResolutionSingleBest(), the default: the leader, the solver the chain ranks first, takes the whole reward.ResolutionTopNEqual(n): the bestnranked solvers split it evenly.ResolutionTopNWeighted(n): the bestnsplit it by rank. Withkranked solvers, rankr(0 is best) gets weightk - r; energies decide only the order.
from xquad.sa.quip import ResolutionTopNEqual
order = solver.create_order(model, resolution=ResolutionTopNEqual(2))
n runs from 1 to 32. The rule’s own constructor checks it, so a bad
n raises ValueError
(ResolutionTopNEqual: n must be from 1 to 32, got 33) or TypeError
before any order exists. n=1 warns
ResolutionTopNEqual(1) pays exactly like ResolutionSingleBest(). Any
other value for resolution is refused: with ValueError on the
constructor, and with QuipOrderOptionError on an order:
resolution='TopNEqual' is not a payout rule; pass 'SingleBest' or one of ResolutionSingleBest(), ResolutionTopNEqual(n), ResolutionTopNWeighted(n)
ResolutionTopNWeighted needs runtime 119 or later (see Runtime
compatibility). On a network
below runtime 119, create_order(), set(), solve() and quote()
refuse it before signing (captured on runtime 118):
QuipOrderOptionError: ResolutionTopNWeighted needs runtime 119 or later, and the chain runs runtime 118; older runtimes weight by absolute energy, so a worse answer can take the larger share
A top-N rule with n above 1 and block_wait=0 warns, because the
order closes on its first answer:
resolution=top 3, equal split: with block_wait=0 only the first answer is accepted, so there can be only one winner.
Settlement
The chain ranks solvers by energy, lowest first, with the earlier
submitter first on a tie. Winners are the first n ranked solvers, so
an order with fewer answers than n has fewer winners. A receipt’s
settlement() computes each winner’s share:
TopNEqual: each winner getsreward // k, and rank 0 also gets the remainder. A reward of 10 over 3 winners splits[4, 3, 3].TopNWeighted: withtotal = k * (k + 1) / 2, rankrgets(reward // total) * w + (reward % total) * w // totalfor its weightw, and rank 0 also gets the remainder. A reward of 10 over 3 winners splits[6, 3, 1].
A top-2 order answered by three solvers, two of them tied at the best energy:
print(order.settlement())
{'claimed': False, 'winners': [{'solver': '5Gn1BACWJGtxCnkHpbonhLZJw3ZYF3LzwXEoWjub74E9RPv9', 'energy_milli': -1790000, 'allocation_planck': 500000000000}, {'solver': '5FNGHjGLzoo8CXv4iiX7z71xJycYpTGZUaA37281kZhWoWun', 'energy_milli': -1790000, 'allocation_planck': 500000000000}]}
An allocation is not a payment. One claim_reward extrinsic pays every
winner; until then the receipt shows each as awaiting claim, and
claimed turns True once the chain closes the order. Below runtime
119, settlement() of a TopNWeighted order raises
NotImplementedError, and the receipt prints that message as its
Reward row. The order box and the receipt show the rule as
single best, top 2, equal split or top 2, weighted by rank.
Access
The access option decides who may answer. "Open" or AccessOpen(),
the default, admits every registered solver.
AccessAllow(accounts=[...], types=[...]) admits a solver whose account
is listed or whose declared type is listed:
from xquad.sa.quip import AccessAllow
order = solver.create_order(model, access=AccessAllow(types=["QpuDwave"]))
Solver types are self-declared when a solver registers, and the protocol
does not verify them, so a type entry admits any solver that claims it.
On aglais, an order allowlisted to Cpu was answered by our own
solver and three others.
The allowlist is bounded:
- It must name at least one account or type:
AccessAllow: name at least one account or solver type; the chain refuses an empty list(ValueError, from the constructor). An empty list would fail on chain after the fee was paid. - At most 8 types (
ValueError). - At most
MaxBidMinersaccounts, 16 onaglais:AccessAllow lists 17 accounts; the chain's MaxBidMiners is 16(QuipOrderOptionError). - Type names come from the runtime’s
MinerType, onaglaisCpu,Gpu,QpuDwave,QpuIbm,QpuIonq,QpuPasqalandAsic:AccessAllow: unknown solver type 'Fpga'; the chain has Cpu, Gpu, QpuDwave, QpuIbm, QpuIonq, QpuPasqal, Asic(QuipOrderOptionError).
Over either cap the runtime fails with no readable error, which is why
the client refuses first. Duplicate entries are dropped, and accounts,
given as SS58, 0x hex or bytes, are normalised to 0x hex. A bad
value for access raises ValueError on the constructor and
QuipOrderOptionError on an order:
access='Bid' is not an access rule; pass 'Open' or one of AccessOpen(), AccessAllow(accounts=[...], types=[...]).
An allowlisted account that is not a registered solver is never refused, but it warns, naming the account in SS58:
UserWarning: access: 5DL9B7rhMw1cg7kXzqmdj5QZ1V67vBvfYPq2bTEgZfSXiBSJ is not a registered solver, so it cannot answer until it registers
The constructor warns for its default access, and create_order() and
set() warn when they change it. When nobody on the allowlist answers,
the no-answer error says so:
order 22 has no answers; only allowlisted solvers could answer (accounts 5DL9..iBSJ) and none did; once it is final, reclaim its reward with order.reclaim().
A solver that is not on the list and answers anyway is refused on chain
with System.ExtrinsicFailed: QuantumComputeMempool.NotEligibleSolver,
followed by the error’s description when the metadata carries one.
Network coordinators submit to allowlisted orders regardless and pay for
each refused submission, so a block explorer can show many refused
submissions against one allowlisted order: 4 to 142 per order on
aglais.
Quality floors
Three per-order options set a minimum quality an answer must reach. None of them is a client default.
max_energy: an energy ceiling. The chain drops each submitted vector above it.min_solutions: how many vectors each answer must carry. The chain stores exactly that many from each submission.min_diversity: how different those vectors must be, as a fraction of the placed spins.0means no floor.
max_energy is in model units, the same units as result.energy. For a
BINARY model the chain’s energy differs from the model’s by a constant
offset, which the client subtracts before it sends the ceiling; An
Energy Ceiling on the Quip
Network
works through the conversion. A ceiling that moves shows both values in
the order box’s Floors row, here for max_energy=-2 on the model
-2*x0 + x0*x1:
│ Floors energy <= -2 (chain -1.25) │
Refusals (QuipOrderOptionError unless marked) and warnings:
| Option | Refused | Warns |
|---|---|---|
max_energy | A bool or a non-number (TypeError); not finite; too large for a float; outside i64 once converted to milli; below the lowest energy the placed model can reach: max_energy=-1000000.0 is below -5388, the lowest energy the placed model can reach, so no answer could meet it | At or above the highest energy: max_energy=1000000.0 is at or above 7576, the highest energy the placed model can reach, so it filters nothing. On a model whose coefficients were rounded: max_energy=-1: the model's coefficients were rounded to the chain's milli grid, and the chain applies the ceiling to the rounded model, so answers near it can land on either side |
min_solutions | Not an int (TypeError); outside 1 to MaxSolutions (20 on aglais): min_solutions=21 must be at least 1 and at most MaxSolutions (20) | Exactly 1: min_solutions=1: the chain stores only the first vector of each answer, but ranks the answer by its best vector |
min_diversity | Not a number (TypeError); outside 0 to 0.5; not a whole number of thousandths; above what the placed spins allow: min_diversity=0.4 is above 1/3, the most diversity 3 placed spins allow, since a vector and its full flip count as one; positive with min_solutions=1: min_diversity=0.3 needs min_solutions of at least 2 or unset; one solution has no diversity (the chain's InvalidDiversityConfig) |
What the chain does with each floor:
- The ceiling drops each submitted vector above it. A submission is
refused (
InsufficientEnergy) only when none of its vectors is left. - With
min_solutions=k, the chain stores exactly thekmost diverse vectors of each submission. That subset can leave out a solver’s best vector, which is one reasonbest()can fall back (see Reading the answer). - Diversity is the average pairwise Hamming distance over the placed
spins, in thousandths, with a vector and its full flip counted as the
same vector. That is why
nspins cap it atfloor(n/2)/n: a 3-spin model caps at 0.333.
Floors narrow the answers a solver can give, and a small model may have
too few distinct vectors to meet min_solutions or min_diversity at
all, so an order with floors can go unanswered. The no-answer error then
names them: best() in model units, solve() in chain units. The order
box shows floors in model units; the receipt shows them in chain units;
both show none when no floor is set.
Receipts
A submitted order has a receipt, a JobOrderReceipt. It holds only the
order id and the network’s genesis hash, needs no model, and reads the
chain on every call. order.receipt() returns it; on a draft it raises
QuipSubmissionError: this order is a draft; submit() it first. Any
session rebuilds a receipt from the order id:
solver = SolverQuip.for_network("aglais")
print(solver.list_orders())
receipt = solver.get_receipt(20)
print(receipt.status())
[24, 23, 21, 20]
{'state': 'finalized', 'order_id': 20, 'chain_status': 'Expired', 'created_at': 1898, 'first_solution_at': 1904, 'effective_expiry': 1914, 'current_block': 2219, 'solution_count': 2}
get_receipt() raises QuipConnectionError for an id that names no
order
(order 1000000000 not found on-chain (QuantumComputeMempool.JobOrders)).
list_orders(state=None, account=None) returns the ids of an account’s
orders that are not yet closed, newest first: the chain drops an order
from the account’s list once a claim or a reclaim closes it. state is
"submitted", "finalized" or None, and any other value raises
ValueError; account is SS58, 0x hex or bytes, and defaults to the
signer. It returns at most the newest 32 ids, after the state filter,
and warns when there are more:
showing 32 of N orders on this account. This 32-order page limit was imposed for the old protocol cap of 32 orders per account; please flag it to the xquad team so it can be lifted.
The receipt and the in-session JobOrder share these methods:
| Method | Returns |
|---|---|
status() | state (submitted or finalized, which sticks once reached), order_id, chain_status (the chain’s OrderStatus), created_at, first_solution_at, effective_expiry, current_block and solution_count |
wait() | The receipt (or order) once it is final, polling every poll_interval; raises QuipTimeoutError after the client’s timeout |
solvers() | One entry per solver that answered: solver, ranked_energy_milli, energy_milli, submitted_at, num_solutions. Ranked solvers come first, then the rest by stored energy and then by submission time. energy_milli is the solver’s stored best, which a later resubmission can overwrite |
raw_solutions(solver=None) | {solver: {"energy_milli": ..., "spins": ...}} over the placed chain nodes, not decoded onto the model; KeyError for a solver that did not answer |
settlement() | {"claimed": bool, "winners": [...]}; see Settlement |
reclaim() | The planck refunded; see below |
chain_status and state answer different questions. state is
finalized once the chain head passes the order’s effective expiry,
while chain_status is the chain’s own record: Opened until the
effective expiry, Expired from then, and Closed once a claim or a
reclaim closes the order. The chain expires a bounded number of orders
per block, so when many expire at once an order can read Opened for a
few blocks past its expiry. The deprecated SolverQuip.status() returns
the old keys, status and is_final, in place of state and
chain_status.
wait() raises QuipTimeoutError with the receipt attached, so the
caller can wait again instead of rebuilding it:
from xquad.sa.quip import QuipTimeoutError
try:
order.wait()
except QuipTimeoutError as exc:
receipt = exc.receipt
receipt.wait()
QuipTimeoutError drops the receipt when it is pickled, since the
receipt holds a live connection; another process rebuilds it with
get_receipt(exc.order_id).
The receipt box
print(receipt) renders a box with five sections: the network, proposer
and submission block and time; the problem; the terms, with floors in
chain units; the outcome (status, answer count, leader and its energy);
and, under a double rule, the money. The fee is what the chain charged
the proposing extrinsic, tip included, read from the block that placed
the order; when it cannot be read the row says unknown. The Reward
row is reserved, leader so far, reserved, no answers yet,
reclaimed, or unanswered: reclaim with get_receipt(N).reclaim(); an
answered, final order shows one Winner row per winner,
awaiting claim or claimed:
╭─ Job order 20 receipt ───────────────────────────────────────────────╮
│ Network aglais │
│ Proposer 5DL9B7rhMw1cg7kXzqmdj5QZ1V67vBvfYPq2bTEgZfSXiBSJ │
│ Submitted block 1898, 2026-10-15 04:22:24 UTC │
├──────────────────────────────────────────────────────────────────────┤
│ Problem 16 spins, 96 couplings │
│ Spec 0x8f46f3a31321d1d093314fc769c42cbe7a83d71a0b69e6571a0f68 │
│ e2a04067f0 │
├──────────────────────────────────────────────────────────────────────┤
│ Payout single best │
│ Access open │
│ Floors none │
│ Deadline 100 blocks │
│ Block wait 10 blocks │
│ Reward 1.000000000000 AGLS │
├──────────────────────────────────────────────────────────────────────┤
│ Status final │
│ Answers 2 │
│ Leader 5GqG8c8zWHX33ruGJX4FDokxCTFrASVi268einKJY1XnHNYn │
│ Energy -1790 │
╞══════════════════════════════════════════════════════════════════════╡
│ Fee 0.002011933394 AGLS │
│ Winner 1.000000000000 AGLS, awaiting claim │
╰──────────────────────────────────────────────────────────────────────╯
Reclaiming
reclaim() returns an unanswered order’s reserved reward and returns
the planck refunded. It reads the head block, then refuses before
signing, with QuipReclaimRefusedError, in this order:
order N was proposed by <account>, not by this accountorder N is already closed (reclaimed or settled)order N is open until block B (about M min); reclaim after it closesorder N was answered by K solvers; its reward awaits a claim and cannot be reclaimed
If the chain refuses the reclaim, it raises QuipReclaimRefusedError
naming the block and the chain’s error, and the fee is charged. If the
outcome is unknown, it raises
QuipSubmissionError("the reclaim of order N (<extrinsic>) was sent but its outcome is unknown");
a retry is safe, since a reclaim that landed makes the next one refuse
as already closed. A reclaim costs its own transaction fee, about 0.0019
AGLS on aglais.
Only solve(), and the deprecated query(), reclaim on their own: when
an order finalizes with no answers, they reclaim it and raise
QuipJobFailedError, saying whether the reclaim succeeded:
QuipJobFailedError: order 25 finalized with no solutions; only allowlisted solvers could answer (accounts 5DL9..iBSJ) and none did; the reserved reward was reclaimed
JobOrder.wait(), JobOrder.best() and a receipt’s wait() never
reclaim.
Reading the answer
An in-session JobOrder holds the model it was built from, so it
decodes answers onto it:
solution = order.best()
print(solution.energy)
print(solution.solver, solution.leader, solution.energy_milli)
-696
5GqG8c8zWHX33ruGJX4FDokxCTFrASVi268einKJY1XnHNYn True -1790000
best() returns a Solution (importable from xquad.sa.quip, or
xqsa.quip, only) with five fields:
| Field | Meaning |
|---|---|
sample | The vector decoded onto the model, as an XQMX sample |
energy | Its energy recomputed on your model; authoritative |
energy_milli | Its energy on the chain’s milli scale, over the coefficients the order carried |
solver | The account that answered with it |
leader | Whether the chain ranks that account first |
solutions(solver=None) returns every stored answer as a Solution,
best first by recomputed energy with the leader’s vectors first on a
tie. With solver, it returns that solver’s vectors alone, and raises
KeyError for a solver that did not answer. An unanswered order returns
an empty list.
best() returns the leader’s best vector, the answer the chain pays
for, not necessarily the best vector any solver stored. It falls back to
the best stored vector overall, the leader first on a tie, in two cases:
- The ranking cannot be read, or names a solver with no stored answer.
metadata["leader_read"]is thenFalseon asolve()result. - None of the leader’s stored vectors recomputes to the energy the chain
ranked it at.
energy_matches_chainis thenFalse, and the client logs both energies atWARNING. A worse resubmission can overwrite a solver’s stored answer, andmin_solutionscan store a subset without the best vector, so this is not necessarily an encoding fault.
Under a fallback, leader stays True only when the leader’s own
vector is also the best overall. best() reads the chain each time, so
on an open order a later call can return a different answer. It never
reclaims: with no answers, it raises QuipJobFailedError and tells you
to call order.reclaim() once the order is final.
Decoding needs the in-session JobOrder, because a receipt does not
hold the model. From another session, get_receipt(order_id) gives you
the order’s status, solvers, raw vectors, settlement and reclaim, but
not a decoded sample; the deprecated query(order_id, model, ...) is
the only way to decode one there (see Deprecated in
v0.4.3).