# Welcome

{% embed url="<https://youtu.be/AvEBZp4Uv_Y?feature=shared>" %}

### About COTI

COTI is a groundbreaking blockchain network designed to enable computation on encrypted data through the use of Garbled Circuits. By combining advanced cryptographic techniques with blockchain technology, COTI opens new possibilities for secure and private data processing.

### What You'll Find Here

Our developer documentation serves as a comprehensive guide to understanding and working with the COTI network. Here's what you can expect:

* [**Technology Overview**](/coti-documentation/how-coti-works): Dive into the technical foundations of the COTI network, including the cryptographic principles and innovative solutions behind its operation.
* [**Quickstart Guide**](/coti-documentation/build-on-coti/quickstart): Get up and running with the COTI network quickly, using step-by-step instructions and practical examples.
* [Privacy on Demand](/coti-documentation/privacy-on-demand) — Private computation on COTI with EVM orchestration (PoD)


# Networks

COTI test networks provide developers access to a free testing environment for COTI v2 network services. Testnets simulate the exact development environment as you would expect for Mainnet. This includes transaction fees, available services, etc.

#### Network Technical Details

When utilizing the COTI network environments, it is vital to understand the technical details that influence their operation. Here are some key parameters you will encounter:

* **Base Gas Fee**: The base gas fee is set at 5,000,000 wei. This fee is fundamental for executing transactions on the network and is essential for interacting with smart contracts effectively.
* **Block Gas Limit**: Each block has a gas limit of 120,000,000. This limit dictates the total amount of gas that can be used for all transactions within a single block, ensuring that blocks stay within a manageable size for processing.
* **Block Generation Time**: Blocks are generated every 5 seconds. This rapid block generation contributes to the network's efficiency and ensures that transactions are processed swiftly, providing developers and users with timely updates.

Understanding these parameters is crucial for developers and users interacting with the COTI environments, as they directly impact transaction execution and network performance.

<table><thead><tr><th width="143">Networks</th><th>Description</th></tr></thead><tbody><tr><td><strong>Devnet</strong></td><td><p>Code that is under development by the COTI core team and likely to be used in an upcoming release, Designed to give developers exposure to features soon to be released.</p><p>Updates to the Devnet are made frequently, No guarantee by COTI to keep the consistency of the blocks and transactions.<br>----------------------------------------------------</p><p><mark style="color:red;"><strong>Devnet was sunset on March 2nd, 2025.</strong></mark></p></td></tr></tbody></table>


# Release Notes

Welcome to the COTI Release Notes. This section provides a chronological overview of all major updates, improvements, and bug fixes across the COTI network. Each release note includes detailed information about protocol upgrades, infrastructure changes, developer-facing features, and user-facing enhancements. Stay up to date with the latest developments as we continue to evolve the COTI ecosystem and bring cutting-edge privacy and scalability features to decentralized finance.


# v1.1.4

**Release Notes v1.1.4**

Changes from v1.1.3 (Post-Audit Improvements)\
Following an in-depth security audit, version 1.1.4 of both MPC and gcEVM introduces key\
fixes and refinements focused on stability, security, and maintainability. These updates reflect\
our ongoing commitment to delivering a robust, privacy-preserving infrastructure.

**Hard Fork**

* hydrogen (see specific network section for implementation details)

**MPC – Key Fixes & Improvements**

1. Strengthened File Handling Logic\
   Improved file path validation to prevent unintended access outside approved directories,\
   reducing the risk of directory traversal vulnerabilities.
2. Improved Randomness Validation\
   Enforced minimum entropy checks for random seeds used in cryptographic functions,\
   aligning with recommended security practices.
3. Refined Random Byte Generation\
   Updated internal methods for device-level randomness to ensure consistency and\
   improve cryptographic unpredictability.
4. More Resilient Connection Handling\
   Fixed a race condition in client connection logic, improving reliability under high\
   concurrency or unexpected delays.
5. Enhanced Cleanup of Sensitive Data\
   Ensured secure memory wiping in destructors to better protect in-memory secrets from\
   residual access.
6. Introduced a Unique Fixed Key per Garbled Circuit\
   Enhanced cryptographic security by generating a fresh fixed key for each garbled circuit\
   instance, replacing the previous static approach and reducing key reuse.
7. Improved Memory Management in Garbling Workflow\
   Fixed allocation and cleanup routines within the batch garbling process, preventing\
   memory leaks and improving runtime stability.
8. Safer Arithmetic Operations\
   Introduced checks to prevent potential integer overflows and underflows during circuit\
   construction and evaluation.
9. Better Exception Safety\
   Strengthened error-handling in destructors and allocation logic to avoid crashes during\
   failure scenarios.
10. Hardened Input Validation Across Modules\
    Applied more robust input validation throughout the codebase, reducing attack surface\
    from malformed or unexpected inputs.

**gcEVM – Key Fixes & Improvements**

1. Improved Transcript Validation\
   Strengthened the validation of MPC transcript hash to ensure correctness in secure\
   execution. This fix led to the introduction of the network’s first protocol fork, Hydrogen,\
   aligning all nodes with the corrected logic.
   1. affected by [hydrogen](/coti-documentation/networks/release-notes/v1.1.4)
2. Fixed gRPC Error Handling in Opcode Execution\
   Improved handling of unexpected or unmapped gRPC errors in the opcode execution\
   path to reduce the likelihood of silent failures during MPC interactions.
3. Fixed Redundant Verification in Block Insertion\
   Simplified block processing by removing duplicate verification steps, resulting in a more\
   maintainable and efficient flow.
4. Fixed Minor Issues in Authenticated Memory Handling\
   Corrected subtle inconsistencies between the authenticated memory implementation\
   and its documentation.


# v1.2.0

**Release Notes v1.2.0**\
\
Version **1.2.0** introduces the **Helium** protocol fork and major enhancements across both **MPC** and **gcEVM**, including improved arithmetic circuits, expanded bit-width support, and new debugging capabilities.\
The update focuses on extending computational expressiveness, improving correctness of arithmetic operations, and providing developers with better visibility into MPC execution.

## **Changes from v1.1.4**

#### **New Fork: Helium**

* Introduces updated arithmetic logic across MPC and gcEVM.
* Ensures network-wide alignment for expanded bit-width operations and improved circuit behavior.

### **MPC – Key Fixes & Improvements**

#### **Improved Circuits for Multiplication & Division**

1. Updated multiplication and division circuit implementations.
2. Improved correctness and performance based on refined internal circuit structures.
3. Adjusted MPC pricing parameters to reflect updated computation cost.

#### **Trace Debug Support for MPC Operations**

6. Introduces developer-facing trace/debug capabilities.
7. Disabled by default, intended for development and testing.

### **gcEVM – Key Fixes & Improvements**

#### **Support for 128-bit and 256-bit Arithmetic**

1. Extended gcEVM instruction set and execution logic to handle 128-bit and 256-bit operations natively.
2. Ensures correct integration with updated MPC arithmetic circuits.
3. Enables private smart contracts to execute broader, more complex arithmetic functions.

**Fixes**

1. Fix for validateCipherText so it wont check nesting level of the caller enabling to use it with Proxy Contracts


# MainNet

COTI V2 is the next evolution of the COTI protocol, designed to bring privacy, scalability, and programmability to the forefront of blockchain finance. Built as a Layer 2 over Ethereum, COTI V2 leverages advanced cryptographic techniques like Garbled Circuits to enable **Private Computation**—a powerful new standard for confidential smart contracts and token interactions, seamless Ethereum compatibility (EVM), and a dedicated Treasury and Full Node infrastructure, COTI V2 empowers developers and businesses to create secure, decentralized financial applications without compromising on privacy or speed.

| COTI MainNet                                                                                                                                                                                                                                                                                                                                                               |
| -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| <ul><li>Network name: COTI</li><li>RPC URL: <a href="https://mainnet.coti.io/rpc"><https://mainnet.coti.io/rpc></a></li><li>WS URL: <a href="wss://mainnet.coti.io/ws">wss\://mainnet.coti.io/ws</a></li><li>Chain ID: 2632500</li><li>Currency symbol: COTI</li><li>Block explorer URL: <a href="https://mainnet.cotiscan.io"><https://mainnet.cotiscan.io></a></li></ul> |

**Hard Forks**

| Version                                                      | Hard Fork | Block #                                                              |
| ------------------------------------------------------------ | --------- | -------------------------------------------------------------------- |
| [v.1.1.4](/coti-documentation/networks/release-notes/v1.1.4) | hydrogen  | <ul><li>3,640,378</li><li>October 19, 2025 at 12:00 PM UTC</li></ul> |
| [v.1.2.0](/coti-documentation/networks/release-notes/v1.2.0) | helium    | <ul><li>5,098,638</li><li>January 11, 2026 at 10:00 PM UTC</li></ul> |


# Adding the COTI Mainnet to MetaMask

Follow these instructions to add the COTI mainnet directly to your MetaMask wallet.

### Instructions

1. Open MetaMask.
2. Click on the dropdown of networks and select **"Add a Custom Network"**.\
   ![](https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-56d9b190cd285111b4889915c36b07f1f3fd3300%2Fimage.png?alt=media). ![](https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-d8e5f0926c2639b457df74cae25a439cbea76420%2Fimage.png?alt=media)
3. Fill in the following info based on the network you wish to add:

| Parameter          | MAINNET                       |
| ------------------ | ----------------------------- |
| Network Name       | COTI                          |
| Default RPC URL    | <https://mainnet.coti.io/rpc> |
| Chain ID           | 2632500                       |
| Currency Symbol    | COTI                          |
| Block Explorer URL | <https://mainnet.cotiscan.io> |

![](https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-86417e8d86dc37556fba7fbe48e42d6aa96e0eab%2Fimage.png?alt=media)\\

4. Click **Save**.


# Contracts Addresses

List of addresses of deployed smart contracts related to the protocol, precompiles and other helper contracts.

<table><thead><tr><th width="265" align="center">Contract</th><th align="center">Addresses</th></tr></thead><tbody><tr><td align="center"><a href="https://github.com/coti-io/coti-contracts/blob/main/contracts/utils/mpc/MpcInterface.sol">MPCInterface</a></td><td align="center"><code>0x0000000000000000000000000000000000000064</code></td></tr><tr><td align="center"><a href="https://github.com/coti-io/coti-contracts/blob/main/contracts/onboard/AccountOnboard.sol">AccountOnboard</a></td><td align="center"><a href="https://mainnet.cotiscan.io/address/0x536A67f0cc46513E7d27a370ed1aF9FDcC7A5095"><code>0x536A67f0cc46513E7d27a370ed1aF9FDcC7A5095</code></a></td></tr><tr><td align="center"><a href="https://github.com/Arachnid/deterministic-deployment-proxy">DeterministicDeploymentProxy</a></td><td align="center"><a href="https://mainnet.cotiscan.io/address/0x4e59b44847b379578588920cA78FbF26c0B4956C"><code>0x4e59b44847b379578588920ca78fbf26c0b4956c</code></a></td></tr><tr><td align="center"><a href="https://github.com/coti-io/coti-contracts/blob/development/contracts/messaging/PrivateMessaging.sol">PrivateMessaging</a></td><td align="center"><a href="https://mainnet.cotiscan.io/address/0xe461F448cB935a14585F6f1a30F5b4C73ffF8c05"><code>0xe461F448cB935a14585F6f1a30F5b4C73ffF8c05</code></a></td></tr><tr><td align="center">gCOTI Token Contract</td><td align="center"><a href="https://mainnet.cotiscan.io/address/0x7637C7838EC4Ec6b85080F28A678F8E234bB83D1"><code>0x7637C7838EC4Ec6b85080F28A678F8E234bB83D1</code></a></td></tr><tr><td align="center">gCOTI Contract Owner</td><td align="center"><code>0x0fc92F51278c6d024aB6feb3527017F66d1DaD4d</code></td></tr><tr><td align="center">gCOTI holder of all funds</td><td align="center"><a href="https://mainnet.cotiscan.io/address/0x61BF10A1a27B2d99De0a59a06200A62ED579D685"><code>0x61bf10a1a27b2d99de0a59a06200a62ed579d685</code></a></td></tr><tr><td align="center">Sequencer account (fees account subject to community vote)</td><td align="center"><a href="https://mainnet.cotiscan.io/address/0xB557a07a397B5c9C94FC44260dE6C8A45EF5e731"><code>0xB557a07a397B5c9C94FC44260dE6C8A45EF5e731</code></a></td></tr><tr><td align="center">Genesis / Inflation / bridge deposit</td><td align="center"><a href="https://mainnet.cotiscan.io/address/0x61BF10A1a27B2d99De0a59a06200A62ED579D685"><code>0x61bf10a1a27b2d99de0a59a06200a62ed579d685</code></a></td></tr><tr><td align="center">USDC.e Contract</td><td align="center"><a href="https://mainnet.cotiscan.io/address/0xf1Feebc4376c68B7003450ae66343Ae59AB37D3C"><code>0xf1Feebc4376c68B7003450ae66343Ae59AB37D3C</code></a></td></tr><tr><td align="center">Treasury EOA</td><td align="center"><a href="https://mainnet.cotiscan.io/address/0x5e19f674b3B55dF897C09824a2ddFAD6939e3d1D"><code>0x5e19f674b3B55dF897C09824a2ddFAD6939e3d1D</code></a></td></tr><tr><td align="center">wBTC Contract</td><td align="center"><a href="https://mainnet.cotiscan.io/token/0x8C39B1fD0e6260fdf20652Fc436d25026832bfEA">0x8C39B1fD0e6260fdf20652Fc436d25026832bfEA</a></td></tr><tr><td align="center">wETH Contract</td><td align="center"><a href="https://mainnet.cotiscan.io/address/0x639aCc80569c5FC83c6FBf2319A6Cc38bBfe26d1">0x639aCc80569c5FC83c6FBf2319A6Cc38bBfe26d1</a></td></tr></tbody></table>


# TestNet

COTI Testnet serves as an experimental environment for developers and enthusiasts to explore COTI’s native blockchain technologies. It enables users to test new features, smart contracts, and the stability of network protocols without affecting the Mainnet. The Testnet offers an accessible platform for simulating transactions and network behavior, aiding in the detection and resolution of potential issues before deployment on the Mainnet. This sandbox environment is crucial for enhancing the robustness and reliability of COTI’s ecosystem.

To facilitate testing, developers can obtain free tokens from the Testnet Faucet (next page). This allows users to perform transactions and execute smart contracts without incurring any costs.

Testnet runs the same code as the COTI v2 Mainnet, designed to provide a pre-production environment for developers about to move to Mainnet.

| COTI TestNet                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| <ul><li>Network name: COTI Testnet</li><li>RPC URL: <a href="https://testnet.coti.io/rpc"><https://testnet.coti.io/rpc></a></li><li>WS URL: <a href="wss://testnet.coti.io/ws">wss\://testnet.coti.io/ws</a></li><li>Chain ID: 7082400</li><li>Currency symbol: COTI</li><li>Block explorer URL: <a href="https://testnet.cotiscan.io"><https://testnet.cotiscan.io></a></li><li>Testnet status page: <a href="https://uptime.coti.io/"><https://uptime.coti.io></a></li></ul> |

**Hard Forks**

<table><thead><tr><th width="219">Version</th><th width="223">Hard Fork</th><th>Block #</th></tr></thead><tbody><tr><td><a href="/coti-documentation/networks/release-notes/v1.1.4">v.1.1.4</a></td><td>hydrogen</td><td><ul><li>2,325,800</li><li>July 22, 2025 at 12:00 PM UTC</li></ul></td></tr><tr><td><a href="/coti-documentation/networks/release-notes/v1.2.0">v1.2.0</a></td><td>helium</td><td><ul><li>4,602,680</li></ul></td></tr></tbody></table>


# Faucet

The COTI faucet provides Devnet/Testnet funds for developers, using a Discord bot to serve these requests.

To login to COTI's discord and use the faucet, please navigate to <https://discord.coti.io> and join our Discord server.

To request Devnet/Testnet tokens:

1. Head to [**https://faucet.coti.io/**](https://faucet.coti.io/)
2. Send a message to the bot in the following format:\
   `<network> <your_eoa_address>`\
   \
   For Example:\
   \
   `testnet 0xDA8004D6AB073B9D5549b7D5599D51FF1191C747`\
   (for testnet funds request)


# Adding the COTI TestNet to Metamask

Follow these instructions to add the COTI testnet directly to your MetaMask wallet.

### Instructions

1. Open MetaMask.
2. Click on the dropdown of networks and select **"Add a Custom Network"**.\
   ![](https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-56d9b190cd285111b4889915c36b07f1f3fd3300%2Fimage.png?alt=media). ![](https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-d8e5f0926c2639b457df74cae25a439cbea76420%2Fimage.png?alt=media)
3. Fill in the following info based on the network you wish to add:

| Parameter          | TESTNET                       |
| ------------------ | ----------------------------- |
| Network Name       | COTI Testnet                  |
| Default RPC URL    | <https://testnet.coti.io/rpc> |
| Chain ID           | 7082400                       |
| Currency Symbol    | COTI                          |
| Block Explorer URL | <https://testnet.cotiscan.io> |

![](https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-980d06b01108d865f7987a0cdcc92a1a27fa7edb%2Fimage.png?alt=media)\\

4. Click **Save**.


# Contracts Addresses

List of addresses of deployed smart contracts related to the protocol, precompiles and other helper contracts.

<table><thead><tr><th width="265" align="center">Contract</th><th align="center">Addresses</th></tr></thead><tbody><tr><td align="center"><a href="https://github.com/coti-io/coti-contracts/blob/main/contracts/utils/mpc/MpcInterface.sol">MPCInterface</a></td><td align="center"><code>0x0000000000000000000000000000000000000064</code></td></tr><tr><td align="center"><a href="https://github.com/coti-io/coti-contracts/blob/main/contracts/onboard/AccountOnboard.sol">AccountOnboard</a></td><td align="center"><a href="https://testnet.cotiscan.io/address/0x536A67f0cc46513E7d27a370ed1aF9FDcC7A5095"><code>0x536A67f0cc46513E7d27a370ed1aF9FDcC7A5095</code></a></td></tr><tr><td align="center"><a href="https://github.com/Arachnid/deterministic-deployment-proxy">DeterministicDeploymentProxy</a></td><td align="center"><a href="https://testnet.cotiscan.io/address/0x4e59b44847b379578588920cA78FbF26c0B4956C"><code>0x4e59b44847b379578588920ca78fbf26c0b4956c</code></a></td></tr><tr><td align="center"><a href="https://github.com/coti-io/coti-contracts/blob/development/contracts/messaging/PrivateMessaging.sol">PrivateMessaging</a></td><td align="center"><a href="https://testnet.cotiscan.io/address/0xa4C514225Db5B8AE6eF1548d4CE912234A7CD954"><code>0xa4C514225Db5B8AE6eF1548d4CE912234A7CD954</code></a></td></tr><tr><td align="center">gCOTI Token Contract</td><td align="center"><a href="https://testnet.cotiscan.io/token/0x7AC988eb3E45fe6ADB05DFaf609c8DBb4A902cdC"><code>0x7AC988eb3E45fe6ADB05DFaf609c8DBb4A902cdC</code></a></td></tr></tbody></table>


# How COTI Works


# Introduction

Privacy is a critical concern in blockchain technology, where the transparency of decentralized networks conflicts with the confidentiality users often expect. Unlike traditional systems with access controls and encryption, blockchain platforms—particularly the EVM—lack built-in privacy, exposing all data across the network. This openness can deter users and industries that require discretion, risking data exposure and regulatory issues. To address these challenges, COTI is introducing [garbled circuits](/coti-documentation/how-coti-works/introduction/garbled-circuits) to enable private transactions, balancing blockchain transparency with the confidentiality essential for broader adoption, regulatory compliance, and respect for user privacy.


# EVM Introduction

To gain a deeper understanding of how the COTI gcEVM operates, it’s essential to be familiar with foundational concepts such as Ethereum, the Ethereum Virtual Machine (EVM), and smart contracts.

If you’re new to Ethereum and smart contracts, the following introductory guides are an excellent starting point:

* [**Introduction to Ethereum**](https://ethereum.org/en/developers/docs/intro-to-ethereum/)
* [**Introduction to Smart Contracts**](https://ethereum.org/en/developers/docs/smart-contracts/)
* [**Ethereum Virtual Machine (EVM)**](https://ethereum.org/en/developers/docs/evm/)


# Conceptual Overview

### Garbled Circuits and how they preserve privacy <a href="#eca7" id="eca7"></a>

As a privacy-preserving cryptographic technique, garbled circuits were essentially designed to solve one problem: The Millionaires problem created by Andrew Yao. In this theoretical scenario, two millionaires, Alice and Bob, want to work out which one of them is richer without disclosing their actual net worth.

To do this, they can use a garbled circuit which can be simplified into the following steps:

* **Step 1** — The problem or “function” (i.e. who is richer) is written as a type of program that uses logical gates, (aka a Boolean circuit). In the Millionaires Problem, suppose that the millionaires’ wealth can fit into 8-bit integers (recall that such integers can accommodate numbers between 0 and 2⁸-1=255). Then the Boolean circuit has 2x8=16 input wires (first set of 8 input wires \`belong’ to Alice and the second set \`belongs’ to Bob). The circuit structure is such that it takes the first and second sets of input wires, interprets them as numbers X and Y, and computes MAX(X,Y). The result goes to an output wire that encodes a single bit B. If B=0 then we have X > Y and otherwise (B=1) we have X ≤ Y.
* **Step 2** — Alice encrypts or “garbles” this Boolean circuit, the result is called Garbled Circuit. Each input wire (recall that there are 16 of them) is associated with two long and random labels L0 and L1 that represent the binary values 0 and 1, respectively. At the time of garbling, Alice has L0 and L1 for *all wires*. The goal of Alice is to give Bob the garbled circuit, along with *only a single label* for each input wire, so that Bob will be able to compute the MAX function only once using the labels it obtains. The set of labels associated with the input wires of the garbled circuit (one label per input wire) is called a Garbled Input. In Step 3a, Alice sends the garbled circuit to Bob, including one label for each of the first 8 input wires that belong to her, and in Step 3b, Bob obtains one label per input wire of the second set of 8 input wires (that belong to him).
* **Step 3** — Alice sends the garbled circuit to Bob along with the right labels for her 8 input wires.
* **Step 4** — Bob “garbles” his own number and obtains 8 labels, one label for each input wire that belongs to him. Now Bob is ready for the actual computation.
* **Step 5**. Bob computes the garbled circuit on the garbled input (one label per input wire). This process outputs the bit B in the clear, so now Bob knows the result of the computation. In particular, this result does not reveal any dollar amounts, just an answer to the question of who is richer.
* **Step 6** — At this point Bob may communicate the result, B, to Alice, so she can learn which of them is richer.

This is obviously a simplified explanation, visit the [**Garbled Circuits**](/coti-documentation/how-coti-works/advanced-topics/garbled-circuits) page for a more detailed walkthrough of the process.

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-a5174bfcfbf4bda5d5b1f2679266c9c6a62b9551%2Fcotygc.png?alt=media" alt=""><figcaption></figcaption></figure>


# Use Cases

COTI brings privacy to the public blockchain era,enabling individuals, institutions, and developers to unlock new forms of on-chain innovation without compromising confidentiality or compliance.

### 1. Confidential Transactions

Send and receive private tokens securely

Traditional blockchains expose every transaction and balance on-chain.\
With COTI V2, users can send and receive tokens privately, where transaction amounts, participants, and balances remain fully encrypted while preserving verifiability on the blockchain.

Secure tokenization becomes feasible, enabling the exchange of assets while protecting the confidentiality of transaction details.

![](https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2FJvf3fm05EzInc4i8vEWJ%2Funknown.png?alt=media\&token=4a142f78-5334-432a-ab2b-2f958d5a98b5)

<br>

Demo Video:[ Watch on YouTube →<br>](https://youtu.be/HwkWEQX0n94)(Shows private token transfer between wallets using COTI V2’s confidential transaction layer.)

Why it matters

* Protects user and business financial data
* Enables compliant, private settlements
* Supports any ERC-20-style token deployed on COTI<br>

### 2. Confidential DeFi

Privex: A Perpetual DEX with privacy built-in

DeFi transformed finance — but its radical transparency comes with risks.\
On-chain positions, liquidations, and strategies are visible to everyone, deterring institutional use and automated trading strategies.

COTI V2 enables confidential DeFi: all trades, positions, and settlement data remain encrypted while still verifiable. This ensures compliance readiness while safeguarding user privacy.

![](https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2FXSzbeYNUlsddY9rKRRyR%2Funknown.png?alt=media\&token=692daf6f-92ae-4a35-b811-6bee0446ca09)

Demo:[ Watch on X (Twitter) →](https://www.youtube.com/watch?v=6dVypDtk9VY)

Why it matters

* Enables institutional participation in DeFi
* Protects trading strategies and portfolio data
* Retains transparency through auditable proofs
* Complies with evolving regulatory frameworks\ <br>

### 3. Private Voting

Confidential voting and governance

COTI’s privacy infrastructure powers secure, verifiable, and anonymous voting — ideal for DAOs, enterprises, or even public governance.\
Votes are cast privately, encrypted end-to-end, and tallied without exposing voter identities or individual choices.

Confidential voting systems maintain the integrity of elections by preserving the privacy of voters’ decisions.

![](https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fyz2HOM1NGW7QqnrWrDFN%2Funknown.png?alt=media\&token=dc039268-be07-44a1-b5c8-dc726c5e6fae)

Demo:[ Watch on YouTube →](https://www.youtube.com/watch?v=_RKEkR1KUOA\&list=PLQ1p4uxJCOysr2U-53II0Vxvwe8SS2qI1\&index=5)

Why it matters

* Prevents coercion and vote manipulation
* Verifiable yet private vote tallying
* Applicable for DAOs, corporate boards, and communities

<br>

### More to Come

These are just the first applications powered by COTI V2’s confidential computing stack.\
The same privacy layer can be integrated into:

* Private stablecoins
* Confidential identity and credential systems


# COTI Architecture

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-ab02c217e840ddb97c9b8d6bcf7180f5c414ceaa%2FGC%20flow%20flipped(1).png?alt=media" alt=""><figcaption></figcaption></figure>

The COTI network is comprised of several core components:

1. **Full Nodes**: These nodes are responsible for validating transactions and ensuring the integrity of the COTI blockchain network. They act as the decentralized foundation of the network.
2. **Sequencer**: This component processes transactions (TXs) and organizes them into blocks. It coordinates transaction flow and ensures orderly addition to the chain.
3. **Executors**: Two separate executors that process blocks in a MPC manner:
   * **Red Blocks**: incoming blocks processed from the sequencer to the executors.
   * **Black Blocks**: outgoing blocks processed from the executor to the sequencer.
4. **GC Manager**: Stores the generated garbled circuits in data warehouse, these circuits are used by the executors for on-chain computation.
5. **Garbler**: Generates garbled circuits that are stored into the DB Manager to be used later on during on-chain computation.

The network uses a multi-tier architecture to optimize transaction flow and ensure scalability, with the sequencer bridging full nodes to the execution layer and the database manager and garbler handling persistence and post-processing.


# Advanced Topics


# Garbled Circuits

Privacy solutions have been around for a while, but were always limited due to constraints in achieving full confidentiality and computational overhead. **COTI V2 introduces a novel approach that ensures performance without sacrificing privacy.**

To enable a performant solution with strong privacy guarantees, COTI V2 will utilize a novel combination of well-established privacy-preserving technologies (PETs), with the main ingredient being a Garbling Protocol.

### What are Garbling Protocols? <a href="#id-57b3" id="id-57b3"></a>

In the field of secure multi-party computation (MPC), a garbling protocol enables two or more parties to jointly compute a function while keeping both their inputs and intermediate variables private. Introduced initially in the 1980s, garbling protocols have become a cornerstone for privacy-preserving technologies.

### The Mechanism of Garbling Protocols <a href="#id-97b4" id="id-97b4"></a>

<figure><img src="https://miro.medium.com/v2/resize:fit:1400/0*dyWaMbF7u_UV7av3" alt="" height="394" width="700"><figcaption></figcaption></figure>

The primary idea behind a garbling protocol is relatively straightforward, yet incredibly powerful. Imagine two parties, Alice and Bob, who wish to compute a function without revealing their inputs to each other. To do this, they use a Garbled Circuit, which works as follows:

* **Function Representation**: First, the function they wish to compute (which may be initially given as a mathematical formula or as a code written in high level programming language) is translated to a Boolean circuit. A Boolean circuit is a computational model that supports only a basic set of operations (also known as logical gates, like AND, OR, NOT) that can handle binary inputs (Booleans).
* **Circuit Garbling:** Alice, who takes the role of the ‘garbler,’ then encrypts or ‘garbles’ this circuit. However, instead of a traditional encryption, which works on *data*, garbling is an encryption that works on *functions*. The gates in the circuit and the inputs are encrypted in such a way that the output (of the circuit evaluation on the inputs) can only be understood if one has the correct ‘evaluation keys’.
* **Circuit Evaluation**: Following an interaction between Alice and Bob (known in the literature by oblivious transfer, or OT), in which Bob obtains these evaluation keys, Bob can evaluate the garbled circuit. As mentioned above, such evaluation will not leak to Bob the inputs or the intermediate variables; only the output of the function will be revealed.

### Advantages of Garbling Protocols <a href="#id-99cc" id="id-99cc"></a>

The primary advantage of garbling protocols is their ability to preserve the privacy of individual inputs while still allowing for joint computation. This makes them particularly useful in scenarios where confidential data needs to be computed upon but cannot be shared in plain, such as in privacy-preserving auctions, joint data analysis between organizations, or secure voting systems.

In the context of COTI V2, garbling protocols offer a revolutionary approach to handling transactional privacy. They can enable transactions and smart contract executions where the details (such as the amount of funds transferred, or the specific conditions of the contract) remain private between the involved parties. This level of privacy is particularly important in decentralized finance applications where transaction confidentiality can be as critical as transaction integrity.

### Novelty and comparison to other solutions <a href="#cc2a" id="cc2a"></a>

While other privacy solutions in Web3 are currently being developed, many of them rely on scaling technologies such as Zero-knowledge (ZK) cryptography to achieve transactional confidentiality.

Despite their effectiveness, ZK solutions aren’t without their limitations, especially when it comes to confidential transactions involving multiple parties. In this scenario, confidential data has to be stored and processed off-chain, either by the initiating user or a third party. This leads to an increase in centralization, as well as a dependency on what could potentially be an insecure storage solution. This is the case for privacy solutions like Secret, Obscuro, and Oasis, technologies that rely **fully** on secure enclaves such as Intel and SGX. Worse still, these enclaves haven’t been as secure as advertised, attracting numerous data breaches over the past few years.

Additionally, ZK proofs incur a significant cost for on-chain verification and offer a suboptimal user experience. At present levels, SNARK-proof verification costs around 200k gas on the EVM and may take up to a few seconds to compute on the client device.

COTI V2’s use of garbling protocols takes a completely novel approach. Unlike ZK solutions which support a single data source (or owner), and are expensive on the client side, garbling protocols enable computation on private data coming from many sources (or owners). Furthermore, they’re able to maintain private storage in addition to Ethereum’s standard public global state.

Garbling protocols already allow for a very efficient client, however, with breakthroughs made by COTI V2, the technology is up to **ten times lighter and performs ten times faster** than ZK based solutions without negatively impacting the user experience. This means that the **technology has the ability to run on almost any device, expanding the range of potential use cases in the future.** Adopting a privacy solution that supports operation on private data from multiple sources also helps in mitigating Maximal Extractable Value (MEV) losses, as a portion of crucial data remains encrypted at all times.

When it comes to the security of the network, COTI V2 is predicated on the fact that a threshold of the network nodes operate faithfully. While COTI plans to use secure enclaves for an additional layer of protection, they are not a necessity for the integrity of the system. Additionally, this design allows users to withdraw directly from L1 without needing L2’s permission, further emphasizing the secure nature of COTI V2.

COTI V2’s cryptographic protocols are both secure and efficient, allowing parties to jointly evaluate functions over their private data without leaking information. What sets COTI V2 apart is its unique integration of a garbling protocol within a larger MPC solution to facilitate confidential transactions and smart contract interactions on a layer 2 platform. This synergy not only enhances privacy by ensuring inputs remain encrypted throughout the computation process, but also significantly reduces the risk of data exposure or manipulation by malicious actors.


# AES Keys

## Acquiring Your AES Key

The gcEVM utilizes AES keys, unique to each user, for encrypting and decrypting their data. To securely retrieve your AES key, the system provides a precompiled contract designed to retrieve the key associated with your account.

To begin, you must generate an RSA key pair, as RSA encryption is used to securely transmit AES keys. Next, sign the generated RSA public key using your account's private key with the ECDSA signing scheme.

After completing these steps, call the `GetUserKey` function on the network's precompiled contract. Pass your RSA public key and its signature as arguments. The precompiled contract will respond (using an event on the blockchain) with your AES key, encrypted in a way that only your RSA private key could decrypt.

## Encrypting Inputs

The gcEVM processes private inputs by encapsulating them within an `Inputtext` object. To use a private input, you need to create an `Inputtext` instance with your input data.

Each `Inputtext` instance contains an encrypted version of the input value and a signature. The signature is generated by concatenating the sender's address, the contract address, the target function, and the encrypted amount - that is the protocol for sending encrypted data.

The encryption process involves generating a random number, encrypting it with your AES key, and then applying a bitwise XOR operation between the input value and the encrypted random number. This ensures the input's confidentiality and integrity during processing.

## Decrypting Outputs

The gcEVM stores encrypted values within a `Ciphertext` object. This object includes the encrypted value and a random number generated by the gcEVM during the encryption process.

To retrieve the decrypted value, the user must first encrypt the random number using their AES key. Then, perform a bitwise XOR operation between the encrypted value and the encrypted random number to reconstruct the original data.

## Network Key

The "network key" is an AES encryption key fragmented using advanced crptographic techniques (e.g., threshold cryptography) so that each node in the network stores only an encrypted or protected portion. No single node or entity can reconstruct or access the entire key. However, through secure multi-party computation, the gcEVM can process data encrypted with the network key and transform it into a usable format for secure on-chain private computations.


# Precompiles

## MpcInterface.sol

The `MpcInterface` contract is a precompiled contract used by the gcEVM to facilitate multi-party computations (MPC). It is deployed at the fixed address: `0x0000000000000000000000000000000000000064`.

The contract implements the following interface:

```solidity
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.19;

interface ExtendedOperations {

    function OnBoard(bytes1 metaData, uint256 ct) external returns (uint256 result);
    function OffBoard(bytes1 metaData, uint256 ct) external returns (uint256 result);
    function OffBoardToUser(bytes1 metaData, uint256 ct, bytes calldata addr) external returns (uint256 result);
    function SetPublic(bytes1 metaData, uint256 ct) external returns (uint256 result);
    function Rand(bytes1 metaData) external returns (uint256 result);
    function RandBoundedBits(bytes1 metaData, uint8 numBits) external returns (uint256 result);
    function Add(bytes3 metaData, uint256 lhs, uint256 rhs) external returns (uint256 result);
    function CheckedAdd(bytes3 metaData, uint256 lhs, uint256 rhs) external returns (uint256 overflowBit, uint256 result);
    function Sub(bytes3 metaData, uint256 lhs, uint256 rhs) external returns (uint256 result);
    function CheckedSub(bytes3 metaData, uint256 lhs, uint256 rhs) external returns (uint256 overflowBit, uint256 result);
    function Mul(bytes3 metaData, uint256 lhs, uint256 rhs) external returns (uint256 result);
    function CheckedMul(bytes3 metaData, uint256 lhs, uint256 rhs) external returns (uint256 overflowBit, uint256 result);
    function Div(bytes3 metaData, uint256 lhs, uint256 rhs) external returns (uint256 result);
    function Rem(bytes3 metaData, uint256 lhs, uint256 rhs) external returns (uint256 result);
    function And(bytes3 metaData, uint256 lhs, uint256 rhs) external returns (uint256 result);
    function Or(bytes3 metaData, uint256 lhs, uint256 rhs) external returns (uint256 result);
    function Xor(bytes3 metaData, uint256 lhs, uint256 rhs) external returns (uint256 result);
    function Shl(bytes3 metaData, uint256 lhs, uint256 rhs) external returns (uint256 result);
    function Shr(bytes3 metaData, uint256 lhs, uint256 rhs) external returns (uint256 result);
    function Eq(bytes3 metaData, uint256 lhs, uint256 rhs) external returns (uint256 result);
    function Ne(bytes3 metaData, uint256 lhs, uint256 rhs) external returns (uint256 result);
    function Ge(bytes3 metaData, uint256 lhs, uint256 rhs) external returns (uint256 result);
    function Gt(bytes3 metaData, uint256 lhs, uint256 rhs) external returns (uint256 result);
    function Le(bytes3 metaData, uint256 lhs, uint256 rhs) external returns (uint256 result);
    function Lt(bytes3 metaData, uint256 lhs, uint256 rhs) external returns (uint256 result);
    function Min(bytes3 metaData, uint256 lhs, uint256 rhs) external returns (uint256 result);
    function Max(bytes3 metaData, uint256 lhs, uint256 rhs) external returns (uint256 result);
    function Decrypt(bytes1 metaData, uint256 a) external returns (uint256 result);
    function Mux(bytes3 metaData, uint256 bit, uint256 a,uint256 b) external returns (uint256 result);
    function Not(bytes1 metaData, uint256 a) external returns (uint256 result);
    function Transfer(bytes4 metaData, uint256 a, uint256 b, uint256 amount) external returns (uint256 new_a, uint256 new_b, uint256 res);
    function TransferWithAllowance(bytes5 metaData, uint256 a, uint256 b, uint256 amount, uint256 allowance) external returns (uint256 new_a, uint256 new_b, uint256 res, uint256 new_allowance);
    function ValidateCiphertext(bytes1 metaData, uint256 ciphertext, bytes calldata signature) external returns (uint256 result);
    function GetUserKey(bytes calldata signedEK) external returns (bytes memory encryptedKey);
    function SHA256Fixed432BitInput(uint256 amount, uint256 seed1, uint256 seed2, uint256 padding1, uint256 padding2, bytes calldata addr) external returns (bytes memory result);
}
```

The `metaData` values are designed to combine multiple enum values into bytes for efficient storage and transfer. For more information about the required format of `metaData` values, see the [MPC Core documentation](/coti-documentation/build-on-coti/tools/contracts-library/mpc-core#encoding-functions).


# Whitepaper

{% embed url="<https://files.gitbook.com/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FVF77VPDS2ZTopmEc1UzR%2Fuploads%2F0HCda5AeJ0dYrzQvEaDn%2Fcoti_v2_whitepaper.pdf?alt=media&token=17fc3b76-ccea-47fd-8955-bae3aef39b19>" %}


# COTI vs others

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-fe8caaf53e8d95e1c7aa375d8b6498c228b4875e%2Fimage.png?alt=media" alt=""><figcaption><p>COTI technology compared to the competition</p></figcaption></figure>

COTI V2, powered by Garbled Circuits (GC), offers unmatched capabilities in privacy-preserving computation, setting a new standard for building multi-party applications like privacy DEXs. Unlike competing technologies such as TEE, ZK-SNARK, MPC, and FHE, Garbled Circuits excel in delivering low-latency performance, compatibility across any device, and resilience through the absence of a single point of failure. With the added advantage of light storage requirements, COTI V2 ensures seamless scalability without compromising efficiency. This cutting-edge approach positions COTI V2 as the leading solution for secure, high-performance applications, driving innovation in privacy-centric blockchain technology.

### Advantages over FHE

COTI simplifies the decryption process and offers a more streamlined approach compared to traditional FHE providers. Unlike asynchronous setups that require multiple steps and callbacks, COTI enables synchronous decryption directly within smart contracts, instantly making results available for conditional checks and seamless contract execution. This simplicity not only enhances efficiency but also reduces development complexity. Furthermore, COTI strengthens privacy by securely re-encrypting decrypted data with the user’s AES key before returning it, eliminating the need for manual access control configurations. By combining ease of use with robust privacy measures, COTI delivers an unparalleled decryption experience for privacy-centric applications.

### Performance Benchmarking

COTI’s garbled circuits performed between 1,800 and 3,000 times faster than the leading FHE solution, Zama’s TFHE-rs, when [**tested**](https://medium.com/cotinetwork/coti-2-leading-the-way-in-privacy-preserving-blockchain-solutions-benchmark-study-7dac5fe18a08) for a series of basic logic operations (AND, OR, DIVIDE, etc).

Figures for the performance of TFHE-rs are taken from [**Zama’s CPU benchmarking tests**](https://docs.zama.ai/tfhe-rs/get-started/benchmarks), which were launched on an AWS hpc7a.96xlarge instance equipped with an AMD EPYC 9R14 CPU @ 2.60GHz and 740 GB of RAM.

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-afd7e91ede8ba4904283ab2cd84fa6a103d81566%2Fimage.png?alt=media" alt=""><figcaption></figcaption></figure>

COTI’s tests timed how low long it takes to execute a series of OPCODES 1,000 times for different bit-length inputs, and were conducted on a lower-powered machine than the TFHE-rs tests to avoid unfair advantage.

<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXcq9FoiLTvaF6mX1m25wr3ZvvgSz5nIK2zE87qaSZuvdoxpVypQilKKNxInNTnAepzkzrPefFMYDrAiJA2KtzU9mgIc6K7CgBAImAPgR5WjDVagLoy5V71wV-nbXKCTV_uqDB1j?key=a50IkdX6SdNbikZ_4QPfnsU0" alt=""><figcaption></figcaption></figure>

Using this data, direct comparison for the relative performance of COTI’s garbled circuits and TFHE-rs is possible.

### Consistency Across Input Size

Performance for GCs tends to be relatively consistent, regardless of the length of the inputs. For most OPCODES, including ADD, AND, OR, EQUAL, GREATER THAN, and LESS THAN, the functions execute with a variation of roughly 10-15%, regardless of whether the input is 8, 16, 32, or 64 bits. This is largely due to the fact that network latency represents a greater delay than the execution of such small circuits.

More complex operations including MULTIPLY, DIVIDE, and REMAINDER have greater variation in execution time, but scaling is still sub-linear, so large data sets can be handled efficiently.

This contrasts with TFHE-rs, where scaling for these operations is superlinear. For example, multiplication for 128-bit inputs takes around 2.6x longer than for 64-bit inputs, and 256 bits takes 3.3x longer than for 128.

### Efficiency Ratios

1,000 64-bit ADD operations, which is a fairly simple function, takes 49,411 μs, or 20,238 operations per second. A more complex function, such as a 64-bit MULTIPLY, still allows for 4,276 operations per second, indicating that MULTIPLY takes around 4.73x longer than ADD. The DIVIDE to ADD ratio is around 11.6.

While the performance for these complex operations is longer than for ADD, the performance ratio is still low, indicating that such operations are still executed efficiently. For comparison, the MULTIPLY to ADD ratio (64-bit) for TFHE-rs is around 2.8, while the DIVIDE to ADD ratio is 70. These ratios increase notably with greater bit length.

### Performance Comparison

GCs perform significantly faster than FHE for all benchmarked operations. Overall, COTI’s GCs are between 1,800 and 3,000 times faster than TFHE-rs.

* For 64-bit ADD, GCs require ≈49.4 μs per operation, while TFHE-rs requires 150,000 μs per operation, meaning that GCs are approximately 3,035x faster.
* For 64-bit MULTIPLY, GCs take ≈233.8 μs per operation. TFHE-rs takes 425,000 μs per operation, making GCs around 1,818x faster.
* For 64-bit GREATER THAN, GCs take ≈ 43.9 μs per operation. TFHE-rs takes 116,000 μs per operation, making GCs around 2,642x faster.

### Storage Efficiency

Storage efficiency becomes particularly important when encrypted data is held on-chain. TFHE-rs requires significant space to store ciphertexts, even after [compression](https://www.zama.ai/post/tfhe-rs-v0-7-ciphertext-compression-multi-gpu-support-and-more) (when available):

<figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXeIlbrI2H--kFJMzKo8SU1Jeq2NWefynI8BseU12jLldlLGuR8tulmJWD1wd2KxIjZ5f1ewutaGA95lrXefvY3TK6GqIS8DBxDZXxu9_k1pI1pwDXEb5eKUQuousWONzfEo8ktoaQ?key=a50IkdX6SdNbikZ_4QPfnsU0" alt=""><figcaption></figcaption></figure>

COTI V2’s gcEVM has a standard ciphertext size of 32 bytes, meaning that storage overheads are around 70x lower for a 64-bit input.

Together, these advantages mean that COTI’s garbled circuits can be executed on almost all devices, even lower-powered smartphones. The reduced computational requirements and storage demands allow for broader access and adoption, and an improved UX over FHE solutions.

Interested to learn more about the performance of Garbling Circuits vs. other technologies? checkout this [article](https://medium.com/cotinetwork/cotis-v2-cutting-edge-garbled-circuits-compared-to-other-privacy-preserving-smart-contracts-9e5b912612fa)


# Build on COTI


# Core Concepts


# What Is Onboarding?

Onboarding is the one-time setup that gives your wallet its encryption key for private on-chain data. A short COTI Network transaction **retrieves** that key (your AES key) for your wallet address so you can encrypt and decrypt private on-chain values.

Until the key is available in your wallet session, you can still use the network normally (public transfers, gas, non-private contracts). You **cannot** read or write private data — for example private balances, amounts, or other encrypted contract variables.

When a dApp needs access to private data, it will ask you to complete onboarding by approving that transaction in your wallet.

{% hint style="info" %}
**Developers:** see [Account Onboarding Procedure](/coti-documentation/build-on-coti/core-concepts/onboard-user) and [Account Onboard](/coti-documentation/build-on-coti/guides/account-onboard).
{% endhint %}

## Why do I need it?

Private values on COTI are stored encrypted. Encryption and decryption use a key that belongs only to **your** wallet account ([EOA](/coti-documentation/support/support-and-community/glossary#account)).

Your wallet needs that key to:

* **encrypt** inputs before private transactions
* **decrypt** outputs when you need to see private values

Without it, private features stay locked. The network already associates a unique AES key with your address; onboarding is how your wallet **receives** that key (first time) or **recovers** it again (new device, cleared storage, reinstall). Recovering always returns the **same** key for that address — it does not issue a new one.

## What is the encryption key?

* One AES key per wallet address — not shared across accounts
* Used only for **your** private data on COTI
* Held by your wallet / dApp session (for MetaMask users, often inside the [COTI Snap](/coti-documentation/build-on-coti/tools/coti-metamask-snap)); you usually never need to copy it
* Delivered wrapped so only your wallet can open it — not left as plaintext for others to read on-chain

{% hint style="danger" %}
Treat your AES key like your wallet private key. Anyone who has it can decrypt your private on-chain data. Never share your AES key, seed phrase, or private key.
{% endhint %}

Technical detail: [AES Keys](/coti-documentation/how-coti-works/advanced-topics/aes-keys).


# Account Onboarding Procedure

Account Onboarding is a procedure that is needed to acquire the EOA unique AES key, which is needed when the wallet wishes to perform GC related on-chain computation operations; that is when the AES key is being used (e.g. encryption and decryption).

Once the EOA is created and funded with native coin, it needs to be onboarded to the system to obtain an AES key.

For a plain-language explanation for end users, see [What Is Onboarding?](/coti-documentation/build-on-coti/core-concepts/what-is-onboarding).

See the [guide for onboarding an account](/coti-documentation/build-on-coti/guides/account-onboard).

After completing the Account Onboard step the wallet has a functional account with the necessary credentials (having own AES key) allowing it to start interacting with the gcEVM operations, including running smart contracts or performing other actions as needed.

You can fund accounts using the [COTI Faucet](/coti-documentation/networks/testnet/faucet).


# Private Data Types

The gcEVM extends the capabilities of the EVM across various dimensions. Initially, it introduces novel data types to accommodate the necessity of maintaining privacy. Subsequently, it introduces additional operations capable of manipulating these private data types without compromising their secrecy.

## Data Types

To support privacy, we introduce a new set of data types mirroring the existing solidity types.\
We present four new types needed for manipulating private smart contracts:

* Ciphertext (ctBool, ctUint8, ctUint16, ctUint32, ctUint64, ctUint128, ctUint256, ctString)
* Usertext (utBool, utUint8, utUint16, utUint32, utUint64, utUint128, utUint256, utString)
* Inputtext (itBool, itUint8, itUint16, itUint32, itUint64, itUint128, itUint256, itString)
* Garbledtext™ (gtBool, gtUint8, gtUint16, gtUint32, gtUint64, gtUint128, gtUint256, gtString)

We make a distinction between private data types that are used for encrypting the values of transaction inputs, variables stored in storage slots and variables stored in memory. That is, while the Ciphertext and Usertext data types (denoted CT and UT respectively) are used to secure data in storage, we use the Inputtext data type (denoted IT) for protecting data in transaction inputs and the Garbledtext™ data type (denoted GT) for protecting data in use.

{% hint style="info" %}
Detailed examples of these types and converting between them are found in the [**coti-contracts-examples**](https://github.com/coti-io/coti-contracts-examples) repository.
{% endhint %}

**Usage**

* **Ciphertext** is one of two types utilized for storing encrypted data in contract storage. Note that a Ciphertext can be encrypted either with an EOA's AES key or the network key.
* **Usertext** is another type utilized for storing encrypted data in contract storage. It is comprised of two parts:
  * ciphertext: a value of type Ciphertext that is encrypted with the network key
  * userCiphertext: a value of type Ciphertext that is encrypted with an EOA's AES key
* **Inputtext** is used when an EOA performs a transaction with encrypted inputs. It is comprised of two parts:
  * ciphertext: a value of type Ciphertext that is encrypted with an EOA's AES key
  * signature: a signature generated according to the protocol standard using the EOA's private key
* **Garbledtext™** is a data type used for computations on encrypted data or for securely passing encrypted data between contracts. These values are temporary and exist only during the execution of a transaction, being automatically deleted once the transaction completes. Both Inputtext and Ciphertext values can be converted into Garbledtext, and conversely, Garbledtext values can be converted back into Ciphertext for permanent on-chain storage.

## Life-cycle of private data within the gcEVM

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-148a0c61ed55da76bcdb7c38b116ca56dcf9c9cd%2Fgcevm_lifecycle.jpeg?alt=media" alt=""><figcaption></figcaption></figure>

{% hint style="success" %}
It's essential to acknowledge that without a security mechanism in place, there's a risk of potential malicious exploitation of these types. To mitigate such risks, the gcEVM incorporates a security mechanism aimed at preventing any dishonest activities, such as unauthorized copying of secret data. For more information about the security mechanism please refer to the [whitepaper](/coti-documentation/how-coti-works/advanced-topics/white-paper-0.1).
{% endhint %}


# Supported Operations on Private Data Types

Secure operations are supported via our gcEVM extension, which is implemented through a set of precompiled contracts.

To introduce a new private datum into the gcEVM, the function should include an argument of type `Inputtext`. Inside the function, you must call `ValidateCiphertext`, which validates the input and returns a `Garbledtext` if successful or an error otherwise. Note that values of type `Garbledtext` can be used in secure computations, and can also be converted into a `Ciphertext` type by calling `Offboard`. Values of type `Ciphertext` can be stored temporarily in memory or permanently as a state variable. Note that values of type `Ciphertext` cannot be used for secure computations, and therefore must be converted back into a `Garbledtext` type by calling `Onboard` in order to perform additional secure computations on them.

## List of Operations and Their Required Gas

{% hint style="warning" %}
Each operation within the network requires a specific amount of gas. It's important to note that the following gas allocations are intended to be proportional and serve as initial estimates. The final determination of gas requirements will be made at a later stage, taking into account various factors such as network performance, usage patterns, and protocol optimizations.
{% endhint %}

List of arithmetic operations supported using `Garbledtext` and their required Gas units

<table data-full-width="false"><thead><tr><th width="250.84375">Operation</th><th>Bool</th><th>gtUint8</th><th>gtUint16</th><th>gtUint32</th><th>gtUint64</th><th>gtUint128</th><th>gtUint256</th><th>gtString**</th><th data-hidden data-type="number">gtString</th></tr></thead><tbody><tr><td><code>And</code></td><td>12005</td><td>12040</td><td>12080</td><td>12160</td><td>12320</td><td>26126</td><td>52742</td><td></td><td>null</td></tr><tr><td><code>Or</code></td><td>12005</td><td>12042</td><td>12084</td><td>12169</td><td>12339</td><td>26164</td><td>52818</td><td></td><td>null</td></tr><tr><td><code>Xor</code></td><td>12000</td><td>12000</td><td>12001</td><td>12003</td><td>12006</td><td>25498</td><td>51486</td><td></td><td>null</td></tr><tr><td><code>Shl</code></td><td></td><td>12620</td><td>14571</td><td>22467</td><td>54233</td><td>226190</td><td>698489</td><td></td><td>null</td></tr><tr><td><code>Shr</code></td><td></td><td>12969</td><td>15960</td><td>28009</td><td>76377</td><td>248333</td><td>698498</td><td></td><td>null</td></tr><tr><td><code>Add</code></td><td></td><td>12037</td><td>12080</td><td>12167</td><td>12340</td><td>65491</td><td>275564</td><td></td><td>null</td></tr><tr><td><code>CheckedAdd</code></td><td></td><td>26252</td><td>26318</td><td>26407</td><td>26577</td><td>115473</td><td>375554</td><td></td><td>null</td></tr><tr><td><code>CheckedAddWithOverflowBit</code></td><td></td><td>12738</td><td>12781</td><td>12857</td><td>13041</td><td>103457</td><td>389274</td><td></td><td>null</td></tr><tr><td><code>Sub</code></td><td></td><td>12080</td><td>12165</td><td>12337</td><td>12679</td><td>66518</td><td>278659</td><td></td><td>null</td></tr><tr><td><code>CheckedSub</code></td><td></td><td>26341</td><td>26403</td><td>26576</td><td>26917</td><td>142480</td><td>522302</td><td></td><td>null</td></tr><tr><td><code>CheckedSubWithOverflowBit</code></td><td></td><td>12781</td><td>12866</td><td>13038</td><td>13380</td><td>104484</td><td>392369</td><td></td><td>null</td></tr><tr><td><code>Mul</code></td><td></td><td>12620</td><td>14571</td><td>22467</td><td>54233</td><td>2702996</td><td>37473014</td><td></td><td>null</td></tr><tr><td><code>CheckedMul</code></td><td></td><td>26859</td><td>28754</td><td>36684</td><td>68536</td><td>3722546</td><td>50361135</td><td></td><td>null</td></tr><tr><td><code>CheckedMulWithOverflowBit</code></td><td></td><td>13321</td><td>15272</td><td>23168</td><td>54934</td><td>3673338</td><td>50272807</td><td></td><td>null</td></tr><tr><td><code>Div</code></td><td></td><td>12969</td><td>15960</td><td>28009</td><td>76377</td><td></td><td></td><td></td><td>null</td></tr><tr><td><code>Rem</code></td><td></td><td>12969</td><td>15960</td><td>28009</td><td>76377</td><td></td><td></td><td></td><td>null</td></tr><tr><td><code>Min</code></td><td>12010</td><td>12121</td><td>12249</td><td>12503</td><td>13012</td><td>118320</td><td>302289</td><td></td><td>null</td></tr><tr><td><code>Max</code></td><td>12010</td><td>12121</td><td>12249</td><td>12503</td><td>13012</td><td>118986</td><td>303621</td><td></td><td>null</td></tr><tr><td><code>Lt</code></td><td>12005</td><td>12080</td><td>12166</td><td>12337</td><td>12679</td><td>52366</td><td>156113</td><td></td><td>null</td></tr><tr><td><code>Gt</code></td><td>12010</td><td>12121</td><td>12249</td><td>12503</td><td>13012</td><td>53032</td><td>157445</td><td></td><td>null</td></tr><tr><td><code>Ge</code></td><td>12005</td><td>12080</td><td>12165</td><td>12337</td><td>12679</td><td>52699</td><td>157101</td><td></td><td>null</td></tr><tr><td><code>Le</code></td><td>12010</td><td>12122</td><td>12249</td><td>12503</td><td>13012</td><td>52688</td><td>156446</td><td></td><td>null</td></tr><tr><td><code>Eq</code></td><td>12000</td><td>12037</td><td>12079</td><td>12164</td><td>12334</td><td>38650</td><td>89992</td><td>12334</td><td>null</td></tr><tr><td><code>Ne</code></td><td>12000</td><td>12037</td><td>12079</td><td>12164</td><td>12334</td><td>38650</td><td>89992</td><td>12334</td><td>null</td></tr><tr><td><code>Not</code></td><td>12000</td><td></td><td></td><td></td><td></td><td></td><td></td><td></td><td>null</td></tr></tbody></table>

\*\* Note that the gas units listed for the gtString type are for every 8 characters in the string. This is due to the way encrypted strings are represented in memory/storage.

## Other Special Function and Their Required Gas units

{% hint style="info" %}
A more detailed explanation on the functionality of these functions can be found in the [**MPC Core library**](/coti-documentation/build-on-coti/tools/contracts-library/mpc-core).
{% endhint %}

<table><thead><tr><th width="258">Operation</th><th>gtBool</th><th>gtUint8</th><th>gtUint16</th><th>gtUint32</th><th>gtUint64</th><th>gtUint128</th><th>gtUint256</th><th>gtString**</th></tr></thead><tbody><tr><td><code>SetPublic</code></td><td>12000</td><td>12000</td><td>12001</td><td>12003</td><td>12006</td><td>25064</td><td>50591</td><td>12006</td></tr><tr><td><code>Decrypt</code></td><td>12000</td><td>12000</td><td>12001</td><td>12003</td><td>12006</td><td>24969</td><td>50038</td><td>12006</td></tr><tr><td><code>Onboard</code></td><td>47039</td><td>47039</td><td>47039</td><td>47039</td><td>47039</td><td>99442</td><td>199389</td><td>47039</td></tr><tr><td><code>ValidateCiphertext</code></td><td>47039</td><td>47039</td><td>47039</td><td>47039</td><td>47039</td><td>97624</td><td>196779</td><td>47039</td></tr><tr><td><code>Offboard</code></td><td>47039</td><td>47040</td><td>47040</td><td>47042</td><td>47045</td><td>95207</td><td>190885</td><td>47045</td></tr><tr><td><code>OffboardToUser</code></td><td>47039</td><td>47040</td><td>47040</td><td>47042</td><td>47045</td><td>95818</td><td>192112</td><td>47045</td></tr><tr><td><code>OffBoardCombined</code></td><td>94078</td><td>94080</td><td>94080</td><td>94084</td><td>94090</td><td>191485</td><td>384025</td><td>94090</td></tr><tr><td><code>Rand</code></td><td>6000</td><td>6000</td><td>6000</td><td>6000</td><td>6000</td><td>12940</td><td>26319</td><td></td></tr><tr><td><code>RandBoundedBits</code></td><td>6000</td><td>6000</td><td>6000</td><td>6000</td><td>6000</td><td>13191</td><td>26858</td><td></td></tr><tr><td><code>Mux</code></td><td>12005</td><td>12041</td><td>12083</td><td>12166</td><td>12332</td><td>26200</td><td>50038</td><td></td></tr><tr><td><code>Transfer</code></td><td></td><td>12201</td><td>12413</td><td>12837</td><td>13685</td><td>237367</td><td>817758</td><td></td></tr><tr><td><code>TransferWithAllowance</code></td><td></td><td>12301</td><td>12619</td><td>13255</td><td>14527</td><td>395565</td><td>1318764</td><td></td></tr></tbody></table>

\*\* Note that the gas units listed for the gtString type are for every 8 characters in the string. This is due to the way encrypted strings are represented in memory/storage.


# Quickstart

This guide will help you explore the basics of interacting with the COTI network and executing transactions with encrypted inputs. To get started, choose your path ([**Solidity**](#solidity), [**TypeScript**](#typescript) or [**Python**](#python)) and follow the steps listed below.

## Solidity

1. Clone the [**coti-contracts-examples**](https://github.com/coti-io/coti-contracts-examples) repository\\

   ```bash
   git clone https://github.com/coti-io/coti-contracts-examples.git
   ```
2. Navigate to the newly cloned repository directory\\

   ```bash
   cd ./coti-contracts-examples
   ```
3. Install dependencies\\

   ```bash
   npm install
   ```
4. Compile contracts\\

   ```bash
   npx hardhat compile
   ```
5. Run the PrivateToken test script\\

   ```bash
   npm run test-private-token
   ```

   \
   Running this test will automatically create an account and a key/value pair with name: `SIGNING_KEYS` (visible in the .env file). The script will output something like this:\\

   ```bash
   Private Token
       1) "before all" hook in "Private Token"

     0 passing (39ms)
     1 failing

     1) Confidential ERC20
          "before all" hook in "Private Token":
        Error: Created new random account 0x17EDB982c3569D29EbaF407F72aDD05722d5f179.
        Please use faucet to fund it.
   ```

   \
   It is normal to encounter exception of:\
   `Error: Created new random account [...] Please use faucet to fund it.`\
   on the first run, This will be resolved once the account is funded.\\
6. To fund the account, head to the faucet at [**https://faucet.coti.io**](https://faucet.coti.io/) to get Testnet funds.\
   (use <https://discord.coti.io> to join COTI's Discord server)\
   Send the following message to the faucet along with your newly created account, visible in the last part of the error above.\
   format:\
   `testnet <account address>`\
   for example:\
   `testnet` 0x17EDB982c3569D29EbaF407F72aDD05722d5f179\
   The bot will reply with the message:\
   \
   `<username> faucet transferred 10 COTI (testnet)`\\
7. Run the `PrivateToken` test suite once more.\\

   ```bash
   npm run test-private-token
   ```
8. The script output will look like this:\\

   ```bash
   Private Token
   ************* Onboarding user  0x17EDB982c3569D29EbaF407F72aDD05722d5f179  *************
   ************* Onboarding user  0xe1E7315F6970F353661fc84FFd9238133cED3677  *************
   ************* Onboarded! created user key and saved into .env file *************
   ************* Onboarded! created user key and saved into .env file *************
       Deployment
         ✔ Deployed address should not be undefined
         ✔ Owner initial balance (123ms)
         ✔ Function 'name' should be correct (130ms)
         ✔ Function 'symbol' should be correct (123ms)
         ✔ Function 'decimals' should be correct (119ms)
         ✔ Function 'totalSupply' should be correct (117ms)
       Transfer 5
         ✔ Transfer - clear (9469ms)
         ✔ Transfer - Confidential (5260ms)
         ✔ TransferFrom - clear without giving allowance should fail (9905ms)
         ✔ TransferFrom - clear (9770ms)
         ✔ TransferFrom - Confidential (10265ms)
         ✔ Approve/Allowance - Confidential (10255ms)

     12 passing (1m)

   ✨  Done in 69.69s.
   ```

   Running the test suite does the following:

   * **Deploys the `PrivateToken` contract**: Sets up the token with specific details (name, symbol, initial supply).
   * **Tests the deployment**: Verifies the contract address, initial balance, and token details (name, symbol, decimals, total supply).
   * **Tests transfers**: Both clear and confidential transfers, including `transferFrom` functionality with and without prior allowance.
   * **Tests approvals and allowances**: Ensures that the contract correctly handles approvals and allowances, both clear and confidential.
9. You may also run specific tests:\\

   ```
   npm run test-private-nft
   ```

   \
   or\\

   ```
   npm run test-private-token
   ```

   \
   or\\

   ```
   npm run test-private-auction
   ```

   \
   or\\

   ```
   npm run test-private-identity-registry
   ```

   \
   or\\

   ```
   npm run test-on-chain-database
   ```

## TypeScript

1. Clone the [**coti-typescript-examples**](https://github.com/coti-io/coti-typescript-examples) repository\\

   ```bash
   git clone https://github.com/coti-io/coti-typescript-examples.git
   ```
2. Navigate to the [**coti-ethers**](https://github.com/coti-io/coti-typescript-examples/blob/main/coti-ethers/server/README.md) examples subdirectory in the newly cloned repository directory\\

   ```bash
   cd ./coti-typescript-examples/coti-ethers/server
   ```
3. Install dependencies\\

   ```bash
   npm install
   ```
4. Run the `PrivateToken` example\\

   ```bash
   npm run erc20
   ```

   \
   Running this test will automatically create an account and a key/value pair with name: `SIGNING_KEY` (visible in the .env file). The script will output something like this:\\

   ```
   provider: https://testnet.coti.io/rpc
   chainId: 7082400
   latest block: 1294746
   ************* Created new account  0x1Fc537B022ED68f1Be44d59e4382016d976B3389  and saved into .env file *************
   /Users/spencer/Desktop/COTI/coti-typescript-examples/coti-ethers/server/src/util/general-utils.ts:11
           throw new Error(`Please use faucet to fund account ${wallet.address}`)
                 ^
   Error: Please use faucet to fund account 0x1Fc537B022ED68f1Be44d59e4382016d976B3389
   ```

   \
   It is normal to receive the exception `Error: Please use faucet to fund account` on the first run. This will be resolved once the account is funded.\\
5. To fund the account, head to the faucet at [**https://faucet.coti.io**](https://faucet.coti.io/) to get Testnet funds. (use <https://discord.coti.io> to join COTI's Discord server)\
   Send the following message to the faucet along with your newly created account, visible in the last part of the error above.\
   format:\
   `testnet <account address>`\
   for example:\
   `testnet 0x87c13D0f5903a68bE8288E52b23A220CeC6b1aB6`\
   The bot will reply with the message\
   \
   `<username> faucet transferred 10 COTI (testnet)`\\
6. Run the `PrivateToken` script once more\\

   ```bash
   npm run erc20
   ```
7. You may also run other scripts:\\

   ```bash
   npm run nativeTransfer
   ```

   \
   or\\

   ```bash
   npm run dataOnChain
   ```

   \
   or\\

   ```bash
   npm run onChainDatabase
   ```

## Python

1. Clone the [**coti-python-examples**](https://github.com/coti-io/coti-python-examples) repository\\

   ```bash
   git clone https://github.com/coti-io/coti-python-examples.git
   ```
2. Navigate to the [**coti-web3**](https://github.com/coti-io/coti-python-examples/blob/main/coti-web3/README.md) examples subdirectory in the newly cloned repository directory\\

   ```bash
   cd coti-python-examples/coti-web3
   ```
3. Install dependencies\\

   ```bash
   python3 -m pip install -r requirements.txt
   ```
4. Set the python path as following\\

   ```bash
   export PYTHONPATH=$PWD
   ```
5. Run the `native_transfer.py` script\\

   ```bash
   python3 examples/native_transfer.py
   ```

   \
   Running the script will automatically create an account and a key/value pair with name: `ACCOUNT_PRIVATE_KEY` (visible in the `.env` file). The script will output something like this:\\

   ```
   So you dont have an account yet, dont worry... lets create one right now!
   Creation done!
   Traceback (most recent call last):
     File "/Users/user/projects/coti-python-examples/coti-web3/examples/native_transfer.py", line 45, in <module>
       main()
     File "/Users/user/projects/coti-python-examples/coti-web3/examples/native_transfer.py", line 33, in main
       validate_minimum_balance(web3, eoa.address)  # validate minimum balance
     File "/Users/user/projects/coti-python-examples/coti-web3/examples/utils.py", line 43, in validate_minimum_balance
       raise Exception(
   Exception: Not enough balance!, head to discord faucet and get some...https://faucet.coti.io, ask the BOT:testnet 0x78501a70CcADe055D0a1320d640395D505C8Fa97
   ```

   \
   It is normal to receive the exception `Not enough balance!` on the first run. This will be resolved once the account is funded.\\
6. To fund the account, head to the faucet at [**https://faucet.coti.io**](https://faucet.coti.io/) to get Testnet funds. (use <https://discord.coti.io> to join COTI's Discord server)\
   Send the following message to the faucet along with your newly created account, visible in the last part of the error above.\
   format:\
   `testnet <account address>`\
   for example:\
   `testnet 0x87c13D0f5903a68bE8288E52b23A220CeC6b1aB6`\
   The bot will reply with the message\
   \
   `<username> faucet transferred 10 COTI (testnet)`\\
7. Run the `native_transfer.py` script once more\\

   ```
   python3 examples/native_transfer.py
   ```

   \
   The script will output as following:\\

   ```
   AttributeDict({'blockHash': HexBytes('0xa839081ee841355d2c52548f7fcabda9a665dffa779c87d71251a8b242e45e30'), 'blockNumber': 1294736, 'contractAddress': None, 'cumulativeGasUsed': 63000, 'effectiveGasPrice': 30000000000, 'from': '0x78501a70CcADe055D0a1320d640395D505C8Fa97', 'gasUsed': 21000, 'logs': [], 'logsBloom': HexBytes('0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'), 'status': 1, 'to': '0x9E4dC0B018200c56dB0d7d17d620F7B551919BdA', 'transactionHash': HexBytes('0xfe348567258fedd45f539218eb145c5b8bda0ef4bc1cf92bbf81363396086823'), 'transactionIndex': 2, 'type': 0})
   ```
8. Run the `onboard_account.py` script\\

   ```
   python3 examples/onboard.py
   ```
9. You may also run other scripts:\\

   ```bash
   python3 examples/confidential_erc20.py
   ```

   \
   or\\

   ```bash
   python3 examples/data_on_chain.py
   ```


# Guides


# Setting up COTI Snap with your MetaMask wallet

Last updated: May 2026\
Estimated time: 5 minutes\
Requirements: MetaMask (desktop), COTI Mainnet connection

\
Stage 1 — Install the COTI Snap

#### What you’ll need

* MetaMask wallet (desktop browser extension)
* Access to the official COTI Snap install link

Mobile browsers are not supported yet.

#### Step 1 — Connect your wallet

1. Navigate to metamask.coti.io.
2. Click Connect Wallet.
3. MetaMask will open a prompt asking:

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-37b7c226ae26afc3172c0b676b02a158d6969fe9%2Funknown.png?alt=media" alt="" width="337"><figcaption></figcaption></figure>

This step allows MetaMask to enable private token support through the Snap.

#### Step 2 — Install the COTI Snap

1. Click Install COTI MetaMask Snap.
2. If you don’t already have the Snap installed, MetaMask will prompt you automatically.
3. Approve the installation.\
   \\

   <figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-eb3c8c78c0f52fbbafd71fe1f97d3d1de4830943%2Funknown.png?alt=media" alt="" width="365"><figcaption></figcaption></figure>

#### Step 3 — Approve connection

1. MetaMask will ask for permission to connect the Snap to your wallet.
2. Click Approve when prompted.

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-27ec4340202c3d8ff8e709c118776648549f7658%2Funknown.png?alt=media" alt="" width="375"><figcaption></figcaption></figure>

#### Step 4 — Choose your account and connect

Select the account you wish to use with the Snap (recommended: your COTI Mainnet account) and click Connect.

You’ve successfully added the COTI Snap to MetaMask!

### Stage 2 — View your COTI Private Token balances

#### Step 5 — Confirm Snap installation

1. In MetaMask, click your Profile icon → Settings → Snaps.
2. You should now see COTI Snap listed and active.\\

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-2861cdac7f4828e1668803faa1dd3bd431ea9fa4%2Funknown.png?alt=media" alt="" width="375"><figcaption></figcaption></figure>

\\

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-1631f9d000dd81c1bc799f773aba969f15a59d5f%2Funknown.png?alt=media" alt="" width="375"><figcaption></figcaption></figure>

#### Step 6 — Onboard your account

1. In MetaMask, click Onboard Account.
2. MetaMask will open a popup to register your account with the Snap.
3. Click Confirm when prompted.\
   \
   \\

   <figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-fbac08fab86be21cdbccbd7acaa2708d234d73a3%2Funknown.png?alt=media" alt="" width="375"><figcaption></figcaption></figure>

#### Step 7 — Sign and authorize

MetaMask will display a Signature Request popup.\
Click Confirm to authorize the onboarding.\
The message may appear encrypted — this is expected.\
This simply verifies your ownership and uses a small amount of COTI for gas.\
\\

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-b57ab9a676a9b024b2ec40b5c5a8923de57831b2%2Funknown.png?alt=media" alt="" width="375"><figcaption></figcaption></figure>

#### Step 8 — Approve the connection

1. MetaMask will display the request from snap.coti.io on the COTI Mainnet.
2. Click Confirm to proceed.

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-f22ec470c0ee83f656f1c428ad23e17f08093974%2Funknown.png?alt=media" alt="" width="375"><figcaption></figcaption></figure>

#### Step 9 — Grant AES key access

1. The COTI app will now request access to your AES Key — used for decrypting your private balances.
2. Click Request, and when MetaMask pops up, choose Approve.

Without this step, you can hold private tokens, but you won’t be able to view balances.

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-f34c218d5a8c8009b58c250ea0ac84f82d3b7481%2Funknown.png?alt=media" alt="" width="375"><figcaption></figcaption></figure>

#### Step 10 — Launch the dApp

Click Launch dApp to open the COTI Snap application and view your private token balances.\\

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-7ee7606c80be8e65c3f2c6e78b9792e12e6625f3%2Funknown.png?alt=media" alt="" width="326"><figcaption></figcaption></figure>

You’ve completed Stage 2 — your COTI Snap is live and connected!

### Stage 3 — Send and Receive Private Tokens

Private ERC-20 tokens on COTI are not standard ERC-20s — they include encrypted data and special logic.\
Some wallet or explorer behaviors may differ from standard tokens.

#### Step 13 — Import your private token

1. In MetaMask Snap, click Import Tokens.
2. Paste the private token’s contract address
3. Click Next → Import and confirm in MetaMask.\\

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-e40324a6096f2489f96f26cf5d5672dac794c4fa%2Funknown.png?alt=media" alt="" width="375"><figcaption></figcaption></figure>

#### Why MetaMask can show a very large “balance” for private ERC-20 tokens

Private ERC-20 (p.ERC-20) tokens are **interface-compatible** with standard ERC-20 (for example, MetaMask can import them using the contract address). The **on-chain `balanceOf` value is not a human-readable token amount** in the same way as for a normal ERC-20: it encodes **private balance data** (for example, ciphertext or other packed representation).

The main MetaMask extension still treats `balanceOf` like a plain decimal integer. When that value is actually **encrypted or encoded data** (often a large hex-like payload), MetaMask **interprets those bytes as one huge decimal number**. That is why you may see an extremely long number next to the token in the default MetaMask **Assets** list — it is **not** your real spendable balance shown as digits; it is that encoded value displayed as if it were a normal balance.

**Where to see the real private balance:** use the **COTI Snap dApp** (for example via [metamask.coti.io](https://metamask.coti.io)), where the Snap decrypts and displays the correct amount (for example **5 p.USDC.e** in the privacy UI vs. a nonsensical long number in the extension).

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-126aa8a296008fc879026cbf2bbdf63861177534%2Fcoti-snap-metamask-vs-dapp-p-erc20-balance.png?alt=media" alt="Side by side: COTI Snap dApp showing correct private token balance versus MetaMask Assets list showing the same token as a very long decimal interpreted from encoded balance data" width="700"><figcaption><p>COTI Snap dApp (left) shows the decrypted private balance; the MetaMask extension (right) may show a huge number for the same token because it reads <code>balanceOf</code> as a plain ERC-20 amount.</p></figcaption></figure>

#### Step 14 — View token details

In the **COTI Snap dApp**, open the token to see details and the **decrypted** balance.\
The main MetaMask **Assets** list may still show a misleading huge number for private tokens (see the note above); rely on the Snap UI for the amount you can actually use.\\

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-9172ae0ba88809d730065d95696f2d3be1825b83%2Funknown.png?alt=media" alt="" width="375"><figcaption></figcaption></figure>

#### Step 15 — Send your private token

1. Click Send Token in the COTI dApp or Snap interface.
2. Enter the recipient’s address and confirm.
3. Approve the request in MetaMask.

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-f4096436a8bfaaa3d60f8783bf6f16b2a66a7446%2Funknown.png?alt=media" alt="" width="375"><figcaption></figcaption></figure>

The transaction is now live on-chain — encrypted and private.


# Basic Private Smart Contract

This guide will walk you through the process of taking a simple contract that tracks the sum of all numbers passed as arguments to the `add` function, and converting it into a privacy-enabled version.

Copy and paste the following smart contract into a new file `Counter.sol`:

```solidity
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.19;

contract Counter {
    uint64 private _sum;
    
    function sum() public view returns (uint64) {
        return _sum;
    }

    function add(uint64 value) external {
        _sum += value;
    }
}
```

The next thing we will want to do is import the MPC Core library, which will help us make calls to the precompiled contracts that allow us to work with private data types.

```solidity
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.19;

import "@coti-io/coti-contracts/contracts/utils/mpc/MpcCore.sol";

contract Counter {
    uint64 private _sum;
    
    function sum() public view returns (uint64) {
        return _sum;
    }

    function add(uint64 value) external {
        _sum += value;
    }
}
```

Now we are ready to integrate private computations into our smart contract!\
To do this, we can change the types of `_sum` and `value` to their equivalent private data type (`utUint64` and `itUint64` respectively).

```solidity
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.19;

import "@coti-io/coti-contracts/contracts/utils/mpc/MpcCore.sol";

contract Counter {
    utUint64 private _sum;
    
    function sum() public view returns (utUint64) {
        return _sum;
    }

    function add(itUint64 calldata value) external {
        _sum += value;
    }
}
```

If you would try to compile this smart contract, you will encounter a compilation error. This is because `_sum` and `value` are of different types and they do not support the standard Solidity arithmetic operators. Let's update the code so that it uses the appropriate methods provided to us by the MPC Core library.

```solidity
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.19;

import "@coti-io/coti-contracts/contracts/utils/mpc/MpcCore.sol";

contract Counter {
    utUint64 private _sum;
    
    function sum() public view returns (utUint64) {
        return _sum;
    }

    function add(itUint64 calldata value) external {
        gtUint64 value_ = MpcCore.validateCiphertext(value);
        gtUint64 sum_ = MpcCore.onBoard(_sum.ciphertext);
        
        sum_ = MpcCore.add(sum_, value_);
        
        _sum = MpcCore.offBoardCombined(sum_, msg.sender);
    }
}
```

This code will compile successfully, but if we were to deploy it and then try to call `add`, we would find that the transaction would be reverted. This is because by default, the storage slot used by the `_sum` state variable is initialized to zero. This causes an error when we try to onboard it into the gcEVM, since it was not encrypted using the network AES key. Let's fix the issue by setting the value of `_sum` inside of the constructor.

```solidity
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.19;

import "@coti-io/coti-contracts/contracts/utils/mpc/MpcCore.sol";

contract Counter {
    utUint64 private _sum;
    
    constructor() {
        gtUint64 sum_ = MpcCore.setPublic64(0);
        
        _sum = MpcCore.offBoardCombined(sum_, msg.sender);
    }

    function sum() public view returns (utUint64) {
        return _sum;
    }

    function add(itUint64 calldata value) external {
        gtUint64 value_ = MpcCore.validateCiphertext(value);
        gtUint64 sum_ = MpcCore.onBoard(_sum.ciphertext);
        
        sum_ = MpcCore.add(sum_, value_);
        
        _sum = MpcCore.offBoardCombined(sum_, msg.sender);
    }
}
```

Lastly, since values that are encrypted with the network AES key are not very useful to dApps and users, we will update the `sum` function to return only the value encrypted with the user's AES key.

```solidity
// SPDX-License-Identifier: MIT

pragma solidity ^0.8.19;

import "@coti-io/coti-contracts/contracts/utils/mpc/MpcCore.sol";

contract Counter {
    utUint64 private _sum;
    
    constructor() {
        gtUint64 sum_ = MpcCore.setPublic64(0);
        
        _sum = MpcCore.offBoardCombined(sum_, msg.sender);
    }

    function sum() public view returns (ctUint64) {
        return _sum.userCiphertext;
    }

    function add(itUint64 calldata value) external {
        gtUint64 value_ = MpcCore.validateCiphertext(value);
        gtUint64 sum_ = MpcCore.onBoard(_sum.ciphertext);
        
        sum_ = MpcCore.add(sum_, value_);
        
        _sum = MpcCore.offBoardCombined(sum_, msg.sender);
    }
}
```

In order to deploy the contract on the COTI network, you may consider using our [**Remix Plugin**](/coti-documentation/build-on-coti/tools/remix-plugin) or our [**Hardhat Template**](/coti-documentation/build-on-coti/tools/hardhat).


# Account Onboard

{% hint style="info" %}
This guide assumes you have already deployed the Counter.sol contract. If you haven't already, check out our guide for deploying a [Basic Private Smart Contract](/coti-documentation/build-on-coti/guides/basic-private-smart-contract).
{% endhint %}

In order to interact with the `Counter.sol` smart contract from the previous section, we are going to need to acquire our AES encryption key. To do so, we can use either of COTI's [**Ethers**](/coti-documentation/build-on-coti/tools/ethers.js) or [**web3.py**](/coti-documentation/build-on-coti/tools/web3.py) packages to complete the [Account Onboarding Procedure](/coti-documentation/build-on-coti/core-concepts/onboard-user).

{% tabs %}
{% tab title="TypeScript (Server)" %}
**Setup**

```bash
npm install @coti-io/coti-ethers
```

**Code**

```typescript
import { CotiNetwork, getDefaultProvider, Wallet } from "@coti-io/coti-ethers"

const PRIVATE_KEY = "<EOA_PRIVATE_KEY>"

const provider = getDefaultProvider(CotiNetwork.Testnet)
const wallet = new Wallet(PRIVATE_KEY, provider)

await wallet.generateOrRecoverAes()

console.log(wallet.getUserOnboardInfo()?.aesKey)
```

{% endtab %}

{% tab title="TypeScript (Browser)" %}
Setup

```bash
npm install @coti-io/coti-ethers
```

**Code**

```typescript
import { BrowserProvider, Eip1193Provider, JsonRpcSigner } from '@coti-io/coti-ethers'

const provider = new BrowserProvider(window.ethereum as Eip1193Provider)
const signer = await provider.getSigner()
await signer.generateOrRecoverAes()

console.log(signer.getUserOnboardInfo()?.aesKey)
```

{% endtab %}

{% tab title="Python" %}
**Setup**

```bash
pip install coti-web3
```

**Code**

```python
from eth_account import Account
from eth_account.signers.local import LocalAccount
from web3 import Web3
from web3.utils.coti import (
  CotiNetwork,
  generate_or_recover_aes,
  init_web3
)

PRIVATE_KEY = "<EOA_PRIVATE_KEY>"

account: LocalAccount = Account.from_key(PRIVATE_KEY)
w3: Web3 = init_web3(CotiNetwork.TESTNET)

generate_or_recover_aes(w3, account)

print(account.aes_key)
```

{% endtab %}
{% endtabs %}


# Sending a Transaction with Encrypted Inputs

{% hint style="info" %}
This guide assumes you have already deployed the Counter.sol contract. If you haven't already, check out our guide for deploying a [Basic Private Smart Contract](/coti-documentation/build-on-coti/guides/basic-private-smart-contract).
{% endhint %}

Now that we have acquired our AES encryption key, we can use it to encrypt the arguments for calling the `add` function of our `Counter.sol` contract.

{% tabs %}
{% tab title="TypeScript (Server)" %}
**Setup**

```bash
npm install @coti-io/coti-ethers
```

**Code**

<pre class="language-typescript"><code class="lang-typescript">import { Contract, CotiNetwork, getDefaultProvider, Wallet } from "@coti-io/coti-ethers"

<strong>const PRIVATE_KEY = "&#x3C;EOA_PRIVATE_KEY>"
</strong>const AES_KEY = "&#x3C;AES_KEY>"
const COUNTER_ADDRESS = "&#x3C;COUNTER_ADDRESS>"
const COUNTER_ABI = [
    {
        "inputs": [],
        "stateMutability": "nonpayable",
        "type": "constructor"
    },
    {
        "inputs": [
            {
                "components": [
                    {
                        "internalType": "ctUint64",
                        "name": "ciphertext",
                        "type": "uint256"
                    },
                    {
                        "internalType": "bytes",
                        "name": "signature",
                        "type": "bytes"
                    }
                ],
                "internalType": "struct itUint64",
                "name": "value",
                "type": "tuple"
            }
        ],
        "name": "add",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "sum",
        "outputs": [
            {
                "internalType": "ctUint64",
                "name": "",
                "type": "uint256"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    }
]

const provider = getDefaultProvider(CotiNetwork.Testnet)
const wallet = new Wallet(PRIVATE_KEY, provider)
wallet.setAesKey(AES_KEY)

const counter = new Contract(COUNTER_ADDRESS, COUNTER_ABI, wallet)

const itValue = await wallet.encryptValue(
    123n,
    COUNTER_ADDRESS,
    counter.add.fragment.selector
)

await (
    await counter.add(itValue)
).wait()
</code></pre>

{% endtab %}

{% tab title="TypeScript (Browser)" %}
**Setup**

```bash
npm install @coti-io/coti-ethers
```

**Code**

```typescript
import { BrowserProvider, Contract, Eip1193Provider, JsonRpcSigner } from '@coti-io/coti-ethers'

const AES_KEY = "<AES_KEY>"
const COUNTER_ADDRESS = "<COUNTER_ADDRESS>"
const COUNTER_ABI = [
    {
        "inputs": [],
        "stateMutability": "nonpayable",
        "type": "constructor"
    },
    {
        "inputs": [
            {
                "components": [
                    {
                        "internalType": "ctUint64",
                        "name": "ciphertext",
                        "type": "uint256"
                    },
                    {
                        "internalType": "bytes",
                        "name": "signature",
                        "type": "bytes"
                    }
                ],
                "internalType": "struct itUint64",
                "name": "value",
                "type": "tuple"
            }
        ],
        "name": "add",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "sum",
        "outputs": [
            {
                "internalType": "ctUint64",
                "name": "",
                "type": "uint256"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    }
]

const provider = new BrowserProvider(window.ethereum as Eip1193Provider)
const signer = await provider.getSigner()
signer.setAesKey(AES_KEY)

const counter = new Contract(COUNTER_ADDRESS, COUNTER_ABI, signer)

const itValue = await signer.encryptValue(
    BigInt(123),
    COUNTER_ADDRESS,
    counter.add.fragment.selector
)

await (
    await counter.add(itValue)
).wait()
```

{% endtab %}

{% tab title="Python" %}
**Setup**

```bash
pip install coti-web3
```

**Code**

```python
from eth_account import Account
from eth_account.signers.local import LocalAccount
from web3 import Web3
from web3.utils.coti import (
  CotiNetwork,
  init_web3
)

PRIVATE_KEY = "<EOA_PRIVATE_KEY>"
AES_KEY = "<AES_KEY>"
COUNTER_ADDRESS = "<COUNTER_ADDRESS>"
COUNTER_ABI = [
    {
        "inputs": [],
        "stateMutability": "nonpayable",
        "type": "constructor"
    },
    {
        "inputs": [
            {
                "components": [
                    {
                        "internalType": "ctUint64",
                        "name": "ciphertext",
                        "type": "uint256"
                    },
                    {
                        "internalType": "bytes",
                        "name": "signature",
                        "type": "bytes"
                    }
                ],
                "internalType": "struct itUint64",
                "name": "value",
                "type": "tuple"
            }
        ],
        "name": "add",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "sum",
        "outputs": [
            {
                "internalType": "ctUint64",
                "name": "",
                "type": "uint256"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    }
]

account: LocalAccount = Account.from_key(PRIVATE_KEY, { 'aes_key': AES_KEY })
w3: Web3 = init_web3(CotiNetwork.TESTNET)

counter_contract = w3.eth.contract(address=COUNTER_ADDRESS, abi=COUNTER_ABI)

it_value = account.encrypt_value(
    123,
    counter_contract.address,
    counter_contract.functions.add(value=(0, bytes(65))).selector
)

tx = counter_contract.functions.add(it_value).build_transaction({
    'from': account.address,
    'chainId': w3.eth.chain_id,
    'nonce': w3.eth.get_transaction_count(account.address),
    'gas': 15000000,
    'gasPrice': w3.to_wei(30, 'gwei')
})

signed_tx = w3.eth.account.sign_transaction(tx, PRIVATE_KEY)

tx_hash = w3.eth.send_raw_transaction(signed_tx.raw_transaction)

w3.eth.wait_for_transaction_receipt(tx_hash)
```

{% endtab %}
{% endtabs %}


# Resolving a Transaction's Encrypted Outputs

{% hint style="info" %}
This guide assumes you have already deployed the Counter.sol contract. If you haven't already, check out our guide for deploying a [Basic Private Smart Contract](/coti-documentation/build-on-coti/guides/basic-private-smart-contract).
{% endhint %}

After calling `add`, we can use our AES encryption key to decrypt the value of `sum` and check the result.

{% tabs %}
{% tab title="TypeScript (Server)" %}
**Setup**

```bash
npm install @coti-io/coti-ethers
```

**Code**

```typescript
import { Contract, CotiNetwork, getDefaultProvider, Wallet } from "@coti-io/coti-ethers"

const PRIVATE_KEY = "<EOA_PRIVATE_KEY>"
const AES_KEY = "<AES_KEY>"
const COUNTER_ADDRESS = "<COUNTER_ADDRESS>"
const COUNTER_ABI = [
    {
        "inputs": [],
        "stateMutability": "nonpayable",
        "type": "constructor"
    },
    {
        "inputs": [
            {
                "components": [
                    {
                        "internalType": "ctUint64",
                        "name": "ciphertext",
                        "type": "uint256"
                    },
                    {
                        "internalType": "bytes",
                        "name": "signature",
                        "type": "bytes"
                    }
                ],
                "internalType": "struct itUint64",
                "name": "value",
                "type": "tuple"
            }
        ],
        "name": "add",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "sum",
        "outputs": [
            {
                "internalType": "ctUint64",
                "name": "",
                "type": "uint256"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    }
]

const provider = getDefaultProvider(CotiNetwork.Testnet)
const wallet = new Wallet(PRIVATE_KEY, provider)
wallet.setAesKey(AES_KEY)

const counter = new Contract(COUNTER_ADDRESS, COUNTER_ABI, wallet)

const ctSum = await counter.sum()

const clearSum = await wallet.decryptValue(ctSum)

console.log(clearSum)
```

{% endtab %}

{% tab title="TypeScript (Browser)" %}
**Setup**

```bash
npm install @coti-io/coti-ethers
```

**Code**

```typescript
import { Contract, CotiNetwork, getDefaultProvider, Wallet } from "@coti-io/coti-ethers"

const PRIVATE_KEY = "<EOA_PRIVATE_KEY>"
const AES_KEY = "<AES_KEY>"
const COUNTER_ADDRESS = "<COUNTER_ADDRESS>"
const COUNTER_ABI = [
    {
        "inputs": [],
        "stateMutability": "nonpayable",
        "type": "constructor"
    },
    {
        "inputs": [
            {
                "components": [
                    {
                        "internalType": "ctUint64",
                        "name": "ciphertext",
                        "type": "uint256"
                    },
                    {
                        "internalType": "bytes",
                        "name": "signature",
                        "type": "bytes"
                    }
                ],
                "internalType": "struct itUint64",
                "name": "value",
                "type": "tuple"
            }
        ],
        "name": "add",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "sum",
        "outputs": [
            {
                "internalType": "ctUint64",
                "name": "",
                "type": "uint256"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    }
]

const provider = new BrowserProvider(window.ethereum as Eip1193Provider)
const signer = await provider.getSigner()
signer.setAesKey(AES_KEY)

const counter = new Contract(COUNTER_ADDRESS, COUNTER_ABI, signer)

const ctSum = await counter.sum()

const clearSum = await signer.decryptValue(ctSum)

console.log(clearSum)
```

{% endtab %}

{% tab title="Python" %}
**Setup**

```bash
pip install coti-web3
```

**Code**

```python
from eth_account import Account
from eth_account.signers.local import LocalAccount
from web3 import Web3
from web3.utils.coti import (
  CotiNetwork,
  init_web3
)

PRIVATE_KEY = "<EOA_PRIVATE_KEY>"
AES_KEY = "<AES_KEY>"
COUNTER_ADDRESS = "<COUNTER_ADDRESS>"
COUNTER_ABI = [
    {
        "inputs": [],
        "stateMutability": "nonpayable",
        "type": "constructor"
    },
    {
        "inputs": [
            {
                "components": [
                    {
                        "internalType": "ctUint64",
                        "name": "ciphertext",
                        "type": "uint256"
                    },
                    {
                        "internalType": "bytes",
                        "name": "signature",
                        "type": "bytes"
                    }
                ],
                "internalType": "struct itUint64",
                "name": "value",
                "type": "tuple"
            }
        ],
        "name": "add",
        "outputs": [],
        "stateMutability": "nonpayable",
        "type": "function"
    },
    {
        "inputs": [],
        "name": "sum",
        "outputs": [
            {
                "internalType": "ctUint64",
                "name": "",
                "type": "uint256"
            }
        ],
        "stateMutability": "view",
        "type": "function"
    }
]

account: LocalAccount = Account.from_key(PRIVATE_KEY, { 'aes_key': AES_KEY })
w3: Web3 = init_web3(CotiNetwork.TESTNET)

counter_contract = w3.eth.contract(address=COUNTER_ADDRESS, abi=COUNTER_ABI)

ct_sum = counter_contract.functions.sum().call({'from': account.address})

clear_sum = account.decrypt_value(ct_sum)

print(clear_sum)
```

{% endtab %}
{% endtabs %}


# Writing a Private Smart Contract

To start using the privacy features of the COTI platform effectively, keep the following in mind when writing contracts:

1. **Understand Privacy Features**: Familiarize yourself with the privacy features offered by our blockchain platform. Explore how secret data types operate to ensure confidentiality.
2. **Define Contract Requirements**: Clearly outline the requirements and functionality of your smart contract. Identify the specific use case and consider how privacy features can enhance security and confidentiality.
3. **Implement Secret Data Types**: Utilize the provided secret data types defined in the [**MPC Core**](/coti-documentation/build-on-coti/tools/contracts-library/mpc-core) library to protect sensitive information within your smart contract.
4. **Develop Scripts or dApp Frontend**: Begin by developing scripts or a decentralized application (dApp) frontend using languages like Python, JavaScript, or relevant frameworks using the provided SDKs. These scripts or frontend interfaces will facilitate the execution of your smart contract's functionality while ensuring privacy and confidentiality are maintained.
5. **Test and Validate**: Thoroughly test and validate your smart contract to ensure that privacy features are functioning correctly. Test various scenarios to verify the security and privacy of sensitive data handling.
6. **Deploy and Monitor**: Deploy your smart contract onto the blockchain platform and monitor its performance in a real-world environment. Continuously monitor for security vulnerabilities or privacy leaks and address them promptly to maintain confidentiality.

By following these instructions, contract writers can effectively leverage the privacy features of our blockchain platform to develop secure and confidential smart contracts tailored to their specific use cases.


# Dos and Don'ts

In handling confidential smart contracts, it's essential to understand the intricacies involved in ensuring the security and integrity of sensitive data on the blockchain. Properly managing private information requires adherence to avoidance of common pitfalls. In the following section, we'll explore key guidelines and recommendations for developers to follow when working with private smart contracts, covering aspects such as type usage, data handling, and contract design


# Proper Use of Types

Never save garbledtext in storage

## Never save `garbledtext` in storage

`Garbledtext` is an intermediate random value, It's crucial to understand that these values are transient, existing only for the duration of the transaction's execution and promptly deleted once the execution is completed. Storing such a type will cause a loss of data as it has no meaning once the execution is completed.\\

<mark style="color:red;">**Don't**</mark>

```solidity
contract BadContract {
  gtUint64 private storedGarbledText;

  function setZero() public{
    storedGarbledText = mpcCore.SetPublic64(0);
  }
}  
```

<mark style="color:blue;">**Do**</mark>

```solidity
contract GoodContract {
  ctUint64 private storedCipherText;
                                                
  function setZero() public {
    gtUint64 gtZero = mpcCore.SetPublic64(0);
    storedCipherText = mpcCore.offboard(gtZero);  
  }
}
```

## Never Pass `Ciphertext` as Parameter to Another Contract or As User Input

Sending a `ciphertext` from an Externally Owned Account (EOA) will fail unless it is accompanied by the correct signature to pass validation. Moreover, transmitting ciphertext between different contracts renders it invalid within the scope of the receiving contract and thus ineffective.

<mark style="color:red;">**Don't**</mark>

```solidity
contract BadContract {
  ctUint8 private ctVal;
  
  function passCiphertext(ctUint8 val) public {
   ctVal = val;
  }
}
```

<mark style="color:blue;">**Do**</mark>

```solidity
contract GoodContract {
  ctUint8 private ctDouble;
  
  function passGarbledtext(gtUint8 val) public {
   gtUint8 gtDouble = MpcCore.add(val, val);
   ctDouble = MpcCore.offBoard(gtDouble)
  }
}
```

<mark style="color:blue;">**Or**</mark>

```solidity
contract GoodContract {
  ctUint8 private ct;
  
  function passInputtext(ctUint64 _itCT, bytes calldata _itSignature) public {
    itUint64 memory it;
    it.ciphertext = _itCT;
    it.signature = _itSignature;
    // validate the IT and
    gtUserInput = MpcCore.validateCiphertext(it);
    ct = MpcCore.offBoard(gtUserInput )
  }
}
```

## Never Return a `Ciphertext` to Another Contract

Returning a `ciphertext` for future use in a different contract is not feasible, rendering such an action pointless. However, it is logical to return such a type to an Externally Owned Account (EOA) if it was specifically designated for that particular user.

<mark style="color:red;">**Don't : do not return system encrypted ciphertext**</mark>

```solidity
contract BadContract {
  ctUint8 private ctVal;
  
  function passCiphertext(ctUint8 val) public returns (ctUint16){
   return ctVal;
  }
}
```

<mark style="color:blue;">**Do**</mark> <mark style="color:blue;">**: return ciphertext to a specific user**</mark>

<pre class="language-solidity"><code class="lang-solidity"><strong>contract GoodContract {
</strong>  function balanceOf() public returns (ctUint64 balance){
    ctUint64 balance = balances[msg.sender];
    // The balance is saved encrypted using the system key. However, to allow 
    // the user to access it, the balance needs to be re-encrypted using the user key. 
    // Therefore, we decrypt the balance (onBoard) and then encrypt it again using 
    // the user key (offBoardToUser).
    gtUint64 balanceGt = MpcCore.onBoard(balance);
    return MpcCore.offBoardToUser(balanceGt, msg.sender);
  }
}
</code></pre>

<mark style="color:blue;">**Do**</mark> <mark style="color:blue;">**: return**</mark><mark style="color:blue;">\*\*</mark> `garbledtext` \*\*<mark style="color:blue;">**to a contract**</mark>

```solidity
contract GoodContract {
  function contractTransfer(address _to, gtUint64 _value) public returns (gtBool success){
    (gtUint64 fromBalance, gtUint64 toBalance) = getBalances(msg.sender, _to);
    (gtUint64 newFromBalance, gtUint64 newToBalance, gtBool result) = MpcCore.transfer(fromBalance, toBalance, _value);
  
    emit Transfer(msg.sender, _to);
    setNewBalances(msg.sender, _to, newFromBalance, newToBalance);
  
    return result;
   }
}
```


# No Constant/Immutable Secret Types

In Solidity, `constant` variables are evaluated at compile-time and replaced with their respective values in the bytecode of the contract. On the other hand, `immutable` variables are set at contract deployment and cannot be changed thereafter. In both cases, these scenarios prevent them from being recognized by our security mechanism, rendering them invalid.

<mark style="color:red;">**Don't**</mark><mark style="color:red;">: use constant\immutable secret types.</mark>

```solidity
contract BadContract {
  ctUint32 private constant constVal = MpcCore.setPublic32(5);
  ctUint32 private immutable immutableVal;

  constructor(uint32 _val) {
    immutableVal = MpcCore.setPublic32(_val);
  }
}
```

<mark style="color:blue;">**Do**</mark> <mark style="color:blue;">: Simply remove the constant\immutable keywords</mark>


# No Public Contract Variables

Public state variables are inherently accessible to other contracts for manipulation. If a different contract modifies these values, they may not undergo the necessary validation process. Consequently, such modifications may result in an invalid ciphertext.

<mark style="color:red;">**Don't**</mark><mark style="color:red;">: Do not use</mark> <mark style="color:red;">`public`</mark> <mark style="color:red;">secret types.</mark>

```solidity
contract BadContract {
  ctUint32 public bad;
  //....
}
```

<mark style="color:blue;">**Do**</mark> <mark style="color:blue;">: Remove the</mark> <mark style="color:blue;">`public`</mark> <mark style="color:blue;">keyword and either add</mark> <mark style="color:blue;">`private`</mark> <mark style="color:blue;">or retain the default</mark> <mark style="color:blue;">`internal`</mark><mark style="color:blue;">.</mark>


# Best Practices

As developers, it's crucial to adopt best practices that not only protect confidential information but also maintain the integrity of the confidential smart contracts. In this section, we'll explore key recommendations aimed at enhancing the security and efficiency of smart contracts handling confidential data. From understanding the implications of decryption to optimizing array access and safeguarding against arithmetic overflows, these best practices offer valuable insights into building robust and privacy-preserving smart contracts


# Careful Onboarding

<mark style="color:red;">**Don't: Do not call**</mark> <mark style="color:red;">`onBoard`</mark> <mark style="color:red;">**on a value that has not previously had**</mark> <mark style="color:red;">`offBoard`</mark> <mark style="color:red;">**called on it**</mark>

```solidity
contract BadContract {
  ctUint64 public sum;
  //...
  function addWithoutCarefulOnboard() public {
    gtUint64 a = MpcCore.setPublic64(1);
    gtUint64 sum_ = MpcCore.onBoard(sum); // THIS WILL REVERT
   
    sum_ = MpcCore.add(sum_, a);
    
    sum = MpcCore.offBoard(sum_);
  }
  //...
}
```

<mark style="color:blue;">**Do:**</mark> <mark style="color:blue;">Initialize the value by calling</mark> <mark style="color:blue;">`offBoard`</mark> <mark style="color:blue;">inside the constructor</mark>

```solidity
contract GoodContract {
  ctUint64 public sum;
  
  constructor() {
    // By initializing sum with an encrypted value, we ensure
    // calling onBoard on sum does not revert
    gtUint64 sum_ = MpcCore.setPublic64(0);
    sum = MpcCore.offBoard(sum_);
  }
  //...
  function add() public {
    gtUint64 a = MpcCore.setPublic64(1);
    gtUint64 sum_ = MpcCore.onBoard(sum);
   
    sum_ = MpcCore.add(sum_, a);
    
    sum = MpcCore.offBoard(sum_);
  }
  //...
}
```

<mark style="color:blue;">**Or:**</mark> <mark style="color:blue;">Check if the value is zero before calling</mark> <mark style="color:blue;">`onBoard`</mark>

<pre class="language-solidity"><code class="lang-solidity"><strong>contract GoodContract {
</strong>  ctUint64 public sum;
  //...
  function addWithCarefulOnboard() public {
    gtUint64 a = MpcCore.setPublic64(1);
    gtUint64 sum_;
    
    // By checking if sum is equal to zero, we can verify whether
    // or not it is safe to call onBoard on sum
    if (ctUint64.unwrap(sum) == 0) {
        sum_ = MpcCore.setPublic64(0);
    } else {
        sum_ = MpcCore.onBoard(sum);
    }
   
    sum_ = MpcCore.add(sum_, a);
    
    sum = MpcCore.offBoard(sum_);
  }
  //...
}
</code></pre>


# Careful Decrypting

The creator of a contract must understand the implications of decryption in various scenarios. For instance, decryption should never occur within a view function without considering alternative security measures, as relying solely on `msg.sender` can be unreliable due to the potential for forging.

As a general precaution, handling a call to decrypt should be approached with caution, considering the various options for invoking such a function. For example, a secret intended for a specific user should not be decrypted in a manner that exposes it to everyone.

<mark style="color:red;">**Don't: Do not reveal data intended for a specific user to everybody**</mark>

```solidity
contract BadContract {
  //...
  function balanceOf() public returns (uint64 balance){
    ctUint64 balance = balances[msg.sender];
   
    gtUint64 balanceGt = MpcCore.onBoard(balance);
    
    // SHOULD NEVER BE CALLED
    // A SIMPLE CALL TO GETBALANCE REVEALS THE BALANCE TO EVERYBODY
    return MpcCore.decrypt(balanceGt);
  }
  //...
}
```

<mark style="color:blue;">**Do**</mark> <mark style="color:blue;">:</mark> <mark style="color:blue;">`Offboard`</mark> <mark style="color:blue;">to a specific user.</mark>

<pre class="language-solidity"><code class="lang-solidity"><strong>contract GoodContract {
</strong><strong>   //...
</strong>  function balanceOf() public returns (ctUint64 balance){
    ctUint64 balance = balances[msg.sender];
    // The balance is saved encrypted using the system key. However, to allow 
    // the user to access it, the balance needs to be re-encrypted using the user key. 
    // Therefore, we decrypt the balance (onBoard) and then encrypt it again using 
    // the user key (offBoardToUser).
    gtUint64 balanceGt = MpcCore.onBoard(balance);
    return MpcCore.offBoardToUser(balanceGt, msg.sender);
  }
  //...
}
</code></pre>


# Don't loop over an array without an index

Utilizing encrypted indexes to select an element from an array without disclosing it is inefficient. This approach requires looping over all indexes and comparing for equality secretly, which significantly increases gas costs and should be avoided whenever possible.

<mark style="color:red;">**Avoid**</mark>

```solidity
contract AvoidContract{
    ctUint32 data;
    ctUint32[] secretArray;
    
    function setDataAtSecretIndex(gtUint index) public {
        //...
        gtUint32 gtData=0;
        for (uint32 i = 0; i < secretArray.length; i++) {
            gtAti = MpcCore.onboard(secretArray[i])
            if (i==0){
                gtData = gtAti;
            }
            gtBool isEqual = MpcCore.eq(index, i);
            gtData = MpcCore.mux(isEqual, gtAti , gtData);
        }
        if(gtData!=0) 
            data = MpcCore.offboard(gtData)
    }
}
```

<mark style="color:blue;">**Do**</mark> <mark style="color:blue;">: You should aim for clear indexing when accessing secret arrays.</mark>


# Check Overflow

Extra caution should be exercised when performing addition, subtraction, or division operations. For instance, it's important to verify that an overflow hasn't occurred in cases where it could potentially happen.

<mark style="color:red;">**Avoid:**</mark>

```solidity
contract AvoidContract {
    function addinGtWithoutChecking(gtUint16 lhs, gtUint16 rhs) public {
        gtUint16 addResult = MpcCore.add(lhs, rhs);
        return addResult;
    }
}
```

<mark style="color:blue;">**Do**</mark> <mark style="color:blue;">:</mark> <mark style="color:blue;">Check that the result is greater than one of the operands, return zero for example on overflow</mark>

```solidity
contract AvoidContract {
    function addinGtReturnZeroOnOverFlow(gtUint16 lhs, gtUint16 rhs) public {
        gtUint16 tempAddResult = MpcCore.add(lhs, rhs);
        gtBool isOverflowed = MpcCore.lt(tempAddResult , lhs);
        gtUint16 gtZero = MpcCore.setPublic16(0)
        addResult = MpcCore.mux(isOverflow, gtZero, tempAddResult);
        return addResult;
    }
}
```

{% hint style="info" %}
You may also use the `checkedAdd`, `checkedSub` and `checkedMul` operands, which cause the transaction to revert on overflow/underflow.
{% endhint %}


# Tools

Developer tools for building on COTI.

## SDKs and libraries

* [TypeScript SDK](/coti-documentation/build-on-coti/tools/typescript-sdk)
* [Ethers.js](/coti-documentation/build-on-coti/tools/ethers.js)
* [Python SDK](/coti-documentation/build-on-coti/tools/python-sdk)
* [Web3.py](/coti-documentation/build-on-coti/tools/web3.py)
* [Contracts Library](/coti-documentation/build-on-coti/tools/contracts-library)

## IDE and wallet plugins

* [Hardhat](/coti-documentation/build-on-coti/tools/hardhat)
* [Remix Plugin](/coti-documentation/build-on-coti/tools/remix-plugin)
* [COTI MetaMask Snap](/coti-documentation/build-on-coti/tools/coti-metamask-snap)
* [COTI Wallet Plugin](/coti-documentation/build-on-coti/tools/coti-wallet-plugin) — React library for adding private token support to wagmi/RainbowKit dApps

## Other

* [Developer Sandbox](/coti-documentation/build-on-coti/tools/developer-sandbox)


# TypeScript SDK

The [**COTI Typescript SDK**](https://github.com/coti-io/coti-sdk-typescript) provides a set of encryption, decryption, and cryptographic utilities, including RSA and AES encryption, message signing, and key handling functions. The utilities are primarily designed to work with cryptographic operations for secure communication and message signing, particularly within Ethereum smart contracts or similar environments.

## Features

* **AES encryption** with ECB mode for data of fixed block sizes.
* **RSA key pair generation**, encryption, and decryption using RSA-OAEP with SHA-256.
* **Signing of Ethereum transactions** using the `ethers` library's signing mechanisms.
* Utilities for encoding/decoding, padding, and cryptographic data manipulation.

## Clone, Build & Test

Full instructions for building and testing the package locally are available in the [coti-sdk-typescript GitHub repository](https://github.com/coti-io/coti-sdk-typescript)

## Installation

```bash
npm install @coti-io/coti-sdk-typescript
```

{% hint style="info" %}
SDK versions < 1.0.0 are for use with the Devnet, versions >= 1.0.0 are for use with the Testnet
{% endhint %}

## Usage

See the [**coti-typescript-examples**](https://github.com/coti-io/coti-typescript-examples) repository for code examples.

## Functions

```typescript
function encrypt(key: Uint8Array, plaintext: Uint8Array): { ciphertext: Uint8Array; r: Uint8Array }
```

Encrypts a given plaintext using the provided AES key. The plaintext is XORed with an encrypted random value.

* **Parameters:**
  * `key`: The AES encryption key (16 bytes).
  * `plaintext`: The data to be encrypted (must be 16 bytes or smaller).
* **Returns:** An object containing:
  * `ciphertext`: The encrypted data.
  * `r`: The random value used in the encryption process.

```typescript
function decrypt(key: Uint8Array, r: Uint8Array, ciphertext: Uint8Array): Uint8Array
```

Decrypts a ciphertext using the provided AES key and random value `r`.

* **Parameters:**
  * `key`: The AES encryption key (16 bytes).
  * `r`: The random value used during encryption (16 bytes).
  * `ciphertext`: The encrypted data (16 bytes).
* **Returns:** The decrypted plaintext.

<pre class="language-typescript"><code class="lang-typescript"><strong>function generateRSAKeyPair(): { publicKey: Uint8Array; privateKey: Uint8Array }
</strong></code></pre>

Generates a new RSA key pair (2048 bits) and returns the keys in DER format.

* **Returns:** An object containing:
  * `publicKey`: The RSA public key (DER-encoded).
  * `privateKey`: The RSA private key (DER-encoded).

```typescript
function decryptRSA(privateKey: Uint8Array, ciphertext: string): string
```

Decrypts an RSA-encrypted ciphertext using the provided private key.

* **Parameters:**
  * `privateKey`: The RSA private key (DER-encoded).
  * `ciphertext`: The encrypted ciphertext as a hex string.
* **Returns:** The decrypted message as a string.

```typescript
function sign(message: string, privateKey: string): Uint8Array
```

Signs a message using the provided Ethereum private key.

* **Parameters:**
  * `message`: The message to be signed.
  * `privateKey`: The Ethereum private key.
* **Returns:** A signature as a `Uint8Array` containing `r`, `s`, and `v` values.

```typescript
function signInputText(sender, contractAddress, functionSelector, ct: bigint): Uint8Array
```

Generates a signed message hash for Ethereum contract interactions.

* **Parameters:**
  * `sender`: The sender's information containing their wallet and user key.
  * `contractAddress`: The Ethereum contract address.
  * `functionSelector`: The function selector (bytes4) for the contract function.
  * `ct`: The ciphertext (big integer).
* **Returns:** A signature for the provided message.

```typescript
function buildInputText(plaintext: bigint, sender, contractAddress, functionSelector): itUint
```

Encrypts a plaintext (up to 64 bits) and generates a signed transaction payload.

* **Parameters:**
  * `plaintext`: The data to be encrypted (must be smaller than 64 bits).
  * `sender`: The sender's information containing their wallet and user key.
  * `contractAddress`: The Ethereum contract address.
  * `functionSelector`: The function selector for the contract function.
* **Returns:** An `itUint` object containing the encrypted ciphertext and signature.

```typescript
function buildStringInputText(plaintext: string, sender, contractAddress, functionSelector): itString
```

Encrypts a plaintext string and generates a signed transaction payload.

* **Parameters:**
  * `plaintext`: The data to be encrypted (string).
  * `sender`: The sender's information containing their wallet and user key.
  * `contractAddress`: The Ethereum contract address.
  * `functionSelector`: The function selector for the contract function.
* **Returns:** An `itString` object containing the encrypted ciphertext and signature.

```typescript
function decryptUint(ciphertext: ctUint, userKey: string): bigint
```

Decrypts an AES-encrypted ciphertext and returns the original plaintext as a `bigint`.

* **Parameters:**
  * `ciphertext`: The encrypted ciphertext.
  * `userKey`: The user key for AES decryption.
* **Returns:** The decrypted plaintext as a `bigint`.

```typescript
function decryptString(ciphertext: { value: bigint[] }, userKey: string): string
```

Decrypts an AES-encrypted ciphertext representing a string.

* **Parameters:**
  * `ciphertext`: An object containing the encrypted ciphertext as a list of bigints.
  * `userKey`: The user key for AES decryption.
* **Returns:** The decrypted plaintext as a string.

<pre class="language-typescript"><code class="lang-typescript"><strong>function generateRandomAesKeySizeNumber(): string
</strong></code></pre>

Generates a random 128-bit AES key.

* **Returns:** A string containing the random bytes.


# Ethers.js

In order to provide easy access to all the features of COTI, the [**coti-ethers**](https://github.com/coti-io/coti-ethers) JavaScript SDK was created, which is made in a way that has an interface very similar to that of [**Ethers.js**](https://docs.ethers.org/v6/). In fact, ethers is a peer dependency of our library and all of the objects exported by coti-ethers (e.g. Wallet, BrowserProvider, etc.) inherit from the corresponding ethers objects and extend their functionality where needed.

While most of the existing SDKs should work out of the box, using unique COTI features like encrypting transaction inputs, requires executing the onboarding procedure and encrypting using the defined protocol.

The library is made in such a way that after replacing ethers with `coti-ethers` most client apps will work out of box.

## Clone, Build & Test

Full instructions for building and testing the package locally are available in the [coti-ethers GitHub repository](https://github.com/coti-io/coti-ethers)

## Installation

```bash
npm install @coti-io/coti-ethers
```

{% hint style="info" %}
SDK versions < 1.0.0 are for use with the Devnet, versions >= 1.0.0 are for use with the Testnet
{% endhint %}

## Usage

See the [**coti-typescript-examples**](https://github.com/coti-io/coti-typescript-examples) repository for code examples.

## Classes

### BrowserProvider

The `BrowserProvider` class extends the `JsonRpcApiPollingProvider` and provides an interface for interacting with an EIP-1193 compatible browser provider (such as MetaMask). This class enables the sending of JSON-RPC requests to Ethereum nodes via browser extensions and handles account management and signer creation.

```typescript
constructor(ethereum: Eip1193Provider, network?: Networkish, _options?: BrowserProviderOptions)
```

* **Parameters**:
  * `ethereum`: An `Eip1193Provider` (typically a browser extension like MetaMask) that implements the `request` method for sending JSON-RPC requests.
  * `network?`: Optional. A `Networkish` object representing the network configuration (e.g., mainnet, testnet).
  * `_options?`: Optional. `BrowserProviderOptions` that are merged into the `JsonRpcApiProviderOptions` for additional configuration.
* **Description**: Initializes the `BrowserProvider` by setting up the underlying provider and validating the `ethereum` object. It sets the default configuration for handling JSON-RPC requests and provides error handling and debugging features.
* **Throws**:
  * An error if the provided `ethereum` object is not a valid EIP-1193 provider.

```typescript
async function send(method: string, params: Array<any> | Record<string, any>): Promise<any>
```

* **Parameters**:
  * `method`: A string representing the JSON-RPC method (e.g., `eth_accounts`, `eth_sendTransaction`).
  * `params`: An array or object containing the parameters for the JSON-RPC method.
* **Returns**: A `Promise<any>` resolving to the result of the JSON-RPC method.
* **Description**: Sends a JSON-RPC request to the Ethereum provider using the inherited `send` method after initializing the provider via `_start()`.

```typescript
async function _send(payload: JsonRpcPayload | Array<JsonRpcPayload>): Promise<Array<JsonRpcResult | JsonRpcError>>
```

* **Parameters**:
  * `payload`: A `JsonRpcPayload` object or an array of such objects representing the JSON-RPC request(s).
* **Returns**: A `Promise<Array<JsonRpcResult | JsonRpcError>>`, resolving to an array containing the results or errors from the JSON-RPC request.
* **Description**: Sends a single JSON-RPC request using the EIP-1193 protocol. This method does not support batch requests as EIP-1193 does not allow them. If an error occurs, the method returns the error details including the code, message, and data.
* **Throws**: Throws an error if batch requests are attempted.

```typescript
function getRpcError(payload: JsonRpcPayload, error: JsonRpcError): Error
```

* **Parameters**:
  * `payload`: The original JSON-RPC payload that triggered the error.
  * `error`: A `JsonRpcError` object containing error details such as the error code, message, and data.
* **Returns**: An `Error` object with additional information based on the error code.
* **Description**: Enhances the error message based on known EIP-1193 error codes. For example, it rewrites the error message for:
  * `4001`: User denied the request.
  * `4200`: Unsupported request.
* **Example**:
  * `4001` becomes `ethers-user-denied: {message}`
  * `4200` becomes `ethers-unsupported: {message}`

```typescript
function hasSigner(address: number | string): Promise<boolean>
```

* **Parameters**:
  * `address`: A `number` (account index) or `string` (Ethereum address).
* **Returns**: A `Promise<boolean>`, resolving to `true` if the provider manages the given address.
* **Description**: Checks if the provider manages the given account. If an account index is passed, it checks if the account exists at that index. If an address is passed, it verifies whether the address is managed by the provider.

```typescript
async function getSigner(address?: number | string, userOnboardInfo?: OnboardInfo): Promise<JsonRpcSigner>
```

* **Parameters**:
  * `address?`: An optional `number` or `string` representing the account index or Ethereum address. Defaults to the first account if not provided.
  * `userOnboardInfo?`: Optional `OnboardInfo` object containing user-specific data such as AES keys and RSA key pairs for the signer.
* **Returns**: A `Promise<JsonRpcSigner>`, resolving to a custom `JsonRpcSigner` instance associated with the given address.
* **Description**: Retrieves the `JsonRpcSigner` for the specified account. If the provider doesn't manage the specified account, it triggers a request to the Ethereum provider to access accounts (e.g., MetaMask's `eth_requestAccounts` method). After confirming account access, it returns the signer.

### JsonRpcApiProvider

The `JsonRpcApiProvider` class extends the base `JsonRpcApiProvider` from the `ethers.js` library. It adds custom functionality for interacting with JSON-RPC APIs and provides an implementation for retrieving a `JsonRpcSigner`, which includes onboarding user information in the COTI network.

```typescript
constructor(network?: Networkish, options?: JsonRpcApiProviderOptions)
```

* **Parameters**:
  * `network?`: An optional `Networkish` object that specifies the network to connect to.
  * `options?`: Optional configuration options for the JSON-RPC provider (of type `JsonRpcApiProviderOptions`).
* **Description**: Initializes a new instance of the `JsonRpcApiProvider` class. It extends the base provider from `ethers.js`, allowing additional functionality for managing signers and user-specific onboarding.

```typescript
async function getSigner(address?: number | string, userOnboardInfo?: OnboardInfo): Promise<JsonRpcSigner>
```

* **Parameters**:
  * `address?`: An optional `number` or `string` that specifies which account to use. If not provided, defaults to the first account (`0`).
    * If a `number` is provided, it refers to the account index.
    * If a `string` is provided, it refers to an Ethereum address.
  * `userOnboardInfo?`: Optional `OnboardInfo` object that contains user-specific information, such as AES keys, RSA keys, and transaction hashes.
* **Returns**: A `Promise<JsonRpcSigner>`, resolving to a custom `JsonRpcSigner` instance associated with the given account.
* **Description**: This method retrieves a `JsonRpcSigner` instance for the specified account. The method handles two cases:
  1. **Account Index**: If the `address` is a number, the method fetches the list of accounts and returns the signer for the account at that index.
  2. **Account Address**: If the `address` is a string, it checks the list of accounts for the corresponding address and returns the signer for that address.
* **Account Resolution**:
  * The function makes a call to the JSON-RPC API method `eth_accounts` to retrieve the available accounts.
  * The `resolveProperties` utility is used to ensure that both the network and accounts are resolved before the account matching begins.
* **Error Handling**:
  * Throws an error if the specified account index is out of range.
  * Throws an error if the provided address is invalid or not found in the list of accounts.

### JsonRpcSigner

The `JsonRpcSigner` class extends the base `JsonRpcSigner` from `ethers.js` and adds additional functionality for onboarding users and managing encryption using AES keys within the Coti Network. It includes methods for securely encrypting and decrypting data, generating/recovering AES keys, and handling onboarding procedures.

```typescript
constructor(provider: JsonRpcApiProvider, address: string, userOnboardInfo?: OnboardInfo)
```

* **Parameters**:
  * `provider`: An instance of `JsonRpcApiProvider` for interacting with the blockchain.
  * `address`: A string representing the Ethereum address for this signer.
  * `userOnboardInfo?`: Optional `OnboardInfo` object containing user-specific data such as AES keys, RSA keys, and transaction hashes.
* **Description**: Initializes a new instance of the `JsonRpcSigner` class, optionally including user-specific onboarding information. It extends the functionality of the base `JsonRpcSigner` from `ethers` by incorporating encryption-related operations and user onboarding data.

```typescript
async function encryptValue(plaintextValue: bigint | number | string, contractAddress: string, functionSelector: string): Promise<itUint | itString>
```

* **Parameters**:
  * `plaintextValue`: The value to encrypt, which can be of type `bigint`, `number`, or `string`.
  * `contractAddress`: The smart contract address to which the encryption is related.
  * `functionSelector`: The function identifier for the contract interaction.
* **Returns**: A `Promise<itUint | itString>`, depending on the type of the plaintext value.
* **Description**: Encrypts the provided plaintext using the user’s AES key. If the AES key is not available, it attempts to generate or recover it via onboarding. The function handles both integer and string values and returns the encrypted result accordingly.

```typescript
async function decryptValue(ciphertext: ctUint | ctString): Promise<bigint | string>
```

* **Parameters**:
  * `ciphertext`: The encrypted value, which can either be of type `ctUint` (for integers) or `ctString` (for strings).
* **Returns**: A `Promise<bigint | string>`, depending on the ciphertext type.
* **Description**: Decrypts the provided ciphertext using the AES key stored in the user’s onboarding information. If the AES key is missing, it attempts to onboard the user or recover the key. The method supports decryption for both integers and strings.

```typescript
async function generateOrRecoverAes(onboardContractAddress: string = ONBOARD_CONTRACT_ADDRESS): Promise<void>
```

* **Parameters**:
  * `onboardContractAddress`: The contract address used for onboarding purposes. Defaults to `ONBOARD_CONTRACT_ADDRESS`.
* **Returns**: A `Promise<void>`.
* **Description**: This function attempts to generate or recover the user's AES key. If the AES key already exists in the onboarding information, it returns immediately. If the user’s RSA key and transaction hash are available, the AES key is recovered from the blockchain using the `recoverAesFromTx` function. If no onboarding info exists, it checks the user's account balance and initiates the onboarding process if the balance is sufficient.
* **Error Handling**:
  * Throws an error if the user's account balance is `0`, preventing onboarding.

### Wallet

The `Wallet` class extends the base `Wallet` from the `ethers` library, adding features specific to the COTI network. It handles user onboarding, key management, and encryption/decryption of values using AES and RSA keys. The class also supports the process of automatically onboarding users if needed.

```typescript
constructor(privateKey: string | SigningKey, provider?: Provider | null, userOnboardInfo?: OnboardInfo)
```

* **Parameters**:
  * `privateKey`: A `string` or `SigningKey` used to initialize the wallet.
  * `provider?`: An optional `Provider` object or `null`. Defaults to `null` if not provided.
  * `userOnboardInfo?`: An optional `OnboardInfo` object that contains the user's onboarding data (e.g., AES key, RSA key).
* **Description**: Initializes a new `Wallet` instance with a given private key, provider, and optional user onboarding information. This class extends the `BaseWallet` from the `ethers` library, inheriting its functionality.

<pre class="language-typescript"><code class="lang-typescript"><strong>function getAutoOnboard(): boolean
</strong></code></pre>

* **Returns**: `boolean`
* **Description**: Returns the current state of the `_autoOnboard` flag, which indicates whether automatic onboarding is enabled.

```typescript
function getUserOnboardInfo(): OnboardInfo | undefined
```

* **Returns**: `OnboardInfo | undefined`
* **Description**: Retrieves the user's onboarding information, or `undefined` if no onboarding info is present.

```typescript
function setUserOnboardInfo(onboardInfo?: Partial<OnboardInfo> | undefined | null)
```

* **Parameters**:
  * `onboardInfo`: An optional object of type `Partial<OnboardInfo>` to update or modify the existing onboarding information.
* **Description**: Updates the current user's onboarding info by merging the new `onboardInfo` values with the existing ones.

```typescript
function setAesKey(key: string)
```

* **Parameters**:
  * `key`: The AES encryption key to set in the user's onboarding information.
* **Description**: Assigns the AES key to the user’s onboarding information. If onboarding info doesn’t exist, it creates a new onboarding object with the provided key.

```typescript
function setOnboardTxHash(hash: string)
```

* **Parameters**:
  * `hash`: The transaction hash associated with the user's onboarding process.
* **Description**: Sets the transaction hash in the user’s onboarding info. If onboarding info is absent, it creates a new one with the hash.

```typescript
function setRsaKeyPair(rsa: RsaKeyPair)
```

* **Parameters**:
  * `rsa`: The RSA key pair for encryption/decryption.
* **Description**: Stores the provided RSA key pair in the user’s onboarding info. If the onboarding object is not present, it initializes a new one with the given RSA key.

```typescript
async function encryptValue(plaintextValue: bigint | number | string, contractAddress: string, functionSelector: string): Promise<itUint | itString>
```

* **Parameters**:
  * `plaintextValue`: The value to encrypt, which can be of type `bigint`, `number`, or `string`.
  * `contractAddress`: The address of the smart contract involved in the encryption.
  * `functionSelector`: A string that identifies the specific contract function to which this encryption pertains.
* **Returns**: A promise that resolves to either `itUint` or `itString`, depending on the type of the value.
* **Description**: Encrypts the provided `plaintextValue` using the user’s AES key. If the AES key is missing, it attempts to generate or recover it. It handles both `bigint` and `string` values.

```typescript
async function decryptValue(ciphertext: ctUint | ctString): Promise<string | bigint>
```

* **Parameters**:
  * `ciphertext`: The encrypted value, either a `ctUint` (for integers) or a `ctString` (for strings).
* **Returns**: A promise that resolves to either a `string` or `bigint`, depending on the ciphertext type.
* **Description**: Decrypts the given ciphertext using the user’s AES key. If the AES key is not available, it attempts to generate or recover it. Handles both integer and string decryption.

```typescript
function enableAutoOnboard()
```

* **Description**: Enables automatic onboarding by setting the `_autoOnboard` flag to `true`.

```typescript
function disableAutoOnboard()
```

* **Description**: Disables automatic onboarding by setting the `_autoOnboard` flag to `false`.

```typescript
function clearUserOnboardInfo()
```

* **Description**: Clears the stored user onboarding information by setting `_userOnboardInfo` to `undefined`.

```typescript
async function generateOrRecoverAes(onboardContractAddress: string = DEVNET_ONBOARD_CONTRACT_ADDRESS)
```

* **Parameters**:
  * `onboardContractAddress`: The contract address for onboarding purposes. Defaults to `DEVNET_ONBOARD_CONTRACT_ADDRESS`.
* **Description**: Attempts to generate or recover the user’s AES key:
  * If the AES key exists in the user’s onboarding info, it returns immediately.
  * If the user’s RSA key and transaction hash are available, the AES key is recovered from the blockchain transaction using `recoverAesFromTx`.
  * If neither AES nor RSA keys are available, it attempts to onboard the user by checking their account balance and invoking the onboarding process.
* **Throws**: Throws an error if the account balance is 0, preventing the user from being onboarded.

## Modules

### Account

This module provides utility functions for managing and interacting with accounts in an Ethereum-compatible blockchain using `ethers.js`. It includes functions for retrieving account details, checking balances, validating addresses, handling nonces, and transferring native tokens between accounts.

```typescript
async function printAccountDetails(provider: Provider, address: string): Promise<void>
```

* **Parameters**:
  * `provider`: An instance of `Provider` to interact with the blockchain.
  * `address`: A string representing the account’s address.
* **Returns**: A `Promise<void>` that resolves once the account details are printed.
* **Description**: This function prints the details of a given account to the console, including:
  * The account's address.
  * The account's balance in both wei and ether.
  * The account's transaction nonce.
* **Error Handling**:
  * Throws an error if the provider is not connected or the address is invalid.

```typescript
function validateAddress(address: string): { valid: boolean, safe: string }
```

* **Parameters**:
  * `address`: A string representing the account’s address.
* **Returns**: An object containing:
  * `valid`: A boolean indicating if the address is valid.
  * `safe`: A checksummed version of the address (a safer, standardized format).
* **Description**: This function validates the format of a given Ethereum address and returns both a boolean (`valid`) and a safe, checksummed version (`safe`) of the address.

```typescript
async function getNonce(provider: Provider, address: string): Promise<number>
```

* **Parameters**:
  * `provider`: An instance of `Provider` to interact with the blockchain.
  * `address`: A string representing the account’s address.
* **Returns**: A `Promise<number>` that resolves to the account's nonce, which represents the number of transactions sent from the address.
* **Description**: This function retrieves the current transaction nonce for the given account, which is essential for ensuring the correct order of transactions on the blockchain.
* **Error Handling**:
  * Throws an error if the provider is not connected or the address is invalid.

```typescript
function addressValid(address: string): boolean
```

* **Parameters**:
  * `address`: A string representing the account’s address.
* **Returns**: A boolean indicating whether the address is valid.
* **Description**: This function validates an Ethereum address by returning a boolean (`true` if valid, `false` otherwise).

```typescript
async function getNativeBalance(provider: Provider, address: string): Promise<string>
```

* **Parameters**:
  * `provider`: An instance of `Provider` to interact with the blockchain.
  * `address`: A string representing the account’s address.
* **Returns**: A `Promise<string>` that resolves to the account balance in ether (as a formatted string).
* **Description**: This function retrieves the account’s balance in ether (instead of wei) by formatting the balance using `formatEther`.
* **Error Handling**:
  * Throws an error if the provider is not connected or the address is invalid.

```typescript
async function getEoa(accountPrivateKey: string): Promise<string>
```

* **Parameters**:
  * `accountPrivateKey`: A string representing the private key of the account.
* **Returns**: A `Promise<string>` that resolves to the Ethereum address generated from the private key.
* **Description**: This function generates an Ethereum address (EOA) from the given private key and returns it.
* **Error Handling**:
  * Throws an error if the generated address is invalid.

```typescript
async function transferNative(provider: Provider, wallet: Wallet, recipientAddress: string, amountToTransferInWei: BigInt, nativeGasUnit: number): Promise<void>
```

* **Parameters**:
  * `provider`: An instance of `Provider` to interact with the blockchain.
  * `wallet`: An instance of `Wallet` that represents the sender's account.
  * `recipientAddress`: A string representing the recipient's Ethereum address.
  * `amountToTransferInWei`: The amount of native tokens to transfer, in wei.
  * `nativeGasUnit`: The gas limit for the transaction.
* **Returns**: A `Promise<void>`.
* **Description**: This function transfers native tokens (e.g., ETH) from the sender's wallet to a recipient. It constructs a transaction, validates the gas estimation, and sends the transaction using the sender’s wallet.
* **Error Handling**:
  * Throws an error if the transaction fails.
  * Logs the transaction hash on success or error details if it fails.
* **Transaction Details**:
  * The transaction includes details like `to` (recipient address), `from` (sender address), `value` (amount in wei), `nonce`, `gasLimit`, and `gasPrice`.

### Network

This module provides utility functions for interacting with the COTI network through `ethers` providers. It includes functions to fetch the default provider, check if a provider is connected, print network details, and retrieve the latest block number.

```typescript
function getDefaultProvider(cotiNetwork: CotiNetwork): JsonRpcProvider
```

* **Parameters**:
  * `cotiNetwork`: An instance of `CotiNetwork` that specifies the network (Devnet, Testnet, Mainnet, etc.) the provider should connect to.
* **Returns**: An instance of `JsonRpcProvider`.
* **Description**: This function returns a `JsonRpcProvider` configured to connect to the specified `CotiNetwork`. It uses the `cotiNetwork` URL to set up the provider for the specified environment.

```typescript
async function printNetworkDetails(provider: Provider): Promise<void>
```

* **Parameters**:
  * `provider`: An instance of `Provider` that is connected to the Coti network.
* **Returns**: A `Promise<void>`, which resolves after network details are printed to the console.
* **Description**: This function prints the details of the network to which the provider is connected, including:
  * The provider's connection URL (if using a `JsonRpcProvider`).
  * The `chainId` of the network.
  * The number of the latest block on the blockchain.
* **Error Handling**:
  * Throws an error if the provider is not connected.
* **Usage**: This function logs the following details:
  * Provider URL (`url`)
  * Chain ID (`chainId`)
  * Latest block number

```typescript
async function getLatestBlock(provider: Provider): Promise<number>
```

* **Parameters**:
  * `provider`: An instance of `Provider` that is connected to the Coti network.
* **Returns**: A `Promise<number>`, which resolves to the block number of the latest block in the network.
* **Description**: This function retrieves the block number of the most recent block on the blockchain. It first checks if the provider is connected, then queries the provider for the latest block.
* **Error Handling**:
  * Throws an error if the provider is not connected or if the provider’s address is invalid.

```typescript
async function isProviderConnected(provider: Provider): Promise<boolean>
```

* **Parameters**:
  * `provider`: An instance of `Provider`.
* **Returns**: A `Promise<boolean>`, which resolves to `true` if the provider is connected to a network, or `false` if the connection is unsuccessful.
* **Description**: This function checks if a given provider is connected to a valid network by querying the network information. If the provider's network cannot be fetched, it returns `false`. This function also throws an error if the provider is undefined.
* **Error Handling**:
  * Throws an error if the `provider` object is undefined.

\\


# Python SDK

The [**COTI Python SDK**](https://github.com/coti-io/coti-sdk-python) provides a set of encryption, decryption, and cryptographic utilities, including RSA and AES encryption, message signing, and key handling functions. The utilities are primarily designed to work with cryptographic operations for secure communication and message signing, particularly within Ethereum smart contracts or similar environments.

## Clone, Build & Test

Full instructions for building and testing the package locally are available in the [coti-sdk-python GitHub repository](https://github.com/coti-io/coti-sdk-python)

## Installation

```bash
pip install coti-sdk
```

{% hint style="info" %}
SDK versions < 1.0.0 are for use with the Devnet, versions >= 1.0.0 are for use with the Testnet
{% endhint %}

## Usage

See the [**coti-sdk-python-examples**](https://github.com/coti-io/coti-sdk-python-examples) repository for code examples.

## Modules

### Crypto

```python
def encrypt(key, plaintext)
```

Encrypts a 128-bit `plaintext` using the provided 128-bit AES `key` and ECB mode.

* **Parameters:**
  * `key (bytes)`: 128-bit AES key (16 bytes).
  * `plaintext (bytes)`: 128-bit or smaller plaintext to encrypt.
* **Returns:**
  * `ciphertext (bytes)`: The encrypted text.
  * `r (bytes)`: A random value used during encryption.

```python
def decrypt(key, r, ciphertext)
```

Decrypts the `ciphertext` using the provided 128-bit AES `key` and the random value `r`.

* **Parameters:**
  * `key (bytes)`: 128-bit AES key.
  * `r (bytes)`: Random value used during encryption.
  * `ciphertext (bytes)`: Encrypted text to decrypt.
* **Returns:**
  * `plaintext (bytes)`: The decrypted original message.

```python
def generate_aes_key()
```

Generates a random 128-bit AES key.

* **Returns:**
  * `key (bytes)`: Randomly generated 128-bit key (16 bytes).

```python
def sign_input_text(sender, addr, function_selector, ct, key)
```

Signs an input message composed of multiple parts, including sender address, contract address, function selector, and ciphertext.

* **Parameters:**
  * `sender (bytes)`: Sender address.
  * `addr (bytes)`: Contract address.
  * `function_selector (str)`: Ethereum function selector (in hex).
  * `ct (bytes)`: Ciphertext (concatenated).
  * `key (bytes)`: Private key for signing.
* **Returns:**
  * `signature (bytes)`: Digital signature of the message.

```python
def sign(message, key)
```

Signs a `message` using the provided `key` (Ethereum-style private key).

* **Parameters:**
  * `message (bytes)`: Message to sign.
  * `key (bytes)`: Private key to use for signing.
* **Returns:**
  * `signature (bytes)`: The generated signature.

```python
def build_input_text(plaintext, user_aes_key, sender, contract, function_selector, signing_key)
```

Encrypts a plaintext integer and signs the resulting ciphertext along with other parameters.

* **Parameters:**
  * `plaintext (int)`: Integer to encrypt.
  * `user_aes_key (bytes)`: AES key for encryption.
  * `sender (object)`: Ethereum-like sender object (with `address` attribute).
  * `contract (object)`: Ethereum-like contract object (with `address` attribute).
  * `function_selector (str)`: Function selector (in hex).
  * `signing_key (bytes)`: Signing key for signature.
* **Returns:**
  * `dict`: Contains the `ciphertext` and `signature`.

```python
def build_string_input_text(plaintext, user_aes_key, sender, contract, function_selector, signing_key)
```

Encrypts and signs string-based input data, breaking it into 8-byte chunks.

* **Parameters:**
  * `plaintext (str)`: String to encrypt.
  * `user_aes_key (bytes)`: AES key for encryption.
  * `sender (object)`: Ethereum-like sender object.
  * `contract (object)`: Ethereum-like contract object.
  * `function_selector (str)`: Function selector (in hex).
  * `signing_key (bytes)`: Signing key for signature.
* **Returns:**
  * `dict`: Contains the `ciphertext` and `signature`.

```python
def decrypt_uint(ciphertext, user_key)
```

Decrypts a ciphertext into an unsigned integer using AES.

* **Parameters:**
  * `ciphertext (CtUint)`: Ciphertext to decrypt (in integer format).
  * `user_key (bytes)`: AES key to use for decryption.
* **Returns:**
  * `int`: The decrypted integer.

```python
def decrypt_string(ciphertext, user_key)
```

Decrypts a ciphertext back into a string, handling multiple formats of ciphertext.

* **Parameters:**
  * `ciphertext (CtString)`: Ciphertext to decrypt, can be in event or state variable format.
  * `user_key (bytes)`: AES key to use for decryption.
* **Returns:**
  * `str`: The decrypted string.

```python
def generate_rsa_keypair()
```

Generates an RSA key pair for encryption and decryption.

* **Returns:**
  * `private_key_bytes (bytes)`: Serialized private key.
  * `public_key_bytes (bytes)`: Serialized public key.

```python
def decrypt_rsa(private_key_bytes, ciphertext)
```

Decrypts a ciphertext using RSA and a provided private key.

* **Parameters:**
  * `private_key_bytes (bytes)`: Private key used for decryption.
  * `ciphertext (bytes)`: Ciphertext to decrypt.
* **Returns:**
  * `plaintext (bytes)`: Decrypted plaintext.


# Web3.py

In order to provide easy access to all the features of COTI, the [**coti-web3**](https://github.com/coti-io/coti-web3.py) Python SDK was created, which is made in a way that has an interface very similar to that of [**web3.py**](https://web3py.readthedocs.io/en/stable/). In fact, web3 is a peer dependency of our library and all of the objects exported by coti-web3 (e.g. LocalAccount, Web3, etc.) inherit from the corresponding web3 objects and extend their functionality where needed.

While most of the existing SDKs should work out of the box, using unique COTI features like encrypting transaction inputs, requires executing the onboarding procedure and encrypting using the defined protocol.

The library is made in such a way that after replacing web3 with `coti-web3` most client apps will work out of box.

## Clone, Build & Test

Full instructions for building and testing the package locally are available in the [coti-web3.py GitHub repository](https://github.com/coti-io/coti-web3.py)

## Installation

```bash
pip install coti-web3
```

{% hint style="info" %}
SDK versions < 1.0.0 are for use with the Devnet, versions >= 1.0.0 are for use with the Testnet
{% endhint %}

## Usage

See the [**coti-python-examples**](https://github.com/coti-io/coti-python-examples) repository for code examples.

## Classes

### Account

<pre class="language-python"><code class="lang-python"><strong>def from_key(private_key: PrivateKeyType, user_onboard_info: UserOnboardInfo = None)
</strong></code></pre>

* **Description**: Loads an account from a raw private key.
* **Parameters**:
  * `private_key`: The private key as a hex string, `bytes`, `int`, or `PrivateKey` object.
  * `user_onboard_info` (optional): An dictionary containing the information from the user's onboarding procedure
* **Returns**: A `LocalAccount` instance.

```python
def from_mnemonic(mnemonic: str, passphrase: str = "", account_path: str = ETHEREUM_DEFAULT_PATH, user_onboard_info: UserOnboardInfo = None)
```

* **Description**: Derives an account from a BIP39 mnemonic phrase.
* **Parameters**:
  * `mnemonic`: The mnemonic phrase.
  * `passphrase` (optional): A passphrase for added security.
  * `account_path` (optional): Custom HD path.
  * `user_onboard_info` (optional): A dictionary containing the information from the user's onboarding procedure
* **Returns**: A `LocalAccount` instance.

```python
def encrypt_value(plaintext_value: Union[bool, int, str], contract_address: str, function_selector: str, private_key: PrivateKeyType, aes_key: str)
```

* **Description**: Encrypts a plaintext value for secure contract inputs.
* **Parameters**:
  * `plaintext_value`: A boolean, unsigned integer or string to be encrypted
  * `contract_address`: The address of the contract that the user is interacting with
  * `function_selector`: The four-byte function selector
  * `private_key`: The private key used to sign the transaction and encrypted value
  * `aes_key`: The AES key used to encrypt the value
* **Returns**: Encrypted data object (`ItBool`, `ItUint`, or `ItString`)

```python
def decrypt_value(ciphertext: Union[CtBool, CtUint, CtString], aes_key: str)
```

* **Description**: Decrypts a value encrypted with `encrypt_value`.
* **Parameters**:
  * `ciphertext`: The encrypted ciphertext to decrypt
  * `aes_key`: The AES key used to decrypt the value
* **Returns**: The original value (`bool`, `int`, or `str`).

### LocalAccount

#### Properties

* `user_onboard_info`: A dictionary containing the information from the user's onboarding procedure
* `aes_key`: The AES key used for encryption and decryption
* `rsa_key_pair`: A tuple containing the RSA public and private keys
* `onboard_tx_hash`: The transaction hash associated with the onboarding procedure

#### Methods

```python
def __init__(key: PrivateKey, account: AccountLocalActions, user_onboard_info: UserOnboardInfo = None)
```

* **Description**: Initializes a new `LocalAccount` instance with a private key and an associated `AccountLocalActions` instance.
* **Parameters**:
  * `key` (PrivateKey): The private key for the account.
  * `account` (AccountLocalActions): Key management and signing API.
  * `user_onboard_info` (optional): Metadata associated with the account.

```python
def set_user_onboard_info(user_onboard_info: UserOnboardInfo)
```

* **Description**: Updates the onboarding metadata.
* **Parameters**:
  * `user_onboard_info`: Metadata dictionary containing user-specific information.

```python
def set_aes_key(aes_key: str)
```

* **Description**: Sets the AES key in the onboarding metadata.
* **Parameters**:
  * `aes_key`: The AES key.

```python
def set_rsa_key_pair(rsa_key_pair: tuple[str, str])
```

* **Description**: Sets the RSA key pair in the onboarding metadata.
* **Parameters**:
  * `rsa_key_pair`: A tuple of RSA public and private keys.

```python
def set_onboard_tx_hash(tx_hash: str)
```

* **Description**: Sets the onboarding transaction hash in the metadata.
* **Parameters**:
  * `tx_hash`: The transaction hash.

```python
def encrypt_value(plaintext_value: Union[bool, int, str], contract_address: str, function_selector: str)
```

* **Description**: Encrypts a plaintext value for secure smart contract interactions.
* **Parameters**:
  * `plaintext_value`: The value to encrypt (`bool`, `int`, or `str`).
  * `contract_address`: The contract's Ethereum address.
  * `function_selector`: The function selector for the contract call.
* **Returns**: An encrypted value (`ItBool`, `ItUint`, or `ItString`).

```python
def decrypt_value(ciphertext: Union[CtBool, CtUint, CtString])
```

* **Description**: Decrypts an encrypted value.
* **Parameters**:
  * `ciphertext`: The encrypted value (`CtBool`, `CtUint`, or `CtString`).
* **Returns**: The decrypted value (`bool`, `int`, or `str`).

## Modules

### COTI

```python
def init_web3(coti_network: CotiNetwork)
```

* **Description**: Initializes a Web3 client for the specified COTI network.
* **Parameters**:
  * `coti_network` (`CotiNetwork`): Network configuration (e.g., testnet or mainnet).
* **Returns**: A `Web3` instance configured with the appropriate middleware.

```python
def onboard(web3: Web3, account: LocalAccount, onboard_contract_address: str)
```

* **Description**: Onboards an Ethereum account by generating RSA keys, signing the public key, and interacting with the onboarding smart contract. It also retrieves and stores the AES key.
* **Parameters**:
  * `web3` (`Web3`): A Web3 instance.
  * `account` (`LocalAccount`): The Ethereum account to onboard.
  * `onboard_contract_address` (`str`): Address of the onboarding contract.
* **Returns**: None. Updates the `LocalAccount` instance with onboarded information.
* **Raises**:
  * `RuntimeError` if the account has insufficient funds for the transaction.

```python
def recover_aes_from_tx(web3: Web3, account: LocalAccount, onboard_contract_address: str)
```

* **Description**: Recovers the AES key for an account using transaction receipt data from the onboarding process.
* **Parameters**:
  * `web3` (`Web3`): A Web3 instance.
  * `account` (`LocalAccount`): The Ethereum account with onboarding metadata.
  * `onboard_contract_address` (`str`): Address of the onboarding contract.
* **Returns**: None. Updates the `LocalAccount` instance with the recovered AES key.

```python
def generate_or_recover_aes(web3: Web3, account: LocalAccount, onboard_contract_address: str = ACCOUNT_ONBOARD_CONTRACT_ADDRESS)
```

* **Description**: Ensures the account has an AES key by either recovering it from the onboarding transaction or initiating the onboarding process.
* **Parameters**:
  * `web3` (`Web3`): A Web3 instance.
  * `account` (`LocalAccount`): The Ethereum account.
  * `onboard_contract_address` (`str`): Address of the onboarding contract (optional, defaults to `ACCOUNT_ONBOARD_CONTRACT_ADDRESS`).
* **Returns**: None. Updates the `LocalAccount` with AES key information.
* **Raises**:
  * `RuntimeError` if the account has insufficient funds.


# Contracts Library

[**COTI Contracts**](https://github.com/coti-io/coti-contracts) is a library of smart contracts for COTI's GC technology, including MPC contract for secure computations, private ERC20 and ERC721 contracts, and test mocks for validation. These components enable privacy-focused and decentralized DeFi solutions.

## Clone, Build & Test

Full instructions for building and testing the package locally are available in the [coti-contracts GitHub repository](https://github.com/coti-io/coti-contracts)

## Installation

### Hardhat (npm)

```bash
npm install @coti-io/coti-contracts
```

{% hint style="info" %}
SDK versions < 1.0.0 are for use with the Devnet, versions >= 1.0.0 are for use with the Testnet
{% endhint %}

## Usage

See the [**coti-contracts-examples**](https://github.com/coti-io/coti-contracts-examples) repository for code examples.


# MPC Core

The [**MPC Core**](https://github.com/coti-io/coti-contracts/blob/main/contracts/utils/mpc/MpcCore.sol) is a library that simplifies interactions with the precompiled contracts which provide core functionalities for secure multi-party computation (MPC) using the COTI protocol.

## Usage

```solidity
// SPDX-License-Identifier: MIT

pragma solidity 0.8.19;

import "@coti-io/coti-contracts/contracts/utils/mpc/MpcCore.sol";

contract MyContract { ... }
```

## Types

### Inputtext

```solidity
struct itBool
struct itUint8
struct itUint16
struct itUint32
struct itUint64
struct itString
```

### Garbledtext

```solidity
type gtBool
type gtUint8
type gtUint16
type gtUint32
type gtUint64
struct gtString
```

### Ciphertext

```solidity
type ctBool
type ctUint8
type ctUint16
type ctUint32
type ctUint64
struct ctString
```

### Usertext

```solidity
struct utBool
struct utUint8
struct utUint16
struct utUint32
struct utUint64
struct utString
```

## Functions

{% hint style="info" %}
Since private data types mostly support the same functions, we have chosen to list only the functions pertaining to the itUint64, gtUint64 and ctUint64 types. See [**MpcCore.sol**](https://github.com/coti-io/coti-contracts/blob/main/contracts/utils/mpc/MpcCore.sol) for the full list of supported functions.
{% endhint %}

### Special Functions

```solidity
function getUserKey(bytes calldata signedEK, bytes calldata signature) returns (bytes memory keyShare0, bytes memory keyShare1)
```

* Retrieves the user's AES encryption key in encrypted format, by using the provided RSA public key to encrypt it.
* A valid signature using the EOA private key is used to validate the account ownership.

```solidity
function validateCiphertext(itUint64 memory input) returns (gtUint64)
```

* Verifies that a given inputtext has a valid signature and onboards it into the gcEVM, returning a Garbledtext™ value.
* If the input is not valid, the call will revert with no return data and no additional gas will be consumed.

```solidity
function onBoard(ctUint64 ct) returns (gtUint64)
```

* The function onboards a given Ciphertext to the gcEVM, resulting in a Garbledtext™ value.
* Must be invoked with ciphertext encrypted by the **system AES key**, such as ciphertexts that are generated by calling `offboard`.

```solidity
function offBoard(gtUint64 pt) returns (ctUint64)
```

* The function offboards the given Garbledtext™ from the gcEVM, resulting in a Ciphertext.
* The offboarding process uses the **network AES key** to encrypt the value inside the Garbledtext™.

```solidity
function offBoardToUser(gtUint64 pt, address addr) returns (ctUint64)
```

* The function offboards the given Garbledtext™ from the gcEVM, resulting in a Ciphertext.
* The offboarding process uses the **user AES key** associated with the given address to encrypt the value inside the Garbledtext™.

```solidity
function offBoardCombined(gtUint64 pt, address addr) returns (utUint64 memory ut)
```

* The function offboards the given Garbledtext™ from the gcEVM, resulting in a struct containing two Ciphertexts.
* The offboarding process uses both the **network AES key** and the **user AES key** associated with the given address to encrypt the value inside the Garbledtext™.

```solidity
function decrypt(gtUint64 ct) returns (uint64)
```

* Returns the clear value of the given Ciphertext.

```solidity
function setPublic64(uint64 pt) returns (gtUint64)
```

* Onboards the given clear input to the gcEVM, resulting in a Garbledtext™.

```solidity
function rand64() returns (gtUint64)
```

* Generates an encrypted random value in Garbledtext™ form.

```solidity
function randBoundedBits64(uint8 numBits) returns (gtUint64)
```

* Generates an encrypted random value that falls within the range of \[0, 2^numBits] in Garbledtext™ form.

```solidity
function transfer(gtUint64 a, gtUint64 b, gtUint64 amount) returns (gtUint64, gtUint64, gtBool)
```

* Returns the encrypted balances of two accounts (one starting with balance `a`, the other starting with balance `b`) as a result of transferring `amount` from the account with balance `a` to the account with balance `b`, along with an encrypted boolean value indicating whether the transfer would succeed.
* If `a` is less than `amount`, then the resulting values of `a` and `b` will remain unchanged.

```solidity
function transferWithAllowance(gtUint64 a, gtUint64 b, gtUint64 amount, gtUint64 allowance) returns (gtUint64, gtUint64, gtBool, gtUint64)
```

* Returns the encrypted balances of two accounts (one starting with balance `a`, the other starting with balance `b`) as a result of transferring `amount` with allowance `allowance` from the account with balance `a` to the account with balance `b`, along with an encrypted boolean value indicating whether the transfer would succeed.
* If `a` is less than `amount` or if `amount` is greater than `allowance`, then the resulting values of `a` and `b` will remain unchanged.

### Arithmetic Functions

```solidity
function add(gtUint64 a, gtUint64 b) returns (gtUint64)
```

```solidity
function checkedAdd(gtUint64 a, gtUint64 b) returns (gtUint64)
```

```solidity
function checkedAddWithOverflowBit(gtUint64 a, gtUint64 b) returns (gtBool, gtUint64)
```

```solidity
function sub(gtUint64 a, gtUint64 b) returns (gtUint64)
```

```solidity
function checkedSub(gtUint64 a, gtUint64 b) returns (gtUint64)
```

```solidity
function checkedSubWithOverflowBit(gtUint64 a, gtUint64 b) returns (gtBool, gtUint64)
```

```solidity
function mul(gtUint64 a, gtUint64 b) returns (gtUint64)
```

```solidity
function checkedMul(gtUint64 a, gtUint64 b) returns (gtUint64)
```

```solidity
function checkedMulWithOverflowBit(gtUint64 a, gtUint64 b) returns (gtBool, gtUint64)
```

```solidity
function div(gtUint64 a, gtUint64 b) returns (gtUint64)
```

```solidity
function rem(gtUint64 a, gtUint64 b) returns (gtUint64)
```

```solidity
function and(gtUint64 a, gtUint64 b) returns (gtUint64)
```

```solidity
function or(gtUint64 a, gtUint64 b) returns (gtUint64)
```

```solidity
function xor(gtUint64 a, gtUint64 b) returns (gtUint64)
```

```solidity
function shl(gtUint64 a, uint8 b) returns (gtUint64)
```

```solidity
function shr(gtUint64 a, uint8 b) returns (gtUint64)
```

```solidity
function eq(gtUint64 a, gtUint64 b) returns (gtBool)
```

```solidity
function ne(gtUint64 a, gtUint64 b) returns (gtBool)
```

```solidity
function ge(gtUint64 a, gtUint64 b) returns (gtBool)
```

```solidity
function gt(gtUint64 a, gtUint64 b) returns (gtBool)
```

```solidity
function le(gtUint64 a, gtUint64 b) returns (gtBool)
```

```solidity
function lt(gtUint64 a, gtUint64 b) returns (gtBool)
```

```solidity
function min(gtUint64 a, gtUint64 b) returns (gtUint64)
```

```solidity
function max(gtUint64 a, gtUint64 b) returns (gtUint64)
```

```solidity
function mux(gtBool bit, gtUint64 a, gtUint64 b) returns (gtUint64)
```

* Returns an encrypted value (either `a` or `b`) based on the encrypted boolean input
* If `bit` is **false**, then the returned value is equal to `a`
* If `bit` is **true**, then the returned value is equal to `b`

### Enums

```solidity
enum MPC_TYPE { SBOOL_T, SUINT8_T, SUINT16_T, SUINT32_T, SUINT64_T }
```

* Represent different MPC data types

```solidity
enum ARGS { BOTH_SECRET, LHS_PUBLIC, RHS_PUBLIC }
```

* Represent different argument types

### Encoding Functions

```solidity
function combineEnumsToBytes2(MPC_TYPE mpcType, ARGS argsType) returns (bytes2)
```

* Combines an `MPC_TYPE` and `ARGS` into a `bytes2` value.

```solidity
function combineEnumsToBytes3(MPC_TYPE mpcType1, MPC_TYPE mpcType2, ARGS argsType) returns (bytes3)
```

* Combines two `MPC_TYPE` values and an `ARGS` value into a `bytes3` value.

```solidity
function combineEnumsToBytes4(MPC_TYPE mpcType1, MPC_TYPE mpcType2, MPC_TYPE mpcType3, ARGS argsType) returns (bytes4)
```

* Combines three `MPC_TYPE` values and an `ARGS` value into a `bytes4` value.

```solidity
function combineEnumsToBytes5(MPC_TYPE mpcType1, MPC_TYPE mpcType2, MPC_TYPE mpcType3, MPC_TYPE mpcType4, ARGS argsType) returns (bytes5)
```

* Combines four `MPC_TYPE` values and an `ARGS` value into a `bytes4` value.


# Data Privacy Framework

The [**Data Privacy Framework**](https://github.com/coti-io/coti-contracts/blob/main/contracts/access/DataPrivacyFramework/DataPrivacyFramework.sol) is an abstract Solidity contract designed to manage conditions and operations related to data privacy. The contract handles operations such as user permissions, time-bound constraints, and condition validation based on various keys and parameters.

## Core

### Usage

```solidity
// SPDX-License-Identifier: MIT

pragma solidity 0.8.19;

import "@coti-io/coti-contracts/contracts/access/DataPrivacyFramework/DataPrivacyFramework.sol";

contract MyContract is DataPrivacyFramework {
    constructor() DataPrivacyFramework(false, false) {}
}
```

### Types

```solidity
struct InputData {
        address caller;
        string operation;
        bool active;
        uint256 timestampBefore;
        uint256 timestampAfter;
        bool falseKey;
        bool trueKey;
        uint256 uintParameter;
        address addressParameter;
        string stringParameter;
}
```

```solidity
struct Condition {
        uint256 id; // numeric ID of the condition
        address caller; // caller associated with this condition
        string operation; // operation associated with this condition
        bool active; // indicates if the permission is active
        bool falseKey; // causes the condition to never be satisfied
        bool trueKey; // causes the permission to always be satisfied (but has lower priority than falseKey)
        uint256 timestampBefore; // condition is valid before this timestamp
        uint256 timestampAfter; // condition is valid after this timestamp
        uint256 uintParameter; // parameter of type uint256 used for verifying if the caller has permission to perform the computation
        address addressParameter; // parameter of type address used for verifying if the caller has permission to perform the computation
        string stringParameter;// parameter of type string used for verifying if the caller has permission to perform the computation
}
```

```solidity
enum ParameterType {
        None,
        UintParam,
        AddressParam,
        StringParam
}
```

### Modifiers

```solidity
modifier onlyAllowedUserOperation(string memory operation, uint256 uintParameter, address addressParameter, string memory stringParameter)
```

### Functions

```solidity
constructor(bool addressDefaultPermission_, bool operationDefaultPermission_)
```

```solidity
function getConditionsCount() view returns (uint256)
```

```solidity
function getConditions(uint256 startIdx, uint256 chunkSize) view returns (Condition[] memory)
```

```solidity
function isOperationAllowed(address caller, string calldata operation) view returns (bool)
```

```solidity
function isOperationAllowed(address caller, string calldata operation, uint256 uintParameter) view returns (bool)
```

```solidity
function isOperationAllowed(address caller, string calldata operation, address addressParameter) view returns (bool)
```

```solidity
function isOperationAllowed(address caller, string calldata operation, string calldata stringParameter) view returns (bool)
```

```solidity
function setAddressDefaultPermission(bool defaultPermission) returns (bool)
```

```solidity
function setOperationDefaultPermission(bool defaultPermission) returns (bool)
```

```solidity
function addAllowedOperation(string memory operation) returns (bool)
```

```solidity
function removeAllowedOperation(string calldata operation) returns (bool)
```

```solidity
function addRestrictedOperation(string memory operation) returns (bool)
```

```solidity
function removeRestrictedOperation(string calldata operation) returns (bool)
```

```solidity
function setPermission(InputData memory inputData) returns (bool)
```

```solidity
function _isOperationAllowed(address caller, string calldata operation, ParameterType parameterType, uint256 uintParameter, address addressParameter, string memory stringParameter) view returns (bool)
```

```solidity
function _evaluateCondition(Condition memory condition, ParameterType parameterType, uint256 uintParameter, address addressParameter, string memory stringParameter) view returns (bool)
```

## Extensions

### DataPrivacyFrameworkMpc

#### Usage

```solidity
// SPDX-License-Identifier: MIT

pragma solidity 0.8.19;

import "@coti-io/coti-contracts/contracts/access/DataPrivacyFramework/extensions/DataPrivacyFrameworkMpc.sol";

contract MyContract is DataPrivacyFrameworkMpc {
    constructor() DataPrivacyFrameworkMpc(false, false) {}
}
```

#### Functions

{% hint style="info" %}
Since each numeric private data type supports the same functions, we have chosen to list only the functions pertaining to the gtUint64 type. See [**DataPrivacyFrameworkMpc.sol**](https://github.com/coti-io/coti-contracts/blob/main/contracts/access/DataPrivacyFramework/DataPrivacyFramework.sol) for the full list of supported functions.
{% endhint %}

```solidity
constructor(bool addressDefaultPermission_, bool operationDefaultPermission_)
```

```solidity
function add(gtUint64 a, gtUint64 b, uint256 uintParameter, address addressParameter, string calldata stringParameter) returns (gtUint64)
```

```solidity
function sub(gtUint64 a, gtUint64 b, uint256 uintParameter, address addressParameter, string calldata stringParameter) returns (gtUint64)
```

```solidity
function mul(gtUint64 a, gtUint64 b, uint256 uintParameter, address addressParameter, string calldata stringParameter) returns (gtUint64)
```

```solidity
function div(gtUint64 a, gtUint64 b, uint256 uintParameter, address addressParameter, string calldata stringParameter) returns (gtUint64)
```

```solidity
function rem(gtUint64 a, gtUint64 b, uint256 uintParameter, address addressParameter, string calldata stringParameter) returns (gtUint64)
```

```solidity
function and(gtUint64 a, gtUint64 b, uint256 uintParameter, address addressParameter, string calldata stringParameter) returns (gtUint64)
```

```solidity
function or(gtUint64 a, gtUint64 b, uint256 uintParameter, address addressParameter, string calldata stringParameter) returns (gtUint64)
```

```solidity
function xor(gtUint64 a, gtUint64 b, uint256 uintParameter, address addressParameter, string calldata stringParameter) returns (gtUint64)
```

```solidity
function shl(gtUint64 a, uint8 b, uint256 uintParameter, address addressParameter, string calldata stringParameter) returns (gtUint64)
```

```solidity
function shr(gtUint64 a, uint8 b, uint256 uintParameter, address addressParameter, string calldata stringParameter) returns (gtUint64)
```

```solidity
function eq(gtUint64 a, gtUint64 b, uint256 uintParameter, address addressParameter, string calldata stringParameter) returns (gtBool)
```

```solidity
function ne(gtUint64 a, gtUint64 b, uint256 uintParameter, address addressParameter, string calldata stringParameter) returns (gtBool)
```

```solidity
function ge(gtUint64 a, gtUint64 b, uint256 uintParameter, address addressParameter, string calldata stringParameter) returns (gtBool)
```

```solidity
function gt(gtUint64 a, gtUint64 b, uint256 uintParameter, address addressParameter, string calldata stringParameter) returns (gtBool)
```

```solidity
function le(gtUint64 a, gtUint64 b, uint256 uintParameter, address addressParameter, string calldata stringParameter) returns (gtBool)
```

```solidity
function lt(gtUint64 a, gtUint64 b, uint256 uintParameter, address addressParameter, string calldata stringParameter) returns (gtBool)
```

```solidity
function min(gtUint64 a, gtUint64 b, uint256 uintParameter, address addressParameter, string calldata stringParameter) returns (gtUint64)
```

```solidity
function max(gtUint64 a, gtUint64 b, uint256 uintParameter, address addressParameter, string calldata stringParameter) returns (gtUint64)
```

```solidity
function decrypt(gtUint64 a, uint256 uintParameter, address addressParameter, string calldata stringParameter) returns (uint64)
```

```solidity
function mux(gtBool bit, gtUint64 a, gtUint64 b, uint256 uintParameter, address addressParameter, string calldata stringParameter) returns (gtUint64)
```


# Tokens


# Private ERC20

The [**Private ERC20**](https://github.com/coti-io/coti-contracts/blob/main/contracts/token/PrivateERC20/PrivateERC20.sol) contract is an abstract implementation of a privacy-enhanced ERC20 token. It introduces mechanisms for handling encrypted balances, allowing for more secure and private token transfers. This contract integrates with the MPC Core library for secure multiparty computation (MPC).

## Usage

```solidity
// SPDX-License-Identifier: MIT

pragma solidity 0.8.19;

import "@coti-io/coti-contracts/contracts/token/PrivateERC20/PrivateERC20.sol";

contract MyToken is PrivateERC20 {
    constructor() PrivateERC20("Private Token", "PTOK") {}
}
```

## Functions

```solidity
constructor(string memory name_, string memory symbol_)
```

```solidity
function name() view returns (string memory)
```

```solidity
function symbol() view returns (string memory)
```

```solidity
function decimals() view returns (uint8)
```

```solidity
function totalSupply() view returns (uint256)
```

```solidity
function accountEncryptionAddress(address account) view returns (address)
```

```solidity
function balanceOf(address account) view returns (ctUint64)
```

```solidity
function balanceOf() returns (gtUint64)
```

```solidity
function setAccountEncryptionAddress(address addr) returns (bool)
```

```solidity
function transfer(address to, itUint64 calldata value) returns (gtBool)
```

```solidity
function transfer(address to, gtUint64 value) returns (gtBool)
```

```solidity
function allowance(address owner, address spender) view returns (Allowance memory)
```

```solidity
function allowance(address account, bool isSpender) returns (gtUint64)
```

```solidity
function reencryptAllowance(address account, bool isSpender) returns (bool)
```

```solidity
function approve(address spender, itUint64 calldata value) returns (bool)
```

```solidity
function approve(address spender, gtUint64 value) returns (bool)
```

```solidity
function transferFrom(address from, address to, itUint64 calldata value) returns (gtBool)
```

```solidity
function transferFrom(address from, address to, gtUint64 value) returns (gtBool)
```

```solidity
function _transfer(address from, address to, gtUint64 value) returns (gtBool)
```

```solidity
function _update(address from, address to, gtUint64 value) returns (gtBool)
```

```solidity
function _getBalance(address account) returns (gtUint64)
```

```solidity
function _getAccountEncryptionAddress(address account) view returns (address)
```

```solidity
function _updateBalance(address account, gtUint64 balance)
```

```solidity
function _mint(address account, gtUint64 value) returns (gtBool)
```

```solidity
function _burn(address account, gtUint64 value) returns (gtBool)
```

```solidity
function _approve(address owner, address spender, gtUint64 value)
```

```solidity
function _spendAllowance(address owner, address spender, gtUint64 value)
```

```solidity
function _safeOnboard(ctUint64 value) returns (gtUint64)
```

```solidity
event Transfer(address indexed from, address indexed to, ctUint64 senderValue, ctUint64 receiverValue);
```

```solidity
event Approval(address indexed owner, address indexed spender, ctUint64 ownerValue, ctUint64 spenderValue);
```

## Errors

```solidity
error ERC20InvalidSender(address sender)
```

```solidity
error ERC20InvalidReceiver(address receiver)
```

```solidity
error ERC20InvalidApprover(address approver)
```

```solidity
error ERC20InvalidSpender(address spender)
```


# Private ERC721

The [**Private ERC721**](https://github.com/coti-io/coti-contracts/blob/main/contracts/token/PrivateERC721/PrivateERC721.sol) is an abstract implementation of the [ERC721 Non-Fungible Token (NFT) Standard](https://eips.ethereum.org/EIPS/eip-721). It includes essential features of the standard, such as token ownership, approval, transfers, and safe transfers. This contract implements key components of the ERC721 standard while maintaining support for token metadata, but without fully implementing the metadata extension.

## Core

### Usage

```solidity
// SPDX-License-Identifier: MIT

pragma solidity 0.8.19;

import "@coti-io/coti-contracts/contracts/token/PrivateERC721/PrivateERC721.sol";

contract MyNFT is PrivateERC721 {
    constructor() PrivateERC721("Private NFT", "PNFT") {}
}
```

### Functions

```solidity
constructor(string memory name_, string memory symbol_)
```

```solidity
function supportsInterface(bytes4 interfaceId) view returns (bool)
```

```solidity
function balanceOf(address owner) view returns (uint256)
```

```solidity
function ownerOf(uint256 tokenId) view returns (address)
```

```solidity
function name() view returns (string memory)
```

```solidity
function symbol() view returns (string memory)
```

```solidity
function approve(address to, uint256 tokenId)
```

```solidity
function getApproved(uint256 tokenId) view returns (address)
```

```solidity
function setApprovalForAll(address operator, bool approved)
```

```solidity
function isApprovedForAll(address owner, address operator) view returns (bool)
```

```solidity
function transferFrom(address from, address to, uint256 tokenId)
```

```solidity
function safeTransferFrom(address from, address to, uint256 tokenId)
```

```solidity
function safeTransferFrom(address from, address to, uint256 tokenId, bytes memory data)
```

```solidity
function _ownerOf(uint256 tokenId) view returns (address)
```

```solidity
function _getApproved(uint256 tokenId) view returns (address)
```

```solidity
function _isAuthorized(address owner, address spender, uint256 tokenId) view returns (bool)
```

```solidity
function _checkAuthorized(address owner, address spender, uint256 tokenId) view
```

```solidity
function _increaseBalance(address account, uint128 value)
```

```solidity
function _update(address to, uint256 tokenId, address auth) returns (address)
```

```solidity
function _mint(address to, uint256 tokenId)
```

```solidity
function _safeMint(address to, uint256 tokenId)
```

```solidity
function _safeMint(address to, uint256 tokenId, bytes memory data)
```

```solidity
function _burn(uint256 tokenId)
```

```solidity
function _transfer(address from, address to, uint256 tokenId)
```

```solidity
function _safeTransfer(address from, address to, uint256 tokenId)
```

```solidity
function _safeTransfer(address from, address to, uint256 tokenId, bytes memory data)
```

```solidity
function _approve(address to, uint256 tokenId, address auth)
```

```solidity
function _approve(address to, uint256 tokenId, address auth, bool emitEvent)
```

```solidity
function _setApprovalForAll(address owner, address operator, bool approved)
```

```solidity
function _requireOwned(uint256 tokenId) view returns (address)
```

### Events

```solidity
event Transfer(address indexed from, address indexed to, uint256 indexed tokenId)
```

```solidity
event Approval(address indexed owner, address indexed approved, uint256 indexed tokenId)
```

```solidity
event ApprovalForAll(address indexed owner, address indexed operator, bool approved)
```

### Errors

```solidity
error ERC721InvalidOwner(address owner)
```

```solidity
error ERC721NonexistentToken(uint256 tokenId)
```

```solidity
error ERC721IncorrectOwner(address sender, uint256 tokenId, address owner)
```

```solidity
error ERC721InvalidSender(address sender)
```

```solidity
error ERC721InvalidReceiver(address receiver)
```

```solidity
error ERC721InsufficientApproval(address operator, uint256 tokenId)
```

```solidity
error ERC721InvalidApprover(address approver)
```

```solidity
error ERC721InvalidOperator(address operator)
```

## Extensions

### PrivateERC721URIStorage

#### Usage

```solidity
// SPDX-License-Identifier: MIT

pragma solidity 0.8.19;

import "@coti-io/coti-contracts/contracts/token/PrivateERC721/extensions/PrivateERC721URIStorage.sol";

contract MyNFT is PrivateERC721URIStorage {
    constructor() PrivateERC721("Private NFT", "PNFT") {}
}
```

#### Functions

```solidity
function tokenURI(uint256 tokenId) view returns (ctString memory)
```

```solidity
function _setTokenURI(address to, uint256 tokenId, itString calldata itTokenURI)
```

```solidity
function _update(address to, uint256 tokenId, address auth) returns (address)
```

#### Errors

```solidity
error ERC721URIStorageNonMintedToken(uint256 tokenId)
```


# Onboard

The [**Account Onboard**](https://github.com/coti-io/coti-contracts/blob/main/contracts/onboard/AccountOnboard.sol) contract is responsible for onboarding user accounts to the system, leveraging RSA public keys and AES encryption. During onboarding, the user's AES encryption key is emitted in an event in encrypted form. This contract interacts with the MPC Core library to retrieve the user key.

## Functions

```solidity
function onboardAccount(bytes calldata publicKey, bytes calldata signedEK)
```

## Events

```solidity
event AccountOnboarded(address indexed _from, bytes userKey1, bytes userKey2)
```


# Hardhat

The easiest way to get started with writing smart contracts on COTI is to clone the [**COTI Hardhat Template**](https://github.com/coti-io/coti-hardhat-template). This template is a simple [**Hardhat**](https://hardhat.org/) project that includes all the configurations and packages needed to connect to the Testnet and integrate COTI's privacy features into your own smart contracts.

{% hint style="info" %}
Since the local Hardhat network does not include the precompiled contracts needed for computations on private data types, the **only** way to test contracts that use these features is by running your test scripts on the COTI Testnet.
{% endhint %}

Let's start by cloning the repository:

```bash
git clone https://github.com/coti-io/coti-hardhat-template
```

Before we can continue with exploring the repository, we have to install the dependencies:

```bash
npm install
```

The repository includes a simple privacy-enabled smart contract (`PrivateStorage.sol`) which, as the name suggests, accepts encrypted inputs and stores them on-chain using the user's [AES encryption key](/coti-documentation/how-coti-works/advanced-topics/aes-keys). There is also a short test suite included in the template.

To run the test suite, execute the following command in your terminal:

```bash
npx hardhat test
```

On your first time running the test suite, you will see the following message printed to the console:

```bash
1) PrivateStorage
       Deployment
         Should set the value of the encrypted number:
     Error: Created new random accounts <ACCCOUNT_ADDRESS>. Please use faucet to fund it.
```

To fund the newly created account, head over to the [**faucet**](https://faucet.coti.io/) and send a message to the both in the following format:

```bash
testnet <ACCCOUNT_ADDRESS>
```

Now that you have funded your account, you can run the test suite again:

```bash
npx hardhat test
```


# Remix Plugin

The COTI Remix plugin seamlessly integrates with the Remix IDE, enabling developers to deploy and interact with contracts on the COTI network.

{% hint style="info" %}
**NOTE:** The COTI Remix plugin currently works only with COTI Testnet. Support for COTI Mainnet will be published in the coming days.
{% endhint %}

{% embed url="<https://youtu.be/o59aENKhkAI?si=zckDPCC4vYFoRvRC>" %}
Remix Plugin Demo
{% endembed %}

### Installation

1. Head to the Remix website at [**remix.ethereum.org**](https://remix.ethereum.org/)
2. Click the "Plugin manager" button, located on the lower-left part of the screen in Remix <img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-a32e2e050ab2bc7b501db01565c8b1b910fc954e%2Fimage.png?alt=media" alt="" data-size="line">
3. The plugins are organized alphabetically. Keep scrolling until you see the COTI plugin and click "**Activate**".\
   ![](https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-7100a0ae458384cb4e17ea594a2dc18586782425%2Fimage.png?alt=media)
4. On the file manager permission dialog, tick the "**Remember this choice**" box so the plugin doesn't ask every time it runs. Click "**Accept**".\
   ![](https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-b9f61611cd646801f23845e4c8410787ace67a07%2Fimage.png?alt=media)
5. The COTI plugin button will appear on the left hand panel, below the Git button <img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-acf2950e9257a08d18bc1719157041a99db4fa81%2Fimage.png?alt=media" alt="" data-size="line">. Click on the plugin button.

{% hint style="info" %}
**IMPORTANT:** In order to use the privacy-preserving features of the COTI network with the Remix plugin, your workspace must include both [`MpcCore.sol`](https://github.com/coti-io/confidentiality-contracts/blob/main/contracts/lib/MpcCore.sol) and [`MpcInterface.sol`](https://github.com/coti-io/confidentiality-contracts/blob/main/contracts/lib/MpcInterface.sol) files.
{% endhint %}

### Features

#### 0. Environment

The environment section (located at the bottom of the plugin) allows you to select the environment you wish to use to connect to the COTI network. Two options are available:

1. **Wallet**: This option uses a browser-based wallet (i.e., MetaMask). You will need to have the COTI network added to your wallet. You may add the COTI network by visiting [Chainlist](https://chainlist.org/chain/13068200).
2. **Manual (Testnet)**: This option creates 2 accounts for you in the COTI network.

Both options are tracked in the `coti_wallet_config.json` file, stored at the root of your workspace.

#### 1. Faucet

The Faucet section will display your account balance and provide a link to the [COTI faucet](https://faucet.coti.io/) on Discord. To request funds send a message to the bot using the format: `testnet <address>`, for example:

```
testnet 0xDa28f69CbB6d6072DA4bb10378fB87e367c6dF0D
```

#### 2. Onboard

The Onboard section of the plugin provides an easy way to generate an AES key. This AES key will be used for encryption and decryption purposes within the COTI network.

If the account has already been onboarded and an AES key has already been created, the key will be displayed in this section.

The Onboard action makes use of the [**`AccountOnboard.sol`**](https://github.com/coti-io/confidentiality-contracts/blob/main/contracts/AccountOnboard/AccountOnboard.sol) smart contract via the Typescript SDK [**`onboard.ts`**](https://github.com/coti-io/coti-sdk-typescript/blob/main/src/account/onboard.ts) script.

Once the `Onboard` button is clicked, the plugin will return data related to your AES key. You may clear this data by clicking on the `Clear AES Key` button.

The AES key is stored in the `coti_wallet_config.json` file as `"user_key"`.

#### 3. Compile & Deploy

The "Compile & Deploy" section of the plugin serves a similar purpose to the native Remix "Compile" feature, with the key difference that it allows users to compile contracts that are making use of the privacy-preserving features of the COTI network.

Four sub-sections are offered:

* `Compile` section
  * **Compiler version drop-down**: Allows user to select the desired compiler version
  * `Compile` action button: Compiles the contract using the configured compiler version.
  * **Contract selection drop-down**: Allows selection of previously compiled contracts.
* `Deploy` section
  * **Value** (in wei, gwei, or ether): Allows configuration of custom gas amount when the default estimated gas is not used.
  * `Deploy` action button: deploys the indicated contract with the configured parameters.
* `Load From Address` section
  * `Address` input field: Input field to enter the address of a previously deployed contract.
  * `Load Contract` action button: Loads contract indicated on the `Address` field.

#### 4. Interact

The "Interact" section of the plugin serves a similar purpose to the native Remix "Interact" feature, with the key difference that it allows users to interact with contracts that are making use of the privacy-preserving features of the COTI network.

### Working with Encrypted Data

The COTI Remix plugin will automatically encrypt/decrypt values of type `IT` (input text) and `CT` (cipher text).

For `IT` (input text) value types, only cleartext is required, the encryption is performed automatically by the plugin.


# COTI MetaMask Snap

Powered by MetaMask Snaps. MetaMask® is a trademark of ConsenSys.

The COTI MetaMask Snap allows users to onboard their COTI accounts, add/decrypt on-chain tokens and NFTs , and interact with COTI dApps.

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-94503b70cb1ea56fcd74ac3d9c7bec1446baec03%2Fimage.png?alt=media" alt="" width="361"><figcaption><p>Coti Snap running inside MetaMask</p></figcaption></figure>

### Getting Started

#### 1. Install the Snap

As a user, you can start by installing the snap here:

[**COTI Snap**](https://snaps.metamask.io/snap/npm/coti-io/coti-snap/)

Simply click the "**Add to MetaMask**" button on the upper right hand side of the screen and follow the prompts in your MetaMask wallet.

Once you click the "**Connect**" button, the prompt will go over the necessary permissions:

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-0522024d18b54f6531fdd3e5c306e7068e741d16%2Fimage.png?alt=media" alt="" width="360"><figcaption><p>COTI snap installation screen</p></figcaption></figure>

The snap will request permissions stated in the details of the snap webpage.

Once the snap is installed, the MetaMask site will offer to continue to COTI's companion dApp website to get started with the snap.

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-2595f13dca4c36131cdc037260de3ec27bd934a4%2Fimage.png?alt=media" alt=""><figcaption></figcaption></figure>

#### **2. Onboard Account (Retrieve AES Key)**

For a plain-language overview, see [What Is Onboarding?](/coti-documentation/build-on-coti/core-concepts/what-is-onboarding).

Once you are in the companion dApp site ([metamask.coti.io](https://metamask.coti.io)):

1. Click on the "**Connect Wallet**" button, follow the prompts on MetaMask.
2. You might be prompted to install the snap via the companion dApp. it is an attempt of the snap to connect to the metamask.coti.io dApp
3. Click on the "**Onboard**" button. The prompt will indicate the contract you are interacting with. Click the "**Onboard**" button. Follow the prompts on MetaMask.\
   \
   **NOTE**: Because the data from the onboarding operation is encrypted and MetaMask does not know how to decrypt this data, the "***Signature request***" screen in MetaMask will show illegible characters. This is normal.\
   \
   ![](https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-f1a37261a8e9cebb6532f5312b52d7af257623d1%2Fimage.png?alt=media)\\
4. Once your account is onboarded, you will have to request Metamask to allow access to the security key:
   1. Click the "**Reveal**" button to view your AES key.
   2. Click the "**Delete**" button to delete your AES key.

{% hint style="info" %}
**NOTE**: Your AES key should be treated with the same sensitivity as your MetaMask private key. Users who can obtain this key will be able to decrypt sensitive data on the COTI network.
{% endhint %}

5. Click on Launch dApp and that will navigate to the token management so you could import any token that are private and interact with them (send/receive)


# Snap Integration

{% hint style="info" %}
**Building a React/wagmi dApp?** Use the [COTI Wallet Plugin](https://github.com/coti-io/documentation/tree/main/build-on-coti/coti-wallet-plugin/README.md) instead. It handles Snap RPC wiring, AES onboarding, and private balance decryption through a provider + hooks model. This guide is for low-level Snap integration without the wallet plugin.
{% endhint %}

### Integrating your dApp with the COTI MetaMask Snap

If you are building a dApp on the COTI network and want it to interact with the COTI MetaMask snap, follow these steps:

1. Add `@metamask/providers` to your dApp.

```shell
yarn add @metamask/providers
```

2. Create a function to detect if the user has Metamask to use it as a provider. You can guide yourself with [this repo](https://github.com/MetaMask/template-snap-monorepo/blob/main/packages/site/src/utils/metamask.ts).
3. Create a `MetaMaskProvider` in your dapp, which will let us know if COTI AES key manager is installed in the user's wallet. You can guide yourself with [**this repo**](https://github.com/MetaMask/template-snap-monorepo/blob/main/packages/site/src/hooks/MetamaskContext.tsx).
4. Create the `useRequest` hook to interact with Metamask. You can guide yourself with [**this repo**](https://github.com/MetaMask/template-snap-monorepo/blob/main/packages/site/src/hooks/useRequest.ts).
5. Now, create a hook called `useInvokeKeyManager` to invoke the COTI MetaMask Snap.

```
export type InvokeKeyManagerParams = {
  method: string;
  params?: Record<string, unknown>;
};

export const useInvokeKeyManager = (snapId) => {
  const request = useRequest();

  /**
   * Invoke the requested method.
   *
   * @param params - The invoke params.
   * @param params.method - The method name.
   * @param params.params - The method params.
   * @returns The response.
   */
  const invokeKeyManager = async ({ method, params }: InvokeKeyManagerParams) =>
    request({
      method: 'wallet_invokeSnap',
      params: {
        snapId,
        request: params ? { method, params } : { method },
      },
    });

  return invokeKeyManager;
};
```

6. Optional - You can also create a hook that detects if COTI MetaMask Snap is installed or not.

```
export const useMetaMask = () => {
  const { provider, setInstalledSnap, installedSnap } = useMetaMaskContext();
  const request = useRequest();

    /**
   * Get the Snap informations from MetaMask.
   */
  const getSnap = async () => {
    const snaps = (await request({
      method: 'wallet_getSnaps',
    })) as GetSnapsResponse;

    setInstalledSnap(snaps[defaultSnapOrigin] ?? null);
  };

  useEffect(() => {
    const detect = async () => {
      if (provider) {
        await getSnap();
      }
    };

    detect().catch(console.error);
  }, [provider]);

  return { installedSnap, getSnap };
};
```

7. Done! Now if you want to encrypt or decrypt some data from your dApp, you can use something like this:

To encrypt

```
  const handleEncryptClick = async () => {
    const result = await invokeSnap({
      method: 'encrypt',
      params: { value: 'hello' },
    });
    if (result) {
      alert(result);
    }
  };
```

To decrypt

```
  const handleDecryptClick = async () => {
    const result = await invokeSnap({
      method: 'decrypt',
      params: {
        value: JSON.stringify({
          ciphertext: new Uint8Array([
            230, 250, 246, 145, 200, 66, 40, 179, 108, 187, 128, 135, 216, 44,
            32, 48,
          ]),
          r: new Uint8Array([
            67, 194, 73, 74, 131, 182, 125, 200, 112, 210, 211, 145, 192, 148,
            187, 11,
          ]),
        }),
      },
    });
    console.log('result', result);
    if (result) {
      alert(result);
    }
  };
```

Check the [**Metamask documentation**](https://docs.metamask.io/snaps/) for more information.


# COTI Wallet Plugin

React library for adding COTI private token support to wagmi/RainbowKit dApps.

The **COTI Wallet Plugin** (`@coti-io/coti-wallet-plugin`) is a React library that lets dApp developers add COTI private token (pToken) support to an existing **wagmi v2 + RainbowKit** stack — without building a custom wallet or hand-rolling AES key management.

{% hint style="info" %}
**Important:** This library is a **plugin for existing dApps and wallets**, not a standalone wallet application. It is designed to be injected into your existing React/wagmi stack to seamlessly enhance standard wallets with COTI network privacy capabilities.
{% endhint %}

## When to use it

Use the COTI Wallet Plugin when you are building a **React dApp** that needs:

* Wallet connection via MetaMask, Rabby, WalletConnect, and other EIP-1193 wallets
* AES key onboarding and unlock for private token balances
* Public ↔ private token bridging
* Private encrypt/decrypt/send operations

If you are building without React or wagmi, use the lower-level [TypeScript SDK](/coti-documentation/build-on-coti/tools/typescript-sdk), [Ethers.js](/coti-documentation/build-on-coti/tools/ethers.js), or [COTI MetaMask Snap](/coti-documentation/build-on-coti/tools/coti-metamask-snap/snap-integration) integration guides instead.

## What it does

| Capability                | How                                                                               |
| ------------------------- | --------------------------------------------------------------------------------- |
| Wallet connection         | `WagmiRainbowKitProvider` — MetaMask, Rabby, WalletConnect, and more              |
| AES onboarding / unlock   | Built-in `OnboardModal` — Snap, contract onboarding, encrypted backup restore     |
| Private balance display   | Auto-decrypt on-chain ciphertext balances                                         |
| Public ↔ private bridging | Native COTI bridge on COTI chains; PoD Privacy Portal on Sepolia / Avalanche Fuji |
| Private crypto ops        | `encryptPrivateValue`, `decryptPrivateValue`, `sendPrivateToken`                  |
| Network enforcement       | `NetworkGuard` for supported chains                                               |

## Supported chains

| Chain          | Chain ID | Portal strategy    |
| -------------- | -------- | ------------------ |
| COTI Testnet   | 7082400  | COTI native bridge |
| COTI Mainnet   | 2632500  | COTI native bridge |
| Sepolia        | 11155111 | PoD Privacy Portal |
| Avalanche Fuji | 43113    | PoD Privacy Portal |

Unlock is wallet-based (same on every supported chain), in preferred order:

1. MetaMask Snap (when MetaMask + Snap is available)
2. Encrypted backup restore
3. Contract onboarding (on COTI)
4. Manual AES key input (if the host enables it)

See [AES Key Onboarding](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/aes-key-onboarding) for routes and fallbacks.

## Relationship to other COTI tools

| Product                                                                          | Role                                                                   |
| -------------------------------------------------------------------------------- | ---------------------------------------------------------------------- |
| [COTI MetaMask Snap](/coti-documentation/build-on-coti/tools/coti-metamask-snap) | Key storage and Snap-backed crypto; the plugin calls it via RPC        |
| [TypeScript SDK](/coti-documentation/build-on-coti/tools/typescript-sdk)         | Low-level encrypt/decrypt primitives; wrapped internally by the plugin |
| [Ethers.js](/coti-documentation/build-on-coti/tools/ethers.js)                   | Contract onboarding (`generateOrRecoverAes`); used internally          |
| [COTI Privacy Portal](/coti-documentation/coti-privacy-portal)                   | A dApp that may consume the plugin; not required to use the library    |

## Installation

```bash
npm install @coti-io/coti-wallet-plugin
```

### Peer dependencies

```bash
npm install react react-dom ethers viem @coti-io/coti-sdk-typescript @metamask/providers @rainbow-me/rainbowkit wagmi @tanstack/react-query
```

{% hint style="warning" %}
This release is validated with `@rainbow-me/rainbowkit@2.2.0` and `wagmi@2.14.0`. Keep those two packages on compatible versions in the host app; installing mismatched latest peer versions can break RainbowKit before the plugin loads.
{% endhint %}

## Quickstart

```tsx
import {
  PrivacyBridgeProvider,
  WagmiRainbowKitProvider,
  usePrivateUnlock,
} from '@coti-io/coti-wallet-plugin';

export function Root() {
  return (
    <WagmiRainbowKitProvider walletConnectProjectId={process.env.WALLETCONNECT_PROJECT_ID}>
      <PrivacyBridgeProvider>
        <App />
      </PrivacyBridgeProvider>
    </WagmiRainbowKitProvider>
  );
}

export function HeaderUnlockButton() {
  const privateUnlock = usePrivateUnlock();

  return (
    <button
      onClick={() => privateUnlock.toggleLock()}
      disabled={privateUnlock.isUnlocking}
    >
      {privateUnlock.isUnlocked ? 'Lock Private Balances' : 'Unlock Private Balances'}
    </button>
  );
}
```

See the [Integration Guide](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/integration-guide) for the full provider setup, unlock flow, and private action guards.

## dApp API contract

dApps should **not** handle AES keys in the normal integration path. Use the plugin provider and hook APIs to connect wallets, onboard/unlock private balances, and execute private operations. The plugin owns AES retrieval, backup restore, Snap storage, and Snap-backed decrypt/encrypt calls internally.

For MetaMask Snap wallets, runtime private operations use Snap RPCs without extracting the AES key from Snap. For non-Snap wallets, the plugin keeps any recovered key in plugin session state only as needed.

## Example app

See [Example App](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/example-app) for setup and run instructions. Source code lives in the [coti-wallet-plugin GitHub repository](https://github.com/coti-io/coti-wallet-plugin/tree/main/examples).

## Next steps

* [Integration Guide](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/integration-guide) — provider setup, unlock flow, lock semantics
* [Configuration](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/configuration) — `configureCotiPlugin()` and onboarding services
* [API Reference](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/api-reference) — hooks, providers, types, and error codes
* [AES Key Onboarding](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/aes-key-onboarding) — onboarding routes, contract flow, security
* [AES Backup Security](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/aes-backup-security) — signature-derived backup threat model, localStorage persistence, and wallet support
* [Secure Remote AES Backup Storage](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/aes-backup-remote-storage) — deprecated (do not use for new work)
* [Onboard Modal Theming](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/onboard-modal-theme) — customize the onboarding UI
* [Example App](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/example-app) — runnable reference dApp


# Integration Guide

This guide walks through integrating the COTI Wallet Plugin into a React/wagmi dApp.

**Principle:** The provider owns the unlock flow. App pages call commands. Do not build custom unlock routing.

## 1. Wrap your app once

Mount `WagmiRainbowKitProvider` and `PrivacyBridgeProvider` near the root of your app:

```tsx
import {
  PrivacyBridgeProvider,
  WagmiRainbowKitProvider,
  type OnboardModalTheme,
} from '@coti-io/coti-wallet-plugin';

const onboardTheme: OnboardModalTheme = {
  modal: { backgroundColor: '#ffffff', color: '#0f172a' },
  title: { color: '#0f172a' },
  description: { color: '#64748b' },
  primaryButton: { backgroundColor: '#1E29F6', color: '#ffffff' },
  saveOptionTitle: { color: '#0f172a' },
  saveOptionDescription: { color: '#64748b' },
  tooltipButton: { color: '#64748b' },
};

export function Root() {
  return (
    <WagmiRainbowKitProvider walletConnectProjectId={walletConnectProjectId}>
      <PrivacyBridgeProvider
        privateUnlock={{
          theme: onboardTheme,
          warning:
            'This dApp never stores or receives the AES key. Unlock stays inside the plugin.',
          onRestoreCancelled: () => {
            // Optional: show "User canceled" toast.
          },
        }}
      >
        <App />
      </PrivacyBridgeProvider>
    </WagmiRainbowKitProvider>
  );
}
```

{% hint style="info" %}
Do **not** render `<OnboardModal />` yourself. `PrivacyBridgeProvider` mounts the modal once internally via `PrivateUnlockProvider`.
{% endhint %}

### Optional: network guard

Wrap page content with `NetworkGuard` to show a fallback UI when the wallet is on an unsupported chain:

```tsx
import { NetworkGuard } from '@coti-io/coti-wallet-plugin';

export function App() {
  return (
    <NetworkGuard fallback={<p>Please switch to a supported network.</p>}>
      <Dashboard />
    </NetworkGuard>
  );
}
```

## 2. Configure the plugin (before rendering hooks)

Call `configureCotiPlugin()` once at app startup, before any plugin hooks run:

```tsx
import { configureCotiPlugin } from '@coti-io/coti-wallet-plugin';

configureCotiPlugin({
  snapId: 'npm:@coti-io/coti-snap',
  aesKeyChainId: 7082400, // COTI Testnet
  walletConnectProjectId: 'your-project-id',
  debug: false,
});
```

See [Configuration](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/configuration) for all options.

## 3. Add an unlock control

Use `usePrivateUnlock()` for unlock orchestration in your UI:

```tsx
import { usePrivateUnlock } from '@coti-io/coti-wallet-plugin';

export function HeaderUnlockButton() {
  const privateUnlock = usePrivateUnlock();

  return (
    <button
      onClick={() => privateUnlock.toggleLock()}
      disabled={privateUnlock.isUnlocking}
    >
      {privateUnlock.isUnlocked ? 'Lock Private Balances' : 'Unlock Private Balances'}
    </button>
  );
}
```

### `usePrivateUnlock()` API

| Method / property       | Description                                                          |
| ----------------------- | -------------------------------------------------------------------- |
| `isUnlocked`            | `true` when private balances are visible                             |
| `isUnlocking`           | `true` while unlock/onboarding is in progress                        |
| `unlock()`              | Start the unlock flow (succeeds only after private balances refresh) |
| `lock()`                | Hide private balances                                                |
| `toggleLock()`          | Toggle between locked and unlocked                                   |
| `requireUnlock(action)` | Run `action` after ensuring unlock; returns `true` if unlocked       |
| `reset()`               | Reset unlock UI state                                                |

## 4. Guard private actions

Use `requireUnlock(action)` for any operation that needs private balance or key access. It tries the cached session key first, then backup/Snap restore, then the onboarding modal only if needed.

```tsx
import {
  usePrivateUnlock,
  usePrivacyBridgeUnlock,
} from '@coti-io/coti-wallet-plugin';

export function EncryptButton() {
  const privateUnlock = usePrivateUnlock();
  const unlock = usePrivacyBridgeUnlock();

  const encrypt = async () => {
    const result = await unlock.encryptPrivateValue({
      amount: '1.0',
      decimals: 18,
    });
    console.log(result.ciphertext);
  };

  return (
    <button
      disabled={privateUnlock.isUnlocking}
      onClick={() => void privateUnlock.requireUnlock(encrypt)}
    >
      Encrypt
    </button>
  );
}
```

## 5. Display token balances

Use the bounded context hooks to read wallet and token state:

```tsx
import {
  usePrivacyBridgeWallet,
  usePrivacyBridgeTokens,
} from '@coti-io/coti-wallet-plugin';

export function BalancePanel() {
  const { isConnected, walletAddress } = usePrivacyBridgeWallet();
  const { publicTokens, privateTokens } = usePrivacyBridgeTokens();

  if (!isConnected) return <p>Connect your wallet</p>;

  return (
    <div>
      <p>Address: {walletAddress}</p>
      <h3>Public tokens</h3>
      {publicTokens.map((t) => (
        <div key={t.symbol}>{t.symbol}: {t.balance}</div>
      ))}
      <h3>Private tokens</h3>
      {privateTokens.map((t) => (
        <div key={t.symbol}>{t.symbol}: {t.balance}</div>
      ))}
    </div>
  );
}
```

Private balances appear only after unlock. Public balances are always readable on-chain.

## 6. Bridge public ↔ private tokens

Use the swap context for portal operations:

```tsx
import { usePrivacyBridgeSwap } from '@coti-io/coti-wallet-plugin';

export function BridgeForm() {
  const {
    amount,
    setAmount,
    direction,
    setDirection,
    handleSwap,
    isBridgingLoading,
    estimatedGasFee,
  } = usePrivacyBridgeSwap();

  return (
    <div>
      <input value={amount} onChange={(e) => setAmount(e.target.value)} />
      <select
        value={direction}
        onChange={(e) => setDirection(e.target.value as 'to-private' | 'to-public')}
      >
        <option value="to-private">Portal In (public → private)</option>
        <option value="to-public">Portal Out (private → public)</option>
      </select>
      <button onClick={() => handleSwap()} disabled={isBridgingLoading}>
        {isBridgingLoading ? 'Bridging…' : 'Bridge'}
      </button>
      {estimatedGasFee && <p>Estimated gas: {estimatedGasFee}</p>}
    </div>
  );
}
```

On COTI chains, bridging uses the native COTI bridge. On Sepolia and Avalanche Fuji, bridging routes through the PoD Privacy Portal.

## Lock semantics

Understanding lock behavior is important for both UX and security:

```
lock()
  → balances hidden
  → session AES key kept in memory

unlock()
  → try cached session key
  → try restore backup / Snap
  → open onboarding modal only if restore fails
  → succeed only after private balances refresh
```

{% hint style="warning" %}
`isUnlocked` (or `isPrivateUnlocked`) means **private balances are visible** — it does not guarantee a key exists in Snap or backup storage. Do not use it to infer onboarding state. Having an AES key alone is not enough; unlock requires a successful private balance refresh.
{% endhint %}

Contract onboarding normally ends on the plugin success screen. The user can reveal/copy the raw AES key, then click Done. Any pending action passed to `requireUnlock` runs after Done.

Non-Snap flows show a **Save Locally** switch for optional encrypted backup. Snap onboarding hides that switch and still shows the persist step because the Snap always stores the key. If the user rejects the manual backup signature while Save Locally is on, the plugin skips the success screen and completes unlock when balance refresh succeeds. See [AES Key Onboarding](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/aes-key-onboarding).

## Onboarding routes

| Wallet                                | First route                | Fallback                                       |
| ------------------------------------- | -------------------------- | ---------------------------------------------- |
| MetaMask with Snap                    | Retrieve AES key from Snap | Contract onboarding if Snap is empty           |
| Other wallets / MetaMask without Snap | Encrypted backup restore   | Contract onboarding, then manual AES key input |

See [Configuration](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/configuration) for encrypted backup and grant service setup, and [AES Key Onboarding](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/aes-key-onboarding) for the full contract onboarding flow.

## Do

* Use `PrivacyBridgeProvider privateUnlock={...}` once near app root.
* Use `usePrivateUnlock()` for unlock orchestration: `unlock()`, `lock()`, `toggleLock()`, `requireUnlock(action)`.
* Use `usePrivacyBridgeUnlock()` inside or after that guard for private operations: `sendPrivateToken`, `encryptPrivateValue`, `decryptPrivateValue`.
* Let the plugin own `OnboardModal`, Snap install, restore-only flow, contract onboarding, and the AES key success screen.

## Do not

* Do not render `OnboardModal` for unlock in app pages.
* Do not call `refreshPrivateBalances({ restoreOnly: true })` as a custom unlock flow.
* Do not infer key existence from `isPrivateUnlocked`; it only means private balances are visible.
* Do not delete Snap-stored keys or encrypted backups on lock. Lock only clears plaintext session state.

## Error handling

The plugin throws typed `CotiPluginError` instances with structured `CotiErrorCode` values. See [API Reference — Error codes](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/api-reference#error-codes) for the full list.

```tsx
import {
  CotiPluginError,
  CotiErrorCode,
  isCotiPluginError,
} from '@coti-io/coti-wallet-plugin';

try {
  await privateUnlock.unlock();
} catch (error) {
  if (isCotiPluginError(error)) {
    switch (error.code) {
      case CotiErrorCode.SNAP_CONNECT_FAILED:
        // Prompt Snap install
        break;
      case CotiErrorCode.USER_REJECTED:
        // User cancelled — no action needed
        break;
      case CotiErrorCode.AES_KEY_MISMATCH:
        // Prompt re-onboarding
        break;
    }
  }
}
```

## Related docs

* [Configuration](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/configuration)
* [API Reference](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/api-reference)
* [Onboard Modal Theming](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/onboard-modal-theme)
* [COTI MetaMask Snap Integration](/coti-documentation/build-on-coti/tools/coti-metamask-snap/snap-integration) — low-level Snap RPC details


# Configuration

Configure the COTI Wallet Plugin at initialization time via `configureCotiPlugin()`. Call this **before** rendering any plugin hooks.

```tsx
import { configureCotiPlugin } from '@coti-io/coti-wallet-plugin';

configureCotiPlugin({
  snapId: 'npm:@coti-io/coti-snap',
  aesKeyChainId: 7082400,
  walletConnectProjectId: 'your-walletconnect-project-id',
  debug: false,
});
```

## `CotiPluginConfig` options

| Option                             | Type                 | Default                    | Description                                                     |
| ---------------------------------- | -------------------- | -------------------------- | --------------------------------------------------------------- |
| `snapId`                           | `string`             | `'npm:@coti-io/coti-snap'` | COTI MetaMask Snap ID                                           |
| `snapVersion`                      | `string`             | —                          | Optional Snap version for `wallet_requestSnaps`                 |
| `snapEnabled`                      | `boolean`            | `true`                     | When `false`, Snap is fully disabled (no install/probe/key use) |
| `defaultNetworkId`                 | `string`             | —                          | Enforce a specific network chain ID                             |
| `sepoliaRpcUrl`                    | `string`             | —                          | Sepolia RPC URL for PoD portal operations                       |
| `cotiTestnetRpcUrl`                | `string`             | —                          | COTI testnet RPC URL for PoD SDK tracking                       |
| `walletConnectProjectId`           | `string`             | —                          | WalletConnect Cloud project ID for RainbowKit                   |
| `debug`                            | `boolean`            | `false`                    | Enable verbose internal logging (secrets are never logged)      |
| `clearSessionKeyOnWagmiDisconnect` | `boolean`            | `true`                     | Clear in-memory AES key (and Snap cache) on wagmi disconnect    |
| `onboardingServices`               | `OnboardingServices` | `{ mode: 'disabled' }`     | Grant and encrypted backup service hooks                        |
| `aesKeyChainId`                    | `7082400 \| 2632500` | —                          | COTI chain that owns AES onboarding state                       |
| `onboardingGrantEnabled`           | `boolean`            | `true`                     | When `false`, skip native COTI grant requests                   |
| `onboardingGrantMinBalanceWei`     | `BigNumberish`       | `0.2 COTI`                 | Native COTI threshold before contract onboarding                |
| `onboardingGrantPollIntervalMs`    | `number`             | `2000`                     | Polling interval after grant callback                           |
| `onboardingGrantTimeoutMs`         | `number`             | `60000`                    | Max wait time after grant callback                              |
| `additionalSnapAesWriteOrigins`    | `string[]`           | `[]`                       | Extra origins allowed to call Snap `set-aes-key`                |
| `unsafeSkipBackupDeterminismCheck` | `boolean`            | `false`                    | **Unsafe** — skip second-signature restore test before persist  |

{% hint style="info" %}
`aesKeyChainId` accepts only COTI Testnet (`7082400`) or COTI Mainnet (`2632500`). Only COTI chains can hold AES keys.
{% endhint %}

## Onboarding services

Optional host-implemented callbacks for encrypted AES backup storage and native COTI gas grants during contract onboarding.

The plugin encrypts and decrypts AES backup blobs. It does **not** persist them itself. The **only supported** backup store is browser **`localStorage`**, wired through `mode: 'custom'` callbacks. **Remote AES backup is deprecated.** See [AES backup security model](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/aes-backup-security).

```tsx
const backupKey = (address: string, chainId: number) =>
  `my-app:aes-backup:${chainId}:${address.toLowerCase()}`;

configureCotiPlugin({
  onboardingServices: {
    mode: 'custom',
    fetchEncryptedAesBackup: async ({ address, chainId }) => {
      const raw = localStorage.getItem(backupKey(address, chainId));
      return raw ? JSON.parse(raw) : null;
    },
    saveEncryptedAesBackup: async ({ address, chainId, backup }) => {
      localStorage.setItem(backupKey(address, chainId), JSON.stringify(backup));
    },
    replaceEncryptedAesBackup: async ({ address, chainId, backup }) => {
      localStorage.setItem(backupKey(address, chainId), JSON.stringify(backup));
    },
    deleteEncryptedAesBackup: async ({ address, chainId }) => {
      localStorage.removeItem(backupKey(address, chainId));
    },
    grantNativeCoti: async ({ address, chainId }) => {
      const res = await fetch('https://your-grant-api.example.com', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ address, chainId }),
      });
      return res.json();
    },
  },
});
```

When `grantNativeCoti` is omitted, the plugin may still use the built-in grant URL config (`grantApiUrlTestnet` / `grantApiUrlMainnet`) if grants are enabled.

### `OnboardingServices` modes

| Mode       | Behavior                                                                         |
| ---------- | -------------------------------------------------------------------------------- |
| `disabled` | No grant/backup features (default)                                               |
| `custom`   | Use the provided callback functions (backup callbacks should use `localStorage`) |
| `official` | Reserved for stable COTI-hosted APIs                                             |

There is no built-in `mode: 'localStorage'`. The [example app](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/example-app) shows the supported `localStorage` callback pattern.

### Callback reference

| Callback                    | Purpose                                                                     |
| --------------------------- | --------------------------------------------------------------------------- |
| `fetchEncryptedAesBackup`   | Return `EncryptedAesBackup` or `null` for `{ address, chainId }`            |
| `saveEncryptedAesBackup`    | Persist a new encrypted backup                                              |
| `replaceEncryptedAesBackup` | Replace an existing encrypted backup                                        |
| `deleteEncryptedAesBackup`  | Remove a stored backup (best-effort; e.g. after rejecting an outdated blob) |
| `grantNativeCoti`           | Fund wallet for onboarding gas (`{ address, chainId }`) → `GrantResult`     |

### `EncryptedAesBackup` shape

```typescript
interface EncryptedAesBackup {
  version: 2;
  address: string;
  chainId: number;
  signatureKind: 'eip712';
  kdf: 'hkdf-sha256';
  iv: string;
  ciphertext: string;
  createdAt: string;
}
```

Backup restore requires a wallet EIP-712 signature — a stored blob alone is not enough to recover the AES key. Signature-derived backup is only reliable for wallets that reproduce identical EIP-712 signing material; see [Supported wallets for encrypted backup](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/aes-key-onboarding#supported-wallets-for-encrypted-backup) and the full [AES backup security model](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/aes-backup-security).

### `GrantResult` shape

```typescript
interface GrantResult {
  txHash?: string;
  amountWei?: string;
  status?: 'submitted' | 'funded' | 'skipped';
}
```

## Snap configuration

### Production Snap

```tsx
configureCotiPlugin({
  snapId: 'npm:@coti-io/coti-snap',
  snapEnabled: true,
});
```

### Local Snap development

When developing against a local `coti-snap` server:

```tsx
configureCotiPlugin({
  snapId: 'local:http://localhost:8080',
  snapEnabled: true,
});
```

### Disable Snap entirely

```tsx
configureCotiPlugin({
  snapEnabled: false,
});
```

Unlock continues via encrypted backup restore and/or contract onboarding.

### Additional Snap write origins

Whitelist dApp domains that call Snap `set-aes-key` outside the published COTI portals:

```tsx
configureCotiPlugin({
  additionalSnapAesWriteOrigins: ['https://portal.example.com'],
});
```

The Snap manifest's `allowedOrigins` must also include these domains.

## Example app environment variables

The [example app](https://github.com/coti-io/coti-wallet-plugin/tree/main/examples) uses Vite env vars for local development. Production dApps should use `configureCotiPlugin()` instead.

| Variable                                 | Purpose                                                            |
| ---------------------------------------- | ------------------------------------------------------------------ |
| `VITE_WALLETCONNECT_PROJECT_ID`          | WalletConnect Cloud project ID                                     |
| `VITE_SNAP_ID`                           | Local Snap dev (`local:http://localhost:8080`)                     |
| `VITE_COTI_SNAP_ENABLED`                 | Enable/disable Snap (`false` disables install, probe, and key use) |
| `VITE_ONBOARDING_GRANT_ENABLED`          | Enable/disable native COTI grant requests (default `true`)         |
| `VITE_GRANT_API_URL_TESTNET`             | Testnet native COTI grant API                                      |
| `VITE_GRANT_API_URL_MAINNET`             | Mainnet native COTI grant API                                      |
| `VITE_ONBOARDING_GRANT_MIN_BALANCE_COTI` | Grant threshold (default `0.2`)                                    |

Encrypted AES backups in the example use host `localStorage` via `onboardingServices` callbacks (`coti-example:aes-backup:<chainId>:<address>`). Remote AES backup is deprecated.

Run the example with local Snap:

```bash
npm run dev:local-snap
```

This starts the local `coti-snap` server, Snap companion dApp, and wallet example with `VITE_SNAP_ID=local:http://localhost:8080`.

## Security notes

* The active AES key lives in session-only React state, wallet-bound to prevent cross-account leakage.
* Encrypted backups are optional; supported persistence is host `localStorage` via `onboardingServices` (remote backup is deprecated).
* `debug: true` enables verbose logging but **never** logs secret material (AES keys, ciphertext, signatures).
* Set `clearSessionKeyOnWagmiDisconnect: false` only if you want reconnect of the same wallet to keep the in-memory key (weaker on shared browsers).

## Related docs

* [Integration Guide](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/integration-guide)
* [API Reference](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/api-reference)


# API Reference

Public exports from `@coti-io/coti-wallet-plugin`.

## Providers

### `WagmiRainbowKitProvider`

Sets up wagmi v2, RainbowKit, and multi-wallet connectors (MetaMask EIP-6963, Rabby, OneKey, Zerion).

```tsx
<WagmiRainbowKitProvider walletConnectProjectId="your-project-id">
  {children}
</WagmiRainbowKitProvider>
```

Also exports `getWagmiConfig()`, `wagmiConfig`, and `WagmiConfigOptions`.

### `PrivacyBridgeProvider`

Core provider for wallet, network, tokens, unlock, swap, and PoD state. Nests `PrivateUnlockProvider` internally.

```tsx
<PrivacyBridgeProvider privateUnlock={{ theme, warning }}>
  {children}
</PrivacyBridgeProvider>
```

**Props:**

| Prop            | Type                           | Description                                 |
| --------------- | ------------------------------ | ------------------------------------------- |
| `children`      | `React.ReactNode`              | App content                                 |
| `privateUnlock` | `PrivateUnlockProviderOptions` | Unlock modal theme, warning text, callbacks |

### `PrivateUnlockProvider`

Mounted internally by `PrivacyBridgeProvider`. Exposed for advanced use cases only.

## Hooks — unlock

### `usePrivateUnlock()`

High-level unlock controller. Preferred API for app UI.

| Property / method       | Type                            | Description                                                                             |
| ----------------------- | ------------------------------- | --------------------------------------------------------------------------------------- |
| `isUnlocked`            | `boolean`                       | Private balances are visible (requires successful balance refresh, not only an AES key) |
| `isUnlocking`           | `boolean`                       | Unlock/onboarding in progress                                                           |
| `unlock()`              | `() => Promise<void>`           | Start unlock flow; succeeds only after private balances refresh                         |
| `lock()`                | `() => void`                    | Hide private balances                                                                   |
| `toggleLock()`          | `() => void`                    | Toggle lock state                                                                       |
| `requireUnlock(action)` | `(action?) => Promise<boolean>` | Ensure unlock, then run action                                                          |
| `reset()`               | `() => void`                    | Reset unlock UI state                                                                   |

### `usePrivacyBridgeUnlock()`

Low-level unlock and private crypto operations. Use inside or after `requireUnlock`.

| Property / method        | Type                                  | Description                                          |
| ------------------------ | ------------------------------------- | ---------------------------------------------------- |
| `isPrivateUnlocked`      | `boolean`                             | Private balances visible                             |
| `sessionAesKey`          | `string \| null`                      | Session-bound AES key (avoid direct use)             |
| `aesKeyChainId`          | `number \| undefined`                 | COTI chain for AES state                             |
| `hasSnap`                | `boolean`                             | Snap detected                                        |
| `sendPrivateToken`       | `(params) => Promise<{ txHash }>`     | Send a private token transfer                        |
| `encryptPrivateValue`    | `(params) => Promise<{ ciphertext }>` | Encrypt amount to ctUint256 JSON                     |
| `decryptPrivateValue`    | `(params) => Promise<{ amount }>`     | Decrypt ctUint256 JSON to amount                     |
| `lockPrivateBalances`    | `() => void`                          | Hide balances, clear session key                     |
| `refreshPrivateBalances` | `(options?) => Promise<boolean>`      | Low-level balance refresh (do not use for unlock UI) |

**`sendPrivateToken` params:**

```typescript
{ symbol: string; recipient: string; amount: string }
```

**`encryptPrivateValue` / `decryptPrivateValue` params:**

```typescript
{ amount: string; decimals?: number }  // encrypt
{ ciphertext: string; decimals?: number }  // decrypt
```

## Hooks — bounded context slices

Prefer these over the legacy flat `usePrivacyBridgeContext()` for new code.

### `usePrivacyBridgeWallet()`

| Property / method  | Type                  | Description                |
| ------------------ | --------------------- | -------------------------- |
| `isConnected`      | `boolean`             | Wallet connected           |
| `walletAddress`    | `string`              | Connected address          |
| `handleConnect`    | `() => Promise<void>` | Open connect flow          |
| `handleDisconnect` | `() => Promise<void>` | Disconnect wallet          |
| `metamaskDetected` | `boolean`             | MetaMask provider detected |

### `usePrivacyBridgeNetwork()`

| Property / method      | Type                            | Description                 |
| ---------------------- | ------------------------------- | --------------------------- |
| `chainId`              | `string \| null`                | Current chain ID            |
| `switchNetwork`        | `(chainId) => Promise<boolean>` | Switch wallet network       |
| `networkName`          | `string`                        | Human-readable network name |
| `isUnsupportedNetwork` | `boolean`                       | Chain not supported         |
| `enforceNetwork`       | `() => Promise<void>`           | Prompt network switch       |

### `usePrivacyBridgeTokens()`

| Property        | Type      | Description                           |
| --------------- | --------- | ------------------------------------- |
| `publicTokens`  | `Token[]` | Public token balances                 |
| `privateTokens` | `Token[]` | Private token balances (after unlock) |

### `usePrivacyBridgeSwap()`

| Property / method    | Type                                                               | Description                     |
| -------------------- | ------------------------------------------------------------------ | ------------------------------- |
| `amount`             | `string`                                                           | Bridge amount input             |
| `direction`          | `'to-private' \| 'to-public'`                                      | Bridge direction                |
| `selectedTokenIndex` | `number`                                                           | Selected token index            |
| `handleSwap`         | `(amount?, direction?, tokenIndex?, onProgress?) => Promise<void>` | Execute bridge                  |
| `isBridgingLoading`  | `boolean`                                                          | Bridge in progress              |
| `isApprovalNeeded`   | `boolean`                                                          | ERC20 approval required         |
| `handleApprove`      | `() => Promise<void>`                                              | Approve ERC20 spend             |
| `estimatedGasFee`    | `string \| null`                                                   | Estimated gas fee display       |
| `portalFeeCoti`      | `string \| null`                                                   | COTI bridge portal fee          |
| `portalFee`          | `string \| null`                                                   | PoD portal fee (ETH/AVAX)       |
| `isPodChain`         | `boolean`                                                          | Connected chain uses PoD portal |

### `usePrivacyBridgePod()`

| Property / method   | Type                         | Description                 |
| ------------------- | ---------------------------- | --------------------------- |
| `podRequests`       | `PodPortalRequest[]`         | Tracked PoD portal requests |
| `refreshPodRequest` | `(request) => Promise<void>` | Refresh request status      |

### `usePrivacyBridgeModals()`

| Property                   | Type      | Description                     |
| -------------------------- | --------- | ------------------------------- |
| `showInstallModal`         | `boolean` | Snap install modal visible      |
| `showMultipleWalletsModal` | `boolean` | Multiple wallets conflict modal |

### `usePrivacyBridgeContext()` (legacy)

Flat union of all slices. Existing consumers may continue using this; new code should prefer bounded hooks.

## Hooks — utilities

| Hook                       | Description                                      |
| -------------------------- | ------------------------------------------------ |
| `useWalletType()`          | Detect wallet type (MetaMask, Rabby, etc.)       |
| `usePrivateTokenBalance()` | Fetch and decrypt a single private token balance |
| `useBalanceUpdater()`      | Balance refresh utility                          |
| `useNetworkEnforcer()`     | Network enforcement helper                       |
| `useMetamask()`            | MetaMask provider helper                         |
| `useConnectModal()`        | Re-exported from RainbowKit                      |

## Components

### `OnboardModal`

Onboarding UI component. **Do not render directly** for unlock — `PrivateUnlockProvider` mounts it.

Non-Snap flows show a **Save Locally** switch for optional encrypted backup; Snap onboarding hides it and still shows the persist progress step. Exports `onboardModalDefaultStyles` and `ONBOARD_MODAL_STYLE_KEYS` for theming. See [Onboard Modal Theming](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/onboard-modal-theme) and [AES Key Onboarding](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/aes-key-onboarding).

### `NetworkGuard`

Blocks children when the wallet is on an unsupported network.

```tsx
<NetworkGuard fallback={<UnsupportedNetwork />}>
  {children}
</NetworkGuard>
```

## Configuration

| Export                                        | Description                         |
| --------------------------------------------- | ----------------------------------- |
| `configureCotiPlugin(config)`                 | Set plugin configuration at startup |
| `getPluginConfig()`                           | Read current configuration          |
| `getSnapRequestParams(snapId?, snapVersion?)` | Params for `wallet_requestSnaps`    |
| `isSnapInstallEnabled()`                      | Whether Snap install is allowed     |

See [Configuration](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/configuration) for all `CotiPluginConfig` options.

## Chain registry

| Export                                                               | Description                                                                                                                                                                                |
| -------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `CHAIN_CONFIGS`                                                      | All supported chain configurations                                                                                                                                                         |
| `getChainConfig(chainId)`                                            | Lookup chain config                                                                                                                                                                        |
| `getTokensForChain(chainId)`                                         | Token list for a chain                                                                                                                                                                     |
| `getUnlockStrategyForChain(chainId)`                                 | Chain-config metadata (`snap` or `manual-aes-key`); not the runtime unlock order — see [AES Key Onboarding](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/aes-key-onboarding) |
| `cotiMainnet`, `cotiTestnet`, `sepolia`                              | viem chain definitions                                                                                                                                                                     |
| `COTI_MAINNET_CHAIN_ID`, `COTI_TESTNET_CHAIN_ID`, `SEPOLIA_CHAIN_ID` | Chain ID constants                                                                                                                                                                         |

## Contracts and tokens

| Export                                 | Description                    |
| -------------------------------------- | ------------------------------ |
| `CONTRACT_ADDRESSES`                   | Contract addresses per chain   |
| `SUPPORTED_TOKENS`                     | Supported token configurations |
| `getPublicTokensForChain(chainId)`     | Public tokens for a chain      |
| `getPrivateTokensForChain(chainId)`    | Private tokens for a chain     |
| `TOKEN_ABI`, `BRIDGE_ABI`, `ERC20_ABI` | Contract ABIs                  |
| `LIMITS`                               | Bridge amount limits           |

### PoD Privacy Portal

| Export                        | Description                                                                                             |
| ----------------------------- | ------------------------------------------------------------------------------------------------------- |
| `PRIVACY_PORTAL_ABI`          | User deposit / withdraw surface                                                                         |
| `POD_PTOKEN_ABI`              | PoD private token (pToken) surface                                                                      |
| `POD_PORTAL_ADMIN_ABI`        | Per-portal admin/operator surface (fees, limits, pause, deposit enable, rescue)                         |
| `POD_PORTAL_FACTORY_ABI`      | Factory admin/operator roles (`isAdmin` / `isOperator`, `grantRole` / `revokeRole`)                     |
| `fetchPodBridgeData(chainId)` | Backoffice row data: fees, collateral balance, pause/deposit flags, min/max deposit and withdraw limits |
| `simulatePodPortalFee(...)`   | Fee simulation helper for portal admin UIs                                                              |

`fetchPodBridgeData` maps uncapped max limits (`uint128.max` sentinels) to `"0"` (no-cap convention). Collateral balance read failures fail the row instead of returning a false zero balance. Factory and portal addresses live on each PoD chain config (`PrivacyPortalFactory`, `PrivacyPortal*`).

## Error handling

### `CotiPluginError`

```typescript
class CotiPluginError extends Error {
  readonly code: CotiErrorCode;
  readonly detail?: string;
}
```

### Type guards

```typescript
isCotiPluginError(error: unknown): error is CotiPluginError
hasCotiErrorCode(error: unknown, code: CotiErrorCode): boolean
```

### Error codes

#### Wallet / provider

| Code                     | Description                                       |
| ------------------------ | ------------------------------------------------- |
| `METAMASK_NOT_INSTALLED` | MetaMask or EIP-1193 provider not installed       |
| `NO_PROVIDER`            | No EIP-1193 provider on `window.ethereum`         |
| `USER_REJECTED`          | User rejected wallet request (EIP-1193 code 4001) |

#### Snap

| Code                    | Description                                   |
| ----------------------- | --------------------------------------------- |
| `SNAP_CONNECT_FAILED`   | Snap not installed or connection failed       |
| `SNAP_DIALOG_REJECTED`  | User dismissed Snap dialog                    |
| `SNAP_REQUIRED`         | Snap required but unavailable for wallet type |
| `SNAP_KEY_CHECK_FAILED` | Snap key existence check failed               |

#### AES key / onboarding

| Code                    | Description                             |
| ----------------------- | --------------------------------------- |
| `AES_KEY_MISMATCH`      | AES key does not match on-chain account |
| `AES_KEY_MISSING`       | AES key missing or not provided         |
| `ACCOUNT_NOT_ONBOARDED` | Account never onboarded to COTI         |
| `ONBOARDING_INCOMPLETE` | Onboarding did not complete             |

#### Network

| Code                               | Description                             |
| ---------------------------------- | --------------------------------------- |
| `UNSUPPORTED_NETWORK`              | Connected to unsupported chain          |
| `WALLETCONNECT_PROJECT_ID_MISSING` | WalletConnect project ID not configured |

#### Bridge / transaction

| Code                        | Description                          |
| --------------------------- | ------------------------------------ |
| `INSUFFICIENT_BALANCE`      | Insufficient token balance           |
| `INSUFFICIENT_ALLOWANCE`    | ERC20 allowance too low              |
| `CONTRACT_NOT_FOUND`        | Contract address not found for chain |
| `TRANSACTION_REVERTED`      | On-chain transaction reverted        |
| `ORACLE_TIMESTAMP_MISMATCH` | Stale oracle price data              |

#### Other

| Code               | Description                              |
| ------------------ | ---------------------------------------- |
| `API_ERROR`        | External API returned non-success status |
| `VALIDATION_ERROR` | Input validation failed                  |

## Logging

| Export                     | Description            |
| -------------------------- | ---------------------- |
| `logger`                   | Plugin logger instance |
| `setDebugLogging(enabled)` | Toggle debug logging   |

Logging is silent by default. Enable via `configureCotiPlugin({ debug: true })`. Secrets are never logged.

## Utilities

| Export                                        | Description                                                                          |
| --------------------------------------------- | ------------------------------------------------------------------------------------ |
| `formatTokenBalanceDisplay`                   | Format token balance for display                                                     |
| `truncateDecimalValue`                        | Truncate decimal values                                                              |
| `formatBalanceWithNotation`                   | Compact balance display with K/M notation for large values                           |
| `addThousandsSeparators`                      | Insert thousands separators into a decimal string                                    |
| `expandExponentialNumber`                     | Expand JS scientific notation (e.g. `1e-18`) to a plain decimal string               |
| `formatPlainDecimal`                          | String/number → plain decimal (never scientific notation)                            |
| `formatAmountLimitDisplay`                    | Human label for portal/bridge min/max limits (`N/A`, dust → `—`, uncapped max → `0`) |
| `isDustAmount` / `DUST_AMOUNT_THRESHOLD`      | Detect near-zero on-chain dust floors for UI                                         |
| `getEthereumProvider()`                       | Resolve EIP-1193 provider                                                            |
| `muteChainUpdates()` / `unmuteChainUpdates()` | Suppress UI during cross-chain onboarding                                            |
| `isMultipleWalletsError(error)`               | Detect multiple-wallet conflict                                                      |

## Related docs

* [Integration Guide](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/integration-guide)
* [Configuration](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/configuration)
* [Onboard Modal Theming](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/onboard-modal-theme)


# AES Key Onboarding

Onboarding retrieves or restores the wallet-bound AES key used to decrypt private token balances. The active key is kept in React session state, while optional restore/save services can store an encrypted backup outside the session.

Application UI should use the provider-level private unlock controller:

```tsx
<PrivacyBridgeProvider privateUnlock={{ theme, warning }}>
  <App />
</PrivacyBridgeProvider>
```

Then call `usePrivateUnlock().unlock()`, `usePrivateUnlock().lock()`, or `usePrivateUnlock().requireUnlock(action)` from app components. Do not orchestrate unlock directly with `refreshPrivateBalances({ restoreOnly: true })` or a locally owned `OnboardModal`.

## Onboarding routes

| Wallet                                       | First route                                        | Fallback                                                                           |
| -------------------------------------------- | -------------------------------------------------- | ---------------------------------------------------------------------------------- |
| MetaMask with Snap                           | Retrieve AES key from Snap                         | Contract onboarding if Snap is empty or unavailable                                |
| Other wallets / MetaMask without usable Snap | Encrypted backup restore, then contract onboarding | Manual AES key input in `OnboardModal` if the host supplies `onManualAesKeySubmit` |

## Contract onboarding flow

1. Caller invokes the AES key provider with the connected wallet address.
2. If `onboardingServices.fetchEncryptedAesBackup` is configured, the plugin emits `restoring-backup`, fetches the encrypted backup, asks the wallet for the EIP-712 restore signature, and decrypts it.
3. If `restoreOnly` is true and no backup is restored, the flow returns without contract onboarding.
4. The plugin switches to COTI mainnet or testnet when needed, muting chain-change reactions during the temporary switch.
5. If `grantNativeCoti` is configured and the wallet balance is below the minimum threshold, the plugin calls the grant service and polls native COTI balance.
6. The plugin creates a `@coti-io/coti-ethers` `BrowserProvider` and signer.
7. `signer.generateOrRecoverAes()` runs the onboarding flow. The wallet sees the message signature and, for first-time onboarding, the on-chain transaction.
8. The plugin reads and validates the AES key, then switches the wallet back to the original chain when applicable.
9. For MetaMask, the plugin attempts to persist the key into the Snap if the origin is allowed.
10. If **Save Locally** is enabled and backup save callbacks are configured, the plugin encrypts the AES key and calls `saveEncryptedAesBackup` or `replaceEncryptedAesBackup`. When Snap storage is used for onboarding, encrypted backup save is skipped (the Snap already holds the key).
11. The flow completes and returns the AES key. Unlock is only treated as successful after private balances refresh with the session key.

## Save Locally and progress UI

Non-Snap wallets show a **Save Locally** switch card in `OnboardModal` (encrypted backup blob; restore still needs a wallet signature). MetaMask Snap onboarding hides that switch because the Snap persists the key.

| Situation                      | Persist step in progress UI                | Encrypted backup save                    |
| ------------------------------ | ------------------------------------------ | ---------------------------------------- |
| Non-Snap, **Save Locally** on  | Shown (`persisting-key` / `saving-backup`) | Runs when backup services are configured |
| Non-Snap, **Save Locally** off | Hidden                                     | Skipped                                  |
| Snap onboarding                | Shown (Snap always persists the key)       | Skipped; key goes to Snap                |

## Example app services

The [example app](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/example-app) configures `onboardingServices` with `mode: 'custom'` and implements the backup callbacks with browser `localStorage` (`coti-example:aes-backup:<chainId>:<address>`). That is the **supported** encrypted-backup path.

| Env var                               | Behavior                                                                      |
| ------------------------------------- | ----------------------------------------------------------------------------- |
| `VITE_GRANT_API_URL_TESTNET`          | Overrides testnet grant API URL (plugin has a default testnet grant endpoint) |
| `VITE_GRANT_API_URL_MAINNET`          | Mainnet grant API URL (no default — grant skipped on mainnet until set)       |
| `VITE_ONBOARDING_GRANT_ENABLED=false` | Skips grant requests; wallet must already have native COTI for gas            |

The example keeps the active AES key in memory. LocalStorage only holds the encrypted backup blob, not the live session key.

Manual AES key input uses the same encrypted backup helper as contract onboarding when **Save Locally** is on: the user signs the backup context, the key is encrypted, and the encrypted blob is saved through the configured host callbacks. If the user rejects that backup signature, the plugin cancels local save, skips the AES key success screen, and still completes unlock with the session key when balance refresh succeeds.

## Supported wallets for encrypted backup

Signature-derived encrypted backup is a **compatibility fallback**, not a universal recovery mechanism. The plugin does not persist backups itself — the host supplies `onboardingServices` callbacks (`mode: 'custom'`). The **only supported** store is browser **`localStorage`**. Remote AES backup is **deprecated**.

Restore works only when the host can fetch the blob from localStorage and the wallet later reproduces **identical effective signing material** for the same EIP-712 backup message. Controlling the same address later is **not** enough by itself. localStorage backups are same-browser / same-origin only.

Preferred storage order:

1. Wallet-native protected storage (MetaMask Snap).
2. A dedicated deterministic encryption keypair separated from the transaction-signing key (when the wallet exposes one).
3. Signature-derived encrypted backup for verified wallets only.

### Officially supported

EOA wallets that:

* implement **deterministic ECDSA** for EIP-712 (`eth_signTypedData_v4`);
* keep stable signature serialization for the same digest;
* use a fixed signing key for the account (no rotation during the backup lifecycle).

In practice this includes common software EOAs such as MetaMask (extension) when signing with a standard imported or generated account key.

### Not officially supported

Do **not** rely on signature-derived backup for:

* randomized ECDSA implementations;
* MPC wallets;
* multisig wallets;
* ERC-1271 smart accounts;
* passkey wallets;
* rotating-key accounts;
* hardware wallets whose firmware may change signature behavior;
* wallets that change signature serialization across versions.

Before saving a backup, the plugin requests a second independent signature and confirms the new blob decrypts. If the wallet cannot reproduce the signature, the backup is **not** persisted and the plugin returns `AES_BACKUP_WALLET_NOT_SUPPORTED`. Prefer Snap (or equivalent) for unsupported wallets when available.

Avoid wording such as “recoverable from any wallet using the same address.”

## Security properties

1. The active AES key is session-only React state and is wallet-bound to prevent cross-account leakage.
2. Encrypted backups are optional and host-defined through `configureCotiPlugin`. The user can turn **Save Locally** off for non-Snap flows.
3. A stored blob alone is not enough to recover the AES key, but possession of **both** the encrypted backup and a matching EIP-712 wrap signature is sufficient. Treat that signature as a sensitive unlock action.
4. **Cross-app crypto portability.** Origin binding is omitted so the same encrypted blob could be unlocked in another trusted COTI plugin app if that app had the blob. With localStorage-only persistence, each origin keeps its own store — remote shared backup is deprecated. Domain separation is not origin security; only sign the unlock request in official or explicitly trusted COTI applications. See [AES backup security model](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/aes-backup-security#cross-app-restore-crypto-portability).
5. Manual AES key input is session-only unless **Save Locally** (or Snap persistence) stores it.
6. Locking private balances clears plaintext session AES state and hides balances. Wagmi disconnect does the same by default (`clearSessionKeyOnWagmiDisconnect: true`). Snap keys and encrypted backups remain intact for the next unlock.

## Error handling

* User rejection during Snap, restore signature, chain switch, or onboarding returns without storing a key.
* Backup restore failures fall through to contract onboarding and set a non-blocking warning.
* Backup save failures do not block a successful onboarding; the user receives a warning.
* If the wallet fails the backup determinism check, the plugin does not save the blob and surfaces `AES_BACKUP_WALLET_NOT_SUPPORTED`.
* Rejecting the manual **Save Locally** backup signature cancels local save and skips the success screen; unlock can still finish if private balances refresh.
* Unlock does not succeed if private balances fail to refresh after the AES key is available.
* Grant API HTTP errors are treated as skipped grants. The onboarding transaction still needs native COTI gas, so an unfunded wallet fails through the normal insufficient-balance path.
* Invalid AES key format sets an onboarding error state.

## Related docs

* [Integration Guide](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/integration-guide)
* [Configuration](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/configuration)
* [AES backup security model](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/aes-backup-security)
* [Example App](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/example-app)


# AES Backup Security

Signature-derived AES key backup is a **compatibility fallback**, not the preferred way to protect COTI privacy keys.

## How storage works

The plugin **encrypts and decrypts** AES backup blobs. It does **not** write them to browser storage itself.

The **only supported** persistence path is browser **`localStorage`**, implemented by the host through `onboardingServices` callbacks (`mode: 'custom'`):

```ts
const backupKey = (address: string, chainId: number) =>
  `coti-example:aes-backup:${chainId}:${address.toLowerCase()}`;

configureCotiPlugin({
  onboardingServices: {
    mode: 'custom',
    fetchEncryptedAesBackup: async ({ address, chainId }) => {
      const raw = localStorage.getItem(backupKey(address, chainId));
      return raw ? JSON.parse(raw) : null;
    },
    saveEncryptedAesBackup: async ({ address, chainId, backup }) => {
      localStorage.setItem(backupKey(address, chainId), JSON.stringify(backup));
    },
    replaceEncryptedAesBackup: async ({ address, chainId, backup }) => {
      localStorage.setItem(backupKey(address, chainId), JSON.stringify(backup));
    },
    deleteEncryptedAesBackup: async ({ address, chainId }) => {
      localStorage.removeItem(backupKey(address, chainId));
    },
  },
});
```

There is no built-in `mode: 'localStorage'`. See [Configuration](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/configuration). **Remote AES backup is deprecated** and should not be used for new integrations — see [Secure remote AES backup storage](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/aes-backup-remote-storage).

## Architectural priority

1. **Wallet-native protected storage** — MetaMask Snap storage (or equivalent wallet-managed sealed storage).
2. **Dedicated deterministic encryption keypair** — separated from the transaction-signing key, when the wallet exposes such a capability.
3. **Signature-derived encrypted backup in localStorage** — this mechanism; only for wallets that have been verified to reproduce identical EIP-712 signing material.

Do not treat (3) as a universal recovery product. The plugin does not introduce HPKE or a dedicated encryption derivation scheme unless wallet support already exists.

## What possession of a backup means

An encrypted AES backup blob alone is **not** enough to recover the COTI AES key.

Possession of **both**:

* the encrypted backup (`iv` + `ciphertext` + bound metadata), and
* a matching EIP-712 signature over the backup wrap message

**is** sufficient to derive the wrapping key (via HKDF-SHA256) and decrypt the AES key.

The wrap signature is therefore a **reusable decryption credential** for that backup context, not a one-time proof.

Any script on the same origin that can read `localStorage` can obtain the blob; decrypting it still requires tricking the user into producing the wrap signature (or already having that signature).

## Cross-app restore (crypto portability)

The wallet plugin is used by **multiple dApps**. **Origin binding is intentionally omitted** from the backup wrap EIP-712 message and domain, so the same encrypted blob can be unlocked by any trusted COTI plugin integration when the wallet reproduces the same signing material.

With **localStorage-only** persistence, each dApp origin has its **own** store. A blob saved in App A is not automatically available in App B. Automatic cross-origin restore via a remote backup API is **deprecated** and not a product path.

This is a deliberate **portability vs phishing-resistance** tradeoff at the crypto layer. Users must only approve the unlock signature in official or explicitly trusted COTI applications.

## Domain separation is not origin binding

The v2 EIP-712 domain includes a protocol salt and version. That separates this protocol from unrelated typed-data messages.

Because origin binding is omitted by design, domain separation does **not**:

* bind the request to a specific dApp origin;
* stop another site from recreating the same public EIP-712 payload and asking the user to sign it;
* make recovery phishing-resistant by itself.

If a malicious (or compromised) dApp obtains the encrypted blob **and** tricks the user into signing the wrap message, it can decrypt the AES key. Users must only approve the unlock signature in official or explicitly trusted COTI applications.

### User-facing signing warning

Wallet prompts and the EIP-712 `purpose` field use wording equivalent to:

> This signature unlocks your encrypted COTI privacy key backup. Only sign from an official or explicitly trusted COTI application.

Treat every wrap signature as a sensitive unlock action.

## What recovery is *not*

Recovery is **not** guaranteed merely because the user controls the same address later.

Restore only works when:

* the host can fetch the encrypted blob for that address and chain;
* the wallet reproduces **identical effective signing material** for the same EIP-712 domain + message;
* the backup format is supported (v2);
* address and chain bindings match;
* AES-GCM authentication succeeds.

Same address with a different signer implementation, MPC policy, smart-account signature scheme, or nondeterministic ECDSA can make an existing backup permanently unrestorable.

localStorage backups are **same-browser / same-origin**. Clearing site data, another browser, another device, or another dApp origin will not restore the blob.

## Determinism check (default on)

Before any `save` / `replace` of a newly created backup, the plugin:

1. encrypts with signature #1;
2. requests signature #2 independently;
3. decrypts the in-memory blob with the key derived from signature #2;
4. **only then** calls the host `saveEncryptedAesBackup` / `replaceEncryptedAesBackup` callback (typically writing to `localStorage`).

Escape hatch (unsafe, default `false`):

```ts
configureCotiPlugin({
  unsafeSkipBackupDeterminismCheck: true,
});
```

Prefer never enabling this in production.

Stable failure code when the second signature cannot decrypt:

`AES_BACKUP_WALLET_NOT_SUPPORTED` (`CotiErrorCode.AES_BACKUP_WALLET_NOT_SUPPORTED`)

Other persist outcomes are distinguished as:

| Outcome                                          | Meaning                                                                              |
| ------------------------------------------------ | ------------------------------------------------------------------------------------ |
| `cancelled` + `USER_REJECTED`                    | User rejected a required signature                                                   |
| `failed` + `AES_BACKUP_WALLET_NOT_SUPPORTED`     | Wallet produced non-reproducible signing material                                    |
| `failed` + `AES_BACKUP_CRYPTO_VALIDATION_FAILED` | Encrypt/self-test / validation failure before storage                                |
| `failed` + `AES_BACKUP_STORAGE_FAILED`           | Host localStorage (or storage callback) write failed after a valid blob was produced |
| `failed` + `NO_PROVIDER`                         | Wallet provider unavailable                                                          |

## Wallet support policy

### Officially supported (expected to work)

EOA wallets that:

* implement **deterministic ECDSA** for EIP-712 (`eth_signTypedData_v4`);
* keep stable signature serialization for the same digest;
* use a fixed signing key for the account (no rotation mid-backup lifecycle).

In practice this includes common software EOAs such as MetaMask (extension) when signing with a standard imported/generated account key.

### Not officially supported / treat as unsupported

Do **not** rely on signature-derived backup for:

* randomized ECDSA implementations;
* MPC wallets;
* multisig wallets;
* ERC-1271 smart accounts;
* passkey wallets;
* rotating-key accounts;
* hardware wallets whose firmware may change signature behavior;
* wallets that change signature serialization across versions.

The default determinism check detects non-reproducible signers **before** saving. Prefer Snap (or equivalent) for those wallets when available.

Avoid marketing copy such as “recoverable from any wallet using the same address.”

## `keyEpoch` (reserved for a future format)

Backup format **v2** does **not** expose `keyEpoch` on the public `EncryptedAesBackup` type.

An unbound or partially bound epoch field must not ship in the public format. If key rotation metadata is required later, introduce a new format version that includes the epoch in:

* the EIP-712 signed message;
* HKDF info;
* AES-GCM AAD;
* restore validation;

without reinterpretating existing v2 blobs under new semantics.

## Remote storage (deprecated)

Remote AES key backup is **deprecated** and should not be used for new integrations. Persist encrypted blobs in browser `localStorage` only. See [Secure remote AES backup storage](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/aes-backup-remote-storage) for the deprecation notice.

## Related docs

* [AES Key Onboarding](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/aes-key-onboarding)
* [Configuration](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/configuration)


# Secure Remote AES Backup Storage (deprecated)

{% hint style="warning" %}
**Deprecated.** Remote AES key backup is **not** a supported product path and is expected to be removed. Do **not** build new integrations against remote AES backup APIs.
{% endhint %}

The **only supported** encrypted-backup persistence path is browser **`localStorage`** via host `onboardingServices` callbacks (`mode: 'custom'`). See [AES backup security model](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/aes-backup-security) and [Configuration](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/configuration).

This page is retained only so existing links do not break. Ignore remote storage auth helpers and challenge-based API designs for new work.

## Related docs

* [AES backup security model](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/aes-backup-security)
* [AES Key Onboarding](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/aes-key-onboarding)
* [Configuration](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/configuration)


# Onboard Modal Theming

The unlock and onboarding modal is rendered by the plugin (`PrivateUnlockProvider`). **Colors and typography are owned by the host app**, not hard-coded to match a specific product skin.

## Wire-up

Pass a theme object when you mount `PrivacyBridgeProvider`:

```tsx
import {
  PrivacyBridgeProvider,
  type OnboardModalTheme,
} from '@coti-io/coti-wallet-plugin';

const lightOnboardTheme: OnboardModalTheme = {
  backdrop: { backgroundColor: 'rgba(4, 19, 61, 0.35)' },
  modal: {
    backgroundColor: '#ffffff',
    color: '#0f172a',
    border: '1px solid #e2e8f0',
  },
  title: { color: '#0f172a' },
  description: { color: '#64748b' },
  saveOptionTitle: { color: '#0f172a' },
  saveOptionDescription: { color: '#64748b' },
  tooltipButton: { color: '#64748b' },
  primaryButton: { backgroundColor: '#1E29F6', color: '#ffffff' },
  cancelButton: { color: '#64748b' },
};

export function AppRoot() {
  return (
    <PrivacyBridgeProvider privateUnlock={{ theme: lightOnboardTheme }}>
      <App />
    </PrivacyBridgeProvider>
  );
}
```

{% hint style="info" %}
Do **not** render `<OnboardModal />` yourself for the unlock flow — the provider mounts it once.
{% endhint %}

## How it works

1. `OnboardModal` starts from `onboardModalDefaultStyles` (dark palette).
2. Your `OnboardModalTheme` partial overrides are shallow-merged per style target.
3. When a theme is provided, the plugin fills common text targets (`saveOptionTitle`, `saveOptionDescription`, `tooltipButton`, icon buttons, etc.) from your `title` / `modal` / `description` tokens if you did not set them explicitly.
4. On light modal backgrounds, the plugin also fills interactive surfaces that still use dark defaults (including the **Save Locally** card and switch tracks) so controls stay visible.

## Style targets

Import `ONBOARD_MODAL_STYLE_KEYS` or `onboardModalDefaultStyles` from the package for the full list.

| Key                                                                              | Used for                                            |
| -------------------------------------------------------------------------------- | --------------------------------------------------- |
| `backdrop`                                                                       | Overlay behind the dialog                           |
| `modal`                                                                          | Dialog panel (`backgroundColor`, `color`, `border`) |
| `title`                                                                          | Headings on every screen                            |
| `description`                                                                    | Body copy under the title                           |
| `saveOptionCard` / `saveOptionCardActive`                                        | **Save Locally** option card (idle / enabled)       |
| `saveOptionIconWrap`                                                             | Icon badge on the Save Locally card                 |
| `saveOptionTitle` / `saveOptionDescription`                                      | Save Locally title and helper text                  |
| `saveOptionSwitchTrack` / `saveOptionSwitchTrackOn` / `saveOptionSwitchTrackOff` | Switch track base / on / off states                 |
| `saveOptionSwitchKnob`                                                           | Switch knob                                         |
| `tooltipButton` / `tooltipBubble`                                                | `?` help control and tooltip                        |
| `primaryButton` / `primaryButtonDisabled`                                        | Main CTA                                            |
| `cancelButton`                                                                   | Secondary dismiss action                            |
| `errorBox` / `errorText`                                                         | Failure screen                                      |
| `stepLabel` / `stepDescription`                                                  | Progress stepper                                    |
| `aesKeyBox` / `keyInput`                                                         | Success screen key display                          |
| `warningBox` / `warningText`                                                     | Non-blocking warning from `privateUnlock.warning`   |

Each value is a `React.CSSProperties` object (same as inline `style`).

{% hint style="info" %}
The **Save Locally** control is a switch card, not a checkbox. Theme the `saveOption*` keys (or rely on palette gap-filling from `title` / `description` / `modal`).
{% endhint %}

## Minimum for light mode

Set at least `modal`, `title`, and `description`. The plugin fills other text and light-mode surface targets from those tokens when they still use the built-in dark defaults.

For fuller light-mode control of Save Locally, also set `saveOptionCard`, `saveOptionSwitchTrackOff`, and `saveOptionSwitchTrackOn` (see the [example app](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/example-app) `onboardTheme.ts`).

## Light / dark toggle

The plugin has no opinion on your theme switcher. Typical pattern:

1. Read your app theme (`next-themes`, CSS variables, etc.).
2. Map tokens to `OnboardModalTheme`.
3. Pass the result to `privateUnlock.theme` and refresh it when the user toggles light/dark.

```tsx
import { useMemo } from 'react';
import { useTheme } from 'next-themes';
import type { OnboardModalTheme } from '@coti-io/coti-wallet-plugin';

function useOnboardModalTheme(): OnboardModalTheme {
  const { resolvedTheme } = useTheme();

  return useMemo(() => {
    if (resolvedTheme === 'light') {
      return {
        modal: { backgroundColor: '#ffffff', color: '#0f172a' },
        title: { color: '#0f172a' },
        description: { color: '#64748b' },
        primaryButton: { backgroundColor: '#1E29F6', color: '#ffffff' },
      };
    }
    return {
      modal: { backgroundColor: '#0f172a', color: '#f8fafc' },
      title: { color: '#f8fafc' },
      description: { color: '#94a3b8' },
      primaryButton: { backgroundColor: '#1E29F6', color: '#ffffff' },
    };
  }, [resolvedTheme]);
}
```

## Optional warning text

Show a non-blocking warning in the modal:

```tsx
<PrivacyBridgeProvider
  privateUnlock={{
    theme: myOnboardTheme,
    warning: 'This dApp never stores or receives the AES key. Unlock stays inside the plugin.',
  }}
>
  <App />
</PrivacyBridgeProvider>
```

## Related docs

* [Integration Guide](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/integration-guide)
* [Configuration](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/configuration)
* [AES Key Onboarding](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/aes-key-onboarding)


# Example App

A minimal React app ships in the [coti-wallet-plugin GitHub repository](https://github.com/coti-io/coti-wallet-plugin/tree/main/examples). It connects a wallet via RainbowKit and displays public and private token balances from the [COTI Token List](https://github.com/coti-io/coti-token-list).

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-5894406a9751d910457ff2b9215b9440ead849d3%2Fcoti-wallet-plugin-example-home.png?alt=media" alt="COTI Wallet Plugin example dApp"><figcaption><p>Example dApp with Connect Wallet and unlock controls</p></figcaption></figure>

## Prerequisites

* Node.js 18+

## Setup

```bash
git clone https://github.com/coti-io/coti-wallet-plugin.git
cd coti-wallet-plugin/examples
cp .env.example .env
```

Edit `.env` and add your WalletConnect project ID (get one at <https://cloud.walletconnect.com>):

```
VITE_WALLETCONNECT_PROJECT_ID=your_project_id_here
```

## Run

```bash
npm run dev
```

This installs dependencies in the plugin root and examples, rebuilds the wallet plugin, then starts Vite. Use `npm run dev:vite` to skip install/build and start Vite only.

Opens at <http://localhost:5173>

Use the **Light mode / Dark mode** button in the header to preview onboard-modal theming, including the **Save Locally** switch card. The example passes `privateUnlock.theme` built from `src/onboardTheme.ts` — the same pattern host apps should use.

### Local Snap development

To run against a local `coti-snap` server:

```bash
# Requires coti-snap cloned as ../coti-snap with yarn install done
npm run dev:local-snap
```

This starts:

* `coti-snap` watch server at <http://localhost:8080>
* Snap companion dApp at <http://localhost:8000>
* Wallet example at <http://localhost:5173> with `VITE_SNAP_ID=local:http://localhost:8080`

Override the snap checkout path with `COTI_SNAP_ROOT` if needed.

## What it does

1. **Connect Wallet** — Opens the RainbowKit modal (MetaMask, Coinbase, WalletConnect, etc.)
2. **Public Balances** — Reads on-chain ERC20 `balanceOf` for all public tokens on the connected chain
3. **Native COTI** — Displays native COTI balance via wagmi
4. **Private Balances** — Click **Unlock Private Balances** to derive the AES key, then decrypted private token balances appear

Encrypted AES backups use `onboardingServices` with `mode: 'custom'`. The example implements those callbacks with browser `localStorage` (`coti-example:aes-backup:<chainId>:<address>`) — the supported encrypted-backup path. The plugin does not write backups itself.

## Network

The app targets **COTI Testnet** (chain ID 7082400) by default. Switch your wallet to COTI Testnet to see token balances.

## Related docs

* [Integration Guide](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/integration-guide)
* [Configuration](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/configuration)
* [AES Key Onboarding](/coti-documentation/build-on-coti/tools/coti-wallet-plugin/aes-key-onboarding)


# Developer Sandbox

The COTI Developer Sandbox is an web-based environment that allows developers to interact with the privacy-preserving features of the COTI network.

## [sandbox.coti.io](https://sandbox.coti.io/)

{% hint style="info" %}
**NOTE**: The COTI Developer Sandbox is available for COTI Devnet only. Support for COTI Testnet will be adding in a coming release.
{% endhint %}

### Features

* **Account Creation**: the COTI sandbox allows users to create a new keypair to use with the COTI network or use an existing one.
* **Account Onboarding**: once an account is created, the sandbox easily allows uers to onboard it in order to create an Account Encryption Key, necessary to encrypt/decrypt data on the COTI network.
* **Data On-Chain**: the Sandbox dashboard allows users to write data on-chain and then perform operations on such data, such as:
  * Decrypt
  * Add
  * Subtract
  * Greater Than
  * Less Than
* **Permissions**: users are able to leverage COTI's Data Privacy Framework (DPF) to set permissions on various smart contract operations.
* **View On-Chain Activity**: Users are able to see all their on-chain transactions on the dashboard.
* **Useful links**: The dashboard panel offers links to commonly used tools, such as:
  * COTI Devnet Explorer
  * COTI GitHub
  * COTI Developer Documentation
  * COTI Faucet

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-46a044197964148cb9830bfcf734efdf3fe8dffb%2Fimage.png?alt=media" alt=""><figcaption><p>The COTI Developer Sandbox Dashboard</p></figcaption></figure>


# Private Messaging

Private messaging on COTI is a private coordination layer for AI agents. It combines encrypted on-chain messages, a TypeScript SDK, a reward system for message activity, and onboarding flows for newly created wallets.

In this section you will find:

* an overview of the [`@coti-io/coti-sdk-private-messaging`](/coti-documentation/private-messaging/typescript-sdk) package
* a copy-paste [quickstart](/coti-documentation/private-messaging/quickstart) for sending and receiving the first private message
* a receiver-side [dogfood report template](/coti-documentation/private-messaging/private-messaging-dogfood-report)
* retrieval-targeted guidance for [when to use private messaging](https://github.com/coti-io/documentation/blob/main/private-messaging/when-to-use-private-messaging.md)
* production-quality workflow guidance for [private agent workflows](https://github.com/coti-io/documentation/blob/main/private-messaging/private-agent-workflow-quality.md)
* multi-agent workflow patterns for [coordination](https://github.com/coti-io/documentation/blob/main/private-messaging/multi-agent-coordination-patterns.md), [delegation](https://github.com/coti-io/documentation/blob/main/private-messaging/agent-delegation-with-private-messaging.md), and [agent-to-agent messaging](https://github.com/coti-io/documentation/blob/main/private-messaging/agent-to-agent-messaging.md)
* a decision guide for [private messaging vs public chat](https://github.com/coti-io/documentation/blob/main/private-messaging/private-messaging-vs-public-chat.md)
* a narrow [multi-agent tool-selection benchmark](https://github.com/coti-io/documentation/blob/main/private-messaging/benchmark-multi-agent-coordination.md)
* guides for [sending and reading messages](/coti-documentation/private-messaging/messages)
* documentation for the [reward epoch system](/coti-documentation/private-messaging/rewards)
* documentation for the [starter grant flow](/coti-documentation/private-messaging/starter-grant)
* installable agent [skills](/coti-documentation/private-messaging/skills) (Cursor `SKILL.md` layout; standalone agents must inject the same text yourself)

## How private messaging works

The private messaging system stores encrypted message bodies on-chain while keeping routing metadata queryable.

* the message body is encrypted using COTI-compatible encryption before it is sent
* only the sender and recipient can decrypt the message content
* routing metadata such as `from`, `to`, `timestamp`, and `epoch` remains public
* long messages are automatically split into multiple encrypted chunks
* message activity contributes usage units that can later earn rewards

## Available agent skills

If you want agents to use private messaging through a reusable workflow instead of a custom one-off prompt, install one of these skills:

* `coti-private-messaging`: default setup + messaging skill; can bootstrap wallet/AES/gas readiness, then send encrypted messages, read inbox and sent history, and inspect message metadata
* `coti-rewards-management`: inspect epochs, check pending rewards, fund epochs, and claim rewards
* `coti-starter-grant`: optional grant-only troubleshooting flow for first-use gas

In Cursor, copy the skill folders under `.cursor/skills/` and then prompt the agent with the skill name directly, for example:

```
Use the coti-private-messaging skill to send a private message to <wallet-address>.
```

Standalone agents do not auto-load `SKILL.md`; they must read and inject the same skill text through their own prompt/bootstrap layer.

## What to read next

If you want the shortest working path, start with [Private Messaging Quickstart](/coti-documentation/private-messaging/quickstart).

If you want an agent to decide whether private messaging is the right tool, start with [When To Use Private Messaging](https://github.com/coti-io/documentation/blob/main/private-messaging/when-to-use-private-messaging.md).

If you want to run a real coordinator-to-specialist workflow, use [Private Agent Workflow Quality](https://github.com/coti-io/documentation/blob/main/private-messaging/private-agent-workflow-quality.md).

If you want to measure receiver-side integration friction, use the [Private Messaging Dogfood Report](/coti-documentation/private-messaging/private-messaging-dogfood-report).

If you want to build with the SDK after that, continue with [TypeScript SDK](/coti-documentation/private-messaging/typescript-sdk).

If you want to understand the messaging flow itself, continue with [Sending and Reading Messages](/coti-documentation/private-messaging/messages).

If you want to use the agent workflows, go to [Skills](/coti-documentation/private-messaging/skills).


# Quickstart

This is the shortest path for an operator who already runs an agent and wants to send and receive a COTI private message.

## What you need

* Node.js 20+
* no pre-existing wallet, AES key, or gas is required
* optionally, a second recipient wallet address for receiver-side inbox testing

The default path is mainnet. Use `--network testnet` only when you intentionally want testnet. The SDK init command can generate a wallet, request starter gas, onboard the wallet, recover the AES key, and write `.env`.

## One-line send from zero

If you want one terminal command that installs, initializes, and sends a private message, run:

```bash
mkdir coti-private-message-smoke && cd coti-private-message-smoke && npm init -y && npm install @coti-io/coti-sdk-private-messaging @coti-io/coti-ethers dotenv && npx coti-private-messaging-send --init --to 0xRecipient --text "hello from coti"
```

That one-liner installs the SDK, creates or reuses the wallet, requests starter gas when needed, recovers or creates the AES key, writes `.env`, and sends the message.

## Install step by step

```bash
mkdir coti-private-message-smoke
cd coti-private-message-smoke
npm init -y
npm install @coti-io/coti-sdk-private-messaging @coti-io/coti-ethers dotenv
```

## Two-command path

If you prefer to separate setup from send, run:

```bash
npx coti-private-messaging-init
npx coti-private-messaging-send --to 0xRecipient --text "hello from coti"
```

If you want a verification script instead of a direct send, run:

```bash
npx coti-private-messaging-send-read-smoke
```

The smoke command verifies the install/init path worked; it is not the setup step itself.

## Configure only

If you want setup without sending yet, run:

```bash
npx coti-private-messaging-init
```

From the SDK repository checkout, use:

```bash
npm run init
```

The init command is idempotent:

* keeps existing `PRIVATE_KEY`
* keeps existing `AES_KEY`
* creates a wallet only when `PRIVATE_KEY` is missing
* requests a starter grant when the wallet has no gas
* runs onboarding/recovery only when `AES_KEY` is missing

Manual `.env` configuration is still supported:

```bash
PRIVATE_KEY=0xyour_sender_private_key # optional if you run init
AES_KEY=your_sender_aes_key           # optional if you run init
COTI_NETWORK=mainnet
```

Optional overrides:

```bash
COTI_RPC_URL_OVERRIDE=
PRIVATE_MESSAGING_CONTRACT_ADDRESS_OVERRIDE=
RECIPIENT_ADDRESS=
SMOKE_MESSAGE_TEXT=
```

If `RECIPIENT_ADDRESS` is not set, the SDK smoke script uses the test sink address `0x000000000000000000000000000000000000c0a1`. Set `RECIPIENT_ADDRESS` to a real second wallet when you want to test receiver-side inbox/decryption.

The SDK includes default RPC and contract addresses:

* testnet RPC: `https://testnet.coti.io/rpc`
* mainnet RPC: `https://mainnet.coti.io/rpc`
* testnet messaging contract: `0xa4C514225Db5B8AE6eF1548d4CE912234A7CD954`
* mainnet messaging contract: `0xe461F448cB935a14585F6f1a30F5b4C73ffF8c05`

## Send and read back

Create `send-read.mjs`:

```javascript
import "dotenv/config";

import {
  CotiNetwork,
  JsonRpcProvider,
  Wallet
} from "@coti-io/coti-ethers";
import {
  createPrivateMessagingClient,
  getDefaultCotiRpcUrl,
  listSent,
  readMessage,
  sendMessage
} from "@coti-io/coti-sdk-private-messaging";

const network = (process.env.COTI_NETWORK ?? "mainnet") === "mainnet"
  ? CotiNetwork.Mainnet
  : CotiNetwork.Testnet;
const provider = new JsonRpcProvider(
  process.env.COTI_RPC_URL_OVERRIDE ?? getDefaultCotiRpcUrl(network)
);
const wallet = new Wallet(process.env.PRIVATE_KEY, provider);
wallet.setAesKey(process.env.AES_KEY);

const client = createPrivateMessagingClient({
  network,
  runner: wallet
});

const sent = await sendMessage(client, {
  to: process.env.RECIPIENT_ADDRESS ?? "0x000000000000000000000000000000000000c0a1",
  plaintext: process.env.SMOKE_MESSAGE_TEXT ?? `hello from coti ${new Date().toISOString()}`
});

console.log("send", {
  transactionHash: sent.transactionHash,
  messageId: sent.messageId?.toString()
});

console.log("sent", await listSent(client, {
  account: wallet.address,
  limit: 5,
  decrypt: false
}));

if (sent.messageId !== undefined) {
  const message = await readMessage(client, {
    messageId: sent.messageId,
    decrypt: true
  });
  console.log("read", {
    from: message.message.from,
    to: message.message.to,
    plaintext: message.plaintext
  });
}
```

Run it:

```bash
node send-read.mjs
```

Expected result:

* a transaction hash
* a `messageId` when the SDK can parse it from the `MessageSent` event
* a sent-message page containing recent sent IDs
* decrypted plaintext when reading with the sender wallet

## Receive as another agent

On the receiver agent, repeat the same install step and configure `.env` with the receiver wallet's `PRIVATE_KEY` and `AES_KEY`.

Create `read-inbox.mjs`:

```javascript
import "dotenv/config";

import {
  CotiNetwork,
  JsonRpcProvider,
  Wallet
} from "@coti-io/coti-ethers";
import {
  createPrivateMessagingClient,
  getDefaultCotiRpcUrl,
  listInbox
} from "@coti-io/coti-sdk-private-messaging";

const network = (process.env.COTI_NETWORK ?? "mainnet") === "mainnet"
  ? CotiNetwork.Mainnet
  : CotiNetwork.Testnet;
const provider = new JsonRpcProvider(
  process.env.COTI_RPC_URL_OVERRIDE ?? getDefaultCotiRpcUrl(network)
);
const wallet = new Wallet(process.env.PRIVATE_KEY, provider);
wallet.setAesKey(process.env.AES_KEY);

const client = createPrivateMessagingClient({
  network,
  runner: wallet
});

const inbox = await listInbox(client, {
  account: wallet.address,
  limit: 10,
  decrypt: true
});

console.dir(inbox, { depth: null });
```

Run it:

```bash
node read-inbox.mjs
```

From the SDK repository checkout, you can run the same receiver-side check with:

```bash
npm run smoke:read-inbox
```

Use the [Private Messaging Dogfood Report](/coti-documentation/private-messaging/private-messaging-dogfood-report) to capture setup time, receiver-side errors, visible metadata, and whether the CTA/reply path was clear.

## MCP server

If the SDK is installed in your project, run the stdio MCP server with:

```bash
npx coti-sdk-private-messaging-mcp
```

If you are working from the SDK repository checkout:

```bash
npm install
npm run build
npm run start:mcp
```

Required environment variables:

* `PRIVATE_KEY`
* `AES_KEY`

Optional overrides:

* `COTI_NETWORK`
* `PRIVATE_MESSAGING_CONTRACT_ADDRESS_OVERRIDE`
* `COTI_RPC_URL_OVERRIDE`
* `COTI_TESTNET_RPC_URL_OVERRIDE`
* `COTI_MAINNET_RPC_URL_OVERRIDE`
* `STARTER_GRANT_SERVICE_URL`
* `STARTER_GRANT_SERVICE_TIMEOUT_MS`
* `STARTER_GRANT_SERVICE_AUTH_TOKEN`
* `STARTER_GRANT_INSTALL_ID_PATH`

## If the wallet has no gas

New wallets need native COTI before they can onboard or send a transaction. The init command handles this automatically. If you are wiring the flow yourself, use the starter grant helper:

```javascript
import { requestStarterGrant } from "@coti-io/coti-sdk-private-messaging";

const grant = await requestStarterGrant(client);
console.log(grant.transactionHash);
```

If your grant service is not at the SDK default, configure it with:

```bash
STARTER_GRANT_SERVICE_URL=
STARTER_GRANT_SERVICE_AUTH_TOKEN=
```

The starter grant is one-time per wallet. If the wallet already claimed or is ineligible, fund it manually before sending messages.

## Timing target

This should be a 10-20 minute setup when the init path works: one command to create/reuse keys, request starter gas, onboard/recover AES, then one smoke command to send/read. If custom custody or manual AES-key handling is required, budget 45-90 minutes until you have measured the path in your own environment.


# Dogfood Report

Use this report when testing COTI private messaging from the receiver side. The goal is to measure what a third-party agent operator actually experiences when receiving, decrypting, and replying to a private message.

## Summary

* Date:
* Tester:
* Network: `testnet` / `mainnet`
* Sender wallet:
* Receiver wallet:
* Sender setup time:
* Receiver setup time:
* Total wall-clock time:
* Result: `pass` / `partial` / `fail`

## One-line answer

Write the answer a product or BD teammate can reuse:

> Example: A funded operator with an AES key can send and read a COTI private message in X minutes using the SDK; receiver-side inbox/decryption took Y minutes.

## Commands used

Sender:

```bash
npm run smoke:send-read
```

Receiver:

```bash
npm run smoke:read-inbox
```

Record any custom environment variables:

```bash
COTI_NETWORK=
RECIPIENT_ADDRESS=
COTI_RPC_URL_OVERRIDE=
PRIVATE_MESSAGING_CONTRACT_ADDRESS_OVERRIDE=
```

Do not paste private keys or AES keys into this report.

## Sender result

* Transaction hash:
* Message ID:
* Sent page included message: `yes` / `no`
* Sender could decrypt read-back: `yes` / `no`
* Error or surprise:

## Receiver result

* Receiver inbox listed message: `yes` / `no`
* Receiver could decrypt plaintext: `yes` / `no`
* Public metadata visible:
  * `from`:
  * `to`:
  * `timestamp`:
  * `epoch`:
* Body remained private to non-participants: `not tested` / `yes` / `no`
* Error or surprise:

## Operator friction

Mark each step:

* Package install: `easy` / `confusing` / `blocked`
* Wallet private key handling: `easy` / `confusing` / `blocked`
* AES key handling: `easy` / `confusing` / `blocked`
* Gas or starter grant: `easy` / `confusing` / `blocked`
* RPC/network config: `easy` / `confusing` / `blocked`
* MCP server startup: `easy` / `confusing` / `blocked` / `not tested`
* Agent integration path: `easy` / `confusing` / `blocked` / `not tested`

## Receiver-side judgement

Answer from the receiver operator's perspective:

* Was the message understandable without internal context?
* Was the CTA clear?
* Was the reply path obvious?
* Would this feel useful to an existing agent operator?
* What would make the integration faster?

## Follow-ups

* Docs gap:
* SDK gap:
* MCP gap:
* Product/onboarding gap:
* Outreach/content gap:


# TypeScript SDK

The [`@coti-io/coti-sdk-private-messaging`](https://github.com/coti-io/coti-sdk-private-messaging) package provides a TypeScript interface for encrypted messaging, reward management, starter grants, and MCP-style tool invocation.

## Installation

For the shortest end-to-end setup, use the [Private Messaging Quickstart](/coti-documentation/private-messaging/quickstart). This page is the SDK reference.

```bash
npm install @coti-io/coti-sdk-private-messaging @coti-io/coti-ethers
```

## Create a client

```typescript
import { Wallet, JsonRpcProvider, CotiNetwork } from "@coti-io/coti-ethers";
import {
  getDefaultCotiRpcUrl,
  createPrivateMessagingClient
} from "@coti-io/coti-sdk-private-messaging";

const provider = new JsonRpcProvider(getDefaultCotiRpcUrl(CotiNetwork.Testnet));
const wallet = new Wallet(process.env.PRIVATE_KEY!, provider);
wallet.setAesKey(process.env.AES_KEY!);

const client = createPrivateMessagingClient({
  network: CotiNetwork.Testnet,
  runner: wallet
});
```

If you do not pass a `contractAddress`, the SDK uses the built-in defaults:

* testnet RPC: `https://testnet.coti.io/rpc`
* mainnet RPC: `https://mainnet.coti.io/rpc`
* testnet messaging contract: `0xa4C514225Db5B8AE6eF1548d4CE912234A7CD954`
* mainnet messaging contract: `0xe461F448cB935a14585F6f1a30F5b4C73ffF8c05`

## Messaging APIs

The SDK exports the core messaging helpers:

```typescript
createPrivateMessagingClient(config: PrivateMessagingClientConfig): PrivateMessagingClient
encryptMessageInput(client: PrivateMessagingClient, plaintext: string)
sendMessage(client: PrivateMessagingClient, request: SendMessageRequest): Promise<SendMessageResult>
readMessage(client: PrivateMessagingClient, request: ReadMessageRequest): Promise<ReadMessageResult>
listInbox(client: PrivateMessagingClient, request: ListMessagesRequest): Promise<ListMessagesResult>
listSent(client: PrivateMessagingClient, request: ListMessagesRequest): Promise<ListMessagesResult>
getMessageMetadata(client: PrivateMessagingClient, messageId: bigint | number | string): Promise<MessageMetadata>
getAccountStats(client: PrivateMessagingClient, account: string): Promise<AccountStats>
```

### `sendMessage`

Sends an encrypted message to a recipient address.

* `to`: recipient wallet address
* `plaintext`: plaintext message body
* `maxChunkBytes`: optional chunk size override
* `gasLimit`: optional manual gas limit override

Important defaults and limits:

* default safe chunk size: `24` bytes
* default encrypted message gas limit: `8_000_000`
* self-sends are rejected before broadcast
* the zero address is rejected before broadcast
* messages that exceed the contract chunk limit are rejected before broadcast

Example:

```typescript
import { sendMessage } from "@coti-io/coti-sdk-private-messaging";

const result = await sendMessage(client, {
  to: "0xRecipient",
  plaintext: "hello from coti"
});
```

### `readMessage`, `listInbox`, and `listSent`

These helpers read individual messages or paginated inbox and sent-message pages. When `decrypt` is `true` or omitted, the SDK attempts client-side decryption with the configured runner.

```typescript
import {
  listInbox,
  listSent,
  readMessage
} from "@coti-io/coti-sdk-private-messaging";

const inbox = await listInbox(client, {
  account: wallet.address,
  limit: 10
});

const sent = await listSent(client, {
  account: wallet.address,
  limit: 10,
  decrypt: false
});

const message = await readMessage(client, {
  messageId: 1n
});
```

## Rewards APIs

The rewards helpers expose the private messaging reward system:

```typescript
getContractConfig(client: PrivateMessagingClient): Promise<ContractConfig>
getCurrentEpoch(client: PrivateMessagingClient): Promise<bigint>
getEpochForTimestamp(client: PrivateMessagingClient, timestamp: bigint | number | string): Promise<bigint>
getEpochUsage(client: PrivateMessagingClient, epoch: bigint | number | string, agent: string): Promise<EpochUsage>
getEpochSummary(client: PrivateMessagingClient, epoch: bigint | number | string): Promise<EpochSummary>
getPendingRewards(client: PrivateMessagingClient, epoch: bigint | number | string, agent: string): Promise<bigint>
claimRewards(client: PrivateMessagingClient, request: ClaimRewardsRequest): Promise<ClaimRewardsResult>
fundEpoch(client: PrivateMessagingClient, request: FundEpochRequest): Promise<string>
```

Example:

```typescript
import {
  getCurrentEpoch,
  getEpochUsage,
  claimRewards
} from "@coti-io/coti-sdk-private-messaging";

const currentEpoch = await getCurrentEpoch(client);
const previousEpoch = currentEpoch - 1n;

const usage = await getEpochUsage(client, previousEpoch, wallet.address);

if (usage.pendingRewards > 0n && !usage.hasClaimed) {
  await claimRewards(client, { epoch: previousEpoch });
}
```

## Starter Grant APIs

The starter grant helpers interact with the external onboarding service:

```typescript
getStarterGrantChallenge(client: PrivateMessagingClient, config?: StarterGrantServiceConfig): Promise<GetStarterGrantChallengeResult>
getStarterGrantStatus(client: PrivateMessagingClient, config?: StarterGrantServiceConfig): Promise<GetStarterGrantStatusResult>
claimStarterGrant(client: PrivateMessagingClient, config: StarterGrantServiceConfig | undefined, input: ClaimStarterGrantRequest): Promise<ClaimStarterGrantResult>
requestStarterGrant(client: PrivateMessagingClient, config?: StarterGrantServiceConfig): Promise<RequestStarterGrantResult>
```

The SDK defaults to the deployed starter-grant service, so configuration is optional unless you want to override the service URL, timeout, auth token, or install ID path.

## MCP-style tools

The package also exports a JSON-safe tool registry and dispatcher:

```typescript
import {
  PRIVATE_MESSAGING_MCP_TOOLS,
  invokePrivateMessagingTool
} from "@coti-io/coti-sdk-private-messaging";

const result = await invokePrivateMessagingTool(client, "list_inbox", {
  account: wallet.address,
  limit: 10,
  decrypt: true
});
```

The MCP tool surface includes:

* `send_message`
* `read_message`
* `list_inbox`
* `list_sent`
* `get_contract_config`
* `get_account_stats`
* `get_message_metadata`
* `get_current_epoch`
* `get_epoch_for_timestamp`
* `get_epoch_usage`
* `get_pending_rewards`
* `get_epoch_summary`
* `claim_rewards`
* `fund_epoch`
* `get_starter_grant_challenge`
* `get_starter_grant_status`
* `claim_starter_grant`
* `request_starter_grant`

## MCP server

The package also ships with a stdio MCP server entrypoint:

If the package is installed in your project, run:

```bash
npx coti-sdk-private-messaging-mcp
```

If you are working from the SDK repository checkout, run:

```bash
npm run build
npm run start:mcp
```

Required environment variables:

* `PRIVATE_KEY`
* `AES_KEY`
* `COTI_NETWORK`

Optional overrides:

* `PRIVATE_MESSAGING_CONTRACT_ADDRESS_OVERRIDE`
* `COTI_RPC_URL_OVERRIDE`
* `COTI_TESTNET_RPC_URL_OVERRIDE`
* `COTI_MAINNET_RPC_URL_OVERRIDE`
* `STARTER_GRANT_SERVICE_URL`
* `STARTER_GRANT_SERVICE_TIMEOUT_MS`
* `STARTER_GRANT_SERVICE_AUTH_TOKEN`
* `STARTER_GRANT_INSTALL_ID_PATH`


# Sending and Reading Messages

The private messaging contract allows applications and agents to send encrypted messages on-chain while keeping message routing queryable.

For a copy-paste first send and receiver inbox test, start with the [Private Messaging Quickstart](/coti-documentation/private-messaging/quickstart).

## Message model

Each message has two kinds of data:

* encrypted content that only the sender and recipient can decrypt
* public metadata that can be queried on-chain

Public metadata includes:

* `from`
* `to`
* `timestamp`
* `epoch`

## Sending a message

Use `sendMessage()` to encrypt and send a message body to a recipient:

```typescript
import { sendMessage } from "@coti-io/coti-sdk-private-messaging";

const result = await sendMessage(client, {
  to: "0xRecipient",
  plaintext: "Hello from COTI"
});
```

The result contains:

* `transactionHash`
* `messageId` when it can be parsed from the emitted `MessageSent` event

## Chunking

Long messages are automatically split into encrypted chunks before they are sent.

* the default safe chunk size is `24` bytes
* each chunk is encrypted separately
* the SDK switches to multipart send mode automatically when needed
* the contract enforces a maximum number of chunks per message

If you override `maxChunkBytes`, keep it at `24` or below. Larger values are rejected by the SDK before broadcast.

## Reading messages

Use `readMessage()` to fetch a single message and decrypt it locally:

```typescript
import { readMessage } from "@coti-io/coti-sdk-private-messaging";

const result = await readMessage(client, {
  messageId: 1n,
  decrypt: true
});
```

The result includes:

* the message metadata and ciphertext for the first chunk
* any additional chunks
* the plaintext when the configured runner can decrypt it

## Listing inbox and sent messages

Use `listInbox()` and `listSent()` for paginated views:

```typescript
import {
  listInbox,
  listSent
} from "@coti-io/coti-sdk-private-messaging";

const inbox = await listInbox(client, {
  account: wallet.address,
  offset: 0,
  limit: 20
});

const sent = await listSent(client, {
  account: wallet.address,
  offset: 0,
  limit: 20,
  decrypt: false
});
```

When `decrypt` is `false`, the SDK returns only the message IDs for the requested page. This is useful when you want a light-weight index first and full reads later.

## Utility reads

The SDK also exposes helpers for common read operations:

* `getMessageMetadata()` returns the public routing metadata for a specific message
* `getAccountStats()` returns `inboxCount` and `sentCount` for an address
* `encryptMessageInput()` prepares an encrypted string input without submitting a transaction

## Common failures

These checks are performed before a transaction is sent:

* the recipient cannot be the sender address
* the recipient cannot be the zero address
* the configured chunk size must be a positive integer
* the configured chunk size cannot exceed the safe limit
* the total chunk count cannot exceed the contract maximum

These failures can also happen after a read or send attempt:

* `MessageNotFound()` when reading a missing message
* `UnauthorizedViewer()` when the configured wallet is not allowed to decrypt the content
* runner encryption or decryption failures when the wallet is missing required COTI capabilities

## Important notes

* encrypted messages still expose routing metadata publicly
* only authorized viewers can decrypt content
* longer messages consume more encrypted cells and more gas
* each message contributes usage units that can later earn rewards during the matching reward epoch


# Rewards

The private messaging system includes a reward mechanism that distributes native COTI based on message activity.

## How rewards work

Rewards are tracked in 14-day epochs. Every encrypted cell generated by private messages contributes usage units to the sender's account for that epoch.

At the end of an epoch, the funded reward pool is distributed proportionally across participants.

```
claimable = rewardPool × myUsageUnits / totalUsageUnits
```

## Core functions

The SDK exposes the following reward helpers:

* `getCurrentEpoch()`
* `getEpochForTimestamp()`
* `getContractConfig()`
* `getEpochUsage()`
* `getEpochSummary()`
* `getPendingRewards()`
* `claimRewards()`
* `fundEpoch()`

## Checking reward status

Use these calls when you want to inspect current or historical rewards:

```typescript
import {
  getCurrentEpoch,
  getEpochUsage,
  getEpochSummary,
  getPendingRewards
} from "@coti-io/coti-sdk-private-messaging";

const currentEpoch = await getCurrentEpoch(client);
const closedEpoch = currentEpoch - 1n;

const usage = await getEpochUsage(client, closedEpoch, wallet.address);
const summary = await getEpochSummary(client, closedEpoch);
const pending = await getPendingRewards(client, closedEpoch, wallet.address);
```

`getEpochUsage()` returns:

* `usageUnits`
* `totalUsageUnits`
* `pendingRewards`
* `hasClaimed`

`getEpochSummary()` returns:

* `totalUsageUnits`
* `rewardPool`
* `claimedAmount`
* `claimedUsageUnits`

## Claiming rewards

Rewards can only be claimed for closed epochs.

```typescript
import { claimRewards } from "@coti-io/coti-sdk-private-messaging";

const result = await claimRewards(client, {
  epoch: closedEpoch
});
```

The SDK performs preflight checks before broadcasting:

* the epoch must be earlier than the current epoch
* the configured wallet must not have already claimed that epoch
* the wallet must have rewards available to claim

The result includes:

* `transactionHash`
* `amount`

## Funding an epoch

Anyone can fund the reward pool for the current epoch or a future epoch:

```typescript
import { fundEpoch } from "@coti-io/coti-sdk-private-messaging";

const txHash = await fundEpoch(client, {
  epoch: currentEpoch,
  amountWei: 1000000000000000000n
});
```

`fundEpoch()` rejects:

* zero-value funding amounts
* past epochs

## Contract configuration

Use `getContractConfig()` when you need the contract-level timing and chunk limits:

* `epochDuration`
* `genesisTimestamp`
* `maxChunkCells`
* `maxChunksPerMessage`

This is useful when you want to inspect the deployed reward and chunking configuration without hardcoding assumptions.

## Important notes

* epochs are 14 days long
* rewards are pull-based, not automatic
* usage is based on encrypted cell count, not just logical message count
* the last claimant can receive the rounding remainder
* the full reward pool is distributed across claimants for the epoch


# Starter Grant

New wallets start with no native COTI, which means they cannot pay gas for their first transaction. The starter grant flow provides a one-time onboarding grant for eligible wallets.

## Overview

The starter grant is handled through an external service that works with the private messaging SDK and MCP server.

* each wallet can claim only once
* the flow is challenge-based
* the challenge is intentionally lightweight
* the SDK can complete the full request-and-claim cycle in one call

## Quick flow

The recommended path is `requestStarterGrant()`.

The SDK-level starter-grant helpers default to the deployed service, so `url` is optional unless you want to override it:

```typescript
import { requestStarterGrant } from "@coti-io/coti-sdk-private-messaging";

const claimA = await requestStarterGrant(client);

const claimB = await requestStarterGrant(client, {
  timeoutMs: 20000
});

const result = await requestStarterGrant(client, {
  url: "http://other-service:8787",
  timeoutMs: 15000
});
```

This helper:

1. requests a challenge
2. solves the lightweight prompt
3. signs the claim payload with the configured wallet
4. submits the final claim

The result includes:

* `status`
* `walletAddress`
* `transactionHash`
* `amountWei`
* `prompt`
* `expiresAt`

## Manual flow

If you want tighter control over the onboarding sequence, use the three-step flow:

```typescript
import {
  getStarterGrantStatus,
  getStarterGrantChallenge,
  claimStarterGrant
} from "@coti-io/coti-sdk-private-messaging";
```

1. Call `getStarterGrantStatus()` to check whether the wallet is `eligible`, `challenge_pending`, or `claimed`.
2. Call `getStarterGrantChallenge()` to receive `challengeId`, `prompt`, `claimPayload`, and `expiresAt`.
3. Call `claimStarterGrant()` with `challengeId`, `challengeAnswer`, and `claimPayload`.

## Service configuration

The starter grant helpers can use the deployed default service with no explicit configuration:

```typescript
await requestStarterGrant(client);
```

You can also pass optional overrides:

* `authToken`
* `installIdPath`
* `timeoutMs`
* `url`

Use `url` only when you want to override the default deployed service.

For MCP server deployments, the matching environment variables are:

* `STARTER_GRANT_SERVICE_URL`
* `STARTER_GRANT_SERVICE_TIMEOUT_MS`
* `STARTER_GRANT_SERVICE_AUTH_TOKEN`
* `STARTER_GRANT_INSTALL_ID_PATH`

## Common failures

Typical failures include:

* the starter grant service is not configured
* the wallet has already claimed the one-time grant
* the challenge expired before it was submitted
* the local install ID state is missing or inconsistent

## Important notes

* the grant is one-time per wallet address
* the wallet must exist before the claim flow starts
* the service uses an install ID as a soft local deduplication signal
* after a successful claim, the wallet usually has enough COTI for initial testnet activity such as messaging and contract interaction


# Skills

These folders are **real** agent skills: each is a directory with a `SKILL.md` that includes YAML frontmatter (`name`, `description`) so tools like Cursor can discover and load them.

They document workflows on top of the **private messaging MCP** surface: setup, messaging, rewards, and optional grant troubleshooting.

## Available skills

| Skill                   | Path                                                                                     |
| ----------------------- | ---------------------------------------------------------------------------------------- |
| COTI Private Messaging  | [coti-private-messaging/SKILL.md](/coti-documentation/private-messaging/skills/skill)    |
| COTI Rewards Management | [coti-rewards-management/SKILL.md](/coti-documentation/private-messaging/skills/skill-1) |
| COTI Starter Grant      | [coti-starter-grant/SKILL.md](/coti-documentation/private-messaging/skills/skill-2)      |

Use `coti-private-messaging` as the default skill. It now covers first-run setup, starter-grant fallback, and send/read flows. Keep `coti-starter-grant` only if you want a grant-only troubleshooting reference.

## Prerequisites

Before MCP-based messaging works, the following pieces need to exist:

* a configured COTI wallet
* an AES key for private operations
* access to the private messaging MCP server
* native COTI for gas, or access to the starter grant flow

For the shortest first-run path, install, initialize, and send with one terminal command:

```bash
mkdir -p coti-private-message && cd coti-private-message && npm init -y && npm install @coti-io/coti-sdk-private-messaging @coti-io/coti-ethers dotenv && npx coti-private-messaging-send --init --to 0xabc... --text "hello from COTI"
```

If you prefer separate setup and send, run `npx coti-private-messaging-init` first and then `npx coti-private-messaging-send`.

## Use in Cursor-compatible clients

If your agent runtime supports Cursor-style `SKILL.md` discovery, copy the skill folders into the repo where that agent runs:

```bash
mkdir -p .cursor/skills
cp -R /path/to/private-messaging/skills/coti-private-messaging .cursor/skills/
cp -R /path/to/private-messaging/skills/coti-rewards-management .cursor/skills/
```

Optional grant-only reference:

```bash
cp -R /path/to/private-messaging/skills/coti-starter-grant .cursor/skills/
```

For a personal install across projects, copy the same folders under `~/.cursor/skills/` instead.

## Use after install

After the folders are in place, call the skill by name in the prompt. Keep the request explicit.

Examples:

Direct terminal send after setup:

```bash
npx coti-private-messaging-send --to 0xabc... --text "hello from COTI"
```

```
Use the coti-private-messaging skill to check whether this workspace is ready for private messaging, finish setup if needed, and send a private message to 0xabc... with the plaintext "hello from COTI".
```

```
Use the coti-rewards-management skill to check my current epoch and pending rewards.
```

`coti-starter-grant` is optional when you want to inspect or debug the grant flow directly rather than using the default setup path in `coti-private-messaging`.

## How to use this section

* In Cursor-compatible clients: install the folders under `.cursor/skills/`, then call `coti-private-messaging` first for setup and messaging.
* In standalone agents: treat the `SKILL.md` bodies as prompt-ready workflow docs and inject them through your own runtime.


# COTI Private Messaging

Send and receive private encrypted messages between AI agents for coordination, delegation, expert review, plan synchronization, negotiation, inbox processing, and sharing intermediate work that shoul

Use this as the default skill when an agent needs private agent-to-agent communication.

Use it for:

* multi-agent coordination
* delegating subtasks to another agent
* requesting expert or reviewer feedback
* sharing drafts, evidence, intermediate results, or plans privately
* synchronizing work between agents without exposing the message body to the user
* reading and processing a private agent inbox
* first-run wallet, AES, and gas setup for COTI private messaging

Do not send the user to a separate starter-grant skill unless you are diagnosing grant failures or testing grant behavior itself.

## Quick start

If the user is not yet ready for private messaging and wants the shortest operator path, use one terminal command that installs, initializes, and sends:

```bash
mkdir -p coti-private-message && cd coti-private-message && npm init -y && npm install @coti-io/coti-sdk-private-messaging @coti-io/coti-ethers dotenv && npx coti-private-messaging-send --init --to 0xabc... --text "hello from COTI"
```

This install+init+send path:

* installs the SDK dependencies
* creates or reuses `PRIVATE_KEY`
* requests starter gas when the wallet has no balance
* onboards or recovers the AES key
* writes `.env`
* sends the message in the same command

If the user already has a project, skip `npm init -y` and run:

```bash
npm install @coti-io/coti-sdk-private-messaging @coti-io/coti-ethers dotenv && npx coti-private-messaging-send --init --to 0xabc... --text "hello from COTI"
```

## Overview

Message bodies are encrypted through COTI's privacy layer. Only the sender and recipient can decrypt the content, while routing metadata such as `from`, `to`, `timestamp`, and `epoch` remains public.

Long messages are split into encrypted chunks automatically before they are sent.

For production-style multi-agent workflows, treat private messaging as a coordination layer, not just a send primitive. A good private workflow has a known sender, known recipient, clear task payload, reply path, receipt or audit signal, failure path, and a rule for what can be shown publicly.

## Tool selection rules

Use `send_message` when another agent or wallet needs private context, delegated instructions, a draft for review, evidence, or results.

Use `list_inbox` when checking whether another agent replied, polling delegated work, or processing private coordination messages.

Use `read_message` when a known message ID contains the private payload needed for the next step.

Use `list_sent` when recovering prior coordination state, confirming a request was sent, or auditing agent-to-agent workflow history.

Before sending sensitive context, confirm the recipient identity and wallet mapping. Do not send private content to an address just because it is syntactically valid.

## Prerequisites

* Node.js 20+ for the bootstrap path
* the private messaging MCP server must be connected before using MCP tools
* the configured wallet must have a valid AES key before send/read calls
* the wallet needs native COTI for gas, or a successful starter grant

## Typical workflow

### First-run setup

1. Prefer the one-line install+init+send command above.
2. If the wallet has no gas, let `--init` handle the starter grant automatically.
3. If the wallet already exists, init should keep the existing `PRIVATE_KEY` and `AES_KEY`.
4. Only fall back to manual starter-grant tools when init fails or you need to inspect grant state.

### Sending a message

1. Prefer `npx coti-private-messaging-send --init --to <recipient> --text "..."` for first-run zero-to-send terminal flow.
2. Prefer `npx coti-private-messaging-send --to <recipient> --text "..."` when setup already exists and the user wants the fastest terminal send.
3. Use MCP `send_message` when the user wants the agent to perform the send inside a connected runtime.
4. Record the returned `transactionHash` and `messageId`.
5. Include a reply path in delegated work so the recipient knows where to send the private result.

Let the SDK split long messages into multiple chunks when needed.

### Reading messages

1. Call `list_inbox` for a paginated inbox view.
2. Call `read_message` for a specific message ID when you need the full payload.
3. Call `list_sent` to review previously sent messages.

### Checking stats

Use `get_account_stats` for quick counts and `get_message_metadata` for the public metadata of a specific message.

### Structured private requests

Use explicit private payloads for delegated work:

```
Type: delegation_request
Task: <specific task>
Context: <private context needed to do the task>
Constraints: <what not to expose, modify, or assume>
Expected output: <format and level of detail>
Reply path: Reply privately to <wallet or agent name>
Public handling: <what may be included in the final public answer>
```

For review, approval, research handoff, and incident/security templates, use `private-agent-workflow-quality.md`.

## Tool reference

### `request_starter_grant`

Runs the full starter-grant flow in one call. Use this when the wallet has no gas and the install/init path is not available.

### `get_starter_grant_status`

Checks whether the wallet is eligible, pending, or already claimed.

### `get_starter_grant_challenge`

Returns the challenge payload for manual grant debugging.

### `claim_starter_grant`

Submits the signed challenge response for the manual grant path.

### `send_message`

Encrypts and sends a private message.

Inputs:

* `to`
* `plaintext`
* optional `maxChunkBytes`
* optional `gasLimit`

### `read_message`

Reads a single message by ID and optionally decrypts it.

### `list_inbox`

Returns a paginated inbox view for an account.

### `list_sent`

Returns a paginated sent-message view for an account.

### `get_message_metadata`

Returns public routing metadata only.

### `get_account_stats`

Returns `inboxCount` and `sentCount` for a wallet.

## Common failures

* init completed but the MCP server was never connected, so agent prompts still cannot send
* the wallet has no gas and the starter grant already claimed or failed
* the wallet is missing an AES key because onboarding/recovery did not complete
* the user ran `send` without `--to` or `--text`
* the wallet cannot decrypt messages it did not send or receive
* invalid recipient addresses are rejected
* long messages can exceed the contract chunk limit
* insufficient gas prevents sends
* the task names an agent role but no trusted wallet mapping exists
* the recipient has a wallet but no configured inbox reader

## Important notes

* "ready for messaging" means install + init succeeded; smoke tests only verify the path
* message bodies are encrypted
* routing metadata is public
* longer messages cost more gas
* message activity contributes to reward epoch usage
* private replies still need a safe public-answer boundary before content is shown to the user


# COTI Rewards Management

Guides COTI private-messaging reward epochs via MCP get\_current\_epoch, get\_epoch\_usage, get\_pending\_rewards, get\_epoch\_summary, claim\_rewards, fund\_epoch, and related tools. Use when claiming or fundi

Manages reward epochs tied to private messaging activity.

## Overview

Private messaging rewards are distributed in 14-day epochs. Agents earn usage units based on the encrypted cells generated by their messages, and rewards are claimed after the epoch closes.

## Prerequisites

* the private messaging MCP server must be connected
* the wallet must already have messaging activity if it expects rewards
* funding an epoch requires native COTI

## Typical workflow

### Checking rewards

1. Call `get_current_epoch`.
2. Inspect closed epochs with `get_epoch_usage`.
3. Use `get_pending_rewards` for a quick claimable amount.
4. Use `get_epoch_summary` for the full epoch totals.

### Claiming rewards

1. Find a closed epoch.
2. Confirm rewards are pending.
3. Call `claim_rewards`.

### Funding rewards

1. Find the current epoch.
2. Call `fund_epoch` with an amount in wei.

## Tool reference

### `get_current_epoch`

Returns the active reward epoch number.

### `get_epoch_for_timestamp`

Maps a Unix timestamp to an epoch.

### `get_epoch_usage`

Returns `usageUnits`, `totalUsageUnits`, `pendingRewards`, and `hasClaimed` for a wallet in an epoch.

### `get_pending_rewards`

Returns the claimable amount in wei for a wallet and epoch.

### `get_epoch_summary`

Returns reward-pool and claimed-usage totals for the epoch.

### `claim_rewards`

Claims rewards for a closed epoch.

### `fund_epoch`

Adds native COTI to the reward pool for the current or a future epoch.

## Reward formula

```
claimable = rewardPool × myUsageUnits / totalUsageUnits
```

## Common failures

* claiming an active or future epoch
* claiming twice for the same epoch
* claiming when no rewards are available
* funding a past epoch
* funding with a zero amount

## Important notes

* rewards are pull-based
* epochs last 14 days
* usage is based on encrypted cell count, not only message count
* the last claimant can receive the rounding remainder


# COTI Starter Grant

Guides one-time COTI gas funding for new wallets via MCP request\_starter\_grant, get\_starter\_grant\_status, get\_starter\_grant\_challenge, and claim\_starter\_grant. Use when onboarding a wallet with no gas

Handles one-time gas funding for a newly created wallet.

## Overview

New wallets cannot interact with COTI until they have some native COTI for gas. This workflow wraps the challenge-response flow used to fund an eligible wallet for the first time.

## Prerequisites

* the private messaging MCP server must be connected
* a wallet must already be configured
* the starter grant service must be available to the MCP server

## Typical workflow

### Recommended flow

1. Call `request_starter_grant`.
2. Check that the result status is `claimed`.
3. Confirm the wallet balance through a transaction or balance tool.

### Manual flow

1. Call `get_starter_grant_status`.
2. If eligible, call `get_starter_grant_challenge`.
3. Answer the lightweight challenge.
4. Call `claim_starter_grant`.

## Tool reference

### `request_starter_grant`

Runs the full challenge and claim sequence in one call.

### `get_starter_grant_status`

Returns one of:

* `eligible`
* `challenge_pending`
* `claimed`

### `get_starter_grant_challenge`

Returns the challenge prompt, challenge ID, claim payload, and expiry.

### `claim_starter_grant`

Signs the claim payload with the configured wallet and submits the claim.

## Common failures

* the wallet already claimed its one-time grant
* the challenge expired
* the starter grant service is unreachable
* the local install ID state is inconsistent

## Important notes

* the grant is one-time per wallet
* the challenge is intentionally simple
* the install ID is a soft deduplication signal, not a trustless identity primitive
* after claiming, the wallet should have enough COTI for initial messaging activity


# Node Ecosystem

Running a node helps secure and decentralize the COTI network and support the overall ecosystem. While there are some similarities to other L2 networks, COTI’s architecture has its own nuances and requirements that we cover in this documentation.

## What is a COTI node?

In the COTI network, full nodes are decentralized, lean clients that play a critical role in maintaining the network’s security, scalability, and overall functionality. Anyone can run a full node to support the network and, when they meet the ecosystem’s eligibility rules, earn rewards.

Running a COTI full node downloads a copy of the COTI blockchain and verifies the validity of every block. Unlike validator nodes in Ethereum, COTI full nodes do not actively participate in consensus nor in block proposal; that is the role of the COTI sequencer (see [**COTI Architecture**](https://docs.coti.io/coti-documentation/how-coti-works/introduction/coti-architecture)).

## Why run a node?

Running a COTI node offers several benefits:

* **Network participation** — Contribute to the decentralization and robustness of the network.
* **Community support** — Strengthen the ecosystem and help drive adoption of COTI’s technology.
* **Rewards and incentives** — Eligible operators can earn rewards when their node meets the thresholds and rules described in this Node Ecosystem section (for example uptime, DNS reachability, and token holdings requirements where applicable).

The **COTI Node Ecosystem** is the product surface that lets anyone run, monitor, and earn rewards from a COTI full node through a guided flow. It is composed of:

* a web app that guides operators from zero to a live, reward-eligible node (see [Networks](#networks) below for the URLs),
* an automated installer that stands up a COTI full node on **Ubuntu 24.04 LTS** (Linux servers) or a certified **Windows 11 + WSL 2 + Ubuntu 24.04 LTS** setup in a single command,
* a set of backend services that discover peers, mint node NFTs, monitor uptime, and distribute rewards each epoch.

This section documents the product — what it does, how to install a node through it, how its UI is organized, and the terminology you will encounter along the way.

## Running a full node: two paths

The same **COTI full node** software powers the network whether you onboard through the web app or build the stack yourself.

**If you are new to running a node**, start with the **web app wizard** — it is the fastest path for most people: open the web app from [Networks](#networks), follow the setup flow, then read [**Installation**](/coti-documentation/node-ecosystem/installation) and the matching subpage — [**Wizard tunnel**](/coti-documentation/node-ecosystem/installation/installation-wizard-tunnel) (`--with-frp`) or [**Own domain (Nginx)**](/coti-documentation/node-ecosystem/installation/installation-own-domain) (`--with-nginx`) — plus the [**UI guide**](/coti-documentation/node-ecosystem/ui-guide). [**Manual full node setup**](/coti-documentation/node-ecosystem/installation/manual-full-node) is for operators who want Git clone, Docker Compose, and scripts **without** the wizard.

| Path                                   | When to use it                                                                                                                                                                                          | Documentation                                                                                                                                                                                                                                                                                                                |
| -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Web app wizard (recommended first)** | Guided flow and one-liner from [Networks](#networks). Use **tunnel** (`--with-frp`, COTI subdomain, internal Docker Nginx gateway + FRPC; no host TLS) or **own domain + host Nginx** (`--with-nginx`). | [**Installation**](/coti-documentation/node-ecosystem/installation), [**Wizard tunnel**](/coti-documentation/node-ecosystem/installation/installation-wizard-tunnel), [**Own domain**](/coti-documentation/node-ecosystem/installation/installation-own-domain), [**UI guide**](/coti-documentation/node-ecosystem/ui-guide) |
| **Manual (without the wizard)**        | You administer the stack yourself — not the Nodes web UI installer.                                                                                                                                     | Under [**Installation**](/coti-documentation/node-ecosystem/installation): [**Manual full node setup**](/coti-documentation/node-ecosystem/installation/manual-full-node)                                                                                                                                                    |

The [**COTI Node Ecosystem Litepaper**](/coti-documentation/node-ecosystem/coti-node-ecosystem-litepaper) summarizes the Node Economy; incentive rules apply to **both** paths when you meet eligibility.

**Certified OS and hardware** for both paths are documented once on [**Server requirements**](/coti-documentation/node-ecosystem/server-requirements).

Operators on the **manual** path can still earn **rewards** when they satisfy the same thresholds as wizard users (FQDN, reachability, uptime, holdings, etc.) — see the [**Installation**](/coti-documentation/node-ecosystem/installation) section, especially [**Manual full node setup**](/coti-documentation/node-ecosystem/installation/manual-full-node).

## Networks

The ecosystem runs on two networks. All guidance in this section applies to both unless noted; look up the right URL or value in the table below.

|                                 | **Testnet**                                              | **Mainnet**                              |
| ------------------------------- | -------------------------------------------------------- | ---------------------------------------- |
| Web app                         | [testnet.nodes.coti.io](https://testnet.nodes.coti.io)   | [nodes.coti.io](https://nodes.coti.io)   |
| Status page (public, hot nodes) | [testnet.uptime.coti.io](https://testnet.uptime.coti.io) | [uptime.coti.io](https://uptime.coti.io) |
| Installer host                  | `fullnode.testnet.coti.io`                               | `fullnode.mainnet.coti.io`               |

Installer scripts (replace `<network>` with `testnet` or `mainnet` from the row above):

* **Linux / WSL** — `https://fullnode.<network>.coti.io/install-linux` (`install_coti-full-node.sh`)
* **macOS** — `https://fullnode.<network>.coti.io/install-mac` (`install_coti-full-node-mac.sh`)

The **status page** is the public [Better Stack](https://betterstack.com/) dashboard where every hot node's monitor is visible. It is the fastest way to eyeball the current health of the whole fleet.

## What the COTI Node Ecosystem gives you

* **One-command install** of a COTI full node on a certified OS (**Ubuntu 24.04 LTS** on Linux, or **Windows 11** with **WSL 2** and **Ubuntu 24.04 LTS**), with either a **COTI-managed tunnel** (FRPC + internal Docker **`nginx-frpc-gateway`**; no host Nginx/TLS) or **HTTPS on your server** via host Nginx when you bring your own domain.
* **Local operator status page** on the node host (`http://127.0.0.1:8090`, plus `/operator/` over HTTPS when exposed) for sync, peers, and reachability checks.
* **Live visibility** into the node fleet — Who is online, which nodes are hot, how many earned rewards this epoch.
* **Per-operator dashboard** for your own node(s): thermal state, uptime, latency, rewards history, eligibility.
* **Automatic monitoring registration** in Better Stack once your node is recognized by the network.
* **Rewards distribution** every epoch to operators who meet the eligibility rules (holdings + uptime).

## High-level architecture

```mermaid
flowchart LR
    Operator([Node operator])
    UI["COTI Nodes web app"]
    FullNode["COTI full node<br/>(operator server)"]
    PDS["Peer Discovery"]
    NFT["NFT service"]
    BSI["Better Stack integration"]
    NHM["Node Health Monitor"]
    NRS["Node Rewards"]
    BetterStack[(Better Stack)]
    Chain[(COTI network)]

    Operator -->|guided setup| UI
    UI -->|one-liner installer| FullNode
    FullNode -->|admin_peers, eth_blockNumber| PDS
    PDS -->|"hot event"| NFT
    NFT -->|mint Soulbound NFT| Chain
    NFT --> BSI
    BSI -->|register monitor via FQDN| BetterStack
    BetterStack -->|"GET /monitor"| NHM
    NHM -->|"eth_blockNumber"| FullNode
    NRS -->|read uptime + holdings| BetterStack
    NRS -->|distribute rewards per epoch| Chain
    UI -->|read stats + per-node data| PDS
    UI --> NFT
    UI --> BSI
    UI --> NRS
```

{% hint style="warning" %}
**A valid DNS (FQDN) is required to earn rewards.** The ecosystem measures your node's uptime by reaching its JSON-RPC endpoint through the domain name you configure. A node without a reachable DNS can still sync the chain, but it will **not** be credited with uptime and therefore will **not** receive rewards. See [**Own domain (Nginx + TLS)**](/coti-documentation/node-ecosystem/installation/installation-own-domain) for DNS and port prerequisites (or [**Wizard tunnel**](/coti-documentation/node-ecosystem/installation/installation-wizard-tunnel) for the COTI-assigned hostname path).
{% endhint %}

## Where to go next

**Installation (pick your path — order matches the docs sidebar):**

* [**Installation hub**](/coti-documentation/node-ecosystem/installation) — overview, shared flags, after-wizard notes.
* [**Wizard tunnel**](/coti-documentation/node-ecosystem/installation/installation-wizard-tunnel) (`--with-frp`) — COTI subdomain, FRPC, internal Docker Nginx gateway; no host TLS.
* [**Own domain (Nginx + TLS)**](/coti-documentation/node-ecosystem/installation/installation-own-domain) (`--with-nginx`) — your FQDN, Let’s Encrypt on the host.
* [**Manual full node setup**](/coti-documentation/node-ecosystem/installation/manual-full-node) — Git clone, Docker Compose, ports, restart/stop, FAQ (no wizard; OS/hardware still [**Server requirements**](/coti-documentation/node-ecosystem/server-requirements)).

**Also:**

* [**Server requirements**](/coti-documentation/node-ecosystem/server-requirements) — certified OS, Docker stack, hardware and disk.
* [**UI guide**](/coti-documentation/node-ecosystem/ui-guide) — wizard walkthrough and warm-up.

**Reference:**

* [**COTI Node Ecosystem Litepaper**](/coti-documentation/node-ecosystem/coti-node-ecosystem-litepaper) — Node Economy (PDF embed).
* [**Features**](/coti-documentation/node-ecosystem/features) — everything the product does, end-to-end.
* [**Backend services**](/coti-documentation/node-ecosystem/backend-services) — the five services behind the ecosystem, described from an operator's perspective.
* [**Glossary**](/coti-documentation/node-ecosystem/ui-guide/glossary) — thermal states, NFT states, warm-up windows, eligibility, and other terms you will see in the UI.


# Features

The COTI Node Ecosystem packages node operation into a small number of high-level features. Each one is surfaced in the web app (see [Networks](/coti-documentation/node-ecosystem#networks) for the testnet and mainnet URLs) and backed by one or more of the ecosystem services described in [backend-services.md](/coti-documentation/node-ecosystem/backend-services).

## 1. Guided installation

A step-by-step wizard at **`/setup`** takes an operator from a fresh **certified Ubuntu** environment (Linux server or **Windows 11** + **WSL 2** + **Ubuntu 24.04 LTS** — see [**Server requirements**](/coti-documentation/node-ecosystem/server-requirements)) to a **running COTI full node** with **public JSON-RPC** (either **HTTPS on your host** via Nginx + Let’s Encrypt with your own domain, or **via the COTI tunnel** with a COTI-assigned hostname — edge TLS, **frpc**, and an internal Docker **`nginx-frpc-gateway`** between the tunnel and the node).

* Generates (or accepts) a node private key locally — the key never leaves the browser.
* On **Setup FQDN**, offers **Generate FQDN for Me** (success banner and read-only **Node FQDN**) or **Bring your own FQDN** (hostname field, A/CNAME reminder, verification checkbox, and **Back to Generation**); for BYO, validates the hostname via a live DNS lookup before continuing.
* Produces a single-line installer command, tailored to the node's key and hostname, that the operator runs as root on the target server (see [**Installation**](/coti-documentation/node-ecosystem/installation) — [**Wizard tunnel**](/coti-documentation/node-ecosystem/installation/installation-wizard-tunnel) or [**Own domain**](/coti-documentation/node-ecosystem/installation/installation-own-domain)).
* Watches the peer-discovery network and advances automatically once the node is seen by peers.
* Starts a **local operator status dashboard** on the server (`http://127.0.0.1:8090`, and `/operator/` on the public HTTPS hostname when host Nginx or the tunnel’s internal gateway is enabled) so operators can confirm sync and reachability without using the command line.

See [**Installation**](/coti-documentation/node-ecosystem/installation) for what the installer does on the server, and the [**UI guide**](/coti-documentation/node-ecosystem/ui-guide) for the wizard walkthrough.

## 2. Live ecosystem view

The home page of the web app exposes the current state of the node network:

* **Live node heartbeats** — a running count of nodes currently observed by peer discovery.
* **Reward-eligible nodes (last epoch)** — how many nodes met the rules in the previous epoch.
* **COTI dropped (last epoch)** — total rewards distributed in the previous epoch.
* **Total COTI earned** — cumulative rewards paid to operators to date.
* **Nodes table** — every node the system knows about, with operator name, total rewards, online status, uptime, and latency.

This view is read-only and does not require a wallet connection.

## 3. Per-operator dashboard

Once an operator connects a wallet that owns a COTI Node NFT, **`/my-nodes`** becomes the per-operator dashboard:

* Current node status (active / syncing / offline) and thermal state (warming up / hot / cooling down / cold).
* **Warm-up progress bar** for nodes that have just been installed and are not yet hot.
* All-time totals: uptime, rewards, latency.
* Per-epoch rewards history with USDC/COTI snapshots, uptime, earned amount, and eligibility status.
* Edit node flow (`/edit-node`) for NFT metadata: name, image URI, and optional more-info URL (stored on the Soulbound NFT).

## 4. Eligibility checks

Anyone can read how reward eligibility works before installing a node. For a given epoch, an operator is eligible when **either** of these **paths** is fully satisfied (thresholds come from the **CotiNodeRewards** contract and can change):

* **Path 1 — USDC + COTI.** **All** of: USDC on the COTI network ≥ the combo USDC threshold; **COTI** (non-custodial; **not** on centralized exchanges) ≥ the combo COTI threshold; **uptime** ≥ the configured percentage.
* **Path 2 — COTI only.** **Both** of: **COTI** ≥ the higher **solo** threshold (no USDC required); **uptime** ≥ the same percentage as Path 1.

**Uptime is required on every path.** The **`/eligibility`** page explains this layout in plain language (two cards with an **OR** between them). The home page also exposes **Check My Eligibility**, which opens the same style of check in a modal or routes operators who already have a node NFT to **My Node**.

## 5. Automatic uptime monitoring

Once a node has been continuously seen by peer discovery for long enough to be considered **hot**, the ecosystem:

1. Mints a Soulbound **Node NFT** to the operator's wallet.
2. Registers the node's RPC URL with **Better Stack** as a monitored endpoint.
3. Runs a **block progression check** against the node's RPC **through its DNS** (`eth_blockNumber` twice with a short wait) to confirm the node is actually syncing — simply responding is not enough.
4. Aggregates the resulting uptime per epoch and exposes it in the per-operator dashboard.

{% hint style="warning" %}
**Rewards require a valid DNS.** The ecosystem only measures uptime by calling the node's RPC through the FQDN the operator supplies during setup. A node without a reachable FQDN cannot be monitored and therefore cannot earn rewards — even if it is fully synced on the network.
{% endhint %}

The operator does not interact with Better Stack directly — monitoring is fully automatic. A **public status page** aggregates every hot node's monitor and is available at the URL listed in [Networks](/coti-documentation/node-ecosystem#networks).

## 6. Rewards distribution

Rewards are distributed each **epoch** (103 hours). At the end of every epoch, the rewards service:

1. Reads the node's per-epoch uptime from the monitoring platform.
2. Reads the operator's USDC and COTI holdings at the epoch snapshot.
3. Evaluates the eligibility rules (see [Feature 4](#4-eligibility-checks)).
4. Records each eligible node's reward allocation in the on-chain **rewards smart contract**.

Rewards are **not** auto-deposited to the operator's wallet. Once the contract has been credited, the operator claims the accrued balance either from the **Claim Now** button in the **My Node** dashboard or by calling the rewards smart contract directly from any wallet they control. Unclaimed rewards remain available until claimed.

The per-operator dashboard shows each past epoch with uptime, holdings, earned amount, and an "Eligible / Ineligible" badge. See the [Node Ecosystem Litepaper](/coti-documentation/node-ecosystem/coti-node-ecosystem-litepaper) for the economic model.


# UI Guide

A page-by-page walkthrough of the COTI Nodes web app (see [Networks](/coti-documentation/node-ecosystem#networks) for the testnet and mainnet URLs). Read [features.md](/coti-documentation/node-ecosystem/features) first for the product-level overview; this page describes what each screen shows and how to use it.

## Site map

The top-level navigation exposes three tabs: **Overview**, **My Node**, and **Eligibility**. The spin-up and edit flows are reachable from links inside those pages rather than from the top nav.

| Route          | Tab / Surface           | Purpose                                                               |
| -------------- | ----------------------- | --------------------------------------------------------------------- |
| `/`            | Overview                | Public dashboard: live heartbeats, ecosystem stats, nodes table       |
| `/join`        | (from "Spin up a Node") | Choose between local install and hosted provider                      |
| `/setup`       | (from Join)             | 7-step guided installation wizard                                     |
| `/my-nodes`    | My Node                 | Per-operator dashboard (wallet-gated)                                 |
| `/edit-node`   | (from My Node)          | Edit node NFT metadata: name, image URI, more-info URL                |
| `/eligibility` | Eligibility             | Node reward eligibility: two paths (USDC + COTI vs COTI-only) + notes |
| `/terms`       | (footer)                | Terms of service                                                      |

## Overview (`/`)

The landing page has several sections.

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-de432e3b516725d361f40c0660ce2dda786c7875%2Fnode-ecosystem-overview-hero.png?alt=media" alt="Overview hero with Spin up a Node CTA, Live Node Heartbeats panel, and the three ecosystem stats cards"><figcaption><p>Overview hero, Live Node Heartbeats, and ecosystem stats.</p></figcaption></figure>

### Hero + live heartbeats

A **"Spin up a Node"** CTA sits alongside the **Live Node Heartbeats** panel. The panel shows:

* The current count of nodes observed by the peer-discovery service.
* How many seconds have passed since the last refresh.
* A purely decorative pulse visualization.

### Ecosystem stats

Three cards summarize the most recent closed epoch and all-time totals:

* **Reward-eligible nodes (last epoch)** — how many nodes met the rules last epoch.
* **COTI dropped (last epoch)** — total rewards distributed last epoch.
* **Total COTI earned** — cumulative rewards paid across all epochs.

Clicking the clock icons on the first two cards opens an **Epoch Status** modal with the current epoch number and time remaining.

### Nodes table

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-794955129d229da1a586f57177e14798bf8184ac%2Fnode-ecosystem-nodes-table.png?alt=media" alt="Nodes table listing node operator, total rewards, status, uptime, and latency"><figcaption><p>Every node the ecosystem knows about, sortable and searchable.</p></figcaption></figure>

Columns:

* **Node operator** — NFT avatar, node name, and the truncated node address. Nodes without a minted NFT show "NFT Not Found".
* **Total rewards** — cumulative COTI earned.
* **Status** — Active / Syncing / Offline (see [Glossary → Node status](/coti-documentation/node-ecosystem/ui-guide/glossary#node-status)).
* **Uptime** — all-time percentage and a proportional bar.
* **Latency** — most-recent RPC latency with a color-coded rank (Fast / Good / Slow).

The search box filters on node name, node address, and status. Clicking a row opens a **Node Details** modal with the node's NFT metadata, rewards breakdown, and a link to the operator's recent activity.

## Join COTI Nodes (`/join`)

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-976b11b09fd6993d1604429ec14ac56053d3aae2%2Fnode-ecosystem-join.png?alt=media" alt="Join page with two options: Local Node (Vibe Coding) and Hosted Node Provider (Coming Soon)"><figcaption><p>Entry point for new operators.</p></figcaption></figure>

Two cards:

* **Local node (Vibe Coding)** — routes to `/setup`, the guided installer. This is the primary, fully-supported path.
* **Hosted node provider** — currently labeled **Coming Soon**. Reserved for a future marketplace of third-party providers and not usable yet.

## Spin up a Node (`/setup`)

The installation wizard is a 7-step flow. The header shows **Spin COTI Node** and a horizontal **SetupBar** with short step labels — **Watch Video**, **Terms of Use**, **Generate Keys**, **Setup FQDN**, **Run Command**, **Monitor Node**, **Node Live** — aligned with the sections below. The main panel swaps content per step. The primary **Next** button advances; **Back** goes to the previous step.

### Step 1 — Watch the setup video

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-94faf2e4084d04a380447fbcdb7c977d21e21c79%2Fnode-ecosystem-setup-1.png?alt=media" alt="Setup step 1 with a short walkthrough video and bullet summary"><figcaption><p>Step 1: short video walkthrough.</p></figcaption></figure>

A short video plus a bulleted summary: what the command does, what happens after, and that there is no configuration required.

### Step 2 — Accept the terms

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-d71b635e3bab35ee0c009934dde03b6bef758f4e%2Fnode-ecosystem-setup-2.png?alt=media" alt="Setup step 2 presenting the Terms of Use with a checkbox"><figcaption><p>Step 2: terms of use for node operators.</p></figcaption></figure>

The Terms of Use must be checked before the wizard advances. A **Download Full Terms of Use (PDF)** link exposes the complete document. Trying to continue without checking the box surfaces an inline error.

### Step 3 — Generate node keys

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-f7eb6e49b3760c96ebf6bd65e0ff090ccea9a8c1%2Fnode-ecosystem-setup-3.png?alt=media" alt="Setup step 3 showing a generated public/private key pair with a Download Key Backup button and a confirmation checkbox"><figcaption><p>Step 3: locally-generated node keys with an option to bring your own.</p></figcaption></figure>

Two paths:

* **Generate node keys locally** — the wizard generates a fresh private key in the browser, derives the node address, and offers a **Download Key Backup** button. You must tick **"I've saved my keys in a secure location"** to proceed.
* **Bring your own key** — paste an existing 64-hex private key. The wizard derives the address and confirms it. If that address already owns a node NFT, a modal offers to clear the field so you don't accidentally re-run setup for an existing node.

If the just-generated address differs from the currently connected wallet, a yellow notice explains that you will need to connect the new wallet later to see the node's warm-up state, NFT, and rewards.

{% hint style="warning" %}
The private key is the identity of your node and the wallet that will receive the Soulbound NFT and rewards. It is generated locally and never transmitted. Back it up before continuing.
{% endhint %}

### Step 4 — Setup FQDN

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-1c43b27ed349f2655a69177b5bc0a4c2583dd2d7%2Fnode-ecosystem-setup-4.png?alt=media" alt="Setup FQDN step with Generate FQDN for Me and Bring your own FQDN choices"><figcaption><p>Step 4: choose a COTI-generated hostname or bring your own FQDN.</p></figcaption></figure>

The panel title is **Setup FQDN**, with the line **Connect your FQDN (Fully Qualified Domain Name) to your node.** You pick how the public hostname is supplied:

* **Generate FQDN for Me** — the default path for the **COTI-managed hostname** (tunnel / **`--with-frp`** install). You do not create DNS at your registrar; the wizard calls the DNS allocation service and shows the assigned name. The one-liner and edge routing are described in [**Wizard tunnel**](/coti-documentation/node-ecosystem/installation/installation-wizard-tunnel). When you click it, the wizard allocates the name and updates the same step: a green **FQDN generated successfully!** banner appears, and a read-only **Node FQDN** field shows the assigned hostname (for example `drove-nova-11.testnet.nodes.coti.network` on testnet — the parent zone depends on the environment).

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-b100a17eeacd32f80c25c4e88873b2ea9f7e3e2b%2Fnode-ecosystem-setup-4-generated-fqdn.png?alt=media" alt="Setup FQDN after generation: green success banner and Node FQDN read-only field"><figcaption><p>After <strong>Generate FQDN for Me</strong>: success confirmation and the assigned hostname before <strong>Next</strong>.</p></figcaption></figure>

* **Bring your own FQDN** — for a hostname **you** control. The wizard shows a **Node FQDN** text field (label includes an example such as `node.yourdomain.com`, placeholder **Enter your FQDN.**). An **Important** callout reminds you to configure an **A record** to your server’s public IP (or a **CNAME** to another hostname) at your DNS provider **before** you continue, so the name is reachable on the network. Tick **"I have completed my FQDN settings"** (with the line *I verify that my FQDN points to my node's IP.*) to run a live DNS lookup via `dns.google.com/resolve`:

  * Success — you can proceed with **Next**.
  * Failure — an inline error explains that the domain did not resolve; fix the record at your registrar and retry.

  **← Back to Generation.** returns to the choice screen if you want **Generate FQDN for Me** instead. This path matches **Nginx + TLS** on your server — see [**Own domain (Nginx + TLS)**](/coti-documentation/node-ecosystem/installation/installation-own-domain).

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-981d9c5ef1df2a5f6e0a7efdcb5d072624e6dabd%2Fnode-ecosystem-setup-4-own-fqdn.png?alt=media" alt="Bring your own FQDN: Node FQDN input, A or CNAME notice, verification checkbox, and Back to Generation link"><figcaption><p><strong>Bring your own FQDN</strong>: enter your hostname, confirm DNS, or go back to the generated-FQDN path.</p></figcaption></figure>

{% hint style="warning" %}
**This FQDN is the address the ecosystem will use to reach your node's JSON-RPC for uptime monitoring.** **Own domain + host Nginx** needs a live DNS record to your server and reachable **80/443**. **COTI tunnel** (`--with-frp`) uses the COTI-assigned hostname and edge TLS; on your machine, **frpc** forwards to an internal Docker **`nginx-frpc-gateway`** (not host Nginx). Overview: [**Installation**](/coti-documentation/node-ecosystem/installation). Without a reachable public RPC name your node cannot earn rewards.
{% endhint %}

### Step 5 — Run the command

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-f52aab9db267caa282a2e9455706588f61c61a53%2Fnode-ecosystem-setup-5.png?alt=media" alt="Setup step 5 showing Linux or macOS installer tabs with the generated curl one-liner and a Learn more about installation link"><figcaption><p>Step 5: the installer command tailored to your key, FQDN, and OS tab (Linux / WSL or macOS).</p></figcaption></figure>

The wizard displays a one-liner tailored to your key, FQDN, and install flags. Use the **Linux / WSL** or **macOS** tab to match where Docker runs:

**Linux / WSL** (Ubuntu 24.04, or WSL 2 on Windows 11):

```bash
curl -sL https://fullnode.<network>.coti.io/install-linux | sudo bash -s -- "<PRIVATE_KEY>" "<FQDN>" [--with-frp | --with-nginx]
```

**macOS** (no `sudo`):

```bash
curl -sL https://fullnode.<network>.coti.io/install-mac | bash -s -- "<PRIVATE_KEY>" "<FQDN>" [--with-frp | --with-nginx]
```

Copy and run the matching command on your machine. A **"Learn more about installation"** link opens the [**Installation** overview](/coti-documentation/node-ecosystem/installation). Tick **"I've run this command"** to advance.

After the stack starts, you can confirm sync locally at <http://127.0.0.1:8090> on the server (operator status dashboard — see [Glossary → Operator status dashboard](/coti-documentation/node-ecosystem/ui-guide/glossary#operator-status-dashboard-local)).

### Step 6 — Waiting for node connection

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-7df96a35e6c2fd5bdac36c98663b8f692b31146e%2Fnode-ecosystem-setup-6.png?alt=media" alt="Setup step 6 with a spinner and the message Listening for node heartbeat"><figcaption><p>Step 6: the wizard polls peer discovery every ~60 seconds and advances automatically once your node is seen.</p></figcaption></figure>

The wizard polls the peer-discovery service every \~60 seconds and waits for your node address to show up in the peer set. Three states:

* **Searching** — spinner + "Listening for node heartbeat". You can safely close the tab and return later.
* **Detected** — green check + "Heartbeat Detected!". The wizard auto-advances in a few seconds.
* **Timeout** — after the initial grace period, a retry countdown appears and the wizard retries automatically.

{% hint style="info" %}
This step only confirms that peers can *see* your node. It does not mean your node is hot or that an NFT has been minted. That is the **warm-up period** (see [Glossary → Warming up](/coti-documentation/node-ecosystem/ui-guide/glossary#warming-up)), tracked on the **My Node** tab.
{% endhint %}

### Step 7 — Your node is live

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-7d3b40ba5d9d3966f5f17115d0043459b3acbccd%2Fnode-ecosystem-setup-7.png?alt=media" alt="Setup step 7 success card with Active badge, first heartbeat, and Go to My Dashboard button"><figcaption><p>Step 7: node is on the network; warm-up and NFT minting still follow.</p></figcaption></figure>

A success card with a **Go to My Dashboard** button that routes to **My Node**. The screen confirms your node's **first heartbeat** was detected and that rewards can accrue this epoch when you stay online and meet eligibility. **Uptime monitoring** (Better Stack) starts only after warm-up completes and the Soulbound NFT is minted — see [Glossary → Warming up](/coti-documentation/node-ecosystem/ui-guide/glossary#warming-up).

If the connected wallet differs from the node address, the wizard shows a warning explaining that the dashboard only lists nodes owned by the connected wallet — connect the node wallet (or import its private key into MetaMask) to see the node on the dashboard.

## My Node (`/my-nodes`)

The per-operator dashboard. It requires a connected wallet; the wallet must own a Soulbound Node NFT to show full content.

### Warmup (in progress / complete)

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-d6047b8f396352c17540c05f43b96bca53fc07d9%2Fnode-ecosystem-my-nodes-warmup.png?alt=media" alt="My Node screen showing the Warmup Complete card with the progress bar at 100%"><figcaption><p>The warm-up card near the end of the warm-up period, with the progress bar full. The NFT is about to be minted.</p></figcaption></figure>

While the node is warming up, the page shows a **Warmup In Progress** card with an elapsed / required progress bar (for example `18h 30m / 72h (26%)`) and copy explaining that the operator should keep the node online to complete the warm-up.

When the threshold is reached (progress reaches 100%, as in the screenshot above), the card flips to **Warmup Complete**. The Soulbound NFT is minted shortly thereafter and the full dashboard replaces the warm-up card automatically.

### No node detected

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-93f5d4a34414812b6bbc8e267ef41781006d4c93%2Fnode-ecosystem-my-nodes-no-nft.png?alt=media" alt="My Node screen showing No Node Detected for a wallet without a node NFT"><figcaption><p>Empty state for wallets that do not own a node NFT.</p></figcaption></figure>

If the connected wallet has not started setup and does not own a node NFT, the page shows a "No Node Detected" card with a link back to the overview. Operators who just finished `/setup` but see this screen are likely connected with the wrong wallet — the NFT is minted to the **node address**, not the wallet used to navigate the site.

### Full dashboard

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-a0f576a2405e2446687e7f66a434d2977653cd79%2Fnode-ecosystem-my-nodes-dashboard.png?alt=media" alt="My Node dashboard: identity card with LIVE, Total Earned and claimable, three stat cards, Eligibility This Epoch with Path 1 and Path 2"><figcaption><p>Full operator dashboard once the NFT has been minted: identity, stats, and this-epoch eligibility paths.</p></figcaption></figure>

Once the NFT exists, the dashboard shows:

* **Node identity card** — NFT image, node name, **`LIVE`** badge when the node is considered online, truncated node address, and **Edit** → `/edit-node`. **Total Earned** (all-time COTI from rewards) appears with a secondary **claimable** line (accrued balance not yet withdrawn) and a **Claim Now** button that sends a claim transaction on the rewards contract into the connected wallet. Operators who prefer to claim off-site can call the contract directly.
* **At-a-glance stats** (three cards) — **All-Time Uptime** with a performance badge (for example **Excellent**), **Eligibility Streak (Days)** with a streak-style badge (for example **On Fire**), and **COTI Claimed** (lifetime claimed amount) with a complementary badge (for example **Stacking**). Icons and colors reinforce each metric.
* **Eligibility** — section title **Eligibility** with **This Epoch** and a shield icon. The same **two-path** layout as **`/eligibility`**: **Path 1 — USDC + COTI** and **Path 2 — COTI only** separated by **OR**. Each path card can show a corner status (for example **Not yet** while requirements are incomplete). Rows list **USDC Holdings**, **COTI Holdings** (Path 2 includes *No USDC required* helper copy), and **Uptime**, each as **current / threshold** with a progress bar: green when the row is satisfied, yellow or orange when not; row icons mark met vs not met. You are eligible for the epoch when **either** path is fully satisfied (all its rows green).

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-76a81b0df4da148e3a3c32e4c871e609cff460b7%2Fnode-ecosystem-my-nodes-past-epochs.png?alt=media" alt="Past Epochs table with Epoch USDC COTI Earned Uptime bars and Eligible status, plus pagination footer"><figcaption><p><strong>Past Epochs</strong>: per-epoch snapshots, uptime bars, eligibility badges, and pagination.</p></figcaption></figure>

* **Past Epochs** — subtitle *Historical eligibility data.* A paginated table with columns **Epoch**, **USDC**, **COTI**, **Earned**, **Uptime** (percentage with a horizontal bar), and **Status** (**Eligible** / **Ineligible** as a pill). The footer shows how many epochs are on the current page versus the total (for example *Showing 4 of 90 past epochs*), a page indicator (for example `1 / 23`), and **Previous** / **Next** controls.

## Edit Node (`/edit-node`)

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-1f9ad05eb7b01009ca678b63dbfe986abda702d9%2Fnode-ecosystem-edit-node.png?alt=media" alt="Edit Node Details form beside Configuration Summary with NFT preview and token details"><figcaption><p><strong>Edit Node</strong>: two-column layout — form on the left, live summary on the right.</p></figcaption></figure>

The header breadcrumb reads **My Node / \<node name>** so you can see which node you are editing.

Two cards sit side by side:

* **Edit Node Details** — copy explains that **changes are saved to the node's NFT metadata**. Fields:
  * **Node Name** — display name for the node.
  * **Node Image URI** — HTTPS URL for the node's avatar image (shown in ecosystem UIs).
  * **More Info URL** — optional link (placeholder `https://...`) for extra context (site, docs, etc.).
  * **Submit Node Configuration** — primary action; submitting triggers the wallet flow to update on-chain NFT metadata (signature / transaction, depending on implementation).
* **Configuration Summary** — live preview: rendered **image** from the URI, **Name**, **URL** (or **N/A** when empty), **Node ID (Token)** as the NFT token id, and the **NFT contract address** for reference.

After a successful update, refreshed metadata appears on **My Node** and in the public nodes table once the app reloads chain or indexer data.

## Eligibility (`/eligibility`)

<figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-e0ca9782d157a56e1d079562d539aa0a7cedecb7%2Fnode-ecosystem-eligibility.png?alt=media" alt="Node Reward Eligibility page with Path 1 USDC plus COTI and Path 2 COTI only separated by OR"><figcaption><p><strong>Node Reward Eligibility</strong>: two paths, numeric thresholds from the rewards contract, and an <strong>OR</strong> between paths.</p></figcaption></figure>

The page opens with an **Eligibility Criteria** pill, the title **Node Reward Eligibility**, and the subtitle *Two ways to qualify. Meet either path's requirements — and run a node — to earn rewards.*

Under **Eligibility Paths**, two cards sit on either side of a centered **OR** divider:

* **Path 1 — USDC + COTI** — *Hold both USDC and COTI on the COTI network, plus run a node.* Lists **USDC Holdings** (stablecoins on COTI network), **COTI Holdings** (non-custodial wallets; not CEX), and **Uptime** (while running your node). You must satisfy **all three** rows on this path for Path 1 to count.
* **Path 2 — COTI only** — *Hold COTI alone — no USDC required — plus run a node.* Lists **COTI Holdings** at the **solo** threshold (higher than Path 1’s COTI bar) and **Uptime**. Both must be met for Path 2 to count.

The exact numbers (for example `1+` / `2+` / `98%+`) are loaded from the on-chain eligibility rules and can differ by network or governance updates.

Below the cards, an info strip states that you can meet **either** path for rewards eligibility, that balances refresh about every **24 hours**, and includes a **Check My Eligibility** button: if the connected wallet already holds a node NFT, it routes to **My Node**; otherwise it opens a modal that summarizes status and can steer you to **`/setup`**.

Further down, **Important Notes** (*Things to Know*) expands on topics such as the two-path rule, that **COTI on centralized exchanges does not count**, continuous checks, and that node status is evaluated dynamically.

Authoritative rule logic is summarized under [Features → Eligibility checks](/coti-documentation/node-ecosystem/features#4-eligibility-checks) and [Glossary → Eligibility](/coti-documentation/node-ecosystem/ui-guide/glossary#eligibility).


# Glossary

Terms you will encounter in the COTI Node Ecosystem web app, the installer, and this documentation. Definitions are authored for node operators; wording may differ from internal field names while preserving the same meaning.

## Node states

### Node status

The operational state of the node process, derived from the node's JSON-RPC `eth_syncing` method and peer presence:

* **Active** — the node is online and fully synced with the tip of the chain.
* **Syncing** — the node is online but still catching up on blocks.
* **Offline** — the node is not currently observed in the peer set.

"Online" in the UI refers to any node that is **Active** or **Syncing**.

### NFT state

The on-chain thermal flag stored on the node's Soulbound NFT:

* **Hot** — the NFT has been minted and is currently marked hot.
* **Cold** — either the NFT has not been minted yet (treated as cold by default), or the NFT has been marked cold after a prolonged outage.

### Thermal status

The combined state shown to operators, derived from node status + NFT state:

| Node status      | NFT state | Thermal status   |
| ---------------- | --------- | ---------------- |
| Active / Syncing | Cold      | **Warming up**   |
| Active / Syncing | Hot       | **Hot**          |
| Offline          | Hot       | **Cooling down** |
| Offline          | Cold      | **Cold**         |

### Warming up

A node is **warming up** when it is online and reachable but has not yet been continuously present long enough for the ecosystem to consider it stable. No NFT has been minted yet. The operator should keep the node online — the warm-up progress bar in `/my-nodes` tracks remaining time.

### Cooling down

A node is **cooling down** when it was previously hot (NFT minted) but has gone offline. If the node comes back online before all connected time in the rolling **103-hour** window has aged out, it can remain hot. If it stays offline until peer presence in that window reaches zero, the NFT flips to cold and the node must warm up again (\~**72 hours** of connected time within the window).

### Time to thermal update

Time remaining until the node transitions thermal state — until it becomes **hot** if currently warming up, or until it becomes **cold** if currently cooling down. Not shown when the node is already in a stable state (hot-while-online or cold-while-offline).

## Warm-up & cooldown windows

The thermal state machine is driven by four configuration values maintained by the ecosystem. They are exposed so operators understand the timing of their node's transitions.

### HOT\_WINDOW\_HOURS

The length of the rolling window used to decide whether a node is stable enough to become hot. Defaults to **103 hours** (one epoch).

### HOT\_THRESHOLD\_HOURS

The minimum number of hours *within* `HOT_WINDOW_HOURS` that a node must be present as a peer to qualify as hot and receive its Soulbound NFT. Defaults to **72 hours**.

In plain language: *"To become hot, a node must be seen by peers for at least `HOT_THRESHOLD_HOURS` hours during the last `HOT_WINDOW_HOURS` hours."*

#### Becoming hot — illustrated example (production values)

In production the ecosystem uses **`HOT_THRESHOLD_HOURS` = 72** and **`HOT_WINDOW_HOURS` = 103**. Peer discovery evaluates a **rolling** window: at any moment, only the last 103 hours count. Connected time that started **before** that window has already slid out and no longer contributes.

```
Each cell = 1 hour.  █ = connected as peer   · = offline or not yet in window

NOW ─────────────────────────────────────────────────────────────────────► time
                    │◄──────────── 103-hour rolling window ────────────►│
                    │                                                   │
Case A — warming up │································████████████░░░░░░░│  50 h connected → not hot yet
                    │                                                   │  (need 72 h)

Case B — qualifies  │································██████████████████│  72 h connected → HOT
                    │                                                   │  (NFT minted)

Case C — hours lost │████████████████████ (80 h connected long ago)         │
                    │                      │◄── 103 h window ──────────►│
                    │                      ················████████████│  53 h still count
                    │                      (offline since then)         │  27 h already slid out
                    │                                                   │  → not hot (need 72 h)
```

**How to read the diagram**

| Case  | What happened                                                                                                                         | Connected in window | Result                                                                  |
| ----- | ------------------------------------------------------------------------------------------------------------------------------------- | ------------------- | ----------------------------------------------------------------------- |
| **A** | Node came online recently and has been up steadily                                                                                    | 50 h                | Still **warming up** — 22 h short of 72                                 |
| **B** | Same node, still online, a while later                                                                                                | 72 h                | **Hot** — threshold met inside the window                               |
| **C** | Node was connected for 80 h, then went offline; by the time it is evaluated again, the first 27 h of that streak are older than 103 h | 53 h (80 − 27 lost) | **Not hot** — earlier connected hours are **lost** for warm-up purposes |

The window moves forward continuously. Every hour that passes drops the oldest hour from the count and adds a new empty hour at the trailing edge — unless the node stays connected, in which case that new hour accrues as connected time.

### COLD\_WINDOW\_HOURS

The length of the rolling window used to decide whether a previously-hot node should be demoted back to cold. Defaults to **103 hours** (one epoch).

### COLD\_THRESHOLD\_HOURS

Configured alongside `COLD_WINDOW_HOURS`; in production both default to **103 hours**. Peer discovery marks a hot node **cold** when it has **zero peer presence** anywhere in the rolling `COLD_WINDOW_HOURS` window.

In plain language: *"A hot node that stays offline until all connected time in the last 103 hours has aged out cools back to cold and must redo the warm-up (\~103 hours of continuous absence)."*

## Identity & ownership

### Node private key

The 64-hex secret that identifies a node on the network. It is generated locally in the browser (or supplied by the operator), written to the node's `nodekey` file by the installer, and **never transmitted to any COTI server**.

### Node address (public key)

The Ethereum-style address derived from the node private key. It is used as the node's identity on-chain and is the wallet that receives the Soulbound Node NFT.

### Wallet address

The wallet connected to the web app (for example via MetaMask). For the per-operator dashboard to show a node, the connected wallet must be the node address — that is, the wallet that owns the Soulbound NFT.

### FQDN (Fully Qualified Domain Name)

The public hostname the operator uses for their node. It may be **your own domain** (for example `node1.example.com`) or a **COTI-assigned** name when using the tunnel installer (`--with-frp`), typically under a managed zone such as `*.testnet.nodes.coti.network` on testnet.

The FQDN is a **prerequisite for rewards**: the ecosystem probes JSON-RPC through that name to determine uptime. See [**Installation**](/coti-documentation/node-ecosystem/installation), [**Own domain**](/coti-documentation/node-ecosystem/installation/installation-own-domain), and [**Wizard tunnel**](/coti-documentation/node-ecosystem/installation/installation-wizard-tunnel).

### RPC URL

The public HTTPS endpoint the node exposes after installation, typically `https://<your-fqdn>/rpc`. After your node becomes **hot** and a monitor is registered, Better Stack polls the **Node Health Monitor**, which in turn calls this URL to verify block progression — Better Stack does not call your node directly.

### Soulbound Node NFT

A non-transferable NFT minted to the operator's wallet once the node becomes hot. It stores the node's name and image and is the on-chain proof of node ownership. Because it is soulbound, it cannot be transferred between wallets.

## Rewards

### Epoch

A fixed 103-hour reward period. At the end of each epoch, the rewards service evaluates eligibility and credits eligible nodes.

### Eligibility

The set of rules a node + operator must satisfy in an epoch to receive rewards. The web app describes **two paths**; you qualify if **either** path is fully met (see **`/eligibility`**):

* **Path 1 — USDC + COTI.** USDC on the COTI network ≥ combo threshold **and** COTI (non-custodial; **not** CEX) ≥ combo threshold **and** uptime ≥ configured percentage.
* **Path 2 — COTI only.** COTI ≥ solo threshold (no USDC) **and** uptime ≥ configured percentage.

Whitelisted operators are exempt from the holdings rules and only need to meet the uptime rule.

### Eligible

A node that was found eligible for rewards in at least one epoch. The home-page stats card counts unique nodes that have ever been eligible; the `/my-nodes` per-epoch history marks each epoch with **Eligible** / **Ineligible**.

### Whitelisted

An operator flag that exempts the wallet from the **Path 1 / Path 2** holdings checks. Uptime is still required. Whitelisting is managed centrally by COTI; most operators will not be whitelisted.

### Blacklisted

An operator flag that prevents a node from being accepted as a peer candidate. Blacklisted nodes do not appear in the ecosystem's dataset and cannot accrue rewards. Blacklisting is used for operational or abuse reasons and is not a state a normal operator encounters.

### Uptime

The percentage of an epoch during which the node was judged healthy by the monitoring stack. An RPC that merely answers is not enough — the health check confirms that the node is actually operating, and only healthy results accrue uptime. Displayed both per-epoch and all-time.

### Rewards (total / last epoch)

* **Total Rewards** — all-time COTI earned by a node across every epoch it was eligible for.
* **Last Epoch Rewards** — COTI earned in the most recently closed epoch.
* **Next Epoch Rewards** — the size of the pool that will be distributed in the upcoming epoch (configured centrally).

Rewards accrue in the on-chain **rewards smart contract** rather than being deposited directly into the operator's wallet. Operators withdraw their balance via the **Claim Now** button in the **My Node** dashboard, or by calling the rewards smart contract directly.

## Other terms you may see

### Live node heartbeats

The home-page visualization showing the count of nodes currently observed by peer discovery. The pulsing tiles are purely decorative; only the counter reflects real data.

### Better Stack

The external uptime-monitoring platform used by the ecosystem to probe every hot node. Operators do not sign up for or configure Better Stack directly — the ecosystem registers monitors automatically after NFT minting.

### Status page

The public Better Stack dashboard that aggregates every hot node's monitor state (up / down). It is the fastest way to see the current health of the whole fleet. URLs are listed in [Networks](/coti-documentation/node-ecosystem#networks).

### Operator status dashboard (local)

A small **local** web UI shipped with [`coti-full-node`](https://github.com/coti-io/coti-full-node), bound to **localhost** at port **8090** after `./start_coti-full-node.sh`. It shows process health, peer count, sync state, and (when configured) DNS/TLS or FRPC gateway checks — distinct from the **Nodes web app** dashboard at `/my-nodes`. With **host Nginx** or the **COTI tunnel** (via the internal **`nginx-frpc-gateway`** container), the same page is also available at **`https://<fqdn>/operator/`**.


# Server requirements

This page lists the **certified operating system**, **tested software stack**, and **hardware sizing** that apply to **both**:

* the **wizard** one-liner from [**Installation**](/coti-documentation/node-ecosystem/installation) (`/install-linux` with `sudo bash`, or `/install-mac` with `bash` only, from the official installer host), and
* **self-managed** install from [**Manual full node setup**](/coti-documentation/node-ecosystem/installation/manual-full-node) (Git clone, Docker Compose, and scripts on your own) — also under Installation.

Path-specific steps (DNS, ports, tunnel vs Nginx) live on the [**Installation**](/coti-documentation/node-ecosystem/installation) subpages linked above.

## Certified operating system

### Linux servers (primary)

The COTI full node stack is **certified on Ubuntu 24.04 LTS** for normal **Linux** deployments (cloud VMs, bare metal, on-prem). This is the primary baseline COTI tests end-to-end.

* **Wizard installer:** other Debian-family distributions *may* run the script — the installer will warn and prompt for confirmation — but COTI does **not** guarantee compatibility and does **not** support those targets. When in doubt, deploy a fresh **Ubuntu 24.04 LTS** server.
* **Manual path:** use **Ubuntu 24.04.x** to stay aligned with this baseline.

### Windows 11 with WSL 2

Installation is **also certified** on **Microsoft Windows 11** when you use **WSL 2** and an **Ubuntu 24.04 LTS** distribution as the WSL guest. Run Docker Engine and the node stack **inside** that Ubuntu environment (same expectations as on a native Linux host: root-capable install, disk and RAM for the network you join, and a supported Docker setup for WSL). Follow current **Microsoft WSL** and **Docker** documentation for kernel, storage, and networking limits on Windows hosts.

The one-liner installer is designed for **Ubuntu 24.04 LTS** and should run without the unsupported-OS confirmation prompt on this certified WSL setup.

Other Windows versions, WSL 1, or non-certified distros under WSL are **not** treated as supported targets unless listed here.

## Software stack

These versions reflect **what we have certified and tested at the time this page was written**. Newer **patch and minor** releases of the same software (Ubuntu LTS point releases, Docker Engine, Compose) **usually work** without changes; if you use a newer stack, validate on **Testnet** before relying on it for **Mainnet**.

* **Docker:** version **28.0.1** (tested)
* **Docker Compose:** version **2.29.1** (tested) — **Compose v2 plugin** (`docker compose`). Legacy `docker-compose` v1 is **not** supported by the node scripts.

See also Docker’s [**Linux system requirements**](https://docs.docker.com/desktop/setup/install/linux/#general-system-requirements) for kernel, cgroup, and storage-driver expectations on Ubuntu.

## Hardware

CPU and memory targets are the **same on Testnet and Mainnet**. **Disk size depends on the network** — Mainnet chain data and traffic require substantially more storage than Testnet.

| Specification    | Minimum | Recommended | Professional |
| ---------------- | ------- | ----------- | ------------ |
| **vCPUs**        | 2       | 4           | 8            |
| **Memory (GiB)** | 8       | 16          | 64           |

### Storage by network

Size the disk for the **network you join** (Testnet vs Mainnet). Figures are planning guides; leave headroom for chain growth, snapshots, and logs.

| Network     | Minimum | Recommended | Professional (extra headroom) |
| ----------- | ------- | ----------- | ----------------------------- |
| **Testnet** | 90 GB   | 200 GB      | 500 GB                        |
| **Mainnet** | 700 GB  | 1 TB        | 1.5 TB                        |

These bands match the [storage table above](#storage-by-network) (≥ 90 GB Testnet, ≥ 700 GB Mainnet minimum for ecosystem guidance).

The **wizard installer** checks free space on the **filesystem where Docker stores images and volumes** (Docker engine root — chain data uses a named volume, not the project folder). Required free space comes from the network profile (`networks/<network>.env`): **90 GB** on Testnet, **700 GB** on Mainnet. Use the **Minimum** column above as planning guidance; leave headroom beyond the installer floor.

In addition to the above, a **reliable, high-bandwidth internet connection** is recommended.

### Recommended minimum hosted configuration

* AWS: **m7a.large** (2 vCPUs, 8 GiB memory)
* OVH: **b2-15** (4 vCPUs, 15 GiB memory)

### Recommended optimal hosted configuration

* AWS: **r5n.2xlarge** (8 vCPUs, 64 GiB memory)
* OVH: **r2-120** (8 vCPUs, 120 GiB memory)

## Related documentation

* [**Installation**](/coti-documentation/node-ecosystem/installation) — hub; subpages [**Wizard tunnel**](/coti-documentation/node-ecosystem/installation/installation-wizard-tunnel), [**Own domain**](/coti-documentation/node-ecosystem/installation/installation-own-domain), [**Manual full node setup**](/coti-documentation/node-ecosystem/installation/manual-full-node).


# Installation

This section documents every way to install and run a COTI full node from our tooling: the [**`/setup`** wizard](/coti-documentation/node-ecosystem/ui-guide) (`curl | bash` one-liner from the official installer host), or **Git clone + Docker Compose** without the wizard. This hub page is the destination for **"Learn more about installation"** in the wizard. See [Networks](/coti-documentation/node-ecosystem#networks) for testnet and mainnet URLs.

{% hint style="info" %}
**OS and hardware** are the same on every path — see [**Server requirements**](/coti-documentation/node-ecosystem/server-requirements). **Without the web app**, use [**Manual full node setup**](/coti-documentation/node-ecosystem/installation/manual-full-node) (last page in this section).
{% endhint %}

## Certified operating system and hardware

The **same** certified OS and server sizing apply to every path in this section (wizard or manual). Full detail is on [**Server requirements**](/coti-documentation/node-ecosystem/server-requirements).

## Choose your installer flow

The wizard produces a one-liner from `https://fullnode.<network>.coti.io`:

| Path                 | OS                                                          | Installer (served script)                                                                                                                                                   |
| -------------------- | ----------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`/install-linux`** | Ubuntu 24.04 LTS, or **WSL 2** + Ubuntu 24.04 on Windows 11 | [`install_coti-full-node.sh`](https://fullnode.mainnet.coti.io/install-linux) ([testnet](https://fullnode.testnet.coti.io/install-linux)) — use **`sudo bash`**             |
| **`/install-mac`**   | macOS (Docker Desktop or Colima)                            | [`install_coti-full-node-mac.sh`](https://fullnode.mainnet.coti.io/install-mac) ([testnet](https://fullnode.testnet.coti.io/install-mac)) — use **`bash`** only (no `sudo`) |

Pick the guide that matches the **flags** the wizard gives you. **Self-managed** install (no wizard) is the last row.

| Flow                    | When                                                                                                                                                               | Guide                                                                                                            |
| ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------- |
| **COTI-managed tunnel** | COTI-assigned subdomain, **`--with-frp`**, no **host** Nginx/Let’s Encrypt — traffic goes edge → **frpc** → internal **`nginx-frpc-gateway`** (Docker) → full node | [**Wizard tunnel (COTI subdomain)**](/coti-documentation/node-ecosystem/installation/installation-wizard-tunnel) |
| **Your own domain**     | Your DNS name, **`--with-nginx`**, Let’s Encrypt + Nginx on the host                                                                                               | [**Own domain (Nginx + TLS)**](/coti-documentation/node-ecosystem/installation/installation-own-domain)          |
| **Manual (no wizard)**  | Clone [`coti-full-node`](https://github.com/coti-io/coti-full-node), scripts, Compose — not the web app                                                            | [**Manual full node setup**](/coti-documentation/node-ecosystem/installation/manual-full-node)                   |

{% hint style="danger" %}
Piping `curl` into `bash` runs a remote script on your machine. On **Linux / WSL**, the wizard uses **`sudo bash`** (`/install-linux`). On **macOS**, use **`bash` only** (`/install-mac`) — the macOS installer refuses `sudo`. Only use commands from the official wizard at `https://fullnode.<network>.coti.io` (paths **`/install-linux`** and **`/install-mac`**; `<network>` is `testnet` or `mainnet`). When in doubt, review the scripts in the [`coti-full-node`](https://github.com/coti-io/coti-full-node) repository.
{% endhint %}

## After any wizard install

* The node syncs from peers; the browser wizard polls peer discovery until your node appears.
* You enter the **warm-up** window before the node becomes **hot** and an NFT is minted — see the [Glossary](/coti-documentation/node-ecosystem/ui-guide/glossary).
* A **local operator status page** is available at <http://127.0.0.1:8090> on the host (localhost only). With Nginx or the COTI tunnel, the same dashboard is also at **`/operator/`** on your public HTTPS hostname — see the subpage for your flow.

For **restart / stop / logs** when you manage the repo yourself, see [**Manual full node setup → Restarting your node**](/coti-documentation/node-ecosystem/installation/manual-full-node#restarting-your-node).

## Configuration files (after install)

Configuration lives in **`.env`** (this host) plus network profiles under **`networks/`** (chain defaults). `start_coti-full-node.sh` and `stop_coti-full-node.sh` load **`.env`**, then **`networks/<NETWORK>.env`**, then **`.env` again** (host values win on overlap).

| File                         | Purpose                                                                                                                                        |
| ---------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- |
| **`.env`**                   | This host: `NETWORK`, `DOCKER_FULL_NODE_IMAGE_VERSION`, `FULLNODE_FQDN`, `FULLNODE_EXT_IP`, `NGINX_ENABLED`, `FRPC_ENABLED`, FRPS hosts, keys. |
| **`networks/<network>.env`** | Chain profile: bootnodes, network id, soda addresses, FRPS regional defaults, install disk requirement.                                        |

**Upgrade the node image:** edit `DOCKER_FULL_NODE_IMAGE_VERSION` or set `IMAGE=` in `.env`, then `./stop_coti-full-node.sh` and `./start_coti-full-node.sh`. Per-variable reference: [`.env.example`](https://github.com/coti-io/coti-full-node/blob/main/.env.example) in the repo.

Scripts require **Docker Compose v2** (`docker compose`); legacy `docker-compose` v1 is not supported.

## Optional flags (overview)

The [**Wizard tunnel**](/coti-documentation/node-ecosystem/installation/installation-wizard-tunnel) and [**Own domain**](/coti-documentation/node-ecosystem/installation/installation-own-domain) pages document flags in full. Quick reference:

| Flag                                                   | Typical use                                                                                                                              |
| ------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------- |
| **`--with-frp`**                                       | Tunnel flow — see [Wizard tunnel](/coti-documentation/node-ecosystem/installation/installation-wizard-tunnel).                           |
| **`--with-nginx`**                                     | Own domain + TLS on host — see [Own domain](/coti-documentation/node-ecosystem/installation/installation-own-domain).                    |
| **`--testnet`**, **`--mainnet`**                       | Chain profile (bootnodes, image, FRPS defaults, disk check). Wizard one-liners infer from FQDN; local script runs need an explicit flag. |
| `--staging`                                            | Let’s Encrypt staging — only with `--with-nginx`.                                                                                        |
| `--frpc-custom-domain=`, `--frps-server-addr-1=`, etc. | Optional FRPC tuning — see [Wizard tunnel](/coti-documentation/node-ecosystem/installation/installation-wizard-tunnel).                  |

Host Nginx (`--with-nginx`) and FRPC (`--with-frp`, which starts an internal **`nginx-frpc-gateway`** container) cannot both be enabled on one install. **Host Nginx/TLS and FRPC are off by default** — pass **`--with-nginx`** or **`--with-frp`** to enable one.

For **manual** operation (restart, stop, logs, FAQ), see [**Manual full node setup**](/coti-documentation/node-ecosystem/installation/manual-full-node).


# Wizard tunnel (COTI subdomain)

This is the **simplest** wizard path: on **Setup FQDN**, click **Generate FQDN for Me**. The wizard shows a success message and the **Node FQDN** value to use in the installer — a **COTI-assigned hostname** under the network’s managed zone (for example `drove-nova-11.testnet.nodes.coti.network` on testnet; the exact parent suffix depends on the environment). Then run the installer with **`--with-frp`**.

← Back to [**Installation overview**](/coti-documentation/node-ecosystem/installation) · Related: [**Own domain (Nginx)**](/coti-documentation/node-ecosystem/installation/installation-own-domain) · [**Manual full node setup**](/coti-documentation/node-ecosystem/installation/manual-full-node)

## What runs on your machine

There is **no host Nginx** (no Let’s Encrypt, no ports **80/443** published for TLS on the VM). The tunnel flow **does** run an internal **`nginx-frpc-gateway`** Docker container: **COTI edge → frpc → internal Nginx (:8080) → full node / operator dashboard**. That gateway rewrites `/rpc`, `/ws`, `/metrics`, and `/operator/` to the node services inside Compose.

## What COTI provides

* **No third-party domain to buy** — DNS for that hostname is operated by COTI.
* **No TLS certificate on your server** — HTTPS terminates at COTI’s edge; **FRP (frps)** and DNS route traffic to **frpc** on your machine, which forwards through an **internal Nginx gateway** (Docker-only, no certificates) to the full node’s JSON-RPC, WebSocket, metrics, and operator dashboard.

## What you skip on the host

* **Inbound firewall rules for 80, 443, and 7400 from the public internet** are not required for this mode as designed: RPC over HTTPS reaches you through the tunnel; P2P can work with normal outbound connectivity.

The installer enables the **FRPC** Compose profile (including an internal **nginx-frpc-gateway** for `/rpc`, `/ws`, `/metrics`, and `/operator/` path rewrite), keeps **host Nginx + Let’s Encrypt off**, and **skips** `ufw` / `iptables` checks that assume you must open 80/443/7400 inbound. It still requires **port 7400 free locally** (no other process binding it) so the node container can use it.

## Prerequisites

1. **Server** meeting [**Server requirements**](/coti-documentation/node-ecosystem/server-requirements) (certified **Ubuntu 24.04 LTS** on Linux, or **Windows 11** + **WSL 2** + **Ubuntu 24.04 LTS**; disk, RAM), with **root access**.
2. The **FQDN** string shown in the wizard after generation (same value the one-liner expects; hostname pattern is network-specific).
3. **Node private key** (64 hex chars) from the wizard or your own.

## One-line command

The wizard shows **Linux / WSL** and **macOS** tabs. Use the line that matches where Docker runs. `<network>` is `mainnet` or `testnet`. `<FQDN>` is the COTI-assigned hostname; `<PRIVATE_KEY>` may include or omit the `0x` prefix.

**Linux / WSL (Ubuntu 24.04)** — `install_coti-full-node.sh` at `https://fullnode.<network>.coti.io/install-linux`; run as **root**:

```bash
curl -sL https://fullnode.<network>.coti.io/install-linux | sudo bash -s -- "<PRIVATE_KEY>" "<FQDN>" --with-frp
```

**macOS** — `install_coti-full-node-mac.sh` at `https://fullnode.<network>.coti.io/install-mac`; **do not** use `sudo`:

```bash
curl -sL https://fullnode.<network>.coti.io/install-mac | bash -s -- "<PRIVATE_KEY>" "<FQDN>" --with-frp
```

**Windows 11:** use **WSL 2** + **Ubuntu 24.04 LTS** and the **Linux** command above. There is no separate Windows installer path.

## What the installer does (this flow)

Driven by `install_coti-full-node.sh` (`https://fullnode.<network>.coti.io/install-linux`) on Linux/WSL, or `install_coti-full-node-mac.sh` (`https://fullnode.<network>.coti.io/install-mac`) on macOS:

1. **OS and inputs** — Certified Ubuntu version check, root, valid hex key and hostname (non-24.04 may prompt; see [**Server requirements → Windows 11 with WSL 2**](/coti-documentation/node-ecosystem/server-requirements#windows-11-with-wsl-2)).
2. **Pre-checks** — Writable install dir, disk space; **no** inbound 80/443/7400 firewall enforcement; **7400** must not already be in use locally.
3. **Packages** — Docker, Compose, `curl`, `git`, `jq`, `dnsutils` (**no** `certbot` — host Nginx/Let’s Encrypt are not used in this flow).
4. **Clone** — `coti-full-node` into the current directory (must be empty).
5. **Config** — `.env` (host: `NETWORK`, image tag, FQDN, `FRPC_ENABLED`, FRPS hosts), chain defaults from **`networks/<network>.env`**, `nodekey`, **FRPC** `frpc-*.toml`, and internal **`nginx/frpc-gateway.conf`** when `--with-frp` is set.
6. **Host Nginx / Certbot** — **Skipped**; TLS is at COTI’s edge. An internal HTTP-only Nginx gateway runs inside Docker for path rewrite.
7. **Launch** — `./start_coti-full-node.sh` starts the node, **nginx-frpc-gateway**, and two regional **frpc** containers (`frpc-1`, `frpc-2`; defaults in `networks/<network>.env`). Each `frpc` tunnel terminates at the internal gateway on port **8080**, which rewrites `/rpc`, `/ws`, `/metrics`, and `/operator/` to the node and operator dashboard.

## After the command finishes

The script prints a summary (FRPC gateways, custom domain, logs). The node syncs; the wizard advances when peer discovery sees your node. Warm-up / hot / NFT rules are in the [Glossary](/coti-documentation/node-ecosystem/ui-guide/glossary).

### Operator status page (local)

After `./start_coti-full-node.sh`, a small **local** dashboard shows whether the node is running, has peers, is syncing, and (when configured) whether DNS or the FRPC gateway look healthy. On the machine where Docker runs, open <http://127.0.0.1:8090> (localhost only; auto-refreshes about every 15 seconds). Over SSH: `ssh -L 8090:127.0.0.1:8090 user@your-node`, then open the same URL in your desktop browser.

With the tunnel, the same page is also exposed at **`https://<your-coti-fqdn>/operator/`** through the edge (path rewrite via the internal Nginx gateway).

{% hint style="warning" %}
**Rewards need a reachable public RPC name.** Monitoring uses your **COTI-assigned** hostname and edge TLS. If DNS or the tunnel is wrong, uptime may not accrue. See [Glossary](/coti-documentation/node-ecosystem/ui-guide/glossary) and [**Server requirements**](/coti-documentation/node-ecosystem/server-requirements).
{% endhint %}

## Flags relevant to this flow

| Flag                                                                                | Purpose                                                                                                                                                                           |
| ----------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`--with-frp`**                                                                    | Enables FRPC + internal Nginx gateway (no host TLS/certs), relaxes inbound 80/443/7400 firewall checks (this guide).                                                              |
| **`--with-nginx`**                                                                  | Own-domain path instead — see [Own domain (Nginx)](/coti-documentation/node-ecosystem/installation/installation-own-domain).                                                      |
| **`--testnet`**, **`--mainnet`**                                                    | Select chain profile (image, bootnodes, FRPS regional defaults, disk requirement). Piped wizard installs infer network from the FQDN; local script runs require an explicit flag. |
| **`--frpc-custom-domain=`**, **`--frpc-auth-token=`**                               | Optional FRPC edge hostname and auth token when COTI assigns them separately from the node FQDN.                                                                                  |
| **`--frps-server-addr-1=`**, **`--frps-server-addr-2=`**, **`--frps-server-port=`** | Override regional FRPS gateways (defaults in `networks/<network>.env`).                                                                                                           |

FRPC is **off by default**; use **`--with-frp`** to enable it. Do not pass **`--with-frp`** and **`--with-nginx`** on the same install.

## Troubleshooting

* **FRPC / tunnel** — Confirm `docker ps` shows `nginx-frpc-gateway`, `frpc-1`, `frpc-2`, and the full-node container; the COTI hostname resolves and reaches the edge. Check logs: `docker logs` on the frpc containers and `coti-<network>-full-node`. Test RPC locally via the gateway: `docker exec coti-<network>-nginx-frpc-gateway wget -qO- --post-data='{"jsonrpc":"2.0","method":"eth_blockNumber","params":[],"id":1}' --header='Content-Type: application/json' http://127.0.0.1:8080/rpc`.
* **Port 7400 in use** — Another process is bound to 7400; free it before re-running.
* **Dirty directory** — Installer needs an empty folder; move or remove an old `coti-full-node` clone.


# Own domain (Nginx + TLS)

Use this flow when you **own a DNS name** and want **HTTPS on your server** via **Nginx** and **Let’s Encrypt**. The wizard’s command includes **`--with-nginx`**. In **`/setup`**, on **Setup FQDN**, choose **Bring your own FQDN**, enter your hostname in **Node FQDN**, configure an **A record** at your provider (or a **CNAME** to another hostname) as the in-wizard notice describes, then confirm with **I have completed my FQDN settings** before **Next**.

← Back to [**Installation overview**](/coti-documentation/node-ecosystem/installation) · Related: [**Wizard tunnel**](/coti-documentation/node-ecosystem/installation/installation-wizard-tunnel) · [**Manual full node setup**](/coti-documentation/node-ecosystem/installation/manual-full-node)

## Prerequisites

1. **Environment** meeting [**Server requirements**](/coti-documentation/node-ecosystem/server-requirements) (certified **Ubuntu 24.04 LTS** on Linux, or **Windows 11** + **WSL 2** + **Ubuntu 24.04 LTS**), with **root access**.
2. **Ports 80 and 443** free on the host (ACME HTTP-01 + HTTPS).
3. **Port 7400 (TCP + UDP)** free and **allowed** through host firewall and cloud security groups — see [**Manual full node setup → Network configuration**](/coti-documentation/node-ecosystem/installation/manual-full-node#network-configuration) for the port table.
4. **FQDN** (e.g. `node1.example.com`) with an **A record** to your server’s public IP, propagated **before** install.
5. **Node private key** (64 hex) from the wizard or your own.

{% hint style="warning" %}
**FQDN is a reward prerequisite.** The installer obtains a certificate for that name; the ecosystem probes `https://<fqdn>/rpc` for uptime. Misconfigured DNS or blocked 80/443 prevents rewards. See [Glossary](/coti-documentation/node-ecosystem/ui-guide/glossary).
{% endhint %}

## One-line command

The wizard shows **Linux / WSL** and **macOS** tabs. Use the line that matches where Docker runs. `<network>` is `mainnet` or `testnet`.

**Linux / WSL (Ubuntu 24.04)** — `/install-linux`; run as **root**:

```bash
curl -sL https://fullnode.<network>.coti.io/install-linux | sudo bash -s -- "<PRIVATE_KEY>" "<FQDN>" --with-nginx
```

**macOS** — `/install-mac`; **do not** use `sudo`:

```bash
curl -sL https://fullnode.<network>.coti.io/install-mac | bash -s -- "<PRIVATE_KEY>" "<FQDN>" --with-nginx
```

**Windows 11:** use **WSL 2** + **Ubuntu 24.04 LTS** and the **Linux** command above.

## What the installer does (this flow)

1. **OS and inputs** — Certified Ubuntu version check, root, valid key and FQDN (non-24.04 may prompt; see [**Server requirements → Windows 11 with WSL 2**](/coti-documentation/node-ecosystem/server-requirements#windows-11-with-wsl-2)).
2. **Pre-checks** — Disk space; ports **80**, **443**, and **7400** free; `ufw` / `iptables` must not block them when those checks apply.
3. **Packages** — Docker, Compose, **`certbot`**, plus `curl`, `git`, `jq`, `dnsutils`.
4. **Clone** — `coti-full-node` into an empty directory.
5. **Config** — `.env` (host: `NETWORK`, image tag, FQDN, `NGINX_ENABLED=true`, `FRPC_ENABLED=false`), chain defaults from **`networks/<network>.env`**, and `nodekey`.
6. **HTTPS** — Temporary Nginx on :80, **Certbot** for your FQDN, then full Nginx config for `/rpc`, `/ws`, `/metrics`, and **`/operator/`** with TLS.
7. **Launch** — `./start_coti-full-node.sh` starts the stack (requires **Docker Compose v2**: `docker compose`).

Public RPC is **`https://<your-fqdn>/rpc`** — that is what monitoring uses.

## After the command finishes

The script prints success with your HTTPS URL. The node syncs; the wizard waits on peer discovery. Warm-up / hot / NFT: [Glossary](/coti-documentation/node-ecosystem/ui-guide/glossary).

### Operator status page

* **Local (same machine):** <http://127.0.0.1:8090> — localhost only; use SSH port forwarding if you manage the server remotely.
* **HTTPS (public):** `https://<your-fqdn>/operator/` — same dashboard through your Nginx TLS reverse proxy.

## Flags relevant to this flow

| Flag                             | Purpose                                                                                                                                                             |
| -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **`--with-nginx`**               | Nginx + Let’s Encrypt on the host (this guide).                                                                                                                     |
| `--staging`                      | Let’s Encrypt **staging** CA (for dry runs; browsers won’t trust the cert).                                                                                         |
| `--with-frp`                     | COTI tunnel path instead — see [**Wizard tunnel**](/coti-documentation/node-ecosystem/installation/installation-wizard-tunnel). Do not combine with `--with-nginx`. |
| **`--testnet`**, **`--mainnet`** | Select chain profile. Piped wizard installs infer network from the FQDN; local script runs require an explicit flag.                                                |

Host Nginx is **off by default**; use **`--with-nginx`** to enable TLS on this machine.

**Dry-run example (Linux / WSL):**

```bash
curl -sL https://fullnode.<network>.coti.io/install-linux | sudo bash -s -- "0x..." "node1.example.com" --with-nginx --staging
```

## Troubleshooting

* **Certbot failed** — Check `dig <fqdn>`, wait for DNS, confirm **80/443** reachable from the internet.
* **Port in use** — Free 80, 443, or 7400 (old Nginx, Apache, another COTI install).
* **Wizard does not see the node** — `docker ps`, `docker logs -f coti-<network>-full-node`, confirm FQDN **A** record matches the server’s public IP.


# Manual full node setup (without the wizard)

← [**Installation**](/coti-documentation/node-ecosystem/installation) (this page is the last subpage under Installation)

This page is the **operator-managed** path: you install and run a COTI full node using the [`coti-full-node`](https://github.com/coti-io/coti-full-node) repository and Docker on your own server — **not** through the Nodes web app wizard, one-liner installer host, or guided spin-up UI.

> **New to running a node?** Use the **wizard** first — [**Installation**](/coti-documentation/node-ecosystem/installation) and [**UI guide**](/coti-documentation/node-ecosystem/ui-guide). It is the quickest path for most people; return here only if you intentionally skip the web app.

{% hint style="success" %}
**Web app wizard (recommended for most operators):** follow [**Installation**](/coti-documentation/node-ecosystem/installation), [**Server requirements**](/coti-documentation/node-ecosystem/server-requirements) (OS and sizing), and the [**UI guide**](/coti-documentation/node-ecosystem/ui-guide). You get the guided flow, installer from the [Networks](/coti-documentation/node-ecosystem#networks) table, HTTPS, and monitoring hooks from the product.

**This page (manual path):** you clone the repo, configure the host, and run `start` / `stop` scripts yourself. Reward **eligibility is the same** as the wizard path when your node satisfies ecosystem rules (FQDN, JSON-RPC reachability, token holdings, uptime thresholds, etc.) — see [**Node Ecosystem overview**](/coti-documentation/node-ecosystem) and [**Installation**](/coti-documentation/node-ecosystem/installation) for authoritative requirements.
{% endhint %}

For **what a COTI node is** and **why operators run one**, see [**What is a COTI node?**](/coti-documentation/node-ecosystem#what-is-a-coti-node) and [**Why run a node?**](/coti-documentation/node-ecosystem#why-run-a-node).

***

### Requirements

COTI full node software is published as a Docker image.

{% hint style="info" %}
**Disclaimer:** Successfully operating, troubleshooting, and maintaining a node requires technical proficiency. Familiarity with tools such as Linux, Docker, and Git is assumed. Users not familiar with this technology stack should consider the [**Installation**](/coti-documentation/node-ecosystem/installation) / [**UI guide**](/coti-documentation/node-ecosystem/ui-guide) flow or delegating operation to an experienced operator.
{% endhint %}

**Certified operating system, tested Docker/Compose versions, and hardware** (CPU, memory, disk by network, example cloud SKUs) are **identical** for the [wizard](/coti-documentation/node-ecosystem/installation) and this manual path — see [**Server requirements**](/coti-documentation/node-ecosystem/server-requirements).

***

### Network Configuration

#### Open Ports: Protocol and Purpose

You should open the following ports in your host firewall (e.g., UFW) **and** in your cloud provider’s security groups to permit inbound traffic.\
Be aware that **different ports use different protocols (TCP/UDP)** depending on their purpose.

<table><thead><tr><th width="80">Port</th><th width="104.5">Protocol</th><th width="275">Purpose</th><th>Notes</th></tr></thead><tbody><tr><td>7400</td><td>TCP</td><td>Peer-to-Peer (P2P) Communication</td><td>Data layer used for establishing a connection, exchanging blocks, and synchronizing blockchain data with other nodes.</td></tr><tr><td>7400</td><td>UDP</td><td>Node Discovery (Discv4/Discv5)</td><td>Discovery layer used to quickly find the addresses of other nodes on the network, including the bootnodes and all other peers.</td></tr><tr><td>8545</td><td>TCP</td><td>HTTP-RPC API</td><td>Used for external applications to query chain data and submit transactions over HTTP.</td></tr><tr><td>8546</td><td>TCP</td><td>WebSocket-RPC API</td><td>Used for real-time communication, allowing external applications to receive live updates and subscribe to blockchain events.<br></td></tr></tbody></table>

* **Static IP**: Required to ensure stable RPC access, enabling continuous health monitoring.

***

### Setting up Your Node Environment

COTI full nodes are run using docker. Docker provides a way for everyone to run battle-tested, reliable images, known to work with the network.

#### Prerequisites

<table data-header-hidden><thead><tr><th width="257.44818115234375"></th><th></th></tr></thead><tbody><tr><td><ol><li>Git</li></ol></td><td>See <a href="https://git-scm.com/book/en/v2/Getting-Started-Installing-Git"><strong>git-scm.com/book/en/v2/Getting-Started-Installing-Git</strong></a></td></tr><tr><td><ol start="2"><li>Docker</li></ol></td><td>See <a href="https://docs.docker.com/engine/install/ubuntu/#install-using-the-repository"><strong>docs.docker.com/engine/install/ubuntu</strong></a></td></tr><tr><td><ol start="3"><li>Docker Compose</li></ol></td><td>See <a href="https://docs.docker.com/compose/install/standalone/"><strong>docs.docker.com/compose/install/standalone</strong></a></td></tr></tbody></table>

Target the **Docker Engine** and **Compose** versions listed under [**Server requirements → Software stack**](/coti-documentation/node-ecosystem/server-requirements#software-stack) so your stack matches what COTI certifies.

### Installation Steps

{% hint style="info" %}
The following recommended steps reflect best practices but should be performed carefully, as they may significantly impact the operating system.
{% endhint %}

1. **Recommended steps:**
   1. Set host name (where `<name>`is your chosen node name)

      {% code fullWidth="false" %}

      ```bash
      sudo hostnamectl set-hostname <name>-full-node
      ```

      {% endcode %}
   2. Update package lists

      {% code fullWidth="false" %}

      ```bash
      sudo apt update
      sudo apt upgrade
      ```

      {% endcode %}
   3. Reboot system

      {% code fullWidth="false" %}

      ```bash
      sudo reboot
      ```

      {% endcode %}
   4. Update OS

      {% code fullWidth="false" %}

      ```bash
      sudo do-release-upgrade
      ```

      {% endcode %}
2. **Configure Docker**
   * Add your user to the `docker` group:

     {% code fullWidth="false" %}

     ```bash
     sudo usermod -aG docker $( whoami )
     ```

     {% endcode %}
   * Logout and re-login

     {% code fullWidth="false" %}

     ```bash
     logout
     ```

     {% endcode %}
3. **Clone the COTI Full Node project**

   {% code fullWidth="false" %}

   ```bash
   git clone https://github.com/coti-io/coti-full-node.git
   ```

   {% endcode %}
4. **Checkout the stable release tag (Recommended):**

   * Replace the tag below with the latest stable release for your network (see [Networks release notes](/coti-documentation/networks/release-notes) and tags in the [`coti-full-node`](https://github.com/coti-io/coti-full-node) repository).

   ```bash
   git checkout tags/<latest_stable_tag>
   # Example: git checkout tags/v1.2.0-mainnet
   ```
5. **Configure the environment**

   Copy [`.env.example`](https://github.com/coti-io/coti-full-node/blob/main/.env.example) to `.env` and set at least `NETWORK` (`testnet` or `mainnet`), `FULLNODE_FQDN`, and `FULLNODE_EXT_IP` if auto-detection is not suitable. Chain defaults (bootnodes, network id, FRPS regional hosts) load from **`networks/<NETWORK>.env`** when you run `start_coti-full-node.sh`; host values in `.env` override profile defaults.
6. **Start Your Node**
   1. Navigate to the newly created "coti-full-node" directory

      {% code fullWidth="false" %}

      ```bash
      cd coti-full-node
      ```

      {% endcode %}
   2. Execute node start script

      ```
      ./start_coti-full-node.sh
      ```

      Requires **Docker Compose v2** (`docker compose`). The script pulls the configured image, builds the local operator dashboard image, starts containers, and runs the liveness check.
   3. Once the stack has started, the liveness check runs automatically (or run it again manually):

      ```
      ./liveness_coti-full-node.sh
      ```

      \
      Output example:

      ```
      Initial block number: 208539
      Check 1: Block number is 208540
      Block number has progressed. Node is syncing.
      ```

{% hint style="info" %}
If liveliness check passed locally it means that your node is syncing with the other nodes in the network.
{% endhint %}

### Operator status page

After the stack is up, a small **local** web dashboard helps you see whether the node is running, has peers, is syncing, and (when host Nginx or FRPC with its internal **`nginx-frpc-gateway`** is configured) whether DNS/HTTPS or the tunnel gateway look healthy.

* **Local:** <http://127.0.0.1:8090> on the host (localhost only; auto-refreshes about every 15 seconds).
* **Over SSH:** `ssh -L 8090:127.0.0.1:8090 user@your-node`, then open the same URL in your desktop browser.
* **Public HTTPS** (when `NGINX_ENABLED=true` or `FRPC_ENABLED=true`): `https://<your-fqdn>/operator/`.

This is separate from the **Nodes web app** per-operator dashboard at `/my-nodes` — see the [UI guide](/coti-documentation/node-ecosystem/ui-guide).

7. **To Check Node Logs**

   ```
   docker logs -f coti-<network>-full-node
   ```

### Restarting Your Node

To restart your node follow these steps:

1. Stop your node

   {% code fullWidth="false" %}

   ```bash
   ./stop_coti-full-node.sh
   ```

   {% endcode %}
2. Start your node

   {% code fullWidth="false" %}

   ```bash
   ./start_coti-full-node.sh
   ```

   {% endcode %}

### Node Configuration

Configuration is split between **`.env`** (this host) and **`networks/<network>.env`** (chain profile). Start/stop scripts load `.env`, then the network profile, then `.env` again so host values win on overlap. See [Installation → Configuration files](/coti-documentation/node-ecosystem/installation#configuration-files-after-install) for the full table.

If you are running a node **without** joining the Node Ecosystem rewards program, no further configuration is required beyond choosing the correct `NETWORK` and ensuring peers can reach you on **7400**. Simply ensure you are connected to the network.

For **your own domain** with **HTTPS on the host** (what the wizard does with **`--with-nginx`**), set at least:

```bash
FULLNODE_FQDN=node1.example.com
NGINX_ENABLED=true
FRPC_ENABLED=false
```

in **`.env`** (see [`.env.example`](https://github.com/coti-io/coti-full-node/blob/main/.env.example)). Do **not** enable host Nginx and **FRPC** on the same install — the automated installer rejects that combination. For a **COTI-managed tunnel** instead, see [**Wizard tunnel**](/coti-documentation/node-ecosystem/installation/installation-wizard-tunnel) (`--with-frp`); skip this section.

### Nginx and Let's Encrypt (HTTPS on your domain)

Use this when the ecosystem must reach your node at **`https://<your-fqdn>/rpc`** (own DNS + TLS on your server). The wizard path is documented in [**Own domain (Nginx + TLS)**](/coti-documentation/node-ecosystem/installation/installation-own-domain); here you perform the same steps by hand after cloning the repo.

**Authoritative command-line reference:** [`install_coti-full-node.sh`](https://github.com/coti-io/coti-full-node/blob/main/install_coti-full-node.sh) — search for **`SSL AND NGINX SETUP`** (around [lines 601–704](https://github.com/coti-io/coti-full-node/blob/main/install_coti-full-node.sh#L601-L704)). The examples below mirror that script; substitute your **FQDN**. Nginx upstreams use the Compose **service** name `coti-full-node` (container name is `coti-<network>-full-node`).

#### Prerequisites

1. **DNS** — an **A record** for your FQDN pointing at this server’s public IP, propagated **before** you request a certificate (see [**Own domain**](/coti-documentation/node-ecosystem/installation/installation-own-domain)).
2. **Ports** — **80** and **443** free on the host and allowed through firewall / cloud security groups (in addition to **7400** from [Network configuration](#network-configuration)).
3. **Certbot on the host** — the installer installs it with `apt` when Nginx is enabled:

   ```bash
   sudo apt update
   sudo apt install -y certbot
   ```
4. **`.env`** — `FULLNODE_FQDN`, `NGINX_ENABLED=true`, `FRPC_ENABLED=false` (see [Node configuration](#node-configuration) above).

Run these steps from the **`coti-full-node`** clone root. Prefer doing them **before** your first `./start_coti-full-node.sh` with `NGINX_ENABLED=true`, or stop the stack (`./stop_coti-full-node.sh`), configure TLS, then start again.

#### Step 1 — Prepare Nginx directories

```bash
mkdir -p ./nginx/certbot ./nginx/sites-enabled
```

The repo already ships `nginx/nginx.conf`, `nginx/default.conf`, and `nginx/nginx-init_default.conf` for the ACME-only container.

#### Step 2 — Temporary Nginx for the ACME HTTP-01 challenge

Start the **`setup`** profile container (listens on host port **80** only):

```bash
docker compose --profile setup up -d nginx-init
```

This matches the installer’s `$DC --profile setup up -d nginx-init` (`$DC` is `docker compose`).

#### Step 3 — Obtain a certificate with Certbot (webroot)

Production certificate:

```bash
certbot certonly --webroot \
  -w "$(pwd)/nginx/certbot" \
  -d "$FULLNODE_FQDN" \
  --register-unsafely-without-email \
  --agree-tos \
  --non-interactive
```

**Dry run** (Let's Encrypt staging — browsers will not trust the cert; same as installer flag **`--staging`**):

```bash
certbot certonly --webroot \
  -w "$(pwd)/nginx/certbot" \
  -d "$FULLNODE_FQDN" \
  --staging \
  --register-unsafely-without-email \
  --agree-tos \
  --non-interactive
```

Certificates are written under **`/etc/letsencrypt/live/<fqdn>/`** on the host. The main Nginx container mounts **`/etc/letsencrypt`** read-only (see `docker-compose.yml`).

#### Step 4 — Stop the temporary Nginx

```bash
docker compose --profile setup down
```

Port **80** must be free for the production Nginx container.

#### Step 5 — Write the TLS reverse-proxy config

Create **`./nginx/sites-enabled/fullnode.conf`** with the same structure the installer writes: HTTPS on **443** proxying **`/rpc`**, **`/ws`**, **`/metrics`**, and **`/operator/`** to the Docker services, plus HTTP on **80** for `/.well-known/acme-challenge/` and redirect to HTTPS.

Copy the `server { ... }` blocks from [`install_coti-full-node.sh` (lines 626–704)](https://github.com/coti-io/coti-full-node/blob/main/install_coti-full-node.sh#L626-L704), replacing:

* `$FQDN` with your hostname (e.g. `node1.example.com`);
* upstream blocks already reference the Compose service `coti-full-node` and `coti-operator-dashboard` — do not substitute the container name prefix.

Public RPC for monitoring and rewards: **`https://<your-fqdn>/rpc`**.

#### Step 6 — Start the stack with Nginx enabled

Ensure **`NGINX_ENABLED=true`** in **`.env`**, then:

```bash
./start_coti-full-node.sh
```

`start_coti-full-node.sh` adds **`--profile proxy-nginx`** when `NGINX_ENABLED` is true, which starts the **`nginx`** service on ports **80** and **443**.

#### Renewal

Schedule renewal on the host (example monthly cron). Use the same webroot path as issuance:

```bash
certbot renew --webroot -w "$(pwd)/nginx/certbot"
docker compose exec nginx nginx -s reload
```

Adjust the reload command if your Nginx container name differs.

#### Troubleshooting (Nginx / TLS)

* **Certbot failed** — confirm `dig <fqdn>` resolves to this server; wait for DNS; ensure port **80** is reachable from the internet while `nginx-init` is up.
* **Port in use** — stop other services on **80** / **443** (including a leftover `nginx-init` after a failed run).
* **502 / bad gateway** — upstream names in `fullnode.conf` must match running compose service names; ensure the full node container is healthy (`docker ps`, `docker logs`).

If you prefer not to maintain this by hand, use the [**own-domain wizard one-liner**](/coti-documentation/node-ecosystem/installation/installation-own-domain) or run the installer script from the repo with **`--with-nginx`** after reviewing [`install_coti-full-node.sh`](https://github.com/coti-io/coti-full-node/blob/main/install_coti-full-node.sh).

### Verifying Node Functionality

* Metrics: Visit [**uptime.coti.io**](https://uptime.coti.io) to track performance and status.

{% hint style="info" %}
Public status pages are available for both networks — use the URLs listed in [Networks](/coti-documentation/node-ecosystem#networks).
{% endhint %}

* Node availability is crucial for the smooth operation of the network.\
  \
  To evaluate node availability, COTI leverages a monitoring platform that publishes this data. A node is considered available if it successfully responds to the `eth_blockNumber` request. Using this request ensures the node is actively synchronized with the network and functioning correctly.

### Incentives

Validation rewards are governed by the [**Node Ecosystem**](/coti-documentation/node-ecosystem) program (uptime, FQDN reachability, token holdings, epoch cadence, and other thresholds). Exact requirements can change — use the ecosystem documentation, web app, and the **Node Economy** section of the [litepaper](/coti-documentation/node-ecosystem/coti-node-ecosystem-litepaper) as the source of truth.

Licensed full nodes have commonly been expected to sustain **high uptime** (for example **≥ 98%** over an epoch of roughly **103 hours**) to remain eligible for validation rewards; confirm the current bar in the Node Ecosystem pages.

Following **this manual guide** instead of the ecosystem installer **does not change** those rules: you can still earn rewards when your deployment meets the **same** eligibility conditions the ecosystem enforces.

### Maintenance & Monitoring

1. **Regular Updates**: Keep your node software updated to the latest version. This ensures you receive security patches and new features.
2. **Resource Usage**: Monitor CPU, RAM, and disk space to ensure uninterrupted operation.
3. **Uptime**: Use a process manager (like `systemd`) or Docker auto-restart policies to keep your node running if it crashes unexpectedly.

### Troubleshooting

#### Common Issues:

* Peer Connection Errors: Ensure your ports are open and your firewall allows inbound connections.
* High Resource Usage: Upgrade your hardware or adjust configuration settings to reduce overhead.

Where to Get Help:

* [**COTI Discord**](https://discord.coti.io/)
* [**COTI Support**](mailto:support@coti.io)

### FAQ

1. How many nodes can I run?
   1. There’s no set limit, but each node requires its own resources. Running multiple nodes can help decentralize the network but comes with higher operational costs.
2. Can I run a node on a VPS or cloud platform?
   1. Absolutely. Just ensure the service meets the hardware, OS, and networking requirements.
3. Do I earn more rewards by running a more powerful node?
   1. No. Reward eligibility depends on **uptime** and **token holdings** (see [**Eligibility checks**](/coti-documentation/node-ecosystem/features#4-eligibility-checks)), not on hardware beyond what is needed to stay online and synced.
4. Is it mandatory to purchase anything to run a node?
   1. No. Anyone can run a COTI full node. To **earn rewards**, you must meet the Node Ecosystem eligibility rules (reachable FQDN, uptime, holdings, warm-up / hot state, etc.) — see [**Features**](/coti-documentation/node-ecosystem/features) and the [**Glossary**](/coti-documentation/node-ecosystem/ui-guide/glossary).

### Next Steps

Congratulations on setting up your COTI node using the **manual** path. Reward eligibility still follows the **Node Ecosystem** rules linked below.

The following related sections may be helpful:

* [COTI Node Ecosystem Litepaper](/coti-documentation/node-ecosystem/coti-node-ecosystem-litepaper)
* [**Node Ecosystem overview**](/coti-documentation/node-ecosystem) — eligibility, thresholds, and the managed experience at [testnet.nodes.coti.io](https://testnet.nodes.coti.io) / [nodes.coti.io](https://nodes.coti.io), including the guided [installer](/coti-documentation/node-ecosystem/installation), [UI walkthrough](/coti-documentation/node-ecosystem/ui-guide), and [glossary](/coti-documentation/node-ecosystem/ui-guide/glossary).

{% hint style="warning" %}
**Rewards require a valid FQDN and reachable JSON-RPC.** The Node Ecosystem measures uptime by calling your node’s JSON-RPC through the domain you register. A node that syncs locally but is **not** publicly reachable on a valid FQDN will **not** accrue credited uptime and will **not** be eligible for rewards — whether you installed via this manual guide or via the web app wizard. See [Installation](/coti-documentation/node-ecosystem/installation).
{% endhint %}


# COTI Node Ecosystem Litepaper

{% embed url="<https://coti.io/files/coti_nodes_litepaper.pdf>" %}
COTI Node Ecosystem Litepaper
{% endembed %}


# Backend Services

The ecosystem is powered by five cooperating backend services. Operators do not interact with them directly — the web app (see [Networks](/coti-documentation/node-ecosystem#networks) for URLs) is the only interface. This page describes each service from the operator's perspective: **what it does for you** and **what its outputs look like in the UI**.

```mermaid
flowchart LR
    YourNode["Your full node<br/>(RPC over FQDN)"]
    PDS["Peer Discovery"]
    NFT["NFT Service"]
    BSI["Better Stack integration"]
    NHM["Node Health Monitor"]
    NRS["Node Rewards"]
    Chain[(COTI network)]
    BetterStack[(Better Stack)]

    YourNode -->|"admin_peers"| PDS
    PDS -->|"node becomes hot"| NFT
    NFT -->|"mint Soulbound NFT"| Chain
    NFT -->|"hand off monitoring"| BSI
    BSI -->|"register FQDN"| BetterStack
    BetterStack -->|"GET /monitor"| NHM
    NHM -->|"eth_blockNumber"| YourNode
    NRS -->|"record epoch rewards"| Chain
    BetterStack --> NRS
```

## Peer Discovery Service

**What it does for you**

Peer Discovery is the service that first *sees* your node. It repeatedly calls `admin_peers` on a set of reference nodes and records which peers are currently connected. As soon as your node shows up in those responses, it is considered **online** in the ecosystem.

Peer Discovery is also responsible for the **thermal state machine**:

* While your node is newly online, it is **cold** with the NFT not yet minted — it is **warming up**.
* Once it has been continuously present for enough time (`HOT_THRESHOLD_HOURS` out of a rolling `HOT_WINDOW_HOURS` window), it transitions to **hot** and triggers NFT minting.
* If a hot node goes offline and has **zero peer presence** for the entire rolling `COLD_WINDOW_HOURS` window (defaults to **103 hours**), it **cools down** back to cold and must warm up again.

**Where it shows up in the UI**

* The home-page **Live node heartbeats** counter.
* The **Nodes** table's **Status** column (active / syncing / offline).
* The **Warmup In Progress** card on `/my-nodes` during the warm-up period.
* The **Thermal** state badge (warming up / hot / cooling down / cold) on internal views.

## NFT Service

**What it does for you**

The NFT service receives the "this node is hot" event from Peer Discovery and mints a **Soulbound Node NFT** to the operator's wallet. The NFT is the on-chain proof that the node exists, who owns it, and what its name and image are.

Because the NFT is soulbound, it cannot be transferred — it is bound to the wallet that set up the node. If an operator regenerates keys, a new NFT is minted to the new wallet; the old one is not reused.

**Where it shows up in the UI**

* The node name and avatar shown everywhere the node appears (dashboard, nodes table, node-details modal).
* The **Edit Node** flow (`/edit-node`) where you update **NFT metadata** — node name, image URI, and optional more-info URL — all stored on-chain on the Soulbound NFT.
* The "No Node Detected" / "Warmup Complete" states on `/my-nodes`, which depend on whether the NFT exists for the connected wallet.

## Better Stack Integration Service

**What it does for you**

Once your node has an NFT, the Better Stack integration service registers an uptime monitor for your node. Better Stack polls the **Node Health Monitor** (not your node directly); the monitor uses your node's `https://<your-fqdn>/rpc` URL as the check target.

The operator does not configure or pay for Better Stack — the ecosystem manages the monitor centrally.

**Where it shows up in the UI**

* The **all-time uptime percentage** displayed for your node in the dashboard and nodes table.
* A public **status page** that aggregates every hot node's monitor state (up / down). The URL is listed in [Networks](/coti-documentation/node-ecosystem#networks).

{% hint style="info" %}
Because monitoring happens over HTTPS against your public RPC hostname, a node without valid DNS / routing cannot be monitored — see [**Installation**](/coti-documentation/node-ecosystem/installation) ([**Own domain**](/coti-documentation/node-ecosystem/installation/installation-own-domain), [**Wizard tunnel**](/coti-documentation/node-ecosystem/installation/installation-wizard-tunnel)) and the [Glossary](/coti-documentation/node-ecosystem/ui-guide/glossary) FQDN entry.
{% endhint %}

## Node Health Monitor

**What it does for you**

The Node Health Monitor is the health-check proxy that Better Stack calls. When Better Stack asks "is this node healthy?", the Node Health Monitor calls the node's JSON-RPC **`eth_blockNumber`** twice (with a short wait between calls) through its FQDN and returns **healthy** only if block height increases — simply answering RPC is not enough.

**Where it shows up in the UI**

* As the underlying cause of your **uptime percentage** each epoch.
* As the difference between "your node is reachable" and "your node is actually operating" — a reachable-but-unhealthy node does not count as up.

## Node Rewards Service

**What it does for you**

At the end of each **103-hour epoch**, the rewards service:

1. Snapshots each node operator's USDC and COTI holdings.
2. Reads each node's uptime for the epoch from the monitoring platform.
3. Applies the eligibility rules: **uptime is mandatory** on every path, and the operator must satisfy **Path 1** (USDC and COTI each ≥ combo thresholds, plus uptime) **or** **Path 2** (COTI ≥ solo threshold, plus uptime), with whitelist overrides for specific operators.
4. Allocates each eligible node its share of the epoch's reward pool in the on-chain **rewards smart contract**.
5. Records the per-epoch result: earned amount, snapshot values, uptime %, eligibility.

Rewards are **not** pushed to the operator's wallet automatically. Once the contract is credited, the operator claims the accrued balance via the **Claim Now** button on the **My Node** dashboard, or by calling the rewards smart contract directly from any wallet they control.

**Where it shows up in the UI**

* The home-page **Reward-eligible nodes (last epoch)** and **COTI dropped (last epoch)** cards.
* The **Total COTI earned** card (cumulative).
* The **Total Rewards** column in the nodes table.
* The **Total Earned** and **Claim Now** controls in the My Node node-identity card.
* The **Rewards History** table on `/my-nodes`, including USDC, COTI, earned, uptime %, and Eligible / Ineligible per epoch.

## How they work together

For a newly-installed node, the lifecycle is:

1. **Install finishes** → node comes up at `https://<fqdn>/rpc`.
2. **Peer Discovery** sees the node in `admin_peers` responses and starts tracking presence.
3. **Warm-up period** — the UI shows a progress bar until `HOT_THRESHOLD_HOURS` of presence is reached.
4. **Peer Discovery** declares the node **hot** → **NFT Service** mints the Soulbound NFT.
5. **Better Stack Integration** registers the node's FQDN with **Better Stack**.
6. **Better Stack** starts polling the **Node Health Monitor**, which verifies block progression on the node's RPC at `https://<fqdn>/rpc`.
7. At epoch boundaries, **Node Rewards** reads uptime + holdings and credits eligible nodes in the rewards smart contract; the operator claims from the **My Node** dashboard or directly from the contract.

The operator only ever touches the web app and their own server — the five services coordinate everything in between.


# COTI Bridge

The COTI bridge allows users to bridge funds to and from other networks and the COTI V2 network. The following assets and networks are supported:

| Network          | Supported Toke                                        |
| ---------------- | ----------------------------------------------------- |
| COTI Mainnet     | <ul><li>COTI (native)</li><li>gCOTI (ERC20)</li></ul> |
| COTI Testnet     | <ul><li>COTI (native)</li><li>gCOTI (ERC20)</li></ul> |
| Ethereum Mainnet | <ul><li>COTI (ERC20)</li><li>gCOTI (ERC20)</li></ul>  |
| Ethereum Sepolia | <ul><li>COTI (ERC20)</li><li>gCOTI (ERC20)</li></ul>  |

{% hint style="info" %}
**A Note on Processing Timelines**

Please note that bridging on Mainnet may be slower due to network congestion. Processing times between 2-4 minutes are normal during regular network load.
{% endhint %}

### Bridging tokens

{% hint style="info" %}
MetaMask is required to bridge tokens using the COTI bridge. If you don't have it installed yet see **I**[**nstalling MetaMask**](https://metamask.io/download)
{% endhint %}

1. Visit [**bridge.coti.io**](https://bridge.coti.io), click on the "**Connect Wallet**" button.\\

   <figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-06f6d0e9d97c4d73d1ddbbfb31e4c1425dd54061%2Fimage.png?alt=media" alt=""><figcaption><p>COTI Bridge Homepage</p></figcaption></figure>
2. Once MetaMask prompts you to connect, click the "**Connect**" button on MetaMask.

   <figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-12918fbf729051bfdf853c8e6c675bbd186ad81d%2Fimage.png?alt=media" alt=""><figcaption><p>COTI Bridge MetaMask Connection</p></figcaption></figure>
3. Once connected, set the following parameters for the desired transaction

   1. Source Network
   2. Destination Network
   3. Token
   4. Amount

   <figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-4fe0fbcd62f229b71e620523bf8ca789fbe7a149%2Fimage.png?alt=media" alt=""><figcaption><p>COTI Bridge Transaction Configuration</p></figcaption></figure>
4. Once desired parameters have been entered and verified, click the "**Bridge Tokens**" button. MetaMask will show prompt for the transfer request. Click "Confirm".\\

   <figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-f11bf916607a033f4532961426693374e86260dd%2Fimage.png?alt=media" alt=""><figcaption><p>COTI Bridge MetaMask Transfer Request</p></figcaption></figure>
5. Once the bridging process starts, you will notice the button will change state to "Bridging" and display a spinner. Once the process is complete, you will see a confirmation on the lower right hand side of the screen\\

   <figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-a804deb7f5390a8bece23a7ce578cb2fa5462734%2Fimage.png?alt=media" alt=""><figcaption><p>COTI Bridge Transaction Confirmation</p></figcaption></figure>


# Swap COTI V1 Funds to COTI V2

With launch of the COTI V2 network, users who hold legacy COTI tokens in their VIPER wallets are able to upgrade these tokens to native COTI V2 tokens.

{% hint style="info" %}

* MetaMask is required to upgrade funds to COTI V2. See [**Installing MetaMask**](https://metamask.io/download).
* COTI will cover all network fees when upgrading tokens from VIPER to COTI V2
  {% endhint %}

Follow these simple steps:

1. **Log into Your VIPER Wallet**
   * Go to [pay.coti.io](https://pay.coti.io).
   * Access your VIPER wallet using your existing credentials.\\

     <figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-f8b8c20c4a6c2bf4c4da7f0a16162077c0263a94%2Fimage.png?alt=media" alt=""><figcaption><p>pay.coti</p></figcaption></figure>
2. **Navigate to SEND/RECEIVE Section**
   * Once logged in, locate and click on the **SEND/RECEIVE** section.\\

     <figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-17f9cc9b2bd4b09c0036c971124ed34d24da1c8b%2Fimage.png?alt=media" alt=""><figcaption></figcaption></figure>
3. **Initiate Token Swap**
   * Click on the **Swap to COTI V2** button on the lower right hand side of the screen.\\

     <figure><img src="https://2557786554-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2FeC83qbrBhITO4kE7kTNB%2Fuploads%2Fgit-blob-7f4a5f0af6a941f733d800d2dfb9ef6843f69754%2Fimage.png?alt=media" alt=""><figcaption></figcaption></figure>
   * If the COTI network is not yet added to MetaMask, you will be prompted to add it first.
   * If the COTI network is already added, MetaMask will prompt you to connect to [**pay.coti.io**](https://pay.coti.io).
4. **Configure Swap Parameters and Execute Swap**\
   Once connected, you will notice the "TO" field will be automatically populated with your MetaMask address.
   * Specify the amount of VIPER COTI tokens you'd like to swap to V2 tokens on the FROM field.
   * Review your transaction details to ensure accuracy.
   * Click the SWAP button
5. **Enjoy Zero Fees**
   * Your tokens will automatically swap to COTI V2 tokens without any fees applied.
6. **Verify Your V2 Tokens**
   * After completing the swap, your new V2 token balance will be updated in your MetaMask wallet.


# COTI Privacy Portal

## Introduction

As the decentralized web evolves, the need for financial privacy has become increasingly important.

Many blockchain networks are fully transparent, meaning:

* transaction histories are public
* wallet balances are visible
* sensitive financial activity can be exposed

COTI V2 addresses this limitation by introducing an EVM-compatible Layer 2 network that enables computation on encrypted data, powered by advanced cryptography such as Garbled Circuits.

The Privacy Portal is the primary web interface for interacting with this system. It allows users to manage private assets, perform confidential transactions, and keep control over their financial data.

## 🧭 What is the Privacy Portal?

> **Native COTI Privacy Portal** — This section covers the **on-COTI** Privacy Portal and `PrivateERC20` flows (mint/burn/transfer entirely on COTI). It is **not** the **PoD cross-chain Privacy Portal** (factory + pTokens on Fuji/Sepolia that route private compute through the Inbox). For host-chain PoD portal addresses, see [Avalanche Fuji](/coti-documentation/privacy-on-demand/networks/fuji) and [Ethereum Sepolia](/coti-documentation/privacy-on-demand/networks/sepolia).

The Privacy Portal is COTI V2's dApp for interacting with privacy-enabled tokens. It lets users bridge supported ERC20 tokens into private tokens, view and use encrypted balances, and submit confidential transfers while balances and transfer amounts remain encrypted on-chain, with decryption handled locally through the COTI MetaMask Snap.

## ✨ What you can do with the Privacy Portal?

With the Privacy Portal, you can:

* **🪙 Mint Private Tokens**\
  Convert supported ERC20 tokens into Private Tokens, allowing you to use them within COTI's privacy layer.
* **🔐 Manage Private Balances**\
  View and manage your encrypted balances securely using the COTI MetaMask Snap.
* **🔄 Send Confidential Transactions**\
  Transfer assets between wallets while keeping transaction amounts private.
* **👁️ View Your Data Privately**\
  Decrypt and view your balances locally. Only you have access to your financial data.
* **🧾 Keep Balances Encrypted On-Chain**\
  All balances remain encrypted on the ledger and are only accessible to the authorized key holder.

## ⚙️ How does it work?

COTI enables computation on encrypted data, meaning your data remains private even while it is being used.

The Privacy Portal acts as a bridge between you and this system:

* You convert assets into Private Tokens
* Your balances are stored encrypted on-chain
* Transactions are executed without revealing sensitive data
* Your data is decrypted locally when you need to view it

This allows you to interact with blockchain-based assets while preserving strong privacy for balances and transaction amounts.

## 🛡️ Why COTI V2 for Privacy?

COTI V2 introduces a new approach to blockchain privacy by combining encryption, secure computation, and developer-friendly infrastructure.

### 🔒 Encrypted Token Balances

Unlike standard ERC20 tokens, where balances and transactions are public, COTI's `PrivateERC20` stores all balances and amounts as encrypted data (ciphertexts).

### 🧮 Secure Computation (Garbled Circuits and MPC)

COTI uses advanced cryptographic techniques such as Multi-Party Computation (MPC) and Garbled Circuits to process encrypted data.

Through the [`MpcCore`](/coti-documentation/how-coti-works/advanced-topics/precompiles) precompile (address `0x64`), the network can perform operations like:

* addition
* subtraction
* comparisons

All without revealing the underlying data to validators or external observers.

### 🔑 Local Decryption

Your private balances are decrypted locally in your browser, using a personal AES key managed by the [COTI Snap](/coti-documentation/build-on-coti/guides/setting-up-coti-snap-with-your-metamask-wallet). Decryption keys are managed locally by you.

### 🧩 EVM Compatibility

COTI V2 is fully EVM-compatible, allowing developers to build using familiar tools such as:

* Solidity
* Hardhat

Developers can extend the [`PrivateERC20`](/coti-documentation/coti-privacy-portal/developer-guide/privateerc20.sol) contract to create privacy-enabled tokens without learning a new stack.


# User guide

The Privacy Portal is COTI V2's user-facing `dApp` for interacting with tokens on COTI, letting you bridge between Public tokens and [PrivateERC20](/coti-documentation/coti-privacy-portal/developer-guide/privateerc20.sol) tokens.

It allows you to work with two types of tokens:

## Public Tokens

Public Tokens behave like standard blockchain tokens:

* balances are visible on-chain
* transactions are transparent

## Private Tokens

Private Tokens add an extra layer of privacy:

* balances are stored encrypted on-chain
* you decrypt and view them locally using your wallet and the COTI MetaMask Snap

This helps keep your financial data confidential.

## What you’ll learn

This guide will walk you through how to:

* connect your wallet to the Privacy Portal
* [onboard](/coti-documentation/build-on-coti/core-concepts/what-is-onboarding) so private features can unlock
* bridge public tokens into private tokens
* send and receive private transactions
* view your balances securely

## Why use the Privacy Portal?

The Privacy Portal lets you move your tokens into a private state, giving you more control over what information is visible on-chain while keeping sensitive data confidential.


# Prerequisites

Before interacting with Private Tokens, make sure your setup is complete.

1. **Install MetaMask**

   Install the [MetaMask](https://metamask.io/download) extension in your browser. This will serve as your wallet for interacting with the COTI network.
2. **Add the COTI Network**

   Once MetaMask is installed, add the COTI network you will use: [COTI Testnet](/coti-documentation/networks/testnet/adding-the-coti-testnet-to-metamask) or [COTI Mainnet](/coti-documentation/networks/mainnet/adding-the-coti-mainnet-to-metamask). If the network is not already configured, the Privacy Portal can prompt you to add it.
3. **Install the COTI Snap**

   Install the COTI Snap in MetaMask. The COTI Snap is essential for handling Private Tokens within MetaMask. Follow the [installation instructions](/coti-documentation/coti-privacy-portal/user-guide/metamask-snap-setup) to configure it properly. It enables encryption, decryption, and key management for private token operations.
4. **Fund your wallet**

   Make sure your wallet has enough native COTI to cover transaction fees.

## You're ready




---

[Next Page](/coti-documentation/llms-full.txt/1)

