# TACo | Cryptographic Public Good

End-to-end Encrypted. End-to-end Secured. End-to-end Decentralized.

**TACo** – **T**hreshold **A**ccess **Co**ntrol – is a free and open source public good; cryptographic infrastructure to undergird a *non-surveilling*, *non-rent-seeking*, *non-exploitative* internet.&#x20;

{% hint style="info" icon="hand-fist" %}
The TACo project is now under the stewardship of the [World Ethical Data Foundation](https://worldethicaldata.org/projects/taco). Although operators may choose to continue running TACo node clients in the interim, a stable version of the service will be relaunched in Q3 2026 – centered around a *Privacy Coalition.* If you represent an organization in the domains of privacy advocacy, anti-surveillance, human rights, encryption/whistleblower technology, or pertinent academic research, and would like to be involved, please get in [touch](https://discord.gg/Rh2728Hk).&#x20;
{% endhint %}

From an app developer's perspective, TACo is a straightforward and affordable *plugin*. Once integrated, TACo can enforce user-authored *policies* over who or what can **access** sensitive data – without single points of failure, data custodians, or centralized authorities.

This is possible thanks to the TACo *network*; an array of independently-controlled nodes each running the TACo client. Apps that have integrated the plugin connect to the network via the `taco-web` API. Groups of TACo nodes respond to user requests by collectively validating sharing or policies. Specifically, nodes individually verify the requestor's fulfillment of pre-specified conditions, before providing the necessary fragment of *decryption material*, enabling the requestor's *access*.&#x20;

Without relying on centralized servers, TACo empowers the application *user* to truly control the conditions under which their data is accessible: by whom, for how long, and contingent on any digital source of truth.&#x20;

*For example: share my archive of evidence with journalists if I do not check-in over the next 7 days.*&#x20;

{% hint style="info" %}
There is also an experimental variant of TACo centered around conditions-based signing. For more information, see [**T**hreshold **A**ction **Co**ntrol](/reference/action-control).&#x20;
{% endhint %}

### **I'm interested in..**

<table data-view="cards" data-full-width="false"><thead><tr><th></th><th></th><th data-hidden data-card-target data-type="content-ref"></th><th data-hidden data-card-cover data-type="image">Cover image</th></tr></thead><tbody><tr><td><strong>Building with TACo</strong></td><td>Integrate TACo into your application and enable low-trust data sharing and automation workflows for your users.</td><td><a href="/pages/9b0qfE08atzo9AEq3888">/pages/9b0qfE08atzo9AEq3888</a></td><td><a href="/files/OBwZMs7bh2UvPRBHFYOD">/files/OBwZMs7bh2UvPRBHFYOD</a></td></tr><tr><td><strong>Evaluating TACo's utility</strong></td><td>Learn about TACo's unique capabilities, including expressive condition logic,  infrastructural flexibility, and low-trust model.</td><td><a href="/pages/bHOw2HttmdLB3jHwP0hg">/pages/bHOw2HttmdLB3jHwP0hg</a></td><td><a href="/files/3REDIHtEpjlgQci6RBbC">/files/3REDIHtEpjlgQci6RBbC</a></td></tr><tr><td><strong>Running a TACo node</strong></td><td>Support TACo's adopting applications by spinning up  a low-memory, low-cost, low-maintenance node client.</td><td><a href="/pages/Hmkn4V4z8VOUUbWRkdz9">/pages/Hmkn4V4z8VOUUbWRkdz9</a></td><td><a href="/files/aeCCJeTuXA4srBcGKNFR">/files/aeCCJeTuXA4srBcGKNFR</a></td></tr></tbody></table>

### **Get Started Now**

* [**Quickstart Guide →**](/for-developers/quickstart-testnet/get-started-with-tac)\
  Your first encrypted data sharing / automated signing flow - up and running in minutes.
* [**Join the Community on Discord →**](http://discord.gg/buildwithtaco)\
  Connect with developers, node operators, and the TACo team.
* [**View TACo on GitHub →**](https://github.com/nucypher/taco-web)\
  Explore the codebase, contribute, or file issues.


# How Threshold Access Control Works

Conditions-based threshold decryption

In a typical flow, private data is encrypted client-side by a *data producer.* This data payload will remain encrypted until it reaches the device of a qualifying *data consumer*. From a privacy point of view, this is equivalent to the end-to-end encryption guarantees in a messaging app like Signal. However, TACo is general-purpose and can protect data flows in all types of applications, rather than being optimized for point-to-point textual communication.

Whether or not a *data consumer* qualifies to decrypt and view a given data payload depends on whether they fulfill certain access conditions. These conditions are specified in advance by the producer or owner of that data, or programmed into the application logic on their behalf. For example, a journalist-facing app might predicate access to submitted evidence based on the proven location of the witness (out of harm's way).\
\
To access the data, a given *data consumer* will have to (1) authenticate themselves and (2) present proof they fulfill the pre-specified conditions. Both are evaluated by a group of TACo nodes, each of which individually validates the data consumer's request by comparing it to retrieved web/web3 state. For example, if perishable health data should not be shared after a certain date, TACo nodes will simply read the UNIX epoch via Ethereum's `block.timestamp` value.

If a sufficient number (a 'threshold') of nodes confirm that the requesting *data consumer* qualifies to see the data, each will send a decryption fragment to the consumer. These fragments can be combined by the data consumer client-side to access the private data. This private data could be the actual content or, more commonly, a symmetric key that provides a lightweight method to access an underlying payload.

## Key concepts

### **Threshold Decryption**

Under the hood, TACo involves splitting a joint secret – a decryption key – into multiples *shares* and distributing those among authorized and collateralized node operators (stakers in the Threshold network). The secret has an associated public key that can be used for encryptions. A minimum number – a *threshold* – of those nodes holding the key shares must be online and actively participate in partial decryptions. These are subsequently combined on the requester's client to reconstruct the original plaintext data that was encrypted using the associated public key.

### **Conditionality**

Conditions are 'attached' on a per-ciphertext basis. In other words, each and every payload, message or bit can be access-restricted by a unique set of specified conditions. A range of access control condition types can be defined by the *adopting developer* and/or *data producer.* For more on condition logic, check out the [Programmable Conditions](/for-developers/conditions) section.

## End-to-end data sharing flow

### Stage 0 | Distributed Key Generation | App Developer

<div data-full-width="false"><figure><picture><source srcset="/files/PLJMM4T7FELZZ4NFezRV" media="(prefers-color-scheme: dark)"><img src="/files/PDYZfujzAdOdvVOlNlFV" alt=""></picture><figcaption><p>Nodes sampled and DKG ritual initialized</p></figcaption></figure></div>

We start from the *adopting developer*'s perspective – i.e. the developers of an application that has integrated TACo.\
\
The first stage – a Distributed Key Generation initialization ritual – assigns the adopting developer the role of `cohortAuthority`. This grants the developer control over the group of nodes which enforce access control within their app, but no power to decrypt private data shared by their users. Note that `cohortAuthority` power is easily transferred to a multisig or DAO.

Firstly, the `cohortAuthority` samples a cohort of nodes from the network. Typically the list of nodes to populate a cohort is generated using a replicable random seed, to prove later that the nodes were not hand-picked. The minimum cohort size is 30, and can be as large as 100.

The sampled nodes will now conduct a DKG initialization ritual, which involves generating transcripts, aggregating transcripts locally, and cross-verifying the aggregates. If any of the nodes submits an incorrect entry, the DKG ritual fails and must start over. That implies that a minimum of one honest party is required at this stage to ensure the secret material is not spoofed.

DKG initializations generate private and public material. The public material combines into a persistent public key, used in the next step by the encryption function. Each node holds onto their fragment of private material, which they will later individually provision to qualifying data consumers.

### **Stage 1 | Encryption & Condition Specification | Data Producer**

<figure><picture><source srcset="/files/FBjVGDm5wundNHtoWbma" media="(prefers-color-scheme: dark)"><img src="/files/BZds4Y3by9Uq72jVUPK8" alt=""></picture><figcaption><p>Plaintext data encrypted and access conditions specified</p></figcaption></figure>

We now switch to the *data producer*'s perspective – we'd like to encrypt and share some private data. We first specify the conditions for accessing the data.

For example, imagine the data producer is a creator on a decentralized Twitch, and wishes to create a paywall for a special livestream. They will only allow a viewer to decrypt the stream if they (a) hold a minimum number of a special purpose NFT, (b) either purchased a previous NFT OR follow the creator on X/Twitter/Mastodon, and (c) the stream will be non-accessible to anyone after 24h.\
\
All these conditions are composed into a `conditionSet` , which can be constructed as a logical sequence – e.g. only check the X API if the prospective data consumer holds below some number of NFTs).\
\
The data producer encrypts the raw data using the Public Key to product a unique secret. This secret will be combined with the tamper-proof conditions to form a ciphertext/payload.

The payload is then transmitted to recipients via a transport layer and/or uploaded to a storage layer. Note that TACo is agnostic to the storage layer and TACo nodes do not store any data.

### Stage 2 | Authentication & Condition Verification | Data Consumer

<figure><picture><source srcset="/files/tgF6xw1wagiC9XbZadrA" media="(prefers-color-scheme: dark)"><img src="/files/t3n3szXxCrewicEUQTsC" alt=""></picture><figcaption><p>Requestor authenticated, access conditions verified, decryption material provisioned &#x26; data decrypted</p></figcaption></figure>

Finally, switching to the *data consumer*'s perspective. Our first step is to retrieve the encrypted payload.

Next, the data consumer presents the payload to the cohort, along with whatever authentication message or proof is required to prove their identity. For example, the message can be as simple as a Sign In With Ethereum 'pass through', where the app has already authenticated the user. The authentication method can also use OAuth, ERC-4337 smart contract wallets, or choose which auth to require at decryption time based on the conditions.\
\
Following authentication, each individual node verifies that the access conditions are fulfilled by the data consumer. In our decentralized Twitch streamer example, this would involve retrieving on-chain state to check transaction history, NFT ownership, and blocktime, and off-chain state to check the creator's X follower list.

Normally, all the nodes will agree. However, the threshold design means there’s no issue if one or two nodes are unreachable, and also prevents a malicious minority from sharing data with illegitimate data consumers.\
\
For each validating node, if the access conditions are met, a decryption fragment is then provided to the consumer. Once a threshold of nodes (e.g. 26 of 50) has provisioned their fragments, the data consumer can locally assemble these fragments to decrypt the payload.

Normally, the 'plaintext data' is a symmetric key that is then used to decrypt the underlying data, via TACo's KEM/DEM mechanism.  


# Programmable Conditions

This section focuses on `Condition` types, composition and usage.

{% hint style="success" %}
**Authoring conditions with an LLM?** Start with [Building Conditions with an LLM](/for-developers/conditions/building-with-llms). The four key references are:

* The [Cookbook](/for-developers/conditions/cookbook) — JSON examples for every condition type.
* The [Discord tipping bot deep-dive](/for-developers/conditions/discord-tipping-bot-deep-dive) — a fully annotated complex condition.
* The [Schema reference](https://github.com/nucypher/taco-web/blob/signing-epic/packages/taco/schema-docs/condition-schemas.md) — auto-generated source of truth (markdown). A [JSON Schema version](https://raw.githubusercontent.com/nucypher/taco-web/signing-epic/packages/taco/schema-docs/condition-schema.json) is available for editors and structured-output LLMs.
* The [Validator script](/for-developers/conditions/validating-conditions) — catch shape errors locally before hitting the network.

When something breaks, see [Troubleshooting](/for-developers/conditions/troubleshooting) and the [Context Variables cheatsheet](/for-developers/conditions/context-variables).
{% endhint %}

## Base Conditions

Base conditions define specific criteria, and each includes a `returnValueTest` to compare the actual execution result with the expected value. These include:

* [`TimeCondition`](/for-developers/conditions/timecondition) – time-based conditions using block height and other blockchain-based timestamps.\
  \&#xNAN;*Example:* only allow access after a certain timestamp.
* [`RpcCondition`](/for-developers/conditions/rpccondition) – based on RPC calls as defined in Ethereum's Official [API](https://ethereum.org/en/developers/docs/apis/json-rpc/#json-rpc-methods).\
  \&#xNAN;*Example:* allow access if the requestor address holds a minimum ETH balance.
* [`ContractCondition`](/for-developers/conditions/contractcondition) – uses on-chain state, allowing arbitrary contract function calls.\
  \&#xNAN;*Example:* allow access if this requestor holds a special-purpose NFT.
* [JSON Endpoint Conditions](/for-developers/conditions/json-endpoint-conditions) - uses state from a JSON HTTPS endpoint.\
  \&#xNAN;*Example:* allow discount on event tickets/merchandise if there is "bad" weather according to a specific weather API.
* [`JWTCondition`](/for-developers/conditions/jwtcondition) - validates [JSON Web Tokens (JWTs)](https://datatracker.ietf.org/doc/html/rfc7519) against a specified public key.
* [Signing Object Conditions](/for-developers/conditions/signing-object-conditions) - validates objects submitted for threshold signing meet specified criteria by inspecting the values of designated fields (eg. `UserOperation` for ERC-4337)

Each base condition defines a [`returnValueTest`](#return-value-test) used to compare the obtained execution value with the expected value for the condition.

### returnValueTest

A `returnValueTest` is a mechanism used by a condition to evaluate whether a specific execution result meets a specified criterion. It allows dynamic comparisons between the actual returned value and the expected value.

It consists of three key components:

* `comparator`: defines the comparison operation to apply between the actual value obtained and the expected value. The available operators include:
  * `==`: equal to
  * `!=`: not equal to
  * `>`: greater than
  * `<`: less than
  * `>=`: greater than or equal to
  * `<=`: less than or equal to
  * `in`: value in array of values
  * `!in`: value not in array of values
* `value`: the expected value to compare against the actual returned value.
* `index` *(optional)*: indicates the position of the value to use for comparison within a list or array when multiple values are returned. If the response includes several values, this index determines which entry to evaluate. If the index is not specified, the entire response is used. For instance, if the array `["apple", "banana", "grape"]` is returned during execution, an index of `1` would select `"banana"` as the value for comparison.

### Context Variables

[Context variables](/for-developers/authentication/conditioncontext-and-context-variables) provide the ability for placeholder values to be defined within conditions at creation time, and be dynamically populated at verification time e.g. current user wallet address.

## Logical Conditions

Logical conditions use control structures to determine overall condition outcomes based on the results of underlying conditions. These include:

* [`CompoundCondition`](/for-developers/conditions/logical-conditions/condition-set) - allows conditions to be combined using logical operators such as `or`, `and` & `not` .
* [`SequentialCondition`](/for-developers/conditions/logical-conditions/sequentialcondition) - chains conditions to be executed in a specific order, where the outcome of one condition can be used by subsequent conditions.
* [`IfThenElseCondition`](/for-developers/conditions/logical-conditions/ifthenelsecondition) - implements branching logic for conditions where the flow follows an if-then-else structure i.e. **IF** `CONDITION_A` **THEN** `CONDITION_B` **ELSE** `CONDITION_C`

{% hint style="info" %}
Since condition evaluations may require making remote calls (e.g. RPC calls, etc.), the number of conditions allowed within a `Logical Condition` is limited.

A single `Logical Condition`can contain a maximum of five conditions. Additionally, the nesting of a `Logical Condition` within a `Logical Condition` is allowed, but the maximum nesting depth is restricted to two levels. Therefore, a `Logical Condition` can contain a sub-`Logical Condition` but that sub-`Logical Condition` cannot subsequently contain a `Logical Condition`.
{% endhint %}


# Any (Major) EVM Chain Condition Support

As the adoption of TACo continues to grow, one challenge that arises is supporting an increasing number of blockchains. Currently, nodes are required to configure a dedicated RPC URL for each blockchain supported for conditions. For example, regardless of the TACo domain (`mainnet`or testnet), nodes must configure an RPC URL for Ethereum (Mainnet/Sepolia). and Polygon (Mainnet/Amoy). Those chains would represent the available chains that can be used for on-chain conditions.

While this configuration ensures reliability, it becomes cumbersome as more blockchains are added to TACo's ecosystem for conditions. Each new blockchain requires nodes to manually set up and manage its RPC URL, increasing the administrative burden over time.

Currently, TACo nodes employ a fallback mechanism to enhance reliability by utilizing a list of public RPC URLs. In this setup, the configured RPC URL is given priority, but if it becomes temporarily unavailable, the node automatically switches to one of the fallback public RPC URLs. For instance, on mainnet, these backup URLs are drawn from the [`mainnet.json` file in the `nucypher/chainlist` repository](https://github.com/nucypher/chainlist/blob/main/mainnet.json). The list of supported chains on `lynx` is available in the [`lynx.json`file in the `nucypher/chainlist`repository](https://github.com/nucypher/chainlist/blob/main/lynx.json).

Rather than requiring nodes to configure specific RPC URLs for each supported blockchain for conditions, we are experimenting with maintaining a curated list of public RPC endpoints for most EVM-compatible chains. This approach allows nodes to utilize these pre-defined public RPC URLs, removing the need for dedicated configurations for every blockchain. Since most chains offer multiple public RPC URLs, the list provides built-in redundancy, ensuring reliability during condition verification.

&#x20;This comprehensive list enables rapid prototyping for dApps across multiple blockchains addressing a growing demand from developers.


# Building Conditions with an LLM

TACo conditions are JSON. That makes them an excellent target for LLM-assisted authoring: describe the access policy you want in plain English, hand the LLM the right context, and iterate until the validator is happy.

This page is the recommended workflow.

## The context you give the LLM

Paste these four things into your LLM of choice (Claude, ChatGPT, Cursor, etc.) at the start of a new conversation:

1. **The full condition schema** — the canonical, machine-readable definition of every condition type, every field, every allowed value. Two formats are auto-generated from the same TypeScript source:

   * [`condition-schemas.md`](https://github.com/nucypher/taco-web/blob/signing-epic/packages/taco/schema-docs/condition-schemas.md) — human-readable reference for prose-style LLMs.
   * [`condition-schema.json`](https://raw.githubusercontent.com/nucypher/taco-web/signing-epic/packages/taco/schema-docs/condition-schema.json) — standard [JSON Schema](https://json-schema.org/) document. Use this with structured-output LLM APIs and editor `$schema` references (see [JSON Schema integration](#json-schema-integration) below).

   Link these directly — do not vendor copies that will drift.
2. **The cookbook** — [JSON examples covering every condition type](/for-developers/conditions/cookbook). Examples teach an LLM patterns far faster than prose.
3. **The deep-dive** — the [Discord tipping bot walkthrough](/for-developers/conditions/discord-tipping-bot-deep-dive). One realistic, complex, fully-annotated condition is worth a thousand toy examples.
4. **The validator script** — [`validate-conditions.ts`](/for-developers/conditions/validating-conditions). Tell the LLM it can run this and iterate on the output.

## A prompt template

```
You are helping me author a TACo condition in JSON.

Reference material (please read before writing any condition):
- Schema (JSON Schema, machine-readable source of truth):
  https://raw.githubusercontent.com/nucypher/taco-web/signing-epic/packages/taco/schema-docs/condition-schema.json
- Schema (prose version, same source):
  https://github.com/nucypher/taco-web/blob/signing-epic/packages/taco/schema-docs/condition-schemas.md
- Cookbook of examples: https://docs.taco.build/for-developers/taco-sdk/references/conditions/cookbook
- Annotated complex example: https://docs.taco.build/for-developers/taco-sdk/references/conditions/discord-tipping-bot-deep-dive

Rules:
- Output a single JSON object (no prose around it) when I ask for a condition.
- Every field must exist in the schema. Do not invent fields.
- Context variables start with ":" and match /^:[a-zA-Z_][a-zA-Z0-9_]*$/.
- CompoundCondition: max 5 operands.
- MultiConditions (CompoundCondition, IfThenElseCondition, SequentialCondition)
  share a combined nesting depth limit of 4.
- SequentialCondition: 2–20 variables.
- After each condition you produce, I will run validate-conditions.ts and
  paste the output back. Fix any validation errors and try again.

What I want the condition to enforce:
<describe your access policy in plain English>
```

## The iteration loop

1. LLM produces a condition.
2. Save it as `conditions.json`.
3. Run `npx tsx validate-conditions.ts` ([source](/for-developers/conditions/validating-conditions)).
4. If invalid, paste the error back to the LLM. If valid, test it end-to-end against a local testnet (see [Quickstart](/for-developers/quickstart-testnet)) or your app.

This loop usually converges in 1–3 rounds even for complex conditions.

## Tips that materially improve LLM output

* **Be explicit about the chain ID.** "Base mainnet" is ambiguous to a model that has not read your config; say `"chain": 8453`.
* **Name the data source.** "Check an NFT balance" is vague. "Call `balanceOf(:userAddress)` on contract `0xabc...` on Polygon (137) and require result `> 0`" is unambiguous.
* **Specify the comparator.** Models default to `==` even when you mean `>=`.
* **For sequential conditions**, list variables in dependency order and remind the model that later variables can reference earlier ones with `:varName`.
* **For ABI validation**, give the model the function signature (e.g. `transfer(address,uint256)`) — it cannot guess parameter order reliably.
* **When using `signing-attribute` / `signing-abi-attribute`**, mention which signing object format you are using (UserOperation, plain transaction, custom struct) so the model picks the right `attributeName`.

## JSON Schema integration

The auto-generated [`condition-schema.json`](https://raw.githubusercontent.com/nucypher/taco-web/signing-epic/packages/taco/schema-docs/condition-schema.json) is the highest-leverage piece of tooling available for condition authoring. It works in three places without any TACo dependency:

### Editors

Add `$schema` to the top of any `conditions.json` and your editor (VS Code, Cursor, JetBrains) will validate it inline as you type, with autocomplete for every field:

```json
{
  "$schema": "https://raw.githubusercontent.com/nucypher/taco-web/signing-epic/packages/taco/schema-docs/condition-schema.json",
  "version": "1.0.0",
  "condition": {
    "conditionType": "time",
    "chain": 137,
    "method": "blocktime",
    "returnValueTest": { "comparator": ">", "value": 1735689600 }
  }
}
```

### LLM structured output

Both Anthropic and OpenAI's APIs accept a JSON Schema directly to constrain model output. Example with the Anthropic SDK:

```ts
import Anthropic from '@anthropic-ai/sdk';

const conditionSchema = await fetch(
  'https://raw.githubusercontent.com/nucypher/taco-web/signing-epic/packages/taco/schema-docs/condition-schema.json'
).then(r => r.json());

const result = await new Anthropic().messages.create({
  model: 'claude-opus-4-6',
  max_tokens: 2048,
  tools: [{
    name: 'emit_condition',
    description: 'Emit a TACo condition matching the requested policy.',
    input_schema: conditionSchema,
  }],
  tool_choice: { type: 'tool', name: 'emit_condition' },
  messages: [{
    role: 'user',
    content: 'Build a condition that allows decryption only if the requester holds at least one NFT from collection 0xabc on Ethereum mainnet.',
  }],
});
```

The model is now structurally constrained — it cannot return invalid shapes.

### Standalone validation (any language)

You no longer need `@nucypher/taco` installed to validate. Any standard JSON Schema validator works:

```bash
pnpm dlx ajv-cli validate \
  -s https://raw.githubusercontent.com/nucypher/taco-web/signing-epic/packages/taco/schema-docs/condition-schema.json \
  -d conditions.json --strict=false
```

The Python equivalent uses [`jsonschema`](https://python-jsonschema.readthedocs.io/), the Go equivalent uses [`gojsonschema`](https://github.com/xeipuuv/gojsonschema), etc.

The `validate-conditions.ts` script ([page](/for-developers/conditions/validating-conditions)) is still useful when you want runtime semantics (e.g. catching nesting-depth errors that JSON Schema cannot express), but for shape validation alone the JSON Schema is enough.


# Cookbook

A working JSON example for every condition type the TACo SDK supports, plus several real-world combinations. Copy, paste, modify.

All addresses below are **placeholders**. Substitute your own. All chain IDs are real (Ethereum=`1`, Polygon=`137`, Base=`8453`, Arbitrum=`42161`).

> Pair this page with the [schema reference](https://github.com/nucypher/taco-web/blob/signing-epic/packages/taco/schema-docs/condition-schemas.md) and the [validator script](/for-developers/conditions/validating-conditions). Together they are everything an LLM needs to author conditions for you.
>
> **Editor tip:** add `"$schema": "https://raw.githubusercontent.com/nucypher/taco-web/signing-epic/packages/taco/schema-docs/condition-schema.json"` to the top of any `conditions.json` and your editor will validate every example below in-place. See [Building Conditions with an LLM → JSON Schema integration](/for-developers/conditions/building-with-llms#json-schema-integration).

## Table of contents

**Single conditions**

1. [TimeCondition — gate after a timestamp](#1-timecondition--after-a-given-time)
2. [RpcCondition — minimum native ETH balance](#2-rpccondition--minimum-eth-balance)
3. [ContractCondition — ERC-20 minimum balance (standard)](#3-contractcondition--erc-20-balance-standard-shorthand)
4. [ContractCondition — ERC-721 ownership (standard)](#4-contractcondition--erc-721-ownership)
5. [ContractCondition — custom view function with `functionAbi`](#5-contractcondition--custom-view-function)
6. [ContractCondition — allowlist mapping lookup](#6-contractcondition--allowlist-mapping-lookup)
7. [JsonApiCondition — public weather API](#7-jsonapicondition--public-weather-api)
8. [JsonApiCondition — authenticated price feed](#8-jsonapicondition--authenticated-api-with-bearer-token)
9. [JsonRpcCondition — arbitrary JSON-RPC call](#9-jsonrpccondition--arbitrary-json-rpc)
10. [JsonCondition — querying a passed-in payload](#10-jsoncondition--querying-a-context-payload)
11. [JwtCondition — verify a signed JWT](#11-jwtcondition--verify-a-jwt)
12. [EcdsaCondition — verify an ECDSA signature](#12-ecdsacondition--verify-an-ecdsa-signature)
13. [SigningObjectAttributeCondition — UserOperation field check](#13-signingobjectattributecondition--useroperation-field-check) (TACo Action Control only)
14. [SigningObjectAbiAttributeCondition — UserOperation calldata check](#14-signingobjectabiattributecondition--validate-calldata) (TACo Action Control only)
15. [ContextVariableCondition — assert a custom variable](#15-contextvariablecondition--assert-a-custom-variable)

**Logical / composed conditions**

16. [CompoundCondition — AND, OR, NOT](#16-compoundcondition--and-or-not)
17. [SequentialCondition — derive a value, then check it](#17-sequentialcondition--derive-then-check)
18. [IfThenElseCondition — branch on a condition](#18-ifthenelsecondition--branching)
19. [Pattern: NFT-or-allowlist gate](#19-pattern-nft-or-allowlist-gate)
20. [Pattern: time-windowed paid access](#20-pattern-time-windowed-paid-access)
21. [Pattern: storm-condition open access](#21-pattern-storm-condition-open-access)
22. [Pattern: balance fetched, normalised, then asserted](#22-pattern-balance-fetched-normalised-then-asserted)

For a worked end-to-end example that combines many features at once, see the [Discord tipping bot deep-dive](/for-developers/conditions/discord-tipping-bot-deep-dive).

***

## 1. `TimeCondition` — after a given time

Decryption is allowed only after the latest block on Polygon has a timestamp greater than `1735689600` (2025-01-01 UTC).

```json
{
  "version": "1.0.0",
  "condition": {
    "conditionType": "time",
    "chain": 137,
    "method": "blocktime",
    "returnValueTest": {
      "comparator": ">",
      "value": 1735689600
    }
  }
}
```

## 2. `RpcCondition` — minimum ETH balance

Allow if the requester holds at least 0.1 ETH on Ethereum mainnet.

```json
{
  "version": "1.0.0",
  "condition": {
    "conditionType": "rpc",
    "chain": 1,
    "method": "eth_getBalance",
    "parameters": [":userAddress", "latest"],
    "returnValueTest": {
      "comparator": ">=",
      "value": 100000000000000000
    }
  }
}
```

`:userAddress` is automatically replaced with the requester's wallet address. The value `100000000000000000` is 0.1 ETH in wei.

## 3. `ContractCondition` — ERC-20 balance (standard shorthand)

Require at least 1000 USDC (6 decimals) on Base.

```json
{
  "version": "1.0.0",
  "condition": {
    "conditionType": "contract",
    "chain": 8453,
    "contractAddress": "0xUSDC_ADDRESS_ON_BASE",
    "standardContractType": "ERC20",
    "method": "balanceOf",
    "parameters": [":userAddress"],
    "returnValueTest": {
      "comparator": ">=",
      "value": 1000000000
    }
  }
}
```

When `standardContractType` is set to `"ERC20"` or `"ERC721"`, the SDK uses the canonical ABI — you do not need to supply `functionAbi`.

## 4. `ContractCondition` — ERC-721 ownership

Hold at least one NFT from the specified collection on Polygon.

```json
{
  "version": "1.0.0",
  "condition": {
    "conditionType": "contract",
    "chain": 137,
    "contractAddress": "0xNFT_COLLECTION_ADDRESS",
    "standardContractType": "ERC721",
    "method": "balanceOf",
    "parameters": [":userAddress"],
    "returnValueTest": {
      "comparator": ">",
      "value": 0
    }
  }
}
```

## 5. `ContractCondition` — custom view function

Call any `view` or `pure` function on any contract by supplying a `functionAbi`. Here we check that the user is a member of a DAO.

```json
{
  "version": "1.0.0",
  "condition": {
    "conditionType": "contract",
    "chain": 1,
    "contractAddress": "0xDAO_MEMBERSHIP_CONTRACT",
    "method": "isMember",
    "parameters": [":userAddress"],
    "functionAbi": {
      "name": "isMember",
      "type": "function",
      "stateMutability": "view",
      "inputs": [
        { "name": "account", "type": "address", "internalType": "address" }
      ],
      "outputs": [
        { "name": "", "type": "bool", "internalType": "bool" }
      ]
    },
    "returnValueTest": {
      "comparator": "==",
      "value": true
    }
  }
}
```

## 6. `ContractCondition` — allowlist mapping lookup

A contract exposes `mapping(address => uint8) public tier;`. Allow only users in tier 2 or higher.

```json
{
  "version": "1.0.0",
  "condition": {
    "conditionType": "contract",
    "chain": 8453,
    "contractAddress": "0xTIER_REGISTRY",
    "method": "tier",
    "parameters": [":userAddress"],
    "functionAbi": {
      "name": "tier",
      "type": "function",
      "stateMutability": "view",
      "inputs": [
        { "name": "", "type": "address", "internalType": "address" }
      ],
      "outputs": [
        { "name": "", "type": "uint8", "internalType": "uint8" }
      ]
    },
    "returnValueTest": {
      "comparator": ">=",
      "value": 2
    }
  }
}
```

## 7. `JsonApiCondition` — public weather API

Allow if today's temperature in London is below 10°C.

```json
{
  "version": "1.0.0",
  "condition": {
    "conditionType": "json-api",
    "endpoint": "https://api.open-meteo.com/v1/forecast",
    "parameters": {
      "latitude": 51.5072,
      "longitude": -0.1276,
      "current": "temperature_2m"
    },
    "query": "$.current.temperature_2m",
    "returnValueTest": {
      "comparator": "<",
      "value": 10
    }
  }
}
```

## 8. `JsonApiCondition` — authenticated API with Bearer token

The decrypter supplies an API token at decryption time via `:apiToken`.

```json
{
  "version": "1.0.0",
  "condition": {
    "conditionType": "json-api",
    "endpoint": "https://api.example.com/v1/subscription",
    "query": "$.status",
    "authorizationToken": ":apiToken",
    "authorizationType": "Bearer",
    "returnValueTest": {
      "comparator": "==",
      "value": "active"
    }
  }
}
```

`authorizationType` may be `"Bearer"`, `"Basic"`, or `"X-API-Key"`.

## 9. `JsonRpcCondition` — arbitrary JSON-RPC

Call any JSON-RPC service. Here we hit a Solana RPC and require a non-zero SOL balance for a specific, hard-coded Solana account. Hard-coding the account address keeps the access policy fixed at encryption time, which is safer than letting the requester name an arbitrary account via a context variable.

```json
{
  "version": "1.0.0",
  "condition": {
    "conditionType": "json-rpc",
    "endpoint": "https://api.mainnet-beta.solana.com",
    "method": "getBalance",
    "params": ["DezXAZ8z7PnrnRJjz3wXBoRgixCa6xjnB7YaB1pPB263"],
    "query": "$.value",
    "returnValueTest": {
      "comparator": ">",
      "value": 0
    }
  }
}
```

If your access policy genuinely depends on the *requester's* Solana account, you would need to receive that value through an authenticated channel (e.g. a JWT claim validated by a sibling `JwtCondition`) rather than simply letting the requester supply it as a context variable.

## 10. `JsonCondition` — querying a context payload

Unlike `JsonApiCondition` (which fetches), `JsonCondition` evaluates JSON that the decrypter has already supplied. Useful for off-chain attestations.

```json
{
  "version": "1.0.0",
  "condition": {
    "conditionType": "json",
    "data": ":attestation",
    "query": "$.claims.kyc_level",
    "returnValueTest": {
      "comparator": ">=",
      "value": 2
    }
  }
}
```

## 11. `JwtCondition` — verify a JWT

Validate a JWT signed by your IDP. The token is supplied at decryption time via the default `:jwtToken` variable.

```json
{
  "version": "1.0.0",
  "condition": {
    "conditionType": "jwt",
    "publicKey": "-----BEGIN PUBLIC KEY-----\\nMIIBIjANBgkqhki...\\n-----END PUBLIC KEY-----",
    "expectedIssuer": "https://auth.example.com/"
  }
}
```

`expectedIssuer` is optional but recommended.

`:jwtToken` is the default context-variable name. You can use any name you like by setting `"jwtToken": ":myCustomToken"` — this is useful when a single condition references more than one JWT (e.g. two sibling `JwtCondition`s validating different issuers), since each needs to receive its own value.

## 12. `EcdsaCondition` — verify an ECDSA signature

Verify that a message was signed by a known public key. This is the same pattern the Discord tipping bot uses to prove a Discord interaction is genuine.

```json
{
  "version": "1.0.0",
  "condition": {
    "conditionType": "ecdsa",
    "message": ":timestamp:discordPayload",
    "signature": ":signature",
    "verifyingKey": "ED25519_PUBLIC_KEY_HEX",
    "curve": "Ed25519"
  }
}
```

The `"message"` value here is a **concatenation of two context variables**, `:timestamp` and `:discordPayload`. At decryption time the SDK substitutes each `:name` token in sequence, so the string `":timestamp:discordPayload"` becomes `<timestamp_value><payload_value>` — which is what the signer signed.

`:message` and `:signature` are the default context-variable names; like `:jwtToken`, they can be renamed (`"message": ":myMessage"`, `"signature": ":mySig"`) — useful when a single condition verifies multiple distinct signatures.

Supported curves: `SECP256k1`, `NIST256p`, `NIST384p`, `NIST521p`, `Ed25519`, `BRAINPOOLP256r1`.

## 13. `SigningObjectAttributeCondition` — UserOperation field check

Used in TACo Action Control. Validate that an attribute on the object being signed (e.g. an ERC-4337 `UserOperation`) matches an expected value.

```json
{
  "version": "1.0.0",
  "condition": {
    "conditionType": "signing-attribute",
    "signingObjectContextVar": ":signingConditionObject",
    "attributeName": "sender",
    "returnValueTest": {
      "comparator": "==",
      "value": "0xEXPECTED_SMART_ACCOUNT_ADDRESS"
    }
  }
}
```

## 14. `SigningObjectAbiAttributeCondition` — validate calldata

Used in TACo Action Control. Decode an attribute (typically `call_data`) using ABI definitions and assert constraints on the decoded parameters. Here: only allow `transfer(recipient, amount)` to a specific address.

```json
{
  "version": "1.0.0",
  "condition": {
    "conditionType": "signing-abi-attribute",
    "signingObjectContextVar": ":signingConditionObject",
    "attributeName": "call_data",
    "abiValidation": {
      "allowedAbiCalls": {
        "transfer(address,uint256)": [
          {
            "parameterIndex": 0,
            "returnValueTest": {
              "comparator": "==",
              "value": "0xALLOWED_RECIPIENT"
            }
          },
          {
            "parameterIndex": 1,
            "returnValueTest": {
              "comparator": "<=",
              "value": 1000000
            }
          }
        ]
      }
    }
  }
}
```

For nested calldata validation (e.g. an ERC-4337 `execute()` that wraps a `transfer()`), see the [Discord tipping bot deep-dive](/for-developers/conditions/discord-tipping-bot-deep-dive).

## 15. `ContextVariableCondition` — assert a custom variable

Apply a return-value test directly to a context variable. Most often used inside a `SequentialCondition` to validate an intermediate result.

```json
{
  "version": "1.0.0",
  "condition": {
    "conditionType": "context-variable",
    "contextVariable": ":kycLevel",
    "returnValueTest": {
      "comparator": ">=",
      "value": 2
    }
  }
}
```

## 16. `CompoundCondition` — AND, OR, NOT

Combine up to 5 sub-conditions. Maximum nesting depth is 4. The nesting limit is counted across **all** MultiConditions — `CompoundCondition`, `IfThenElseCondition`, and `SequentialCondition` — not just compounds. A compound that contains an if-then-else that contains a sequential counts as three levels.

```json
{
  "version": "1.0.0",
  "condition": {
    "conditionType": "compound",
    "operator": "and",
    "operands": [
      {
        "conditionType": "time",
        "chain": 1,
        "method": "blocktime",
        "returnValueTest": { "comparator": ">", "value": 1735689600 }
      },
      {
        "conditionType": "contract",
        "chain": 1,
        "contractAddress": "0xNFT",
        "standardContractType": "ERC721",
        "method": "balanceOf",
        "parameters": [":userAddress"],
        "returnValueTest": { "comparator": ">", "value": 0 }
      }
    ]
  }
}
```

`"operator": "or"` and `"operator": "not"` work the same way (`not` takes a single operand).

## 17. `SequentialCondition` — derive then check

Two to twenty steps, each binding a `varName` that subsequent steps can reference. Use `operations` on a step to transform the obtained value before storing it.

```json
{
  "version": "1.0.0",
  "condition": {
    "conditionType": "sequential",
    "conditionVariables": [
      {
        "varName": "rawBalance",
        "condition": {
          "conditionType": "contract",
          "chain": 8453,
          "contractAddress": "0xUSDC",
          "standardContractType": "ERC20",
          "method": "balanceOf",
          "parameters": [":userAddress"],
          "returnValueTest": { "comparator": ">=", "value": 0 }
        }
      },
      {
        "varName": "checkMinimum",
        "condition": {
          "conditionType": "context-variable",
          "contextVariable": ":rawBalance",
          "returnValueTest": {
            "operations": [
              { "operation": "/=", "value": 1000000 }
            ],
            "comparator": ">=",
            "value": 100
          }
        }
      }
    ]
  }
}
```

## 18. `IfThenElseCondition` — branching

`ifCondition` and `thenCondition` must both be full conditions. Only `elseCondition` accepts a boolean shortcut (`true` means "admit", `false` means "deny").

Before reaching for `IfThenElseCondition`, check whether the rule is really a `CompoundCondition`:

* `if A then true else B` is `OR(A, B)`
* `if A then false else B` is `AND(NOT(A), B)`

`IfThenElseCondition` earns its keep when `thenCondition` and `elseCondition` are themselves meaningful, distinct checks — i.e. holders of the branch-condition's subject face one requirement, and non-holders face another. The example below applies different rules to VIP-pass holders and the general public: holders only need the access window to be open, while everyone else must hold ≥10 USDC.

```json
{
  "version": "1.0.0",
  "condition": {
    "conditionType": "if-then-else",
    "ifCondition": {
      "conditionType": "contract",
      "chain": 1,
      "contractAddress": "0xVIP_PASS",
      "standardContractType": "ERC721",
      "method": "balanceOf",
      "parameters": [":userAddress"],
      "returnValueTest": { "comparator": ">", "value": 0 }
    },
    "thenCondition": {
      "conditionType": "time",
      "chain": 1,
      "method": "blocktime",
      "returnValueTest": { "comparator": ">=", "value": 1735689600 }
    },
    "elseCondition": {
      "conditionType": "contract",
      "chain": 1,
      "contractAddress": "0xUSDC",
      "standardContractType": "ERC20",
      "method": "balanceOf",
      "parameters": [":userAddress"],
      "returnValueTest": { "comparator": ">=", "value": 10000000 }
    }
  }
}
```

## 19. Pattern: NFT-or-allowlist gate

Hold the NFT **or** be on a hard-coded allowlist. The allowlist branch uses a `ContextVariableCondition` to check that `:userAddress` is one of the allowed values — no RPC call is needed for a pure allowlist check.

```json
{
  "version": "1.0.0",
  "condition": {
    "conditionType": "compound",
    "operator": "or",
    "operands": [
      {
        "conditionType": "contract",
        "chain": 1,
        "contractAddress": "0xNFT",
        "standardContractType": "ERC721",
        "method": "balanceOf",
        "parameters": [":userAddress"],
        "returnValueTest": { "comparator": ">", "value": 0 }
      },
      {
        "conditionType": "context-variable",
        "contextVariable": ":userAddress",
        "returnValueTest": {
          "comparator": "in",
          "value": [
            "0xALLOWED_ADDRESS_1",
            "0xALLOWED_ADDRESS_2",
            "0xALLOWED_ADDRESS_3"
          ]
        }
      }
    ]
  }
}
```

## 20. Pattern: time-windowed paid access

Allow decryption between two timestamps **and** require an active subscription on-chain.

```json
{
  "version": "1.0.0",
  "condition": {
    "conditionType": "compound",
    "operator": "and",
    "operands": [
      {
        "conditionType": "time",
        "chain": 8453,
        "method": "blocktime",
        "returnValueTest": { "comparator": ">=", "value": 1735689600 }
      },
      {
        "conditionType": "time",
        "chain": 8453,
        "method": "blocktime",
        "returnValueTest": { "comparator": "<", "value": 1767225600 }
      },
      {
        "conditionType": "contract",
        "chain": 8453,
        "contractAddress": "0xSUBSCRIPTION_REGISTRY",
        "method": "expiresAt",
        "parameters": [":userAddress"],
        "functionAbi": {
          "name": "expiresAt",
          "type": "function",
          "stateMutability": "view",
          "inputs": [{ "name": "user", "type": "address", "internalType": "address" }],
          "outputs": [{ "name": "", "type": "uint256", "internalType": "uint256" }]
        },
        "returnValueTest": { "comparator": ">", "value": 1735689600 }
      }
    ]
  }
}
```

## 21. Pattern: storm-condition open access

A backup safety plan for an outdoor event. If it is raining at the venue **and** the wind is high enough to be unsafe, anyone can decrypt the umbrella-pickup instructions; otherwise only umbrella-NFT holders can. Each branch is a meaningful check — there is no artificial `thenCondition`.

```json
{
  "version": "1.0.0",
  "condition": {
    "conditionType": "if-then-else",
    "ifCondition": {
      "conditionType": "json-api",
      "endpoint": "https://api.open-meteo.com/v1/forecast",
      "parameters": { "latitude": 51.5, "longitude": -0.12, "current": "rain" },
      "query": "$.current.rain",
      "returnValueTest": { "comparator": ">", "value": 0 }
    },
    "thenCondition": {
      "conditionType": "json-api",
      "endpoint": "https://api.open-meteo.com/v1/forecast",
      "parameters": { "latitude": 51.5, "longitude": -0.12, "current": "wind_speed_10m" },
      "query": "$.current.wind_speed_10m",
      "returnValueTest": { "comparator": ">", "value": 30 }
    },
    "elseCondition": {
      "conditionType": "contract",
      "chain": 1,
      "contractAddress": "0xUMBRELLA_NFT",
      "standardContractType": "ERC721",
      "method": "balanceOf",
      "parameters": [":userAddress"],
      "returnValueTest": { "comparator": ">", "value": 0 }
    }
  }
}
```

## 22. Pattern: balance fetched, normalised, then asserted

A `SequentialCondition` that fetches a wei balance, divides it down to whole ETH using `operations`, and then asserts a minimum.

```json
{
  "version": "1.0.0",
  "condition": {
    "conditionType": "sequential",
    "conditionVariables": [
      {
        "varName": "weiBalance",
        "condition": {
          "conditionType": "rpc",
          "chain": 1,
          "method": "eth_getBalance",
          "parameters": [":userAddress", "latest"],
          "returnValueTest": { "comparator": ">=", "value": 0 }
        },
        "operations": [
          { "operation": "weiToEth" }
        ]
      },
      {
        "varName": "checkWholeEth",
        "condition": {
          "conditionType": "context-variable",
          "contextVariable": ":weiBalance",
          "returnValueTest": { "comparator": ">=", "value": 5 }
        }
      }
    ]
  }
}
```

## Available `operations`

Operations can be applied to obtained values inside `returnValueTest.operations` or `ConditionVariable.operations`. Up to five per array.

| Operation                         | Purpose                                                                    |
| --------------------------------- | -------------------------------------------------------------------------- |
| `+=`, `-=`, `*=`, `/=`, `%=`      | Arithmetic with the supplied `value`                                       |
| `toTokenBaseUnits`                | Multiply by `10^value` (decimals → base units)                             |
| `weiToEth`, `ethToWei`            | Native unit conversions                                                    |
| `int`, `float`, `bool`, `str`     | Type coercion                                                              |
| `round`, `floor`, `ceil`, `abs`   | Numeric rounding                                                           |
| `len`, `min`, `max`, `sum`, `avg` | Aggregations on arrays                                                     |
| `index`                           | Pick an array element                                                      |
| `keccak`                          | Hash a string                                                              |
| `create2`                         | Compute a CREATE2 address locally given `deployerAddress` + `bytecodeHash` |

For the full enum see [VariableOperation in the schema reference](https://github.com/nucypher/taco-web/blob/signing-epic/packages/taco/schema-docs/condition-schemas.md#variableoperation).

***

Need something this cookbook does not cover? Read the [schema reference](https://github.com/nucypher/taco-web/blob/signing-epic/packages/taco/schema-docs/condition-schemas.md), then ask an LLM with the [building-with-llms workflow](/for-developers/conditions/building-with-llms).


# Discord Tipping Bot Deep Dive

TACo conditions are arbitrarily expressive — there is no upper bound on what you can compose. This page works through one real-world example that happens to exercise many features at once: nested compound logic, sequential variables, on-chain CREATE2 derivation, JSONPath extraction, snowflake arithmetic, ECDSA verification, and nested ABI calldata validation. Paste the whole page into an LLM's context when you want it to author conditions in this style.

The condition guards a TACo Action Control flow built by an integrator: a Discord user types `/tip 0.50 0xfriend` in a Discord server running a tipping bot, and a smart account derived deterministically from their Discord ID sends 0.50 USDC on Base — but **only** if every clause below is satisfied. The TACo network performs threshold signing on the resulting `UserOperation` exactly when the condition evaluates true.

> **Addresses anonymised.** Where you see `0xUSDC_ON_BASE`, `0xAA_FACTORY`, `0xACCOUNT_BYTECODE_HASH`, `<ED25519_PUBKEY>`, etc., substitute the real values from the [`discord-taco-web`](https://github.com/nucypher/discord-taco-web) repo. The structure is what matters.

## What the condition enforces

1. The Discord interaction payload was actually signed by a known relay's Ed25519 key (proves the request originated from Discord and passed through the trusted relay).
2. The Discord account is at least 6 months old (anti-spam).
3. The smart account submitting the `UserOperation` is the **deterministically derived** account for that exact Discord user — nobody can spend someone else's tip allowance.
4. The `UserOperation` calldata is exactly `execute(USDC, 0, transfer(recipient, amount))`, with the recipient and amount lifted from the Discord slash-command parameters and the amount converted to USDC base units.

If any of those fail, no signature.

## Top-level shape

```json
{
  "version": "1.0.0",
  "condition": {
    "conditionType": "compound",
    "operator": "and",
    "operands": [
      { "...": "1. ECDSA signature on Discord payload" },
      { "...": "2. SequentialCondition: account-age + AA derivation + sender check" },
      { "...": "3. SequentialCondition: parse tip params + ABI calldata validation" }
    ]
  }
}
```

A `CompoundCondition` with `and` over three operands. Two of those operands are themselves `SequentialCondition`s — that is how we get more than the 5-condition limit's worth of expressive power inside a compound.

## Clause 1: Verify the relay's signature

```json
{
  "conditionType": "ecdsa",
  "message": ":timestamp:discordPayload",
  "signature": ":signature",
  "verifyingKey": "<ED25519_PUBKEY>",
  "curve": "Ed25519"
}
```

When the bot relays a Discord interaction to TACo, it passes:

* `:timestamp` — Discord interaction timestamp
* `:discordPayload` — the full JSON of the slash-command interaction
* `:signature` — the Ed25519 signature the relay applied to `timestamp || discordPayload`
* `:verifyingKey` — the relay's well-known public key (a constant, baked into the condition)

If this fails, the network refuses to sign. Nobody can fake a Discord interaction without the relay's private key.

## Clause 2: Sequential — derive the AA and prove sender identity

This is the heart of the condition. Six steps, each binding a variable subsequent steps depend on.

```json
{
  "conditionType": "sequential",
  "conditionVariables": [
    { "varName": "senderId",                "condition": { "..." } },
    { "varName": "minAccountCreationTime",  "condition": { "..." } },
    { "varName": "validateAccountAge",      "condition": { "..." } },
    { "varName": "senderSalt",              "condition": { "..." } },
    { "varName": "senderAA",                "condition": { "..." } },
    { "varName": "validateSender",          "condition": { "..." } }
  ]
}
```

### Step 2.1 — Pull the Discord user ID

```json
{
  "varName": "senderId",
  "condition": {
    "conditionType": "json",
    "data": ":discordPayload",
    "query": "$.member.user.id",
    "returnValueTest": { "comparator": ">", "value": 0 }
  }
}
```

`:discordPayload` is supplied by the bot. JSONPath extracts `member.user.id` (a Discord snowflake — a 64-bit integer encoded as a string). The `> 0` test is a "not empty" sentinel; the real validation happens in step 2.3.

### Step 2.2 — Compute the cutoff timestamp (now − 6 months) in milliseconds

```json
{
  "varName": "minAccountCreationTime",
  "condition": {
    "chain": 8453,
    "method": "blocktime",
    "returnValueTest": { "comparator": ">", "value": 0 },
    "conditionType": "time"
  },
  "operations": [
    { "operation": "*=", "value": 1000 },
    { "operation": "-=", "value": 15768000000 }
  ]
}
```

`TimeCondition` returns Base's latest block timestamp in seconds. The two `operations` on the variable convert it to milliseconds (`*1000`) and subtract six months in ms (`15768000000`). The result is bound as `:minAccountCreationTime`.

> **Why operations on the variable, not on `returnValueTest`?** The `> 0` here is a no-op truthiness check — we just want the timestamp itself. The arithmetic transforms the **stored** value, not the comparison.

### Step 2.3 — Validate Discord snowflake → account age

```json
{
  "varName": "validateAccountAge",
  "condition": {
    "conditionType": "context-variable",
    "contextVariable": ":senderId",
    "returnValueTest": {
      "operations": [
        { "operation": "int" },
        { "operation": "/=", "value": 4194304 },
        { "operation": "+=", "value": 1420070400000 }
      ],
      "comparator": "<",
      "value": ":minAccountCreationTime"
    }
  }
}
```

This is the snowflake decode trick. Discord snowflakes are 64-bit integers where the upper 42 bits encode `(unix_ms - DISCORD_EPOCH)`. To get the account creation time:

1. `int` — coerce the snowflake string to integer
2. `/= 4194304` — right-shift by 22 bits (divide by `2^22`)
3. `+= 1420070400000` — add the Discord epoch (2015-01-01 in ms)

Then assert: creation time `<` `:minAccountCreationTime`. In English: "the account was created before the cutoff", i.e. it is at least 6 months old.

### Step 2.4 — Derive the salt for CREATE2

```json
{
  "varName": "senderSalt",
  "condition": {
    "conditionType": "context-variable",
    "contextVariable": ":senderId",
    "returnValueTest": { "comparator": ">", "value": 0 }
  },
  "operations": [
    { "operation": "str" },
    { "operation": "+=", "value": "|Discord|TipBot" },
    { "operation": "keccak" }
  ]
}
```

We take the Discord ID, stringify it, append `|Discord|TipBot`, and hash it with keccak. The result is the deterministic salt for the smart account derivation. Two different Discord users → two different salts → two different smart accounts. The same Discord user always derives the same salt.

### Step 2.5 — Compute the smart account address

```json
{
  "varName": "senderAA",
  "condition": {
    "chain": 8453,
    "method": "computeAddress",
    "parameters": [
      "0xACCOUNT_BYTECODE_HASH",
      ":senderSalt"
    ],
    "contractAddress": "0xAA_FACTORY",
    "functionAbi": {
      "name": "computeAddress",
      "type": "function",
      "stateMutability": "view",
      "inputs": [
        { "name": "_bytecodeHash", "type": "bytes32", "internalType": "bytes32" },
        { "name": "_salt",         "type": "bytes32", "internalType": "bytes32" }
      ],
      "outputs": [
        { "name": "", "type": "address", "internalType": "address" }
      ]
    },
    "returnValueTest": {
      "comparator": "!=",
      "value": "0x0000000000000000000000000000000000000000"
    },
    "conditionType": "contract"
  }
}
```

A real on-chain `view` call into the AA factory. Given the bytecode hash (constant) and the salt (computed in 2.4), the factory returns the CREATE2 address — the smart account that *would* exist (or already exists) for this Discord user. We bind it as `:senderAA`.

> **Alternative:** the schema also exposes a `create2` operation that computes the address **locally** without an RPC round-trip. The bot uses the on-chain version because the factory's address-derivation logic may evolve; a local computation would have to be kept in sync.

### Step 2.6 — Assert the UserOperation `sender` matches

```json
{
  "varName": "validateSender",
  "condition": {
    "signingObjectContextVar": ":signingConditionObject",
    "attributeName": "sender",
    "conditionType": "signing-attribute",
    "returnValueTest": {
      "comparator": "==",
      "value": ":senderAA"
    }
  }
}
```

`:signingConditionObject` is the `UserOperation` the network is being asked to sign. Read its `sender` field and require it to equal the address we derived in step 2.5.

This is the linchpin. Without this clause, anyone could submit a `UserOperation` from *any* smart account and ride on the rest of the validation. With it, the only smart account that can ever pass is the one deterministically tied to that Discord user.

## Clause 3: Sequential — parse tip params and validate calldata

```json
{
  "conditionType": "sequential",
  "conditionVariables": [
    { "varName": "amountUSDC",       "condition": { "..." } },
    { "varName": "recipientDirect",  "condition": { "..." } },
    { "varName": "validateCalldata", "condition": { "..." } }
  ]
}
```

### Step 3.1 — Extract and normalise the tip amount

```json
{
  "varName": "amountUSDC",
  "condition": {
    "conditionType": "json",
    "data": ":discordPayload",
    "query": "$.data.options[0].options[?(@.name == \"amount\")].value",
    "returnValueTest": {
      "comparator": ">=",
      "value": 0.25,
      "operations": [
        { "operation": "float" }
      ]
    }
  },
  "operations": [
    { "operation": "toTokenBaseUnits", "value": 6 }
  ]
}
```

A JSONPath filter (`?(@.name == "amount")`) finds the slash-command argument named `amount`. The return-value test coerces it to a float and asserts a 0.25 USDC minimum. The variable-level `operations` then multiply the float by `10^6` to get USDC base units, and bind that as `:amountUSDC`.

### Step 3.2 — Extract the recipient address

```json
{
  "varName": "recipientDirect",
  "condition": {
    "conditionType": "json",
    "data": ":discordPayload",
    "query": "$.data.options[0].options[?(@.name == \"address\")].value",
    "returnValueTest": { "comparator": ">", "value": 0 }
  }
}
```

Same trick — pull the `address` argument out of the slash-command payload, bind as `:recipientDirect`.

### Step 3.3 — Validate the UserOperation calldata exactly

```json
{
  "varName": "validateCalldata",
  "condition": {
    "signingObjectContextVar": ":signingConditionObject",
    "attributeName": "call_data",
    "conditionType": "signing-abi-attribute",
    "abiValidation": {
      "allowedAbiCalls": {
        "execute((address,uint256,bytes))": [
          {
            "parameterIndex": 0,
            "subIndices": [0],
            "returnValueTest": {
              "comparator": "==",
              "value": "0xUSDC_ON_BASE"
            }
          },
          {
            "parameterIndex": 0,
            "subIndices": [1],
            "returnValueTest": { "comparator": "==", "value": 0 }
          },
          {
            "parameterIndex": 0,
            "subIndices": [2],
            "nestedAbiValidation": {
              "allowedAbiCalls": {
                "transfer(address,uint256)": [
                  {
                    "parameterIndex": 0,
                    "returnValueTest": {
                      "comparator": "==",
                      "value": ":recipientDirect"
                    }
                  },
                  {
                    "parameterIndex": 1,
                    "returnValueTest": {
                      "comparator": "==",
                      "value": ":amountUSDC"
                    }
                  }
                ]
              }
            }
          }
        ]
      }
    }
  }
}
```

Walking it:

* The `UserOperation`'s `call_data` must decode as `execute((address,uint256,bytes))` — the standard ERC-4337 single-call entry point. It takes one tuple parameter.
* `parameterIndex: 0, subIndices: [0]` — the first field of the tuple (the `address` target) must equal the USDC contract on Base. **No other token can be transferred.**
* `parameterIndex: 0, subIndices: [1]` — the tuple's `value` field must be 0 (no native ETH attached).
* `parameterIndex: 0, subIndices: [2]` — the tuple's `bytes` field is itself ABI-encoded calldata. We decode it with `nestedAbiValidation` and assert it is exactly `transfer(address,uint256)` with:
  * parameter 0 (`address`) equal to `:recipientDirect` — the recipient extracted from the Discord payload in step 3.2
  * parameter 1 (`uint256`) equal to `:amountUSDC` — the normalised amount from step 3.1

The chain of inference: Discord said tip 0.50 USDC to `0xfriend` → bot built a `UserOperation` that calls `execute(USDC, 0, transfer(0xfriend, 500_000))` → TACo signs it only if the calldata literally matches what the Discord message asked for. **Front-running, calldata mutation, or token substitution all fail.**

## What this example teaches

* **Compound + sequential is how you build deep logic.** The 5-condition compound limit is not a limit on expressivity if you embed sequentials inside the operands.
* **Sequential variables are the workhorse.** Use them whenever a check depends on a value computed elsewhere in the condition.
* **`operations` lets you do real arithmetic on-chain-ish** — bit shifts via division, hashing, type coercion, unit conversions. Many things you would otherwise need a smart contract for can be done in the condition itself.
* **`signing-abi-attribute` with `nestedAbiValidation`** is how you enforce policy on ERC-4337 calldata structure. This is the killer feature for Action Control.
* **Anonymise constants only when necessary.** In a production deployment the bytecode hash, factory address, and relay verifying key are all hardcoded and public. They are anonymised on this page for documentation hygiene; an integrator's own conditions normally do not need to be.

## Want to run this?

The full, un-anonymised condition lives at [`discord-taco-web/conditions-mainnet.json`](https://github.com/nucypher/discord-taco-web/blob/main/conditions-mainnet.json). Validate it locally with the [`validate-conditions.ts`](/for-developers/conditions/validating-conditions) script before shipping any modifications.


# Context Variables Cheatsheet

A **context variable** is a placeholder used inside a condition. Its value is supplied at decryption (or signing) time, not at encryption time. That is what makes a single encrypted ciphertext usable across many requesters and many runtime states.

This page is a cheatsheet. For a longer narrative, see [Condition Context](/for-developers/authentication/conditioncontext-and-context-variables).

## Naming rules

* Always start with `:` — e.g. `:userAddress`
* After the `:`, the name must match `/^[a-zA-Z_][a-zA-Z0-9_]*$/`
* Case-sensitive
* No dots, dashes, or spaces. `:user-address`, `:user.address`, `:1stParam` are all invalid.

If you forget the leading `:`, the SDK will treat your value as a literal string and your condition will silently match nothing (or worse, match something).

## Built-in context variables

The SDK recognises a small set of reserved or default context-variable names. Their behaviour differs — read the table carefully before assuming a value is "automatic".

| Variable                  | Kind                          | Available in                                                            | Description                                                                                                                                                                |
| ------------------------- | ----------------------------- | ----------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `:signingConditionObject` | **Auto-injected** (node side) | `SigningObjectAttributeCondition`, `SigningObjectAbiAttributeCondition` | The full object (e.g. UserOperation) being submitted for threshold signing. You do not need to add this to the condition context — the node injects it at evaluation time. |
| `:nullAddress`            | **Auto-injected** (node side) | All conditions                                                          | The zero address (`0x0000…0000`). Useful as a sentinel in allowlists or when a field genuinely means "no address". The node injects the value; you cannot override it.     |
| `:userAddress`            | **Reserved**                  | All conditions                                                          | The Ethereum address of the requester. Its value can only be provided by an `AuthProvider` at request time — it is never set by user code directly.                        |
| `:message`                | **Default name**              | `EcdsaCondition`                                                        | Default for `EcdsaCondition.message`. Not automatically populated — the decrypter must supply a value at request time.                                                     |
| `:signature`              | **Default name**              | `EcdsaCondition`                                                        | Default for `EcdsaCondition.signature`. Not automatically populated — the decrypter must supply a value at request time.                                                   |
| `:jwtToken`               | **Default name**              | `JwtCondition`                                                          | Default for `JwtCondition.jwtToken`. Not automatically populated — the decrypter must supply a value at request time.                                                      |

> The source of truth for this table is the `AUTOMATICALLY_INJECTED_CONTEXT_PARAMS` and `RESERVED_CONTEXT_PARAMS` arrays in [`taco-web/packages/taco/src/conditions/context/context.ts`](https://github.com/nucypher/taco-web/blob/signing-epic/packages/taco/src/conditions/context/context.ts).

### Auto-injected vs. reserved vs. default

* **Auto-injected** means the node populates the value at evaluation time. Setting these manually via `ConditionContext.addCustomContextVariableValues` will throw. `:signingConditionObject` and `:nullAddress` are the only auto-injected variables.
* **Reserved** means the name is fixed and the value can only come from a specific source. `:userAddress` is reserved and can only be supplied by an `AuthProvider` — attempting to set it as a custom parameter will throw.
* **Default name** means the condition *defaults* to that variable name, but you can override it. For example, if a single compound condition contains two `JwtCondition`s that must validate two different tokens, give each its own variable:

```json
{
  "conditionType": "compound",
  "operator": "and",
  "operands": [
    {
      "conditionType": "jwt",
      "jwtToken": ":idpToken",
      "publicKey": "…",
      "expectedIssuer": "https://idp.example.com/"
    },
    {
      "conditionType": "jwt",
      "jwtToken": ":partnerToken",
      "publicKey": "…",
      "expectedIssuer": "https://partner.example.com/"
    }
  ]
}
```

The same applies to `EcdsaCondition.message` and `EcdsaCondition.signature` when verifying multiple signatures in one condition.

## Custom context variables

You can define any name you like, as long as it follows the [naming rules](#naming-rules). The decrypter is responsible for supplying its value at decryption time.

```json
{
  "conditionType": "json",
  "data": ":discordPayload",
  "query": "$.member.user.id",
  "returnValueTest": { "comparator": ">", "value": 0 }
}
```

Here `:discordPayload` is a custom variable. The bot supplies it as part of the decryption request.

### Variables produced by SequentialCondition

`SequentialCondition` lets each step bind a `varName`. That name becomes a context variable usable by subsequent steps:

```json
{
  "conditionType": "sequential",
  "conditionVariables": [
    {
      "varName": "balance",
      "condition": { "...": "fetches an ERC20 balance" }
    },
    {
      "varName": "validate",
      "condition": {
        "conditionType": "context-variable",
        "contextVariable": ":balance",
        "returnValueTest": { "comparator": ">=", "value": 1000 }
      }
    }
  ]
}
```

`varName` is a **plain string** (no leading `:`), but you reference it later **with** the `:` prefix.

## Where context variables can appear

| Field type                                                  | Accepts context variable?                 |
| ----------------------------------------------------------- | ----------------------------------------- |
| `returnValueTest.value`                                     | ✅                                         |
| `ContractCondition.parameters[*]`                           | ✅                                         |
| `JsonCondition.data`                                        | ✅ (required — must be a context variable) |
| `JsonCondition.query`                                       | ✅ (or a JSONPath)                         |
| `JsonApiCondition.endpoint`                                 | ❌ (must be a literal HTTPS URL)           |
| `JsonApiCondition.authorizationToken`                       | ✅                                         |
| `EcdsaCondition.message` / `signature`                      | ✅                                         |
| `JwtCondition.jwtToken`                                     | ✅                                         |
| `chain`, `method`, `comparator`                             | ❌ — must be literal                       |
| ABI function signature keys (`"transfer(address,uint256)"`) | ❌ — must be literal                       |
| ABI parameter `value` inside `returnValueTest`              | ✅                                         |

## Common gotcha: JSONPath vs context variable

`JsonCondition.query` accepts **either** a JSONPath expression (`$.member.user.id`) **or** a context variable (`:somePath`). The schema disambiguates by the leading character — `$` means JSONPath, `:` means context variable. Anything else is a validation error.


# Validating Conditions

Before you ship a condition — or before you ask the network to evaluate one — validate it locally. Client-side validation catches the overwhelming majority of mistakes (typos, wrong field names, missing required properties, illegal nesting depth, comparator typos) without ever touching a node.

You have two options. Pick whichever fits your stack — they overlap heavily.

| Approach                                 | When to use                                                                                                                                                                                                                                            |
| ---------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **JSON Schema validator** (any language) | You want zero TACo dependencies, you are validating in CI, or you want inline editor validation via `$schema`. See [Building Conditions with an LLM → JSON Schema integration](/for-developers/conditions/building-with-llms#json-schema-integration). |
| **`validate-conditions.ts`** (this page) | You are already in TypeScript, or you need the SDK's runtime semantics (e.g. nesting-depth checks that JSON Schema cannot express).                                                                                                                    |

This page gives you a 40-line TypeScript script you can drop into any project.

## The script

Save this as `validate-conditions.ts`:

```ts
#!/usr/bin/env npx tsx
/**
 * Validates conditions.json against the TACo SDK schemas.
 * Catches client-side validation errors before they hit the network.
 */

import { conditions } from '@nucypher/taco';
import * as fs from 'fs';
import * as path from 'path';

async function main() {
  const conditionsPath = path.join(__dirname, '..', 'conditions.json');
  console.log(`Loading conditions from: ${conditionsPath}`);

  const conditionsJson = fs.readFileSync(conditionsPath, 'utf-8');
  const conditionsObj = JSON.parse(conditionsJson);

  console.log('\nConditions loaded:');
  console.log(JSON.stringify(conditionsObj, null, 2));

  console.log('\n--- Validating with TACo SDK ---\n');

  try {
    const expr = conditions.conditionExpr.ConditionExpression.fromObj(conditionsObj);
    console.log('✅ Conditions are VALID according to TACo SDK!');
    console.log('\nParsed condition type:', expr.condition.conditionType);
  } catch (error) {
    console.error('❌ Conditions are INVALID according to TACo SDK!');
    const errorStr = error instanceof Error
      ? `${error.name}: ${error.message}\n\nStack: ${error.stack}`
      : JSON.stringify(error, null, 2);
    const errorPath = path.join(__dirname, 'validation-error.txt');
    fs.writeFileSync(errorPath, errorStr);
    console.error(`\nFull error written to: ${errorPath}`);
    console.error('\nError message:', error instanceof Error ? error.message : String(error));
    process.exit(1);
  }
}

main().catch(console.error);
```

## Setup

```bash
npm install --save-dev @nucypher/taco tsx
# or
pnpm add -D @nucypher/taco tsx
```

Place your condition in `conditions.json` at the project root (or adjust the path inside the script).

## Run it

```bash
npx tsx scripts/validate-conditions.ts
```

### Valid output

```
Loading conditions from: .../conditions.json

Conditions loaded:
{
  "version": "1.0.0",
  "condition": { ... }
}

--- Validating with TACo SDK ---

✅ Conditions are VALID according to TACo SDK!

Parsed condition type: compound
```

### Invalid output

```
❌ Conditions are INVALID according to TACo SDK!

Full error written to: scripts/validation-error.txt

Error message: [
  {
    "code": "invalid_literal",
    "expected": "and",
    "received": "AND",
    "path": ["condition", "operator"],
    "message": "Invalid literal value, expected \"and\""
  }
]
```

The full Zod error (with stack) is written to `validation-error.txt` so you can paste it into an LLM verbatim — see [Building Conditions with an LLM](/for-developers/conditions/building-with-llms) and [Troubleshooting](/for-developers/conditions/troubleshooting).

## What this catches

* Unknown / misspelled `conditionType` values
* Missing required fields (`returnValueTest`, `chain`, `parameters`, …)
* Wrong literal values (`"AND"` instead of `"and"`, `"=="` written as `"="`)
* MultiCondition nesting deeper than the allowed limit (4 levels total across `CompoundCondition`, `IfThenElseCondition`, and `SequentialCondition`)
* Compound condition with more than 5 operands
* Sequential condition with fewer than 2 or more than 20 variables
* Context variable names that violate the `/^:[a-zA-Z_][a-zA-Z0-9_]*$/` pattern
* ABI validation referencing parameter indices that do not exist on the declared function signature
* Invalid hex strings for verifying keys, contract addresses, bytecode hashes

## What this does **not** catch

Client-side validation only checks shape. It cannot tell you:

* Whether the contract at `contractAddress` actually exists or has the function you declared
* Whether the JSON API endpoint will return data in the shape your JSONPath expects
* Whether the user will satisfy the condition at decryption time

For end-to-end testing, run the condition against a local testnet (see [Quickstart](/for-developers/quickstart-testnet)) or your own integration.

## Source

The canonical copy of this script lives at [`nucypher/taco-web/packages/taco/scripts/validate-conditions.ts`](https://github.com/nucypher/taco-web/blob/main/packages/taco/scripts/validate-conditions.ts).


# Troubleshooting

The TACo SDK validates conditions with [Zod](https://zod.dev/), and Zod's error messages are precise but terse. This page maps the errors you will actually see to what they mean and how to fix them.

The fastest workflow: run the [validator script](/for-developers/conditions/validating-conditions), copy the error, look it up here (or paste it into an LLM with the [building-with-llms guide](/for-developers/conditions/building-with-llms)).

## Schema-shape errors

### `Invalid literal value, expected "and"` (or `"or"`, `"not"`)

You wrote `"operator": "AND"` or `"operator": "&&"`. Operators are lowercase strings. Allowed: `"and"`, `"or"`, `"not"`.

### `Invalid literal value, expected "compound"` (or any other `conditionType`)

`conditionType` is wrong or missing. Each condition type has an exact required string — see the [schema reference](https://github.com/nucypher/taco-web/blob/signing-epic/packages/taco/schema-docs/condition-schemas.md). Common typos: `"contractCondition"` (should be `"contract"`), `"jsonApi"` (should be `"json-api"`), `"ifThenElse"` (should be `"if-then-else"`), `"signingAttribute"` (should be `"signing-attribute"`).

### `Required` on a field you thought was optional

The schema reference marks required fields with `(*)`. Common surprises:

* `ContractCondition.parameters` is **required** (use `[]` if the function takes none)
* `ContractCondition.contractAddress` is **required**
* `JsonApiCondition.endpoint` is **required**
* `EcdsaCondition.verifyingKey` and `curve` are **required**
* `TimeCondition.chain` is **required** (`method` defaults to `"blocktime"`)

### `Array must contain at least 2 element(s)` on a SequentialCondition

`SequentialCondition.conditionVariables` requires 2–20 entries. If you only need one variable, you do not need a SequentialCondition — just use the inner condition directly.

### `Array must contain at most 5 element(s)` on a CompoundCondition

`CompoundCondition.operands` is capped at 5. To express more, embed a `SequentialCondition` (or nest another MultiCondition, within the shared depth limit — see next).

### `MultiCondition nesting depth exceeded`

Nesting is limited to **4 levels**, counted across **all** MultiConditions collectively: `CompoundCondition`, `IfThenElseCondition`, and `SequentialCondition`. A compound → if-then-else → sequential chain is three levels, even though no single type is nested within itself. If you hit the limit, flatten the policy — often a `SequentialCondition` with early-exit logic replaces several nested compounds.

### `String must match pattern /^:[a-zA-Z_][a-zA-Z0-9_]*$/`

A context variable name is malformed. They must start with `:`, followed by a letter or underscore, then letters/digits/underscores. No dots, dashes, spaces, or leading digits. See [Context Variables](/for-developers/conditions/context-variables).

### `Invalid url` on `JsonApiCondition.endpoint`

`endpoint` must be a literal HTTPS URL. You cannot pass a context variable here. If you need a dynamic endpoint, you must commit to a specific service at encryption time.

### `Invalid hex string` on `verifyingKey`, `contractAddress`, etc.

Hex strings must contain only `0-9a-fA-F`. `verifyingKey` (Ed25519/SECP256k1 raw bytes) does **not** include a `0x` prefix; addresses **do**. Check the schema for which is which.

## Logical errors that pass validation but fail at runtime

These do not produce a Zod error — they fail when the network actually evaluates the condition. Symptoms: decryption requests are rejected by Porter or by individual nodes.

### "Decryption denied" but the condition looks right

* **Wrong chain ID.** A condition that calls Base USDC with `"chain": 1` (Ethereum) will hit a contract that does not exist there. Sanity-check every chain ID.
* **Stale block tag.** `RpcCondition` defaults its block tag to `"latest"`, but you can pass `"finalized"` or `"safe"`. If you used `"earliest"`, you are reading genesis state.
* **Comparator inverted.** `<` vs `>=` typos are surprisingly common. Re-read the test out loud: "the value must be greater than or equal to X."

### "Cannot decode result" from a `ContractCondition`

The function exists but the `outputs` array in your `functionAbi` does not match what the contract actually returns. Double-check against the source contract or a block explorer's verified ABI.

### `JsonCondition` test always fails

Your JSONPath probably does not match. JSONPath is picky:

* Filter expressions need parens: `[?(@.name == "amount")]`, not `[@.name == "amount"]`.
* Array element access uses `[0]`, not `.0`.
* Test your path against the actual JSON in [jsonpath.com](https://jsonpath.com/) before deploying.

### `SigningObjectAbiAttributeCondition` rejects valid calldata

* Function signature must be **exact**, including parameter types — `transfer(address,uint256)` not `transfer(address, uint256)` (no spaces) and not `transfer(address,uint)` (`uint` is invalid; use `uint256`).
* For tuple parameters, use `(type1,type2,...)`. Example from the [Discord tipping bot deep-dive](/for-developers/conditions/discord-tipping-bot-deep-dive): `execute((address,uint256,bytes))`.
* `subIndices` walk into nested structures one level at a time. For a tuple `(address, uint256, bytes)`, `subIndices: [2]` selects the `bytes` field.

### `SequentialCondition` variable not found

You wrote `:varname` but the bound `varName` was `varName` (no leading colon, case-sensitive). The binding uses the bare name; the reference uses `:` + name.

## When all else fails

1. Run [`validate-conditions.ts`](/for-developers/conditions/validating-conditions) and read the full error in `validation-error.txt`.
2. Compare against the closest example in the [cookbook](/for-developers/conditions/cookbook).
3. Paste the error + your condition + the [schema reference URL](https://github.com/nucypher/taco-web/blob/signing-epic/packages/taco/schema-docs/condition-schemas.md) into an LLM. See [Building Conditions with an LLM](/for-developers/conditions/building-with-llms).
4. Test end-to-end against a local testnet (see [Quickstart](/for-developers/quickstart-testnet)) or your own integration.


# TimeCondition

`TimeCondition` is based on evaluating the timestamp (Unix epoch) of the latest block for a target chain.

Here is an example of using `TimeCondition` .

```typescript
import { conditions } from '@nucypher/taco';

const timeCondition = new conditions.base.time.TimeCondition({
  chain: 1,
  returnValueTest: {
    comparator: '>=',
    value: 1701428400,
  },
});

```

`TimeCondition` contains `returnValueTest` which we can use to select a comparison operator `comparator` and the desired block timestamp threshold of `1701428400` (`2023-12-01T11:00:00Z`). In this case, verification won't pass until the latest block timestamp is greater than or equal to `1701428400`.

### Learn more

* [Broken mention](broken://pages/3qufTlyKsrLICoyVGk9J)


# RpcCondition

`RpcCondition` is based on the evaluation of a JSON-RPC call.

Below, we can see an example of using `RpcCondition` to restrict based on a native asset balance.

```typescript
import { conditions } from '@nucypher/taco';

const ownsETH = new conditions.base.rpc.RpcCondition({
  method: 'eth_getBalance',
  parameters: [':userAddress'],
  chain: 1,
  returnValueTest: {
    comparator: '>=',
    value: 1,
  },
});

```

Knowing the [signature of this method](https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_getbalance), we need to pass a wallet address parameter into the `parameters` field (the default block used is `latest`).

```typescript
 method: 'eth_getBalance',
 parameters: [':userAddress'],
```

`':userAddress'` is a reserved [context variable](/for-developers/authentication/conditioncontext-and-context-variables) that denotes the address of the operation requester. Before the attempt is made, the `':userAddress'` value will be replaced with the actual wallet address.

Now that we've specified our contract call, we need to figure out what to do with the contract call results. Let's take a look at the `returnValueTest` field:

```typescript
returnValueTest: {
  comparator: '>=',
  value: 1,
},
```

`returnValueTest` is going to evaluate the contract call result according to the following logic:

* Since the RPC call returns only one value, we don't need to specify the `index` field
* Compare it using the following comparator, `comparator: '>='`
* Compare the value returned by the RPC call to the following value, `value: 1`

Combining these three, we can see that the `returnValueTest` will "trigger" if the value returned by the RPC call is greater than one.

In other words, our condition is only satisfied if `eth_getBalance(:userAddress) >= 1` i.e. only if the user's wallet has a native balance greater than or equal to 1.

### Learn more

* [Broken mention](broken://pages/3qufTlyKsrLICoyVGk9J)


# ContractCondition

`ContractCondition` is based on the evaluation of contract calls.

Below, we can see an example of using `ContractConditon` to gate access to our data behind an NFT ownership condition.

```typescript
import { conditions } from '@nucypher/taco';

const ownsNFT = new conditions.base.contract.ContractCondition({
  method: 'balanceOf',
  parameters: [':userAddress'],
  standardContractType: 'ERC721',
  contractAddress: '0x1e988ba4692e52Bc50b375bcC8585b95c48AaD77',
  chain: 1,
  returnValueTest: {
    comparator: '>',
    value: 0,
  },
});

```

Now, let's break it down step by step.

With `ContractCondition`, we can either use one of the predefined contracts, such as `ERC20` or `ERC721` standards. Alternatively, we can also use [any arbitrary contract calls provided we include `functionAbi`](/for-developers/conditions/contractcondition/use-custom-contract-calls) into our condition.

In the case of our condition, we use a `standardContractType` of `ERC721`.

```typescript
standardContractType: 'ERC721',
```

This contract exposes a number of methods, including [`balanceOf`](https://docs.openzeppelin.com/contracts/5.x/api/token/erc721#IERC721-balanceOf-address-). Knowing the signature of this method, we need to pass one parameter into `parameters`

```typescript
 method: 'balanceOf',
 parameters: [':userAddress'],
```

`':userAddress'` is a reserved [context parameter](https://app.gitbook.com/o/R2meumXNNad4y1B10iL7/s/WosjlL4zUGUMlcMfuSAp/~/changes/274/app-development/threshold-access-control-tac/conditions/context-and-context-parameters) that denotes the address of the recipient that will attempt to decrypt our data. Before the attempt is made, the `':userAddress'` value will be replaced with the actual wallet address.

Now that we've specified our contract call, we need to figure out what to do with the contract call results. Let's take a look at the `returnValueTest` field:

```typescript
returnValueTest: {
  comparator: '>',
  value: 0,
},
```

`returnValueTest` is going to evaluate the contract call result according to the following logic:

* Since the contract call returns only one value, we don't need to specify the `index` field
* Compare it using the following comparator, `comparator: '>'`
* Compare it to the following value, `value: 0`

Combining these three, we can see that the `returnValueTest` will hold true if the value returned by the contract call is greater than zero.

In other words, our condition is only satisfied if `ERC721.balanceOf(:userAddress) > 0` i.e. if the user's wallet has the relevant NFT balance greater than 0.

### Learn more

* [Broken mention](broken://pages/3qufTlyKsrLICoyVGk9J)


# Use custom contract calls

In this section we show how to use implement support for custom contract calls in `ContractCondition`.

Let's start with the following example:

```typescript
import { conditions } from '@nucypher/taco';

const myFunctionAbi: conditions.base.contract.FunctionAbiProps =  {
  name: 'myFunction',
  type: 'function',
  stateMutability: 'view',
  inputs: [
    {
      internalType: 'address',
      name: 'account',
      type: 'address',
    },
    {
      internalType: 'uint256',
      name: 'myCustomParam',
      type: 'uint256',
    },
  ],
  outputs: [
    {
      internalType: 'uint256',
      name: 'someValue',
      type: 'uint256',
    },
  ],
};

const myContractCallCondition = new conditions.base.contract.ContractCondition({
  method: 'myFunction', // `myMethodAbi.name`
  parameters: [':userAddress', ':myCustomParam'], // `myMethodAbi.inputs`
  functionAbi: myFunctionAbi, // Our custom function ABI
  contractAddress: '0x0...1',
  chain: 5,
  returnValueTest: {
    comparator: '>',
    value: 0,
  },
});
```

First, let's take a look at `myFunctionAbi`:

* We define a complete function ABI with `name`, `type`, `stateMutability`, `inputs`, and `outputs`
* With those fields in place, our function shapes up to be defined as `myFunction(address, uint256): uint256`
* Note that `type` field required to be `function` and `stateMutability` to `pure` and `view` to avoid accidentally mutating the contract state

Now, looking at `myContractCallCondition` we can see that:

* We need to pass `myFunction` as `method` and `myFunctionAbi` as `functionAbi` for our contract call to be recognized correctly
* We've mapped our function parameters to `parameters`, so that `address` is represented as `':userAddress'` and `uint256` is represented as `':myCustomParam'`. See [Condition Context and Context Variables](/for-developers/authentication/conditioncontext-and-context-variables) for more details.
* Lastly, the `myFunctionAbi.outputs` will be used by `returnValueTest` to compare with the selected threshold `value`, resulting in a test `myFunctionAbi.outputs[0] > 0`

### Learn more

* See how to implement revocation using custom contract calls [here](/for-developers/conditions/contractcondition/implementing-revocation-via-smart-contract).


# Implement revocation via smart contract

It is possible to implement *Revocation* using Conditions that rely on a function call to a Custom Smart Contract. This allows the handling of revocation to be decentralized and transparent. Here is an example of a smart contract (not suitable for production):

```solidity
pragma solidity 0.8.7;

contract Revocation {

    mapping(address => bool) public isRevoked;

    function revoke(address user) public {
        isRevoked[user] = true;
    }

    function unRevoke(address user) public {
        isRevoked[user] = false;
    }
}
```

And the associated Condition:

```javascript
const revocationCondition = {
  contractAddress: '<DEPLOYED_CONTRACT_ADDRESS>',
  method: 'isRevoked',
  parameters: [':userAddress'],
  functionAbi: {
    inputs: [
      {
        internalType: 'address',
        name: '',
        type: 'address',
      },
    ],
    name: 'isRevoked',
    outputs: [
      {
        internalType: 'bool',
        name: '',
        type: 'bool',
      },
    ],
    stateMutability: 'view',
    type: 'function',
  },
  chain: 1,
  returnValueTest: {
    comparator: '==',
    value: false,
  },
};
```

The condition we have defined calls the `isRevoked` function of the smart contract and passes the user's address. If the call returns `false` (**not** revoked, i.e. granted), then the TACo operation will occur. If the call returns `true` (**is** revoked), then TACo operation will fail.


# JSON Endpoint Conditions

JSON conditions are useful when a data consumer's request is validated, in full or in part, based on data retrieved from an external HTTPS JSON endpoint ([API](/for-developers/conditions/json-endpoint-conditions/jsonapicondition) or [RPC](/for-developers/conditions/json-endpoint-conditions/jsonrpccondition)) that returns data in JSON format. They can be leveraged to integrate external off-chain data into your access control policies, ensuring access decisions are based on dynamic, real-time data retrieved from JSON endpoints.

## Authorization

Some JSON endpoints require tokens to verify authorization. Because an authorization token is specific to the requester, the `authorizationToken` value must always be provided as a [user-defined custom context variable](/for-developers/authentication/conditioncontext-and-context-variables#context-variables) - a placeholder within conditions to be specified at encryption time, and whose value is provided at decryption time via the [Condition Context](/for-developers/authentication/conditioncontext-and-context-variables).

Where applicable, authorization tokens must be provided as context variables for the following reasons:

* **Security:** Hardcoding the token would expose it in the plaintext condition associated with the encrypted data.
* **Flexibility:** Hardcoding ties the token to a specific user rather than the requester.
* **Expiry:** Tokens typically expire, so hard coding would lead to eventual invalidation.

For these reasons, only custom context variables are allowed for the optional `authorizationToken` property.

Additionally, the `authorizationType` can be optionally specified (`Bearer` is the default) to indicate the type of authorization to use as request headers. Possible types are `Bearer`, `Basic`, or `X-API-Key` .

## **Special Considerations**

* **Immutable Endpoint URLs:**\
  JSON conditions rely on hardcoded HTTPS endpoints as part of the condition definition. Once a condition is associated with a piece of ciphertext, the endpoint URL becomes fixed and cannot be modified. This immutability ensures the integrity of the condition but introduces challenges if the endpoint URL becomes unavailable, deprecated, or requires updates.
* **Dependency on External Systems:**\
  Since JSON conditions depend on external HTTPS endpoints, the availability and reliability of these endpoints directly impact the functionality of the condition. If the endpoint experiences downtime, latency issues, or is retired, access control decisions relying on it may fail.
* **Versioning and Compatibility:**\
  Endpoint URLs often correspond to specific API versions or specifications. If the endpoint provider introduces breaking changes, conditions that depend on the outdated version may no longer function correctly.
* **Security Risks:**\
  Hardcoding the endpoint URL makes the system reliant on the endpoint's continued security and trustworthiness. If the endpoint is compromised or ownership changes, the system could be exposed to potential risks.

The above may not be a concern for some situations, so it is important to evaluate your use case carefully.


# JsonApiCondition

The `JsonApiCondition` works by sending an HTTPS GET request to a specified JSON API endpoint, extracting relevant data from the response using a [`JSONPath`](https://goessner.net/articles/JsonPath/) query, and then comparing the extracted value against an expected result.

It is composed of the following properties:

* `endpoint`: the HTTPS URI for the JSON API endpoint that will be queried, e.g.`https://api.example.com/user/status`
* `parameters`*(Optional)*: a key-value mapping of parameter names and values to pass as part of the HTTPS GET request. These parameters will be appended to the URL as query string parameters.
* `query`*(Optional)*: a `JSONPath` query used to extract specific data from the JSON response.
* [`authorizationToken`](/for-developers/conditions/json-endpoint-conditions#authorization) (Optional): A token that will be included in the HTTPS `Authorization` header. It enables the use of endpoints that require OAuth/JWT authorization.
* `authorizationType` (Optional): If an `authorizationToken` is specified, the type specifies the request header information to specify. Can be `Bearer`, `Basic`, or `X-API-Key`.
* [`returnValueTest`](/for-developers/conditions#returnvaluetest): the test to validate the value extracted by the JSONPath query.

**Error Handling**

* If the HTTPS response does not return a status code of `200`, the condition will fail automatically, and access will be denied.
* If the `JSONPath` query is provided but cannot properly extract the desired value, the condition will fail, resulting in access being denied.
* If an invalid `authorizationToken` is provided, the call to the API will fail, causing the condition to fail and access to be denied.

## **Use Cases**

Any JSON REST API endpoint including:

* Social media APIs e.g. Twitter, Facebook
* Communication application APIs e.g. Discord, Telegram
* Oracle endpoints e.g. prices, weather etc.

## Example

```typescript
import { conditions } from '@nucypher/taco';

const firstBookPrice = new conditions.base.jsonApi.JsonApiCondition({
  endpoint: 'https://api.books.com/data',
  parameters: {
    'name': 'It'
  },
  query: '$.store.book[0].price',
  authorizationToken: ":authToken",
  returnValueTest: {
    comparator: "==",
    value: 1
  },
});
```

The condition would be satisfied if the API endpoint returned something analogous to the following:

<pre class="language-json"><code class="lang-json"><strong>{  
</strong>  "store":{
    "book":[
      {
        "name": "It",
        "author": "Stephen King",
        "price": 1
      }
    ]
  }
}
</code></pre>

## Development References

* Client-side:
  * <https://github.com/nucypher/taco-web/pull/550>
  * <https://github.com/nucypher/taco-web/pull/561>
  * <https://github.com/nucypher/taco-web/pull/599>
* Server-side:
  * <https://github.com/nucypher/nucypher/pull/3511>
  * <https://github.com/nucypher/nucypher/pull/3560>


# JsonRpcCondition

The `JsonRpcCondition` is designed for access control decisions that rely on data from an external HTTPS JSON RPC endpoint following the [JSON RPC 2.0 specification](https://www.jsonrpc.org/specification).

The condition operates by sending an HTTPS `POST` request to a specified JSON RPC endpoint using a [`JSONPath`](https://goessner.net/articles/JsonPath/) query to extract relevant data from the response and comparing the extracted value to an expected result.

It is composed of the following properties:

* `endpoint`: the HTTPS URI for the JSON RPC endpoint that will be queried, e.g.`https://api.example.com`
* `method`: the JSON RPC method to be invoked
* `params`*(Optional)*: Parameters for the specified method, provided as either a dictionary or an array.
* `query`*(Optional)*: a `JSONPath` query used to extract specific data from the JSON response. The query is relative to the `result` entry included in the JSON response.
* [`authorizationToken`](/for-developers/conditions/json-endpoint-conditions#authorization) *(Optional)*: A bearer token that will be included in the HTTPS `Authorization` header. It enables the use of endpoints that require OAuth/JWT authorization.
* `authorizationType` (Optional): If an `authorizationToken` is specified, the type specifies the request header information to specify. Can be `Bearer`, `Basic`, or `X-API-Key`.
* [`returnValueTest`](/for-developers/conditions#returnvaluetest): the test to validate the value extracted by the JSONPath query.

**Error Handling**

* If the HTTPS response does not return a status code of `200`, the condition will fail automatically, and access will be denied.
* If the `JSONPath` query is provided but cannot properly extract the desired value, the condition will fail, resulting in access being denied.
* If an invalid `authorizationToken` is provided, the call to the API will fail, causing the condition to fail and access to be denied.

## **Use Cases**

Any JSON RPC 2.0 endpoint including:

* Requests to non-EVM blockchain JSON RPC endpoints e.g. Bitcoin, Solana. *(A separate non-EVM blockchain condition may be added if there is sufficient use in this area)*
* Oracle endpoints e.g. market data, prices

## Example

```typescript
import { conditions } from '@nucypher/taco';

const firstBookPrice = new conditions.base.jsonRpc.JsonRpcCondition({
  endpoint: 'https://math.example.com/',
  method: 'subtract',
  params: [42, 23],
  query: '$.mathresult',
  authorizationToken: ":authToken",
  returnValueTest: {
    comparator: "==",
    value: 19
  },
});
```

The JSON data for the HTTP POST request to the RPC endpoint will look like the following:

```json
{
    "jsonrpc": "2.0",
    "id":1,
    "method": "subtract",
    "params": [
        42,
        23
    ]
}
```

The condition would be satisfied if the JSON RPC endpoint returned something analogous to the following:

```json
{
    "jsonrpc": "2.0",
    "result": {
        "mathresult": 19
    },
    "id": 1
}
```

## Development References

* Client-side:
  * <https://github.com/nucypher/taco-web/pull/606>
* Server-side:
  * <https://github.com/nucypher/nucypher/pull/3571>


# JWTCondition

The `JWTCondition` validates [JSON Web Tokens (JWTs)](https://datatracker.ietf.org/doc/html/rfc7519) against a specified public key. It supports standard JWT claims like expiration time and "not before" time. This condition type enables integration with existing Web2 authentication and authorization services.

The JWT standard's flexibility allows for various use cases, including:

* DRM frameworks and platforms
* Content distribution
* Identity management
* Access-controlled agentic workflows

## Centralization Considerations

In Web2 environments, JWT issuers are typically trusted central authorities. The presence of *centralized issuance* of JWTs does not impact the *decentralized verification* of those JWTs by the TACo network, but it does have trust implications for the system as a whole.\
Conversely, in Web3 settings, TACo is fully compatible with decentralized JWT issuers – for example, those that leverage threshold digital signatures like threshold ECDSA. From a verification perspective, TACo remains agnostic to the token issuing environment or entity.

## Properties

* `jwtToken`: The JWT context variable to be instantiated during decryption with a JWT and validated.
* `publicKey`: A string containing the digital signature public key in PEM format
* `expectedIssuer` (Optional): A string representing the JWT issuer. If provided, it must match the token's [`issuer` claim](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1)

## Error Handling

The condition will fail and access will be denied in the following cases:

* If the JWT is malformed or cannot be parsed
* If the JWT's signature cannot be verified with the provided public key
* If the JWT has expired (when [`exp` (Expiration Time) claim](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.4) is present)
* If the JWT is not yet valid (when [`nbf` (Not Before) claim](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.5) is present)
* If the `expectedIssuer` is provided but doesn't match the JWT's [`iss` (Issuer) claim](https://datatracker.ietf.org/doc/html/rfc7519#section-4.1.1)
* If any required claims specified in the condition are missing from the JWT

## Example

```typescript
import { conditions } from '@nucypher/taco';

const jwtCondition = new conditions.base.jwt.JWTCondition({
  jwtToken: ":authToken", // Context variable for the JWT token
  publicKey: "-----BEGIN PUBLIC KEY-----\nMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA...", // Public key in PEM format
  expectedIssuer: "https://some-jwt-issuer.com", // Optional issuer validation
});

// The condition would be satisfied if the JWT token:
// 1. Has a valid signature verifiable with the provided public key
// 2. Has not expired (if exp claim is present in the JWT)
// 3. Is currently valid (if nbf claim is present in the JWT)
// 4. Was issued by "https://some-jwt-issuer.com" (since expectedIssuer was specified in the condition)
```

## Development References

* Client-side:
  * <https://github.com/nucypher/taco-web/pull/604>
* Server-side:
  * <https://github.com/nucypher/nucypher/pull/3570>


# Signing Object Conditions

{% hint style="warning" %}
TACo Action Control functionality is currently in Alpha and only available in the `DEVNET` environment.
{% endhint %}

Signing Object Conditions are a powerful way to validate 'internal' attributes of a transaction, request or attestation. For example, TACo Action Control nodes can only sign a UserOperation object if the proposed transaction involves whitelisted contract addresses. In general, TACo nodes are able to validate specific fields within signing request objects before collectively generating a threshold signature. Currently, two condition types are supported:

## `SigningObjectAttributeCondition`

Validatation of a **simple attribute**, such as a top-level field in the request object.

It is composed of the following properties:

* `attributeName`: The name of the attribute to inspect.
* `returnValueTest`: the test to validate the value extracted from the attribute of the object to sign.

### Example

Ensure the `callGasLimit` value for a `UserOperation` is less than 100,000 units of gas.

```typescript
import { conditions } from '@nucypher/taco';

const gasLimitRestriction = new conditions.base.signing.SigningObjectAttributeCondition({
  attributeName: 'callGasLimit',
  returnValueTest: {
    comparator: '<',
    value: 100000,
  },
});
```

## `SigningObjectAbiAttributeCondition`

Validation of ABI-encoded call data submitted for threshold signing (eg. a `UserOperation` 's `callData`) using human readable ABI signatures. This is useful when the object to sign represents a function call (e.g. a smart contract interaction) and one wishes to enforce constraints on the function being called and its arguments.

It is composed of the following properties:

* **`attributeName`**: The name of the field in the signing object containing the ABI-encoded data.
* **`abiValidation`**: See [`AbiCallValidation`](#abicallvalidation) below.

### AbiCallValidation

Defines which function signatures are allowed and what validations must apply to their parameters.

Key Fields:

* `allowedAbiCalls` : A mapping of human-readable ABI signatures (e.g., `"transfer(address,uint256)"`) to an array of `AbiParameterValidation` rules. At least one function signature entry must be defined.

### AbiParameterValidation

Specifies how to validate an individual parameter within the ABI-encoded call.

#### Key Fields:

* `parameterIndex`: zero-based index of the parameter to validate.
* `indexWithinTuple` *(Optional)*: If the value at `parameterIndex` is a tuple, the index of a value within the tuple, if applicable.
* `returnValueTest` *(Optional)*: the test to validate the value extracted from the `parameterIndex` / `indexWithinTuple` combination.
* `nestedAbiValidation` *(Optional)*: Recursively apply ABI call validation on the decoded `bytes` payload if applicable.

**Constraints:**

* Only one of `returnValueTest` or `nestedAbiValidation` may be set for a given parameter.
* Ensures correct Solidity types and validates that indices fall within expected bounds.

### **Example**

In this example, the authority wishes to constrain the threshold signing (and therefore execution) of a `UserOperation` for ERC-4337, such that TACo nodes will only produce signatures *if* the UserOps `callData` field is attempting to:\
(1) send an ERC-20 transfer of token at address `0x818E59818647700913Cf83f691dcC1b50Ac4864E` .\
(2) send an amount < `10000000000000000000` WEI.

Note: assume that the initial execution call data of the `UserOperation` for the smart contract wallet uses `execute(address,uint256,bytes)`.

```typescript
import { conditions } from '@nucypher/taco';

const callDataRestriction = new conditions.base.signing.SigningObjectAbiAttributeCondition({
  attributeName: 'callData',
  abiValidation: {
    allowedAbiCalls: {
      'execute(address,uint256,bytes)': [
        // check address of ERC-20 contract
        {
          parameterIndex: 0,
          returnValueTest: {
            comparator: '==',
            value: '0x818E59818647700913Cf83f691dcC1b50Ac4864E',
          },
        },
        // check bytes used for call to be made on ERC-20 contract
        {
          parameterIndex: 2,
          nestedAbiValidation: {
            allowedAbiCalls: {
              'transfer(address,uint256)': [
                {
                  parameterIndex: 1,
                  returnValueTest: {
                    comparator: '<',
                    value: 10000000000000000000,
                  },
                },
              ],
            },
          },
        },
      ],
    },
  },
});
```

## Implementation References

* Client-side:
  * <https://github.com/nucypher/taco-web/pull/670>
* Server-side:
  * <https://github.com/nucypher/nucypher/pull/3606>
  * <https://github.com/nucypher/nucypher/pull/3609>


# Logical Conditions

Logical conditions enable developers to construct powerful, flexible policies by combining multiple conditions into structured logic flows that can adapt to a wide range of scenarios. By composing conditions with logical operators and control structures, developers can define precise rules that govern who is allowed, under what conditions, and in what sequence—enabling dynamic, context-aware authorization decisions.


# CompoundCondition

`CompoundConditon` allow conditions to be combined into more complex logical statements. The logical operators allowed are `or`, `and`, and `not`.

## `or`/`and` operators

`or` & `and` operators require >= 2 operands to be specified.

Let's take a look at this abbreviated condition example:

```typescript
import { conditions } from '@nucypher/taco';

const conditionA = ...
const conditionB = ...

const aAndB = new conditions.compound.CompoundCondition({
  operator: 'and',
  operands: [conditionA, conditionB],
});
```

Here, we define two conditions `conditionA` and `conditionB`, and combine them into a single condition using `and` operator. So now, our new condition will "trigger" only when both `conditionA` *and* `conditionB` "trigger".

We can use two or more conditions when using `and` and `or` operators:

```typescript
const allMustPass = new conditions.compound.CompoundCondition({
  operator: 'and',
  operands: [conditionA, conditionB, ..., conditionX],
});

const anyOneMustPass = new conditions.compound.CompoundCondition({
  operator: 'or',
  operands: [conditionA, conditionB, ..., conditionX],
});

```

Alternatively, we can use `CompoundCondition.or` and `CompoundCondition.and` short-hand methods

```typescript
const allMustMatch = new conditions.compound.CompoundCondition.and([
    conditionA, conditionB, ..., conditionX],
]);

const atLeastOneMustMatch = new conditions.compound.CompoundCondition.or([
    conditionA, conditionB, ..., conditionX],
]);
```

## `not` operator

The `not` operator only allows one operand.

For example:

```typescript
import { conditions } from '@nucypher/taco';

const conditionA = ...

const notA = new conditions.compound.CompoundCondition({
  operator: 'not',
  operands: [conditionA],
});
```

## Nested Combinations

`CompoundCondition` objects can be nested on itself i.e. you can create compound conditions of compound conditions.

For example:

```typescript
import { conditions } from '@nucypher/taco';

const conditionA = ...
const conditionB = ...
const conditionC = ...
const conditionD = ...
const conditionE = ...

const cOrD = new conditions.compound.CompoundCondition({
  operator: 'or',
  operands: [conditionC, conditionD],
});

const notE = new conditions.compound.CompoundCondition({
  operator: 'not',
  operands: [conditionE],
});

// conditionA AND conditionB AND cOrD AND notE
overallCondition = new conditions.compound.CompoundCondition({
  operator: 'and',
  operands: [conditionA, conditionB, cOrD, notE],
});
```

{% hint style="info" %}
Individual conditions within the `CompoundCondition`can use different chain IDs as long as the chain IDs are supported by the `domain` network being used.
{% endhint %}

## Learn more

* [Broken mention](broken://pages/3qufTlyKsrLICoyVGk9J)


# IfThenElseCondition

The `IfThenElseCondition` allows for conditional branching, where different conditions are executed based on the outcome of an initial "if" condition. It operates with a simple "if-then-else" structure and provides flexibility in determining the behaviour when a condition is true or false.

A condition that allows for if-then-else branching based on underlying conditions\
i.e. **IF** `CONDITION_A` **THEN** `CONDITION_B` **ELSE** `CONDITION_C`.

It is composed of:

* `ifCondition`: A required field that specifies the condition to be evaluated first. If the condition evaluates to `true`, the `thenCondition` is executed; if it evaluates to `false`, the `elseCondition` is executed.
* `thenCondition`: A required field that specifies the action or condition to execute if the `ifCondition` evaluates to `true`.
* `elseCondition`: A required field that specifies the action or condition to execute if the `ifCondition` evaluates to `false`. This can be a `CONDITION`or a boolean value (`true` or `false`), specifying the result when the `ifCondition` is `false`. This gives users control over what the overall condition returns when the `ifCondition` is false.

When combined with other conditions (e.g., `SequentialCondition`, `CompoundCondition`), the `IfThenElseCondition` allows for complex, branching workflows.

## Examples

### IF `conditionA` then `conditionB` else `conditionC`

```typescript
import { conditions } from '@nucypher/taco';

const conditionA = ...
const conditionB = ...
const conditionC = ...

const condition = new conditions.ifThenElse.IfThenElseCondition({
  ifCondition: conditionA,
  thenCondition: conditionB,
  elseCondition: conditionC,
});
```

### IF `conditionA` then `conditionB` else True

```typescript
import { conditions } from '@nucypher/taco';

const conditionA = ...
const conditionB = ...

const condition = new conditions.ifThenElse.IfThenElseCondition({
  ifCondition: conditionA,
  thenCondition: conditionB,
  elseCondition: true,
});
```

### Nested `IfThenElseCondition`

```typescript
import { conditions } from '@nucypher/taco';

const conditionA = ...
const conditionB = ...
const conditionC = ...
const conditionD = ...

const nestedIfThenElseCondition = new conditions.ifThenElse.IfThenElseCondition({
  ifCondition: conditionA,
  thenCondition: conditionB,
  elseCondition: false,
});

const condition = new conditions.ifThenElse.IfThenElseCondition({
  ifCondition: conditionC,
  thenCondition: conditionD,
  elseCondition: nestedIfThenElseCondition,
});
```

{% hint style="info" %}
Individual conditions within the `IfThenElseCondition` can use different chain IDs as long as the chain IDs are supported by the `domain` network being used.
{% endhint %}

## Development References

* Client-side: <https://github.com/nucypher/taco-web/pull/593>
* Server-side: <https://github.com/nucypher/nucypher/pull/3558>


# SequentialCondition

Sequential conditions allow the execution of multiple conditions in order, where the outcome of one condition execution can be used by subsequent conditions. This concept is especially useful in workflows where intermediate results influence subsequent steps.

Each condition is evaluated in sequence, and if any condition in the sequence fails, the overall condition is considered as having failed.

## ConditionVariable

A sequential condition is comprised of a list of `ConditionVariable` objects.

The `ConditionVariable` object represents a condition whose result is tied to a specific variable name, which stores the result of the execution of the condition. This variable name can be referenced by subsequent conditions in the sequence.

`ConditionVariables` have two mandatory attributes:

* `varName`: The name of the variable used to store the execution result of the condition.
* `condition`: Any [`Condition`](/for-developers/conditions) to evaluate, and includes allows nesting of a `SequentialCondition` within itself.

For example:

<pre class="language-typescript"><code class="lang-typescript">import { conditions } from '@nucypher/taco';

const timeCondition = new conditions.base.time.TimeCondition({
<strong>  chain: 1,
</strong>  returnValueTest: {
    comparator: '>=',
    value: 1701428400,
  },
});

const conditionVariable = {
<strong>  varName: 'timeValue',
</strong>  condition: timeCondition,
};
</code></pre>

In this case, the condition variable will store the current block time obtained and used by the `timeCondition` in the `timeValue` variable. Variable names are considered [Custom Context Variables](/for-developers/authentication/conditioncontext-and-context-variables#context-variables) and can be referenced by using the string `:<varName>` i.e. `:timeValue` from this example, in subsequent conditions.

## Example

<pre class="language-typescript"><code class="lang-typescript">import { conditions } from '@nucypher/taco';

// first condition variable
const conditionA = new conditions.base.contract.ContractCondition({
  contractAddress: '0x1e988106FCe9647Bdf1E7877BF73cE8B0BAD5f97',
  method: 'methodForA',
  parameters: [':userAddress'],
  functionAbi: ...,
  chain: 1,
  returnValueTest: {
    comparator: '!=',
    value: "0x",
  },
});
<strong>const conditionVariableA = {
</strong>    varName: 'value_a',
    condition: conditionA,
};

// second condition variable
const conditionB = new conditions.base.contract.ContractCondition({
  contractAddress: '0x4838B106FCe9647Bdf1E7877BF73cE8Bc48AaD77',
  method: 'methodForB',
  parameters: [':value_a'],
  functionAbi: ...,
  chain: 1,
  returnValueTest: {
    comparator: '>',
    value: 0,
  },
});
const conditionVariableB = {
    varName: 'value_b',
    condition: conditionB,
};

// sequential condition
const sequentialCondition = new conditions.sequential.SequentialCondition({
  conditionVariables: [
     conditionVariableA,
     conditionVariableB,
  ],
});
</code></pre>

In this example, the sequential condition is evaluated as follows:

1. `conditionVariableA` is assessed by executing `conditionA`, which is a [`ContractCondition`](/for-developers/conditions/contractcondition).
   * `conditionA` will invoke `methodForA` on the relevant contract, passing the validated user address as an argument.
   * The result of this function is then checked to ensure it is not an empty byte.
   * If `conditionA` fails, the sequential condition is immediately marked as failed, and no further conditions are evaluated. If it succeeds, the result is stored as `:value_a`, and the evaluation proceeds to `conditionVariableB`.
2. `conditionVariableB`, also a [`ContractCondition`](/for-developers/conditions/contractcondition), invokes `methodForB` on the relevant contract, using the output from `conditionA` (`:value_a`) as the argument.
   * The result is verified to ensure it is not equal to 0.
   * If `conditionB` fails, the entire sequential condition is marked as failed. If it passes, the result is stored as `:value_b`.

Since no further condition variables remain and both `conditionA` and `conditionB` have passed, the sequential condition is considered satisfied.

{% hint style="info" %}
Individual conditions within the `SequentialCondition` can use different chain IDs as long as the chain IDs are supported by the `domain` network being used.
{% endhint %}

## Development References

* Client-side: <https://github.com/nucypher/taco-web/pull/581>
* Server-side: <https://github.com/nucypher/nucypher/pull/3500>


# Authentication

Some dynamic access conditions require specific information about the *data consumer,* which needs to be verified/authenticated, such as wallet address or other identity-related information. This verification needs to be done in a way that doesn't allow the *data consumer* to simply provide ***ANY*** value. Instead, the *data consumer* should provide proof that can be verified so that the validity of the value can be confirmed and the value subsequently used for properly evaluating access.

In the case of a wallet address, the *data consumer* must sign a message with the private key corresponding to the public wallet address. This signature serves as proof of ownership, which nodes can then verify before using the corresponding wallet address for decryption condition evaluation. Otherwise, the *data consumer* could specify a wallet address they do not own but still satisfy the required condition, e.g. pretend to own `vitalik.eth` to satisfy an ETH balance condition.

`AuthProvider` is an abstraction provided by `@nucypher/taco-auth` that plays a critical role in generating the necessary proof for authenticating information about the *data consumer*. This proof is then validated as a part of condition evaluation during the decryption process. Instead of directly providing the necessary information (e.g., wallet address), which could be falsified, the *data consumer* uses an `AuthProvider` to generate the requisite proof.

At the moment, [Sign-In With Ethereum (SIWE)](https://docs.login.xyz/general-information/siwe-overview/eip-4361) and [Standard Signature Validation Method for Contracts (EIP-1271)](https://eips.ethereum.org/EIPS/eip-1271) are supported for proof of wallet ownership with more authentication protocols expected to be added in the future.

For more information on specific authentication providers and how they can be utilized alongside access conditions, see [Condition Context](/for-developers/authentication/conditioncontext-and-context-variables).


# Condition Context

A `ConditionContext` is a container for dynamic values to be used in conditions at verification time. A `ConditionContext` contains `ContextVariables` and their corresponding values.

## Context Variables

Context variables can be recognized by the `:` prefix. They act as placeholders within conditions to be specified at the condition definition, whose value is provided at verifiction time.

Context variables are broken up into two groups:

* ***Reserved context variables*****:** these have special functionality within `taco`:

  * [`:userAddress`](#useraddress)

  which requires the use of [Authentication Providers](/for-developers/authentication) to provide verifiable proof of wallet address ownership specified by the *data consumer*. Applications should be cognizant of which reserved context variable they use based on their needs.
* ***Custom context variables*****:** these are application-specific, simple key-value pairs where the *data consumer* can directly specify the values without any verification.

### `:userAddress`

Whenever the `:userAddress` context variable is present in a condition, the *requester* must use one of the following authentication providers for proof of wallet ownership:

* `EIP4361AuthProvider`: Prompts the user to sign an EIP-4361 ([Sign-in With Ethereum](https://docs.login.xyz/general-information/siwe-overview/eip-4361)) message to authenticate the data consumer's wallet address at verification time.

{% hint style="info" %}
To negate the need for repeated wallet signatures for every execution request by the same *requester* the corresponding proof that is generated is cached until an expiry is triggered, after which the *requester* will be prompted again.
{% endhint %}

* `SingleSignOnEIP4361AuthProvider` : Designed for applications that have already integrated Sign-in With Ethereum (SIWE), this provider leverages the existing login signature and message. It enables users to authenticate once with the application and seamlessly reuse that authentication with TACo, eliminating the need to sign multiple messages during execution.

{% hint style="warning" %}
TACo requires that Sign-In With Ethereum (SIWE) messages be issued within the last 2 hours based on the "Issued At" timestamp. For single sign-on usage, the application should refresh the user's cached SIWE login accordingly.
{% endhint %}

* `EIP1271AuthProvider`: Intended for scenarios where the user account is a smart contract wallet that implements [EIP-1271](https://eips.ethereum.org/EIPS/eip-1271).

This signature produced by the authentication providers are provided to nodes to prove wallet address ownership when evaluating conditions.

## Illustrative Example

Let's take a look at this `ContractConditon` example:

```typescript
import { conditions } from '@nucypher/taco';

const ownsNFTRaw = new conditions.base.contract.ContractCondition({
  method: 'balanceOf',
  parameters: [':userAddress'], // <- A context variable
  standardContractType: 'ERC721',
  contractAddress: '0x1e988ba4692e52Bc50b375bcC8585b95c48AaD77',
  chain: 1,
  returnValueTest: {
    comparator: '>',
    value: ':selectedBalance', // <- Another context variable
  },
});
```

In this example, we can see two different context variables

* `:userAddress` - A reserved context variable
* `:selectedBalance` - A custom context variable

To replace the `:userAddress` context variable with an actual wallet address during verification, TACo needs to be provided with an `AuthProvider` to confirm wallet ownership at the condition verification time.

Additionally, the `:selectedBalance` custom context variable has to be provided to the TACo operation by the *requester*.

Both context variables need to be provided to a `ConditionContext` which is then used by the node during verification.

```typescript
import { conditions } from '@nucypher/taco';
import { SingleSignOnEIP4361AuthProvider, USER_ADDRESS_PARAM_DEFAULT } from '@nucypher/taco-auth';

const web3Provider = new ethers.providers.Web3Provider(window.ethereum);

const conditionContext =
  conditions.context.ConditionContext.fromMessageKit(messageKit);
  
// satisfy ":userAddress"
const {messageStr, signature} = ...;  // existing SIWE information from application  
const authProvider = SingleSignOnEIP4361AuthProvider.fromExistingSiweInfo(
  messageStr,
  signature,
);
conditionContext.addAuthProvider(USER_ADDRESS_PARAM_DEFAULT, authProvider);

// satisfy ":selectedBalance"; simple key-value pair
const customParameters: Record<string, conditions.CustomContextParam> = {
  ':selectedBalance': 2,
};
conditionContext.addCustomContextParameterValues(customParameters);

// TACo operation
```

With those context parameters, the condition will be evaluated by nodes at verification time to be:

```typescript
{
  method: 'balanceOf',
  parameters: ['0x...'], // The address of requester's wallet (verified via signature)
  standardContractType: 'ERC721',
  contractAddress: '0x1e988ba4692e52Bc50b375bcC8585b95c48AaD77',
  chain: 1,
  returnValueTest: {
    comparator: '>',
    value: 2, // Concrete value to be used in comparison
  },
}
```

{% hint style="info" %}
This is a contrived example.\
\
For **custom context variables** specifically, time should be taken to think through the use case since the *data consumer* provides these and can be set to any value. In this case, the `selectedBalance` value can simply be set to `-1` by the requester, which would grant them access without their owning any NFT.\
\
Custom context variables, such as providing a Merkle tree root as a parameter to a contract function, are appropriate, but such an example would be overly complex.
{% endhint %}

## Checking for required context variables

If your application utilizes many different conditions each with different context variables, the required context variables for verification can be queried.

The `requestedContextParameters` property of the `ConditionContext` object can be used to identify the necessary context variables for verification. By querying this property, the application can understand the context variables required for the relevant condition.

In this way, the application doesn't need prior knowledge of the condition used but can dynamically determine the context variables needed based on the specific condition being handled.

```typescript
// create condition context
const conditionContext = conditions.context.ConditionContext.fromMessageKit(messageKit);

// illustrative optional example of checking what context parameters are required
if (
  conditionContext.requestedContextParameters.has(USER_ADDRESS_PARAM_DEFAULT)
) {
  // add authentication for ":userAddress" in condition
  const authProvider = ...;
  conditionContext.addAuthProvider(USER_ADDRESS_PARAM_DEFAULT, authProvider);
}
if (
  conditionContext.requestedContextParameters.has(":selectedBalance")
) {  
  // satisfy ":selectedBalance"; simple key-value pair
  const customParameters: Record<string, conditions.CustomContextParam> = {
    ':selectedBalance': 2,
  };
  conditionContext.addCustomContextParameterValues(customParameters);
}
// other context variable checks ...
```

## Learn more

* [Broken mention](broken://pages/3qufTlyKsrLICoyVGk9J)


# Integration Guide

The TACo Access Control SDK allows you to use threshold encryption & decryption in your application.

{% hint style="warning" %}
Threshold Access Control is not currently supported by an stable cohort of node operators running TACo clients, nor the surrounding infrastructure to make mainnet access straightforward (e.g. [Porter](/reference/architecture/porter)). The network will be relaunched by WEDF in Q3 2026..&#x20;

Until then, this page serves solely as a open source reference and blueprint for the community.&#x20;
{% endhint %}

In just a few minutes you will able to:

* **Define decryption conditions** – these are predefined rules or criteria that must be fulfilled before the encrypted data can be decrypted.
* **Encrypt data & assign decryption conditions** – when you encrypt data, you not only secure it but also tie the decryption process to the conditions you defined.
* **Threshold-decrypt data** – once the decryption conditions are met and validated by a threshold of TACo nodes, decryption can occur.

### 1. Installation

Install `taco` , `taco-auth`, and `ethers` with your favorite package manager:

<pre class="language-bash"><code class="lang-bash"><strong>$ npm install @nucypher/taco @nucypher/taco-auth ethers@5.7.2
</strong></code></pre>

{% hint style="warning" %}
TACo currently requires **ethers v5**. The API is not compatible with ethers v6 (`ethers.BrowserProvider`, `ethers.JsonRpcProvider` without the `providers` namespace, etc.).
{% endhint %}

### 2. Configuration

To run the code examples below, you will need the `ritualId` encryption parameter. **In production**, your wallet address (encryptor) will also have to be allow-listed for this specific ritual. Please reach out to us [here](https://discord.com/channels/411401661714792449/1344417143659171991) to receive a `ritualId` and allow-list access.\
\
Additionally, we have [publicly available testnet rituals](/for-developers/quickstart-testnet/get-started-with-tac#threshold-decryption) for use when developing your apps.

### 3. Define decryption condition and encrypt data

With `ritualId` and [a web3 provider from `ethers`](https://docs.ethers.org/v5/api/providers/#providers-getDefaultProvider), we can `taco.encrypt` our data.

In this example, we will use our [`lynx` testnet](/for-developers/quickstart-testnet/get-started-with-tac#threshold-decryption), where you can freely use `ritualId = 27`.

{% hint style="info" %}
The `polygonProvider` below connects to **Polygon Amoy** — this is where TACo's DKG coordination contracts live. It is **not** your application's chain provider. Your conditions can still target any supported EVM chain (Ethereum, Sepolia, etc.) regardless of this provider.
{% endhint %}

The `signerProvider` is required to [authenticate](/for-developers/authentication) the Encryptor.

<pre class="language-typescript"><code class="lang-typescript">import { initialize, encrypt, conditions, domains } from '@nucypher/taco';
import { ethers } from "ethers";

// We have to initialize the TACo library first
await initialize();

// Define decryption condition
const ownsNFT = new conditions.predefined.erc721.ERC721Ownership({
  contractAddress: '0x1e988ba4692e52Bc50b375bcC8585b95c48AaD77',
  parameters: [3591],
  chain: 11155111,  // sepolia
});

const signerProvider = new ethers.providers.Web3Provider(window.ethereum);
const polygonProvider = new ethers.providers.JsonRpcProvider("https://polygon-amoy.drpc.org");

const message = "my secret message";
const ritualId = 27

// encrypt data
const messageKit = await encrypt(
<strong>  polygonProvider,
</strong><strong>  domains.DEVNET,
</strong>  message,
  ownsNFT,
  ritualId,
  signerProvider.getSigner() 
);
</code></pre>

### 4. Decrypt the data

Now we just have to pass the `messageKit` to the intended *data consumer*:

<pre class="language-typescript"><code class="lang-typescript"><strong>import { conditions, decrypt, domains, initialize } from '@nucypher/taco';
</strong>import { EIP4361AuthProvider, USER_ADDRESS_PARAM_DEFAULT } from '@nucypher/taco-auth';
import { ethers } from "ethers";

// We have to initialize the TACo library first
await initialize();

const web3Provider = new ethers.providers.Web3Provider(window.ethereum); 

const conditionContext =
  conditions.context.ConditionContext.fromMessageKit(messageKit);
  
// auth provider when condition contains ":userAddress" context variable
// the decryptor user must provide a signature to prove ownership of their wallet address
const authProvider = new EIP4361AuthProvider(
  web3Provider,
  web3Provider.getSigner(),
);
conditionContext.addAuthProvider(USER_ADDRESS_PARAM_DEFAULT, authProvider);

const decryptedMessage = await decrypt(
  web3Provider,
  domains.DEVNET,
  messageKit,
  conditionContext,
);
</code></pre>

Since `ownsNFT` condition refers to an NFT owned by the *data consumer*, `decrypt` call will prompt the recipient to sign a message and prove the ownership of the caller's wallet.

### Next steps

Learn more about using TACo in a sandboxed environment in the [Testnet](/for-developers/quickstart-testnet/get-started-with-tac) section.

### Example applications

The following samples showcase integrations with React-based web apps, and serve as an 'end-to-end' reference for creating conditions-based encryption & decryption:

* [`taco-web/demos`](https://github.com/nucypher/taco-web/tree/main/demos)
* [`taco-web/examples/taco`](https://github.com/nucypher/taco-web/tree/main/examples/taco)


# Testnet

{% hint style="warning" %}
Threshold Access Control is not currently supported by active/available infrastructure, including testnets. The network will be relaunched by WEDF in Q3 2026.&#x20;

Until then, this page serves solely as a open source reference and blueprint for the community.&#x20;
{% endhint %}

## Testnet domains

To run TACo on testnet, it needs to be configured to use one of the two available domains:

```typescript
import { domains } from '@nucypher/taco';

domains.DEVNET  // "lynx" network
```

* `DEVNET` domain, or `lynx`, is a bleeding-edge developer network that supports the upcoming `taco` release.

{% hint style="warning" %}
`DEVNET` is unsuitable for use in a production setting since it does not have any trust minimization or stability guarantees, which makes it unfit for production or real-world data payloads. Learn more about this in the trust assumptions [section](broken://pages/WugTyY8VT0l9LBPeY79b).
{% endhint %}

## Quick reference

Copy-paste these values to get started immediately. These examples use **ethers v5** (`npm install ethers@5.7.2`), which TACo currently requires.

{% tabs %}
{% tab title="Lynx (bleeding-edge)" %}

```typescript
import { domains } from '@nucypher/taco';
import { ethers } from 'ethers';

const domain = domains.DEVNET;
const ritualId = 27;  // Open ritual, no encryptor allowlist needed

// Provider must connect to Polygon Amoy (where DKG contracts live)
const provider = new ethers.providers.JsonRpcProvider(
  'https://polygon-amoy.drpc.org'
);
```

{% endtab %}
{% endtabs %}

{% hint style="warning" %}
The `provider` configured above reads DKG coordination contracts on the L2 chain (Polygon Amoy for testnet, Polygon for mainnet). It is required by `encrypt()` and can also be used with `decrypt()`. In browser contexts, `decrypt()` typically uses a `Web3Provider` connected to the user's wallet instead. Neither provider determines which chains your conditions can target — conditions can reference any supported EVM chain.
{% endhint %}

## Testnet configuration

### Threshold Decryption

<table><thead><tr><th width="111.73046875">Domain</th><th width="104.45703125">Network</th><th width="97.421875">L1</th><th width="149.21875">L2</th><th width="132.953125" data-type="number">Open Ritual ID</th><th>Cohort</th></tr></thead><tbody><tr><td>DEVNET</td><td><a href="https://lynx-3.nucypher.network:9151/status">lynx</a></td><td>Sepolia<br>(11155111)</td><td>Amoy (80002)</td><td>27</td><td>2-of-3</td></tr></tbody></table>

{% hint style="info" %}
**Open Ritual ID** refers to a DKG cohort & public key with no restrictions on encryptors – i.e. any device or address can use the public key to encrypt data. See the [encryptor allowlist](/for-developers/quickstart-testnet/encryptor-allowlist) section to learn more.
{% endhint %}

{% hint style="info" %}
It should be noted that the blockchains used as L1 and L2 in the various TACo domains (`DEVNET`, `MAINNET`) do not determine the blockchains supported by the TACo conditions. For example, the `DEVNET` can be used to define conditions on Ethereum mainnet.
{% endhint %}

### Threshold Signing

<table><thead><tr><th width="102.328125">Domain</th><th width="110.19921875">Network</th><th width="92.296875">L1</th><th width="150.02734375" data-type="number">Open Cohort ID</th><th width="102.4609375">Cohort</th><th>Supported Chains</th></tr></thead><tbody><tr><td>DEVNET</td><td><a href="https://lynx-3.nucypher.network:9151/status">lynx</a></td><td>Sepolia<br>(11155111)</td><td>1</td><td>2-of-3</td><td><ul><li>84532 (Base)</li><li>11155111 (Sepolia)</li></ul></td></tr></tbody></table>

{% hint style="info" %}
**Open Cohort ID** refers to a signing cohort with a policy condition that always passes and therefore there are no restrictions on what it will sign – i.e. any signing request will be fulfilled.
{% endhint %}

## Contracts

The source code for contracts used on testnet can be found in [`nucypher/nucypher-contracts`](https://github.com/nucypher/nucypher-contracts) repository.

Contract addresses for testnet can be found in their respective contract registries:

* [`lynx.json`](https://github.com/nucypher/nucypher-contracts/blob/main/deployment/artifacts/lynx.json)


# Quickstart — Node.js (Testnet)

Encrypt and decrypt data with TACo in a Node.js environment — no browser or wallet extension required.

{% hint style="warning" %}
Threshold Access Control is not currently supported by active/available infrastructure. The network will be relaunched by WEDF in Q3 2026.&#x20;

Until then, this page serves solely as a open source reference and blueprint for the community.&#x20;
{% endhint %}

This guide walks through a complete encrypt → decrypt flow in Node.js. Unlike the browser quickstart, there's no MetaMask or `window.ethereum` — we use private keys and `JsonRpcProvider` directly.

{% hint style="info" %}
A full working example lives in the taco-web repo: [`examples/taco/nodejs`](https://github.com/nucypher/taco-web/tree/main/examples/taco/nodejs)
{% endhint %}

{% stepper %}
{% step %}

### Install dependencies

```bash
npm install @nucypher/taco @nucypher/taco-auth ethers@5.7.2
```

{% hint style="warning" %}
TACo currently requires **ethers v5**. The API is not compatible with ethers v6. If your project uses ethers v6, you must use `ethers@5.7.2` for TACo integration.
{% endhint %}
{% endstep %}

{% step %}

### Configure provider, domain, and ritual ID

You'll need three things:

| Parameter        | Value                       | Notes                                                                                                                        |
| ---------------- | --------------------------- | ---------------------------------------------------------------------------------------------------------------------------- |
| **RPC URL**      | A Polygon Amoy RPC endpoint | This reads DKG coordination contracts — it's **not** your application's chain. Any public Amoy RPC works.                    |
| **Ritual ID**    | `27` (for LYNX testnet)     | See the [testnet](/for-developers/quickstart-testnet/get-started-with-tac#threshold-decryption) page for other environments. |
| **Private keys** | Two test wallets            | One for the encryptor, one for the consumer. Any Ethereum wallets — no testnet funds required.                               |

```typescript
import { ethers } from 'ethers';
import { domains } from '@nucypher/taco';

// This provider reads DKG coordination contracts on Polygon Amoy.
// It is NOT your application's RPC — it's infrastructure.
const provider = new ethers.providers.JsonRpcProvider(
  'https://polygon-amoy.drpc.org'
);

const domain = domains.DEVNET;  // "lynx"
const ritualId = 27;
```

{% endstep %}

{% step %}

### Encrypt data

```typescript
import { conditions, encrypt, initialize } from '@nucypher/taco';
import { ethers } from 'ethers';

await initialize();

// Any condition works — here's a simple balance check
const hasBalance = new conditions.base.rpc.RpcCondition({
  chain: 80002,  // Polygon Amoy — the chain where this condition is evaluated (can be any supported chain)
  method: 'eth_getBalance',
  parameters: [':userAddress', 'latest'],
  returnValueTest: {
    comparator: '>=',
    value: 0,
  },
});

const encryptorSigner = new ethers.Wallet('<ENCRYPTOR_PRIVATE_KEY>');

const messageKit = await encrypt(
  provider,
  domain,
  'my secret message',
  hasBalance,
  ritualId,
  encryptorSigner,
);

// Serialize for storage or transmission
const encryptedBytes = messageKit.toBytes();
```

{% hint style="warning" %}
**Do not JSON-serialize `ThresholdMessageKit` directly** — it uses a custom binary format. Always use `.toBytes()` for serialization and `ThresholdMessageKit.fromBytes()` for deserialization. If you need to store it in JSON, encode the bytes as base64 or hex first.
{% endhint %}
{% endstep %}

{% step %}

### Decrypt data

```typescript
import {
  ThresholdMessageKit,
  conditions,
  decrypt,
  initialize,
} from '@nucypher/taco';
import {
  EIP4361AuthProvider,
  USER_ADDRESS_PARAM_DEFAULT,
} from '@nucypher/taco-auth';
import { ethers } from 'ethers';

await initialize();

// Deserialize
const messageKit = ThresholdMessageKit.fromBytes(encryptedBytes);

const consumerSigner = new ethers.Wallet('<CONSUMER_PRIVATE_KEY>');

// Build condition context
const conditionContext =
  conditions.context.ConditionContext.fromMessageKit(messageKit);

// In Node.js, EIP4361AuthProvider needs explicit SIWE parameters
// (in the browser, these come from the page origin)
const authProvider = new EIP4361AuthProvider(
  provider,
  consumerSigner,
  { domain: 'localhost', uri: 'http://localhost:3000' },
);
conditionContext.addAuthProvider(USER_ADDRESS_PARAM_DEFAULT, authProvider);

const decryptedBytes = await decrypt(
  provider,
  domain,
  messageKit,
  conditionContext,
);

const decryptedMessage = new TextDecoder().decode(decryptedBytes);
console.log(decryptedMessage); // "my secret message"
```

{% endstep %}
{% endstepper %}

## Key differences from browser usage

{% tabs %}
{% tab title="Provider" %}
**Browser:**

```typescript
const provider = new ethers.providers.Web3Provider(window.ethereum);
```

**Node.js:**

```typescript
const provider = new ethers.providers.JsonRpcProvider('https://polygon-amoy.drpc.org');
```

{% endtab %}

{% tab title="Signer" %}
**Browser:**

```typescript
const signer = provider.getSigner(); // MetaMask popup
```

**Node.js:**

```typescript
const signer = new ethers.Wallet('<PRIVATE_KEY>');
```

{% endtab %}

{% tab title="SIWE Auth" %}
**Browser:** Domain and URI inferred from page origin.

```typescript
const authProvider = new EIP4361AuthProvider(provider, signer);
```

**Node.js:** Must pass `{ domain, uri }` explicitly.

```typescript
const authProvider = new EIP4361AuthProvider(provider, signer, {
  domain: 'localhost',
  uri: 'http://localhost:3000',
});
```

{% endtab %}

{% tab title="Serialization" %}
**Browser:** `messageKit` often stays in memory.

**Node.js:** Use binary serialization for storage or transmission.

```typescript
const bytes = messageKit.toBytes();
const restored = ThresholdMessageKit.fromBytes(bytes);
```

{% endtab %}
{% endtabs %}

## Next steps

* Browse all [condition types](/for-developers/conditions) to define more complex access rules
* See the [full Node.js example](https://github.com/nucypher/taco-web/tree/main/examples/taco/nodejs) in the taco-web repo
* Review [testnet](/for-developers/quickstart-testnet/get-started-with-tac) for ritual IDs and network configuration


# Integrate Into Apps

{% hint style="warning" %}
Note that using TACo in production requires a unique DKG initialization ritual, unique DKG public key, and a unique cohort of nodes running TACo software. These are available via a unique `ritualID` parameter.&#x20;

Threshold Access Control is not currently supported by an stable cohort of node operators running TACo clients, nor the surrounding infrastructure to make mainnet access straightforward (e.g. [Porter](/reference/architecture/porter)). The network will be relaunched by WEDF in Q3 2026.

Until then, this page serves solely as a open source reference and blueprint for the community.
{% endhint %}

### 0. Pick an appropriate `taco` domain

Before we install `taco`, we need to consider which domain we would like to use:

* `MAINNET` (`mainnet` network) - production environment
* `DEVNET` (`lynx` network) - bleeding-edge testnet used for internal development and future features

{% hint style="info" %}
`lynx` is the development testnet.
{% endhint %}

Once you've picked a network, install `@nucypher/taco` from [npm.js](https://www.npmjs.com/package/@nucypher/taco?activeTab=versions) with the appropriate tag based on the chosen network. To find the appropriate version, refer to the "Tags" column in the "Current Tags" section.

You can learn more about the current state of `mainnet` and test networks here:

* [Broken mention](broken://pages/XpbH0VyXQYA9sdAJWhyQ)
* [Testnet](/for-developers/quickstart-testnet/get-started-with-tac)

### 1. Install and integrate `taco`

To begin, we need to install the `taco` , and `taco-auth`libraries:

```bash
yarn add @nucypher/taco @nucypher/taco-auth
```

For this guide, we'll need a few extra packages:

```bash
yarn add ethers@5.7.2 @metamask/detect-provider
```

{% hint style="warning" %}
TACo currently requires **ethers v5**. The API is not compatible with ethers v6.
{% endhint %}

To use `taco`, we have to call `initialize` method first. This method takes care of initializing the WASM module for `taco` dependencies.

```typescript
import { initialize } from '@nucypher/taco';

await initialize();
```

With this out of the way, we're ready to use `taco` in our app.

### 2. Define decryption conditions

Before we encrypt our data, we have to define the decryption *conditions*.

*Conditions* are the requirements for a data recipient to access the plaintext data – i.e. what they will need to prove later to gain decryption rights. There are multiple [`Condition` types](/for-developers/conditions) we can use here, including predefined conditions such as `ERC721Ownership.`

The `ERC721Ownership` condition checks the owner of a given token ID. It can be customized by using the `ownerOf` contract method and comparing it with the requestor's signature. For more information, see the [ContractCondition](/for-developers/conditions/contractcondition) section.

We will now specify the condition that must be met to access the data. In this tutorial, we will require that the requester owns an ERC721 token on Ethereum mainnet with a token ID of `5954`.

```typescript
import { conditions } from '@nucypher/taco';

const ownsNFT = new conditions.predefined.erc721.ERC721Ownership({
  contractAddress: '0xBC4CA0EdA7647A8aB7C2061c2E118A18a936f13D',
  chain: 1,
  parameters: [5954], // We need to specify a token ID here
});
```

We can create more complex conditions by combining them with `CompoundCondition`

```typescript
import { conditions } from '@nucypher/taco';

const combined = new conditions.compound.CompoundCondition({
  operator: 'and',
  operands: [
    ownsNFT,
    // Other conditions can be added here
  ],
});
```

### 3. Encrypt the plaintext

We're ready to encrypt our plaintext and gate access to the encrypted contents with our conditions, `NFTOwnership`.

```javascript
import { encrypt, domains } from '@nucypher/taco';
import { ethers } from "ethers";

const web3Provider = new ethers.providers.Web3Provider(window.ethereum);

const messageKit = await encrypt(
  web3Provider,
  domains.MAINNET,
  message,
  ownsNFT,
  ritualId,
  web3Provider.getSigner() 
);
```

The resulting `messageKit` contains the encrypted data and associated conditions.

#### Serializing the messageKit

If you need to store or transmit the encrypted data, use the binary serialization methods:

```typescript
import { ThresholdMessageKit, toHexString } from '@nucypher/taco';

// Serialize to bytes
const encryptedBytes = messageKit.toBytes();

// For JSON storage, convert to hex or base64
const hexString = toHexString(encryptedBytes);

// Later, deserialize
const restored = ThresholdMessageKit.fromBytes(encryptedBytes);
```

{% hint style="warning" %}
**Do not use `JSON.stringify()` on a `ThresholdMessageKit`** — it uses a custom binary format that JSON serialization will corrupt. Always use `.toBytes()` and `ThresholdMessageKit.fromBytes()`.
{% endhint %}

### 4. Request decryption

Finally, we will test the conditional access control service by requesting decryption:

```typescript
import { conditions, decrypt, domains } from '@nucypher/taco';
import { EIP4361AuthProvider, USER_ADDRESS_PARAM_DEFAULT } from '@nucypher/taco-auth';
import { ethers } from "ethers";

const web3Provider = new ethers.providers.Web3Provider(window.ethereum); 

const conditionContext =
  conditions.context.ConditionContext.fromMessageKit(messageKit);
  
// auth provider when condition contains ":userAddress" context variable
// the decryptor user must provide a signature to prove ownership of the wallet address
const authProvider = new EIP4361AuthProvider(
    web3Provider,
    web3Provider.getSigner(),
);
conditionContext.addAuthProvider(USER_ADDRESS_PARAM_DEFAULT, authProvider);

const decryptedMessage = await decrypt(
    web3Provider,
    domains.MAINNET,
    messageKit,
    conditionContext,
);
```

At decryption time, the requester will be asked to verify their address by signing a message from their wallet. If the requester's address controls the minimum number (or greater) of the specified NFT, they are eligible to receive the requisite number of decryption fragments. By assembling these fragments, they can decrypt the encrypted data and view the plaintext.

{% hint style="info" %}
Note that the requester does not need to manually sign the next time they seek access to the data, as the `taco` client will cache their signature. Fresh plaintexts encrypted under any conditions involving the same wallet address are automatically accessible to any requester who has signed at least once, provided they still fulfill any requisite conditions, and the cached signature has not expired.
{% endhint %}

### Complete example

This is the complete, end-to-end example of `taco` integration

```typescript
import { encrypt, decrypt, conditions, domains, initialize, toBytes  } from '@nucypher/taco';
import { EIP4361AuthProvider, USER_ADDRESS_PARAM_DEFAULT } from '@nucypher/taco-auth';
import { ethers } from "ethers";

// The data encryptor runs this part
const doEncrypt = async (message) => {
  const web3Provider = new ethers.providers.Web3Provider(window.ethereum);
  const ownsNFT = new conditions.predefined.erc721.ERC721Ownership({
      contractAddress: '0x1e988ba4692e52Bc50b375bcC8585b95c48AaD77',
      parameters: [3591],
      chain: 1,
  });
  const messageKit = await encrypt(
      web3Provider,
      domains.MAINNET,
      message,
      ownsNFT,
      ritualId,
      web3Provider.getSigner()
  );
  return messageKit;
};

// The data recipient runs this part
const doDecrypt = async (messageKit) => {
  const web3Provider = new ethers.providers.Web3Provider(window.ethereum);
  
  const conditionContext =
      conditions.context.ConditionContext.fromMessageKit(messageKit);

  // auth provider when condition contains ":userAddress" context variable
  const authProvider = new EIP4361AuthProvider(
      web3Provider,
      web3Provider.getSigner(),
  );
  conditionContext.addAuthProvider(USER_ADDRESS_PARAM_DEFAULT, authProvider);
    
  const decryptedMessage = await decrypt(
      web3Provider,
      domains.MAINNET,
      messageKit,
      conditionContext,
  );
  return decryptedMessage;
};

// We have to initialize TACo library first
await initialize();
  
const message = 'my secret message';
const encryptedMessage = await doEncrypt(message);
const decryptedMessage = await doDecrypt(encryptedMessage);

if (decryptedMessage === toBytes(message)) {
  console.log('Success!');
}
```


# Encryptor Allowlist

## Introduction

The *Encryptor Allowlist* is a simple access control mechanism that only allows specific data producers, or encryptors, to access a given DKG ritual, cohort of TACo nodes, and associated public key. In practice, it means that developers can limit who can use TACo to produce valid encryptions for the public key.

The entity that can authorize encryptors is called the `authority`. Each ritual has an `authority` which corresponds to the address of the wallet that initiated the ritual.

Note that encryptor authorization is enforced at decryption time, which means that ciphertexts produced by unauthorized encryptors will not be decryptable and TACo client will return an error.

The restriction is not enforced at encryption time for several reasons:

* Due to the inherent nature of public key cryptography, anyone can use a public key to encrypt data and generate a ciphertext.
* There's valid scenarios where encryption may happen before authorization.

For these reasons, no error will occur at encryption time when using a wallet that is not authorized (i.e. it's not in the *Encryptor Allowlist*), and authorization enforced is deferred until decryption.

## Managing the Allowlist on Testnet

On [**testnet**](/for-developers/quickstart-testnet/get-started-with-tac) it is possible to use one of the premade rituals without any extra steps since there is no allowlist implemented on them. You can simply configure `taco` to use one of those rituals and use any encryptor (wallet address) to perform the encryption.

If you require to test allowlist management flows, don't hesitate [to reach out to us](https://discord.com/invite/buildwithtaco).

## Managing the Allowlist on Mainnet

If you wish to use TACo in production for your application, you will need to establish your own ritual. Please, [contact us](https://discord.com/invite/buildwithtaco) for further assistance.

The *Encryptor Allowlist* is managed through the *AccessController* contract associated with the ritual.

Only the ritual authority wallet can manage the *Encryptor Allowlist* of the *ritual*. This `authority` was set during the Ritual initialization.

1. The contract address of the ritual's AccessController can be found calling the `getAccessController()` function on the [Coordinator contract](/reference/contract-addresses#contracts-on-polygon-mainnet-l2), specifying the `ritual ID`.
2. The ritual's `authority` wallet can call the `authorize()` function on the `AccessController` contract, specifying the `ritual ID` and a list of addresses to be included in the allowlist.
3. From now on, these encryptor wallets can be used for encrypting in this ritual.

Likewise, the `deauthorize()` function of the AccessController can be called by the authority to remove encryptor addresses from the allowlist.


# Mainnet Deployment

{% hint style="warning" %}
Threshold Access Control is not currently supported by an stable cohort of node operators running TACo clients, nor the surrounding infrastructure to make mainnet access straightforward (e.g. [Porter](/reference/architecture/porter)). The network will be relaunched by WEDF in Q3 2026.

Until then, this page serves solely as a open source reference and blueprint for the community.
{% endhint %}

There are no substantial changes in the code when one switches from using *testnet* domain to *mainnet* domain. However, when calling to the API functions, take into account the following:

* The RPC provider URL (Infura, Alchemy, etc) must be changed to from Polygon Amoy (testnet) to Polygon (mainnet) since the L2 of TACo's mainnet domain is the latter.
* The domain variable must be set to `domains.MAINNET`.
* The ID for the cohort must be set to the relevant cohort.

<pre class="language-typescript"><code class="lang-typescript">import { domains } from "@nucypher/taco"

// This should be a environment variable
const rpcProviderUrl = "https://polygon-mainnet.infura.io/v3/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"

<strong>const provider = new ethers.providers.JsonRpcProvider(rpcProviderUrl);
</strong><strong>const domain = domains.MAINNET;
</strong><strong>const id = 0 // Replace by the relevant ID
</strong>
<strong>// TACo operations
</strong><strong>...
</strong></code></pre>


# TACo SDK

{% hint style="warning" %}
Threshold Access Control is not currently supported by an stable cohort of node operators running TACo clients, nor the surrounding infrastructure to make mainnet access straightforward (e.g. [Porter](/reference/architecture/porter)). The network will be relaunched by WEDF in Q3 2026.

Until then, this page serves solely as a open source reference and blueprint for the community.&#x20;
{% endhint %}

The **TACo SDK** provides a TypeScript/JavaScript interface for development.

With the TACo SDK, developers can easily integrate condition-controlled operations into their applications, including:

* Creating custom programmable control conditions
* Encrypting data with conditions
* Communicating with TACo nodes to submit decryption or signing requests to TACo nodes

***

### 📦 Installation

```bash
npm install @nucypher/taco
```

***

### 📚 Resources

* [GitHub repository](https://github.com/nucypher/taco-web)
* [SDK documentation](https://nucypher.github.io/taco-web)
* [NPM package](https://www.npmjs.com/package/@nucypher/taco)<br>

Improvements and feature requests can be submitted [here](https://github.com/nucypher/taco-web/issues).


# Ecosystem Integrations

TACo is compatible with most Web3 infrastructure. This section contains guides for using TACo in combination with other example layers in the Web3 stack, including decentralized **storage**, decentralized **databases** and decentralized **transport**.

* Use TACo with [OrbisDB](/for-developers/integrations/orbisdb) (Ceramic) SQL database infrastructure.
* Use TACo with the [Waku](/for-developers/integrations/waku) (Logos) transport layer.
* Use TACo with the [Codex](/for-developers/integrations/waku-+-codex) (Logos) data storage protocol.
* Use TACo with the [Irys](/for-developers/integrations/irys) (Arweave) bundler.
* Use TACo with [ComposeDB](/for-developers/integrations/composedb) (Ceramic) GraphQL database infrastructure.
* Use TACo with the [Turbo](/for-developers/integrations/turbo) (Arweave) bundler.


# OrbisDB

This guide walks through how to use [OrbisDB](https://developers.ceramic.network/docs/introduction/orbisdb-overview) and TACo in combination, using the DeForum web application and [repo](https://github.com/nucypher/deforum-taco) as an example. OrbisDB is an easy-to-use SQL interface for querying data stored via the [Ceramic](https://ceramic.network/) network, and alongside TACo, another fundmental building block for the Web3 stack.

{% hint style="info" %}
DeForum is a decentralized web forum that allows end-users to create token-gated posts, make comments, upload images, and build personal profiles. There are two synced reference repos, one hosted by [nucypher](https://github.com/nucypher/deforum-taco) (TACo) and one by [ceramicstudio](https://github.com/ceramicstudio/deforum-taco) (OrbisDB).
{% endhint %}

## OrbisDB overview

Built upon [Ceramic](https://ceramic.network/), OrbisDB is an open-source relational database designed for Web3 applications and beyond. OrbisDB offers a flexible and developer-friendly experience via a (1) web app & SDK for storing and managing datasets, (2) support for SQL and GraphQL queries, and (3) hosted nodes to facilitate DevOps. The Ceramic network, as with ComposeDB, enables sovereign data ownership, composability and scalability. Decentralization is achieved through a population of node operators numbering in the hundreds, that anyone can permissionlessly join to provide data services.

## Use case ideas

* **DeSci.** Even movements centered around open access necessitate private channels, including draft papers, raw data analysis, peer reviews, funding, and other forms of collaboration. OrbisDB and TACo together offer storage and sharing of scientific work that maximizes the researcher's control and sovereignty. Additionally, the combination enables scientific data marketplaces with no trusted intermediary facilitating monetization.
* **AI datasets.** Leverage OrbisBD's provenance verification tooling and TACo's granular (row-level) conditionality to enable safe collaboration on sensitive training data while simultaneously verifying data integrity and quality.
* **Shared knowledge bases.** Combine Ceramic's verifiable credentials with credential-based secrets management to enforce fine-grained, per-page access to organizational resources. OrbisDB enables migration of non-public external data (e.g. via an API or static CSV) which can then be encrypted via TACo for specific group members (e.g. stakers in a network).

***

## Example application & repo

[DeForum](https://github.com/ceramicstudio/deforum-taco) illustrates how TACo and OrbisD can be combined in a browser-based forum app, and also provides a template for virtually any form of token-gated content platform. The demo can run on a public node instance provided by OrbisDB studio for testing, or you can set up your own node in order to save data on the Ceramic network in production. The demo also requires a ThirdWeb account to help with image file upload and storage on IPFS.

Skip to Section II below for an in-depth guide to leveraging TACo in the context of the DeForum app.

***

## Section I – Integration steps

This section covers how TACo can be used in concert with OrbisDB across domains and use cases. Follow these instructions to integrate TACo into your OrbisDB app.

### 1. Install required dependencies

Ensure that you have the required packages in your `package.json` by running:

```bash
npm install @nucypher/taco @nucypher/taco-auth ethers
```

### 2. Update network configuration

Next, update the chain configuration:

<pre class="language-typescript" data-title="app/context/WalletContext.tsx" data-full-width="false"><code class="lang-typescript">import { getDefaultConfig } from '@rainbow-me/rainbowkit';
<strong>import { polygonAmoy } from 'wagmi/chains';
</strong>
const config = getDefaultConfig({
  appName: 'test-app',
  projectId,
  chains: [polygonAmoy],
  ssr: true,
});
</code></pre>

Polygon Amoy is the L2 for TACo's testnet [domains](/for-developers/quickstart-testnet/get-started-with-tac), so encryption/decryption calls from your app go via this network.

### 3. Initialize TACo

Next, intialize TACo by loading WASM modules:

{% code title="app/context/TACoContext.tsx" %}

```typescript
import { useEffect, useState } from "react";
import { initialize } from '@nucypher/taco';

const [isInitialized, setIsInitialized] = useState(false);

const initializeTACo = useCallback(() => {
  initialize().then(() => setIsInitialized(true));
}, [setIsInitialized]);

useEffect(() => {
  if (!isInitialized) {
    initializeTACo();
  }
},[initializeTACo, isInitialized]);
```

{% endcode %}

TACo initialization is required on application startup. For Next.js applications, it is recommended to use a React Context to check if TACo has been initialized before to avoid repeating this process unnecessarily.

### 4. Define access/decryption conditions

Next, define the conditions for accessing private data:

{% code title="components/sections/newPost-modules.tsx" %}

```typescript
import { conditions } from "@nucypher/taco";

const condition = new conditions.base.rpc.RpcCondition({
  chain: 80002,
  method: "eth_getBalance",
  parameters: [":userAddress"],
  returnValueTest: {
    comparator: ">",
    value: 0,
  },
});
```

{% endcode %}

Access conditions are specified at data encryption time. The various types of conditions can be found in the Access Control [section](/for-developers/conditions).

In the code snippet above, we specified that only those data consumer accounts with a positive POL balance on Polygon Amoy (chain ID `80002`) will qualify to decrypt the data. You can hardcode conditions, or create a UI for users to choose their own requirements for data access.

### 5. (Optional) Add TACo encryption and decryption utilities

It's possible to integrate the `encrypt` and `decrypt` functions into a Next.js application by creating a new React Custom Hook. The two core functions to implement are `encryptWithTACo` and `decryptWithTACo`.

In this example, these functions ensure that posts are encrypted before being stored in OrbisDB and decrypted after being queried.

First, create **auxiliary functions** – encoding/decoding base64 strings – to be used with the main functions later:

{% code title="app/hooks/useTaco.ts" %}

```typescript
function encodeB64(uint8Array: any) {
  return Buffer.from(uint8Array).toString("base64") as String;
}

function decodeB64(b64String: any) {
  return new Uint8Array(Buffer.from(b64String, "base64"));
}
```

{% endcode %}

Next, define the two main encryption and decryption functions:

The `encryptWithTACo()` function encrypts a message and simultaneously sets the conditions for decrypting it. The returned value is a base64 string that contains the encrypted message and the decryption conditions.

{% code title="app/hooks/useTaco.ts" %}

```typescript
const domain = "lynx";
const ritualId = 27;

async function encryptWithTACo(
  messageToEncrypt: string,
  condition: conditions.condition.Condition,
  provider: ethers.providers.Provider,
  signer: ethers.Signer,
) {
  const tmk = await encrypt(
    provider,
    domain,
    messageToEncrypt,
    condition,
    ritualId,
    signer,
  );

  return encodeB64(tmk.toBytes());
}
```

{% endcode %}

The `decryptWithTACo()` function takes a base64 string with the encrypted data and decrypts the message if the conditions are met.

A creitcal part of the decryption process is the authentication of the data consumer. In this case, the data consumer (or requestor) proves that they own a given Ethereum address – specifically via the **reuse** of an SIWE authentication. Users have already authenticated themselves on OrbisDB, so this avoids them signing the same message again.

{% code title="app/hooks/useTaco.ts" %}

```typescript
import { SiweMessage } from "@didtools/cacao";
import { DIDSession } from "did-session";
import {
  SingleSignOnEIP4361AuthProvider,
  USER_ADDRESS_PARAM_DEFAULT,
} from "@nucypher/taco-auth";
import { ethers } from "ethers";

// Aux function to get the OrbisDB SIWE session from local storage
async function loadSiweFromOrbisSession(): Promise<{
  message: string | undefined;
  signature: string | undefined;
}> {
  const session = localStorage.getItem("orbis:session");
  const didSession = await DIDSession.fromSession(session);
  const siweMessage = SiweMessage.fromCacao(didSession.cacao);
  const message = siweMessage.toMessageEip55();
  const signature = siweMessage.signature;
  return { message, signature };
}

async function decryptWithTACo(
  encryptedMessage: string,
  provider: ethers.providers.Provider,
) {

  const siweInfo = await loadSiweFromOrbisSession();
  const authProvider =
    await SingleSignOnEIP4361AuthProvider.fromExistingSiweInfo(
      siweInfo.message,
      siweInfo.signature,
    );

  const tmk = ThresholdMessageKit.fromBytes(decodeB64(encryptedMessage));
  const conditionContext =
    conditions.context.ConditionContext.fromMessageKit(tmk);
  conditionContext.addAuthProvider(
    USER_ADDRESS_PARAM_DEFAULT,
    authProvider,
  );

  try {
    const decrypted = await decrypt(provider, domain, tmk, conditionContext);
    return new TextDecoder().decode(decrypted);
  } catch (error) {
    console.error("Decryption failed:", error);
    return "<Decryption failed>";
  }
}
```

{% endcode %}

***

## Section II – Encrypting & decrypting forum posts via the DeForum web application

Below, forum post bodies are encrypted so only data consumers that satisfy given conditions can view them as plaintext. The following code snippet also executes query to store the posts on OrbisDB in said encrypted format, so that only authenticated and qualifying consumers can access them.

{% code title="components/sections/newPost-modules.tsx" %}

```typescript
import { conditions } from "@nucypher/taco";
import useTaco from "@/app/hooks/useTaco";

const { encryptWithTACo } = useTaco();

const createPost = async (): Promise<void> => {
    // [...]
    
    // define TACo condition to decrypt the body of the post
    const condition = new conditions.base.rpc.RpcCondition({
      chain: 80002,
      method: "eth_getBalance",
      parameters: [":userAddress"],
      returnValueTest: {
        comparator: ">",
        value: 0,
      },
    });

    // encrypt post with TACO
    const encryptedBody = await encryptWithTACo(
      body,
      condition,
      provider,
      signer,
    );

    // [...]

    // Upload the forum post with the encrypted data to OrbisDB
    const createQuery = await orbis
        .insert(POST_ID)
        .value({
            title,
            body: encryptedBody,
            imageid: imageUrl ? imageUrl : "",
            created,
          })
          .context(CONTEXT_ID)
          .run();

    // [...]
}
```

{% endcode %}

The decryption of the post bodies occurs on the client side after downloading them from OrbisDB:

{% code title="app/(home)/post/\[slug]/page.tsx" %}

```typescript
import useTaco from "@/app/hooks/useTaco";

const [decryptedBody, setDecryptedBody] = useState<string | undefined>(
    undefined,
);

const { decryptWithTACo } = useTaco();

const getPost = async (stream_id: string): Promise<void> => {
    // [...]
    
    const provider = new ethers.providers.Web3Provider(window.ethereum);
    
    // [...]
    // After a SQL query, the post data is stored at postResult[0] variable
    
    decryptWithTACo(postResult[0].body, provider).then((decrypted) => {
        if (decrypted) {
            setDecryptedBody(decrypted.toString());
        }
    });
    
    // [...]
}

```

{% endcode %}

With a running application, it is possible to check that the encrypted posts are being uploaded to the database with [OrbisDB Studio](https://studio.useorbis.com/):

<figure><img src="/files/bZU85y3FWfU8uZrGT96s" alt=""><figcaption></figcaption></figure>

***

## Using ComposeDB & TACo in production

* For Ceramic, connect to Mainnet (`domains.MAINNET`).
* For TACo, a funded Mainnet `ritualID` is required – this connects the encrypt/decrypt API to a cohort of independently operated nodes, and corresponds to a DKG public key generated by independent parties. A dedicated `ritualID` for Ceramic + TACo projects will be sponsored soon. Watch for updates here or in the Discord[ #taco](https://discord.com/channels/866378471868727316/870383642751430666) channel.

As noted, the parameters specified in this guide are for testing and hacking only. For real-world use cases where uploaded data should remain private & permanent, the production version of TACo is required.


# Waku

This guide explains how to integrate [Waku](https://docs.waku.org/) and TACo, which together offer developers a comprehensive *Web3 privacy development toolkit –* i.e. a general-purpose, censorship-resistant, end-to-end encrypted messaging layer where the access logic for delivered messages is programmable and conditions-based. This enables a variety of performant decentralized applications – including the location-sharing app [Arc](https://lionfish-app-rtgbk.ondigitalocean.app/), a reference prototype and [repository](https://github.com/nucypher/Arc) that accompanies this tutorial.

This guide sets up a minimum viable integration of the two SDKs/protocols, that can be used across use cases and domains. This includes setting up relevant services and dependencies, encrypting/sending messages with NFT-based access conditions, and receiving/decrypting said messages.

## Waku overview

Waku's communication protocols provide gas-free message transport and routing – supporting in-browser light clients, and ensuring protocol-level metadata privacy and end-user anonymity. Ephemeral, real-time messaging with minimal persistence brings efficient data/message transmission within adopting applications. Like TACo, Waku's underlying network makes no compromises with respect to permissionlessness, trust-minimization and decentralization.

For more on Waku's JavaScript SDK, check out their [documentation](https://docs.waku.org/guides/js-waku/).

## Use case ideas

* **Crowdsourcing.** Whether it's weather, flight tracking, large geo-spatical models, traffic data, or even verifying the ingredients used by restaurants, contributors in large-scale crowdsourcing projects are often taken for granted – both in terms of their privacy and their share of the spoils. Leverage Waku's anonymity-preserving data stream transmission, and TACo's payment-conditioned access, to ensure contributors to highly valuable data sets are able to maintain their anonymity while guaranteeing their compensation.
* **Medical Wearables.** Don't rely on HIPAA or the moral compass of medical device manufacturers. Waku's the ideal transport layer for devices which stream small data payloads, while TACo enables strict sharing requirements via composable condition logic. Health data applications which integrate Waku & TACo can uniquely offer their end-users total sovereignty over their sensitive data, where they alone control flows of information to their healthcare providers.
* **GenAI Inference.** Messages to and from an LLM model/chatbot should be 100% private and censorship-resistant, not harvested by an intermediary or blocked by a central authority. Harness Waku's efficient, ephemeral message transmission, alongside TACo's per-message/ciphertext encryption granularity, to provide a smooth and private experience for users of GenAI interfaces. Both TACo and Waku's decentralized/P2P frameworks ensure messages (and decryption material) can propagate without any single entity being able to unilaterally censor interactions with the model.

***

## Example application & repo

[Arc](https://github.com/nucypher/Arc) is an experimental, privacy-preserving, permissionless, decentralized location-sharing application that combines Waku & TACo's capabilites. Arc provides secure, end-to-end encrypted location sharing with fine-grained, on-chain access control, making it a powerful tool for individuals, groups or applications seeking a way to selectively share their location without trusting an intermediary platform.

In this version of Arc, users may set on-chain conditions for accessing their real-time location, such as a specific time window for access (based on block time), a minimum token balance, or the ownership of a special-purpose NFT.

***

## Integration steps

This section walks through a *minimum viable integration* to get developers started. For more powerful extensions and advanced condition logic, check out the [Conditions ](/for-developers/conditions)section.

### 1. Required imports

```javascript
// Waku SDK components for decentralized messaging
import { createLightNode, createEncoder, createDecoder, waitForRemotePeer } from '@waku/sdk'

// TACo components for conditional encryption
import { initialize, encrypt, decrypt, conditions, domains, ThresholdMessageKit } from '@nucypher/taco'

// Authentication provider for TACo
import { EIP4361AuthProvider, USER_ADDRESS_PARAM_DEFAULT } from '@nucypher/taco-auth'

// Ethereum interaction library
import { ethers } from 'ethers'
```

### 2. Define a Waku encoder

```javascript
// Define your application's content topic
// Format: /<application-name>/<version>/<content-type>/<encoding>
const CONTENT_TOPIC = '/taco-waku-example/1/messages/proto'
const encoder = createEncoder({ contentTopic: CONTENT_TOPIC })
const decoder = createDecoder(CONTENT_TOPIC)
```

{% hint style="info" %}
Note that *content topics* are metadata strings embedded into outgoing messages that facilitate protocol-level features like selectively processing incoming messages. These strings can be thought of as 'channels', which determine, among other things, the path of messages through the network. Learn more about content topics [here](https://docs.waku.org/learn/concepts/content-topics/).
{% endhint %}

### 3. Set up Web3 provider

Configure the Ethereum provider for on-chain interactions:

```javascript
const web3Provider = new ethers.providers.Web3Provider(window.ethereum)
await web3Provider.send('eth_requestAccounts', [])
```

### 4. Initialize Waku & TACo services

```javascript
// Initialize TACo
await initialize()

// Create and start Waku node
const node = await createLightNode({ defaultBootstrap: true })
await node.start()
await waitForRemotePeer(node)
```

### **5. Define access conditions**

Next, define the the conditions that must be satisfied for messages to be decrypted into plaintext. In this example, a qualifying *data consumer* must hold a ERC721, in a wallet they provably own at the time of decryption request:

```javascript
const condition = new conditions.predefined.erc721.ERC721Ownership({
  contractAddress: '0x1e988ba4692e52Bc50b375bcC8585b95c48AaD77',
  parameters: [3591],  // Token ID
  chain: 11155111,     // Sepolia testnet
})
```

### 6. Encrypt and transmit messages

In this step, set up the plaintext message to be encrypted locally, with the conditions for access embedded. Once encrypted, the message can be transmitted across the Waku network:

```javascript
const message = 'Hello, encrypted Waku world!'
const ritualId = 27  // This is a testnet-specific ritual ID

// Encrypt message with TACo
const messageKit = await encrypt(
  web3Provider,
  domains.DEVNET,
  message,
  condition,
  ritualId,
  web3Provider.getSigner()
)

// Send via Waku
await node.lightPush.send(encoder, {
  payload: messageKit.toBytes()
})
```

{% hint style="warning" %}
Note that the `ritualID` refers to the group of TACo nodes that will enforce access to the message(s). This example uses `ritualId = 27` and `domains.DEVNET`. These development environments are **not decentralized** and unsuitable for real-world sensitive data.
{% endhint %}

### 7. Receive and decrypt messages

Finally, set up the facility for receiving the encrypted message payload. Once the message is retrieved, the (authenticated) *data consumer* can present the payload to the TACo network and receive decryption material if qualifying – all via the following decryption logic:

```javascript
await node.filter.subscribe([decoder], async (wakuMessage) => {
  if (!wakuMessage.payload) return
  
  try {
    // Convert received bytes to MessageKit
    const receivedMessageKit = ThresholdMessageKit.fromBytes(wakuMessage.payload)
    
    // Set up condition context and authentication
    const conditionContext = conditions.context.ConditionContext.fromMessageKit(receivedMessageKit)
    const authProvider = new EIP4361AuthProvider(web3Provider, web3Provider.getSigner())
    conditionContext.addAuthProvider(USER_ADDRESS_PARAM_DEFAULT, authProvider)
    
    // Decrypt message
    const decryptedMessage = await decrypt(
      web3Provider,
      domains.DEVNET,
      receivedMessageKit,
      conditionContext
    )
    
    console.log('Decrypted message:', decryptedMessage)
  } catch (error) {
    console.error('Error decrypting message:', error)
  }
})
```

### Complete integration example

```javascript
import { createLightNode, createEncoder, createDecoder, waitForRemotePeer } from '@waku/sdk'
import { initialize, encrypt, decrypt, conditions, domains, ThresholdMessageKit } from '@nucypher/taco'
import { EIP4361AuthProvider, USER_ADDRESS_PARAM_DEFAULT } from '@nucypher/taco-auth'
import { ethers } from 'ethers'

const CONTENT_TOPIC = '/taco-waku-example/1/messages/proto'

// Initialize TACo and Waku
await initialize()
const node = await createLightNode({ defaultBootstrap: true })
await node.start()
await waitForRemotePeer(node)

// Setup Web3 provider
const web3Provider = new ethers.providers.Web3Provider(window.ethereum)
await web3Provider.send('eth_requestAccounts', [])

// Define decryption condition
const condition = new conditions.predefined.erc721.ERC721Ownership({
  contractAddress: '0x1e988ba4692e52Bc50b375bcC8585b95c48AaD77',
  parameters: [3591],
  chain: 11155111, // Sepolia testnet
})

// Encrypt and send message
const encoder = createEncoder({ contentTopic: CONTENT_TOPIC })
const message = 'Hello, encrypted Waku world!'
const ritualId = 27 // Replace with your actual ritual ID
const messageKit = await encrypt(
  web3Provider,
  domains.DEVNET,
  message,
  condition,
  ritualId,
  web3Provider.getSigner()
)
await node.lightPush.send(encoder, {
  payload: messageKit.toBytes()
})
console.log('Encrypted message sent over Waku')

// Set up message reception and decryption
const decoder = createDecoder(CONTENT_TOPIC)
await node.filter.subscribe([decoder], async (wakuMessage) => {
  if (!wakuMessage.payload) return
  try {
    const receivedMessageKit = ThresholdMessageKit.fromBytes(wakuMessage.payload)
    const conditionContext = conditions.context.ConditionContext.fromMessageKit(receivedMessageKit)
    const authProvider = new EIP4361AuthProvider(web3Provider, web3Provider.getSigner())
    conditionContext.addAuthProvider(USER_ADDRESS_PARAM_DEFAULT, authProvider)
    const decryptedMessage = await decrypt(
      web3Provider,
      domains.TESTNET,
      receivedMessageKit,
      conditionContext
    )
    console.log('Decrypted message:', decryptedMessage)
  } catch (error) {
    console.error('Error decrypting message:', error)
  }
})
```

***

## Using Waku & TACo in production

The parameters specified in this guide are for testing and hacking only. For real-world use cases, the production version of TACo is required – i.e. a funded Mainnet `ritualID` which connects the encrypt/decrypt API to a cohort of independently operated nodes, and corresponds to a DKG public key generated by independent parties.

A dedicated `ritualID` for Waku + TACo integrations will be sponsored in early 2025. Watch for updates here or in the[ #taco](https://discord.com/channels/866378471868727316/870383642751430666) channel on the Threshold Network discord server.


# Waku + Codex

Powering secure Waku communications and decentralized Codex storage with TACo

<div><figure><img src="/files/FQLYhYXlfg7RTAXe3dYS" alt=""><figcaption></figcaption></figure> <figure><img src="/files/LRoGtGgpfa2XlSncakaD" alt=""><figcaption></figcaption></figure> <figure><img src="/files/DmGuFm9LAHe1De1BBp14" alt=""><figcaption></figcaption></figure></div>

Using *Codex*, *Waku*, and *TACo* together gives developers a powerful, fully decentralized stack for building secure, censorship-resistant applications. *Waku* enables real-time, peer-to-peer communication without relying on centralized servers, while *Codex* provides verifiable, durable storage for any type of content. *TACo* adds a layer of programmable access control and encryption, allowing developers to define precise conditions for data access.

## Waku overview

**Waku's** communication protocols provide gas-free message transport and routing – supporting in-browser light clients, and ensuring protocol-level metadata privacy and end-user anonymity. Ephemeral, real-time messaging with minimal persistence brings efficient data/message transmission within adopting applications. Like TACo, Waku's underlying network makes no compromises with respect to permissionlessness, trust-minimization and decentralization.

For more on Waku's JavaScript SDK, check out their [documentation](https://docs.waku.org/guides/js-waku/).

## Codex overview

**Codex** is a decentralized data storage protocol designed to provide strong censorship resistance and durability guarantees. It aims to address the shortcomings of centralized cloud storage—such as censorship, lack of data ownership, security breaches, and high costs. By leveraging a decentralized network of storage providers, Codex ensures data is stored reliably and remains accessible without relying on centralized entities.

For further information Codex, follow their [documentation](https://docs.codex.storage/) or [API specification](https://api.codex.storage/).

***

## Use case ideas

The combination of communication (*Waku*), storage (*Codex*), and Access Control (*TACo*) layers enable a wide range of use cases, powering practical and fully decentralized solutions such as:

* **Collaboration Apps.** Decentralized alternative to *Google Docs* or *Office 365,* offering fine-grained access control. Documents and version history are stored using *Codex,* while *Waku* protocol enables real-time synchronization and user notifications. *TACo* handles access control for all shared content.
* **Voting and Governance.** An organization that shares proposals and collects votes in real time with verifiable, tamper-resistant outcomes. While *TACo* keeps votes anonymous until results are revealed thanks to its access control conditions, *Waku* distributes ballots and proposals in real time to eligible voters, and results are publicly and verifiably stored by *Codex*.
* **Data markets.** A decentralized marketplace where researchers or companies can exchange sensitive datasets securely and without intermediaries. *Codex* serves as a powerful tool to host these datasets in a decentralized network, *Waku* facilitates peer-to-peer negotiation of data exchanges, and TACo enforces conditional access (e.g., after payment or approval).
* **Social networks.** Social platforms -such as forums, media-sharing apps, and microblogging sites) can benefit from decentralized storage of posts, profiles, images, and videos using *Codex.* *Waku* is well-suited for notifications, user updates and real-time chat, while *TACo* provides access control to posts and social data.

***

## Example application

[*Cyphershare*](https://github.com/hackyguru/cyphershare) is a secure file sharing decentralized application that leverages Codex for storage, Waku for peer-to-peer communication, and TACo for end-to-end encryption and access control. By using Cyphershare, users can securely share files by encrypting them in the browser before upload, ensuring that only authorized recipients -those meeting predefined TACo conditions such as holding a specific token balance or within a time-limited access windows- can decrypt and access the content.

The running application can be found here: [CypherShare](https://share.hackyguru.com/)

***

## Integration steps

This section walks through a minimum viable integration to get developers started. For more powerful extensions and advanced condition logic, check out the [Conditions](/for-developers/conditions) section.

### 1. Required imports

```typescript
// Waku SDK components for decentralized messaging
import { createLightNode, createEncoder, createDecoder, waitForRemotePeer } from '@waku/sdk'

// TACo components for conditional encryption
import { initialize, encrypt, decrypt, conditions, domains, ThresholdMessageKit } from '@nucypher/taco';

// Authentication provider for TACo
import { EIP4361AuthProvider, USER_ADDRESS_PARAM_DEFAULT } from '@nucypher/taco-auth';

// Ethereum interaction library
import { ethers } from 'ethers';
```

### 2. Create and set up a Waku light node

```typescript
const lightNode = await createLightNode({
  defaultBootstrap: false,
  networkConfig: {
    clusterId: 42,
    shards: [0]
  },
});

await lightNode.start();

// Define your application's content topic: /<application-name>/<version>/<content-type>/<encoding>
const CONTENT_TOPIC = `/fileshare/1/room-${roomId}/proto`;

// Create Waku's encoder and decoder
const encoder = createEncoder({ contentTopic: CONTENT_TOPIC });
const decoder = createDecoder(CONTENT_TOPIC);
```

{% hint style="info" %}
Note that *content topics* are metadata strings embedded into outgoing messages that facilitate protocol-level features like selectively processing incoming messages. These strings can be thought of as 'channels', which determine, among other things, the path of messages through the network. Learn more about content topics [here](https://docs.waku.org/learn/concepts/content-topics/).
{% endhint %}

### 3. Send and receive Waku messages

```typescript
// Function that handles the received message
const messageHandler = (wakuMessage: DecodedMessage) => {
}

// Subscribe to content topic
const subscription = await lightNode.filter.subscribe(
  [decoder],
  messageHandler
);

// Send a message
const serializedMessage = FileMessage.encode(protoMessage).finish();

await node.lightPush.send(
  encoder,
  { payload: serializedMessage }
)
```

### 4. Download and upload files from Codex

{% hint style="info" %}
Codex uses a REST API to interact with the nodes, so the complexity of handling HTTP requests is intentionally omitted in this section to keep the focus on the essential concepts. Only the relevant parts are show. The complete documentation for this API is in the [Codex API](https://api.codex.storage/) page.
{% endhint %}

Upload a file to Codex:

```typescript
const url = `/api/codex/v1/data`;
const xhr = new XMLHttpRequest();

// The file to be uploaded
const file = ... [File]

xhr.setRequestHeader('Content-Type', file.type);
xhr.setRequestHeader(
  'Content-Disposition',
  `attachment; filename='${file.name}'`
);

xhr.onload = function () {
  if (xhr.status >= 200 && xhr.status < 300) {
    // Extract the CID from the response
    const cid =
      typeof response === 'object'
        ? response.id ||
          response.cid ||
          (response.data && (response.data.id || response.data.cid))
        : response;
  }
}

xhr.send(file);
```

Download a file from Codex:

<pre class="language-typescript"><code class="lang-typescript"><strong>// CID is the Codex file ID.
</strong><strong>const cid = ...
</strong>const downloadUrl = `/api/codex/v1/data/${cid}/network/stream`;

const response = await fetch(downloadUrl, {
  method: 'GET',
  headers: {
    Accept: '*/*',
    ...(this.authHeaders || {}),
  },
  mode: 'cors',
  credentials: 'include',
});

const data = await response.blob();
</code></pre>

### 5. Encryption and decryption with TACo

Initialize TACo:

```typescript
await initialize()
```

Define access conditions:

<pre class="language-typescript"><code class="lang-typescript">const condition = new conditions.base.rpc.RpcCondition({
  chain: chainId,
  method: "eth_getBalance",
<strong>  parameters: [":userAddress", "latest"],
</strong>  returnValueTest: {
    comparator: ">",
    value: 0,
<strong>  },
</strong>});
</code></pre>

Encrypt files:

<pre class="language-typescript"><code class="lang-typescript"><strong>const ritualId = 27 // This is a testnet-specific ritual ID
</strong>
// Read file as ArrayBuffer
const arrayBuffer = await file.arrayBuffer();
const data = new Uint8Array(arrayBuffer);

const messageKit = await encrypt(
  provider,
  domain,
  data,
  condition,
  ritualId,
  encryptorSigner // ethers.Signer
);

const bytesToUpload = messageKit.toBytes();
</code></pre>

Decrypt files:

```typescript
const messageKit = ThresholdMessageKit.fromBytes(encryptedBytes);
const authProvider = new EIP4361AuthProvider(provider, signer);
const conditionContext =
  conditions.context.ConditionContext.fromMessageKit(messageKit);
conditionContext.addAuthProvider(
  USER_ADDRESS_PARAM_DEFAULT,
  authProvider
);

const result = await decrypt(
  provider,
  domain,
  messageKit,
  conditionContext
);
```

***

## Using Waku + Codex + TACo in production

Waku is currently used to enable private communication in applications such as RAILGUN and Status. Research and development continue with their decentralized DoS protection protocol being trialed on testnet.

*Codex* is currently operating a public, non-incentivized testnet that is open to developers and contributors.

Regarding *TACo*, the parameters specified in this guide are for testing and hacking only. For real-world use cases, the production version of *TACo* is required – i.e. a funded Mainnet `ritualID` which connects the encrypt/decrypt API to a cohort of independently operated nodes, and corresponds to a DKG public key generated by independent parties.

Ready to use *TACo* in production? Check out the [Mainnet Access](broken://pages/XpbH0VyXQYA9sdAJWhyQ) section for everything you need.


# Irys

This three-step guide explains how to integrate TACo with [Irys](https://docs.irys.xyz/), thereby enabling end-users to flexibly share/access encrypted data uploaded to permanent storage on [Arweave](https://www.arweave.org/build).\
\
There are plenty of reasons to combine these technologies. Irys's sub-millisecond upload & data egress can be parallelized with TACo's low-latency decryption flow, ensuring rapid access to shared data. Provenance features like transaction receipts and cryptographic proof-of-time are fully compatible with TACo and are equally (or arguably more) important for sensitive information and messages. Broadly, integrating Irys & TACo offers long-term sovereignty to end-users – i.e. that their private data will remain accessible to qualifying devices forever.

## Use case ideas

* **Governance.** Generate tamper-proof, timestamped records of voting activity, enhancing transparency and reducing trust assumptions.
* **Connected Vehicles.** Store sensitive real-time vehicle diagnostics and geolocation data, such that the data stream is instantly available when required (e.g. while driving) but not leaked beyond known and legitimate recipients (e.g. a smart city traffic system).
* **Private NFTs.** Move beyond the status quo of symbolic receipts stored on centralized platforms, to a world where one owns the *decryption rights* to a movie, track, in-game asset, or piece of art – trustlessly and in perpetuity.

***

## Example application & repo

Check out this [token-gated photo album](https://github.com/lukecd/irys-threshold), an intuitive mini-app that demonstrates the power and simplicity of using Irys & TACo in concert. Images are encrypted via the TACo API and stored on-chain via Irys. To view the images, users must prove they hold special-purpose NFT.

***

## Integration steps

### 1. Installation & dependencies

```typescript
yarn add @nucypher/taco
yarn add @nucypher/taco-auth
yarn add @irys/sdk
```

### 2. Define access condition & encrypt the data

First, we initialize the `taco-web` [library](https://github.com/nucypher/taco-web).

As the data producer, we create an access condition. Here we use the simple condition `ownsNFT` – data consumers must prove ownership of a specific ERC-721 NFT in order to gain decryption material pertaining to the encrypted message. More on condition types [here](/for-developers/conditions).\
\
We encrypt the message using the `ownsNFT` condition, specifying the aforementioned testnet `domain` and `ritualID`, and a standard web3 provider/signer. The output of this function is a `messageKit` – a payload containing both the encrypted data and embedded condition metadata necessary for a qualifying data consumer to decrypt the message. Finally, we convert the `messageKit`to a hex string format, which will help us upload it via Irys in a single transaction.

<pre class="language-typescript"><code class="lang-typescript">import { encrypt, conditions, domains, initialize, toHexString } from '@nucypher/taco';
<strong>import { ethers } from "ethers";
</strong>
// We have to initialize the TACo library first
await initialize();

const web3Provider = new ethers.providers.Web3Provider(window.ethereum);

const ownsNFT = new conditions.predefined.erc721.ERC721Ownership({
  contractAddress: '0x1e988ba4692e52Bc50b375bcC8585b95c48AaD77',
  parameters: [3591],
  chain: 11155111,
});
const ritualId = 27

const message = "this will be here forever";

const messageKit = await encrypt(
  web3Provider,
  domains.DEVNET,
  message,
  ownsNFT,
  ritualId,
  web3Provider.getSigner() 
);
const encryptedMessageHex = toHexString(messageKit.toBytes());
</code></pre>

{% hint style="warning" %}
This guide utilizes the parameters `ritualId = 27` and `domains.DEVNET`. These refer to an open DKG public key and hacker-facing stable testnet respectively. Although fully functional and up-to-date with Mainnet, this development environment is **not decentralized** and unsuitable for real-world sensitive data. For more information, see the [trust assumptions section](broken://pages/WugTyY8VT0l9LBPeY79b).
{% endhint %}

### 3. Connect to Irys & store the data

First, we connect to an Irys [Devnet](https://docs.irys.xyz/developer-docs/using-devnet) node. This requires funding a wallet with any of the devnet [tokens](https://docs.irys.xyz/overview/supported-tokens) supported by Irys. In this example, we're using Polygon (Amoy) MATIC.

{% hint style="info" %}
The Irys Devnet is for testing purposes only. It is not decentralized and data will only remain on Irys servers for \~**60 days**. See the final section on using TACo & Irys in production.
{% endhint %}

We then construct a single JSON object from the `encryptedMessageHex`. We can now upload the encrypted data to Arweave, which will be retrievable once an Irys [gateway](https://docs.irys.xyz/developer-docs/downloading) has indexed the data. This is identifiable via the `receiptID`, which is provided to the data consumer via a side-channel.

<pre class="language-typescript"><code class="lang-typescript">import { WebIrys } from '@irys/sdk';

const token = 'matic';
const network = 'devnet';
const wallet = { rpcUrl: https://rpc-amoy.polygon.technology/, name: 'ethersv5', web3Provider };
const webIrys = new WebIrys({ network, token, wallet });

await webIrys.ready();

<strong>const dataToUpload = JSON.stringify(encryptedMessageHex);
</strong>const receipt = await webIrys.upload(dataToUpload);

console.log(`Data uploaded ==> https://gateway.irys.xyz/${receipt.id}`);
</code></pre>

### 4. Retrieve & decrypt the data

From the data consumer's perspective, we now use the `receiptID` to find and retrieve the encrypted payload via an Irys gateway. Note that the same data identifier works with Arweave gateways.

Finally, we prove we own a wallet that this wallet holds the correct NFT, retrieve fragments of decryption material from TACo nodes, assemble these fragments locally, and decrypt the payload. All of these steps are contained in the `decrypt()` function below.

```typescript
import { conditions, decrypt, ThresholdMessageKit } from '@nucypher/taco';
import { EIP4361AuthProvider, USER_ADDRESS_PARAM_DEFAULT } from '@nucypher/taco-auth';

const response = await fetch(`https://gateway.irys.xyz/${receipt.id}`);
const dataJson = await response.text();
const encryptedMessage = ThresholdMessageKit.fromBytes(
  Buffer.from(JSON.parse(dataJson), 'hex'),
);

// auth provider when condition contains ":userAddress" context variable
// the decryptor user must provide a signature to prove ownership of the wallet address
const authProvider = new EIP4361AuthProvider(
  web3Provider,
  web3Provider.getSigner(),
);
const conditionContext =
  conditions.context.ConditionContext.fromMessageKit(encryptedMessage);
conditionContext.addAuthProvider(USER_ADDRESS_PARAM_DEFAULT, authProvider);

const decryptedMessage = await decrypt(
  web3Provider,
  domains.TESTNET,
  encryptedMessage,
  conditionContext,
);

console.log(decryptedMessage);
```

***

## Using Irys & TACo in production

As noted, the parameters specified in this guide are for testing and hacking only. For real-world use cases where uploaded data should remain private & permanent, production versions of Irys & TACo are required:

* For Irys, connect to a Mainnet Node rather than a Devnet node. This requires a wallet [funded](https://docs.irys.xyz/overview/cost-to-upload) with any of the supported Mainnet payment tokens.
* For TACo, a funded Mainnet `ritualID` is required – this connects the encrypt/decrypt API to a cohort of independently operated nodes and corresponds to a DKG public key generated by independent parties. A dedicated `ritualID` for Irys + TACo projects will be sponsored soon. Watch for updates here or in the Discord [#taco](https://discord.com/channels/866378471868727316/870383642751430666) channel.


# ComposeDB

{% hint style="info" %}
A version of this tutorial is also hosted in Ceramic's [documentation](https://developers.ceramic.network/docs/composedb/examples/taco-access-control).
{% endhint %}

{% hint style="warning" %}
The Ceramic protocol network is currently offline, with no estimated time for restoration. As a result, this TACo-ComposeDB demo is currently non-functional.
{% endhint %}

This guide explains how to integrate TACo with [ComposeDB](https://developers.ceramic.network/docs/introduction/composedb-overview), a[ ](https://ceramic.network/)GraphQL-based data service from [Ceramic](https://ceramic.network/). TACo and Ceramic are mutually complementary components of the Web3 stack, both offering developers ‘Web 2.0’ functionality without compromising on decentralization.

On completion of this tutorial, it will be possible to:

1. Specify fine-grained access control logic for encrypted data/streams saved to the ComposeDB graph database.
2. Have data consumers authenticate themselves with a reused Ceramic sign-in – a Sign-In With Ethereum message – in order to decrypt the retrieved data/stream.
3. Securely gate-keep any format, size or throughput of data stream, with access to it collectively managed by a permissionless and customizable group of TACo nodes.

## ComposeDB overview

A data service built on Ceramic, the ComposeDB graph database offers enhanced composability and ease of querying. ComposeDB comes with native GraphQL support and automatically splits read/write load for additional performance. When running a Ceramic node with ComposeDB, developers can define their[ data models](https://developers.ceramic.network/docs/composedb/create-your-composite) using GraphQL, or choose to begin indexing on existing data models already defined by the community, or both.

ComposeDB can be leveraged in concert with TACo, to ensure that returned data and messages are only decryptable by parties satisfying pre-specified conditions. Interactions with ComposeDB (via the Ceramic network) and TACo (via the Threshold network) can be architected to occur in parallel, which means non-cumulative latency. TACo's Single Sign On framework also provides the basis for extensions to broader user identity and authentication, including compatibility with Ceramic's library of [DIDs](https://github.com/ceramicnetwork/js-did) (e.g. PKH-Ethereum).

## Use case ideas

* **Social platforms**. Leverage Ceramic's verifiable credentials and TACo's credential-based decryption to ensure that private user-generated content is only viewable by those who are supposed to see it, and nobody else.
* **IoT event streams**. Let sensitive data flow from sensors to legitimate recipients, without trusting an intermediary server to handle the routing and harvest metadata. For example, a medical professional can be issued a temporary access token if a patient's wearable output data rises above a certain threshold.
* **LLM chatbots**. Messages to and from a chatbot should be 100% private, not mined by a UX-providing intermediary. Harness Ceramic's web-scale transaction processing and TACo's per-message encryption granularity to provide a smooth and private experience for users of LLM interfaces.

***

## Example application & repo

In this section of the tutorial, we will set up a simple browser-based messaging application that makes combining ComposeDB and TACo more intuitive. This involves running a local Ceramic node, to which TACo-encrypted messages are saved and immediately queryable by data requestors.

{% hint style="info" %}
This demo [repo](https://github.com/nucypher/taco-composedb) is based on a fork of[ ](https://github.com/ceramicstudio/lit-composedb?tab=readme-ov-file)ceramicstudio’s lit-composedb [repo](https://github.com/ceramicstudio/lit-composedb/), with the TACo library replacing LIT. This demonstrates how applications that have already integrated LIT’s permissioned service can easily substitute it for TACo’s decentralized access control plugin.
{% endhint %}

This demo requires:

* A [Metamask](https://metamask.io/) wallet with [Polygon Amoy](https://polygon.technology/blog/introducing-the-amoy-testnet-for-polygon-pos) testnet added, and multiple accounts to mimic a real-world decryption flow.
* A positive balance of Polygon Amoy testnet tokens (> 0.00 MATIC) held in one of the accounts, in order to satisfy the default access conditions.

First, we’ll clone the reference repository.

```
git clone https://github.com/nucypher/taco-composedb
```

Next, we install the dependencies. This requires *node v16* running in our terminal.

```
npm install 
```

Next, we’ll generate parameters for the local Ceramic node. This command defines the ComposeDB config file, and generates the admin seed & admin DID credentials.

```
npm run generate
```

The commands above only need to be executed once.

Finally, we launch the browser application and Ceramic node. This is the only step required for subsequent runs.

```
npm run dev
```

Open the app on <http://localhost:3000> and sign in with Ceramic. Note that this signature can be reused to authenticate the data consumer later in the flow, if we maintain the same session.\
\
However, we begin as the *data producer*, and enter a message into the chat box element:

<div align="left"><figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXcCOLHqCBtpWUg02CuE1ps7SbDNU4LFH4GCu3gfG9smYBejjgVqSOETy6ocBjUBppbVYy2qPllmuqKHDk1R1D7Qcf1o_85WiJczBaA6CUDPB5nt4Ctpi6GFI1SLgAtJiqSrONtDIMMOpsnwbF5OzrbNgP6Y?key=kKsOISObNWoPp0VzTRWAiQ" alt="" width="563"><figcaption></figcaption></figure></div>

When we click Send, the app will prompt us to sign the message. The TACo API requires a specific signature (i.e. not SIWE) to encrypt data, so that later, TACo nodes are able to validate that (1) we are authorized to encrypt, using that particular group of nodes, and (2) we specified the conditions for decryption.\
\
Once we have provided this signature, the message is encrypted locally and we're presented with a ciphertext:

<div align="left"><figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXe8gwQzPkMtVw1Gefjn4If41uNdZuzsa3aRoyrgAHyGE_40YsfkNC2Y-cHs91dpaxsaVPJl5DLZyc7f8hIsjYhj3RmdeNdZXm3i4gJFMI0RGmgMQuDkhKGXuuQQbIEewVY0c_y_74lIWh63Yjy_GYhkxgtG?key=kKsOISObNWoPp0VzTRWAiQ" alt="" width="563"><figcaption></figcaption></figure></div>

We now switch to the *data consumer*’s perspective by disconnecting and reconnecting with a new account in Metamask. This mimics the majority of use cases in which a data consumer will make this request from a new device or identity.

Having established a session with a new account, we will need to authenticate ourselves. When we hit the Decrypt button, it prompts us to sign in with Ceramic. Note that this signature is set to expire after 2 hours, during which time we can decrypt as many messages as we like, as the signature will remain cached.

Our authenticated identity – a unique EVM wallet address that we have proven we control – must now satisfy the prespecified conditions. In this example, the qualifying conditions are the wallet holding any amount of MATIC in a Polygon Amoy wallet greater than zero.

{% hint style="info" %}
Note that in this demo, the end-user is not choosing the conditions for data access via the browser UX. However, developers can modify these default access conditions, or enable the user to specify access conditions directly. This functionality is explored in later sections.
{% endhint %}

We’ll first attempt to decrypt whilst failing to fulfill the conditions. In this instance, this account contains zero MATIC:

<div align="left"><figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXfhwhzb9Ffw9K0j6QL__QVP8J14nc8DUJXcxLwqwQK2gnyi5FO0FZwueBP9KHF-s2BZICz2XiPNACxTMt93mWfGfu9gYZHPc2plQyScvevmgjjEi5ov5g5Nd_ttC9OJT3MK_8iXmpkKDwpmryPpnw0FTps?key=kKsOISObNWoPp0VzTRWAiQ" alt="" width="375"><figcaption></figcaption></figure></div>

As expected, we are denied access to the plaintext:

<div align="left"><figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXeHuX6s6050xVxorFAHDWfygZJyIyaneOxu5Zy1i_O4vGts3N2fSnC39Im5p1h8jPCQYY-1e_ENU_FhZtyWH8ITDOeEXpby7qRZTzfLTB_VqfQMKwSfJokgH3MVmFnrqRAxxc8zxQGZIe-wtZGIVLhXpKM?key=kKsOISObNWoPp0VzTRWAiQ" alt="" width="563"><figcaption></figcaption></figure></div>

We’ll now fund the wallet with MATIC in order to satisfy the requisite conditions. We could also switch accounts to a wallet with the requisite balance.

<div align="left"><figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXftAdpAkl2ZTX9SWyAs5yk10HV8iSTIgxHjnj5VtGOxiLlfTWqYoe54NgI-lcA0YJj7wM17TvmFcNoFSHW3zSLPnMk48IvGaFoTz2iG5XJxoYqynA6DqgjjvjbFczaYmaCToSklW2RVoT80wHZpjqIHi3Wo?key=kKsOISObNWoPp0VzTRWAiQ" alt="" width="375"><figcaption></figcaption></figure></div>

This time, when we hit the Decrypt button and ping the TACo API, the assigned group of nodes validates our fulfillment of the access conditions – without a need to authenticate again. The wallet now satisfies the required access condition and the nodes will subsequently deliver the decryption fragments required for us to decrypt. We assemble the fragments locally, and can view the original plaintext message:

<div align="left"><figure><img src="https://lh7-rt.googleusercontent.com/docsz/AD_4nXfjKA2T8gV6AsDVlmz6l_eiCnHhfTIgSuW2AfNiX-49NJjg937FbZTYBwCl54pqI6zDb-aGURqUIXvUmUc4MNjH03LElIKxGksOOts93YtBLNWyKEociujF8nG1s5rBP0PX98dZffqKVa99r2EqMamqVJe0?key=kKsOISObNWoPp0VzTRWAiQ" alt="" width="563"><figcaption></figcaption></figure></div>

To clear the session data and restart the demo, click the *Reset* button in the navigation bar.

{% hint style="warning" %}
Note that this tutorial utilizes the parameters `ritualId = 6` and `domains.TESTNET`. These refer to an permissionless DKG public key and hacker-facing testnet respectively. Although fully functional and up-to-date with Mainnet, this [development environment](/for-developers/quickstart-testnet/get-started-with-tac) is not decentralized and unsuitable for real-world sensitive data. For more information, see the trust assumptions [section](broken://pages/WugTyY8VT0l9LBPeY79b).
{% endhint %}

***

## Integration steps

Having illustrated the basic concepts via the example application, we’ll now look at the underlying code and how to configure it to your use case.

### 1. Specifying conditions & authentication

There are two distinct ways in which a *data consumer* must prove their right to access the private data. The *data producer* can customize both of these, and combine them in any way they see fit.

1. *Data consumers* must **authenticate** themselves – i.e. prove their identity. In this tutorial, we chose an EVM-based identity. More concretely, we require TACo nodes to process the claim that the requestor has already authenticated themselves within the app they are using, and they did so via Sign In With Ethereum (SIWE).
2. *Data consumers* must satisfy a set of **conditions** – i.e. some public web state that TACo nodes can either validate or invalidate. In the demo above, we simply required requestors to hold testnet MATIC. Here, we’re going to build on this by adding a time-based condition, and then combine them into a ConditionSet.

We’ll begin by specifying the authentication method and the first condition. This is the same configuration as the demo above and can be viewed in the repository [here](https://github.com/nucypher/taco-composedb/blob/main/src/fragments/chatinputbox.tsx#L26-L34). The method\
is an RPC function that checks the balance of the data consumer based on the identity they provide, which in this case will be authenticated via a EIP4361 (SIWE) message already utilized by the application. The chain ID refers to Polygon Amoy.

```typescript
import { conditions } from "@nucypher/taco"; 

const rpcCondition = new conditions.base.rpc.RpcCondition({
    chain: 80002,
    method: 'eth_getBalance',
    parameters: [':userAddress'],
    returnValueTest: {
        comparator: '>',
        value: 0,
    },
});
```

Next, we’ll logically combine this condition with a second condition via a `CompoundCondition`using the `AND` operator, which means both conditions must be satisfied for data access. The second condition allows access only until the end of 2024, via a standard block timestamp.

<pre class="language-typescript"><code class="lang-typescript">const timeBox = new conditions.base.time.TimeCondition({
    chain: 11155111,
    returnValueTest: {
        comparator: '&#x3C;=',
        value: 1735689599,
    },
});

<strong>const twoConditions = new conditions.compound.CompoundCondition({
</strong>    operator: 'and',
    operands: [rpcCondition, timeBox],
});
</code></pre>

Read more about condition types [here](/for-developers/conditions).

### 2. Encrypting & saving the data

Then, we put it all together. We specify the aforementioned testnet `domain` and `ritualId`, and also utilize a standard web3 provider/signer. The output of this function is known as a `messageKit` – a payload containing both the encrypted data and embedded metadata necessary for a qualifying *data consumer* to access the message.

```typescript
import { initialize, encrypt, conditions, domains, toHexString } from '@nucypher/taco';
import { ethers } from "ethers";

// We have to initialize the TACo library first
await initialize();

const web3Provider = new ethers.providers.Web3Provider(window.ethereum);
const ritualId = 6
const message = "I cannot trust a centralized access control layer with this message.";

const messageKit = await encrypt(
    web3Provider,
    domains.TESTNET,
    message,
    twoConditions,
    ritualId,
    web3Provider.getSigner()
);
const encryptedMessageHex = toHexString(messageKit.toBytes());
```

### 3. Querying & decrypting the data

We're now going to enable *data consumers* to access the underlying data, if and only if the two conditions we specified are satisfied. Data consumers interact with the TACo API via the `decrypt` function, including the following arguments:

* `Provider` – Web3 provider to connect to Polygon.
* `Domain` – which TACo network (`MAINNET`, `TESTNET`, `DEVNET`).
* `encryptedMessage`/`ThresholdMessageKit` – this contains the encrypted plaintext and the access conditions, supplied to the *data consumer* via a side channel.
* `conditionContext` – this enables on-the-fly, programmatic population of [context variable](/for-developers/authentication/conditioncontext-and-context-variables) values used within conditions, the most important being data consumer authentication. Developers can predicate certain authentication methods on certain conditions. For example:
  * If conditions are based around EVM state, authenticate via SIWE.
  * (In future versions) If conditions are based around social account ownership, authenticate via OAuth.

```typescript
import {conditions, decrypt, Domain, encrypt, ThresholdMessageKit} from '@nucypher/taco';
import {ethers} from "ethers";

export async function decryptWithTACo(
    encryptedMessage: ThresholdMessageKit,
    domain: Domain,
    conditionContext?: conditions.context.ConditionContext
): Promise<Uint8Array> {
    const provider = new ethers.providers.Web3Provider(window.ethereum);
    return await decrypt(
        provider,
        domain,
        encryptedMessage,
        conditionContext,
    )
}
```

In this case, the data consumer must also provide an existing SIWE message signature, to prove their on-chain identity. This is shown below, along with the definition of the function `decryptWithTaco`:

```tsx
const mkB64 = message.ciphertext;
const mkBytes = await decodeB64(mkB64);
const thresholdMessageKit = ThresholdMessageKit.fromBytes(mkBytes);

// obtain existing SIWE message and signature from application
const {messageStr, signature} = await getCeramicSiweInfo(currentAddress);

// create corresponding user authentication provider
const singleSignOnEIP4361AuthProvider = await SingleSignOnEIP4361AuthProvider.fromExistingSiweInfo(messageStr, signature);
const conditionContext = conditions.context.ConditionContext.fromMessageKit(thresholdMessageKit);
conditionContext.addAuthProvider(USER_ADDRESS_PARAM_DEFAULT, singleSignOnEIP4361AuthProvider);

// decrypt the data
decryptedMessageBytes = await decryptWithTACo(
    thresholdMessageKit,
    domains.TESTNET,
    conditionContext,
);
```

***

## Using ComposeDB & TACo in production

* For Ceramic, connect to Mainnet (`domains.MAINNET`).
* For TACo, a funded Mainnet `ritualID` is required – this connects the encrypt/decrypt API to a cohort of independently operated nodes, and corresponds to a DKG public key generated by independent parties. A dedicated `ritualID` for Ceramic + TACo projects will be sponsored soon. Watch for updates here or in the Discord[ #taco](https://discord.com/channels/866378471868727316/870383642751430666) channel.

As noted, the parameters specified in this guide are for testing and hacking only. For real-world use cases where uploaded data should remain private & permanent, the production version of TACo is required.


# Turbo

This guide explains how to integrate TACo with [Turbo](https://docs.ardrive.io/docs/turbo/what-is-turbo.html) – a bundler SDK which facilitates uploading, retrieving, indexing, and paying for data hosted on [Arweave](https://search.brave.com/search?q=arweave+docs\&source=desktop). Developers who combine TACo and Turbo can offer their end-users permanent data storage that is *private by default*, wherein all sensitive payloads are encrypted, with fine-grained access conditions, prior to being uploaded. Given that Arweave offers access to data in perpetuity, it is even more critical that non-public data will only be decryptable by legitimate and qualifying consumers.

## Turbo overview

Turbo is an OSS kit for the permaweb – helping developers upload and pay for Arweave data storage. It supports ETH payments and Ethereum-based identities, a level of interoperability that makes integration with TACo straightforward. Turbo offers optimistic data caching and indexing, and performant transaction throughput (860/sec).

## Use case ideas

* **Evidence.** Empower researchers and journalists to store critical evidence and analysis on the permaweb, without making all the findings public prematurely. This removes the burden of trusting centralized hosting, which could surveil or deny service to users, and means sensitive evidence can be selectively shared with reviewers and publishers.
* **Health records.** Don't rely on HIPAA or the moral compass of medical data custodians. Build health applications that encrypt highly sensitive patient data, such that the patient has full sovereignty over who has access to said data, and under what conditions. This encrypted data can be stored via Turbo, so patients have access to their valuable records forever.
* **Private NFTs.** The status quo of artistic NFT asset 'ownership' is little more than holding a symbolic receipt, with the actual asset hosted by centralized gate-keepers. Combine TACo, Turbo and Arweave to offer end-users the ability to purchase the *decryption rights* to a movie, track, in-game asset, or piece of art – without trusting either the access control or storage layers in order to play, watch or view their assets.

***

## Integration steps

### 1. Install TACo & Turbo

```typescript
yarn add @nucypher/taco
yarn add @ardrive/turbo-sdk
yarn add arbundles
```

### 2. Define access conditions & encrypt the data

Next, initialize the `taco-web` [library](https://github.com/nucypher/taco-web). This provides, among other functionality, the facility to create access conditions and embed them in the encrypted data payload. The conditions and ciphertext are inextricable, which ensures that only a threshold of TACo nodes can collectively enforce access.

In this example, the *data* *producer* is composing a *time-based* condition for acces&#x73;*.* This requires reading the block height of the Arweave chain, and means *data consumers* may only access the data once a certain block height has been reached. For more on condition types, check out the [Access Control](/for-developers/conditions) section.\
\
We encrypt the message using the `blockHeight` condition, specifying the devnet `domain` and `ritualID`, and a standard web3 provider/signer.

{% hint style="warning" %}
This guide utilizes the parameters `ritualId = 27` & `domains.DEVNET`. These refer to an open DKG public key and hacker-facing bleeding-edge testnet respectively, which are **not decentralized** and unsuitable for real-world sensitive data. For more information, see the [Trust Assumptions](broken://pages/WugTyY8VT0l9LBPeY79b) section.
{% endhint %}

<pre class="language-typescript"><code class="lang-typescript">import { encrypt, conditions, domains, initialize, toHexString } from '@nucypher/taco';
<strong>import { ethers } from "ethers";
</strong>
// We have to initialize the TACo library first
await initialize();

const web3Provider = new ethers.providers.Web3Provider(window.ethereum);

const blockHeight = new conditions.base.jsonApi.JsonApiCondition({
  endpoint: 'https://arweave.net/info',
  query: '$.height',
  returnValueTest: {
    comparator: '>',
    value: 1556508,
  },
});
const ritualId = 27

const message = "this will be here forever";

const messageKit = await encrypt(
  web3Provider,
  domains.DEVNET,
  message,
  blockHeight,
  ritualId,
  web3Provider.getSigner() 
);
const encryptedMessageHex = toHexString(messageKit.toBytes());
</code></pre>

### 3. Connect to Turbo & store the data

Next, connect to a Turbo signer.

{% hint style="info" %}
In this example, the data payload size is lower than 100kb, so it is currently free to upload to Arweave via the Turbo SDK.
{% endhint %}

```typescript
import { TurboFactory, TurboSigner } from '@ardrive/turbo-sdk/web';
import { InjectedEthereumSigner } from 'arbundles';

const signer = new InjectedEthereumSigner(provider);
await signer.setPublicKey();
const turbo = TurboFactory.authenticated({
  signer: signer as unknown as TurboSigner,
});

const dataItem = createData(encryptedMessageHex, signer);
await dataItem.sign(signer);
const response = await turbo.uploadSignedDataItem({
  dataItemStreamFactory: () => dataItem.getRaw(),
  dataItemSizeFactory: () => dataItem.getRaw().byteLength,
});
console.log({ response });
const encryptedMessageId = response.id;
```

### 4. Retrieve & decrypt the data

Next, use the `encryptedMessageId` to find and retrieve the encrypted payload. This can be done via any Arweave gateway.

TACo's decryption logic involves (1) a decryption request sent to the TACo nodes enforcing access control, (2) for decryption fragments to be retrieved and assembled locally, and (3) finally for the payload to be decrypted into plaintext. These steps are all contained in the `decrypt()` function below.

```typescript
import { conditions, decrypt, ThresholdMessageKit } from '@nucypher/taco';

const response = await fetch(`https://arweave.net/${encryptedMessageId}`);
const dataJson = await response.text();
const encryptedMessage = ThresholdMessageKit.fromBytes(
  Buffer.from(JSON.parse(dataJson), 'hex'),
);

const decryptedMessage = await decrypt(
  web3Provider,
  domains.DEVNET,
  encryptedMessage,
);

console.log(decryptedMessage);
```

***

## Using Turbo & TACo in production

As noted, the parameters specified in this guide are for testing and hacking only.

For TACo, a funded Mainnet `ritualID` is required – this connects the encrypt/decrypt API to a cohort of independently operated nodes and corresponds to a DKG public key generated by independent parties. A dedicated `ritualID` for Turbo + TACo projects will be sponsored soon. Watch for updates here or in the Discord [#taco](https://discord.com/channels/866378471868727316/870383642751430666) channel.


# Value Propositions

TACo is a programmable control plug-in that makes your Web3 app more secure, more private, and much more decentralized. TACo provides APIs for application developers who require:

* end-to-end encrypted channels for their users to perform operations safely
* programmable, composable logic and conditionality
* an alternative to centralized control and its associated trust burdens
* 100% open source software, running across a permissionless network, fully decentralized from day one

**End-to-end encryption for almost everything**\
Built on the privacy-for-everyone principles of mainstream end-to-end encrypted messengers, but useful across a wider set of domains and use cases.&#x20;

**Trust-minimization via threshold cryptography and a collusion-resistant node array**\
Key material management and condition verification are operationally distributed across a diverse array of machines/servers, run by economically independent individuals and commercial entities.

**Powerful conditionality**\
Operations can be made contingent on the fulfillment of predefined [conditions](/for-developers/conditions), and those conditions attached to varying granularity – eg. for decryption, a single message, or an entire table, or a petabyte of video footage.

**Flexible condition composability**\
Conditions of all types – NFT-holding, RPC, time-based, known keypairs – can be mixed-and-matched using logical operators and flexible prefix notation into virtually any desired combination. Conditions can also be flexibly surfaced at different stages of runtime.

**Full control over access managers & network parameters**\
Developers have total and perpetual control over the cohort(s) of nodes which manage allowed application operations. Network parameters, such as the size or composition of the cohort, can be tuned directly by the developer, or packaged into user-facing optionality to accommodate diverse risk preferences.

#### **TACo vs. alternatives**

If your app handles any form of private or sensitive data, or require automated signing workflows your choices as a developer, excluding TACo, are fairly limited:

1. **Basic** **Public Key Infrastructure**. Although PKI/PKE typically takes place in the client, and is therefore privacy-preserving (and free), it requires knowing the identifier of a data consumer in advance, the data producer being online at the moment of sharing, and doesn't typically scale beyond a demo or app prototype.
2. **Cloud Key Management Systems**. Although straightforward to integrate (and cheap), using a KMS effectively trusts the cloud provider with all of your users' data, given that they ultimately control the master key and can theoretically (and silently) access everything.
3. **Decentralized-In-Name-Only Protocols**. Although DINO projects claim they are decentralized (or will eventually decentralize), their 'network' is often a permissioned group of insiders – who all know each other and have zero (crypto)economic incentive not to collude – they can get together to decrypt sensitive data quietly and unbeknownst to end-users. In addition, from a regulatory perspective, a permissioned coterie primarily based in a single jurisdiction is a regulatory single-point-of-failure.
4. **Trusted Execution Environments**. Although TEEs have powerful capabilities, they come with an opaque and unpredictable supply chain risk, with many examples of vulnerabilities and exploits. Moreover, they are costly and challenging to run, and therefore difficult to decentralize and expensive – this is restrictive overkill for a lightweight access control service.
5. **Secret Sharing Schemes (SSS).** Tends to be best suited of one-time recovery scenarios since the secret is explicitly revealed after combining secret shares. This contrasts with threshold decryption systems like TACo, where partial decryption shares are combined for ciphertext decryption without ever exposing the underlying private key.

##

<br>


# Use cases

Decentralized end-to-end encryption is applicable to virtually any web or web3 system that handles private data, and wishes to provide a surveillance & censorship-free experience to their users.

Today, TACo is integrated into various real-world applications, each handling sensitive real-world data. Here is a selection of case studies in which the TACo network is an indispensable component:

**Seed phrase recovery & transfer**

**Digital Rights Management for on-chain assets**

**Trustless channels for journalists, archivists & whistleblowers**

**Crowdsourcing real-world data with trustless contribution**


# Trust Model

This page explains the principles behind the overall trust model, considers the more subjective or domain-specific aspects of the model with respect to risk and security, and touches upon potential future features that would reduce the trust burden and provide more optionality for developers and end-users.

### Cohort-related trust assumptions

The foundation of the TACo trust model – and much of threshold cryptography in general – is the concept of a *Cohort;* a group of nodes employed to:

1. Collectively generate and manage part of a public key that can be used to encrypt one or more data payloads.
2. Collectively manage a decryption fragment associated with a single data payload and provide the fragment to requesters who fulfill pre-specified conditions.
3. Collective manage s signing key used for generating signatures for signing requests

All Cohorts are parametrized on formation, including the Cohort *size* (`n`) and Cohort *threshold* (`m`). These parameters are the inputs for the following core trust assumptions:

#### (1) Orderly Threshold

The first is the *orderly threshold* assumption, wherein the protocol relies on a minimum number – the threshold – of node operators within each Cohort to follow the protocol correctly. For example, a 16-of-32 cohort would require at least **16** nodes to be online, responsive, and run an up-to-date version of TACo software. If any fewer than 16 are online, data requesters will be unable to retrieve decryption fragments.

#### (2) Honest Threshold

The second is the *honest threshold* assumption, the protocol's most fundamental form of collusion-resistance – that is, protection against deliberate, unlawful attempts to access private data. In this case, the protocol relies on a minimum number of nodes to be ‘honest’ – i.e. not susceptible to bribery, coercion,, or other attempts to maliciously collude. This minimum is calculated as the threshold node count (`m`) subtracted from the total cohort size, plus one (`n - m + 1`). Using the same example as before, a 16-of-32 Cohort would require a minimum of 32 - 16 + 1 = **17** honest nodes. In other words, if at least 17 individual operators refuse to collude, there is nothing the remaining 15 nodes can do, regardless of their war chest or aggregate deposit power.

Note that the *orderly threshold* and *honest threshold* assumptions are conceptually similar to the more common *honest majority* assumption. However, they are more flexible than simply requiring those who control 67% of the deposited collateral to be honest. Unlike most BFT or pBFT-based protocols, the *honest threshold* can be partially decoupled from the nodes wealth, depending on the cohort sampling parameters specified by the developer or end-user (see next section).

### Nodes sampling-related trust assumptions

The *orderly threshold* and *honest threshold* trust assumptions above treat each Cohort as an isolated group, where the chosen parameters (`m-of-n`) determine the group’s redundancy, latency, and collusion resistance. However, the reality is that each Cohort is selected from a larger sample of Threshold nodes, which is larger than the typical/optimal size of each cohort.\
\
Therefore, the mechanisms through which nodes are selected to form Cohorts carry their own trust assumptions. More specifically, the *sampling parameters* impact the security and collusion-resistance of a given data-sharing flow. Sampling parametrization can be divided into; (1) those relating to frequency and prompting of (re-)sampling, and (2) compositional requirements to form a Cohort, besides the top-level `m` & `n` parameters.


# Fee Model

{% hint style="warning" %}
Threshold Access Control is not currently supported by an stable cohort of node operators running TACo clients, and hence there is no active fee model or paywall. The network will be relaunched by WEDF in Q3 2026.

Until then, this page serves solely as a open source reference and blueprint for the community.&#x20;
{% endhint %}

### `sponsor` & `cohortAdmin` Roles

Adopting developers pay in advance for use of TACo mainnet by transacting with the relevant contract, triggering the cohort formation to have a group of TACo nodes under their exclusive control. There are two key roles associated with a given DKG ritual:

* `sponsor`*:* This address sends the initial transaction, triggering the cohort formation, and also paying the upfront fees. This address does not have any special privileges or power over the cohort of nodes. Indeed, any EOA can create a new cohort or sponsor an existing cohort.
* `cohortAdmin`*:* This address has unilateral control over the parameters governing the cohort of nodes. Note that the `cohortAdmin` address does not have to participate during the initiation process.

The `sponsor` and `cohortAdmin` roles can use the same address or use different addresses. For example, the `cohortAdmin` could be a cold wallet address, while the `sponsor` might simply be a one-off software address. External developers may also prefer to set a DAO, a Multisig, or any kind of smart contract as the `cohortAdmin`, which would reduce the trust burden on their end-users with respect to control over encryptors and the TACo cohort.

Currently, cohort formation is not permissionless and must be pre-approved in the [`Coordinator`](https://github.com/nucypher/nucypher-contracts/blob/main/contracts/contracts/coordination/Coordinator.sol) contract by the NuCypher team.&#x20;

### Fee structure

#### **Threshold Decryption**

Adopting developers pay for the TACo service via a dual fee model, which covers:

1. Availability of the service, via a **duration-based fee**\
   \&#xNAN;*Currently 0.75 DAI per node per day*
2. Usage of the service via a **fee based on the number of unique data producer identities encrypting data at any one time**\
   \&#xNAN;*Currently 2.5 DAI per encryptor slot per year*\
   \
   Note that encrypting privileges can be added and removed from identities/addresses at will, without charge or limit, provided the sponsor has pre-paid for sufficient credits and there are encryptor slots available.

There is no charge, payment gate or limit on:

* encryptions
* types or combinations of conditions specified
* unique requestor identities
* throughput/number of requests
* decryptions
* condition validations/invalidations
* additions/removals of addresses to/from the `authAdmin` list
* additions/removals of addresses to/from the encryptor allowlist
* any other communication with the network or API


# Threshold Action Control

Conditions-based threshold signing

{% hint style="warning" %}
Threshold Action Control is not currently supported by active/available infrastructure (e.g. testnets, TACo node clients). This page serves solely as a open source reference and blueprint.&#x20;
{% endhint %}

*Action Control* enables the generation of user-controlled, automated, multi-party digital signatures leveraging both signing data-specific and existing conditions available in TACo.

Like access control, action control is executed by a decentralized cohort of nodes, but instead of recovering a plaintext if conditions are satisfied, they collaboratively produce a digital signature. This aggregated signature can be used to authorize account-abstraction blockchain transactions, attest to off-chain data, or approve sensitive workflows – **only if the pre-specified conditions are fulfilled**.

## Key concepts

### **Threshold Signing**

TACo's Action Control works similarly to a multisig system: each node in a signing cohort independently produces an ECDSA signature, and a **threshold** number of those signatures are aggregated by simply appending them together.

A threshold of cohort nodes must be online and approve the request by signing the provided payload. The resulting overall signature is an ordered bundle of 65-byte ECDSA signatures.

Each signing cohort is associated with a **multisig smart contract** (for each relevant supported chain) that implements `isValidSignature(hash, signature)` according to [ERC-1271](https://eips.ethereum.org/EIPS/eip-1271). This contract can validate the aggregated signature against the known set of signing addresses for the cohort.

Alternatively, integrators can implement their own verification logic:

* Split the aggregated signature into 65-byte chunks.
* Recover the signer from each chunk.
* Ensure that each recovered address is part of the cohort and that at least the threshold number have signed.

### **Conditionality**

Signing conditions are at the heart of TACo Action Control's utility. They can be used as safety mechanisms/guardrails, to program oracle logic, or to facilitate automation workflows.\
\
Conditions are categorized as follows:

* [Signing Object Conditions](/for-developers/conditions/signing-object-conditions)\
  \&#xNAN;*Example:* only sign the UserOperation if the transaction sum is < 0.1 ETH.
* [TimeCondition](/for-developers/conditions/timecondition)\
  \&#xNAN;*Example:* only sign after a certain timestamp.
* [RpcCondition](/for-developers/conditions/rpccondition)\
  \&#xNAN;*Example:* only sign if the requestor address holds a minimum ETH balance.
* [ContractCondition](/for-developers/conditions/contractcondition)\
  \&#xNAN;*Example:* only sign if the execution wallet holds a special-purpose NFT.
* [JSON Endpoint Conditions](/for-developers/conditions/json-endpoint-conditions)\
  \&#xNAN;*Example:* only sign – enabling an executable discount on event tickets – if the temperature is below freezing, according to a multiple weather APIs.
* [JWTCondition](/for-developers/conditions/jwtcondition)\
  \&#xNAN;*Example:* only sign a pre-generated JWT if it is scoped to expire within 1 minute.

Conditions are defined **per chain**. This means that: you can define different signing conditions for different chains (e.g., Ethereum Mainnet vs. Base). However, a single cohort **cannot** have multiple sets of conditions for the **same chain**. This design ensures that policy enforcement remains deterministic and auditable per environment.

## Signing Flow

At it's simplest, TACo Action Control works as follows:

1. Define top-level signing [conditions](/for-developers/conditions) for the cohort of TACo nodes.\
   This could include introspection of the object being signed, wallet ownership, contract call results, Web 2.0 responses. Only a `cohortAuthority` can set top-level conditions.
2. Clients submit signing requests to the Action Control cohort, including:
   * The identifier for the cohort.
   * The chain ID for the signature.
   * The data object to sign e.g. a `UserOperation` from [ERC-4337](https://www.erc4337.io/), attestation data, etc.
3. Nodes in the Action Control cohort independently evaluate the conditions. If they are satisfied, nodes will each produce an ECDSA signature, with a threshold of signatures required.
4. The Action Control API assembles the aggregated ECDSA signatures into a threshold of signatures. This can be returned to the client and verified using the multisig contract associated with the signing cohort. Alternatively the aggregated signature can be split into it's corresponding individual signatures and verified them based on each node signer addresses.

## Use Cases

Action Control introduces a new primitive in the TACo toolkit:

* **Automated authorization** with policy-controlled triggers.
  * Composable with existing signing/decryption conditions.
  * Guardrails built-in **-** custom conditions, limited scope of actions.
* **Decentralized attestations** for identity, credentials, or events.
  * No single point of failure – signing requires consensus among nodes,
* **Verifiable approvals** for smart contracts or external systems.
  * Delegated signing with user-defined constraints.


# Integration Guide

{% hint style="warning" %}
Threshold Action Control is not currently supported by active/available infrastructure (e.g. testnets, TACo node clients). This page serves solely as a open source reference and blueprint.&#x20;
{% endhint %}

## Guide&#x20;

The TACo SDK allows you to implement conditional signing for [ERC-4337](https://www.erc4337.io/) (a.k.a *Account Abstraction*) UserOperations. In just a few minutes, you'll be able to:

* **Define signing conditions** – Specify rules that must be fulfilled before UserOperations can be signed
* **Request signatures** – Get signatures from the decentralized network when conditions are met
* **Execute signed UserOperations** – Use the aggregated signature to execute transactions via Account Abstraction

### 1. Installation

Install `@nucypher/taco`, `@nucypher/taco-auth`, and `ethers` with your favorite package manager:

```bash
npm install @nucypher/taco @nucypher/taco-auth ethers@5.7.2
```

### 2. Configuration

To run the code examples below, you will need a `cohortId` signing parameter. For development and testing, we provide publicly available testnet cohorts:

| Network | Domain   | Cohort ID | Chain ID | Description        |
| ------- | -------- | --------- | -------- | ------------------ |
| Lynx    | `DEVNET` | 1         | 11155111 | Development cohort |

For production use, please [contact us](https://docs.taco.build/contact) to have a dedicated cohort configured.

### 3. Define Signing Conditions (Admin Only)

Only the cohort admin can set conditions. The testnet cohort comes pre-configured with a time-based condition that allows signing after a certain timestamp.

Admins can set conditions on the contract easily:

```typescript
import { conditions, domains, setSigningCohortConditions } from '@nucypher/taco';
import { ethers } from 'ethers';

// Connect with admin wallet (only admin can set conditions)
const signerProvider = new ethers.providers.Web3Provider(window.ethereum);

// Example: Define a time-based condition
const timeCondition = new conditions.base.time.TimeCondition({
  returnValueTest: {
    comparator: '>',
    value: Math.floor(Date.now() / 1000), // Current timestamp in seconds
  },
});

// Set conditions for your cohort (requires admin privileges)
await setSigningCohortConditions(
  signerProvider,
  domains.DEVNET,
  timeCondition,
  cohortId, // your cohort ID
  chainId,
  signerProvider.getSigner()
);
```

### 4. Request Signatures

Once conditions are set, you can request signatures for UserOperations that meet those conditions:

```typescript
import { conditions, domains, signUserOp, initialize } from '@nucypher/taco';
import { UserOperation } from '@nucypher/shared';
import { ethers } from 'ethers';

// Initialize TACo library (required once per application)
await initialize();

// Set up provider
const ethProvider = new ethers.providers.JsonRpcProvider('https://ethereum-sepolia-rpc.publicnode.com');

const userOp: UserOperation = {
  sender: '0x...',
  nonce: 0,
  callData: '0x...',
  callGasLimit: 50000,
  // ... other UserOperation fields
};

const conditionContext = await conditions.context.ConditionContext.forSigningCohort(
  ethProvider,
  domains.DEVNET,
  1, // cohortId
  11155111 // chainId
);

const aaVersion = '0.8.0';

const signResult = await signUserOp(
  ethProvider,
  domains.DEVNET,
  1, // cohortId
  11155111, // chainId
  userOp,
  aaVersion,
  conditionContext,
);

console.log('Message Hash:', signResult.messageHash);
console.log('Aggregated Signature:', signResult.aggregatedSignature);
```

### 5. Execute with Signature

The aggregated signature can now be used to execute the UserOperation through an ERC-4337 bundler:

```typescript
// Add the signature to your UserOperation
userOp.signature = signResult.aggregatedSignature;

// Submit to your ERC-4337 bundler
// The bundler will validate the signature against the multisig contract
const bundlerResponse = await bundlerClient.sendUserOperation(userOp);

// The multisig contract's isValidSig method is called automatically
// by the EntryPoint contract during UserOperation validation
```

The signature validation happens automatically when the bundler submits the UserOperation to the EntryPoint contract. The EntryPoint calls the account's `validateUserOp` function, which in turn verifies the signature using the multisig contract's `isValidSig` method.

### Next Steps

* **Advanced Conditions**: See the [Signing Object Conditions Guide](/for-developers/conditions/signing-object-conditions) for more complex condition examples
* **Production Access**: [Contact us](https://docs.taco.build/contact) for a dedicated production cohort
* **Examples**: Check out [example implementations](https://github.com/nucypher/taco-web/tree/main/examples)

For additional support, join our [Discord community](https://discord.gg/taco)


# Action Control Product Info

{% hint style="warning" %}
Threshold Action Control is not currently supported by active/available infrastructure (e.g. testnets, TACo node clients). This page serves solely as a open source reference and blueprint.&#x20;
{% endhint %}

TACo Action Control enables decentralized, conditional signing of data using threshold cryptography. It allows applications to request signatures from a distributed network of nodes, where signatures are only generated when predefined conditions are met.

## Problems Addressed

### 1. Centralized Signing Authority

**Problem**: Traditional signing systems rely on a single key holder, creating security vulnerabilities and single points of failure.

**Solution**: TACo distributes signing authority across multiple nodes using threshold cryptography. No single entity can unilaterally generate signatures, reducing the risk of non-availability, key compromise, or malicious signing.

### 2. Unconditional Signing Powers

**Problem**: Once an entity is granted signing authority, they can sign any data at any time without restrictions. Providing unrestricted access to private keys to automated system such as bots or trading agents poses significant security risks.

**Solution**: TACo allows delegating limited signing authority to automated systems and strictly enforces conditional signing, where signatures are produced only when predefined conditions are independently verified by multiple signers, each and every time. This enables fine-grained, dynamic control over what can be signed and under what circumstances, without exposing master keys.

## TACo Signing vs. Alternatives

* **Custodial Signing APIs.** Centralized services (e.g., Fireblocks, Coinbase Custody) can be used to manage signing keys on behalf of users or applications. However these present with a s*ingle point of failure and trust,* where if the provider is compromised or misbehaves, the signing authority is at risk. There is typically no native support for programmable access control.
* **Threshold MPC Services.** Proprietary threshold signing solutions using multi-party computation (MPC) are often provided as SaaS. Such services are opaque and vendor-locked. The policy or signing condition logic is often inaccessible or non-verifiable, and reliance on a single provider reduces decentralization and auditability.
* **Basic Multisig Wallets.** On-chain wallets (e.g., Gnosis Safe) similarly require M-of-N approvals to execute transactions. However, practically, they are manual and rigid, requiring individual wallet owners to coordinate signatures manually. It also lacks support for automated, dynamic policies based on real-time data or off-chain claims.
* **Single-Key Wallets.** Traditional wallets or smart contract accounts can be secured by a single private key. In such cases, there is no fault tolerance and if the key is lost or compromised, the signing authority is irrecoverably broken. Additionally, there is no native support for automated/conditional logic or multi-party governance.


# Use Cases

### Community Bot-Assisted Transactions

Community platforms (e.g. a Discord server) leverage TACo, account abstraction, and specialized bots for secure member-to-member transactions .

* **Challenge**: User wants to tip another user without setting up their own wallet, nor trusting a Discord bot with unconstrained access to their funds.
* **Solution**: Conditional signing that only allows transactions that meet specific criteria – amount limits, recipient validation, rate limiting, etc.
* **Benefits**: Users can tip each other and manage their smart account from within their communication application via bot slash commands, with guardrails preventing errant or malicious transactions.

### DAO Treasury Management

Decentralized organizations use TACo to automate and safeguard Treasury/DAO operations and transactions.

* **Challenge**: DAO requires multisig functionality with programmable and governance-driven execution conditions, without requiring manual approvals at each vote or payroll event.
* **Solution**: Conditional signing with execution conditions based on election/governance/proposal/council outcomes and higher-level 'constitutional' guardrails.
* **Benefits**: Automated and trust-minimized execution of approved transactions without manual coordination, signatory availability risk, or human error.

### Trading Agents and DeFi Automation

Automated trading systems leverage TACo for safe and constrained trade execution.

* **Challenge**: Trading bots need to execute trades but require safeguards against exploits, hallucinations, and illicit behavior.
* **Solution**: Conditions that validate trade parameters – slippage limits, approved tokens, maximum position sizes, etc.
* **Example**: A trading agent can only execute transactions that have been signed to confirm the proposed trade:
  * Is via approved DEXs.
  * Is within position size limits.
  * Meets minimum return thresholds.
  * Executes during specific time windows.

### Gaming and NFT Platforms

Gaming platforms use TACo for in-game transactions.

* **Challenge**: Players need transaction signing without exposing their primary wallets.
* **Solution**: Game-specific conditions that limit signing to valid game actions.
* **Example**: A gaming application can only execute transactions that have been signed to confirm the proposed action falls into one of these categories:
  * Transfer of game assets between validated players.
  * Remains within daily transaction limits.
  * Target/utilizes verified game contracts.


# Limitations

TACo Action Control product represents cutting-edge innovation in decentralized conditional signing. As an early-stage technology pushing the boundaries of what's possible with distributed systems and cryptography, there are some current limitations to be aware of as the platform continues to evolve.

We're actively developing new capabilities and optimizations. These limitations represent the current state of the technology, not its final form.

### Current Limitations

#### Transaction Simulation

Conditions are currently evaluated based on transaction parameters rather than simulation outcomes. This means conditions can check values like gas limits, addresses, and amounts, but cannot validate what would happen if the transaction were executed. Forthcoming versions will incorporate simulation-based validation for more sophisticated condition logic.

#### On-Chain Operations

Setting and updating conditions requires on-chain transactions, which incur gas costs. While this ensures transparency and immutability of signing rules, it means condition updates cannot be instant or free.

#### Condition Complexity

While the system supports sophisticated conditional logic through compound conditions, extremely complex nested conditions may impact performance. We recommend keeping condition structures as simple as possible while meeting your security requirements.

### Looking Forward

These limitations are actively being addressed by our development team. We're committed to building the most advanced conditional signing infrastructure available.

If you'd like to know more, or would like to share feedback/ideas, please come and say hello in our [Discord](http://discord.gg/buildwithtaco).


# Contract Addresses

## Ethereum Mainnet (L1) <a href="#contracts-on-ethereum-mainnet-l1" id="contracts-on-ethereum-mainnet-l1"></a>

| **Contract**    | **Address**                                                                                                             |
| --------------- | ----------------------------------------------------------------------------------------------------------------------- |
| PolygonRoot     | ​[0x51825d6e893c51836dC9C0EdF3867c57CD0cACB3](https://etherscan.io/address/0x51825d6e893c51836dC9C0EdF3867c57CD0cACB3)​ |
| TACoApplication | ​[0x347CC7ede7e5517bD47D20620B2CF1b406edcF07](https://etherscan.io/address/0x347CC7ede7e5517bD47D20620B2CF1b406edcF07)​ |

## Polygon Mainnet (L2) <a href="#contracts-on-polygon-mainnet-l2" id="contracts-on-polygon-mainnet-l2"></a>

| **Contract**         | **Address**                                                                                                                |
| -------------------- | -------------------------------------------------------------------------------------------------------------------------- |
| Coordinator          | ​[0xE74259e3dafe30bAA8700238e324b47aC98FE755](https://polygonscan.com/address/0xE74259e3dafe30bAA8700238e324b47aC98FE755)​ |
| GlobalAllowList      | [0x3E37C7A9a83B326a0d156DE3Ee6B18fd8079f698](https://polygonscan.com/address/0x3E37C7A9a83B326a0d156DE3Ee6B18fd8079f698)   |
| PolygonChild         | ​[0x1f5C5fd6A66723fA22a778CC53263dd3FA6851E5](https://polygonscan.com/address/0x1f5C5fd6A66723fA22a778CC53263dd3FA6851E5)​ |
| ReimbursementPool    | [0x65849eaDc5cE348EaA131Fdbd35aeC9235688EEB](https://polygonscan.com/address/0x65849eaDc5cE348EaA131Fdbd35aeC9235688EEB)   |
| TACoChildApplication | ​[0xFa07aaB78062Fac4C36995bF28F6D677667973F5](https://polygonscan.com/address/0xFa07aaB78062Fac4C36995bF28F6D677667973F5)  |


# Architecture

This section provides detailed information about the TACo protocol's architecture, components, and interactions. Use the links below to explore different aspects of the architecture.

## Core Documents

* [Protocol Architecture](/reference/architecture/protocol-architecture) - Comprehensive overview of the TACo protocol layers and components
* [Protocol Flow](/reference/architecture/protocol-flow) - Detailed explanation of the protocol operations and interactions
* [UML Use Case Diagram](/reference/architecture/uml-usecase-diagram) - Visual representation of actors and use cases
* [Porter](/reference/architecture/porter) - Details about the Porter service that connects applications to the TACo node network

## Architecture Overview

TACo's architecture consists of interconnected layers and components working together to provide secure, decentralized access control:

* **Application Integration Layer** - SDK and interfaces for developers building Web3 & Web2 Applications
* **Protocol Layer** - Core protocol services and infrastructure
* **Porter Service** - Protocol abstraction and gateway
* **Web3 Integration** - Connections to blockchain networks and other web3 infrastructure

See the [Protocol Architecture](/reference/architecture/protocol-architecture) document for an in-depth exploration of these components.


# Protocol Architecture

TACo (Threshold Access Control) is a decentralized protocol that enables end-to-end encrypted data sharing with programmable access control. This document provides a comprehensive architectural overview of the protocol's components, layers, and their interactions.

<figure><img src="/files/qC45vPb5dZzytunVROaG" alt=""><figcaption><p>TACo Protocol Layers &#x26; Components</p></figcaption></figure>

## Architectural Overview

TACo's architecture consists of interconnected layers and components that work together to provide secure, decentralized access control:

* **Application Integration Layer** - SDK and interfaces for developers to implement TACo functionality
* **Protocol Layer** - Core services and infrastructure for network operations
* **Porter Service** - Protocol abstraction and gateway between applications and the node network
* **Web3 Integration** - multiple components interacts with blockchain networks and other decentralized infrastructure

These layers are not strictly hierarchical but rather interconnected systems that collaborate to achieve the protocol's objectives. Cryptographic operations are embedded throughout these layers to provide the security backbone of the protocol.

## Architectural Component Structure

### Protocol Operations Flow

TACo's security model is built on a sequence of cryptographic operations distributed across the protocol components:

1. **Distributed Key Generation (DKG)** - One-time setup process that generates public encryption keys and distributes private fragments across nodes
2. **Client-side Encryption** - Data producers encrypt content locally using the public key and specify access conditions
3. **Condition Verification & Fragment Generation** - Nodes independently verify conditions and provide decryption fragments when conditions are met
4. **Client-side Decryption** - Data consumers combine a threshold of fragments to decrypt content locally

This end-to-end encrypted approach ensures no single entity can access the data without meeting the specified conditions. The distribution of cryptographic operations across different actors provides security without centralized trust points.

For a detailed explanation of the end-to-end data sharing flow, including diagrams and examples, see [How TACo Works](broken://pages/9Ozb5lxqTHHni6lJzwo5).

### Layer Components

TACo's architecture is composed of several layers, each with specific components that provide key functionality:

#### Application Integration Layer

This layer bridges applications with the TACo protocol through developer-friendly interfaces:

* **Client SDK (taco-web)**: Primary toolkit for developers to integrate TACo functionality
  * Provides encrypt/decrypt APIs and cross-platform support
  * Streamlines implementation of complex cryptographic operations
  * Handles encryption and decryption operations on the client-side
* **Authentication Component**: Verifies identity through Web3 and OAuth methods
  * Manages Web3 signatures, SIWE messages, and/or OAuth flows
  * Handles identity verification before processing decryption requests
  * Provides secure authentication flow management
* **Access Control Framework**: Enables programmable access policies
  * Supports per-ciphertext access conditions
  * Implements various condition types:
    * **TimeCondition**: Time-based access using blockchain timestamps
    * **RpcCondition**: Using RPC calls to Ethereum API
    * **ContractCondition**: On-chain state and contract function calls
    * **JsonApiCondition**: External API data sources
    * **CompoundCondition**: Logical combinations (AND/OR/NOT)
    * **Additional Conditions**: Extensible for custom verification logic
* **Decryption Context Parameters Collection**: Manages parameters for decryption requests
  * Collects necessary context parameters
  * Secures transmission alongside ciphertext
  * Validates parameter integrity

#### Porter Service

Acts as a protocol abstraction and gateway layer:

* Provides an interface between applications and the TACo node network
* Simplifies protocol integration for developers
* Can be self-hosted or used as a public service (similar to Infura for Ethereum)

#### Protocol Layer

The protocol layer manages the ongoing operation of the TACo network:

* **TACo Node Client**: Manages core infrastructure operations
  * **Registration & Staking**: Economic security layer for node participation
  * **DKG Participation**: One-time key generation process triggered by Cohort Admin
  * **Condition Verification**: Independent validation of access conditions
  * **Fragment Generation**: Creation of decryption shares when conditions are fulfilled
* **Staking & Governance**: Coordinates network security
  * **Protocol Governance**: Parameter management for the protocol
  * **Staking & Slashing**: Economic incentives to ensure node compliance
  * **Node Monitoring**: System health verification
  * **Economic Security**: On-chain management via smart contracts
* **Protocol Coordination**: Manages network orchestration
  * **Ritual Coordination**: Orchestrates node participation in DKG processes
  * **RitualID Management**: Tracks unique identifiers for specific DKG rituals
  * **Protocol Management**: Handles protocol-level activities with smart contract support
* **Economic Framework**: Handles payments and incentives
  * **Fee Collection & Distribution**: Processing payments to node operators
  * **Subscription Management**: User payment models and access tiers
  * **Payment Infrastructure**: Financial transactions supported by on-chain mechanisms

#### Web3 Integration

Connects TACo to blockchain networks and decentralized infrastructure:

* **Blockchain Verification**: Uses on-chain data for condition validation
* **Blockchain Interactions**: Leverages blockchain infrastructure for governance and staking
* **Decentralized Identity**: Incorporates Web3 signatures and SIWE messages

### End-User Applications

TACo can integrate with various application types through the Application Integration Layer.

## System Actors & Interaction Model

The TACo protocol architecture defines four primary actors who interact with the system in distinct ways:

### Core System Actors

* **Adopting Developer (Cohort Authority)**: Application developers who adopt TACo, configure network parameters, and manage node cohorts
* **Data Producer**: Entities that encrypt data using TACo and define access conditions
* **Data Consumer**: Users who request and decrypt data when they meet the specified conditions
* **Node Operator**: Infrastructure providers who participate in DKG and provide decryption services

### Protocol Interaction Patterns (UML Use Cases)

TACo involves several key interaction patterns that define how actors engage with the protocol:

* **Distributed Key Generation (DKG)** - One-time setup process for generating encryption keys
* **Cohort Management** - Activities for managing node participation and parameters
* **Data Encryption with Conditional Access** - Process for securing data with programmable access rules
* **Conditional Data Decryption** - Workflow for accessing encrypted data when conditions are met

For detailed descriptions of these interaction patterns, including primary actors, supporting actors, and full interaction flows, see the [UML Use Case Diagram](/reference/architecture/uml-usecase-diagram) document.

## System Interconnections

### Layer Interactions

The TACo protocol's effectiveness comes from the coordinated interactions between its components:

* **Web Applications → SDK**: End-user applications interact with the protocol through the Client SDK
* **SDK → Porter → Node Client**: The SDK communicates with nodes through Porter gateway services
* **TACo System ↔ Web3**: The protocol integrates with blockchain networks for verification and coordination
* **Node Client Operations**: Nodes participate in DKG and later provide decryption shares when conditions are met

### Data Flow Directions

* **Application Integration Flow**: Web3 & Web2 applications connect to the TACo ecosystem through the Application Integration Layer (using the Client-SDK: taco-web)
* **Layer Communication**: Components in the Application Integration Layer communicate with the Protocol Layer through Porter Service that abstracts and simplifies communication.
* **Protocol Operations Flow**:
  * DKG initialization (one-time setup by cohort authority)
  * Local encryption by data producers
  * Independent condition verification by nodes
  * Fragment provision to qualifying data consumers
  * Client-side decryption with threshold of fragments

This interconnected architecture ensures that the TACo protocol maintains security and privacy without requiring trust in any central authority, while providing developers with flexible tools for implementing sophisticated access control systems.

**Learn More:**

* For a detailed technical explanation of protocol operations, see the [Protocol Flow](/reference/architecture/protocol-flow) document
* For an end-to-end overview of how data moves through the TACo ecosystem, see [How TACo Works](broken://pages/9Ozb5lxqTHHni6lJzwo5)


# Protocol Flow

This document details the operational flow of the TACo protocol - how information and operations move through the system. For an overview of the actors involved and their roles, see the [UML Use Case Diagram](/reference/architecture/uml-usecase-diagram).

The TACo protocol operations follow several distinct but interconnected flows:

## Cohort Formation

* Adopting developers (`cohortAuthority`) initiate and manage a one-time cohort formation network setup
* The initialization produces a logical group of nodes with each node maintaining its own private material
* The `cohortAuthority` manages parameters for the cohort
* A minimum of one honest party is required during cohort formation to ensure that private material is not spoofed

## Condition Configuration

* Programmable conditions define who can execute a successful TACo operation (decryption/signing). Conditions are either associated with encrypted payloads (encryption) or configured for a signing cohort.

## TACo Services

* Nodes independently verify that the consumer satisfy the requisite conditions before servicing any request
* For each node that validates the conditions, a response fragment is provided to the consumer
* Once a threshold of nodes have provided their fragments, the consumer can locally combine these fragments to complete the operation

## Cohort Management

* Node participation is secured through economic staking in the TACo Nodes Network
* Cohorts can rotate members according to predefined rules set by the `cohortAuthority`
* The rotation rules can be tailored to balance security, availability, and decentralization needs

For a more detailed explanation of the protocol operations, see [How TACo Works](https://github.com/nucypher/taco-docs/blob/main/reference/readme/key-concepts.md).

## Integration Points

The protocol flow integrates with various TACo components:

* **User applications** interact with the TACo protocol through the Client SDK (taco-web)
* **Programmable Conditions** enables the definition and validation of conditional execution of operations
* **TACo Nodes Network** provides the economic staking mechanism for node operators
* **Coordinator contracts** manage cohort formation on-chain

For details on how these components relate to each other architecturally, see [Protocol Architecture](/reference/architecture/protocol-architecture).

For conceptual explanations of the protocol's design principles, see [How TACo Works](https://github.com/nucypher/taco-docs/blob/main/reference/readme/key-concepts.md).


# UML Use Case Diagram

The TACo protocol enables secure, conditional access to encrypted data across a decentralized network. This document outlines the primary actors in the TACo ecosystem and their key interactions with the protocol.

## Core Actors

<figure><img src="/files/a4z9QIMWIPHi0lv6VjpE" alt=""><figcaption><p>TACo Protocol's UML Use Case Diagram</p></figcaption></figure>

The TACo protocol facilitates interactions between four main actors:

### Adopting Developer (Cohort Authority)

The adopting developer integrates TACo into their application and becomes the `cohortAuthority`, responsible for:

* Initiating and managing the Distributed Key Generation (DKG) process
* Setting network parameters for their application's cohort
* Managing cohort composition and rotation policies
* Transferring cohort authority (optionally) to a multisig or DAO for decentralized governance

### Data Producer

Data producers are users or systems that want to share encrypted data with conditional access:

* Encrypt content using the public key generated by the DKG
* Specify access conditions that must be met before content can be decrypted
* Create logical combinations of conditions (AND, OR, NOT) for complex access patterns
* Distribute the encrypted content via channels outside the TACo protocol

### Data Consumer

Data consumers are users or systems that request access to encrypted content:

* Request decryption services from the TACo network
* Present authentication and verification materials to prove they meet access conditions
* Receive decryption fragments from nodes when conditions are met
* Combine fragments locally to decrypt content

### Node Operator

Node operators are entities that run TACo nodes in the network:

* Participate in DKG rituals to generate and manage key material
* Verify access conditions when decryption is requested
* Provide decryption fragments to qualified data consumers
* Stake tokens to secure network participation

## Primary Use Cases

### Distributed Key Generation

The DKG process is the foundation of the TACo protocol:

1. The adopting developer initiates the DKG ritual
2. A cohort of nodes (30-100) is selected from the network
3. Nodes collaboratively generate a unified public key and distribute private key fragments
4. The public key becomes available for data encryption

### Conditional Data Encryption

Data producers use TACo to create encrypted content with programmable access control:

1. The data producer retrieves the public key for their target application
2. They define access conditions that must be met for decryption
3. They encrypt their content locally using the public key
4. The conditions are embedded with the encrypted content
5. The encrypted package is distributed to potential consumers

### Conditional Data Decryption

Data consumers request access to encrypted content:

1. The consumer requests decryption from the TACo network
2. They provide authentication and condition-fulfillment proofs
3. Network nodes independently verify the consumer meets all conditions
4. Qualifying consumers receive decryption fragments from nodes
5. Once a threshold of fragments is received, the content can be decrypted locally

### Cohort Management

The TACo protocol includes mechanisms for managing node participation:

1. The cohortAuthority establishes cohort parameters
2. Node operators stake tokens to participate in the network
3. The cohortAuthority can define rotation policies for cohort membership
4. Cohort composition may change over time according to predefined rules

For a detailed explanation of how these use cases are implemented in the protocol's operations, see the [Protocol Flow](/reference/architecture/protocol-flow) document.


# Porter

## Overview

Porter can be described as the *“Infura for TACo”*. Porter is a web-based service that performs TACo-based protocol operations for applications.

Its goal is to simplify and abstract the complexities surrounding the TACo protocol to negate the need for applications to interact with it via a Python client. Porter introduces the TACo protocol to cross-platform functionality, including web and mobile applications.

<figure><img src="/files/Ke5PAOfIiMzriJwk7bm0" alt=""><figcaption></figcaption></figure>

Any publicly available Porter instance can be used to interface with the TACo nodes, or some application developers opt to [run their own](https://github.com/nucypher/nucypher-porter).

## Public Porter Instances

Public Porter instances are operated by centralized entities and have different security properties than user-operated instances.

To use the default Porter URIs in `taco`:

```typescript
import { domains, getPorterUri } from '@nucypher/taco';

const porterUri = getPorterUri(domains.MAINNET);  // mainnet
// OR
const devPorterUri = getPorterUri(domains.DEVNET);  // lynx testnet

```

Alternatively, you can use your own non-default Porter URIs.


# Minimum System Requirements

{% hint style="warning" %}
The TACo network is currently dormant ahead of a relaunch by WEDF scheduled for Q3 2026. Although  independent operators may choose to continue running TACo nodes at their discretion, there is no coordinated group of providers running TACo software at this time.&#x20;

A stable version of the service will be relaunched in Q3 2026 – centered around a *Privacy Coalition.* If you represent an organization in the domains of privacy advocacy, anti-surveillance, human rights, encryption/whistleblower technology, or pertinent academic research, and would like to be involved, please get in [touch](https://discord.gg/Rh2728Hk).&#x20;

Until then, this page serves as a reference for prospective members of this new node coalition.&#x20;
{% endhint %}

TACo nodes are very light and can be run inexpensively on a remote instance or local machine. Specs similar to (or greater) than the following will suffice:

* **Debian** or **Ubuntu** (recommended distributions)
* **20GB** storage
* **4GB** RAM
* **x86** architecture
* Static IP address
* Exposed TCP port **9151**

TACo can be run on cloud infrastructure. For example, a Digital Ocean [4GB Basic Droplet](https://www.digitalocean.com/pricing/droplets) satisfies the requirements listed above.

{% hint style="info" %}
The current version of TACo required the operator account (i.e. not the provider) to be funded with at least 15 POL (Polygon POS) to connect to the network and participate in DKG initialization rituals. This may be deprecated ahead of the network relaunch Q3 2026.
{% endhint %}


# Run a TACo Node with Docker

{% hint style="warning" %}
The TACo network is currently dormant ahead of a relaunch by WEDF scheduled for Q3 2026. Although  independent operators may choose to continue running TACo nodes at their discretion, there is no coordinated group of providers running TACo software at this time.&#x20;

A stable version of the service will be relaunched in Q3 2026 – centered around a *Privacy Coalition.* If you represent an organization in the domains of privacy advocacy, anti-surveillance, human rights, encryption/whistleblower technology, or pertinent academic research, and would like to be involved, please get in [touch](https://discord.gg/Rh2728Hk).&#x20;

Until then, this page serves as a reference for prospective members of this new node coalition.&#x20;
{% endhint %}

## Before you begin

* Running a TACo node requires maintenance and comes with certain constraints. Please review the duties expected of a node operator, and make sure you are comfortable with the minimum deauthorization delay of 6 months.
* Please review the system requirements for provisioning the TACo service.
* Your operator account will need to be funded with at least 15 POL (Polygon POS) to connect to the Threshold network. You should transfer these funds after getting the node running.
* Once TACo is running smoothly on your machine or VPS, the next step is to authorize your stake to the TACo app and register/bond the node to that provider address.

## Technical Overview

The overall procedure for setting up a TACo Node is as follows:\
\
1\. Get Docker Image\
2\. Create Operator Ethereum Wallet\
3\. Set Passwords\
4\. Initialize the Node\
5\. Launch the Node\
6\. (Optional) Automatic Updates\
7\. (Optional) Expose Prometheus Metrics

This excludes registration and authorization, which you should attempt once completing the steps on this page.

## 1. Get Docker Image

If Docker is not already installed on your server, follow the official Docker installation [instructions](https://docs.docker.com/engine/install/ubuntu/). If you are using a DigitalOcean VPS, you may find these [instructions](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-docker-on-ubuntu-22-04) helpful.

Pull the latest Docker image from NuCypher's primary repo:

```bash
docker pull nucypher/nucypher:latest
```

Note that NuCypher is a contributing team to the Threshold Network and the primary developers of the TACo application.

## 2. Create Operator Ethereum Wallet

The *operator* is a dedicated Ethereum wallet address that will be used to identify your TACo node. You will map this address to a staking provider on the threshold dashboard later. This mapping step is also referred to as 'bonding' and 'registering'. This wallet must be in Geth-compatible JSON format ([Web3 Secret Storage Format](https://github.com/ethereum/wiki/wiki/Web3-Secret-Storage-Definition)) and can be generated with a variety of publicly available tools like [go-ethereum](https://geth.ethereum.org/) ("geth") or [MyCryptoWallet](https://mycrypto.com/).

In this step you will create an ethereum software wallet using Geth, following these installation [instructions](https://geth.ethereum.org/docs/getting-started/installing-geth). Note that installing Geth on an Ubuntu server can generate errors with newer versions. To avoid this, choose a long term support version – e.g. Ubuntu 20.04 (LTS).

Once Geth is installed, create a new ethereum wallet:

```bash
geth account new
```

A successful output should look like this:

```bash
$ geth account new
...
Your new account is locked with a password. Please give a password. Do not forget this password.
Password: 
Repeat password: 

Your new key was generated

Public address of the key:   0xdEB634255A534870505D085717898F1A8A0B53d8
Path of the secret key file: /home/user/.ethereum/keystore/UTC--2023-12-08T18-58-13.845048610Z--deb634255a534870505d085717898f1a8a0b53d8
...
```

{% hint style="info" %}
Take note of your new operator address and secret key file path, as you will need them in the next steps.
{% endhint %}

{% hint style="danger" %}
Secure and back-up your password and operator secret key file off-site. Loss of your operator wallet or password may result in service disruption, loss of rewards, and/or manual intervention.
{% endhint %}

## 3. Set Passwords

There are two passwords associated with a TACo node:

* *nucypher keystore password* - This password is used to encrypt your network participation keys. You can create this password now.
* *operator password* - This password will be used to unlock you operator ethereum wallet. Enter the same password you used when you created your (geth) wallet.

Create a plain text file named `nucypher.env` containing the following variables. Replace <...> with your passwords.

```bash
NUCYPHER_KEYSTORE_PASSWORD=<YOUR NUCYPHER KEYSTORE PASSWORD>
NUCYPHER_OPERATOR_ETH_PASSWORD=<YOUR OPERATOR ETH ACCOUNT PASSWORD>
```

## 4. Initialize the Node

TACo nodes must be initialized before launching. This is an interactive one-time step that will create network participation keys and an initial JSON configuration file:

{% hint style="warning" %}
Initializing a TACo node is a one-time procedure that requires you to secure a mnemonic seed phrase. This phrase is used to derive cryptographic keys used in TACo protocols. It is critical to maintain custody of the mnemonic in case of password loss or host relocation.
{% endhint %}

{% hint style="danger" %}
Loss of the TACo secret mnemonic may result in your stake being slashed.
{% endhint %}

<pre class="language-bash"><code class="lang-bash">docker run -it --rm                        \
--name ursula-init                         \
<strong>-v ~/.local/share/nucypher:/root/.local/share/nucypher:rw \
</strong>-v ~/.ethereum/:/root/.ethereum:ro         \
-p 9151:9151                               \
--env-file nucypher.env                    \
nucypher/nucypher:latest                   \
<strong>nucypher ursula init                       \
</strong>--signer keystore:///root/.ethereum/keystore/&#x3C;WALLET FILENAME> \
--domain mainnet                           \
<strong>--eth-endpoint &#x3C;ETH PROVIDER URI>          \
</strong><strong>--polygon-endpoint &#x3C;POLYGON PROVIDER URI>  \
</strong>--operator-address &#x3C;OPERATOR ADDRESS>      
</code></pre>

Replace the following values with your own:

* `<ETH ENDPOINT URI>` The URI of an ethereum JSON-RPC endpoint (e.g. `https://infura.io/…`)
* `<POLYGON ENDPOINT URI>` The URI of a polygon JSON-RPC endpoint (e.g.. `https://infura.io/...`)
* `<WALLET FILENAME>` The *filename* of your operator software wallet
* `<OPERATOR ADDRESS>` The dedicated ethereum address to be used by the TACo node

Follow the in-terminal prompts. You will see a public key for your TACo node and be assigned a mnemonic phrase.

## 5. Launch the Node

{% hint style="danger" %}
The first time a taco node is launched the public key generated in the previous step is committed on-chain. After this commitment, loss of the private keys is a protocol offensive that will result in reward withholding and/or stake slashing.
{% endhint %}

Run the following command to launch the node:

```bash
docker run -d                     \
--name ursula                     \
--restart unless-stopped          \
-v ~/.local/share/nucypher:/root/.local/share/nucypher:rw \
-v ~/.cache/nucypher:/root/.cache/nucypher:rw \
-v ~/.ethereum/:/root/.ethereum:ro   \
-p 9151:9151                      \
--env-file nucypher.env           \
nucypher/nucypher:latest          \
nucypher ursula run 
```

Successful execution will resemble this example:

```bash
$ docker run -d --name ursula ...
5ecaa04eb319da576c3b2fa2b8aee9cc1a7079cd2675e3202047b50174696a84
```

### View Logs

When your node starts up, it will connect to Polygon and Ethereum mainnet to determine if the two qualification criteria are satisfied:

1\. Operator account is funded with MATIC (Polygon POS); at least 15 MATIC is recommended.\
2\. Operator account is mapped/bonded to a staking provider.

{% hint style="info" %}
Operator bonding must be performed on the Threshold Staking dashboard. Once complete there is a \~20 minute waiting period for your node's status to be automatically bridged to Polygon.\
\
If your node is not bonded and synced the following message will be displayed in logs during this waiting period:\
\
`! Bonded staking provider address 0xDB1970...0991D096 on Mainnet not yet synced to child application on Polygon/Mainnet ; waiting for sync`
{% endhint %}

\
Verify your node is running correctly by viewing the logs:

```bash
docker logs -f ursula
```

The following is an example of the expected output for a TACo node that is both funded with\
POL and correctly bonded to an operator on the threshold dashboard.

```bash
...
! Bonded staking provider address 0xDB1970...0991D096 on Mainnet not yet synced to child application on Polygon/Mainnet ; waiting for sync
✓ Operator 0x27cd20d513cD3aB1D030e60f3aFb75599A33Bc2D is bonded to staking provider 0xDB1970e65B501f906f0fD220164800a0E824456E
! Checking provider's DKG participation public key for 0xDB1970e65B501f906f0fD220164800a0E824456E on Polygon/Mainnet at Coordinator 0xE74259e3dafe30bAA8700238e324b47aC98FE755
Broadcasting SETPROVIDERPUBLICKEY Legacy Transaction (0.021561263955835524 ETH @ 119.649197331 gwei)
TXHASH 0xa034bc89f8f30980e1222c9a17a71683849119ae7953e7c04d659a057f77f384
Waiting 600 seconds for receipt
✓ Successfully published provider's DKG participation public key for 0xDB1970e65B501f906f0fD220164800a0E824456E on Polygon/Mainnet with txhash 0x40cda7a3120d4555e64802e813f2fd9de2ea5c3616cff24393d332daa92ce2d2)
✓ Start Operator Bonded Tracker
✓ Rest Server https://182.16.254.42:9151
Working ~ Keep Ursula Online!
```

{% hint style="success" %}
Working \~ Keep Ursula Online! Indicates successful launch :tada:
{% endhint %}

## 6. (Optional) Automatic Updates

You can optionally configure your server to automatically update any running docker containers using watchtower. This will automatically relaunch your node with the same commands and environment when an update to nucypher is published:

```bash
docker run --detach \
--name watchtower   \
--restart unless-stopped \
--volume /var/run/docker.sock:/var/run/docker.sock \
containrrr/watchtower ursula --cleanup
```

{% hint style="info" %}
This command assumes the name of your node docker container is `ursula`
{% endhint %}

For more information check out the official Watchtower [documentation](https://containrrr.dev/watchtower/).

## 7. (Optional) Expose Prometheus Metrics

In order to aid with monitoring, the TACo node can expose various metrics via [prometheus](https://prometheus.io/). Ursula can optionally provide a metrics endpoint as a data source for real-time monitoring.

The metrics endpoint is disabled by default but can be enabled by providing the following parameters to the `nucypher ursula run` command:

* `--prometheus` - a boolean flag to enable the prometheus endpoint
* `--metrics-port <PORT>` - the HTTP port to run the prometheus endpoint on. If not specified, the default is port `9101`.

{% hint style="warning" %}
The docker container will need to expose the specified port i.e. add `-p <PORT>:<PORT>` to the `docker run` command. For example, if the default port (`9101`) is used then add `-p 9101:9101`.
{% endhint %}

* `--metrics-interval <INTERVAL>` - the frequency of metrics collection in seconds. If not specified, the default is `90` seconds i.e. metrics are collected every 90 seconds.

{% hint style="warning" %}
In general, metrics collection will increase the number of RPC requests made to your provider endpoint; increasing the frequency of metrics collection will further increase this number.
{% endhint %}

The corresponding endpoint, `http://<node_ip>:<PORT>/metrics`, can be used as a prometheus data source.


# TACo Node Management

{% hint style="warning" %}
The TACo network is currently dormant ahead of a relaunch by WEDF scheduled for Q3 2026. Although  independent operators may choose to continue running TACo nodes at their discretion, there is no coordinated group of providers running TACo software at this time.&#x20;

A stable version of the service will be relaunched in Q3 2026 – centered around a *Privacy Coalition.* If you represent an organization in the domains of privacy advocacy, anti-surveillance, human rights, encryption/whistleblower technology, or pertinent academic research, and would like to be involved, please get in [touch](https://discord.gg/Rh2728Hk).&#x20;

Until then, this page serves as a reference for prospective members of this new node coalition.&#x20;
{% endhint %}

### Automatically Upgrade Node

Configure your server to automatically update any running docker containers, via Watchtower. This will relaunch your node with the same environment and commands whenever a new version of nucypher is published. For more information check out the official Watchtower [documentation](https://containrrr.dev/watchtower/).

```bash
docker run --detach \
--name watchtower   \
--restart unless-stopped \
--volume /var/run/docker.sock:/var/run/docker.sock \
containrrr/watchtower ursula --cleanup
```

### Manually Upgrade Node

Alternatively, you can manually update your node. This means checking for new version releases in the Threshold [announcements](https://discord.com/channels/866378471868727316/870384195636199455) channel, then running the following commands to upgrade your node.

```bash
# Stop container
$ docker stop ursula && docker rm ursula

# Get latest image
$ docker pull nucypher/nucypher:latest

# Start node 
$ docker run -d                   \
--name ursula                     \
--restart unless-stopped          \
-v ~/.local/share/nucypher:/root/.local/share/nucypher:rw \
-v ~/.cache/nucypher:/root/.cache/nucypher:rw \
-v ~/.ethereum/:/root/.ethereum:ro   \
-p 9151:9151                      \
--env-file nucypher.env           \
nucypher/nucypher:latest          \
nucypher ursula run 
```

{% hint style="info" %}
If you run into errors, note that you have to stop the node from running before attempting to troubleshoot or attempting a different configuration.
{% endhint %}

To confirm that your node is now running the latest version, you can use any browser to check your node's status page. You may need to skip through a warning page.

```
https://<NODE_IP_ADDRESS>/status/
```

### Update Node Configuration

Configuration settings will be stored in an Ursula configuration file, `ursula.json`, stored in `/home/<user>/.local/share/nucypher` by default.

{% hint style="info" %}
After making configuration changes, the node must be restarted for those changes to take effect.
{% endhint %}


# TACo Node Recovery

{% hint style="warning" %}
The TACo network is currently dormant ahead of a relaunch by WEDF scheduled for Q3 2026. Although  independent operators may choose to continue running TACo nodes at their discretion, there is no coordinated group of providers running TACo software at this time.&#x20;

A stable version of the service will be relaunched in Q3 2026 – centered around a *Privacy Coalition.* If you represent an organization in the domains of privacy advocacy, anti-surveillance, human rights, encryption/whistleblower technology, or pertinent academic research, and would like to be involved, please get in [touch](https://discord.gg/Rh2728Hk).&#x20;

Until then, this page serves as a reference for prospective members of this new node coalition.&#x20;
{% endhint %}

As a node operator, it is critical to maintain the security of your keystores (private keys), passwords, and mnemonic phrases throughout the lifecycle of your node's operation. Ensuring the safekeeping of these elements is essential for continued access and control of your node. In the event of a loss, several built-in recovery tools are available to assist you in restoring normal operations.

Below are the three possible high-level recovery scenarios:

1. **Recovery using a backup of keystore and password**
2. **Recovery using mnemonic**
3. **Complete loss of keystore and mnemonic**

This documentation outlines the procedures to manage scenarios 1 and 2. However, please be advised that in the case of a complete loss of both the keystore and mnemonic, there are currently no recovery options available and you will need to shut down your node until a re-onboarding mechanism is included in a future software upgrade (this will result in reward withholding and/or stake slashing).

If you find yourself in this situation, please reach out for assistance by opening a support ticket in the Threshold Discord server under the `#support-ticket` channel.

### Recovery

**Recover a TACo node using a mnemonic and existing config**

This command can be used to restore private keys on an existing node.

{% hint style="info" %}
If using Docker commands, start by pulling the latest recovery image:

```bash
docker pull nucypher/nucypher:recovery
```

{% endhint %}

{% tabs %}
{% tab title="CLI" %}

```bash
nucypher ursula recover
```

{% endtab %}

{% tab title="Docker" %}

```bash
docker run -it -v ~/.local/share/nucypher:/root/.local/share/nucypher:rw -v ~/.ethereum/:/root/.ethereum:ro nucypher/nucypher:recovery nucypher ursula recover
```

{% endtab %}
{% endtabs %}

#### **Recover or relocate a TACo node by creating a new configuration with mnemonic**

This command can be used to completely relocate a node to a new host from scratch while preserving the original private keys.

{% tabs %}
{% tab title="CLI" %}

```bash
nucypher ursula init ... --with-mnemonic
```

{% endtab %}

{% tab title="Docker" %}

```bash
docker run -it -v ~/.local/share/nucypher:/root/.local/share/nucypher:rw -v ~/.ethereum/:/root/.ethereum:ro nucypher/nucypher:recovery nucypher ursula init ... --with-mnemonic
```

{% endtab %}
{% endtabs %}

### **View public keys for a given mnemonic**

This command is useful if you have a mnemonic but are unsure which public keys it produces.

{% tabs %}
{% tab title="CLI" %}

```bash
nucypher ursula public-keys --from-mnemonic
```

{% endtab %}

{% tab title="Docker" %}

```bash
docker run -it nucypher/nucypher:recovery nucypher ursula public-keys --from-mnemonic
```

{% endtab %}
{% endtabs %}

### Auditing <a href="#auditing" id="auditing"></a>

Below is documentation for node auditing commands that can be used to ensure correctness of passwords and mnemonics.Comment

#### **Audit password and mnemonic**

{% tabs %}
{% tab title="CLI" %}

```bash
nucypher ursula audit

nucypher ursula audit --config-file <config path>

nucypher ursula audit --keystore-filepath <keystore path>
```

{% endtab %}

{% tab title="Docker" %}

```bash
docker run -it -v ~/.local/share/nucypher:/root/.local/share/nucypher:rw -v ~/.ethereum/:/root/.ethereum:ro nucypher/nucypher:recovery nucypher ursula audit
```

{% endtab %}
{% endtabs %}

#### **View mnemonic**

This command can be used to view the mnemonic for existing private keys. This assumes you have the keystore file and it's associated password.

{% tabs %}
{% tab title="CLI" %}

```bash
nucypher ursula audit ... --view-mnemonic
```

{% endtab %}

{% tab title="Docker" %}

```bash
docker run -it -v ~/.local/share/nucypher:/root/.local/share/nucypher:rw -v ~/.ethereum/:/root/.ethereum:ro nucypher/nucypher:recovery nucypher ursula audit --view-mnemonic
```

{% endtab %}
{% endtabs %}


# Duties, Compensation & Penalties

{% hint style="warning" %}
The TACo network is currently dormant ahead of a relaunch by WEDF scheduled for Q3 2026. Although  independent operators may choose to continue running TACo nodes at their discretion, there is no coordinated group of providers running TACo software at this time, **nor are there any test DKG rituals.**

A stable version of the service will be relaunched in Q3 2026 – centered around a *Privacy Coalition.* If you represent an organization in the domains of privacy advocacy, anti-surveillance, human rights, encryption/whistleblower technology, or pertinent academic research, and would like to be involved, please get in [touch](https://discord.gg/Rh2728Hk).&#x20;

Until then, this page serves as a reference for prospective members of this new node coalition.&#x20;
{% endhint %}

### Node **Operator Duties**

{% hint style="danger" %}
Failure to adhere to node operator duties may result in on-chain punishments, including the withholding of rewards and slashing of stakes. See Violations & Penalties section below.
{% endhint %}

Operating a TACo node requires active engagement. To provide high-quality service and maintain the reliability of the Threshold Network, TACo node operators must:

1. **Safeguard TACo private Keys and secret mnemonic**: This includes creating an off-site record of the mnemonic assigned to your node in the initialization step and securing a copy of the keystore directory and passwords. Loss of the keystore (private keys) and password, or mnemonic will result in reward withholding and/or penalties.
2. **Maintain continuous server accessibility**: Ensure the server or machine running TACo software is online and accessible at all times, allowing for immediate verification and response to incoming decryption requests. This prevents delays in reaching a threshold and providing decryption material to qualifying data recipients. Continuous downtime or network unavailability will result in reward withholding and/or penalties.
3. **Update to the latest TACo version**: Ensure your node is running the latest version of TACo. New releases will be announced on the Threshold Discord #announcements channel; enable notifications to stay updated. Running an outdated version of TACo will result in reward withholding and/or penalties.

### Violations & Penalties

{% hint style="warning" %}
**As of September 2024, violations are being detected and logged on-chain via the Infraction Collector contract.**

Today, avoiding committing attributable violations increases the likelihood that a node is selected by TACo's adopting developers to participate in DKG Initialization Rituals, and hence form part of TACo cohorts managing access to real-world data.

Reward withholding and stake slashing, as a consequence of committing attributable violations, are not yet in effect, but will be rolled out in the near future.
{% endhint %}

Stakers operating TACo nodes must ensure that their nodes are up-to-date, available/reachable and correctly configured at all times. Failure to do may result in protocol divergent behavior, attributable failures, and the levying of economic penalties, including reward withholding and slashing of collateral.

<figure><img src="/files/Go12H6Nn92tChAJzJ5gq" alt=""><figcaption></figcaption></figure>

Nodes are 'tested' during Distributed Key Generation (DKG) rituals, wherein the following violations are detectable, attributable and punishable on-chain:

* Node is unreachable, non-responsive or offline.
* Node fails to submit a valid transcript or the transcript is missing.
* Node's keystore is mismatched with the ritual-coordinating smart contract. This implies the node operator has reset their node without following the correct recovery path, which is a protocol violation. If you have reset your node, please file a [support ticket](https://discord.com/channels/866378471868727316/1025113672185552938) – noting that resetting a TACo node (for any reason) is categorized as node mismanagement and may incur on-chain economic penalties in the future.

The following DKG ritual failure modes are considered outside of the node operator's control and currently do not incur any penalties:

* DKG ritual fails to generate a valid aggregation of transcripts.
* DKG ritual fails or times out due to blockchain or web3 provider issues.
* DKG ritual failure due to a bug in a node client release.

DKG rituals can occur at any time. There are two types of rituals:

(1) **Initialization rituals.** These are DKG rituals initiated by TACo's adopting developers – the first step for a developer to integrate TACo access control in production and control their own cohort of nodes.

(2) **Test & Heartbeat rituals.** These are manually initiated and automated dummy rituals, respectively. These generate statistics on the health of the network and flush out errant nodes ahead of Initialization rituals.

DKG rituals are initiated without any warning – it is almost impossible to predict when a DKG ritual will occur, when your TACo node will be called into action, and when a violation could be detected and attributed. It is therefore safest to have your TACo node up-to-date, available/reachable, and correctly configured at all times.

{% hint style="info" %}
All nodes must successfully complete several test/heartbeat rituals before being selected for an Initialization ritual with an adopting developer.
{% endhint %}

The following parameters serve as a guide for economic penalties that will deployed in forthcoming versions of TACo. The first three violations committed by a node will be penalized via the withholding of monthly T rewards, with escalating severity for repeat offenses. If more than three violations occur, then the node operator will have a part of their stake slashed.\
\
**1st** violation: **30%** rewards withholding for 3 months.\
**2nd** violation: **60%** rewards withholding for 3 months.\
**3rd** violation: **90%** rewards withholding for 3 months.\
**4th** violation: (% TBD) slashing of stake, one-off.\
\
Note that the violation count is reset if a node manages to avoid committing any further violations during a given 3 month 'penalty period'. However, every violation restarts the penalty period. This means that, firstly, the duration over which reward withholding is levied is always three months from the date of that violation. Secondly, the violation count, and corresponding severity of punishment, will continue to ratchet up with each offense, until a node manages to avoid committing a violation for three months.

{% hint style="info" %}
**Note for developers using TACo**\
Critical operations required for encryption & decryption follow a threshold design, hence isolated errant behavior will not disrupt end-user data sharing. However, DKG rituals, which are prerequisite for usage of TACO, are less fault tolerant and can fail if a single selected node fails to follow the protocol, inadvertently or otherwise. Hence, TACo's disincentive protocol is centered around DKG rituals, which (1) generate on-chain evidence for faults and (2) flush out negligent or bad actors before they can affect end-users.
{% endhint %}

### **Deauthorization Delay**

Adopters of TACo require reassurance that the cohorts of nodes managing their users' data will remain intact for extended durations. Although cohort members can be securely replaced, while maintaining a persistent public key, it is preferable for economic reasons to minimize the number of node replacement rituals – particularly in the genesis era of the service. Hence, TACo service provision ideally involves a commitment to provide service for years, rather than months.

{% hint style="warning" %}
The *deauthorization dela*y is the time one must wait between initiating a withdrawal from TACo service provision and being able to complete that withdrawal. From genesis, the deauthorization delay is set to **6 months** (183 days).
{% endhint %}

Note that this delay is universal and independent of any other token lock-up or bonus mechanism.


