Confidential P2P Price Negotiation on Fhenix
Two parties negotiate a price without ever revealing their numbers — unless they agree.
- What is BlindDeal?
- Why BlindDeal?
- How It Works
- Privacy Model
- FHE Operations Deep Dive
- Escrow Settlement
- Architecture
- Live Demo
- Project Structure
- Getting Started
- Usage — Hardhat Tasks
- Tests
- Roadmap
- Tech Stack
BlindDeal is a sealed-bid negotiation protocol where:
- A buyer submits their maximum acceptable price (encrypted with FHE)
- A seller submits their minimum acceptable price (encrypted with FHE)
- The smart contract computes — entirely on encrypted data — whether the prices overlap
- If they match: the deal closes at the midpoint price, revealed only to both parties
- If they don't match: neither price is ever revealed — zero information leakage
- On match, BlindDealEscrow settles the deal trustlessly with native ETH — buyer funds, approves release, dispute + arbiter resolution
This is fundamentally impossible on transparent blockchains. On Ethereum, submitting a price means the world sees it. On Fhenix with FHE, the contract computes on ciphertext without ever seeing the plaintext.
Traditional negotiation has an information asymmetry problem: whoever reveals their number first loses leverage. BlindDeal eliminates this by ensuring simultaneous, encrypted submission with conditional disclosure — prices are only revealed when there's a deal, and even then, only the fair midpoint.
| Scenario | What happens on transparent chains |
|---|---|
| Salary negotiation | Employer sees your minimum, offers exactly that |
| OTC trade | Counterparty front-runs your limit price |
| Service pricing | Client sees your floor rate, negotiates you down to it |
| M&A / IP licensing | Seller sees buyer's ceiling, extracts maximum |
In every case, revealing your price destroys your negotiating position.
Fhenix's Fully Homomorphic Encryption allows the smart contract to:
- Compare encrypted values (
buyerMax >= sellerMin?) without decrypting them - Compute on encrypted values (
(buyerMax + sellerMin) / 2) without seeing either number - Conditionally reveal results only when both parties benefit
This is not possible with commit-reveal schemes (they require eventual reveal), zero-knowledge proofs (complex setup, limited computation), or trusted third parties (counterparty risk).
| Criterion | BlindDeal |
|---|---|
| Privacy Architecture | Cannot exist without FHE — prices encrypted end-to-end, conditional disclosure, zero-leakage on no-match |
| Innovation & Originality | First sealed-bid negotiation protocol on FHE — uses 6 FHE operations for encrypted price discovery |
| User Experience | Two-party side-by-side flow, real-time status updates, one-click escrow settlement |
| Technical Execution | Solidity v6 contracts (BlindDeal + BlindDealEscrow) + React frontend + CoFHE SDK — full stack deployed on Arbitrum Sepolia |
| Market Potential | OTC trading, salary negotiation, service pricing, M&A — any bilateral price discovery with information asymmetry |
┌─────────────┐
│ createDeal │ Buyer initiates, names the seller
└──────┬──────┘
│
▼
┌──────────────────────────────────────────┐
│ DealState: Open │
│ │
│ submitBuyerPrice(encrypted max) │
│ submitSellerPrice(encrypted min) │
│ (either order, both required) │
│ │
│ cancelDeal() — either party can exit │
└──────────────┬───────────────────────────┘
│ Both prices submitted
▼
┌────────────────────────────────────────────┐
│ _resolve() — FHE Engine │
│ │
│ 1. FHE.gte(buyerMax, sellerMin) │
│ → encrypted boolean: match? │
│ │
│ 2. FHE.add(buyerMax, sellerMin) │
│ FHE.div(sum, 2) │
│ → encrypted midpoint price │
│ │
│ 3. FHE.select(match, midpoint, 0) │
│ → encrypted deal price │
│ │
│ 4. ITaskManager.createDecryptTask(isMatch)│
│ FHE.allowGlobal(isMatch) │
│ → enables client-side decryptForView │
└──────────────┬─────────────────────────────┘
│ Client decrypts via CoFHE SDK
▼
┌──────────────────────────────────────────┐
│ clientFinalizeDeal() │
│ │
│ ✅ Match → DealState: Matched │
│ Both parties unseal the midpoint │
│ → Escrow settlement begins │
│ │
│ ❌ No Match → DealState: NoMatch │
│ Neither price is ever revealed │
│ Privacy fully preserved │
└──────────────────────────────────────────┘
│ On match
▼
┌──────────────────────────────────────────┐
│ BlindDealEscrow Settlement │
│ │
│ 1. Buyer creates escrow │
│ 2. Buyer funds with native ETH │
│ 3. Seller requests release │
│ 4. Buyer approves → seller redeems │
│ 5. Dispute & arbiter resolution │
└──────────────────────────────────────────┘
Buyer max = 1000, Seller min = 800
| Step | Action | On-Chain State | Who Sees What |
|---|---|---|---|
| 1 | Buyer calls createDeal(seller, "Logo design") |
Deal created, state = Open | Both see deal exists |
| 2 | Buyer encrypts 1000, calls submitBuyerPrice(enc(1000)) |
buyerMax = ciphertext |
Nobody sees 1000 |
| 3 | Seller encrypts 800, calls submitSellerPrice(enc(800)) |
sellerMin = ciphertext |
Nobody sees 800 |
| 4 | Contract auto-resolves | gte(enc(1000), enc(800)) → enc(true) |
Contract can't see the result |
| 5 | Midpoint computed | (enc(1000) + enc(800)) / 2 → enc(900) |
Still all encrypted |
| 6 | Frontend decrypts isMatch via CoFHE SDK |
Boolean available | Public: "matched" |
| 7 | clientFinalizeDeal(true) |
State = Matched | Both unseal 900 |
| 8 | Escrow created, linked, funded | 900 USDC in conditional escrow | Settlement in progress |
| 9 | Seller redeems escrow | Funds released | Settlement complete |
If buyer max = 500, seller min = 800: gte(enc(500), enc(800)) → enc(false). State = NoMatch. The values 500 and 800 are never revealed to anyone, ever.
| Data | Why |
|---|---|
| Buyer & seller addresses | Role enforcement + ACL |
| Deal description | User-provided context |
| Deal state (Open / Matched / NoMatch / Cancelled / Expired) | Protocol state machine |
| Whether each party submitted | Coordination signal |
| Match result (boolean only) | Minimal disclosure |
| Data | Type | Revealed when | To whom |
|---|---|---|---|
| Buyer's max price | euint64 |
Only on match | Both parties (as midpoint) |
| Seller's min price | euint64 |
Only on match | Both parties (as midpoint) |
| Deal price (midpoint) | euint64 |
Only on match | Both parties |
- Failed negotiations leak nothing — only a boolean "no match" is disclosed
- Individual prices never revealed — even on match, only the midpoint is disclosed
- No front-running — prices encrypted client-side before submission
- ACL-enforced access — only authorized addresses can decrypt specific ciphertexts
The contract uses 6 distinct FHE operations from @fhenixprotocol/cofhe-contracts/FHE.sol:
d.buyerMax = FHE.asEuint64(encryptedMax);Converts client-side encrypted input (ZK-proven) into an on-chain euint64 ciphertext handle.
ebool match_ = FHE.gte(d.buyerMax, d.sellerMin);Computes buyerMax >= sellerMin on encrypted values. Returns an encrypted boolean — the contract cannot read the result.
euint64 sum = FHE.add(d.buyerMax, d.sellerMin);euint64 midpoint = FHE.div(sum, two);Produces the fair deal price: (buyerMax + sellerMin) / 2.
d.dealPrice = FHE.select(match_, midpoint, zero);FHE ternary: if match, return midpoint; else zero. You cannot branch on encrypted data — select is the pattern.
// Request Threshold Network decryption (replaces FHE.decrypt in v0.5.x)
ITaskManager(TASK_MANAGER_ADDRESS).createDecryptTask(uint256(ebool.unwrap(d.isMatch)), address(this));
FHE.allowGlobal(d.isMatch); // Enable client-side access// Client-side via CoFHE SDK v0.5.x
const matched = await cofheClient.decryptForView(matchHandle, FheTypes.Bool).execute();After a deal matches, BlindDeal uses BlindDealEscrow — a purpose-built conditional escrow with buyer approval and dispute resolution:
- Create Escrow — Buyer calls
createEscrow(dealId)directly (only when deal is Matched) - Fund Escrow — Buyer sends native ETH via
fundEscrow(escrowId) - Request Release — Seller calls
requestRelease(escrowId) - Approve / Reject — Buyer calls
approveRelease()orrejectRelease()(rejection auto-opens dispute) - Redeem — Seller calls
redeem(escrowId)after approval
- Either party can
openDispute(escrowId, reason)from Funded or ReleaseRequested state - Designated
arbiterresolves viaresolveDispute(escrowId, releaseToSeller, resolution) - Timeout protections:
- Seller can
forceRelease()if buyer ignores request for 7 days (APPROVAL_WINDOW) - Either party can
forceResolveDispute()(auto-refund to buyer) if arbiter is unresponsive for 14 days (DISPUTE_RESOLUTION_WINDOW)
- Seller can
- Refund on failed deal:
refundOnFailedDeal(escrowId)for NoMatch / Cancelled / Expired
BlindDeal also integrates with ReineiraOS (@reineira-os/sdk v0.3.1) for deals that require true confidentiality — amounts and counterparties are FHE-encrypted on-chain.
When to use Reineira:
- High-value deals where amount confidentiality matters
- Sellers who prefer ConfUSDC over native ETH
- Buyers who want optional insurance coverage from Reineira pools
Flow:
- Create — API server uses
EscrowBuilderwithBlindDealResolveras the condition gate (encodeResolverData(['uint256'], [dealId])) - Link —
linkEscrow(escrowId, dealId)wires the escrow to our resolver - Fund — Buyer funds with ConfUSDC via FHE-encrypted payment
- Auto-release —
BlindDealResolver.isConditionMet()returns true when deal = Matched; seller redeems - Insurance (optional) —
purchaseCoverage()via CoverageManager for dispute protection
Dual settlement UI: Buyers see two cards on match:
- Simple →
BlindDealEscrow(native ETH, transparent) - Confidential → Reineira (ConfUSDC, FHE-encrypted, insurance optional)
API Routes:
| Route | Purpose |
|---|---|
POST /api/escrow/create |
Create Reineira escrow with BlindDealResolver condition + auto-link |
POST /api/escrow/fund |
Fund with FHE-encrypted ConfUSDC |
POST /api/escrow/redeem |
Seller redeems when condition is met |
POST /api/escrow/insurance |
Purchase coverage (returns 503 on testnet until CoverageManager is deployed) |
| Contract | Address | Purpose |
|---|---|---|
| BlindDeal | 0x9e3A...6C8 |
Core FHE negotiation (v6: consensus finalization) |
| BlindDealResolver | 0x1fc2...f40 |
Condition resolver |
| BlindDealEscrow | 0x6215...e17 |
Buyer-approved escrow with dispute |
v6 fix: createDecryptTask wrapped in try/catch because the Sepolia TASK_MANAGER doesn't support this function.
┌──────────────────────────────────────────────────────────┐
│ Frontend (React) │
│ │
│ @cofhe/react wagmi v2 Dual Settlement │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ encrypt() │ │ writeContract│ │ Simple │ │
│ │ decrypt() │ │ readContract │ │ Confidential │ │
│ │ unseal() │ │ │ │ │ │
│ └──────┬───────┘ └──────┬───────┘ └───────┬──────┘ │
└─────────┼──────────────────┼───────────────────┼─────────┘
│ │ │
▼ ▼ ▼
┌─────────────────┐ ┌──────────────┐ ┌──────────────────┐
│ CoFHE │ │ BlindDeal │ │ BlindDealEscrow │
│ Coprocessor │ │ .sol │ │ .sol (native) │
│ │ │ │ │ │
│ FHE math on │ │ 6 FHE ops │ │ Native ETH │
│ ciphertext │ │ ACL control │ │ Buyer approval │
└─────────────────┘ └──────┬───────┘ └────────┬─────────┘
│ │
│ ┌───────────────┘
│ │
▼ ▼
┌──────────────────────────────┐
│ Arbitrum Sepolia │
│ │
│ BlindDealResolver v6 │
│ BlindDealEscrow v1 │
│ │
│ ── Confidential path ── │
│ Reineira ConfidentialEscrow │
│ ConfUSDC (FHE-encrypted) │
│ CoverageManager (future) │
└──────────────────────────────┘
| Component | URL |
|---|---|
| Frontend | Vercel deployment (Arbitrum Sepolia + Ethereum Sepolia) |
| BlindDeal v6 (Arb) | 0x9e3A...6C8 |
| Resolver v6 (Arb) | 0x1fc2...f40 |
| Escrow v1 (Arb) | 0x6215...e17 |
| BlindDeal v5 (Eth) | 0x36a1...Bd49 |
The complete escrow lifecycle has been verified on Arbitrum Sepolia:
- Escrow create with resolver condition (escrow #30+)
- ConfUSDC approval + fund via viem (bypasses ethers.js encoding bug)
- Condition-verified redeem with tx hash persisted to localStorage
- Explorer links: fund/redeem txs link to
sepolia.arbiscan.io - Escrow status persists across page reloads (fixes "redeem button still enabled" bug)
blinddeal/
├── contracts/
│ ├── BlindDeal.sol # Core FHE negotiation (v6: DealType, joinDeal)
│ └── BlindDealResolver.sol # Privara escrow condition resolver
├── frontend/
│ ├── api/escrow/
│ │ ├── create.ts # Vercel API: create escrow via Privara SDK (viem)
│ │ ├── fund.ts # Vercel API: fund escrow with ConfUSDC (viem + ABI)
│ │ └── redeem.ts # Vercel API: redeem escrow (viem direct call)
│ ├── src/
│ │ ├── components/
│ │ │ ├── CreateDeal.tsx # Deal creation form (Direct/Open toggle)
│ │ │ ├── Dashboard.tsx # My Deals + Marketplace tabs + activity tags
│ │ │ │ ├── MCPServer.tsx # MCP tools test panel
│ │ │ ├── DealDetail.tsx # Full lifecycle: negotiate → settle → cancel
│ │ │ ├── Header.tsx # Navigation + wallet connection
│ │ │ └── Toast.tsx # Notification system
│ │ ├── config/
│ │ │ ├── cofhe.tsx # CoFHE SDK provider setup
│ │ │ ├── contract.ts # ABIs, addresses, escrow config
│ │ │ └── wagmi.ts # Wagmi + chain configuration
│ │ └── hooks/
│ │ └── useEscrow.ts # Escrow state + tx hash persistence (localStorage)
│ ├── dev-api.mjs # Local dev API server (port 3002, viem)
│ └── vite.config.ts # Custom CoFHE worker plugin
├── telegram-bot/
│ ├── index.ts # Telegram bot: notifications + /list /subscribe + event polling
│ └── package.json
├── mcp-server/
│ └── index.ts # MCP server (HTTP transport, Node.js CJS workaround)
├── start-bot.ts # Multi-service runner: MCP + Telegram as child processes
├── Procfile # Render worker: npx tsx start-bot.ts all
├── render.yaml # Render Background Worker deployment config
├── tasks/ # Hardhat tasks (deploy, create, submit, finalize, verify, agent)
├── test/
│ └── BlindDeal.test.ts # 34 tests (all passing, 7 new Open marketplace tests)
├── deployments/ # Contract addresses per network
├── hardhat.config.ts
└── package.json
- Node.js v20+
- pnpm (
npm install -g pnpm)
git clone <your-repo-url> blinddeal
cd blinddeal
pnpm installcp .env.example .env
# Edit .env: set PRIVATE_KEY and RPC URLspnpm compile
pnpm testpnpm arb-sepolia:deploy-blinddeal
pnpm arb-sepolia:deploy-resolverOne command — all 4 services:
pnpm dev:allLaunches with color-coded logs, port conflict detection, and auto-restart:
[frontend ] http://localhost:3000 (Vite dev server)
[api ] http://localhost:3002 (Escrow API — create/fund/redeem)
[mcp ] http://localhost:3001 (MCP Server for AI agents)
[telegram ] Bot running (Telegram notifications)
Press Ctrl+C to stop all services.
Or run individually:
pnpm dev:fe # Frontend only (:3000)
pnpm dev:api # API server only (:3002)
pnpm mcp # MCP Server only (:3001)
pnpm start:bot telegram # Telegram bot only
pnpm start:bot # MCP + Telegram (production-like)Open http://localhost:3000
Full deployment guide in DEPLOYMENT_GUIDE.md.
| Component | Platform | Service |
|---|---|---|
| Frontend + Escrow API | Vercel | Serverless (auto-scaling) |
| MCP Server + Telegram Bot | Render (Web Service) or Fly.io | Free tier, health check + keepalive |
# Vercel (frontend + API)
cd frontend
vercel --prod
# Render (MCP + Telegram via Web Service)
# Import repo as Web Service → start: npx tsx start-bot.ts allEnvironment variables reference and step-by-step instructions in DEPLOYMENT_GUIDE.md.
# Deploy
npx hardhat deploy-blinddeal --network arb-sepolia
# Create a deal
npx hardhat create-deal --network arb-sepolia \
--seller 0xSellerAddress --description "Logo design"
# Create an Open marketplace deal (anyone can join)
npx hardhat create-deal --network arb-sepolia \
--seller 0x0000000000000000000000000000000000000000 --description "Open deal"
# Submit encrypted prices
npx hardhat submit-price --network arb-sepolia \
--deal 0 --price 1000 --role buyer
npx hardhat submit-price --network arb-sepolia \
--deal 0 --price 800 --role seller
# Finalize
npx hardhat finalize-deal --network arb-sepolia --deal 0
# AI Agent: discover deals, join, submit price (demo command)
npx hardhat agent --price 500 --network arb-sepolia
# AI Agent: create Open deal + full flow demo
npx hardhat agent --mode full --price 500 --network arb-sepoliaThe agent autonomously discovers open marketplace deals, joins as a seller, and submits FHE-encrypted prices — all from the command line.
- Discovers deals — scans the contract for Open marketplace deals without a seller
- Joins as seller — calls
joinDeal()(first-come-first-served) - Encrypts price — uses
@cofhe/hardhat-pluginfor FHE encryption (runs in Node.js) - Submits price — calls
submitSellerPrice()with encrypted input - Finalizes — decrypts match result and calls
finalizeDeal()
# Discover + join + submit (finds an Open deal automatically)
npx hardhat agent --price 500 --network arb-sepolia
# Full demo: create deal + join + submit buyer + submit seller + finalize
npx hardhat agent --mode full --price 500 --network arb-sepolia
# Create an Open deal for others to discover
npx hardhat create-deal --network arb-sepolia \
--seller 0x0000000000000000000000000000000000000000 \
--description "Open negotiation"34 tests across 7 categories using mock FHE (@cofhe/hardhat-plugin):
| Category | Tests | What's Verified |
|---|---|---|
| Deal Creation | 5 | State init, ID increment, per-user tracking, deadline |
| Price Submission | 4 | Encrypted input, role enforcement, double-submit prevention |
| Match Resolution | 3 | FHE comparison, midpoint arithmetic, equal prices |
| No Match | 2 | FHE no-match, price privacy (revert on getDealPrice) |
| Cancellation | 4 | Buyer/seller cancel, outsider rejection, post-cancel rejection |
| Deal Expiry | 4 | Deadline enforcement, early expire rejection |
| Open Marketplace | 7 | joinDeal flow, Direct deal rejection, seller replacement, full Open deal lifecycle |
-
BlindDeal.sol— FHE negotiation with 6 encrypted operations - ACL-based access control, deal deadlines, per-user tracking
- 34 tests covering all paths
- Hardhat tasks + deployed to Arbitrum Sepolia & Ethereum Sepolia
- React frontend with
@cofhe/reacthooks + wagmi wallet connection - Full deal lifecycle UI: create → submit encrypted prices → finalize → cancel
- FHE price unsealing via
cofheClient.decryptForView() -
BlindDealResolver.sol— condition contract for Privara escrow - Privara SDK integration (
@reineira-os/sdk): create → fund → redeem - Server-side API routes for FHE-encrypted escrow operations
- End-to-end escrow lifecycle verified on Arbitrum Sepolia
- FHE Engine Upgrade — Migrated to
@cofhe/sdk/@cofhe/hardhat-pluginv0.5.2 - Smart contract v5 —
createDecryptTaskwrapped in try/catch,Expiredstate,createdAttimestamp - Telegram bot for deal notifications + share links (
/status,/share,/subscribe) - Multi-deal marketplace view — Browse all open deals alongside "My Deals"
- Deal filters — All / Open / Closed tabs on "My Deals"
- Live countdown timer — Shows remaining time on open deals
- Share improvements — Copy link + Telegram share button
- Address validation — CreateDeal form validates seller address format
- Escrow rewrite — All API routes use viem with explicit ABI (fixes ethers.js encoding bug)
- Persisted escrow state — Status + tx hashes saved to localStorage, survives page reload
- Explorer links — Fund/redeem tx hashes link to
sepolia.arbiscan.io - CCTP bridge removed — Was irrelevant (bridges regular USDC, escrow needs ConfUSDC)
- Contract verification tasks —
verify-blinddealandverify-resolverHardhat tasks
- Open marketplace deals —
createDeal(address(0))→DealType.Open, first-come-first-served viajoinDeal() - Open deal UI — Seller card shows "Waiting for seller...", "Join as Seller" button, activity tags
- Marketplace view — Search/filter by state, search by deal ID, "Load More" pagination, "Open for sellers" badge
- Telegram bot —
/list,/status,/create,/submit,/share,/subscribe, event polling every 15s - Telegram deep-links —
?action=create,?deal=X&action=submit,?action=mcpfrom App.tsx - MCP Server — HTTP transport, 6 tools (
get_deal,list_deals,get_user_deals,get_events,subscribe_deal,create_deal), 6 resources (blinddeal://contract/*, etc.) - MCP page — Interactive test buttons, Claude Desktop config guide, tool/resource listings
- Multi-service runner —
start-bot.tsspawns MCP + Telegram as child processes with auto-restart - AI Agent task —
npx hardhat agent --price 500 --network arb-sepoliadiscovers Open deals, joins, submits FHE price - 34 Hardhat tests pass, TypeScript clean, Vite build succeeds
- BlindDealEscrow v1 — Buyer-funded, buyer-approved release, dispute resolution with arbiter, force-release timeout, refund on failed deal
- Escrow integration — DealDetail fully rewritten for new escrow flow (create → fund → request → approve → redeem + dispute)
- Dual settlement choice — Buyers choose between
BlindDealEscrow(simple, native ETH) and ReineiraOS (confidential, ConfUSDC, insurance) - Reineira upgraded integration —
EscrowBuilderwithBlindDealResolvercondition gate, autolinkEscrow,encodeResolverData, insurance API route - On-chain reputation — Client-side score from deal history + opponent deal count badges in marketplace (green ≥5 deals)
- Landing page — Protocol explanation, how-it-works steps, feature grid, CTA to create deal
- Security review — v6 BlindDeal with 2-of-2 client consensus, description limits, custom errors
- Redeploy v6 contracts to Arbitrum Sepolia with full Arbiscan verification
| Layer | Technology |
|---|---|
| FHE Contracts | Solidity 0.8.25, @fhenixprotocol/cofhe-contracts v0.1.3 |
| FHE Coprocessor | Fhenix CoFHE (TaskManager, FHEOS, Threshold Network) |
| Client SDK | @cofhe/sdk v0.5.2 + @cofhe/react v0.5.2 (encrypt, decrypt, permits) |
| Escrow (Simple) | BlindDealEscrow.sol v1 — native ETH, buyer approval, dispute resolution |
| Escrow (Confidential) | @reineira-os/sdk v0.3.1 — FHE-encrypted ConfUSDC, BlindDealResolver condition gate, optional insurance |
| Frontend | Vite 8, React 18, wagmi v2, Tailwind CSS |
| Dev Framework | Hardhat 2.22, @cofhe/hardhat-plugin v0.5.2 (mock FHE testing) |
| Blockchain | Arbitrum Sepolia (escrow + FHE), Ethereum Sepolia (FHE only) |
| Deployment | Vercel (frontend + API), Hardhat (contracts) |
MIT
Contributions are welcome! Please feel free to submit a Pull Request.