Tutorial: private Adder on Sepolia
This walkthrough is the primitive-only path: your host-chain contract calls PodLib helpers (the SDK surface for MpcLib-style primitives) and never deploys custom Solidity on COTI. If you are unsure whether that is enough for your product, read Tutorials: building Privacy on Demand (PoD) dApps first.
This guide shows how to build a minimal Privacy on Demand dApp that adds two encrypted integers on COTI and stores the encrypted sum on your EVM contract. It follows the same ideas as the SDK’s MpcAdder.sol example, extended with Sepolia routing presets and request correlation suitable for a real UI.
For background on async flows and fees, see Async private operations, How do PoA fees work?, and the SDK’s Fees, gas, and oracle page.
Writing a PoD example
In this example we will do the following:
Use one built-in executor operation from
PodLib(the sample below usesadd256) so you do not write a custom COTI contract yet.Submit a payable request that forwards
msg.valueand splits outcallbackFeeLocalWeifor the return leg (two-way Inbox message).Implement a success callback that decodes
abi.encode(ctUint256)and stores the ciphertext.Wire
onDefaultMpcError.selectorso failed remote runs surface through the SDK’s default error path (and emitErrorRemoteCallfromPodUser).
After that works, you harden for production: per-user request ownership, explicit pending / completed / failed state, fee estimation via the Inbox, and tests for under-funded sends. The SDK’s Examples with description lists what the shipped MpcAdder omits on purpose.
Prerequisites
Solidity toolchain (Foundry or Hardhat) targeting Ethereum Sepolia (where the SDK’s
PodUserSepoliaInbox is deployed).Node.js 18+ for scripts and
fetchused by encryption helpers.Sepolia ETH for deployment and for
msg.valueon eachaddcall (plus gas).User onboarding so your client can obtain an account AES key for decryption (see the SDK’s TypeScript integration and Onboarding / account AES key docs).
Always confirm Inbox, COTI chain id, and MPC executor against the version of PodUserSepolia.sol in your installed @coti/pod-sdk package; constants can change between releases.
Step 1: Install the SDK
Step 2: Create the PrivateAdder contract
PrivateAdder contractSave as PrivateAdder.sol. The contract:
Inherits
PodLibandPodUserSepolia(Sepolia defaults for Inbox and COTI routing).Calls
add256with the caller’s encrypted inputs and your callback selector.Resolves
requestIdin the callback the same way as the SDK’s Getting started example.
Notes:
onDefaultMpcErroris implemented onPodLibBaseand forwards failures toErrorRemoteCallonPodUser. Your UI can listen for that event to mark a request failed.addCallbackmust stayonlyInboxso random accounts cannot forge results.
Step 3: Compile and deploy on Sepolia
Configure remappings so @coti/pod-sdk resolves (Hardhat paths, Foundry remappings.txt, etc.), then compile and deploy PrivateAdder to Ethereum Sepolia. Record the deployed address for scripts.
Step 4: Budget msg.value and callbackFeeLocalWei
msg.value and callbackFeeLocalWeiTwo-way Inbox traffic needs enough native token to cover outbound execution and the callback. Use the deployed Inbox’s calculateTwoWayFeeRequiredInLocalToken (or your operator’s runbook) to pick msg.value and callbackFeeLocalWei. Undershooting typically leaves the request stuck or failing.
Step 5: Encrypt the two summands (TypeScript)
CotiPodCrypto.encrypt calls the PoD encryption service. For Sepolia-style test usage, pass "testnet" as the network key (see coti-pod-crypto.ts in the SDK: testnet maps to the COTI testnet encryption endpoint).
Use DataType.itUint256 when you build itUint256 calldata yourself (for example with ethers.Contract). If you use PodContract.encryptAndCallMethod in the next step, you can skip manual encryption: pass plaintext numeric strings and DataType.itUint256 in each PodMethodArgument, and the SDK encrypts before encoding the transaction.
Step 6: Submit the add transaction (PodContract, fees, extractRequestIds)
add transaction (PodContract, fees, extractRequestIds)PodContract wraps your ethers.Contract: it **estimateFee**s against the Inbox, maps PodMethodArgument values (including encryptAndCallMethod encryption for it* types), injects the callBackFee into the slot marked isCallBackFee: true, sends value: totalFee on payable functions, and exposes extractRequestIds(txHash) to read requestId values from MessageSent logs on the Inbox (reliable across layouts where parsing logs from the app contract alone is brittle).
Tune forwardGasLimit, callBackGasLimit, and callBackDataSize from measurements on your contract; estimateFee requires callBackGasLimit and callBackDataSize together or neither. For callMethod instead of encryptAndCallMethod, supply JSON ciphertext strings for it* arguments (what the encryption service returns), for example when the browser already called CotiPodCrypto.encrypt.
Alternative (raw ethers.Contract) — If you are not using PodContract, call add with toitUint256(encA), toitUint256(encB), callbackFeeLocalWei, and { value: totalWei }, then still use extractRequestIds for correlation:
Private addition is asynchronous: the sum appears only after the Inbox invokes addCallback in a later transaction. Poll statusByRequest(requestId) or wait for AddCompleted.
Step 7: Read the encrypted sum and decrypt locally
After status is Completed, read sumByRequest(requestId). The value is ctUint256 (ciphertext), not plaintext.
CotiPodCrypto.decrypt delegates to @coti-io/coti-sdk-typescript and expects a scalar ciphertext as a hex string for Uint64, plus the user’s AES key (see SDK source coti-pod-crypto.ts).
Step 8: Sanity checks and next steps
Callback decode must stay
(ctUint256)— changing the executor op or COTI-side behavior without updating the decode tuple will corrupt storage reads.Type lane — This contract uses
add256withitUint256/ctUint256on chain.CotiPodCrypto.decryptstill takes aDataTypefor the scalar decode; keepDataType.Uint64(orUint256, etc.) aligned with how your app and onboarding produce the ciphertext for this flow, per your installed SDK.Production: add tests for non-Inbox callers on
addCallback, under-fundedmsg.value, and decrypt failures; follow the first production checklist in Getting started.
Reference links
Last updated
Was this helpful?