Build an authorize half-proof and submit it through a relayer.
Placing an order on zkScatter means:
- Picking an active relayer.
- Building a Merkle proof for the deposit you want to spend.
- Generating an
authorizeGroth16 proof binding an EdDSA-signed order to that leaf. - Submitting the proof + signature to the relayer’s HTTP API.
1. Pick a relayer
import { loadRelayersWithApiInfo } from "@zkscatter/sdk/relayer";
const relayers = await loadRelayersWithApiInfo(
network.contracts.relayerRegistry,
readProvider,
);
const candidates = relayers.filter((r) => r.online && r.api);
candidates.sort((a, b) => Number(a.fee) - Number(b.fee));
const target = candidates[0];
if (!target) throw new Error("No relayer online");2. Build the Merkle proof
Maintain an IncrementalMerkleTree fed by CommitmentInserted events:
import {
IncrementalMerkleTree,
COMMIT_TREE_DEPTH,
loadCommitmentInsertedHistory,
} from "@zkscatter/sdk";
const tree = new IncrementalMerkleTree(COMMIT_TREE_DEPTH);
const history = await loadCommitmentInsertedHistory(
readProvider,
network.contracts.commitmentPool,
);
for (const row of history) tree.insert(row.commitment);
const merkleProof = tree.getProof(myStoredNote.leafIndex);For long-lived sessions, persist the tree state and apply incremental
updates from subscribeCommitmentInserted.
3. Generate the authorize proof
import {
generateAuthorizeProof,
randomFieldElement,
deriveEdDSAKey,
} from "@zkscatter/sdk/zk";
const eddsa = await deriveEdDSAKey(signer);
const nonce = randomFieldElement();
// AuthorizeProofInput uses `note.token` for the sell side — there is
// no separate `sellToken` field. `sellAmount` must be ≤ `note.amount`;
// any residual is rolled into a change commitment using `newSalt`.
const expiry = BigInt(Math.floor(Date.now() / 1000) + 600);
const authResult = await generateAuthorizeProof(
{
note: myStoredNote.note,
leafIndex: myStoredNote.leafIndex,
merkleProof,
sellAmount: amountIn,
buyToken: tokenB,
buyAmount: amountOut,
maxFee,
expiry,
nonce,
relayer: target.address,
eddsaPrivateKey: eddsa.privateKey,
// `recipient` is the address the payout will be paid to. The
// `claim` circuit binds the payout to this address — anyone
// can submit a valid claim proof (the call is permissionless),
// but the funds always land at `recipient`.
claims: [
{
secret: randomFieldElement(),
recipient: payoutRecipient, // see comment above
token: tokenB,
amount: amountOut,
releaseTime: 0n,
},
],
newSalt: randomFieldElement(),
},
{ wasm: "/zk/authorize.wasm", zkey: "/zk/authorize_final.zkey" },
);4. Submit to the relayer
import { RelayerClient, type OrderData } from "@zkscatter/sdk/relayer";
const client = new RelayerClient(target.url);
// Wire-format payload — see sdk/relayer for the full type. `claims`
// must be the exact distribution committed in the authorize proof's
// claimsRoot; the relayer rejects any divergence.
const order: OrderData = {
maker: account,
sellToken: tokenA,
buyToken: tokenB,
sellAmount: amountIn.toString(),
buyAmount: amountOut.toString(),
maxFee: Number(maxFee),
expiry: Number(expiry),
nonce: Number(nonce),
claims,
};
// `signature` is a relayer-protocol string committing to `order` —
// see the relayer's `/api/info` for the format it accepts.
const response = await client.submitOrder(order, signature, "cover_taker");
if (response.status === "rejected") {
throw new Error("Relayer rejected order");
}5. Track status
Poll or subscribe via the relayer’s history endpoint:
const orders = await client.getOrders(account);
const me = orders.find((o) => o.nonce === nonce.toString(16));
// me.status: "queued" | "matched" | "settled" | "expired" | "cancelled"When me.status === "settled", the matched payout is now a commitment
in the pool addressed to whatever recipient you set in the claim
entry above. See Claim a payout.
Pre-flight UX
Always run pre-sign preview before triggering the proof so the user
sees what they’re about to authorize:
<PreSignPreview
sellToken={tokenA}
buyToken={tokenB}
sellAmount={amountIn}
buyAmount={amountOut}
expiry={expiry}
relayer={target.url}
fee={target.fee}
/>The apps/pro reference component has full styling — copy or adapt.
Common errors
`Bad merkle proof`
Tree state diverged from on-chain. Reload loadCommitmentInsertedHistory
and re-derive the proof.
`Order expired` from the relayer
Pick an expiry at least 2× your worst-case prove time. On mobile,
aim for 10 minutes minimum.
EdDSA signature rejected
Verify deriveEdDSAKey was called with the same wallet that owns
the note. Account switches mid-flow desync the key.