Private Checkout v1
Prove the hackathon private-checkout moment: encrypted amount validation plus a private fulfillment trigger.
Privacy target
Private Checkout v1 is a Private Checkout Proof MVP. It proves that a checkout contract can validate encrypted amount equality and emit only a fulfillment-safe paid/rejected result.
The implemented local-dev token is ConfidentialUSDMock: an official-style mintable confidential cUSDT mock keyed by wallet address. It is not a MetaMask ERC20 token and should not be tested through a public ERC20 transfer.
The privacy claim is scoped to checkout business data in PrivateCheckoutSettlement storage and events. Public observers should not see merchant wallet, payout wallet, project id, order id, or amount there. In the direct-wallet MVP, the paying wallet is still visible as the EVM transaction sender, and withdraw recipient privacy is not claimed.
| Data | Chain treatment | Who knows in v1 |
|---|---|---|
| Buyer wallet | Direct-wallet MVP submits as msg.sender; not stored as an order field | Public chain observers can see tx sender; ZamaPay can map the checkout |
| Merchant wallet | Checkout uses settlementBucketCommitment and bucketOwnerCommitment | ZamaPay, merchant backend, and withdraw observers |
| Payout wallet | Not stored or emitted during checkout/payment; v1 withdraw recipient is calldata | Merchant backend and withdraw observers |
| Project / order id | Hashed into orderCommitment | ZamaPay and merchant backend |
| Gross and paid amount | FHE encrypted handles | ZamaPay in v1, hidden from public chain observers |
| Paid/rejected | Public boolean after decrypting accepted | Everyone |
MVP boundary
The design has two layers. The first is required for the hackathon proof. The second must be explicitly named so the demo does not confuse encrypted equality with real asset settlement.
| Layer | What it proves | v1 position |
|---|---|---|
| Private Checkout Proof | expectedAmount and paidAmount stay encrypted; the contract checks FHE.eq and reveals only accepted. | Required in the hackathon demo. |
| Payment Rail | The buyer actually paid, or the demo honestly simulated settlement before finalization. | Implemented as mock confidential cUSDT balance on local-dev. |
| Merchant Settlement / Withdraw | Merchant net, platform fee, and payout close without per-order public disclosure. | Implemented for local-dev; payout-recipient privacy is not claimed in v1. |
Field contract
Use this field contract as the implementation boundary. Core checkout values may exist on-chain only as FHE handles. Public checkout fields must be commitments, coarse status, or time bounds. The direct-wallet payer is public as the transaction sender, and v1 withdraw reveals the authorized recipient in calldata.
| Boundary | Field | Type | Meaning | Public rule |
|---|---|---|---|---|
| On-chain encrypted, v1 core | expectedAmount | euint64 | Order amount due. | Stored only as an FHE handle; never emitted as plaintext. |
| merchantNetAmount | euint64 | Merchant net split for this checkout. | Imported with the same input proof; only added to encrypted pending if payment succeeds. | |
| platformFeeAmount | euint64 | Platform fee split for this checkout. | Imported with the same input proof; only added to encrypted pending if payment succeeds. | |
| splitCheck | ebool | Encrypted result of merchantNetAmount + platformFeeAmount == expectedAmount. | Never decrypted per order; gates payment acceptance. | |
| paidAmount | externalEuint64 | Buyer-submitted payment amount. | Submitted with inputProof and imported through FHE.fromExternal. | |
| paymentCheck | ebool | Encrypted result of paidAmount == expectedAmount. | Only this boolean is publicly decrypted as accepted. | |
| On-chain encrypted, v1 settlement | encryptedMerchantPending[settlementBucketCommitment] | euint64 | Merchant aggregate settlement balance. | Accrued only by accepted checkouts; moved by merchant-authorized encrypted withdraw. |
| encryptedPlatformPending | euint64 | Platform aggregate fee balance. | Fee balance stays encrypted until platform settlement is explicitly added. | |
| On-chain public | orderCommitment | bytes32 | hash(orderId, projectId, amount, salt). | Stable order reference only; raw ids and amount stay off-chain. |
| settlementBucketCommitment | bytes32 | hash(merchantId, settlementEpoch, randomSalt), not merchant address. | Rotate by checkout, batch, day, or week; never use a permanent merchant id. | |
| bucketOwnerCommitment | bytes32 | hash(settlementBucketCommitment, bucketOwner). | Submitted during checkout creation instead of the raw merchant wallet. | |
| paymentStatus | enum | created / submitted / accepted / rejected / expired. | Coarse fulfillment state; no counterparty or amount data. | |
| expiresAt | uint64 | Checkout deadline. | Public time bound used to reject stale payment attempts. | |
| paidAt | uint256 | Finalization timestamp. | Time signal only; do not pair it with raw order or wallet fields. | |
| buyer tx sender | address | Wallet that submits submitPrivatePayment in the direct MVP. | Public because of EVM mechanics; do not claim payer-address privacy in this MVP. | |
| Never public in checkout calldata/events | merchant address | address | Merchant wallet or dashboard identity. | Checkout events do not emit it; withdraw authorization may reveal it. |
| Withdraw calldata | payout wallet during withdraw | address | Settlement destination. | Bound by merchant EIP-712 authorization; local-dev does not claim payout-recipient privacy. |
| Never public on-chain | amountDue plaintext | uint64 / token minor units | Plain order amount. | Use encrypted expectedAmount or an order commitment instead. |
| merchantNet plaintext | uint64 / token minor units | Plain merchant net split. | Use encrypted settlement accumulators and merchant-only dashboard projection. | |
| platformFee plaintext | uint64 / token minor units | Plain platform fee split. | Use encrypted settlement accumulators and platform-only projection. | |
| projectId plaintext | string / bytes | ZamaPay project id. | Hash into orderCommitment; never store the raw business id. | |
| orderId plaintext | string / bytes | Merchant order id. | Hash into orderCommitment; never store the raw order id. |
Payment rail
Encrypted equality proves that an encrypted paidAmount equals the encrypted expectedAmount. It does not prove that value moved. The selected rail must be part of the demo contract, backend policy, or test script.
| Rail | What it proves | Use in hackathon |
|---|---|---|
| Direct mock cUSDT confidential balance | Buyer has a demo confidential balance and the buyer-submitted transaction deducts an encrypted amount. | Implemented local-dev path. |
| ERC-7984 / confidential wrapper transfer | A confidential token balance or transfer amount moves through a token contract. | Post-MVP unless address linkage and operator semantics are deliberately handled. |
Payment flow
The normal checkout path decrypts only accepted, an ebool. Per-order gross, merchant net, and platform fee stay encrypted and can be handled by settlement batches later.
Expected and paid amounts are encrypted as external inputs with input proofs, then imported by the contract. Local-dev uses Hardhat/FHEVM mock RPC; Sepolia uses Zama's official test relayer through `@zama-fhe/relayer-sdk` `SepoliaConfig`.
accepted = FHE.eq(paidAmount, expectedAmount)Contract shape
The checkout record stays small. It keeps encrypted gross/net/fee handles for validation, while aggregate pending balances live in bucket mappings outside each checkout.
enum PaymentStatus {
None,
Created,
Submitted,
Accepted,
Rejected,
Expired
}
struct PrivateCheckout {
bytes32 orderCommitment;
bytes32 settlementBucketCommitment;
euint64 expectedAmount;
euint64 merchantNetAmount;
euint64 platformFeeAmount;
ebool splitCheck;
ebool paymentCheck;
PaymentStatus status;
uint64 expiresAt;
uint256 paidAt;
}Safety controls
Payment intent and lifecycle controls are part of the MVP, because they stop the demo from becoming a free-card oracle.
| Control | Rule | Failure blocked |
|---|---|---|
| Payment intent binding | Bind orderCommitment, encrypted amount handle, asset, chainId, settlement contract, nonce, and expiresAt before adding any future sponsored submitter mode. | Replaying one valid encrypted amount against another checkout. |
| Withdraw authorization | Merchant signs settlementBucketCommitment, withdrawalNonce, bucketOwner, recipient, encryptedAmount handle, inputProofHash, deadline, chainId, and settlement contract. | A chain submitter moving an unauthorized bucket or swapping the encrypted withdraw input. |
| Expiry | Reject payment submission after expiresAt. | Late payment after merchant order is stale. |
| Nonce reuse | Reject reused payment nonce or already-submitted intent. | Duplicate payment attempts and replay. |
| Final status lock | Accepted, rejected, and expired checkouts cannot be submitted or finalized again. | Double fulfillment. |
| Rotating bucket | settlementBucketCommitment rotates by checkout, batch, day, or week. | Long-lived merchant activity graph. |
Acceptance criteria
The demo contract should stay separate from the old transparent settlement shape. Transparent settlement exposed merchant, payoutWallet, payer, and amountDue by design.
Private Checkout v1 succeeds when PrivateCheckoutSettlement events expose only commitments, encrypted handles, status, and timestamps, while CardForge still receives a fulfillment-ready webhook.
Create one private checkout on the active contract environment from a CardForge order and store only commitments plus encrypted expectedAmount on-chain.
Submit encrypted payment directly from the buyer wallet. Local-dev uses mock RPC for proofs; Sepolia uses Zama official test relayer proofs.
Verify the ConfidentialUSDMock debit, then use FHE equality to compare encrypted paidAmount with encrypted expectedAmount.
Decrypt only accepted, map orderCommitment back to the demo order, and release the card through the existing webhook path.
Merchant signs an EIP-712 withdraw authorization; the local chain submitter sends the encrypted withdraw transaction and the read model records the chain hash.
Prove replay, expired payment, resubmit-after-final, double-finalize, and unauthorized withdraw paths are rejected.
Ready to wire a merchant project?
Create the project in the console, then keep external checkout creation on the project API-key path.