zkTLS Integration
End-to-end architecture, privacy model, sybil resistance, and CCA behavior for Umia's zkTLS integration
This page is the implementation-level companion to Umia Extension and zkTLS. It explains how Umia integrates Reclaim proofs into Tailored Auctions, how the privacy model works in practice, how sybil resistance is enforced, and what this changes for the Continuous Clearing Auction (CCA).
Why We Use Reclaim
Tailored Auctions need a way to make statements like "this round is for GitHub contributors" or "this bucket is for a specific community" without turning the auction into a centralized KYC flow.
Reclaim gives Umia a way to:
- gate early auction steps by offchain credentials;
- bind a proof to the bidder's wallet so it cannot be replayed by someone else;
- preserve public onchain execution of the auction itself;
- add identity-provider-scoped sybil resistance for gated rounds;
- keep the CCA's price discovery and settlement logic unchanged.
What Umia Actually Integrates
At a high level, Umia combines six layers:
| Layer | Role |
|---|---|
| Umia provider registry | Published provider recipes, each with immutable versions, that say what to prove about an offchain account |
| Umia Extension + attestor | The extension runs the provider flow in the user's browser; Umia's attestor witnesses the TLS session and signs the claim |
| Umia app frontend | Requests proofs, stores them locally, and attaches them to bids as hookData |
| Umia API | Signs proof-request sessions, serves the extension's session and provider endpoints, verifies proofs off-chain, stores registration proofs, computes identity hashes, and serves eligibility |
UmiaValidationHook | Verifies wallet binding, provider compatibility, monotonic eligibility, and onchain identity ownership |
| CCA | Calls the hook on every bid; accepts or rejects the bid based on verification state |
The important design choice is that Reclaim is the proof protocol, while Umia is the auction-specific policy layer. Proofs use Reclaim's claim format and its beacon contract, but Umia hosts the stack: the attestor, the provider registry, the session backend and the extension. Umia decides which proofs are accepted for which auction step and how that affects bid eligibility.
Architecture Diagram
Sequence Diagram
Current default app flow
For ordinary early-bid steps, verify off-chain, store the proof in the backend, then send that proof inline in hookData when bidding. Once the wallet is registered onchain, later bids on ordinary proof-gated steps send empty hookData. Steps configured with verifyEveryBid use a server permit while the wallet's allowance covers the bid, otherwise a fresh, amount-bound proof generated for that bid, and ignore standing registration.
How We Integrate zkTLS Into an Auction
1. We deploy and pair the hook
Each gated auction gets a UmiaValidationHook. The hook is paired to a single CCA via setCCA(address).
That pairing matters because the hook caches the CCA's step ranges and resolves the current step from block number during validate().
2. We configure allowed providers per step
Auction steps are configured with providerHash values, plus human-readable providerId labels for UI/indexing. Each accepted hash is also assigned a permanent identity namespace with setIdentityProvider before proofs for it can pass.
In practice, the CLI computes or accepts provider hashes and then calls:
enableStepBatch(stepIndices, providerHashesPerStep, providerIdsPerStep)during deployment;addStepProviders(...)orsetStepProviders(...)for later updates.
This is how a founder says, for example, "step 0 accepts GitHub provider X" while later steps may accept a broader or different set.
3. We expose readable requirements to the app
The API/indexer layer stores a human-readable description per provider and per step. That is what powers the lock-requirements UI in the app.
4. We mint proof requests server-side
The Umia app never sees the app signing key.
Instead, the API signs a session request with Umia's own app key, pins the exact published provider version, and points the request at Umia's backend. The wallet must be one of the caller's linked wallets. The API records the session for that user and returns a serialized config that carries the session, a short-lived attestor credential, and the chain and beacon epoch the proof is for. During that step, Umia binds the bidder wallet into the proof context:
ReclaimProofRequest.init(appId, appSecret, providerId, { providerVersion, envUrl: umiaBackend })
proofRequest.setJsonContext({ contextAddress: wallet, contextMessage, identityProvider })contextMessage names the auction: register:<chain>:<auction>:<nonce> for a registration proof, or bid:<chain>:<auction>:<step>:<amount>:<nonce> for a bid proof. The API rejects any other form for an auction.
That wallet binding is critical. It means the proof is not just "someone has this credential". It is "this credential was issued for this wallet context".
5. We verify off-chain before the user bids
The extension submits the proof to the user's session, and the API accepts it only if the attestor's signature, chain, beacon epoch and pinned provider match the session. When the app then stores a registration proof, the API re-checks it and applies Umia-specific policy checks:
- the auction must actually use a zkTLS hook;
- the requested step must be enabled and must not verify every bid;
- the proof must be a registration proof (bid proofs are never stored);
- the proof must come from the caller's accepted session for this auction and hook;
- the proof's
providerHashand pinned provider must match one of the step's accepted providers; - the proof's
contextAddressis the wallet bound at request time; - the proof is normalized to the earliest step that accepts that provider and whose registration carries to the requested step.
That last point is important for monotonic access. If a proof qualifies a user for an earlier locked step, Umia records the earliest valid step so later bids can reuse that stronger eligibility.
6. We store the proof and reuse it at bid time
After off-chain verification, the proof is stored in the API database and cached locally in the browser. On an ordinary proof-gated step, the frontend sends, in order:
- empty
hookDataif the wallet is already registered onchain; - an in-memory freshly verified proof;
- a locally cached stored proof;
- a backend-stored proof fetched for the wallet;
- a server permit, only when the wallet holds no proof and the step accepts permits.
A permit-only step always takes a server permit. A step that verifies every bid takes a permit while the wallet's allowance covers the bid, and otherwise a fresh bid proof.
What Goes Onchain
The bid path still ends at the CCA's normal submitBid(...) call, but now hookData may carry verification material.
For Reclaim proofs, Umia encodes:
hookData = abi.encodePacked(uint256 proofStep) ++ abi.encode(Reclaim.Proof)The validation hook then:
- resolves the current auction step;
- checks whether that step is gated;
- on an ordinary proof-gated step, checks whether the wallet is already registered from this or an earlier step;
- if not, decodes
hookDataand verifies the proof forproofStep; - on an ordinary step, registers the wallet from
proofStepfor monotonic access; on a step that verifies every bid, consumes the bid proof instead.
This means verification is part of bid admission, not part of price formation.
How This Affects CCA
Reclaim changes who can submit a valid bid in a given step, but it does not change the core economic mechanics of the auction.
| CCA surface | Effect of Reclaim integration |
|---|---|
| Bid entry | A bid may require valid hookData before it is accepted |
| Clearing price | Unchanged |
| Fill logic | Unchanged |
| Settlement and liquidity seeding | Unchanged |
| Auction step timing | Same CCA steps, but some steps are gated |
| User state | Once registered from step N, the user needs no credential at N and later ordinary proof-gated steps |
In other words, Reclaim adds an admission-control layer around the auction. It does not fork or replace the CCA itself.
The Privacy Model in Practice
What is private
Umia does not require public KYC and does not need a real-world legal identity to gate an auction. The bidder proves eligibility through a signed Reclaim proof that is bound to their wallet.
What is intentionally bound
The wallet address is intentionally inserted into the proof context. That is how the system prevents replay across wallets.
What is not guaranteed by Umia alone
Privacy depends on what the provider recipe includes in the signed context and extractedParameters fields.
If the provider emits a privacy-preserving pseudonymous identifier, Umia can enforce one-wallet-per-identity without learning a raw username. If the provider emits a raw handle, then that handle may be present in the proof payload and privacy is correspondingly weaker.
Important privacy nuance
Umia's integration is privacy-preserving by architecture, but not magically data-free. The actual privacy level depends on provider design. The safest provider design is to expose only the minimum fields needed for wallet binding, provider selection, and sybil resistance.
The Privacy-Preserving OPRF Piece
The sybil-resistance design is built around a stable identity shared across approved recipes for one account provider:
identityKey = keccak256(abi.encode(identityProvider, canonicalExtractedParameters))Two details matter here:
identityProvideris the permanent namespace assigned to the approved claim hash. Recipes for the same account provider share it;canonicalExtractedParametersis taken verbatim from the signed Reclaim context both off-chain and onchain, so both layers compute the same key byte-for-byte.
Why this is useful:
- if the provider recipe captures an OPRF-derived pseudonymous identifier, the same real-world account maps to the same stable identity across recipes without exposing the raw account identifier;
- Umia only needs the stable pseudonym to enforce one-wallet-per-identity;
- the contract and API never need a traditional user database keyed by a Web2 username.
The OPRF output is computed during proof generation by the attestor Umia operates, and comes back signed inside the proof context. Umia derives identityKey from that signed output and never needs the raw account value.
Sybil Resistance
Umia enforces sybil resistance in two places.
Off-chain gate
When the API stores a proof, it computes identityKey and tries to claim that identity for the current hook.
It uses:
- a transaction-level advisory lock on
(hookId, identityKey); - a unique database index on
(hook_id, identity_hash); - same-wallet re-verification logic that replaces older proof rows.
If a different wallet already claimed that identity, the API rejects the proof with a conflict instead of letting the user proceed to bid.
Onchain gate
The validation hook independently enforces the same identity claim onchain:
_identityToUser[identityKey]stores the wallet that owns that identity across approved recipes;- first claim wins;
- re-verification by the same wallet is idempotent;
- a different wallet reverts with
IdentityAlreadyClaimed(...).
This means the backend improves UX and reduces wasted bid attempts, while the contract remains the final enforcement layer.
Missing or empty extractedParameters fail verification on both paths. Recipes grouped under one namespace must emit the same stable account value and canonical JSON shape.
Submission Modes
Umia supports two verification modes in the same hook:
| Mode | How it works | Typical use |
|---|---|---|
| Inline Reclaim proof | Bid carries proofStep + Reclaim.Proof in hookData | Current default app UX |
| Server permit | Bid carries an EIP-712 permit in hookData (inline-only; no pre-submission path) | Admin-curated or fallback allowlists |
The same auction can mix Reclaim steps and permit-enabled steps.
Why the Monotonic Model Matters
Registration is monotonic: if a wallet registers at step 0, it is also valid for later ordinary proof-gated steps 1, 2, 3, and so on. Permit-only steps and steps that verify every bid still ask for a credential on each bid.
That matters because Tailored Auctions often want a funnel like:
- early blocks reserved for a narrow cohort;
- later blocks widened to a broader cohort;
- final blocks fully open.
Without monotonic registration, users would need to re-prove eligibility at every step transition. With monotonic registration, one strong proof unlocks the rest of the auction path that should logically include that user.
Tradeoffs and Design Intent
This integration intentionally chooses:
- onchain enforcement over trust in the frontend;
- wallet-bound proofs over reusable anonymous bearer credentials;
- identity-provider-scoped sybil resistance over fully open one-wallet-many-wallet behavior;
- optional backend prechecks for UX, while keeping the contract as the source of truth.
The tradeoff is that privacy quality depends on provider design, and the first gated bid carries more verification data than a fully open bid.
Summary
Reclaim gives Umia a way to add offchain audience targeting to an otherwise fully onchain auction.
- Reclaim proves eligibility.
- Umia decides which providers are allowed for which auction steps.
- The API binds proofs to wallets, stores proofs, and blocks obvious sybil reuse early.
- The validation hook enforces the same rules onchain during
validate(). - The CCA keeps its native auction mechanics; only bid admission changes.
For the user-facing overview, see Umia Extension and zkTLS. For the contract-level interface, see Validation Hook.