# Brief Overview of Flap

The Ultimate Decentralized Token Creation Platform

<figure><img src="https://2671086575-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F5KujUBwRoHVjn8OZEgtZ%2Fuploads%2FmvnHT1CMyrLxyaeRbRb4%2FFlap%20Banner.png?alt=media&amp;token=35e4edd8-4a93-40e2-b062-6f330ac93f9d" alt=""><figcaption></figcaption></figure>

Flap is an on-chain token launch protocol on EVM, born out of an ETHGlobal hackathon victory. Flap's mission is to empower projects and creators with decentralized, on-chain tools to launch tokens and design customized token standards seamlessly.

#### Rich Terminal & Wallets Support

Tokens launched via Flap are supported by a broad ecosystem of wallets, trading platforms, and bots, including **OKX Wallet, Binance Wallet, GMGN, Debot, Ave.ai, Dragun**, **Axiom** and more—ensuring strong discoverability and liquidity from day one.

#### Non-Tax & Tax Token Support

Flap supports both **non-tax tokens** and **tax tokens**. For tax tokens, creators can customize trading tax fee rates at **1%, 3%, 5%, or 10%**, enabling flexible use cases such as community incentives, marketing, treasury funding, or charitable contributions.

### Flap Tax Vaults

Flap offers the ability for developers to create custom smart contracts for tax token management. By adhering to the provided vault specifications, enhanced insights and details can be accessed via our website and supported terminals. Developers can also build and deploy their own vault templates (Vault Factory), offering a monetization opportunity when users opt to utilize their templates.


# Flap Official Links

Flap Website:<https://flap.sh/>

Flap X (Formally Twitter):<https://twitter.com/flapdotsh>

Flap Telegram: <https://t.me/FlapOfficial>

Flap Discord: <https://discord.gg/flapdotsh>

Flap Farcaster: <https://warpcast.com/flap>


# Developers

This guide is structured to introduce you to the different facets of developing with Flap. It's recommended to first familiarize yourself with the **basic mechanisms of Flap**. Once you're comfortable with the fundamentals, proceed to the section that suits your specific role:

* **Wallets & Terminal & Bot Developers:** For developers who want to build an indexer for Flap and trade tokens launched from Flap
* **Launcher Developers:** For applications leveraging Flap's infrastructure to launch tokens.
* **Vault Developers:** For those interested in building upon Flap's Vault system.
* **Flap Token Locker:** For wallets and dApps that lock Flap tax tokens under a vesting schedule while keeping their dividends.

Please read the section corresponding to your role for detailed instructions and guidance.

* [Basic & Mechanism](/flap/developers/basic-and-mechanism)
* [Pin Existing IPFS Content](/flap/developers/pin-existing-ipfs-content)
* [Wallets & Terminal & Bot Developers](/flap/developers/wallet-and-terminal-and-bot-developers)
* [Launcher Developers](/flap/developers/token-launcher-developers)
* [Vault Developers](/flap/developers/vault-developers)
* [Flap Token Locker](/flap/developers/flap-token-locker)


# Deployed Contract Addresses

This page lists all deployed contract addresses across supported chains. Each chain has a table with the contract name, address, and any relevant notes.

## Mainnet

### BNB Chain

| Name                   | Address                                      | Notes                                                                     |
| ---------------------- | -------------------------------------------- | ------------------------------------------------------------------------- |
| Portal                 | `0xe2cE6ab80874Fa9Fa2aAE65D277Dd6B8e65C9De0` | v5.23.1                                                                   |
| Standard Token Impl    | `0x8b4329947e34b6d56d71a3385cac122bade7d78d` | Vanity suffix: 8888                                                       |
| Tax Token V3 Impl      | `0x024f18294970B5c76c0691b87f138A0317156422` | Vanity suffix: 7777                                                       |
| Standard Token V3 Impl | `0x88881b6f03090462a969eC7f48385744Eeb63333` | Vanity suffix: 8888                                                       |
| VaultPortal            | `0x90497450f2a706f1951b5bdda52B4E5d16f34C06` |                                                                           |
| Trigger Service        | `0xcf4EE25035CF883895110f367F5BA8172416a7F9` | See [Flap Trigger Service](/flap/developers/preview/flap-trigger-service) |
| AI Oracle              | `0xaEe3a7Ca6fe6b53f6c32a3e8407eC5A9dF8B7E39` | Preview. See [Flap AI Oracle](/flap/developers/preview/flap-ai-oracle)    |
| Candy Box              | `0x6255fbd731272a517022e99f6CaCf6A5De9414Ee` | Preview. See [Flap Candy Box](/flap/developers/preview/flap-candy-box)    |
| Tax Token Helper       | `0x53841c73217735F37BC1775538b03b23feFD8346` |                                                                           |
| FlapTokenLocker        | `0x0000A8B6741441BB4Ba0DFE313a2bc7b2Cf10000` | See [Flap Token Locker](/flap/developers/flap-token-locker)               |
| FlapTokenRegistry      | `0x0000e9f9F5e469f0EEC27F192B4DdDE04D4c0000` | Upgradeable proxy; admission for the locker                               |

### Robinhood

| Name                | Address                                      | Notes                                                                     |
| ------------------- | -------------------------------------------- | ------------------------------------------------------------------------- |
| Portal              | `0x26605f322f7fF986f381bB9A6e3f5DAb0bEaEb09` | v5.23.1                                                                   |
| Standard Token Impl | `0x88882688a067FE97E11C2185b996286e53132222` | Vanity suffix: 8888                                                       |
| Tax Token V3 Impl   | `0x7777C8743C88B3aff3cf262135beF2c8b2e83333` | Vanity suffix: 7777                                                       |
| VaultPortal         | `0xe9F7AB7DE8FB8756acbB6a1cd13316a43308197B` |                                                                           |
| Tax Token Helper    | `0xb10bD2672aE63735d677164A54B573a016f0203C` |                                                                           |
| Trigger Service     | `0xD3421B1b616a72bB88993A0cf75709BB8D532cc1` | See [Flap Trigger Service](/flap/developers/preview/flap-trigger-service) |

### Base

| Name              | Address                                      | Notes               |
| ----------------- | -------------------------------------------- | ------------------- |
| Portal            | `0x0000BC1c4fD15Dd79029AF8F5d77D68ae4490000` | v5.23.1             |
| Tax Token V3 Impl | `0x777782e7EbB89802E2379E5fFcF7336538f43333` | Vanity suffix: 7777 |
| VaultPortal       | `0x00003877386745d059d92fa39159595BdaBF0000` |                     |
| Tax Token Helper  | `0x0000fdc1C95d399ef5aAe461216b3628C6020000` |                     |

### Toshimart

| Name                | Address                                      | Notes               |
| ------------------- | -------------------------------------------- | ------------------- |
| Portal              | `0x1ea172Fb88C24DFC21Ff6Fa38762511C123bA948` | v4.11.0             |
| Standard Token Impl | `0xF3c514E04f83166E80718f29f0d34F206be40A0A` | Vanity suffix: 8453 |

### X Layer

| Name                | Address                                      | Notes               |
| ------------------- | -------------------------------------------- | ------------------- |
| Portal              | `0xb30D8c4216E1f21F27444D2FfAee3ad577808678` | v5.23.1             |
| Standard Token Impl | `0x12Dc83157Bf1cfCB8Db5952b3ba5bb56Cc38f8C9` | Vanity suffix: 1111 |
| Tax Token V3 Impl   | `0x777782e7EbB89802E2379E5fFcF7336538f43333` | Vanity suffix: 7777 |
| VaultPortal         | `0x00003877386745d059d92fa39159595BdaBF0000` |                     |
| Tax Token Helper    | `0x0000fdc1C95d399ef5aAe461216b3628C6020000` |                     |

### Muffun

| Name                | Address                                      | Notes               |
| ------------------- | -------------------------------------------- | ------------------- |
| Portal              | `0x4267F317adee7C6478a5EE92985c2BD5D855E274` | v4.13.2             |
| Standard Token Impl | `0x2a770E952bB2700393238199B5889013693A8271` | Vanity suffix: 8888 |

### Monad

| Name                | Address                                      | Notes               |
| ------------------- | -------------------------------------------- | ------------------- |
| Portal              | `0x30e8ee7b5881bf2E158A0514f2150aabe2c68b23` | v5.1.2              |
| Standard Token Impl | `0xB88189aA1162850D75A1c1e16F837b7979994184` | Vanity suffix: 8888 |
| Tax Token V3 Impl   | `0x777782e7EbB89802E2379E5fFcF7336538f43333` | Vanity suffix: 1111 |
| VaultPortal         | `0x00003877386745d059d92fa39159595BdaBF0000` |                     |
| Tax Token Helper    | `0x0000fdc1C95d399ef5aAe461216b3628C6020000` |                     |

## Testnet

### BNB Testnet

| Name                | Address                                      | Notes                                                                     |
| ------------------- | -------------------------------------------- | ------------------------------------------------------------------------- |
| Portal              | `0x5bEacaF7ABCbB3aB280e80D007FD31fcE26510e9` | v5.8.5                                                                    |
| Standard Token Impl | `0x87D5f292ba33011997641C7a7Bd2b17799aaA814` | Vanity suffix: 8888                                                       |
| Tax Token V1 Impl   | `0x87d8D03d0c3E064ACdb48E42fecbE8a8538dE6Fc` | Vanity suffix: 7777                                                       |
| Tax Token V2 Impl   | `0x2486e3ff5502bac48D2D86457e7c24B2bB0dDDb5` |                                                                           |
| Tax Token V3 Impl   | `0xE6Ff967a887084c16D0fD71548CF709542cc1557` |                                                                           |
| VaultPortal         | `0x027e3704fC5C16522e9393d04C60A3ac5c0d775f` |                                                                           |
| Trigger Service     | `0x560E9830926C9e0EB98a59c6b9902383Fc0D9Eb2` | See [Flap Trigger Service](/flap/developers/preview/flap-trigger-service) |
| AI Oracle           | `0xFfddcE44e8cFf7703Fd85118524bfC8B2f70b744` | Preview. See [Flap AI Oracle](/flap/developers/preview/flap-ai-oracle)    |
| Candy Box           | `0x8e6C16Bf07022a7Da9398B543f38846E9355Bd70` | Preview. See [Flap Candy Box](/flap/developers/preview/flap-candy-box)    |
| Tax Token Helper    | `0xD64441e5FcD02D342B8cf6eBA10Ef6E40d0dA90f` |                                                                           |
| FlapTokenLocker     | `0x0000A8B6741441BB4Ba0DFE313a2bc7b2Cf10000` | See [Flap Token Locker](/flap/developers/flap-token-locker)               |
| FlapTokenRegistry   | `0x0000e9f9F5e469f0EEC27F192B4DdDE04D4c0000` | Upgradeable proxy; admission for the locker                               |

### Robinhood Testnet

| Name                | Address                                      | Notes                                                                     |
| ------------------- | -------------------------------------------- | ------------------------------------------------------------------------- |
| Portal              | `0x26605f322f7fF986f381bB9A6e3f5DAb0bEaEb09` | v5.14.16                                                                  |
| Standard Token Impl | `0x88882688a067FE97E11C2185b996286e53132222` | Vanity suffix: 8888                                                       |
| Tax Token V3 Impl   | `0x7777C8743C88B3aff3cf262135beF2c8b2e83333` | Vanity suffix: 7777                                                       |
| VaultPortal         | `0xe9F7AB7DE8FB8756acbB6a1cd13316a43308197B` |                                                                           |
| Tax Token Helper    | `0xb10bD2672aE63735d677164A54B573a016f0203C` |                                                                           |
| Trigger Service     | `0x34e7624e2c8F944Db1adD9a604fdB7C439CaEa1c` | See [Flap Trigger Service](/flap/developers/preview/flap-trigger-service) |


# Pin Existing IPFS Content

Use the Flap **Pin By CID** API when a file or directory is already available on Public IPFS but is not yet retained by the Flap Pinata account. After the pin finishes, Flap applications and approved Vault UIs can load the content through the Flap IPFS gateway.

This API imports an existing CID. It does **not** upload local files or create a CID for you.

{% hint style="warning" %}
The production endpoint is available only after the corresponding Flap Edge Functions release has been deployed. If the endpoint returns an HTML `404` page, contact Flap; that response is not a CID validation result.
{% endhint %}

## When to use this API

Use it when:

* You already have a public IPFS CID.
* The content works through its original IPFS provider but the Flap gateway reports that it is not pinned by the gateway owner.
* A custom Vault UI needs an immutable image that must be available through the Flap gateway.

For a new token image and metadata JSON that have not been uploaded yet, use the [token metadata upload flow](/flap/developers/token-launcher-developers/launch-token-through-portal#id-1-prepare-token-metadata) instead.

## Requirements

Before submitting a request:

* Confirm that the content is available on **Public IPFS**.
* Copy the complete raw CID exactly as returned by the original provider.
* Keep the original provider pin active until Flap reports `pinned` and the content works through the Flap gateway.
* Submit only a raw CIDv0 or base32 CIDv1 value.

Do not submit an `ipfs://` URI, a gateway URL, or a path below the CID.

```
# Valid
bafy...
Qm...

# Invalid
ipfs://bafy...
https://example-gateway.com/ipfs/bafy...
bafy.../image.webp
```

## Submit a CID

```http
POST https://funcs.flap.sh/api/v1/ipfs/pins
Content-Type: application/json
```

The public endpoint does not require a caller API key. Flap's Pinata credential remains on the server and must never be included in a request.

Request body:

```json
{
  "cid": "bafy...wejvfy",
  "name": "vault-ui-hero-image"
}
```

The CID above is intentionally shortened for documentation. Replace it with the complete raw CID returned by your IPFS provider when making a request.

* `cid` is required.
* `name` is optional and is used only as an operator-friendly Pinata label.

Example with cURL:

```bash
curl --request POST \
  --url "https://funcs.flap.sh/api/v1/ipfs/pins" \
  --header "Content-Type: application/json" \
  --data '{
    "cid": "YOUR_CID_HERE",
    "name": "vault-ui-hero-image"
  }'
```

The API returns `202 Accepted` while Pinata is processing a new request. If the CID is already pinned or already being processed, the API returns the existing result instead of creating a duplicate job. A CID that is already pinned returns `200 OK`.

Example response:

```json
{
  "data": {
    "cid": "bafy...wejvfy",
    "status": "queued",
    "providerStatus": "prechecking",
    "jobId": "00000000-0000-0000-0000-000000000000",
    "name": "vault-ui-hero-image"
  },
  "requestId": "00000000-0000-0000-0000-000000000000"
}
```

## Check the pin status

```http
GET https://funcs.flap.sh/api/v1/ipfs/pins?cid=<CID>
```

Example:

```bash
curl --get \
  --url "https://funcs.flap.sh/api/v1/ipfs/pins" \
  --data-urlencode "cid=YOUR_CID_HERE"
```

The stable Flap statuses are:

| Status       | Meaning                                                    |
| ------------ | ---------------------------------------------------------- |
| `queued`     | Pinata is prechecking the CID.                             |
| `retrieving` | Pinata found the content and is retrieving it.             |
| `processing` | Pinata is still processing the request.                    |
| `pinned`     | The content is retained by the Flap Pinata account.        |
| `failed`     | Pinata could not complete the request. Check `errorCode`.  |
| `not_found`  | Flap has no active or completed Pinata record for the CID. |

Pinning is asynchronous. Poll at a reasonable interval and do not treat the initial `202` response as completion.

## Verify through the Flap gateway

After the status becomes `pinned`, verify the content through the Flap gateway:

```
# Single file
https://flap.mypinata.cloud/ipfs/<CID>

# File inside a pinned directory
https://flap.mypinata.cloud/ipfs/<DIRECTORY_CID>/<FILE_PATH>
```

Only remove the original provider pin after at least one real file has loaded successfully through the Flap gateway.

## Files and directories

* For one existing image, submit its image CID once.
* For an existing IPFS directory, submit the directory CID once. Do not submit every file path below it.
* If every image has a separate CID, each CID is a separate Pin request; this API does not combine them into a directory.

## Browser and server usage

The endpoint is public but does not enable wildcard browser CORS. Call it from a command-line client or your server, not directly from arbitrary browser JavaScript.

## Troubleshooting

| Result                                         | Action                                                                                                         |
| ---------------------------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `400 INVALID_REQUEST`                          | Copy the complete raw CID again. Remove any gateway URL, `ipfs://` prefix, or file path.                       |
| `413 REQUEST_TOO_LARGE`                        | Send only the small JSON body containing `cid` and optional `name`. Do not upload file bytes to this endpoint. |
| `status: failed`                               | Inspect `errorCode` and confirm the original provider still serves the content on Public IPFS.                 |
| `502`, `503`, or `504`                         | The upstream Pinata service rejected, rate-limited, or timed out. Retry later.                                 |
| The endpoint itself returns an HTML `404` page | The deployed Flap Edge Functions release does not include this API yet. Contact Flap before retrying.          |
| The Flap gateway still cannot load the content | Wait until the API reports `pinned`, then verify the exact CID and optional directory path again.              |


# Basic & Mechanism

This chapter explains the core mechanisms behind Flap and the foundational components developers need to understand before integrating. It covers bonding curve behavior, the DEX migration flow, Portal vs VaultPortal differences, and the Flap Tax Token model.

## In this chapter

* [📈 Bonding Curve](/flap/developers/basic-and-mechanism/bonding-curve)
* [⚖️ Migrated To DEX](/flap/developers/basic-and-mechanism/list-on-dex)
* [Portal vs VaultPortal](/flap/developers/basic-and-mechanism/portal-vs-vaultportal)
* Flap Tax Token
  * [PreBond Tax](/flap/developers/basic-and-mechanism/flap-tax-token/prebond-tax)
  * [Tax Token V1](/flap/developers/basic-and-mechanism/flap-tax-token/tax-token-v1)
  * [Tax Token V2](/flap/developers/basic-and-mechanism/flap-tax-token/tax-token-v2)


# Bonding Curve

{% hint style="info" %}
Since v2.4.0, Flap supports multiple bonding curves, but only one is active for a quote token.
{% endhint %}

### What is a Bonding Curve

Before migrating to DEX, each token launched on flap is bonded to a bonding curve. When you buy tokens from the bonding curve, you send your ETH ( or other quote token: BNB, USDT etc) to the bonding curve as a reserve, and the bonding curve mints tokens to your address. Or if you sell your tokens to the bonding curve, the bonding curve will burn your selling tokens and send the ETH back to you.

{% hint style="info" %}
Under the hood, we don't implement it as burn & mint. Initially, all the tokens are in our bonding curve contract until the user buys 80% of the total supply and the remaining token will be added to DEX as liquidity.
{% endhint %}

<figure><img src="https://2671086575-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F5KujUBwRoHVjn8OZEgtZ%2Fuploads%2FxYdFM0ADkLAeAIJo56Kh%2Fimage.png?alt=media&amp;token=a0bf6998-b2e0-4a40-9c4d-bc568f96cfa7" alt=""><figcaption></figcaption></figure>

A bonding curve defines the relationship between the trading token's supply and the reserve (i.e. Quote tokens like ETH or BNB). The change of the reserve respect to the supply is the price. Our bonding curve is based on a constant product equation. You may have heard about this equation, which Uniswap popularized.

You may even wonder what is the difference between a bonding curve token launching platform like flap and Unsiwap? The answer is the liquidity. The liquidity does not change on the bonding curve. However, anyone can add liquidity to the Uniswap pools.

### Flap's Bonding Curve V2

The token created on our platform has the same max supply of $$10^9$$ , with 18 decimals. For each token, the amounts of token and the Quote (ETH, BNB or USD\*) follow the following constant product equation:

$$
(x+ h)(y + r) = K
$$

$$r$$ ,$$h$$ and $$K$$ are constant parameters dependent on the target chain ( check [#bonding-curves-on-different-chains](#bonding-curves-on-different-chains "mention") for more details).

$$
(x+ h)(y + r) = K
$$

* $$x$$ is the amount of token in the bonding curve (i.e: token reserve), initially, it is $$10^9$$, which means all the tokens are in the bonding curve in the beginning.
* $$y$$ is the amount of quote token (i.e: quote reserve) , it is 0 in the beginning.
* Our curve have 3 constant parameters:
  * $$r$$ can be interpreted as the virtual reserve of quote token in the bonding curve
  * $$h$$ can be interpreted as the virtual reserve of the launched token in the bonding curve
  * $$K$$ is the square of the virtual liquidity

### Bonding Curves On Different Chains

{% hint style="danger" %}
The following table may be out of date.\
\
We don't recommend that you hardcode the bonding curve parameters in your application. Instead, you should use `getTokenV5` method from the Portal contract to get the parameters for each token. The parameters are immutable for each token, so you only need to fetch them once and cache them in your application.
{% endhint %}

For different and different quote token , the constants are different:

<table><thead><tr><th width="317.43804931640625">Chain</th><th width="126.9757080078125">r</th><th>h</th><th>K</th></tr></thead><tbody><tr><td>BSC (Legacy before block #42042177 )</td><td>15</td><td>0</td><td><span class="math">15 \cdot 10^{9}</span></td></tr><tr><td>BSC (Legacy since block 42042177 and before 47855189 )</td><td>4</td><td>0</td><td><span class="math">4 \cdot 10^{9}</span></td></tr><tr><td>BSC (Legacy since block 47855189 and before <a href="https://bscscan.com/block/51314696">51314696</a>)</td><td>2</td><td>0</td><td><span class="math">2 \cdot 10^{9}</span></td></tr><tr><td>BSC(before 61837444, BNB as payment Token)</td><td>4</td><td>0</td><td><span class="math">4 \cdot 10^{9}</span></td></tr><tr><td>BSC(before 61837444, USD1/lisUSD as payment token)</td><td>2500</td><td>0</td><td><span class="math">2500 \cdot 10^{9}</span></td></tr><tr><td>BSC(lastest, BNB as payment Token)</td><td>6.14</td><td>107036752</td><td>6797205657.28</td></tr><tr><td>BSC(latest, USD1/lisUSD as payment token)</td><td>3837</td><td>107036752</td><td>4247700017424</td></tr><tr><td>ToshiMart</td><td>0.5</td><td>0</td><td><span class="math">0.5 \cdot 10^{9}</span></td></tr><tr><td>XLayer (Before block 31564005)</td><td>28</td><td>0</td><td><span class="math">28 \cdot 10^{9}</span></td></tr><tr><td>XLayer (Before block 32470187)</td><td>21.25</td><td>0</td><td><span class="math">21.25 \cdot 10^{9}</span></td></tr><tr><td>XLayer(Since block 32470187)</td><td>28.25</td><td>108002126</td><td>31301060059</td></tr><tr><td>Monad (mainnet)</td><td>50000</td><td>107036752</td><td>55351837600000</td></tr></tbody></table>

### Example Curve BNB

{% embed url="<https://www.desmos.com/calculator/qu77rif6q0>" %}


# Migrated To DEX

## Overview

Each token launched on flap has a maximum supply of 1 Billion. When a token reaches the milestone to be listed on the DEX, the remaining supply (non circulating) tokens and all the reserve (i.e., the quote token, e.g., ETH/BNB) will be added as liquidity to DEX.

For different deployment, the milestone to be listed on DEX is different. In previous section ( [Bonding Curve](/flap/developers/basic-and-mechanism/bonding-curve) ), we know that the reserve of a token can be found by its circulating supply. So we can represent the milestone for migrating to DEX in either circulating supply or reserve. We list both of them in the following table for BNB chain when quote is BNB:

<table><thead><tr><th width="365">Deployment</th><th>Circulating Supply</th><th>Reserve</th></tr></thead><tbody><tr><td>BSC (Legacy before block #42042177 )</td><td>800M</td><td>30.1 BNB</td></tr><tr><td>BSC (Legacy since block #42042177)</td><td>800M</td><td>16 BNB</td></tr><tr><td>BSC (Legacy since block 47855189 and before <a href="https://bscscan.com/block/51314696">51314696</a>)</td><td>800M</td><td>8 ETH</td></tr><tr><td>BSC ( After <a href="https://bscscan.com/block/51314696">51314696</a>)</td><td>800M</td><td>16 ETH / 10000USD1</td></tr></tbody></table>

Let's take a look at the BSC Legacy deployment as an example. The purple point is the milestone when the token can be listed on the DEX, which is a circulating supply of 800M, or a reserve of 16 BNB or a market cap of 100 BNB. When the token reaches the milestone, it will be listed on DEX. If we are adding this token to Uniswap V3 (or its fork, e.g, PancakeSwap V3), the reserve (16 BNB) and the remaining token (200M) will be added as liquidity. ~~Besides, the Flap protocol is flexible and allows more token to be sold on the bonding curve. For example (the red curve on the following graph), if the last buyer buys a lot of token on the bonding curve, the resulting circulating supply would be 900M , and the reserve will be 36 BNB giving a market cap of 400 BNB. The token will still be listed on DEX with the remaining 100M token and 36 BNB reserve.~~ (CHANGES: You can not buy more than 800M no matter it is an old token or new one.)

{% hint style="info" %}
When a token is a tax token, it can only be migrated to Uniswap V2 or its forks.
{% endhint %}

For non-tax token, we will first try to add liquidity to Uniswap V3 (or its fork). If it is not viable, we will fallback to Uniswap V2 (or its fork).

## Adding Liquidity To Uniswap V3 (or its fork)

When adding liquidity to Uniswap V3, we only add liquidity to a concentrated range of prices, giving the token a better depth. The lowest price is the initial price on the bonding curve (i.e., the price when no one has bought the token yet), the current price is the last price on the bonding curve before the token is listed on DEX, and the max price is 10000 times of the current price.

<figure><img src="https://2671086575-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F5KujUBwRoHVjn8OZEgtZ%2Fuploads%2FvGvCdUzOpRlJHUov59rS%2Fimage.png?alt=media&amp;token=6cea8a96-a22b-4049-98d0-cd58a00eab09" alt=""><figcaption></figcaption></figure>


# Portal vs VaultPortal

## Overview

The `Portal` is the entry point of the Flap bonding curve protocol. It launches standard tokens and tax tokens and wires them to the bonding curve lifecycle. For each tax token launched, if the "funds recipient wallet" percentage is set to non-zero, a portion or all tax revenue is sent to that wallet. And when that wallet is a smart contract and implements Vault interface, it is called a `Vault`.

The `VaultPortal` builds on top of the `Portal` to add Vault functionality for tax tokens. It launches a tax token and sets up a Vault in one flow.

For Vault concepts and behavior, see [developers/vault-developers/flap-tax-vault.md](/flap/developers/vault-developers/flap-tax-vault).

## When to use which

* Use `Portal` when you only need to launch a standard token or a tax token without Vault features or your custom Vault implementation.
* Use `VaultPortal` when you want Vault-backed tax tokens and using a vault factory. (You can use any Vault Factory, this is permissionless.)

## Relationship diagram

![Vault vs VaultPortal relationship](https://2671086575-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F5KujUBwRoHVjn8OZEgtZ%2Fuploads%2Fgit-blob-56780fc409e642507100ba5aa3f4626a767bde85%2Fvault-vs-vaultportal.png?alt=media)


# protocol-economics

Flap charges protocol-level rates to sustain the on-chain infrastructure that powers token launches, tax settlement, and automation across all supported chains. All rates are enforced at the protocol

***

#### Bonding Curve Rate <a href="#bonding-curve-rate" id="bonding-curve-rate"></a>

A rate applied to every buy and sell against the bonding curve, taken as a percentage of the trade amount. This rate applies only during the bonding curve phase — once a token graduates to DEX, it no longer applies.

1% on BNB Chain, Robinhood, X Layer, and Monad. 2.5% on Morph.

***

#### Tax Token Protocol Rate <a href="#tax-token-protocol-rate" id="tax-token-protocol-rate"></a>

Tax tokens are subject to an additional protocol rate to support automated tax collection, settlement, and payout infrastructure.

Up to 0.3% of trade volume, regardless of the token's tax rate.

Exact rate is publicly verifiable in the protocol contract.


# Flap Tax Token


# PreBond Tax

Flap supports Tax tokens. Since V5.4.0 (on BNB Chain & XLayer) , tax is not only applied to migrated tax tokens but also the ones that are still on the bonding curve.

When a tax token has already migrated DEX, it will charges a tax on every trade through the main pool and accumulate the tax in the token first. Once the accumulated token amount reaches a specific liquidation threshold, an automatic liquidation happens to sell the token for quote and sent the the beneficiary through the Tax Splitter. (Check below to learn more detaills)

When a tax token is still on the bonding curve, we don’t implement it as a “real tax” like a migrated one. To make it simple and easy to do an off-chain quote, we implement it as an `extra fee` added upon the current bonding curve fee.

For example:

* For a non-tax token, the current bonding curve fee on BNB chain is 1%.
* For a token with a tax of 3%, the effective trading fee becomes: 1% + 3% = 4%
* For a token with a tax of 1%, the effective trading fee is 2%


# Tax Token V1

{% hint style="info" %}
Tax Token V1 is still used and maintained. A V1 Tax token is created when you are using the vault mode or when 100% tax to Funds Recipient Wallet.
{% endhint %}

## Overview

In V1 tax token, all the tax will be sent to a single beneficiary. However, you can build a smart contract as the beneficiary to receive the tax, where you can implement your own logics. We have a recommended interface for such contracts (check [https://github.com/flap-sh/gitbook/tree/main/developers/basic-and-mechanism/flap-tax-token/flap-tax-vault.md](https://github.com/flap-sh/gitbook/tree/main/developers/basic-and-mechanism/flap-tax-token/flap-tax-vault.md "mention") for more details)

## Tax Splitter

To avoid DOS, we do not liquidate and distribute the Tax in the tax token directly. For each v1 tax token, a "Tax Splitter" contract will be deployed on creation. Both the liquidated Tax (when token is on DEX) and the Tax on the bonding curve will be directly sent to the "Tax Splitter" first. Then anyone could trigger the tax distribution by calling the "Tax Splitter" contract's `split` method. However, for most of the time you don't need to manually call "Tax Splitter" yourself , Flap will run a bot to automatically trigger the distribution when needed.

<figure><img src="https://2671086575-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F5KujUBwRoHVjn8OZEgtZ%2Fuploads%2FqK4wyplxJRg7marop3hj%2Fimage.png?alt=media&amp;token=9dd01e18-673f-451c-8710-9179c1f40a0b" alt=""><figcaption></figcaption></figure>


# Tax Token V2

## Overview

The V2 tax token let you split the tax for different usage:

<figure><img src="https://2671086575-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F5KujUBwRoHVjn8OZEgtZ%2Fuploads%2FolhAX5erGo4Q7uPNBHoH%2Fimage.png?alt=media&amp;token=d5aa6fd2-2aa7-4bb1-b37a-5ea5a762a2c1" alt="" width="375"><figcaption></figcaption></figure>

{% hint style="warning" %}
When the "Funds Recipient Wallet" section is 100%, a v1 tax token will be created instead
{% endhint %}

### Tax Processor & Dividend

<figure><img src="https://2671086575-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F5KujUBwRoHVjn8OZEgtZ%2Fuploads%2F9Ysh83xjntwFHSxzijPg%2Fimage.png?alt=media&amp;token=680166fd-ccdb-4a36-ac70-b7df9bbc08fe" alt=""><figcaption></figcaption></figure>

For every Tax Token V2 launch, the following contracts are deployed:

1. **Tax Processor** - Always deployed
2. **Dividend Contract** - Optionally deployed if dividend distribution is enabled (when `dividendBps > 0`)

When the token is still trading on the bonding curve (before DEX migration):

```
User buys/sells on Portal bonding curve
    ↓
Portal collects extra trading fee as tax in quote tokens (WETH/BNB/USDT/etc.)
    ↓
Portal sends extra fee to TaxProcessor.processBondingCurveTax(quoteAmount)
    ↓
Tax Processor accumulates quote tokens for later distribution
```

While the token is migrated to DEX and when users trade the Flap Tax Token V2 on DEX:

```
User trades token on DEX
    ↓
Tax Token V2 applies tax (e.g., 5%) on transfer
    ↓
Tax tokens accumulate in Token contract
    ↓
When threshold reached → Triggers tax liquidation
    ↓
Token contract calls TaxProcessor.processTaxTokens(taxAmount)
```

**Anyone** can call `TaxProcessor.dispatch()` to distribute accumulated funds:

```
Anyone calls dispatch()
    ↓
1. Send feeQuoteBalance → Funds Recipient Wallet
2. Send marketQuoteBalance → Market Address
3. Send dividendQuoteBalance → Dividend Contract
4. Process preBondBurnFunds (the quote accumuated on bonding curve phase for the burn):
   - If token on bonding curve: Use Portal to buyback & burn
   - If token on DEX: Swap quote tokens for tax tokens & burn  
5. Process burn: 
   - Send token to flapBlackHole or Burn address 
```

* Flap Tax Trigger Bot monitors balances and calls `dispatch()` when economically viable
* Bot pays gas fees to ensure timely distribution
* Anyone can call dispatch() if they want to pay the gas

If dividend contract is deployed (`dividendBps > 0`):

```
Token holder balance changes (buy/sell/transfer)
    ↓
Token contract calls Dividend.setShare(user, newBalance)
    ↓
Dividend contract updates user's share
    ↓
Settles any pending rewards before share change
```

**Excluded from dividends:**

* DEX pool addresses
* Token contract itself
* Dividend contract
* Dead address (0x000...dEaD)
* FlapBlackHole
* Zero address
* Addresses with balance below `minimumShareBalance`

**Manual Claiming:**

```
Token holder calls withdrawDividends()
    ↓
Dividend contract calculates withdrawable amount
    ↓
If dividendToken == WETH: Unwraps to ETH and sends
If dividendToken == other: Sends ERC20 tokens
    ↓
Updates withdrawnDividends tracking
```

**Automated Claiming (by Bot):**

```
Flap Tax Trigger Bot monitors pending dividends
    ↓
When holder's pending > threshold:
    Bot calls distributeDividend([holder1, holder2, ...])
    ↓
Dividend contract sends rewards to holders
    ↓
Bot pays gas fees
```

**Anyone Can Trigger:**

```
Any external caller can call:
- withdrawDividendsFor(user) - claim for specific user
- distributeDividend([users]) - batch claim for multiple users
- Caller pays gas fees
```


# Tax Token V3

{% hint style="info" %}
`FlapTaxTokenV3` (`TOKEN_TAXED_V3`) is the recommended token type for all new integrations. It is launched via the `newTokenV6` entry point on Portal v5.9.0+.
{% endhint %}

## Overview

Tax Token V3 introduces four major capabilities over V1/V2:

1. **Asymmetric buy/sell tax rates** — independent rates for buys and sells.
2. **Commission receiver** — a permanent, protocol-enforced fee share for third-party launchpad integrators.
3. **Unified launcher (`newTokenV6`)** — single entry point for all current token types.
4. **Bidirectional dynamic liquidation threshold** — self-correcting threshold that prevents permanent drift in either direction.

***

## 1. Asymmetric buy / sell tax rates

`FlapTaxTokenV3` supports independent buy and sell tax rates, enabling configurations such as a low buy tax (to encourage accumulation) and a higher sell tax (to discourage dumping), or the reverse.

### Reading tax rates on-chain

```solidity
IFlapTaxTokenV3 token = IFlapTaxTokenV3(tokenAddress);

uint16 buy  = token.buyTaxRate();   // e.g. 300  (3%)
uint16 sell = token.sellTaxRate();  // e.g. 1000 (10%)
uint16 eff  = token.taxRate();      // max(buy, sell) — backward-compatible single rate
```

`taxRate()` always returns `max(buyTaxRate, sellTaxRate)`. This preserves compatibility with existing off-chain systems and aggregators that only understand a single tax rate: they will always see the worst-case value and will never under-report the tax.

### Indexing tax rates from events

Two events are emitted at launch for every V3 tax token:

| Event                                                                       | Contents               | Purpose                                                                              |
| --------------------------------------------------------------------------- | ---------------------- | ------------------------------------------------------------------------------------ |
| `FlapTokenTaxSet(address token, uint256 tax)`                               | `max(buyTax, sellTax)` | Backward-compatible single-rate event; existing indexers continue to work unchanged. |
| `FlapTokenAsymmetricTaxSet(address token, uint256 buyTax, uint256 sellTax)` | Both rates             | New event for systems that support asymmetric rates.                                 |

For symmetric V1/V2 tokens the two events carry the same value. For V3 tokens, always prefer `FlapTokenAsymmetricTaxSet` when available.

**Example log output (buy=300, sell=1000):**

```
FlapTokenTaxSet(token, 1000)                // max of buy and sell
FlapTokenAsymmetricTaxSet(token, 300, 1000) // full asymmetric detail
```

{% hint style="info" %}
Old indexers that only listen to `FlapTokenTaxSet` remain fully functional — they will display the effective (worst-case) rate.
{% endhint %}

### Inspecting a token with `getTokenV8`

The `getTokenV8` helper returns the full token state including separate buy and sell tax rates:

```solidity
IPortalLens.TokenStateV8 memory state = portal.getTokenV8(tokenAddress);

state.buyTaxRate   // e.g. 300  (3%)
state.sellTaxRate  // e.g. 1000 (10%)
state.status       // TokenStatus enum (BondingCurve, DEX, …)
state.price        // current bonding curve price
state.progress     // progress toward DEX graduation (0–1e18)
```

`getTokenV8` is the recommended state query endpoint for any system that needs to display asymmetric tax information. It is a view function and can be called by any EOA or contract.

***

## 2. Commission receiver

### What it is

The commission receiver feature allows any third-party launchpad operator or integrator to earn a **permanent, protocol-enforced fee share** from every tax token they deploy through Flap. Tokens launched with a commission receiver will be visible on Flap and the commission accrues automatically on every taxable transaction (bonding curve buy, DEX buy, and DEX sell), for the entire lifetime of the tax.

### How to enable it

Set `commissionReceiver` to any non-zero address in `NewTokenV6Params`. Only `TOKEN_TAXED_V3` tokens support commission — passing a non-zero receiver for a non-tax token reverts.

```solidity
IPortalTypes.NewTokenV6Params memory params = IPortalTypes.NewTokenV6Params({
    // … other fields …
    tokenVersion:        IPortalTypes.TokenVersion.TOKEN_TAXED_V3,
    commissionReceiver:  0xYourLaunchpadAddress,  // receives commission
    // NOTE: commissionBps is NOT set here — the protocol calculates it
});
```

### How the commission rate is determined

The commission rate is **calculated entirely by the protocol** at token creation time via `_commissionForTax(effectiveTaxRate)`. The integrator cannot specify it. The formula is:

```
effectiveTaxRate = max(buyTaxRate, sellTaxRate)

if effectiveTaxRate <= 100 bps (1%):
    commissionBps = 600  (6% of the post-fee remainder)
else:
    commissionBps = floor(10000 * 6 / effectiveTaxRate)
```

This means commission is inversely proportional to the tax rate:

| Effective tax rate | Commission bps | Commission % |
| ------------------ | -------------- | ------------ |
| 1% (100 bps)       | 600            | 6.0%         |
| 2% (200 bps)       | 300            | 3.0%         |
| 3% (300 bps)       | 200            | 2.0%         |
| 5% (500 bps)       | 120            | 1.2%         |
| 10% (1000 bps)     | 60             | 0.6%         |

{% hint style="info" %}
The maximum possible commission is **6%** of the post-protocol-fee tax remainder, achievable only when the effective tax rate is ≤ 1%.
{% endhint %}

### Reading commission configuration

```solidity
ITaxProcessor processor = ITaxProcessor(IFlapTaxTokenV3(token).taxProcessor());

address receiver = processor.commissionReceiver();     // address(0) if disabled
uint16  bps      = processor.commissionBps();          // rate the protocol set
uint256 pending  = processor.commissionQuoteBalance(); // accrued, not yet dispatched
```

Call `processor.dispatch()` to push accumulated balances (fee, commission, marketing, dividends) to their respective receivers.

***

## 3. `newTokenV6` — unified token launcher

`newTokenV6` is the single entry point for creating **all** current token types. The specific token implementation is selected via the `tokenVersion` field:

| `tokenVersion`    | Enum value | Token type                  | Allowed tax rates                          | Commission  |
| ----------------- | ---------- | --------------------------- | ------------------------------------------ | ----------- |
| `TOKEN_V2_PERMIT` | 1          | Standard ERC-20 with permit | none (must be 0)                           | not allowed |
| `TOKEN_TAXED`     | 2          | FlapTaxToken V1             | symmetric only; `mktBps` must be 10000     | not allowed |
| `TOKEN_TAXED_V2`  | 4          | FlapTaxTokenV2              | symmetric only; `mktBps` must not be 10000 | not allowed |
| `TOKEN_TAXED_V3`  | **6**      | FlapTaxTokenV3              | asymmetric allowed; any valid distribution | supported   |

{% hint style="warning" %}
`TOKEN_TAXED` and `TOKEN_TAXED_V2` are deprecated and will be removed in a future release. New integrations should always use `TOKEN_TAXED_V3`.
{% endhint %}

### Salt computation

When computing the vanity salt off-chain, always use the **`tokenImplTaxedV3` implementation address** as the clone base, regardless of the `mktBps` or distribution parameters you intend to use:

```solidity
// Always use tokenImplTaxedV3 for TOKEN_TAXED_V3 salt vanity search
bytes32 salt = _findVanitySalt(VanityType.VANITY_7777, tokenImplTaxedV3, portalAddress);
```

### Launching with a vault: `IVaultPortal.newTokenV6WithVault`

`IVaultPortal` exposes `newTokenV6WithVault` for integrators that want to attach a revenue vault to a `TOKEN_TAXED_V3` token in the same transaction. Only `TOKEN_TAXED_V3` is accepted — any other `tokenVersion` reverts with `OnlyV3TaxTokenAllowed`.

```solidity
IVaultPortalTypes.NewTokenV6WithVaultParams memory params = IVaultPortalTypes.NewTokenV6WithVaultParams({
    // … same fields as NewTokenV6Params (without beneficiary) …
    tokenVersion:  IPortalTypes.TokenVersion.TOKEN_TAXED_V3,
    vaultFactory:  0xYourVaultFactory, // any factory; unregistered → UNVERIFIED
    vaultData:     abi.encode(/* vault-specific init data */)
});

address token = vaultPortal.newTokenV6WithVault(params);
```

The vault address can be retrieved after creation via `IVaultPortal.getVault(token)`.

### Dividend token

`FlapTaxTokenV3` always deploys a Dividend contract, regardless of the `dividendBps` value. This means you can launch a token with `dividendBps == 0` and still have a fully functional share-tracking contract attached.

#### Supported dividend token modes

There are three modes for the `dividendToken` field:

| Mode                         | `dividendToken` value                                                | Description                                                                                                                                                                   |
| ---------------------------- | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Case 1 — Launching Token** | `0xfEEDFEEDfeEDFEedFEEdFEEDFeEdfEEdFeEdFEEd` (`MAGIC_DIVIDEND_SELF`) | The tax token itself is distributed as dividend. Holders earn more of the same token.                                                                                         |
| **Case 2 — Quote Token**     | `address(0)` (native) or `quoteToken` address                        | The quote token (e.g. WBNB, USDT) is distributed as dividend. This is the simplest and most common setup.                                                                     |
| **Case 3 — Any ERC-20**      | Any ERC-20 contract address                                          | Any token with sufficient DEX liquidity can be used. The protocol swaps the collected quote token into the dividend token automatically on every dispatch via `SwapRegistry`. |

{% hint style="info" %}
**`MAGIC_DIVIDEND_SELF`** (`0xfEEDFEEDfeEDFEedFEEdFEEDFeEdfEEdFeEdFEEd`) is a sentinel address recognised by the Portal. When you pass this value, the protocol resolves it to the actual tax token address after the token is created.
{% endhint %}

#### Case 3 eligibility — SwapRegistry

For Case 3, the protocol checks at launch time that a valid swap path exists from the quote token to the chosen dividend token. If the pair is not supported, the transaction reverts with `DividendSwapNotSupported`.

You can check eligibility before launching:

```solidity
ISwapRegistry registry = ISwapRegistry(swapRegistryAddress);

// Simple boolean check
bool supported = registry.isSwapSupported(quoteToken, dividendToken);

// Detailed check — returns (supported, errorCode, errorMessage)
(bool ok, uint8 code, string memory reason) =
    registry.isSwapSupportedWithDetailedErrors(quoteToken, dividendToken);
```

**Trust status** — the `SwapRegistry` assigns a trust level to every dividend token:

| Value  | Meaning                                                                                                     |
| ------ | ----------------------------------------------------------------------------------------------------------- |
| `0x00` | Unknown — token is not vetted; the Flap UI shows a warning to users                                         |
| `0x01` | Whitelisted — token is audited/vetted by Flap                                                               |
| `0x02` | Blacklisted — token is known-malicious; dividend conversion is silently skipped (no revert during dispatch) |

```solidity
uint8 trust = registry.getTrustStatus(dividendToken);
bool  blacklisted = registry.isBlacklisted(dividendToken);
```

#### Tracker-only mode (`dividendBps == 0`)

When `dividendBps == 0`, no tax revenue is automatically routed to the Dividend contract, but the contract is still deployed and tracks holder share balances. This is useful for off-chain reward systems that read share data on-chain but push rewards via their own pipeline.

In tracker-only mode, `dividendToken` may be `MAGIC_DIVIDEND_SELF` (`0xfEEDFEEDfeEDFEedFEEdFEEDFeEdfEEdFeEdFEEd`) or any ERC-20 — the swap-path check is skipped because no automatic conversion occurs. Integrators can call `dividend.deposit(token, amount)` manually at any time to distribute any ERC-20 to current share holders.

**`dividendBps` summary:**

| `dividendBps` | Behaviour                                                                                                                     |
| ------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `> 0`         | Tax revenue is automatically routed to the Dividend contract. `dividendToken` must be Case 1, 2, or an eligible Case 3 token. |
| `== 0`        | No automatic payout. Dividend contract exists and tracks shares only. Any `dividendToken` value is accepted.                  |

#### Retrieving the dividend contract

```solidity
address dividendContract = IFlapTaxTokenV3(token).dividendContract();
address sameAddress      = ITaxProcessor(IFlapTaxTokenV3(token).taxProcessor()).dividendAddress();
// dividendContract == sameAddress always holds; both are non-zero for all V3 tokens
```

#### Code examples

```solidity
// Case 1 — tax token itself as dividend
params.quoteToken    = address(0);  // BNB
params.dividendBps   = 500;         // 5%
params.dividendToken = 0xfEEDFEEDfeEDFEedFEEdFEEDFeEdfEEdFeEdFEEd; // MAGIC_DIVIDEND_SELF

// Case 2 — native quote token (address(0) resolves to WBNB)
params.quoteToken    = address(0);
params.dividendBps   = 500;
params.dividendToken = address(0);  // OK — same as quoteToken

// Case 2 — ERC-20 quote token
params.quoteToken    = USDT;
params.dividendBps   = 500;
params.dividendToken = USDT;        // must match quoteToken for Case 2

// Case 3 — any ERC-20 (must be supported by SwapRegistry)
params.quoteToken    = address(0);  // BNB
params.dividendBps   = 500;
params.dividendToken = CAKE;        // any ERC-20 with a registered swap path

// Tracker-only — any dividendToken accepted, no swap-path check
params.dividendBps   = 0;
params.dividendToken = MY_TOKEN;    // no payout, just share tracking
```

#### SwapRegistry deployed addresses

| Network               | SwapRegistry Address                         |
| --------------------- | -------------------------------------------- |
| **BNB Chain Mainnet** | `0x644A8f560138418bAD4EdEFC7c17878a3c2fBEB6` |
| **BNB Chain Testnet** | `0xEd62Df92e52b89C8100F8EC3B8Eefea073df3408` |

***

## 4. Bidirectional dynamic liquidation threshold

### Background

Tax tokens accumulate their own tokens from buy/sell taxes and periodically liquidate (sell) the accumulated balance to the DEX to convert it to the quote token. The **liquidation threshold** governs how many tokens must accumulate before a liquidation is triggered.

### The V1 problem

`FlapTaxTokenV1` had a one-directional threshold: it could only decrease (making liquidations more frequent) but could never recover upward. In adverse market conditions this caused the threshold to drift to its minimum floor permanently, resulting in constant small liquidations that negatively impacted price.

### V3 fix: bidirectional adjustment

`FlapTaxTokenV3` introduces a **fully bidirectional** threshold that self-corrects in both directions after every liquidation. The `TaxProcessor` records a reference expected output amount (`liqExpectedOutputAmount`) at initialization and compares actual DEX swap output against it after each liquidation, returning an `int8 liqThresholdDirection` to the token:

| `liqThresholdDirection` | Market condition                                  | Threshold action                                                        |
| ----------------------- | ------------------------------------------------- | ----------------------------------------------------------------------- |
| `> 0` (positive)        | Swap output **below** reference — price is weak   | Increase threshold by 1% (wait for more tokens before next liquidation) |
| `< 0` (negative)        | Swap output **above** reference — price is strong | Decrease threshold by 1% (liquidate smaller amounts more frequently)    |
| `== 0`                  | Output matched reference                          | No change                                                               |

The threshold is bounded between two implementation-level immutables shared by all clones:

```solidity
FlapTaxTokenV3.MIN_LIQ_THRESHOLD    // hard floor — threshold never goes below this
FlapTaxTokenV3.START_LIQ_THRESHOLD  // starting value and hard ceiling for recovery
```

An individual token also retains `initialLiquidationThreshold` (set to `START_LIQ_THRESHOLD` at creation), which acts as the recovery ceiling. The threshold gradually climbs back toward `initialLiquidationThreshold` as market conditions improve, up to +1% per liquidation event.

### Inspecting the current threshold

```solidity
uint256 threshold  = IFlapTaxTokenV3(token).liquidationThreshold();
uint256 initial    = IFlapTaxTokenV3(token).initialLiquidationThreshold();
uint256 minLimit   = IFlapTaxTokenV3(token).MIN_LIQ_THRESHOLD();
uint256 startLimit = IFlapTaxTokenV3(token).START_LIQ_THRESHOLD();
```

***

## 5. Deployed contract addresses

### `FlapTaxTokenV3` implementation (clone base)

Use this address when computing vanity salts off-chain.

| Network               | Address                                      |
| --------------------- | -------------------------------------------- |
| **BNB Chain Mainnet** | `0x024f18294970B5c76c0691b87f138A0317156422` |
| **BNB Chain Testnet** | `0xE6Ff967a887084c16D0fD71548CF709542cc1557` |

### Example: launching a `TOKEN_TAXED_V3` token

```solidity
address tokenImplTaxedV3 = 0x024f18294970B5c76c0691b87f138A0317156422; // BSC mainnet
bytes32 salt = _findVanitySalt(VanityType.VANITY_7777, tokenImplTaxedV3, address(portal));

IPortalTypes.NewTokenV6Params memory params = IPortalTypes.NewTokenV6Params({
    name:                "My Token",
    symbol:              "MTK",
    meta:                "ipfs://...",
    dexThresh:           IPortalCommonTypes.DexThreshType.FOUR_FIFTHS,
    salt:                salt,
    migratorType:        IPortalTypes.MigratorType.V2_MIGRATOR,
    quoteToken:          address(0),         // BNB
    quoteAmt:            0,
    beneficiary:         msg.sender,
    permitData:          "",
    extensionID:         bytes32(0),
    extensionData:       "",
    dexId:               IPortalTypes.DEXId.DEX0,
    lpFeeProfile:        IPortalTypes.V3LPFeeProfile.LP_FEE_PROFILE_STANDARD,
    buyTaxRate:          300,                // 3% buy tax
    sellTaxRate:         1000,               // 10% sell tax (asymmetric)
    taxDuration:         365 days,
    antiFarmerDuration:  1 days,
    mktBps:              10000,              // 100% of remainder → beneficiary
    deflationBps:        0,
    dividendBps:         0,
    lpBps:               0,
    minimumShareBalance: 0,
    dividendToken:       address(0),         // native → resolved to WETH
    commissionReceiver:  0xYourAddress,      // enable commission
    tokenVersion:        IPortalTypes.TokenVersion.TOKEN_TAXED_V3
});

address token = portal.newTokenV6(params);
```


# Tax Liquidation Mechanism

{% hint style="info" %}
This page explains **how tax gets collected and converted into rewards** for holders, in plain terms. Some technical detail is included for developers, but you don't need to read Solidity to follow along.
{% endhint %}

## Overview

Every time someone buys or sells a Flap tax token, a small percentage is taken as **tax**. What happens to that tax next depends on one thing: **has the token's bonding curve finished (migrated to a DEX) or not?**

```mermaid
flowchart LR
    A[User buys/sells] --> B{Where does the trade happen?}
    B -->|Still on the Bonding Curve| C[Quote token tax paid directly]
    B -->|Migrated to DEX| D[Tax token itself accumulates]
    C --> E[Tax Processor]
    D -->|Threshold reached + trade through main pool| F[Liquidation: swap tax token → quote]
    F --> E
    E --> G[Fee / Market / LP / Burn buckets]
    E --> H[Dividend Contract]
    H --> I[Holders claim rewards]
```

The rest of this page walks through each stage of that diagram.

***

## 1. How tax is charged

### While still on the Bonding Curve

Before a token graduates to a DEX, all trading happens directly against Flap's bonding curve — there is no pool yet, so there is nothing to "liquidate." The tax is taken **in the quote token itself**, at the moment of the trade, and forwarded straight to the Tax Processor.

* If the quote is **BNB** (or another chain's native gas token), the trade contract wraps it into WBNB (or the equivalent wrapped native token) and forwards it.
* If the quote is an **ERC-20** (e.g. a token like `SPCXB`), it's forwarded as-is — no wrapping needed.

```mermaid
sequenceDiagram
    participant U as User
    participant BC as Bonding Curve (Portal)
    participant TP as Tax Processor

    U->>BC: Buy or sell (pays in BNB or ERC-20 quote)
    BC->>BC: Calculate tax amount (in quote token)
    alt Quote is BNB (native)
        BC->>BC: Wrap BNB → WBNB
        BC->>TP: processBondingCurveTax(WBNB amount)
    else Quote is ERC-20 (e.g. SPCXB)
        BC->>TP: processBondingCurveTax(SPCXB amount)
    end
    TP->>TP: Split into fee / market / LP / burn / dividend buckets
```

There's no waiting, no threshold, no separate "liquidation" step here — the quote token tax is already in the currency everyone wants (BNB, SPCXB, USDT, etc.), so it can be split and forwarded immediately.

### After the token migrates (is listed on a DEX)

Once a token graduates and starts trading on a DEX pool (PancakeSwap / Uniswap / a Uniswap-v2-style fork), the mechanism changes. The tax token contract now taxes trades **in the tax token itself** — every taxed buy or sell leaves a small amount of the *tax token* sitting inside the token's own contract balance.

That balance just sits there, growing with every trade, until it's large enough to be worth converting — that conversion step is called **liquidation** (covered in the next section).

```mermaid
flowchart TD
    subgraph BondingCurve["While on Bonding Curve"]
        BC1[Trade tax] --> BC2["Paid directly in quote token<br/>(BNB / SPCXB / etc.)"]
        BC2 --> BC3[Forwarded immediately to Tax Processor]
    end

    subgraph Migrated["After migration to DEX"]
        M1[Trade through pool] --> M2["Tax paid in the TAX TOKEN itself"]
        M2 --> M3["Accumulates inside the token contract's own balance"]
        M3 -->|Threshold reached| M4[Liquidation triggered]
        M4 --> M5[Swapped to quote token via the pool]
        M5 --> BC3
    end
```

***

## 2. When does liquidation trigger?

Liquidation only applies to **migrated** (DEX-listed) tokens — bonding curve tokens never need it, since their tax is already in quote-token form.

Two conditions must **both** be true for a liquidation to fire:

1. **The accumulated tax-token balance has reached the current liquidation threshold.**
2. **Someone is trading through the token's&#x20;*****main pool*** — the primary PancakeSwap/Uniswap-v2(-fork) pool paired with the token's quote token. Liquidation is checked (and can only trigger) on transfers that touch this pool; trades on unrelated/secondary pools don't count.

```mermaid
flowchart TD
    T[Someone trades through the main pool] --> C1{"Accumulated tax-token<br/>balance ≥ threshold?"}
    C1 -->|No| Skip[No liquidation — balance keeps growing]
    C1 -->|Yes| L[Liquidation runs]
    L --> S["Swap accumulated tax token → quote token<br/>(through the same main pool)"]
    S --> Adj{Was the swap price better or worse than expected?}
    Adj -->|Worse — big price impact| Down["Lower the threshold<br/>(liquidate smaller amounts, more often)"]
    Adj -->|Better — small price impact| Up["Raise the threshold<br/>(wait longer, liquidate bigger batches)"]
    Down --> Next[Threshold adjusts for next round]
    Up --> Next
```

### The threshold is dynamic, not fixed

The liquidation threshold **self-adjusts after every liquidation** — it isn't a fixed number. It moves within a range that's typically **50,000 to 400,000 tokens**, depending on the specific token's configuration:

* If a liquidation caused **more price impact than expected** (the swap was "expensive" — the price moved a lot), the threshold is **lowered** so future liquidations sell smaller amounts more frequently. Smaller, more frequent sells put less pressure on the price each time.
* If a liquidation caused **less price impact than expected** (there was plenty of liquidity to absorb it), the threshold is **raised** toward its starting ceiling, so the contract waits for more tokens to build up before selling again.

This means the mechanism is self-correcting: in a thin/volatile market it automatically liquidates in smaller, gentler batches; in a healthy/liquid market it can afford to wait and batch more together.

{% hint style="info" %}
**Why gate liquidation on "the main pool" specifically?** The main pool is where price discovery actually happens for the token. Restricting the liquidation check to main-pool trades (rather than any transfer) keeps the tax mechanism predictable and prevents it from firing based on unrelated token movement (e.g. wallet-to-wallet transfers, or activity on some other unrelated pool).
{% endhint %}

***

## 3. How the dividend mechanism works

Flap tax tokens can automatically reward holders with a share of the tax revenue. This works through two connected pieces: **share tracking** (who owns how much, updated on every transfer) and **dividend deposits** (rewards flowing in, distributed proportionally).

### Step A — every transfer updates your "share"

Every time tokens move — a buy, a sell, or a plain wallet-to-wallet transfer — the tax token contract calls into a separate **Dividend contract** to record the sender's and receiver's *new* balance as their "share." Your share is simply your current token balance (some addresses, like pools and the contracts themselves, are excluded).

```mermaid
sequenceDiagram
    participant From as Sender
    participant To as Receiver
    participant Token as Tax Token Contract
    participant Div as Dividend Contract

    From->>Token: transfer(to, amount)
    Token->>Token: Move balances (minus tax, if any)
    Token->>Div: setShare(from, newBalanceOfFrom)
    Token->>Div: setShare(to, newBalanceOfTo)
    Div->>Div: Settle any pending rewards for the old share, then record the new share
```

### Eligibility — when do you actually count as a holder?

* You must hold **at least the minimum share balance** the token was configured with (some tokens set this to a small non-zero amount to filter out dust holders; others accept any balance above zero).
* Certain addresses are **always excluded**, no matter their balance: the token contract itself, the burn address, the Dividend contract, the Tax Processor, and any registered trading pool. This prevents tax revenue from effectively being paid to itself or to liquidity pools instead of real holders.
* Being excluded or below the minimum sets your recorded share to **zero** — you stop accruing new rewards from that point on, though anything already earned remains claimable.

### Step B — where liquidated funds go before reaching holders

When a liquidation happens (or when quote-token tax arrives directly from the bonding curve), the proceeds don't go straight to holders. They pass through the **Tax Processor** first, which splits the incoming amount into buckets according to the token's configuration — for example: protocol fee, marketing/vault share, liquidity, burn (deflation), and **dividend**.

Only the portion allocated to the **dividend bucket** continues on to the Dividend contract. The Tax Processor accumulates this dividend portion, and periodically (via `dispatch()`) pushes it — as an actual token deposit — into the Dividend contract.

```mermaid
flowchart TD
    A["Tax revenue arrives<br/>(from bonding curve OR liquidation)"] --> TP[Tax Processor]
    TP --> Split{Split into buckets}
    Split --> Fee[Protocol fee]
    Split --> Market[Marketing / Vault]
    Split --> LP[Liquidity]
    Split --> Burn[Burn / Deflation]
    Split --> DivBucket[Dividend bucket]
    DivBucket -->|dispatch| Deposit["Dividend.deposit(amount)"]
    Deposit --> AccUpdate["Update accumulator:<br/>accPerShare += amount / totalShares"]
    AccUpdate --> Holders["All current eligible holders'<br/>claimable rewards increase proportionally"]
```

### Step C — how a deposit turns into "your" claimable rewards

The Dividend contract uses a well-known, gas-efficient pattern often called **"accumulated rewards per share"** (the same idea used by many staking/farming contracts). Here's the intuition, without the math notation:

1. The contract keeps one running number: **rewards accumulated per unit of share, ever** (`accPerShare`). Every time a deposit lands, this number increases by `deposit amount ÷ total shares outstanding at that moment`.
2. Each holder's claimable balance is effectively `(their share × the current accPerShare) − (their share × the accPerShare value from the last time their share changed or they claimed)`.
3. Because the accumulator only moves forward and each holder's personal "starting point" is recorded whenever their share changes (or they claim), this correctly gives everyone their **proportional slice of every deposit that happened while they held their current share** — without the contract needing to loop over every holder on every deposit.

```mermaid
flowchart LR
    subgraph Global["Global state (one accumulator)"]
        Acc["accPerShare<br/>(increases on every deposit)"]
        Total["totalShares<br/>(sum of all eligible holders' shares)"]
    end

    subgraph PerUser["Per-holder state"]
        Share["share<br/>(= token balance, if eligible)"]
        Debt["rewardDebt<br/>(accPerShare snapshot at last share change/claim)"]
        Pending["pendingBalance<br/>(rewards settled but not yet claimed)"]
    end

    Deposit[New deposit arrives] --> Acc
    Acc --> Claimable["Claimable = share × accPerShare − rewardDebt + pendingBalance"]
    Share --> Claimable
    Debt --> Claimable
    Pending --> Claimable
```

A practical consequence of this design: **you only earn rewards for the time you actually hold an eligible balance.** If you buy in after a big deposit already happened, your `rewardDebt` snapshot is set to the *current* accumulator value, so you don't retroactively get a share of rewards that were deposited before you held any tokens. Conversely, once you're holding, every future deposit is automatically counted for you — no need to "stake" or opt in separately; simply holding the token is enough.

### Claiming

Holders (or anyone calling on their behalf) can withdraw their claimable balance at any time. The reward token you receive depends on the tax token's configuration — it might be the quote token (e.g. BNB/WBNB), the tax token itself, or a different ERC-20 entirely; whichever was chosen as the token's "dividend token" at launch.

### Anyone can pay the gas to send *your* dividend to *you*

This is a subtle but genuinely useful feature of the Dividend contract: claiming is **permissionless on behalf of others**. There is no requirement that a holder pays their own gas to receive their rewards — any address, call it **A**, can trigger a withdrawal **for** another holder, call it **B**, and the reward tokens still land in **B's** wallet. A only pays the gas; A never receives B's funds.

```mermaid
sequenceDiagram
    participant A as Address A (pays gas)
    participant Div as Dividend Contract
    participant B as Address B (holder)

    A->>Div: withdrawDividendsFor(B)
    Div->>Div: Calculate B's claimable balance
    Div->>B: Send reward tokens directly to B
    Note over A,B: A never receives B's funds — A only paid the gas
```

Why would anyone bother paying gas to claim for someone else? Because it opens the door to **automated reward delivery** — you don't need every holder to remember to claim manually.

**Flap runs exactly this kind of automation as an off-chain bot:**

1. The bot continuously watches each holder's pending (claimable) dividend balance — the same `withdrawableDividendOf(user)` value described in [Step C](#step-c-how-a-deposit-turns-into-your-claimable-rewards) above.
2. Once a holder's pending rewards cross a **$4 USD-equivalent threshold**, the bot calls the withdraw-for-user function on their behalf.
3. The bot pays the gas for that transaction; the reward tokens are still sent directly to the holder's own wallet.

```mermaid
flowchart TD
    Bot["Off-chain bot"] -->|"1. Continuously polls"| Check["Holder's pending dividend balance"]
    Check --> Threshold{"Pending rewards ≥ $4?"}
    Threshold -->|No| Wait["Keep monitoring, do nothing"]
    Threshold -->|Yes| Pay["Bot pays gas: withdrawDividendsFor(holder)"]
    Pay --> Send["Reward tokens sent directly to the holder's wallet"]
```

The practical effect: small holders who might never bother claiming a few dollars' worth of rewards (because the gas cost isn't worth their own time or money) still get paid out automatically, without ever having to sign a transaction or spend their own gas.

***

## Summary

| Stage                   | Bonding Curve phase                                                       | After migration (DEX)                                                |
| ----------------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------- |
| Tax is collected in     | Quote token (BNB/WBNB, or the ERC-20 quote, e.g. SPCXB)                   | The tax token itself                                                 |
| Where it accumulates    | Forwarded to Tax Processor immediately                                    | Inside the tax token contract's own balance                          |
| Trigger to move forward | None needed — already in quote form                                       | Liquidation: threshold reached **and** a trade through the main pool |
| Threshold behavior      | N/A                                                                       | Dynamic, self-adjusting (typically \~50k–400k tokens)                |
| Final destination       | Tax Processor → fee/market/LP/burn/dividend buckets                       | Same — once liquidated into quote token                              |
| Holder rewards          | Dividend bucket deposited into Dividend contract → accPerShare accounting | Same                                                                 |

Whether a token is still bonding or already trading on a DEX, tax revenue always ends up flowing through the same **Tax Processor → Dividend contract** pipeline — the only thing that changes is *what form* the tax is in before it gets there, and *what has to happen* to convert it into that form.


# Wallet & Terminal & Bot Developers


# A Quick Start For Wallet & Terminal & Bot Developers

This page outlines the minimal steps to integrate with Flap for wallets, terminals and bots. Each step links to the detailed documentation.

1. **Find the `Portal` entrypoint.** Our main protocol has a single entrypoint contract called `Portal`. Use the deployed addresses listed in [Deployed Contract Addresses](/flap/developers/wallet-and-terminal-and-bot-developers/deployed-contract-addresses).
2. **Index tokens (recommended).** To index tokens launched on Flap, listen to `TokenCreated` and related events emitted by `Portal`. See [Index Token Created Events](/flap/developers/wallet-and-terminal-and-bot-developers/index-token-created-events) for the full event list and indexing strategy. The `TokenCreated` event only contains the IPFS CID of the metadata. To parse token metadata, follow [Parse Token Meta](/flap/developers/wallet-and-terminal-and-bot-developers/parse-token-meta).
3. **Skip indexing and query on demand (alternative).** If you do not want to index, you can query token info directly from `Portal` with the `getTokenV*` methods. See [Inspect A Token](/flap/developers/wallet-and-terminal-and-bot-developers/inspect-a-token) for details.
4. **Tax tokens: query dynamic info on-chain.** Tax tokens have dynamic fields (e.g., accumulated stats) that are best retrieved on-chain. Use the Tax Token Helper as described in [Inspect A Tax Token](/flap/developers/wallet-and-terminal-and-bot-developers/inspect-a-tax-token). For vault-specific data, see [how to inspect a tax token's vault](/flap/developers/wallet-and-terminal-and-bot-developers/inspect-a-tax-token#how-to-inspect-a-tax-tokens-vault).
5. **Trade tokens.** You can quote on-chain using `quoteExactInput`, or compute quotes off-chain and submit swaps using `swapExactInput`. See [Trade Tokens](/flap/developers/wallet-and-terminal-and-bot-developers/trade-tokens) for details.


# Deployed Contract Addresses

See [Deployed Contract Addresses](/flap/developers/deployed-contract-addresses).


# Index Token Created Events

This page describes how to index newly launched tokens by consuming events emitted by the `Portal` contract.

## Overview

For backward compatibility, a token launch can emit multiple events instead of a single one. Always index `TokenCreated`, then enrich the token record with any optional events that appear in the same transaction.

{% hint style="info" %}
`TokenCreated` only includes the IPFS CID of the metadata. To resolve and parse token metadata, see [Parse Token Meta](/flap/developers/wallet-and-terminal-and-bot-developers/parse-token-meta).
{% endhint %}

## Events to index

**Required**

* `TokenCreated`: emitted for every token launch.

**Optional (apply defaults if missing)**

* `TokenCurveSet`: if missing, curve defaults to the first item in `CurveType` (legacy curve, `curveParameter = 16 ether`).
* `TokenCurveSetV2`: starting from v4.7.0, always emitted even for legacy curve.
* `TokenDexSupplyThreshSet`: if missing, defaults to the first item in `DexThreshType` (6.67e8 ether).
* `TokenQuoteSet`: if missing, defaults to native gas token (zero address).
* `TokenMigratorSet`: if missing, defaults to `V3_MIGRATOR`.
* `TokenVersionSet`: if missing, defaults to legacy token version (see [Token version specification](/flap/developers/wallet-and-terminal-and-bot-developers/token-version-specification)).
* `FlapTokenTaxSet`: if missing, tax is 0 (non-tax token).
* `FlapTokenStaged`: emitted when a token is staged but not yet created (two-step token launch).
* `TokenExtensionEnabled`: emitted when an extension is enabled for a token.
* `TokenDexPreferenceSet`: if missing, defaults to DEX0 with STANDARD fee profile.
* `FlapTokenAsymmetricTaxSet`: emitted alongside `FlapTokenTaxSet` for Tax Token V3 launches; carries separate buy and sell tax rates. If missing, treat buy and sell rates as equal to the value from `FlapTokenTaxSet`.

## Event reference (arguments and meaning)

### `TokenCreated`

Emitted for every token launch.

* `ts`: block timestamp when the token is created.
* `creator`: address that initiated the token creation.
* `nonce`: portal nonce for this creation (unique per `Portal`).
* `token`: deployed token address.
* `name`: token name.
* `symbol`: token symbol.
* `meta`: IPFS CID of the token metadata JSON.

### `FlapTokenStaged`

Emitted when a token is staged (two-step launch) but not yet created.

* `ts`: block timestamp when staging happens.
* `creator`: address that staged the token.
* `token`: predetermined token address (not yet deployed).

### `TokenCurveSet`

Emitted when the bonding curve configuration is set for legacy curve format.

* `token`: token address.
* `curve`: curve contract address.
* `curveParameter`: curve parameter for the legacy curve (defaults to `16 ether` if missing).

### `TokenCurveSetV2`

Emitted when the bonding curve parameters are set in the newer format.

* `token`: token address.
* `r`: virtual ETH reserve parameter.
* `h`: virtual token reserve parameter.
* `k`: square of virtual liquidity parameter.

### `TokenDexSupplyThreshSet`

Emitted when the DEX listing supply threshold is set.

* `token`: token address.
* `dexSupplyThresh`: circulating supply threshold for DEX listing (defaults to the first `DexThreshType` if missing).

### `TokenQuoteSet`

Emitted when the quote token is set.

* `token`: token address.
* `quoteToken`: quote token address (zero address means native gas token).

### `TokenMigratorSet`

Emitted when the migrator type is set.

* `token`: token address.
* `migratorType`: migrator enum value.

```solidity
enum MigratorType {
    V3_MIGRATOR,              // Migrate liquidity to a Uniswap V3-like pool
    V2_MIGRATOR,              // Migrate liquidity to a Uniswap V2-like pool
    V4_UNI_MIGRATOR,          // Migrate liquidity to a Uniswap V4 pool (Base, XLayer)
    PCS_INFINITY_CL_MIGRATOR  // Migrate liquidity to a PancakeSwap Infinity CL pool (BNB)
}
```

`migratorType` says *which version/protocol shape* the liquidity migrates into (V2-style, V3-style, or one of the two CL-style migrators). It does not by itself say *which DEX* — that's `TokenDexPreferenceSet.dexId` (below). The two together fully determine the destination.

### `TokenVersionSet`

Emitted when the token implementation version is set.

* `token`: token address.
* `version`: token version enum value (see [Token version specification](/flap/developers/wallet-and-terminal-and-bot-developers/token-version-specification)).

### `FlapTokenTaxSet`

Emitted when a tax is set for a token.

* `token`: token address.
* `tax`: tax rate in basis points (0 means non-tax token).

### `TokenExtensionEnabled`

Emitted when an extension is enabled for a token.

* `token`: token address.
* `extensionID`: extension identifier (bytes32).
* `extensionAddress`: extension contract address.
* `version`: extension interface version.

### `TokenDexPreferenceSet`

Emitted when DEX preference and fee profile are set.

* `token`: token address.
* `dexId`: preferred DEX ID (`DEX0`, `DEX1`, `DEX2`).
* `lpFeeProfile`: preferred V3 LP fee profile (`STANDARD`, `LOW`, `HIGH`).

`dexId` is chain-specific — the same enum value maps to a different DEX protocol depending on which chain the `Portal` is deployed on:

| Chain           | `DEX0`      | `DEX1`      | `DEX2` |
| --------------- | ----------- | ----------- | ------ |
| BNB Chain       | PancakeSwap | —           | —      |
| Robinhood Chain | Uniswap     | PancakeSwap | —      |
| X Layer         | PotatoSwap  | —           | —      |
| Monad           | Uniswap     | PancakeSwap | Monday |

{% hint style="warning" %}
Note the difference between chains: on BNB, `DEX0` is PancakeSwap. On Robinhood Chain, `DEX0` is Uniswap and `DEX1` is PancakeSwap — the reverse assignment. Always resolve `dexId` against the chain you're indexing, never hardcode a mapping across chains.
{% endhint %}

#### Resolving the destination: `dexId` + `migratorType`

`dexId` picks the DEX protocol (per the table above), and `migratorType` picks the pool version/shape on that protocol. Read both events from the same token-creation transaction to know exactly where a token will graduate to:

| `migratorType`             | Destination                                                                      |
| -------------------------- | -------------------------------------------------------------------------------- |
| `V3_MIGRATOR`              | V3 pool on the DEX selected by `dexId`                                           |
| `V2_MIGRATOR`              | V2 pool on the DEX selected by `dexId`                                           |
| `V4_UNI_MIGRATOR`          | Uniswap V4 (`dexId` is not relevant — always Uniswap)                            |
| `PCS_INFINITY_CL_MIGRATOR` | PancakeSwap Infinity CL (`dexId` is not relevant — always PancakeSwap, BNB only) |

Example — Robinhood Chain, `dexId = DEX1` (PancakeSwap) and `migratorType = V3_MIGRATOR` → the token graduates to **PancakeSwap V3** on Robinhood Chain.

If `TokenDexPreferenceSet` is missing, apply the default noted above (`DEX0` with `STANDARD` fee profile). If `TokenMigratorSet` is missing, it defaults to `V3_MIGRATOR`.

### `FlapTokenAsymmetricTaxSet`

Emitted for Tax Token V3 (`TOKEN_TAXED_V3`) launches alongside `FlapTokenTaxSet`. Carries the full asymmetric buy and sell tax rates.

* `token`: token address.
* `buyTax`: buy tax rate in basis points.
* `sellTax`: sell tax rate in basis points.

{% hint style="info" %}
`FlapTokenTaxSet` is always emitted for all tax tokens and carries `max(buyTax, sellTax)` for backward compatibility. `FlapTokenAsymmetricTaxSet` is only emitted for V3 tokens. Prefer `FlapTokenAsymmetricTaxSet` when available to get the full asymmetric rate detail.
{% endhint %}

## Suggested indexing flow

1. Listen to `TokenCreated` events on `Portal`.
2. In the same transaction, collect optional events for the same token address.
3. Apply defaults for any missing optional events.
4. Persist the token record and metadata CID.


# Indexing Vault Factories & Vaults

This chapter teaches the generic pattern for indexing every vault created through `VaultPortal` — the factory that produced it, and the vault itself. It is vault-type agnostic on purpose: one event covers all vaults, and you branch on the factory to enrich each record.

Vault-type-specific guides (which dividend asset a vault pays, how to read its composition, which extra events it emits) are the sub-chapters that follow, such as [Index & Query a Stocks Vault](/flap/developers/wallet-and-terminal-and-bot-developers/indexing-vaults/index-and-query-a-stocks-vault). Each vault type is documented the same way.

## Overview

A tax token can be launched together with a revenue vault in a single transaction through `VaultPortal.newTaxTokenWithVault` / `newTokenV6WithVault`. No matter which factory produced the vault, `VaultPortal` emits one universal signal:

```solidity
/// @notice Emitted when a new tax token with an associated vault is successfully created
/// @param token        The address of the newly deployed tax token
/// @param vault         The address of the newly created vault that will receive tax revenue
/// @param vaultFactory  The vault factory address that was used to create the vault
event FlapTaxVaultTokenCreated(
    address indexed token,
    address indexed vault,
    address indexed vaultFactory
);
```

All three arguments are indexed, so you can filter by `token`, `vault`, or `vaultFactory` directly at the log level.

{% hint style="info" %}
`FlapTaxVaultTokenCreated` is emitted by `VaultPortal` — not by the individual factories. Index it on the `VaultPortal` address for the chain you are following (see [Deployed Contract Addresses](/flap/developers/deployed-contract-addresses)). Individual factories may emit their own richer events; those are documented per vault type in the sub-chapters.
{% endhint %}

## Suggested indexing flow

1. **Listen** to `FlapTaxVaultTquokenCreated` on `VaultPortal`.
2. **Persist** `(token, vault, vaultFactory)` plus the block/tx context.
3. **Classify** the vault by its `vaultFactory` address (see [Identify the vault type](#identify-the-vault-type)).
4. **Enrich** the record with vault-type-specific data by reading from the vault/factory, or by calling `VaultPortal.getVault` / `tryGetVault` for a generic summary.

Because the vault type is fully determined by `vaultFactory`, adding support for a new vault type is additive: keep indexing the same event, and add a new branch for the new factory address. Each vault type is documented in its own sub-chapter.

## Identify the vault type

**The `vaultFactory` address is the type key.** How you decode a vault (which dividend asset it pays, which extra events it emits) is determined entirely by the factory that produced it, so maintain a per-chain allowlist mapping each known `vaultFactory` address to a vault type and its decoding logic. A `vaultFactory` that is not in your allowlist is a vault type you do not decode yet — flag it so you know to add a decoding branch. The Stocks (RWA / Index) factory addresses are listed in [Index & Query a Stocks Vault](/flap/developers/wallet-and-terminal-and-bot-developers/indexing-vaults/index-and-query-a-stocks-vault).

## Enrich via VaultPortal (generic summary)

If you only need a generic summary and do not want to talk to each factory, `VaultPortal` exposes two view helpers:

```solidity
struct VaultInfo {
    address vault;
    address vaultFactory;   // zero address if unknown
    string  description;    // human-readable, from the vault's description()
    bool    isOfficial;     // endorsed by the protocol
    RiskLevel riskLevel;    // audit risk classification
}

/// @notice Complete vault information for a tax token. Reverts if no vault is found.
function getVault(address taxToken) external view returns (VaultInfo memory info);

/// @notice Non-reverting variant. Falls back to searching the Portal.
///         For fallback results, isOfficial is always false.
/// @return found Whether a vault was found for this tax token
/// @return info  The VaultInfo struct (empty if not found)
function tryGetVault(address taxToken) external view returns (bool found, VaultInfo memory info);
```

Prefer `tryGetVault` in an indexer: it never reverts, so a token with no vault simply returns `found = false` instead of failing your batch call. `getVault` reverts with `VaultNotFound(taxToken)` when there is no vault.

{% hint style="info" %}
`VaultInfo.description` is produced by the vault's own `description()` method and is intended for display. It is not a stable schema — do not parse it for machine-readable fields. Read typed values from the vault/factory instead (see the per-vault-type sub-chapters).
{% endhint %}

## Vault types

* [Index & Query a Stocks Vault](/flap/developers/wallet-and-terminal-and-bot-developers/indexing-vaults/index-and-query-a-stocks-vault) — RWA / tokenized-stock vaults, across all three factory versions.


# Index & Query a Stocks Vault

A **Stocks Vault** pays dividends in one or more Real-World Assets (RWA) — in practice tokenized equities such as NVDAon, AAPLon, TSLAon, MSFTon. This sub-chapter builds on the generic flow in [Indexing Vault Factories & Vaults](/flap/developers/wallet-and-terminal-and-bot-developers/indexing-vaults) and covers the two things specific to Stocks Vaults: how to index the **token + vault** each factory produces, and how to read the **dividend stock token(s)** that vault distributes.

There are **three factory versions** in production. All three are Stocks Vaults and all three surface through the same `FlapTaxVaultTokenCreated` event on `VaultPortal`. They differ only in how you read the dividend asset(s).

## Factory addresses

Factory addresses are per-chain. Maintain the allowlist keyed by chain.

**BNB Mainnet**

| Version | Factory contract    | Vault Factory address                        | Dividend model          |
| ------- | ------------------- | -------------------------------------------- | ----------------------- |
| v1      | `RWAVaultFactory`   | `0xf8aC088F06D155f3C3F531f1Ef80B14f1604530a` | Single RWA asset        |
| v2      | `RWAVaultFactory`   | `0x40a9a2FDa017E0923EA0B403F2f063f9E51168Fb` | Single RWA asset        |
| v3      | `IndexVaultFactory` | `0x5418f7e8fF90354DB0eCD48c8b710219244Eb3C5` | Composite basket (1..N) |

**Robinhood Mainnet**

| Version | Factory contract    | Factory address                              | Dividend model          |
| ------- | ------------------- | -------------------------------------------- | ----------------------- |
| v3      | `IndexVaultFactory` | `0xe6ca297D1d963b6F00d5b216986123CAeB883AF6` | Composite basket (1..N) |

v1 and v2 are read the same way (both deploy an `RWAVault` paying a single asset). v3 (`IndexVaultFactory`) pays a **basket** of 1..N assets chosen at creation time.

## Step 1 — Index the token & vault

Start from the generic signal and branch on `vaultFactory`:

```solidity
event FlapTaxVaultTokenCreated(
    address indexed token,
    address indexed vault,
    address indexed vaultFactory
);
```

Persist `(token, vault, vaultFactory)`. When `vaultFactory` matches one of the three addresses above, record the vault as a Stocks Vault and remember the **version** — it decides how you read the dividend token in Step 2.

### v3 also emits the basket directly

`IndexVaultFactory` (v3) emits its own creation event carrying the dividend token and its underlying subset, so you can capture them at index time and skip the follow-up call in Step 2:

```solidity
/// @param taxToken The tax token this vault backs
/// @param vault    The IndexVault proxy
/// @param basket   The IndexBasketToken (the ERC-20 dividend token holders receive)
/// @param subset   The underlying stock tokens selected for this vault's basket
event IndexVaultCreated(
    address indexed taxToken,
    address indexed creator,
    address vault,
    address basket,
    address[] subset
);
```

## Step 2 — Read the dividend stock token(s)

This is the part that differs by version.

### v1 & v2 — single token via `rwaAsset`

For v1/v2 the vault is an `RWAVault` and pays exactly one RWA stock token. Read it from the vault:

```solidity
interface IRWAVault {
    /// @notice The single RWA dividend token this vault is configured for.
    ///         Returns address(0) until it is resolved (see note below).
    function rwaAsset() external view returns (address);
}

address token = IRWAVault(vault).rwaAsset();
```

{% hint style="warning" %}
**`rwaAsset` resolves lazily.** The vault is created *before* the tax token exists, so `rwaAsset` starts as `address(0)` and is only cached the first time the token receives tax revenue (or when anyone calls `resolveRwaAsset()`). If you read `address(0)`, retry later, or call `resolveRwaAsset()` once the token has launched to prime it.
{% endhint %}

### v3 — basket via `supportedAssets()`

For v3 the vault is an `IndexVault` and pays a **basket**. `rwaAsset` does not exist on v3. Read instead:

```solidity
interface IIndexVault {
    /// @notice The underlying stock tokens this vault distributes (the subset chosen at creation).
    function supportedAssets() external view returns (address[] memory);

    /// @notice The IndexBasketToken — the ERC-20 that dividend holders actually receive.
    ///         Holders can unwrap it into the underlying subset tokens.
    function basketToken() external view returns (address);
}

address[] memory tokens = IIndexVault(vault).supportedAssets();
```

`supportedAssets()` returns the immutable subset baked in at creation — the same list carried by `IndexVaultCreated.subset`, so you can capture it at index time and skip the call.

{% hint style="info" %}
For v3, dividends are paid in the vault's own `IndexBasketToken` (an ERC-20 basket), not directly in the underlying stocks. Read `basketToken()` when you need the address that appears in holders' dividend balances; read `supportedAssets()` for the underlying stock tokens it unwraps into.
{% endhint %}

## Suggested indexing flow (Stocks Vault)

1. Index `FlapTaxVaultTokenCreated` on `VaultPortal`; persist `(token, vault, vaultFactory)`.
2. Branch when `vaultFactory` is one of the three Stocks factories; record the version.
3. Resolve the dividend stock token(s):
   * **v1 / v2**: `rwaAsset()` on the vault (retry while it returns `address(0)`).
   * **v3**: `supportedAssets()` on the vault (underlying stocks) and `basketToken()` (the token holders receive), or capture them from `IndexVaultCreated` at index time.


# Bonding Curve In Developers' Perspective

## How to find the reserve/price/fdv from the circulating supply of a token?

### Option1: Read from the Chain

You can directly read the information about a token through the `getTokenV2` call to the on chain contract.

```solidity
/// @notice Get token state (V5)
/// @param token  The address of the token
/// @return state  The state of the token (V5) with all curve parameters (r, h, k)
function getTokenV5(address token) external view returns (TokenStateV5 memory state);

/// @dev Token version
/// Which token implementation is used
enum TokenVersion {
    TOKEN_LEGACY_MINT_NO_PERMIT,
    TOKEN_LEGACY_MINT_NO_PERMIT_DUPLICATE, // for historical reasons, both 0 and 1 are the same: TOKEN_LEGACY_MINT_NO_PERMIT
    TOKEN_V2_PERMIT,
    TOKEN_GOPLUS
}
/// @notice the status of a token
/// The token has 4 statuses:
//    - Tradable: The token can be traded(buy/sell)
//    - InDuel: (obsolete) The token is in a battle, it can only be bought but not sold.
//    - Killed: (obsolete) The token is killed, it can not be traded anymore. Can only be redeemed for another token.
//    - DEX: The token has been added to the DEX
enum TokenStatus {
    Invalid, // The token does not exist
    Tradable,
    InDuel, // obsolete
    Killed, // obsolete
    DEX
}

/// @notice the state of a token (with all V4 fields plus all curve parameters)
struct TokenStateV5 {
    /// The status of the token (see TokenStatus enum)
    TokenStatus status;
    /// The reserve amount of the quote token held by the bonding curve
    uint256 reserve;
    /// The circulating supply of the token
    uint256 circulatingSupply;
    /// The current price of the token (in quote token units, 18 decimals)
    uint256 price;
    /// The version of the token implementation (see TokenVersion enum)
    TokenVersion tokenVersion;
    /// The curve parameter 'r' used for the bonding curve
    uint256 r;
    /// The curve parameter 'h' - virtual token reserve
    uint256 h;
    /// The curve parameter 'k' - square of virtual liquidity
    uint256 k;
    /// The circulating supply threshold for adding the token to the DEX
    uint256 dexSupplyThresh;
    /// The address of the quote token (address(0) if native gas token)
    address quoteTokenAddress;
    /// Whether native-to-quote swap is enabled for this token
    bool nativeToQuoteSwapEnabled;
    /// The extension ID used by the token (bytes32(0) if no extension)
    bytes32 extensionID;
}


```

### Option 2: Derive price/fdv/reserve from the current circulating supply

{% hint style="warning" %}
Check [Bonding Curve](/flap/developers/basic-and-mechanism/bonding-curve#bonding-curves-on-different-chains)for the r of each deployment\
\
We don't recomend that you hardcode the bonding curve parameters in your application. Instead, you should use `getTokenV5` method from the Portal contract to get the parameters for each token. The parameters are immutable for each token, so you only need to fetch them once and cache them in your application.
{% endhint %}

Whenever a user buys or sells token on the bonding curve, the following event would be emitted:

```solidity
/// @notice emitted when the circulating supply of a token changes
/// @param token The address of the token
/// @param newSupply The new circulating supply
event FlapTokenCirculatingSupplyChanged(address token, uint256 newSupply);

```

You can simply index this event to get the latest circulating supply of any token. With the circulating supply of the token, you can get the reserve, price and FDV with the following `typescript` snippet:

```typescript

import { Decimal } from "decimal.js";

const BILLION: Decimal = new Decimal("1000000000");


// The latest curve is CDPV2
export class CDPV2 {
    // the initial virtual reserve
    private r: number;
    private h: number;
    private k: number;


    static defaultDexSupplyThreshold() {
        return new Decimal(8e8);
    }

    static getCurve(r: number, h?: number, k?: number): CDPV2 {
        if (h == null) {
            return new CDPV2(r, 0, 1e9 * r);
        }
        return new CDPV2(r, h, k);
    }

    constructor(r: number, h: number = 0, k: number = 0) {
        this.r = r;
        this.h = h;
        this.k = k;
    }

    estimateSupply(reserve: string): Decimal {
        // s = 1e9 + h - k/(r + eth)
        if (!reserve) return new Decimal(0);
        return new Decimal(BILLION).add(this.h).sub(
            new Decimal(this.k).div(new Decimal(reserve).add(this.r))
        );
    }

    estimateReserve(amount: string): Decimal {
        // eth = k/(h + 1e9 - s) - r
        if (!amount) return new Decimal(0);
        return new Decimal(this.k)
            .div(new Decimal(BILLION).add(this.h).sub(new Decimal(amount)))
            .sub(this.r);
    }

    mc(reserve: string): Decimal {
        return this.fdv(this.totalSupply(reserve).toString());
    }

    price(supply: string): Decimal {
        // Price: k/(h + 1e9 - s)^2
        const denominator = new Decimal(BILLION).add(this.h).sub(new Decimal(supply || 0));
        return new Decimal(this.k).div(denominator.pow(2));
    }

    fdv(supply: string): Decimal {
        return this.price(supply).mul(new Decimal(BILLION));
    }
}
```

## How to calculate the progress of a token

The progress is defined as the ratio of the current reserve to the required reserve for the token to migrate.

You need the following information to calculate the progress:

* `Circulating Supply`: The current circulating supply of the token. You can either index the `FlapTokenCirculatingSupplyChanged` event, or use the `getTokenV5` method to get the `circulatingSupply` of the token, or simply use the `balanceOf(portal)` to estimate the circulating supply (This estimation is accurate as long as no one directly sends token to our contract).
* `Curve Parameters`: You can use the `getTokenV5` to get the curve parameters: `r`, `h` and `k`. Or you can index the `TokenCurveSetV2` event. The curve parameters are immutable for a token, you only need to get them once.
* `Dex Threshold`: You can use the `getTokenV5` to get the `dexThreshold` of the token, or index the `TokenDexThresholdSet` event. The dex threshold is immutable, you only need to get it once. It represents the circulating supply at which the token will be migrated.

The latter two variables are immutable for a token, you only need to get them once. We highly recommend that you to use the the `getTokenV5` method to get all the parameters at once.

With all the above information, you can calculate the prorgress of the token:

* We first construct a `Curve` instance using the curve parameters. Note the curve parameteres are in 18 decimals, so we need to divide them by `1e18` to get the actual values (or use formatEther from viem).
* We first calculate the `required reserve` for the token to migrate
* Then we derive the `current reserve`
* The progress is the ratio betweeen `current reserve` and `required reserve`

```typescript


// assume we have all the following parameters from  getTokenV5 
// Note: all parameters are in 18 decimals 

// curve parameters  
let r,h,k: bigint;

// circulating supply
let circulatingSupply: bigint; 

// dex threshold 
let dexThreshold: bigint;  




const curve =  CDPV2.getCurve( 
    Number( formatEther(r) ), 
    Number( formatEther(h) ), 
    Number( formatEther(k) ) 
); 



// The reserve required at the dex threshold 
const reserveRequired = curve.estimateReserve(
    formatEther(dexThreshold)
);

// The current reserve at the current circulating supply
const currentReserve = curve.estimateReserve(
    formatEther(circulatingSupply)
);


// The progress can be calculated as:
const progress = currentReserve / reserveRequired; 

```


# Inspect A Token

## Inspect A Token

We have multiple methods for querying the state of a token. The latest ones are `getTokenV8` and `getTokenV8Safe` (currently only available on BNB mainnet and BNB testnet). For other networks, use `getTokenV7`. To avoid the broken code problem, we still reserve the legacy methods `getTokenV2` and `getTokenV3`. We always recommend using the newest available method for your network.

```solidity

/// @notice the state of a token (with dex related fields)
struct TokenStateV2 {
    TokenStatus status; // the status of the token
    uint256 reserve; // the reserve of the token
    uint256 circulatingSupply; // the circulatingSupply of the token
    uint256 price; // the price of the token
    TokenVersion tokenVersion; // the version of the token implementation this token is using
    uint256 r; // the r of the curve of the token
    uint256 dexSupplyThresh; // the cirtulating supply threshold for adding the token to the DEX
}

/// @notice the state of a token (with all V2 fields plus quoteTokenAddress and nativeToQuoteSwapEnabled)
struct TokenStateV3 {
    /// The status of the token (see TokenStatus enum)
    TokenStatus status;
    /// The reserve amount of the quote token held by the bonding curve
    uint256 reserve;
    /// The circulating supply of the token
    uint256 circulatingSupply;
    /// The current price of the token (in quote token units, 18 decimals)
    uint256 price;
    /// The version of the token implementation (see TokenVersion enum)
    TokenVersion tokenVersion;
    /// The curve parameter 'r' used for the bonding curve
    uint256 r;
    /// The circulating supply threshold for adding the token to the DEX
    uint256 dexSupplyThresh;
    /// The address of the quote token (address(0) if native gas token)
    address quoteTokenAddress;
    /// Whether native-to-quote swap is enabled for this token
    bool nativeToQuoteSwapEnabled;
}

/// @notice the state of a token (with all V3 fields plus extensionID and 'r' curve parameter only)
struct TokenStateV4 {
    /// The status of the token (see TokenStatus enum)
    TokenStatus status;
    /// The reserve amount of the quote token held by the bonding curve
    uint256 reserve;
    /// The circulating supply of the token
    uint256 circulatingSupply;
    /// The current price of the token (in quote token units, 18 decimals)
    uint256 price;
    /// The version of the token implementation (see TokenVersion enum)
    TokenVersion tokenVersion;
    /// The curve parameter 'r' used for the bonding curve
    uint256 r;
    /// The circulating supply threshold for adding the token to the DEX
    uint256 dexSupplyThresh;
    /// The address of the quote token (address(0) if native gas token)
    address quoteTokenAddress;
    /// Whether native-to-quote swap is enabled for this token
    bool nativeToQuoteSwapEnabled;
    /// The extension ID used by the token (bytes32(0) if no extension)
    bytes32 extensionID;
}

/// @notice the state of a token (with all V4 fields plus all curve parameters)
struct TokenStateV5 {
    /// The status of the token (see TokenStatus enum)
    TokenStatus status;
    /// The reserve amount of the quote token held by the bonding curve
    uint256 reserve;
    /// The circulating supply of the token
    uint256 circulatingSupply;
    /// The current price of the token (in quote token units, 18 decimals)
    uint256 price;
    /// The version of the token implementation (see TokenVersion enum)
    TokenVersion tokenVersion;
    /// The curve parameter 'r' used for the bonding curve
    uint256 r;
    /// The curve parameter 'h' - virtual token reserve
    uint256 h;
    /// The curve parameter 'k' - square of virtual liquidity
    uint256 k;
    /// The circulating supply threshold for adding the token to the DEX
    uint256 dexSupplyThresh;
    /// The address of the quote token (address(0) if native gas token)
    address quoteTokenAddress;
    /// Whether native-to-quote swap is enabled for this token
    bool nativeToQuoteSwapEnabled;
    /// The extension ID used by the token (bytes32(0) if no extension)
    bytes32 extensionID;
}

/// @notice the state of a token (with all V5 fields plus taxRate, pool, and progress)
struct TokenStateV6 {
    /// The status of the token (see TokenStatus enum)
    TokenStatus status;
    /// The reserve amount of the quote token held by the bonding curve
    uint256 reserve;
    /// The circulating supply of the token
    uint256 circulatingSupply;
    /// The current price of the token (in quote token units, 18 decimals)
    uint256 price;
    /// The version of the token implementation (see TokenVersion enum)
    TokenVersion tokenVersion;
    /// The curve parameter 'r' used for the bonding curve
    uint256 r;
    /// The curve parameter 'h' - virtual token reserve
    uint256 h;
    /// The curve parameter 'k' - square of virtual liquidity
    uint256 k;
    /// The circulating supply threshold for adding the token to the DEX
    uint256 dexSupplyThresh;
    /// The address of the quote token (address(0) if native gas token)
    address quoteTokenAddress;
    /// Whether native-to-quote swap is enabled for this token
    bool nativeToQuoteSwapEnabled;
    /// The extension ID used by the token (bytes32(0) if no extension)
    bytes32 extensionID;
    /// The tax rate in basis points (0 if not a tax token)
    uint256 taxRate;
    /// The DEX pool address (address(0) if not listed on DEX)
    address pool;
    /// The progress towards DEX listing (0 to 1e18, where 1e18 = 100%)
    uint256 progress;
}

/// @notice the state of a token (with all V6 fields plus lpFeeProfile)
struct TokenStateV7 {
    /// The status of the token (see TokenStatus enum)
    TokenStatus status;
    /// The reserve amount of the quote token held by the bonding curve
    uint256 reserve;
    /// The circulating supply of the token
    uint256 circulatingSupply;
    /// The current price of the token (in quote token units, 18 decimals)
    uint256 price;
    /// The version of the token implementation (see TokenVersion enum)
    TokenVersion tokenVersion;
    /// The curve parameter 'r' used for the bonding curve
    uint256 r;
    /// The curve parameter 'h' - virtual token reserve
    uint256 h;
    /// The curve parameter 'k' - square of virtual liquidity
    uint256 k;
    /// The circulating supply threshold for adding the token to the DEX
    uint256 dexSupplyThresh;
    /// The address of the quote token (address(0) if native gas token)
    address quoteTokenAddress;
    /// Whether native-to-quote swap is enabled for this token
    bool nativeToQuoteSwapEnabled;
    /// The extension ID used by the token (bytes32(0) if no extension)
    bytes32 extensionID;
    /// The tax rate in basis points (0 if not a tax token)
    uint256 taxRate;
    /// The DEX pool address (address(0) if not listed on DEX)
    address pool;
    /// The progress towards DEX listing (0 to 1e18, where 1e18 = 100%)
    uint256 progress;
    /// The V3 LP fee profile for the token
    V3LPFeeProfile lpFeeProfile;
    /// The Dex Id
    DEXId dexId;
}

struct TokenStateV8 {
    /// The status of the token (see TokenStatus enum)
    TokenStatus status;
    /// The reserve amount of the quote token held by the bonding curve
    uint256 reserve;
    /// The circulating supply of the token
    uint256 circulatingSupply;
    /// The current price of the token (in quote token units, 18 decimals)
    uint256 price;
    /// The version of the token implementation (see TokenVersion enum)
    TokenVersion tokenVersion;
    /// The curve parameter 'r' used for the bonding curve
    uint256 r;
    /// The curve parameter 'h' - virtual token reserve
    uint256 h;
    /// The curve parameter 'k' - square of virtual liquidity
    uint256 k;
    /// The circulating supply threshold for adding the token to the DEX
    uint256 dexSupplyThresh;
    /// The address of the quote token (address(0) if native gas token)
    address quoteTokenAddress;
    /// Whether native-to-quote swap is enabled for this token
    bool nativeToQuoteSwapEnabled;
    /// The extension ID used by the token (bytes32(0) if no extension)
    bytes32 extensionID;
    /// The buy tax rate in basis points (0 if not a tax token)
    uint256 buyTaxRate;
    /// The sell tax rate in basis points (0 if not a tax token)
    uint256 sellTaxRate;
    /// The DEX pool address (address(0) if not listed on DEX)
    address pool;
    /// The progress towards DEX listing (0 to 1e18, where 1e18 = 100%)
    uint256 progress;
    /// The V3 LP fee profile for the token
    V3LPFeeProfile lpFeeProfile;
    /// The Dex Id
    DEXId dexId;
}

/// @notice A forward-compatible version of TokenStateV8, where enum-typed fields are returned
///         as uint8 instead of their Solidity enum types.
struct TokenStateV8Safe {
    uint8 status;
    uint256 reserve;
    uint256 circulatingSupply;
    uint256 price;
    uint8 tokenVersion;
    uint256 r;
    uint256 h;
    uint256 k;
    uint256 dexSupplyThresh;
    address quoteTokenAddress;
    bool nativeToQuoteSwapEnabled;
    bytes32 extensionID;
    uint256 buyTaxRate;
    uint256 sellTaxRate;
    address pool;
    uint256 progress;
    uint8 lpFeeProfile;
    uint8 dexId;
}

/// @notice Get token state
/// @param token  The address of the token
/// @return state  The state of the token
function getTokenV2(address token) external view returns (TokenStateV2 memory state);
/// @notice Get token state (V3)
/// @param token  The address of the token
/// @return state  The state of the token (V3)
function getTokenV3(address token) external view returns (TokenStateV3 memory state);
/// @notice Get token state (V4)
/// @param token  The address of the token
/// @return state  The state of the token (V4) with only 'r' curve parameter
function getTokenV4(address token) external view returns (TokenStateV4 memory state);
/// @notice Get token state (V5)
/// @param token  The address of the token
/// @return state  The state of the token (V5) with all curve parameters (r, h, k)
function getTokenV5(address token) external view returns (TokenStateV5 memory state); 

/// @notice Get token state (V6)
/// @param token  The address of the token
/// @return state  The state of the token (V6) with all V5 fields plus taxRate, pool, and progress
function getTokenV6(address token) external view returns (TokenStateV6 memory state);
/// @notice Get token state (V7)
/// @param token  The address of the token
/// @return state  The state of the token (V7) with all V6 fields plus lpFeeProfile
function getTokenV7(address token) external view returns (TokenStateV7 memory state);
/// @notice Get token state (V8)
/// @param token  The address of the token
/// @return state  The state of the token (V8) with asymmetric buyTaxRate and sellTaxRate
function getTokenV8(address token) external view returns (TokenStateV8 memory state);
/// @notice Get token state (V8Safe)
/// @dev Returns enum-typed fields (TokenStatus, TokenVersion, V3LPFeeProfile, DEXId) as uint8
///      instead of their Solidity enum types, preventing ABI-decoding reverts when new enum
///      variants are introduced. Use this method when you need forward/backward compatibility
///      with future contract upgrades that may add new enum values.
/// @param token  The address of the token
/// @return state  The state of the token with enum fields encoded as uint8
function getTokenV8Safe(address token) external view returns (TokenStateV8Safe memory state);


/// @dev Token version
/// Which token implementation is used
enum TokenVersion {
    TOKEN_LEGACY_MINT_NO_PERMIT,
    TOKEN_LEGACY_MINT_NO_PERMIT_DUPLICATE, // for historical reasons, both 0 and 1 are the same: TOKEN_LEGACY_MINT_NO_PERMIT
    TOKEN_V2_PERMIT, // 2
    TOKEN_GOPLUS, // 3
    TOKEN_TAXED, // 4: The original tax token (FlapTaxToken)
    TOKEN_TAXED_V2 // 5: The new advanced tax token (FlapTaxTokenV2)

}

/// @dev the quote token, i.e, the token as the reserve
enum QuoteTokenType {
    NATIVE_GAS_TOKEN, // The native gas token
    ERC20_TOKEN_WITH_PERMIT, //  The ERC20 token with permit
    ERC20_TOKEN_WITHOUT_PERMIT // The ERC20 token without permit

}

/// @notice the status of a token
/// The token has 5 statuses:
//    - Tradable: The token can be traded(buy/sell)
//    - InDuel: (obsolete) The token is in a battle, it can only be bought but not sold.
//    - Killed: (obsolete) The token is killed, it can not be traded anymore. Can only be redeemed for another token.
//    - DEX: The token has been added to the DEX
//    - Staged: The token is staged but not yet created (address is predetermined)
enum TokenStatus {
    Invalid, // The token does not exist
    Tradable,
    InDuel, // obsolete
    Killed, // obsolete
    DEX,
    Staged // The token is staged (address determined, but not yet created)

}


/// @notice the V3 LP fee profile
/// @dev determines the LP fee tier to use when migrating tokens to Uniswap V3 or Pancake V3
enum V3LPFeeProfile {
    LP_FEE_PROFILE_STANDARD, // Standard fee tier:  0.25% on PancakeSwap, 0.3% on Uniswap
    LP_FEE_PROFILE_LOW, // Low fee tier: typically, 0.01% on PancakeSwap, 0.05% on Uniswap
    LP_FEE_PROFILE_HIGH // High fee tier (1% for exotic pairs)

}

/// @notice the DEX ID
/// @dev determines the DEX we want to migrate to
/// On BSC:
///   - only DEX0 will be enabled, which is PancakeSwap
/// On xLayer:
///   - only DEX0 will be enabled, which is PotatoSwap
/// On Monad:
///   - DEX0 is Uniswap
///   - DEX1 is PancakeSwap
///   - DEX2 is Monday
/// On Robinhood Chain:
///   - DEX0 is Uniswap
///   - DEX1 is PancakeSwap
/// Note that, currently, we only support at most 3 DEXes
/// We may add more DEXes in the future if needed
enum DEXId {
    DEX0,
    DEX1,
    DEX2
}
```

## What is the difference between getTokenV2, getTokenV3, getTokenV4, getTokenV5, getTokenV6, getTokenV7, getTokenV8 and getTokenV8Safe?

Each version of the `getToken` method returns progressively more information about a token's state. The protocol maintains backward compatibility by preserving all legacy methods while recommending the latest available version for new integrations.

### Version Comparison Table

| Version    | New Fields Added                                 | Use Case                                         |
| ---------- | ------------------------------------------------ | ------------------------------------------------ |
| **V2**     | Base fields only                                 | Legacy - basic token state                       |
| **V3**     | `quoteTokenAddress`, `nativeToQuoteSwapEnabled`  | Multi-quote token support                        |
| **V4**     | `extensionID`                                    | Extension/plugin support (e.g., Farcaster)       |
| **V5**     | `h`, `k`                                         | Complete bonding curve parameters                |
| **V6**     | `taxRate`, `pool`, `progress`                    | Tax tokens, DEX pool info, progress tracking     |
| **V7**     | `lpFeeProfile`, `dexId`                          | Multi-DEX support with custom fee tiers          |
| **V8**     | `buyTaxRate`, `sellTaxRate` (replaces `taxRate`) | Asymmetric tax rates for Tax Token V3            |
| **V8Safe** | Same as V8, but enum fields returned as `uint8`  | Forward-compatible; safe against new enum values |

### Recommendation

Use `getTokenV8` / `getTokenV8Safe` on BNB mainnet and BNB testnet. For other networks, use `getTokenV7`. `getTokenV8Safe` is recommended when you need forward/backward compatibility with future contract upgrades that may add new enum variants.

`getTokenV5` is supported on all mainnets (BSC, xLayer, Monad, Morph). `V6` is available on all mainnets except Morph, while `V7` is available on all mainnets except Morph and Base.

{% hint style="info" %}
`getTokenV8` and `getTokenV8Safe` are currently only available on BNB mainnet and BNB testnet.
{% endhint %}

We will gradually make `getTokenV8` and `getTokenV8Safe` available on other mainnets in the future.


# Parse Token Meta

Whenever a token is launched, a `TokenCreated` event will be emitted:

```solidity
/// @notice emitted when a new token is created
///
/// @param ts The timestamp of the event
/// @param creator The address of the creator
/// @param nonce The nonce of the token
/// @param token  The address of the token
/// @param name  The name of the token
/// @param symbol  The symbol of the token
/// @param meta The meta URI of the token
event TokenCreated(
  uint256 ts, address creator, uint256 nonce, address token, string name, string symbol, string meta
);
```

The `TokenCreated` event contains the IPFS CID of the token’s metadata, which can be used to retrieve the metadata from IPFS.

Alternatively, you can also get the metadata through the token’s contract by calling the `metaURI` method of the token contract. The `meta` method returns the IPFS CID of the token’s metadata.

```solidity
interface IFlapToken {
    function metaURI() external view returns (string memory);
}
```

Let’s take the JACK token as an example to illustrate how to get the image:

If you call the `metaURI` method of the 0x4122a43daecd13cfb2543ad339470fd87bc11111 [token](https://x.flap.sh/0x4122a43daecd13cfb2543ad339470fd87bc11111), you will get the following cid: `bafkreieepx7lh6zcpqsneo2jdeqmcgxjg7facam34w5ezh4myajjauuc24`.

You can get the meta json from any ipfs gateway. However, it would be fast to use our ipfs gateway. Our gateway is located at <https://flap.mypinata.cloud/> . Such that, you can get the meta json here: `https://flap.mypinata.cloud/ipfs/bafkreieepx7lh6zcpqsneo2jdeqmcgxjg7facam34w5ezh4myajjauuc24`.

The meta json is as follows:

```json
{
    "buy": null,
    "creator": "0x7F403F924dDE32bFd4D4432E3996291aeFCe2f89",
    "description": "OKIE,OKIE",
    "image": "bafkreibwjuzzns6yf4wytfr5nk6vujb4yja4iejff2k76nshn2z5jkmiy4",
    "sell": null,
    "telegram": null,
    "twitter": null,
    "website": null
}
```

Note that the image field is also a cid, which is the cid of the image. Similarly, we can get the image here: `https://flap.mypinata.cloud/ipfs/bafkreibwjuzzns6yf4wytfr5nk6vujb4yja4iejff2k76nshn2z5jkmiy4`.


# Token Version Specification

### Overview

The `TokenVersion` field is an enum that indicates which token implementation is being used for a specific token. This field is crucial for determining the capabilities and features available for each token launched through the Portal contract.

### TokenVersion Enum Values

```solidity
    /// @dev Token version
    /// Which token implementation is used
    enum TokenVersion {
        TOKEN_LEGACY_MINT_NO_PERMIT,
        TOKEN_LEGACY_MINT_NO_PERMIT_DUPLICATE, // for historical reasons, both 0 and 1 are the same: TOKEN_LEGACY_MINT_NO_PERMIT
        TOKEN_V2_PERMIT, // 2
        TOKEN_GOPLUS, // 3
        TOKEN_TAXED, // 4: The original tax token (FlapTaxToken)
        TOKEN_TAXED_V2, // 5: The new advanced tax token (FlapTaxTokenV2)
        TOKEN_TAXED_V3, // 6: The next-generation tax token with asymmetric buy/sell rates (FlapTaxTokenV3)
        TOKEN_V3_PERMIT // 7: Non-tax token using TokenV3 implementation with permit support

    }
```

The `TokenVersion` has the following values:

| Value | Name                                    | Description                                                                                                               |
| ----- | --------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- |
| 0     | `TOKEN_LEGACY_MINT_NO_PERMIT`           | Legacy token implementation without permit functionality                                                                  |
| 1     | `TOKEN_LEGACY_MINT_NO_PERMIT_DUPLICATE` | Historical duplicate (identical to value 0)                                                                               |
| 2     | `TOKEN_V2_PERMIT`                       | V2 token implementation with EIP-2612 permit support                                                                      |
| 3     | `TOKEN_GOPLUS`                          | Token implementation with GoPlus security integration                                                                     |
| 4     | `TOKEN_TAXED`                           | Original tax token implementation (FlapTaxToken)                                                                          |
| 5     | `TOKEN_TAXED_V2`                        | Advanced tax token implementation (FlapTaxTokenV2)                                                                        |
| 6     | `TOKEN_TAXED_V3`                        | Next-generation tax token with asymmetric buy/sell rates (FlapTaxTokenV3)                                                 |
| 7     | `TOKEN_V3_PERMIT`                       | Non-tax TokenV3 implementation with permit support, designed for PancakeSwap Infinity CL or Uniswap v4 style integrations |

#### Version Details

* **TOKEN\_LEGACY\_MINT\_NO\_PERMIT (0 & 1)**: The obsolete token implementation. Due to historical reasons, both values 0 and 1 represent the same implementation type.
* **TOKEN\_V2\_PERMIT (2)**: Enhanced token with permit functionality allowing gasless approvals via signed messages (EIP-2612 standard).
* **TOKEN\_GOPLUS (3)**: Token integrated with GoPlus security features for enhanced safety and verification. (Not Used in current deployments)
* **TOKEN\_TAXED (4)**: The first generation tax token (`FlapTaxToken`) that implements tax mechanisms on transfers.
* **TOKEN\_TAXED\_V2 (5)**: The second generation tax token (`FlapTaxTokenV2`) with advanced tax features and improvements over the original tax token.
* **TOKEN\_TAXED\_V3 (6)**: The third generation tax token (`FlapTaxTokenV3`) with asymmetric buy/sell tax rates, commission receiver support, and bidirectional dynamic liquidation threshold. Launched via `newTokenV6`. This is the recommended token type for all new integrations.
* **TOKEN\_V3\_PERMIT (7)**: A zero-tax token (`taxRate = 0`) using the TokenV3 implementation with permit support. This version is intended for launches that combine with PancakeSwap Infinity CL or Uniswap v4 style liquidity flows, and is created via `newTokenV7`.

### How to Get Token Version

There are two primary methods to retrieve the token version for a specific token:

#### Method 1: Index from Events

The protocol emits a `TokenVersionSet` event whenever a token's version is set or updated. You can listen to this event or query historical events to determine a token's version.

**Event Definition:**

```solidity
event TokenVersionSet(address token, TokenVersion version);
```

**Example Usage:**

* Listen for `TokenVersionSet` events on the Portal contract
* Filter events by the token address
* The `version` parameter contains the `TokenVersion` enum value

#### Method 2: Using getTokenV\* Methods

The Portal contract provides multiple view functions to query token state, each returning progressively more fields. All of these methods include the `tokenVersion` field in their return values.

**Available Methods:**

1. **getTokenV5** - Returns `TokenStateV5` structure

   ```solidity
   function getTokenV5(address token) external view returns (TokenStateV5 memory state);
   ```
2. **getTokenV6** - Returns `TokenStateV6` structure

   ```solidity
   function getTokenV6(address token) external view returns (TokenStateV6 memory state);
   ```
3. **getTokenV7** - Returns `TokenStateV7` structure

   ```solidity
   function getTokenV7(address token) external view returns (TokenStateV7 memory state);
   ```
4. **getTokenV8** - Returns `TokenStateV8` structure with asymmetric `buyTaxRate` and `sellTaxRate` (BNB mainnet & testnet only)

   ```solidity
   function getTokenV8(address token) external view returns (TokenStateV8 memory state);
   ```
5. **getTokenV8Safe** - Returns `TokenStateV8Safe` with enum fields as `uint8` for forward compatibility (BNB mainnet & testnet only)

   ```solidity
   function getTokenV8Safe(address token) external view returns (TokenStateV8Safe memory state);
   ```

**All TokenState structures include:**

* `TokenVersion tokenVersion` - The version of the token implementation

**Example Usage:**

```solidity
// Using getTokenV8 on BNB mainnet/testnet (recommended — includes asymmetric tax rates)
TokenStateV8 memory state = portal.getTokenV8(tokenAddress);
TokenVersion version = state.tokenVersion;
uint256 buyTax = state.buyTaxRate;
uint256 sellTax = state.sellTaxRate;

// Using getTokenV7 on other networks
TokenStateV7 memory state = portal.getTokenV7(tokenAddress);
TokenVersion version = state.tokenVersion;
```


# Inspect A Tax Token

## Overview

You can get the tax info of any tax token using the Tax Token Helper contract. Our website uses this contract to get tax info for tokens and show them to users:

<figure><img src="https://2671086575-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F5KujUBwRoHVjn8OZEgtZ%2Fuploads%2FeCNDbbhq7lDU8jAkw7bm%2Fimage.png?alt=media&amp;token=82717aa1-3278-4d91-af71-31670a9c92d7" alt="" width="563"><figcaption></figcaption></figure>

## Tax Token Helper

| Chain             | Tax Token Helper                           |
| ----------------- | ------------------------------------------ |
| BNB Mainnet       | 0x53841c73217735F37BC1775538b03b23feFD8346 |
| BNB Testnet       | 0xD64441e5FcD02D342B8cf6eBA10Ef6E40d0dA90f |
| Robinhood Mainnet | 0xb10bD2672aE63735d677164A54B573a016f0203C |

```solidity
// SPDX-License-Identifier: MIT
pragma solidity =0.8.24;

/// @title ITaxTokenHelper
/// @notice Interface for the TaxTokenHelper contract that provides utilities for interacting with tax tokens
/// @dev This helper supports both legacy (TOKEN_TAXED) and new (TOKEN_TAXED_V2) tax token versions
interface ITaxTokenHelper {
    /// @notice Structure containing comprehensive tax token configuration and statistics
    /// @dev All BPS values are in basis points (1 BPS = 0.01%)
    struct TaxTokenInfo {
        /// @notice Marketing tax allocation in basis points (0-10000)
        uint16 marketBps;

        /// @notice Deflation (burn) tax allocation in basis points (0-10000)
        uint16 deflationBps;

        /// @notice Liquidity provision tax allocation in basis points (0-10000)
        uint16 lpBps;

        /// @notice Dividend distribution tax allocation in basis points (0-10000)
        uint16 dividendBps;

        /// @notice Total tax rate applied to transfers in basis points (sum of all allocations)
        uint16 taxRate;

        /// @notice Cumulative amount of tokens burnt (sent to black hole and dead addresses)
        uint256 burntTokenAmount;

        /// @notice Cumulative amount of quote tokens distributed to dividend holders
        uint256 totalQuoteSentToDividend;

        /// @notice Cumulative amount of quote tokens added to liquidity pools
        uint256 totalQuoteAddedToLiquidity;

        /// @notice Cumulative amount of tax tokens added to liquidity pools
        uint256 totalTokenAddedToLiquidity;

        /// @notice Cumulative amount of quote tokens sent to marketing wallet
        uint256 totalQuoteSentToMarketing;

        /// @notice Address receiving marketing tax allocations
        address marketingWallet;

        /// @notice Address of the quote token used for tax swaps (address(0) for native token)
        address quoteToken;

        /// @notice Minimum token balance required to receive dividend distributions
        uint256 minimumShareBalance;
    }

    /// @notice Information about the marketing wallet or vault associated with a tax token
    struct MarketingWalletInfo {
        /// @notice The marketing wallet address, which could be a plain EOA, a Flap Vault, or any smart contract
        address addr;

        /// @notice The vault factory address that created this vault.
        ///         Zero if the marketing wallet is not a Flap Vault or the factory is unknown.
        address factory;

        /// @notice Risk level as assessed by Flap auditors.
        ///         UNVERIFIED(0), LOW_RISK(1), LOW_MEDIUM_RISK(2), MEDIUM_RISK(3), HIGH_RISK(4)
        uint8 riskLevel;

        /// @notice Whether the vault is an official Flap-endorsed vault
        bool isOfficialVault;

        /// @notice Whether the marketing wallet is a registered Flap Vault (via VaultPortal)
        bool isVault;

        /// @notice Whether the marketing wallet is an AI consumer.
        ///         True when the vault category is TYPE_AI_ORACLE_POWERED, or when the address
        ///         is a non-EOA contract that has made at least one request via FlapAIProvider.
        bool isAIConsumer;
    }

    /// @notice Extended tax token info with vault and AI oracle metadata.
    ///         Prefer this over TaxTokenInfo for new integrations.
    struct TaxTokenInfoV2 {
        /// @notice Marketing tax allocation in basis points (0-10000)
        uint16 marketBps;

        /// @notice Deflation (burn) tax allocation in basis points (0-10000)
        uint16 deflationBps;

        /// @notice Liquidity provision tax allocation in basis points (0-10000)
        uint16 lpBps;

        /// @notice Dividend distribution tax allocation in basis points (0-10000)
        uint16 dividendBps;

        /// @notice Tax rate applied on buys, in basis points (e.g., 500 = 5%)
        uint16 buyTaxRate;

        /// @notice Tax rate applied on sells, in basis points (e.g., 500 = 5%).
        ///         Currently equal to buyTaxRate as the contract uses a single taxRate.
        uint16 sellTaxRate;

        /// @notice Cumulative amount of tokens burnt (sent to black hole and dead addresses)
        uint256 burntTokenAmount;

        /// @notice Cumulative amount of quote tokens distributed to dividend holders
        uint256 totalQuoteSentToDividend;

        /// @notice Cumulative amount of quote tokens added to liquidity pools
        uint256 totalQuoteAddedToLiquidity;

        /// @notice Cumulative amount of tax tokens added to liquidity pools
        uint256 totalTokenAddedToLiquidity;

        /// @notice Cumulative amount of quote tokens sent to marketing wallet
        uint256 totalQuoteSentToMarketing;

        /// @notice The token distributed as dividends to shareholders.
        ///         address(0) means the native gas token (e.g., BNB or ETH);
        ///         any other address is the ERC20 reward token.
        address dividendToken;

        /// @notice Address of the quote token used for tax swaps (address(0) for native token)
        address quoteToken;

        /// @notice Minimum token balance required to receive dividend distributions
        uint256 minimumShareBalance;

        /// @notice Information about the marketing wallet / vault that receives tax revenue
        MarketingWalletInfo vaultInfo;
    }

    /// @notice Returns the version of the TaxTokenHelper contract
    /// @return Version string in semantic versioning format
    function version() external pure returns (string memory);

    /// @notice Retrieves comprehensive information about a tax token's configuration and statistics
    /// @dev Returns zero/empty values for non-tax tokens (TOKEN_V2_PERMIT)
    /// @dev For legacy TOKEN_TAXED tokens, only marketBps, taxRate, marketingWallet, quoteToken, and totalQuoteSentToMarketing are populated
    /// @dev For TOKEN_TAXED_V2 tokens, all fields are populated based on TaxProcessor state
    /// @param taxToken Address of the tax token to query
    /// @return info TaxTokenInfo struct containing all tax token configuration and statistics
    function getTaxTokenInfo(address taxToken) external view returns (TaxTokenInfo memory info);

    /// @notice Retrieves extended tax token info including vault and AI oracle metadata
    /// @dev Returns zero/empty values for non-tax tokens (TOKEN_V2_PERMIT)
    /// @dev For legacy TOKEN_TAXED tokens, marketBps, buyTaxRate/sellTaxRate, vaultInfo.addr, quoteToken, and totalQuoteSentToMarketing are populated
    /// @dev For TOKEN_TAXED_V2 tokens, all fields including vaultInfo are populated
    /// @param taxToken Address of the tax token to query
    /// @return info TaxTokenInfoV2 struct containing extended tax token configuration, statistics, and vault metadata
    function getTaxTokenInfoV2(address taxToken) external view returns (TaxTokenInfoV2 memory info);

    /// @notice Retrieves dividend information for a specific user and tax token
    /// @dev Returns (0, 0) if the tax token has no dividend contract configured
    /// @param taxToken Address of the tax token
    /// @param user Address of the user to query dividend information for
    /// @return claimed Total amount of dividends already claimed by the user
    /// @return pending Amount of dividends currently available for withdrawal
    function getDividendInfo(address taxToken, address user) external view returns (uint256 claimed, uint256 pending);

    /// @notice Claims accumulated dividends for the caller (msg.sender)
    /// @dev Reverts if the tax token has no dividend contract configured
    /// @dev For WETH dividends, automatically unwraps to native token
    /// @param taxToken Address of the tax token to claim dividends from
    function claimDividend(address taxToken) external;

    /// @notice Claims accumulated dividends for the caller with optional native token unwrapping
    /// @dev Reverts if the tax token has no dividend contract configured
    /// @param taxToken Address of the tax token to claim dividends from
    /// @param unwrapWETH If true, unwraps WETH dividends to native token; if false, sends WETH
    function claimDividend(address taxToken, bool unwrapWETH) external;

    /// @notice Claims accumulated dividends on behalf of a specified user
    /// @dev Reverts if the tax token has no dividend contract configured
    /// @dev Can be called by anyone to trigger dividend withdrawal for any user
    /// @param taxToken Address of the tax token to claim dividends from
    /// @param unwrapWETH If true, unwraps WETH dividends to native token; if false, sends WETH
    /// @param user Address of the user to claim dividends for
    function claimDividendForUser(address taxToken, bool unwrapWETH, address user) external;
}
```

## getTaxTokenInfo vs getTaxTokenInfoV2

The contract exposes two query functions. Use the table below to decide which one to call.

|                           | `getTaxTokenInfo`                 | `getTaxTokenInfoV2`                                  |
| ------------------------- | --------------------------------- | ---------------------------------------------------- |
| **Return type**           | `TaxTokenInfo`                    | `TaxTokenInfoV2`                                     |
| **Tax rate fields**       | Single `taxRate`                  | Separate `buyTaxRate` and `sellTaxRate`              |
| **Marketing address**     | `marketingWallet` (plain address) | `vaultInfo.addr` + full `MarketingWalletInfo`        |
| **Vault metadata**        | None                              | `isVault`, `factory`, `riskLevel`, `isOfficialVault` |
| **AI oracle flag**        | None                              | `isAIConsumer`                                       |
| **Dividend reward token** | Not available                     | `dividendToken` (`address(0)` = native gas token)    |
| **Recommended for**       | Legacy integrations               | All new integrations                                 |

### Using getTaxTokenInfo

`getTaxTokenInfo` is the original query function. Call it when you only need the core tax breakdown and cumulative statistics:

```solidity
ITaxTokenHelper helper = ITaxTokenHelper(TAX_TOKEN_HELPER);
ITaxTokenHelper.TaxTokenInfo memory info = helper.getTaxTokenInfo(taxToken);

// Tax allocation pie (all in BPS, sum = taxRate)
// info.marketBps      — share going to marketing wallet
// info.deflationBps   — share burnt
// info.lpBps          — share added to liquidity
// info.dividendBps    — share distributed to dividend holders

// Tax rate
// info.taxRate        — total tax charged on every transfer (BPS)

// Cumulative statistics
// info.burntTokenAmount          — tokens sent to black hole + dead address
// info.totalQuoteSentToDividend  — quote tokens paid to dividend holders
// info.totalQuoteAddedToLiquidity
// info.totalTokenAddedToLiquidity
// info.totalQuoteSentToMarketing

// Addresses
// info.marketingWallet           — wallet receiving marketing revenue
// info.quoteToken                — address(0) means native gas token (BNB/ETH)

// Dividend gating
// info.minimumShareBalance       — min token balance to receive dividends
```

{% hint style="info" %}
For V1 tax tokens (`TOKEN_TAXED`), only `marketBps` (always `10000`), `taxRate`, `marketingWallet`, `quoteToken`, and `totalQuoteSentToMarketing` are populated. All other fields are zero.
{% endhint %}

### Using getTaxTokenInfoV2

`getTaxTokenInfoV2` is the recommended function for all new integrations. It returns everything `getTaxTokenInfo` returns, plus richer metadata about the marketing wallet and dividend token.

```solidity
ITaxTokenHelper helper = ITaxTokenHelper(TAX_TOKEN_HELPER);
ITaxTokenHelper.TaxTokenInfoV2 memory info = helper.getTaxTokenInfoV2(taxToken);

// Tax allocation — same BPS fields as TaxTokenInfo
// info.marketBps / deflationBps / lpBps / dividendBps

// Separate buy and sell tax rates (replaces the single taxRate field)
// info.buyTaxRate    — tax on buys (BPS)
// info.sellTaxRate   — tax on sells (BPS, currently equal to buyTaxRate)

// Cumulative statistics — same as TaxTokenInfo
// info.burntTokenAmount / totalQuoteSentToDividend / totalQuoteAddedToLiquidity
// info.totalTokenAddedToLiquidity / totalQuoteSentToMarketing

// Dividend reward token (new in V2)
// info.dividendToken — address(0) = native gas token; otherwise the ERC20 reward token
// info.quoteToken
// info.minimumShareBalance

// Marketing wallet / vault metadata (new in V2)
// info.vaultInfo.addr            — the marketing wallet or vault address
// info.vaultInfo.isVault         — true if registered via VaultPortal
// info.vaultInfo.factory         — vault factory that created it (zero if not a vault)
// info.vaultInfo.riskLevel       — 0 UNVERIFIED · 1 LOW · 2 LOW_MEDIUM · 3 MEDIUM · 4 HIGH
// info.vaultInfo.isOfficialVault — true if Flap-endorsed
// info.vaultInfo.isAIConsumer    — true if vault uses Flap AI Oracle
```

#### Reading vault risk level

`riskLevel` maps to the `IVaultPortalTypes.RiskLevel` enum:

| Value | Meaning                             |
| ----- | ----------------------------------- |
| 0     | UNVERIFIED (default for non-vaults) |
| 1     | LOW\_RISK                           |
| 2     | LOW\_MEDIUM\_RISK                   |
| 3     | MEDIUM\_RISK                        |
| 4     | HIGH\_RISK                          |

#### Detecting AI-powered vaults

`isAIConsumer` is `true` when either:

* The vault's category in VaultPortal is `TYPE_AI_ORACLE_POWERED`, **or**
* The marketing wallet is a smart contract (not an EOA or EIP-7702 delegate) that has submitted at least one request through `FlapAIProvider`.

### V1 tax token caveats

For V1 tax tokens (`TOKEN_TAXED`), both functions populate only a subset of fields: `marketBps` (always `10000`, meaning 100% goes to the marketing/funds-recipient wallet), `buyTaxRate`/`sellTaxRate` (or `taxRate`), `vaultInfo.addr`/`marketingWallet`, `quoteToken`, and `totalQuoteSentToMarketing`.

The `totalQuoteSentToMarketing` field may be zero for older V1 tokens. If that is the case, we provide a backend to get the `totalQuoteSentToMarketing` for V1 Tax tokens:

```
https://t3w1p53k7a.execute-api.eu-west-3.amazonaws.com/donation?token=0x36F2FD027F5f27C59B8C6d64dF64bcC8E8C97777 
```

> Note: you only need this fallback when `totalQuoteSentToMarketing` returned by `getTaxTokenInfo` is zero for V1 Tax tokens. For V2 Tax tokens, the `totalQuoteSentToMarketing` is always accurate.

## how to inspect a tax token's vault

When using `getTaxTokenInfoV2`, vault metadata is already included in the `vaultInfo` field of `TaxTokenInfoV2` — no separate VaultPortal call is needed for the basic case.

For deeper, vault-type-specific data, you can still query VaultPortal directly. Use the VaultPortal `tryGetVault` method to get basic vault information; the returned `description` gives a human-readable summary of the vault. For details on the fields returned by `tryGetVault`, refer to [Get Vault Info](/flap/developers/vault-developers/flap-tax-vault#get-vault-info).

If you want vault-type-specific data, compare the `vaultFactory` returned by `tryGetVault` against the known registered vault factories and then query the vault using its own interface. The list of registered vault factories and their interfaces is available at [developers/token-launcher-developers/registered-vaults.md](/flap/developers/token-launcher-developers/registered-vaults).

## How to determine whether a token or a factory has low risk

`riskLevel` (the `IVaultPortalTypes.RiskLevel` enum: `UNVERIFIED(0)`, `LOW_RISK(1)`, `LOW_MEDIUM_RISK(2)`, `MEDIUM_RISK(3)`, `HIGH_RISK(4)`) exists at two places: the **vault factory** and the **individual token**. A token's risk level either comes from its factory (inherited automatically at launch) or from its own independent audit — and only one of those two paths emits an event on the token itself.

{% hint style="danger" %}
**If you only index token-level events, you will miss tokens that are low risk purely because their factory is low risk.** When a token is launched from an already-verified, `LOW_RISK` factory, the token inherits that risk level immediately — but **no event is emitted for the token** at that moment. The only way to know is to also track the factory's risk level and combine it with your own list of "which tokens came from which factory." If your indexer only watches for a token-level audit event, a token that is actually `LOW_RISK` (via its factory) will silently look `UNVERIFIED` in your data.
{% endhint %}

### Indexing which factory created a token

Since inheriting risk from a factory emits no event on the token, you need a separate way to know that a given token *came from* a given factory in the first place — otherwise you have nothing to join the factory's risk level against.

* **The event to watch:** `FlapTaxVaultTokenCreated(address indexed token, address indexed vault, address indexed vaultFactory)`, emitted exactly once when the token is launched through `newTokenV6WithVault` / `newTokenV7WithVault`. All three parameters are indexed, so you can filter by `vaultFactory` to find every token launched from a specific factory, or look up a single token to find its `vaultFactory`.
* Keep a `token → vaultFactory` map from this event. Combine it with the `factory → riskLevel` map from `FlapTaxVaultFactoryRegistered` (below) to derive each token's inherited risk level — `token`'s risk = `riskLevel` of `vaultFactory` at the time of creation, unless a later `AuditReportSubmitted` overrides it for that specific token.

### Getting a factory's risk level

* **Indexing:** listen for `FlapTaxVaultFactoryRegistered(address factory, bool enabled, bool official, RiskLevel riskLevel)`. Every emission carries the factory's full current state, not a delta — just overwrite whatever you had stored for that `factory`.
* **Direct query (no indexing needed):** call `VaultPortal.vaultFactories(factory)`, which returns `(bool enabled, bool official, RiskLevel riskLevel, bytes29 reserved)` — a single always-current on-chain read.

{% hint style="warning" %}
There is a separate `FactoryAuditReportSubmitted` event tied to `submitFactoryAuditReport(...)`. Don't use it to track a factory's effective risk level — it only records a historical audit report and does not necessarily reflect the risk level actually used when new tokens are launched from that factory. `FlapTaxVaultFactoryRegistered` is the one that matters for "what risk level will a new token from this factory get right now."
{% endhint %}

### Getting a token's own risk level

A token's own risk level can be set independently of its factory — for example, upgrading a specific token from `UNVERIFIED` to `LOW_RISK` after it passes its own audit, regardless of what its factory's rating is.

* **The event to watch:** `AuditReportSubmitted(address indexed taxToken, address indexed auditor, RiskLevel riskLevel, string ipfsCid)`. This is the only event that fires when a token's own stored risk level changes — including the "not low risk → low risk" transition. Overwrite your stored value for that `taxToken` every time it fires.
* Remember: this event only fires for **independent, per-token** audits. It does **not** fire when a token simply inherits its risk level from its factory at launch (see the warning above) — those tokens need to be picked up via the factory's `FlapTaxVaultFactoryRegistered` event instead.

### Getting the current risk level without indexing anything

If you don't want to run an indexer and just need the current status for one token right now, there are two equivalent single-call options — both always return the live value, whether it came from factory inheritance or an independent audit:

**Option A — `TaxTokenHelper.getTaxTokenInfoV2`** (recommended; see the [`getTaxTokenInfoV2`](#using-gettaxtokeninfov2) section above):

```solidity
ITaxTokenHelper.TaxTokenInfoV2 memory info = ITaxTokenHelper(TAX_TOKEN_HELPER).getTaxTokenInfoV2(taxToken);
uint8 riskLevel = info.vaultInfo.riskLevel; // 0 UNVERIFIED · 1 LOW · 2 LOW_MEDIUM · 3 MEDIUM · 4 HIGH
```

This one call also gets you the rest of the tax breakdown at the same time.

**Option B — direct `VaultPortal.tryGetVault`**, if you don't need the tax breakdown:

```solidity
(bool found, IVaultPortalTypes.VaultInfo memory info) = IVaultPortal(VAULT_PORTAL).tryGetVault(taxToken);
IVaultPortalTypes.RiskLevel riskLevel = info.riskLevel;
```

### Summary

| Question                                                         | Where to look                                                                                                                                |
| ---------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- |
| What's a factory's current risk level?                           | `FlapTaxVaultFactoryRegistered` event, or `VaultPortal.vaultFactories(factory)`                                                              |
| Which factory did a token launch from?                           | `FlapTaxVaultTokenCreated` event (`token → vaultFactory`)                                                                                    |
| Did a specific token's own risk level just change?               | `AuditReportSubmitted` event                                                                                                                 |
| Did a token become low risk just by inheriting from its factory? | **No event on the token** — join `FlapTaxVaultTokenCreated` (which factory) with `FlapTaxVaultFactoryRegistered` (that factory's risk level) |
| I don't want to index anything, just tell me the current status  | `TaxTokenHelper.getTaxTokenInfoV2(token).vaultInfo.riskLevel` or `VaultPortal.tryGetVault(token)`                                            |


# Trade Tokens

## Get A Quote

{% hint style="warning" %}
If a token is already migrated, you can still get a quote using the following method. However, we will always use the pool that the token has migrated to, this may not be the best quote.\
\
You can also do an off-chain quote to save the RPC calls. Check the end of this document to learn more about the off-chain quote.
{% endhint %}

To get a quote , we call the `quoteExactInput` function:

```solidity
/// @notice Parameters for quoting the output amount for a given input
struct QuoteExactInputParams {
    /// @notice The address of the input token (use address(0) for native asset)
    address inputToken;
    /// @notice The address of the output token (use address(0) for native asset)
    address outputToken;
    /// @notice The amount of input token to swap (in input token decimals)
    uint256 inputAmount;
}

/// @notice Quote the output amount for a given input
/// @param params The quote parameters
/// @return outputAmount The quoted output amount
/// @dev refer to the swapExactInput method for the scenarios
function quoteExactInput(QuoteExactInputParams calldata params) external returns (uint256 outputAmount);
```

Note: the `quoteExactInput` method is not a view function, but we don’t need to send a transaction to get the quote (an `eth_call` or the simulation in viem will do the work) .

Here are the possible scenarios:

* **When the quote token is the native gas token (BNB or ETH):**
  * buy a token:
    * `inputToken` is zero address, representing the gas token
    * `outputToken` is the token you wanna buy
  * sell a token:
    * `inputToken` is the token you wanna sell
    * `outputToken` is zero address, representing the gas token
* **When the quote token is not the native gas token (taking USD1 as an example)** :
  * buy a token with USD1:
    * `inputToken` is USD1 address
    * `outputToken` is the token you wanna buy
  * **buy with native gas token** (only when the token’s `nativeToQuoteSwap` is enabled , check below to inspect if a token’s quote token supports `nativeToQuoteSwap` ):
    * `inputToken` is zero address, representing the gas token
    * `outputToken` is the token you wanna buy
    * under the hood, we will help you swap BNB for for USD1 on PancakeSwap as an intermediate step in the contract.
  * sell a token:
    * `inputToken` is the token you wanna sell
    * `outputToken` is USD1
  * Sell directly from token to BNB is also supported, but we will not support this in our UI.

## Swap

To swap we call the `swapExactInput` method:

```solidity
/// @notice Parameters for swapping exact input amount for output token
struct ExactInputParams {
    /// @notice The address of the input token (use address(0) for native asset)
    address inputToken;
    /// @notice The address of the output token (use address(0) for native asset)
    address outputToken;
    /// @notice The amount of input token to swap (in input token decimals)
    uint256 inputAmount;
    /// @notice The minimum amount of output token to receive
    uint256 minOutputAmount;
    /// @notice Optional permit data for the input token (can be empty)
    bytes permitData;
}

/// @notice Swap exact input amount for output token
/// @param params The swap parameters
/// @return outputAmount The amount of output token received
/// @dev Here are some possible scenarios:
///   If the token's reserve is BNB or ETH (i.e: the quote token is the native gas token):
///      - BUY: input token is address(0), output token is the token address
///      - SELL: input token is the token address, output token is address(0)
///   If the token's reserve is another ERC20 token (eg. USD*, i.e, the quote token is an ERC20 token):
///      - BUY with USD*: input token is the USD* address, output token is the token address
///      - SELL for USD*: input token is the token address, output token is the USD* address
///      - BUY with BNB or ETH: input token is address(0), output token is the token address.
///        (Note: this requires an internal swap to convert BNB/ETH to USD*, nativeToQuoteSwap must be anabled for this quote token)
/// Note: Currently, this method only supports trading tokens that are still in the bonding curve state.
///       However, in the future, we may also support trading tokens that are already in DEX state.
function swapExactInput(ExactInputParams calldata params) external payable returns (uint256 outputAmount);
```

This is quite straightforward after getting a quote.

#### How to construct the permitData in ExactInputParams ?

When selling the tokens, you can use our permitData field to save an approve transaction. The permitData field is the abi encoding of each field from the Permit struct plus the EIP712 signature. Check the `genPermitData` below :

```typescript

import { Account, Address, createWalletClient, encodeAbiParameters, encodeDeployData, encodeFunctionData, formatEther, getContract, getContractAddress, HDAccount, Hex, hexToBigInt, http, keccak256, parseEther, parseGwei, parseSignature, parseTransaction, parseUnits, PublicClient, serializeTransaction, toBytes, toHex } from 'viem';

// generate ERC20 permit data for token (returns all fields, not abi encoded)
async function genPermitDataNoPack(account: Account, token: `0x${string}`, spender: `0x${string}`, amount: bigint, client: PublicClient) {
  const tokenInst = getContract({
    abi: [
      {
        type: "function",
        name: "name",
        inputs: [],
        outputs: [{ name: "", type: "string" }],
        stateMutability: "view"
      },
      {
        type: "function",
        name: "nonces",
        inputs: [{ name: "", type: "address" }],
        outputs: [{ name: "", type: "uint256" }],
        stateMutability: "view"
      },
      {
        type: "function",
        name: "permit",
        inputs: [
          { name: "owner", type: "address" },
          { name: "spender", type: "address" },
          { name: "value", type: "uint256" },
          { name: "deadline", type: "uint256" },
          { name: "v", type: "uint8" },
          { name: "r", type: "bytes32" },
          { name: "s", type: "bytes32" }
        ],
        outputs: [],
        stateMutability: "nonpayable"
      }
    ],
    address: token,
    client,
  });

  const nonce = await tokenInst.read.nonces([account.address]) as bigint;
  const name = await tokenInst.read.name() as string;
  const deadline = BigInt(Date.now() + 10 * 60 * 1000); // 10 minutes ttl

  const sig = await (account as HDAccount).signTypedData({
    domain: {
      name,
      version: "1",
      chainId: bsc.id, // or bsc.id for mainnet
      verifyingContract: token as `0x${string}`
    },
    types: {
      Permit: [
        { name: "owner", type: "address" },
        { name: "spender", type: "address" },
        { name: "value", type: "uint256" },
        { name: "nonce", type: "uint256" },
        { name: "deadline", type: "uint256" }
      ]
    },
    primaryType: "Permit",
    message: {
      owner: account.address,
      spender: spender,
      value: amount,
      nonce,
      deadline,
    }
  });

  const { r, s, v } = parseSignature(sig) as { r: `0x${string}`, s: `0x${string}`, v: bigint, yParity: number };

  return {
    owner: account.address,
    spender,
    value: amount,
    nonce,
    deadline,
    v: Number(v),
    r,
    s
  };
}

// generate ERC20 permit data for token (returns abi encoded data)
async function genPermitData(account: Account, token: `0x${string}`, amount: bigint, client: PublicClient) {
  const permitFields = await genPermitDataNoPack(account, token, flapConfig.portal, amount, client);
  const data = encodeAbiParameters([
            { name: "owner", type: "address" },
            { name: "spender", type: "address" },
            { name: "value", type: "uint256" },
            { name: "deadline", type: "uint256" },
            { name: "v", type: "uint8" },
            { name: "r", type: "bytes32" },
            { name: "s", type: "bytes32" }
          ],
          [permitFields.owner, permitFields.spender, permitFields.value, permitFields.deadline, permitFields.v, permitFields.r, permitFields.s]);
  return data;
}
```

## Events

### `TokenBought`

Emitted when a user buys tokens through the Portal.

```solidity
event TokenBought(
    uint256 ts,
    address token,
    address buyer,
    uint256 amount,
    uint256 eth,
    uint256 fee,
    uint256 postPrice
);
```

**Parameters:**

* `ts`: Timestamp of the trade.
* `token`: Address of the token bought.
* `buyer`: Address of the buyer.
* `amount`: Amount of tokens bought.
* `eth`: Amount of ETH (or quote token) spent.
* `fee`: Amount of ETH (or quote token) spent as a fee.
* `postPrice`: Price of the token after this trade.

**When emitted:**\
Whenever a user successfully buys tokens via the Portal.

### `TokenSold`

Emitted when a user sells tokens through the Portal.

```solidity
event TokenSold(
    uint256 ts,
    address token,
    address seller,
    uint256 amount,
    uint256 eth,
    uint256 fee,
    uint256 postPrice
);
```

**Parameters:**

* `ts`: Timestamp of the trade.
* `token`: Address of the token sold.
* `seller`: Address of the seller.
* `amount`: Amount of tokens sold.
* `eth`: Amount of ETH (or quote token) received.
* `fee`: Amount of ETH (or quote token) deducted as a fee.
* `postPrice`: Price of the token after this trade.

**When emitted:**\
Whenever a user successfully sells tokens via the Portal.

### `LaunchedToDEX`

Emitted when a token finishes its bonding-curve phase and is launched to the target DEX.

```solidity
event LaunchedToDEX(address token, address pool, uint256 amount, uint256 eth);
```

**Parameters:**

* `token`: Address of the token being migrated.
* `pool`: Compatibility field for the DEX target.
* `amount`: Amount of token liquidity added.
* `eth`: Amount of quote-token liquidity added.

{% hint style="info" %}
For classic `V2_MIGRATOR` and `V3_MIGRATOR` launches, `pool` is the actual pool address.

For `PCS_INFINITY_CL_MIGRATOR`, `pool` is the PancakeSwap Infinity vault address.

For `V4_UNI_MIGRATOR`, `pool` is the Uniswap PoolManager address.

This is kept mainly for compatibility with older indexers. For these two CL-style migrators, the actual pool identifier should be read from `FlapTokenCLPoolCreated`.
{% endhint %}

### `FlapTokenCLPoolCreated`

This event is only relevant for the concentrated-liquidity launch paths: **PancakeSwap Infinity CL** and **Uniswap v4**.

```solidity
/// @notice emitted when a new CL pool is created for a token
/// @param token The address of the token
/// @param poolId The ID of the created CL pool
/// @param sqrtPriceX96 The initial sqrt price used to initialize the CL pool
event FlapTokenCLPoolCreated(address token, bytes32 poolId, uint160 sqrtPriceX96);
```

**Parameters:**

* `token`: Address of the launched token.
* `poolId`: The CL pool ID. For `PCS_INFINITY_CL_MIGRATOR` and `V4_UNI_MIGRATOR`, this is the value you should index and store as the real pool identifier.
* `sqrtPriceX96`: The initial sqrt price used to initialize the CL pool.

**When emitted:**\
Emitted when the token is launched through `PCS_INFINITY_CL_MIGRATOR` or `V4_UNI_MIGRATOR` and the CL pool is initialized.

### FlapTokenProgressChanged

{% hint style="warning" %}
This event is available since v4.12.1
{% endhint %}

Whenever a token's progress changes, the `FlapTokenProgressChanged` event will be emitted. Note that the `newProgress` is in `wad` (i.e, with 18 decimals, 1 ether = 100%).

```solidity
/// @notice emitted when the progress of a token changes
/// @param token The address of the token
/// @param newProgress The new progress value in Wad
event FlapTokenProgressChanged(address token, uint256 newProgress);
```

## How To Do An Off-Chain Quote?

### \[Off-Chain Quote] 1. The Preliminary

The preliminary steps to quote off-chain is to get the info of a token first. You can either use the methods provided by our smart contract (i.e: [getTokenV6 or getTokenV7](https://github.com/flap-sh/gitbook/tree/main/developers/wallet-and-terminal-and-bot-developers/inspect-a-token/README.md) ), or you can build your indexer by indexing the following events:

* `TokenCreated`: This event gives us the basic info of the token
* `TokenCurveSet` and `TokenCurveSetV2`: We use these events to determine our bonding curve parameters.
* `TokenDexSupplyThreshSet`: this event gives us the circulating supply threshold for the token be migrated.
* `FlapTokenCirculatingSupplyChanged`: This event gives us the current circulating supply of the token.
* `LaunchedToDEX` : This event indicates that the token has been launched to a decentralized exchange (DEX), and the bonding curve equation ceases to be valid. For `PCS_INFINITY_CL_MIGRATOR` and `V4_UNI_MIGRATOR`, note that the `pool` field is a compatibility address (Infinity vault / PoolManager) rather than the final CL pool ID.
* `FlapTokenTaxSet` : (optional), if a token does not emit this event, the tax is 0 or it is not a tax token.
* `FlapTokenProgressChanged` : emitted when the bonding curve progress of a token changes
* `FlapTokenCLPoolCreated` : For `PCS_INFINITY_CL_MIGRATOR` and `V4_UNI_MIGRATOR`, use this event's `poolId` as the real CL pool identifier.

### \[Off-Chain Quote] 2. The Curve Library

With the bonding curve parameters (either a single `r` from `TokenCurveSet` or the full set (`r`, `h`, `k`) from `TokenCurveSetV2`), you can construct a curve instance. Here are the reference typescript and solidity code. You can always feed the code to your AI (Claude or others), and ask them to help you to convert to your preferred programming language:

* The legacy curve is a special case of the new curve where h = 0. For constructing a legacy curve, we can pass only `r` as the parameter.
* For latest curve, we should pass all the parameters (`r`, `h`, `k`).

```tsx

import { Decimal } from "decimal.js";

const BILLION: Decimal = new Decimal("1000000000");

// The latest curve is CDPV2
export class CDPV2 {
  // the initial virtual reserve
  private r: number;
  private h: number;
  private k: number;

  static defaultDexSupplyThreshold() {
    return new Decimal(8e8);
  }

  static getCurve(r: number, h?: number, k?: number): CDPV2 {
    if (h == null) {
      return new CDPV2(r, 0, 1e9 * r);
    }
    return new CDPV2(r, h, k);
  }

  constructor(r: number, h: number = 0, k: number = 0) {
    this.r = r;
    this.h = h;
    this.k = k;
  }

  estimateSupply(reserve: string): Decimal {
    // s = 1e9 + h - k/(r + eth)
    if (!reserve) return new Decimal(0);
    return new Decimal(BILLION).add(this.h).sub(
            new Decimal(this.k).div(new Decimal(reserve).add(this.r))
    );
  }

  estimateReserve(amount: string): Decimal {
    // eth = k/(h + 1e9 - s) - r
    if (!amount) return new Decimal(0);
    return new Decimal(this.k)
            .div(new Decimal(BILLION).add(this.h).sub(new Decimal(amount)))
            .sub(this.r);
  }

  mc(reserve: string): Decimal {
    return this.fdv(this.totalSupply(reserve).toString());
  }

  price(supply: string): Decimal {
    // Price: k/(h + 1e9 - s)^2
    const denominator = new Decimal(BILLION).add(this.h).sub(new Decimal(supply || 0));
    return new Decimal(this.k).div(denominator.pow(2));
  }

  fdv(supply: string): Decimal {
    return this.price(supply).mul(new Decimal(BILLION));
  }
}
```

The solidity version of the curve lib:

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

pragma solidity ^0.8.13;

import {FixedPointMathLib} from "solady/utils/FixedPointMathLib.sol";

/// @title constant product bonding curve
/// @author The Flap Team
/// @dev v2
///
/// Spec:
///   - max supply: 1 Billion tokens
///   - The constant product equation is :
///         (1e9 + h  - s) * (eth + r) = k
///      - s is the current circulating supply of the token
///      - eth is the current eth reserve
///      - special case: When h = 0, k = r * 1e9
///   - estimateSupply(estimate s given eth): s = 1e9 + h - k/(r + eth)
///   - estimateReserve(estimate eth given s): eth =  k/(h + 1e9 - s) - r
///   - price estimation:  k/(h + 1e9 - s)**2
library LibCurve {
  /// @notice The curve type is represented by a struct
  /// refer to Uniswap V3 white paper.
  struct Curve {
    uint256 r; // Virtual ETH reserve
    uint256 h; // Virtual token reserve
    uint256 k; // The square of the virtual Liquidity
  }

  // The total supply of the token
  uint256 public constant TOTAL_SUPPLY = 1_000_000_000 ether;

  // custom error type

  /// @notice error if the new supply is greater than the total supply
  error SupplyExceedsTotalSupply(uint256 newSupply);

  /// @notice error if reserve is greater than the max reserve
  error ReserveExceedsMaxReserve(uint256 reserve);

  // @notice Return the estimate supply given the reserve amount
  /// @param reserve  The reserve amount
  /// @dev The resulting supply is rounded down and may even subtract small amount
  ///
  ///      This function is used when a user wants to buy tokens,
  ///      a rounded down value is more favorable to the protocol.
  function estimateSupply(Curve memory curve, uint256 reserve) internal pure returns (uint256 supply) {
    // s = 1e9 + h - k/(r + eth)
    // Round down for protocol safety when buying
    supply = TOTAL_SUPPLY + curve.h - FixedPointMathLib.divWadUp(curve.k, curve.r + reserve);
  }

  /// @notice estimate the reserve given the supply
  /// @dev This function returns a roundup value, because we want the following invariant to hold:
  ///         currReserve >= estimateReserve_without_roudup(currSupply)
  ///
  ///      This function is used when a user wants to sell tokens, a rounded up value
  ///      is more favorable to the protocol.
  function estimateReserve(Curve memory curve, uint256 supply) internal pure returns (uint256 reserve) {
    if (supply > TOTAL_SUPPLY) {
      revert SupplyExceedsTotalSupply(supply);
    }

    // eth = k/(h + 1e9 - s) - r
    // Round up for protocol safety when selling
    reserve = FixedPointMathLib.divWadUp(curve.k, TOTAL_SUPPLY + curve.h - supply) - curve.r;
  }

  /// @notice price (wei) of a token (1e18) if you buy/sell inifinitesimal amount at current supply
  function price(Curve memory curve, uint256 supply) internal pure returns (uint256) {
    // Price: k/(h + 1e9 - s)^2
    uint256 denominator = TOTAL_SUPPLY + curve.h - supply;

    if (denominator < 1e9 + 1) {
      return type(uint256).max;
    }

    // Calculate (h + 1e9 - s)^2 using mulWad for precision
    uint256 denominator_squared = FixedPointMathLib.mulWad(denominator, denominator);
    return FixedPointMathLib.divWad(curve.k, denominator_squared);
  }

  // helper from r to curve
  function fromR(uint256 r) internal pure returns (Curve memory) {
    // legacy curve: h = 0 and k = r * TOTAL_SUPPLY
    return Curve({r: r, h: 0, k: FixedPointMathLib.mulWad(r, TOTAL_SUPPLY)});
  }

  // helper from r,h,k to curve
  function fromRHK(uint256 r, uint256 h, uint256 k) internal pure returns (Curve memory) {
    return Curve({r: r, h: h, k: k});
  }

  /// @notice Return the estimate supply given the reserve amount with support for different reserve token decimals
  /// @param curve The curve parameters
  /// @param reserve The reserve amount
  /// @param reserveDecimals The number of decimals of the reserve token (must be <= 18)
  /// @dev The resulting supply is rounded down, same as estimateSupply
  ///      This function is used when a user wants to buy tokens,
  ///      a rounded down value is more favorable to the protocol.
  function estimateSupplyV2(Curve memory curve, uint256 reserve, uint8 reserveDecimals)
  internal
  pure
  returns (uint256 supply)
  {
    if (reserveDecimals > 18) {
      revert("Reserve decimals must be <= 18");
    }

    if (reserveDecimals == 18) {
      // If reserve token has 18 decimals, use the original estimateSupply function
      return estimateSupply(curve, reserve);
    }

    // Adjust the reserve to 18 decimals
    uint256 scaleFactor = 10 ** (18 - reserveDecimals);
    uint256 scaledReserve = reserve * scaleFactor;

    // Use the adjusted reserve amount
    supply = TOTAL_SUPPLY + curve.h - FixedPointMathLib.divWadUp(curve.k, curve.r + scaledReserve);
  }

  /// @notice Estimate the reserve given the supply with support for different reserve token decimals
  /// @param curve The curve parameters
  /// @param supply The token supply
  /// @param reserveDecimals The number of decimals of the reserve token (must be <= 18)
  /// @dev This function returns a roundup value, same as estimateReserve
  ///      This function is used when a user wants to sell tokens, a rounded up value
  ///      is more favorable to the protocol.
  function estimateReserveV2(Curve memory curve, uint256 supply, uint8 reserveDecimals)
  internal
  pure
  returns (uint256 reserve)
  {
    if (reserveDecimals > 18) {
      revert("Reserve decimals must be <= 18");
    }

    if (supply > TOTAL_SUPPLY) {
      revert SupplyExceedsTotalSupply(supply);
    }

    if (reserveDecimals == 18) {
      // If reserve token has 18 decimals, use the original estimateReserve function
      return estimateReserve(curve, supply);
    }

    // Calculate the reserve in 18 decimals
    uint256 scaledReserve = FixedPointMathLib.divWadUp(curve.k, TOTAL_SUPPLY + curve.h - supply) - curve.r;

    // Convert the reserve from 18 decimals to the actual reserve decimals
    uint256 scaleFactor = 10 ** (18 - reserveDecimals);

    // Divide scaled reserve by scaleFactor (rounding up)
    // For rounding up division: (a + b - 1) / b
    reserve = (scaledReserve + scaleFactor - 1) / scaleFactor;
  }
}
```

Note that the curve has the following methods:

* `estimateSupply(reserve: string)`: Estimates the token circulating supply given the reserve amount.
* `estimateReserve(amount: string)`: Estimates the reserve amount given the token’s circulating supply.

### \[Off-Chain Quote] 3. Quote With The Curve Instance

We will use the typescript pseudo code to demonstrate how to quote with the curve instance.

**Case1: Buy 1 BNB Value of Token**

```tsx

//// Values From your Indexer or getTokenV6/V7

// current circulating supply 
// This can be obtained from getTokenV6 or the FlapTokenCirculatingSupplyChanged event.
let curr_circulating_supply;
// The circulating supply threshold for the token to be migrated to DEX
// Typicially, this is 800M 
// This can be obtained from getTokenV6 or the TokenDexSupplyThreshSet event. 
let dex_supply_threshold = 800_000_000;

// The input bnb amount 
let input_bnb = 1;

//// Compute the Quote 

// We cannot buy more than the dex_supply_threshold on the bonding curve 
// The max_reserve is the cap of the reserve 
let max_reserve = curve.estimateReserve(dex_supply_threshold);
// We estimate the current reserve from the circulating supply  
let curr_reserve = curve.estimateReserve(curr_circulating_supply);

// protocol fee on BNB chain is 1% 
let fee = 0.01; // 1% fee 

// Charge a 1% fee on the input bnb. 
let input_after_fee = input_bnb * (0.99);

// If the user pays more than required, we will automatically refund the user 
let new_reserve = curr_reserve + min(input_after_fee, max_reserve - curr_reserve);
// estimate new circulating supply from new reserve 
let new_circulating_supply = curve.estimateSupply(new_reserve);

// The diff of circulating supply is the output token amount 
let output_amt = new_circulating_supply - curr_circulating_supply; 
```

**Case 2: Sell 1M token for bnb**

```tsx

//// From Indexer Or getTokenV6/V7 

// current circulating supply 
let curr_circulating_supply;
// The circulating supply threshold for the token to be migrated to DEX
// Typicially, this is 800M 
// This can be obtained from getTokenV6/V7 or the TokenDexSupplyThreshSet event.
let dex_supply_threshold;

// BNB fee 
let fee = 0.01; // 1% fee

// The input token amount
let input_token_amt = 1_000_000;

// estimate the current reserve 
let curr_reserve = curve.estimateReserve(curr_circulating_supply);

// estimate the new resereve 
let new_reserve = curve.estimateReserve(curr_circulating_supply - input_token_amt);

// The diff is the output amount without deducting the fee 
let output_eth_before_fee =  curr_reserve - new_reserve;


// we take 1% fee from the quote token 
let output_eth = output_eth_before_fee * (0.99);
```

### \[Off-Chain Quote] 4. Quote for tax tokens

If a token has tax, we also charge the tax on the bonding curve. For off-chain quotes, this means you should **add the tax rate to the protocol fee**.

* Effective fee = protocol fee (1%) + token tax rate
* Tax rate can be read from `getTokenV6` / `getTokenV7`, or indexed from the `FlapTokenTaxSet` event

Below are the same examples as above, updated for tax tokens. You can also read the dedicated tax guide at [developers/basic-and-mechanism/flap-tax-token/prebond-tax.md](/flap/developers/basic-and-mechanism/flap-tax-token/prebond-tax).

**Case 1: Buy 1 BNB value of token (tax token)**

```tsx
//// Values From your Indexer or getTokenV6/V7

// current circulating supply 
// This can be obtained from getTokenV6 or the FlapTokenCirculatingSupplyChanged event.
let curr_circulating_supply;
// The circulating supply threshold for the token to be migrated to DEX
// Typicially, this is 800M 
// This can be obtained from getTokenV6 or the TokenDexSupplyThreshSet event. 
let dex_supply_threshold = 800_000_000;
// The token's tax rate
// This can be obtained from getTokenV6 or the FlapTokenTaxSet event from the Portal
let tax = 0;

// The input bnb amount 
let input_bnb = 1;

//// Compute the Quote 

// We cannot buy more than the dex_supply_threshold on the bonding curve 
// The max_reserve is the cap of the reserve 
let max_reserve = curve.estimateReserve(dex_supply_threshold);
// We estimate the current reserve from the circulating supply  
let curr_reserve = curve.estimateReserve(curr_circulating_supply);

// Effective fee = 1% protocol fee + tax
let fee = 0.01 + tax;

// Charge the fee on the input bnb. 
let input_after_fee = input_bnb * (1 - fee);

// If the user pays more than required, we will automatically refund the user 
let new_reserve = curr_reserve + min(input_after_fee, max_reserve - curr_reserve);
// estimate new circulating supply from new reserve 
let new_circulating_supply = curve.estimateSupply(new_reserve);

// The diff of circulating supply is the output token amount 
let output_amt = new_circulating_supply - curr_circulating_supply; 
```

**Case 2: Sell 1M token for BNB (tax token)**

```tsx
//// From Indexer Or getTokenV6/V7 

// current circulating supply 
let curr_circulating_supply;
// The circulating supply threshold for the token to be migrated to DEX
// Typicially, this is 800M 
// This can be obtained from getTokenV6/V7 or the TokenDexSupplyThreshSet event.
let dex_supply_threshold;
// The token's tax rate 
// This can be obtained from getTokenV6 or the FlapTokenTaxSet event from the Portal 
let tax = 0.03;

// BNB fee 
// Effective fee: 1% protocol + tax
let fee = 0.01 + tax;

// The input token amount
let input_token_amt = 1_000_000;

// estimate the current reserve 
let curr_reserve = curve.estimateReserve(curr_circulating_supply);

// estimate the new reserve 
let new_reserve = curve.estimateReserve(curr_circulating_supply - input_token_amt);

// The diff is the output amount without deducting the fee 
let output_eth_before_fee =  curr_reserve - new_reserve;

// apply the fee on the quote token 
let output_eth = output_eth_before_fee * (1 - fee);
```


# Token Migration

When a token's circulating supply reaches its `dexSupplyThresh`, it graduates off the bonding curve and its liquidity is migrated to a DEX. The event `LaunchedToDEX` is emitted to indicate that the token has graduated.

```solidity
/// @notice emitted when adding liquidity to DEX
/// @param token The address of the token
/// @param pool The address of the pool
/// @param amount The amount of token added
/// @param eth The amount of quote Token added
event LaunchedToDEX(address token, address pool, uint256 amount, uint256 eth);
```

`LaunchedToDEX` tells you *that* the token graduated and gives you liquidity amounts, but it does not tell you *which DEX* it graduated to — and for two of the four migrator types, `pool` is not even the final pool address. See the two caveats below.

## Knowing which DEX a token will graduate to, ahead of time

You don't need to wait for `LaunchedToDEX` to know the destination. The destination is decided at launch time and is fully determined by two events emitted in the token-creation transaction:

* `TokenDexPreferenceSet.dexId` — which DEX protocol (chain-specific mapping).
* `TokenMigratorSet.migratorType` — which pool version/shape on that DEX (V2, V3, or one of the CL-style migrators).

See [Index Token Created Events § TokenDexPreferenceSet](/flap/developers/wallet-and-terminal-and-bot-developers/index-token-created-events#tokendexpreferenceset) for the full `dexId`-to-DEX table per chain and the `dexId` + `migratorType` → destination mapping, with a worked example for Robinhood Chain.

## Caveat: `pool` is not always the real pool address

`LaunchedToDEX.pool` behaves differently depending on `migratorType`:

| `migratorType`             | `pool` field                                                 |
| -------------------------- | ------------------------------------------------------------ |
| `V2_MIGRATOR`              | Actual pool (pair) address                                   |
| `V3_MIGRATOR`              | Actual pool address                                          |
| `V4_UNI_MIGRATOR`          | Uniswap `PoolManager` address — **not** the pool itself      |
| `PCS_INFINITY_CL_MIGRATOR` | PancakeSwap Infinity vault address — **not** the pool itself |

For the two CL-style migrators (`V4_UNI_MIGRATOR`, `PCS_INFINITY_CL_MIGRATOR`), read `FlapTokenCLPoolCreated.poolId` instead — that's the real pool identifier to index:

```solidity
/// @notice emitted when a new CL pool is created for a token
/// @param token The address of the token
/// @param poolId The ID of the created CL pool
/// @param sqrtPriceX96 The initial sqrt price used to initialize the CL pool
event FlapTokenCLPoolCreated(address token, bytes32 poolId, uint160 sqrtPriceX96);
```

See [Trade Tokens § LaunchedToDEX](/flap/developers/wallet-and-terminal-and-bot-developers/trade-tokens#launchedtodex) for more detail on these events.


# Token Launcher Developers


# Quick start for token launcher developers

This page outlines the minimal steps to integrate with Flap for token launcher developers. Each step links to the detailed documentation.

1. **Find the `Portal` entrypoint.** Use the deployed addresses listed in [Deployed Contract Addresses](/flap/developers/token-launcher-developers/deployed-contract-addresses).
2. **Launch a token through `Portal`.** `Portal` is the core protocol for launching standard tokens and tax tokens. See [Portal vs VaultPortal](/flap/developers/basic-and-mechanism/portal-vs-vaultportal) for the conceptual difference, then follow [Launch token through Portal](/flap/developers/token-launcher-developers/launch-token-through-portal) for the implementation details.
3. **Launch a token through `VaultPortal` (tax tokens with vaults).** `VaultPortal` builds on top of `Portal` to add Vault functionality for tax tokens. Follow [Launch token through VaultPortal](/flap/developers/token-launcher-developers/launch-token-through-vaultportal). Each vault factory defines its own vault data schema—see [Registered vaults](/flap/developers/token-launcher-developers/registered-vaults) before encoding `vaultData`.


# Deployed Contract Addresses

See [Deployed Contract Addresses](/flap/developers/deployed-contract-addresses).


# Launch token through Portal

This page explains how to launch a token directly through `Portal`. The current launch entry points are:

* `newTokenV6` for `TOKEN_V2_PERMIT` and all currently supported tax-token versions (`TOKEN_TAXED`, `TOKEN_TAXED_V2`, `TOKEN_TAXED_V3`)
* `newTokenV7` for `TOKEN_V3_PERMIT`, the TokenV3-based non-tax flow used for CL-style migration

Legacy methods (`newTokenV2`, `newTokenV3`, `newTokenV4`, `newTokenV5`) remain available for backward compatibility.

{% hint style="info" %}
`TOKEN_TAXED_V3` (`FlapTaxTokenV3`) is the recommended token type for all new tax token launches. It supports asymmetric buy/sell tax rates, commission receivers, and bidirectional dynamic liquidation thresholds. Use `newTokenV6` with `tokenVersion = TOKEN_TAXED_V3` for new integrations.
{% endhint %}

{% hint style="info" %}
`TOKEN_V3_PERMIT` is launched through `newTokenV7`, not `newTokenV6`. Use it when you want the TokenV3 non-tax path together with CL-style migration.
{% endhint %}

## 1) Prepare token metadata

Token metadata is stored on IPFS. You can upload your image and metadata JSON through the Flap upload API.

Example (TypeScript):

```typescript
async function uploadTokenMeta(cfg: {
    buy: string | null;
    creator: string;
    description: string;
    sell: string | null;
    telegram: string | null;
    twitter: string | null;
    website: string | null;
    image_path: string;
}) {
    const form = new FormData();

    const MUTATION_CREATE = `
    mutation Create($file: Upload!, $meta: MetadataInput!) {
      create(file: $file, meta: $meta)
    }
    `;

    form.append(
        "operations",
        JSON.stringify({
            query: MUTATION_CREATE,
            variables: {
                file: null, meta: {
                    website: cfg.website,
                    twitter: cfg.twitter,
                    telegram: cfg.telegram,
                    description: cfg.description,
                    creator: "0x0000000000000000000000000000000000000000",
                }
            },
        })
    );

    form.append(
        "map",
        JSON.stringify({
            "0": ["variables.file"],
        })
    );

    const file = new File([fs.readFileSync(cfg.image_path)], "image.png", {
        type: "image/png",
    });
    form.append("0", file);

    const res = await axios.postForm(FlapConfig.api, form, {
        headers: {
            "Content-Type": "multipart/form-data",
        },
    });

    if (res.status !== 200) {
        throw new Error(`failed to upload the token meta: ${res.statusText}`);
    }

    const cid = res.data.data.create;
    return cid;
}
```

{% hint style="warning" %}
You must upload the image to IPFS and pin the metadata using our API: <https://funcs.flap.sh/api/upload>.

Our indexer will not be able to fetch your file if it is not pinned on our gateway. Besides, many terminals fetch files through our gateway and we will warm the CDN on upload to improve loading speed.

If your file or directory is already available on Public IPFS and you only need the Flap Pinata account to retain the existing CID, use [Pin Existing IPFS Content](/flap/developers/pin-existing-ipfs-content) instead of uploading the same content again.
{% endhint %}

## 2) Call `newTokenV6`

`newTokenV6` is the recommended unified entry point for all token types. The token implementation is selected via the `tokenVersion` field.

```solidity
/// @notice Create a new token (V6) — unified entry point for all token versions.
/// @param params The parameters for the new token (see NewTokenV6Params).
/// @return token The address of the created token.
function newTokenV6(NewTokenV6Params calldata params) external payable returns (address token);
```

`NewTokenV6Params` covers all token types via `tokenVersion`:

```solidity
struct NewTokenV6Params {
    string name;
    string symbol;
    string meta;
    DexThreshType dexThresh;
    bytes32 salt;
    MigratorType migratorType;
    address quoteToken;
    uint256 quoteAmt;
    address beneficiary;
    bytes permitData;
    bytes32 extensionID;
    bytes extensionData;
    DEXId dexId;
    V3LPFeeProfile lpFeeProfile;
    // Tax fields (set to 0 for non-tax tokens)
    uint16 buyTaxRate;
    uint16 sellTaxRate;
    uint64 taxDuration;
    uint64 antiFarmerDuration;
    uint16 mktBps;
    uint16 deflationBps;
    uint16 dividendBps;
    uint16 lpBps;
    uint256 minimumShareBalance;
    // V3-only fields
    address dividendToken;
    address commissionReceiver;
    TokenVersion tokenVersion;
}
```

### Token version dispatch

The `tokenVersion` field controls which token implementation is created:

| `tokenVersion`    | Token type                  | Notes                                                                                                          |
| ----------------- | --------------------------- | -------------------------------------------------------------------------------------------------------------- |
| `TOKEN_V2_PERMIT` | Standard ERC-20 with permit | Set both tax rates to 0, `commissionReceiver` must be `address(0)`                                             |
| `TOKEN_TAXED`     | FlapTaxToken V1             | Legacy; symmetric rates only, `mktBps` must be 10000, no commission. **Not recommended for new launches.**     |
| `TOKEN_TAXED_V2`  | FlapTaxTokenV2              | Legacy; symmetric rates only, `mktBps` must not be 10000, no commission. **Not recommended for new launches.** |
| `TOKEN_TAXED_V3`  | FlapTaxTokenV3              | **Recommended.** Asymmetric rates, commission supported.                                                       |

{% hint style="warning" %}
`TOKEN_TAXED` and `TOKEN_TAXED_V2` are deprecated. New integrations should always use `TOKEN_TAXED_V3`.
{% endhint %}

`TOKEN_V3_PERMIT` is intentionally not part of `newTokenV6`. Use `newTokenV7` for that launch path.

### Launching a Tax Token V3 (recommended)

Set `tokenVersion = TOKEN_TAXED_V3` and fill in the tax fields:

```typescript
import { parseEther, zeroAddress } from "viem";

const params = {
    name: "My Token",
    symbol: "MTK",
    meta: "<ipfs-cid>",
    dexThresh: DexThreshType.TWO_THIRDS,
    salt, // must produce 7777 vanity suffix
    migratorType: MigratorType.V2_MIGRATOR,
    quoteToken: zeroAddress, // native gas token
    quoteAmt: parseEther("0.01"),
    beneficiary: beneficiaryAddress,
    permitData: "0x",
    extensionID: "0x0000000000000000000000000000000000000000000000000000000000000000",
    extensionData: "0x",
    dexId: DEXId.DEX0,
    lpFeeProfile: V3LPFeeProfile.LP_FEE_PROFILE_STANDARD,
    buyTaxRate: 300, // 3%
    sellTaxRate: 1000, // 10%
    taxDuration: 365n * 24n * 60n * 60n,
    antiFarmerDuration: 60n * 60n,
    mktBps: 10000, // all tax to beneficiary (after protocol fee)
    deflationBps: 0,
    dividendBps: 0,
    lpBps: 0,
    minimumShareBalance: 0n,
    dividendToken: zeroAddress, // Case 2: quote token (zeroAddress = native)
    // Case 1: 0xfEEDFEEDfeEDFEedFEEdFEEDFeEdfEEdFeEdFEEd (MAGIC_DIVIDEND_SELF = launching token itself)
    // Case 3: any ERC-20 supported by SwapRegistry
    commissionReceiver: zeroAddress, // set to your address to earn commission
    tokenVersion: TokenVersion.TOKEN_TAXED_V3,
};

const hash = await walletClient.writeContract({
    address: portalAddress,
    abi: portalAbi,
    functionName: "newTokenV6",
    args: [params],
    value: parseEther("0.01"),
});
```

Key notes:

* `meta` is the IPFS CID of your metadata JSON.
* `migratorType` must be `V2_MIGRATOR` for tax tokens.
* `quoteToken = address(0)` uses the native gas token.
* `salt` must produce the required vanity suffix: `7777` for tax tokens, `8888` for standard tokens.
* `mktBps + deflationBps + dividendBps + lpBps` must equal 10000.
* `dividendToken` supports three modes: **Case 1** — `0xfEEDFEEDfeEDFEedFEEdFEEDFeEdfEEdFeEdFEEd` (`MAGIC_DIVIDEND_SELF`, distributes the launching token itself); **Case 2** — `address(0)` or the quote token address (most common); **Case 3** — any ERC-20 that has a registered swap path in `SwapRegistry`. See [Tax Token V3](/flap/developers/basic-and-mechanism/flap-tax-token/tax-token-v3#dividend-token) for full details.
* Set `commissionReceiver` to your address to earn a protocol-calculated commission from every taxable transaction.

### Launching a standard token

Set `tokenVersion = TOKEN_V2_PERMIT` and all tax fields to 0:

```solidity
function launchStandardToken(IPortal portal) external payable returns (address token) {
    IPortalTypes.NewTokenV6Params memory params = IPortalTypes.NewTokenV6Params({
        // ... base fields ...
        buyTaxRate:         0,
        sellTaxRate:        0,
        // ... other tax fields = 0 ...
        commissionReceiver: address(0),
        tokenVersion:       IPortalTypes.TokenVersion.TOKEN_V2_PERMIT
    });

    token = portal.newTokenV6{value: msg.value}(params);
}
```

## 3) Call `newTokenV7`

`newTokenV7` is the TokenV3-based launch entry point. It is intended for CL-style migration and introduces `feeConfigs`, which replace the old `beneficiary + mktBps/deflationBps/dividendBps/lpBps` split used by `newTokenV6` tax launches.

```solidity
/// @notice Create a new token with V4/PCS Infinity migration support
/// @param params The V7 token parameters
/// @return token The created token address
function newTokenV7(NewTokenV7Params calldata params) external payable returns (address token);

enum FeeType {
    NONE,
    MARKETING_OR_VAULT,
    DIVIDEND,
    DEFLATION,
    LP_BPS
}

struct FeeConfig {
    FeeType feeType;
    uint16 bps;
    address marketingAddress;
    address dividendToken;
    uint256 minimumShareBalance;
}

struct NewTokenV7Params {
    string name;
    string symbol;
    string meta;
    DexThreshType dexThresh;
    bytes32 salt;
    MigratorType migratorType;
    address quoteToken;
    uint256 quoteAmt;
    bytes permitData;
    bytes32 extensionID;
    bytes extensionData;
    DEXId dexId;
    uint16 buyTaxRate;
    uint16 sellTaxRate;
    uint64 taxDuration;
    uint64 antiFarmerDuration;
    address commissionReceiver;
    TokenVersion tokenVersion;
    FeeConfig[4] feeConfigs;
}
```

### Current `newTokenV7` rollout details

The interface includes both `TOKEN_V3_PERMIT` and `TOKEN_TAXED_V3` paths, but the current implementation is narrower:

* Public `newTokenV7` launches currently support `TOKEN_V3_PERMIT` only.
* `TOKEN_TAXED_V3` through `newTokenV7` currently reverts with `NewTokenV7RuleViolation(4)`.
* For tax-token launches, keep using `newTokenV6`.
* The current implementation accepts `PCS_INFINITY_CL_MIGRATOR` for the public `TOKEN_V3_PERMIT` path.
* `commissionReceiver` must be `address(0)` for `TOKEN_V3_PERMIT`.
* `buyTaxRate` and `sellTaxRate` must both be `0` for `TOKEN_V3_PERMIT`.
* `antiFarmerDuration` must not exceed `365 days`.

{% hint style="warning" %}
Although `MigratorType` includes `V4_UNI_MIGRATOR`, the current `TOKEN_V3_PERMIT` implementation behind `newTokenV7` validates against `PCS_INFINITY_CL_MIGRATOR`. If you send any other migrator type on the public path, the call reverts with `InvalidMigratorType()`.
{% endhint %}

### How `feeConfigs` work

`feeConfigs` has 4 fixed slots. General validation rules from the launcher are:

* every slot with `feeType != NONE` must have `bps > 0`
* duplicate fee types are not allowed
* all active slots together must sum to exactly `10000` bps
* `MARKETING_OR_VAULT` requires a non-zero `marketingAddress`
* `DIVIDEND` uses `dividendToken` and `minimumShareBalance`

For the current public `TOKEN_V3_PERMIT` rollout, there are extra restrictions:

* only **one** active fee mode is currently supported
* supported mode A: a single `MARKETING_OR_VAULT` slot with `bps = 10000`
* supported mode B: a single `DIVIDEND` slot with `bps = 10000` and `dividendToken == quoteToken`
* `DEFLATION` and `LP_BPS` are not enabled yet for `TOKEN_V3_PERMIT` and revert with `UnsupportedV7TokenV3PermitFeeType(...)`

There is no standalone `beneficiary` field in `NewTokenV7Params`. If you use `MARKETING_OR_VAULT`, the first such slot's `marketingAddress` becomes the primary beneficiary used by the launcher.

### Example: zero-tax `TOKEN_V3_PERMIT` launch

```typescript
const zeroAddress = "0x0000000000000000000000000000000000000000";

const params = {
    name: "My CL Token",
    symbol: "MCL",
    meta: "<ipfs-cid>",
    dexThresh: DexThreshType.FOUR_FIFTHS,
    salt: vanitySalt, // must produce 8888 suffix
    migratorType: MigratorType.PCS_INFINITY_CL_MIGRATOR,
    quoteToken: zeroAddress,
    quoteAmt: parseEther("0.1"),
    permitData: "0x",
    extensionID: "0x0000000000000000000000000000000000000000000000000000000000000000",
    extensionData: "0x",
    dexId: DEXId.DEX0,
    buyTaxRate: 0,
    sellTaxRate: 0,
    taxDuration: 0,
    antiFarmerDuration: 0,
    commissionReceiver: zeroAddress,
    tokenVersion: TokenVersion.TOKEN_V3_PERMIT,
    feeConfigs: [
        {
            feeType: FeeType.MARKETING_OR_VAULT,
            bps: 10000,
            marketingAddress: beneficiaryAddress,
            dividendToken: zeroAddress,
            minimumShareBalance: 0n,
        },
        {
            feeType: FeeType.NONE,
            bps: 0,
            marketingAddress: zeroAddress,
            dividendToken: zeroAddress,
            minimumShareBalance: 0n,
        },
        {
            feeType: FeeType.NONE,
            bps: 0,
            marketingAddress: zeroAddress,
            dividendToken: zeroAddress,
            minimumShareBalance: 0n,
        },
        {
            feeType: FeeType.NONE,
            bps: 0,
            marketingAddress: zeroAddress,
            dividendToken: zeroAddress,
            minimumShareBalance: 0n,
        },
    ],
};

const tx = await portal.newTokenV7(params, { value: params.quoteAmt });
```

If you use the dividend-only mode instead of the marketing mode, set exactly one `DIVIDEND` slot with `bps = 10000`, and set `dividendToken` to the same address as `quoteToken`.

## 4) Find the salt (vanity suffix)

`Portal` uses CREATE2 for deterministic deployments. The `salt` must produce a token address with the required suffix:

* Non-tax token: address ends with `8888`.
* Tax token: address ends with `7777`.

To find a valid `salt`, repeatedly hash a random seed and check the predicted address until it matches the suffix. You can predict the address using CREATE2 with the `Portal` address and the token implementation address for the token type you’re launching.

Example (TypeScript):

```typescript
/// Find a vanity salt by predicting the CREATE2 address.
async function findVanityTokenSalt(suffix: string, tokenImpl: Address, portal: Address) {
    if (suffix.length !== 4) {
        throw new Error("Suffix must be exactly 4 characters");
    }

    const predictVanityTokenAddress = (salt: Hex): Address => {
        const bytecode = "0x3d602d80600a3d3981f3363d3d373d3d3d363d73"
            + tokenImpl.slice(2).toLowerCase()
            + "5af43d82803e903d91602b57fd5bf3" as Hex;

        return getContractAddress({
            from: portal,
            salt: toBytes(salt),
            bytecode,
            opcode: "CREATE2",
        });
    };

    // you don't need to use privatekey, you can use any random seed as long as it is unique for each attempt. Using a privatekey is just a convenient way to get a random 32-byte value.
    const seed = generatePrivateKey();
    let salt = keccak256(toHex(seed));
    let iterations = 0;

    while (!predictVanityTokenAddress(salt).endsWith(suffix)) {
        salt = keccak256(salt);
        iterations++;
    }

    return {
        salt,
        address: predictVanityTokenAddress(salt),
        iterations,
    };
}
```

Use the correct `tokenImpl` and `portal` addresses for your network (see the deployed addresses page in this section).

Token implementation selection for launch methods:

* `TOKEN_V2_PERMIT` (standard token): use the **Standard Token Impl** (`tokenImplV2`).
* `TOKEN_TAXED_V3` (recommended tax token): use the **Tax Token V3 Impl** (`tokenImplTaxedV3`).
* `TOKEN_TAXED` (legacy V1, not recommended): use the **Tax Token V1 Impl** (`tokenImplTaxed`).
* `TOKEN_TAXED_V2` (legacy V2, not recommended): use the **Tax Token V2 Impl** (`tokenImplTaxedV2`).
* `TOKEN_V3_PERMIT` via `newTokenV7`: use the **Standard Token V3 Impl**. It is still a non-tax launch, so the vanity suffix is `8888`.

See [deployed-contract-addresses.md](/flap/developers/token-launcher-developers/deployed-contract-addresses) for the exact implementation addresses.

{% hint style="warning" %}
For new launches, always use `tokenImplTaxedV3` when computing salts for tax tokens. Legacy tax token implementations (`TOKEN_TAXED`, `TOKEN_TAXED_V2`) are deprecated and should not be used for new integrations.
{% endhint %}

{% hint style="warning" %}
`newTokenV7` currently does not expose a public `TOKEN_TAXED_V3` rollout. For tax-token launches, compute the salt against the `newTokenV6` implementation path and use `newTokenV6`.
{% endhint %}

## 5) Legacy methods

`newTokenV2`, `newTokenV3`, `newTokenV4`, and `newTokenV5` are still supported for legacy integrations. They follow the same flow but provide fewer features than the current launch paths. New integrations should use `newTokenV6` for tax tokens and `TOKEN_V2_PERMIT`, or `newTokenV7` for `TOKEN_V3_PERMIT`.

## IPortal.sol

Full interface reference for developers:

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

pragma solidity ^0.8.13;

import {IAccessControlUpgradeable} from "@openzeppelin-contracts-upgradeable/access/IAccessControlUpgradeable.sol";
import {IUniswapV3MintCallback} from "uni-v3-core/interfaces/callback/IUniswapV3MintCallback.sol";
import {IPancakeV3MintCallback} from "pancake-v3-core/interfaces/callback/IPancakeV3MintCallback.sol";

/// @dev Magic address value for `dividendToken` in NewTokenV6Params.
///      When set to this address, the dividend token is resolved to the tax token's own address
///      after it is created. This is a sugar for devs who do not want to pre-compute the tax token
///      address (it is deterministically computed from the salt).
///      Uses a well-known non-conflicting sentinel (0xfEED...fEED) to avoid conflict with:
///        - address(0): native gas token dividend (only valid when quoteToken is also native gas)
///        - any ERC-20 address: use that token as dividend (including quoteToken)
address constant MAGIC_DIVIDEND_SELF = address(0xfEEDFEEDfeEDFEedFEEdFEEDFeEdfEEdFeEdFEEd);

/// @title Common Types
/// @notice This interface defines common types shared across the portal
interface IPortalCommonTypes {
    /// @dev curve Types
    enum CurveType {
        CURVE_LEGACY_15, // r = 15
        CURVE_4, // r = 4
        CURVE_0_974, // r = 0.974
        CURVE_0_5, // r = 0.5
        CURVE_1000, // r = 1000
        CURVE_20000, // r = 20000
        CURVE_2500, // r = 2500
        CURVE_500, // r = 500
        CURVE_2, // r = 2
        CURVE_6, // r = 6
        CURVE_75, // r = 75
        CURVE_4M, // r= 4 M
        CURVE_28, // r = 28
        CURVE_21_25, // r = 21.25
        CURVE_RH_UNUSED, // r = 27.6, h = 352755468
        CURVE_RH_28D25_108002126, // r = 28.25, h = 108002126, k = 31301060059.5
        CURVE_RH_14981_108002125, // r = 14981, h = 108002125, k = 16598979834625
        CURVE_RH_TOSHI_MORPH_2ETH, // r = 0.7672, h = 107036751, k = 849318595.3672 - TOSHI/MORPH 2ETH curve
        CURVE_RH_TOSHI, // r = 6140351, h = 107036752, k = 6797594227179952 - TOSHI Curve
        CURVE_RH_BGB, // r = 767.5, h = 107036752, k = 849650707160 - BGB curve
        CURVE_RH_BNB, // r = 6.14, h = 107036752, k = 6797205657.28 - BNB Curve
        CURVE_RH_USD, // r = 3837, h = 107036752, k = 4247700017424 - USD curve
        CURVE_RH_MONAD, // r = 50000, h = 107036752, k = 55351837600000 - MONAD curve
        CURVE_RH_MONAD_V2, // r = 107400, h = 107036752, k = 118895747164800  - MONAD V2 curve
        CURVE_RH_KGST // r = 380000, h = 107036752, k = 420673965760000 - KGST curve
    }

    /// @dev dex threshold types
    enum DexThreshType {
        TWO_THIRDS, //  66.67% supply
        FOUR_FIFTHS, // 80% supply
        HALF, // 50% supply
        _95_PERCENT, // 95% supply
        _81_PERCENT, // 81% supply
        _1_PERCENT // 1% supply => mainly for testing
    }

    /// @notice Fee profile for tokens
    /// @dev Determines the fee structure applied to a token's trades and liquidity operations
    /// Fees are represented in basis points (bps), where 1% = 100 bps
    enum FlapFeeProfile {
        FEE_GLOBAL_DEFAULT, // Default fee profile used when no specific profile is set for a token
        FEE_FLAPSALE_V0, // Fee profile for FlapSale V0
        FEE_ZERO // Zero fee profile: no protocol fee charged on trades
    }

    //
    // Custom Errors for Common Types
    //

    /// @notice error if the curve type is invalid
    /// @param curveType The invalid curve type
    error InvalidCurveType(CurveType curveType);

    /// @notice error if the dex threshold type is invalid
    error InvalidDexThresholdType(DexThreshType threshold);
}

/// @title Types and Structs
/// @notice This interface defines the types and structs used in the portal
interface IPortalTypes is IPortalCommonTypes {
    //
    // public constants
    //

    //
    // Types and Structs
    //

    /// @dev Profile types for deployment-specific parameters or behaviors
    enum Profile {
        DEFAULT,
        TOSHI_MART,
        X_LAYER,
        MORPH,
        MONAD
    }

    /// @dev Token version
    /// Which token implementation is used
    enum TokenVersion {
        TOKEN_LEGACY_MINT_NO_PERMIT,
        TOKEN_LEGACY_MINT_NO_PERMIT_DUPLICATE, // for historical reasons, both 0 and 1 are the same: TOKEN_LEGACY_MINT_NO_PERMIT
        TOKEN_V2_PERMIT, // 2
        TOKEN_GOPLUS, // 3
        TOKEN_TAXED, // 4: The original tax token (FlapTaxToken)
        TOKEN_TAXED_V2, // 5: The new advanced tax token (FlapTaxTokenV2)
        TOKEN_TAXED_V3, // 6: The next-generation tax token with asymmetric buy/sell rates (FlapTaxTokenV3)
        TOKEN_V3_PERMIT // 7: Non-tax token using TokenV3 implementation with permit support
    }

    /// @dev the quote token, i.e, the token as the reserve
    enum QuoteTokenType {
        NATIVE_GAS_TOKEN, // The native gas token
        ERC20_TOKEN_WITH_PERMIT, //  The ERC20 token with permit
        ERC20_TOKEN_WITHOUT_PERMIT // The ERC20 token without permit
    }

    /// @notice the status of a token
    /// The token has 5 statuses:
    //    - Tradable: The token can be traded(buy/sell)
    //    - InDuel: (obsolete) The token is in a battle, it can only be bought but not sold.
    //    - Killed: (obsolete) The token is killed, it can not be traded anymore. Can only be redeemed for another token.
    //    - DEX: The token has been added to the DEX
    //    - Staged: The token is staged but not yet created (address is predetermined)
    enum TokenStatus {
        Invalid, // The token does not exist
        Tradable,
        InDuel, // obsolete
        Killed, // obsolete
        DEX,
        Staged // The token is staged (address determined, but not yet created)
    }

    /// @notice the migrator type
    /// @dev the migrator type determines how the liquidity is added to the DEX.
    /// Note: To mitigate the risk of DOS, if a V3 migrator is used but the liquidity cannot
    /// be added to v3 pools, the migrator will fallback to a V2 migrator.
    /// A TAX token must use a V2 migrator.
    enum MigratorType {
        V3_MIGRATOR, // Migrate the liquidity to a Uniswap V3 like pool
        V2_MIGRATOR, // Migrate the liquidity to a Uniswap V2 like pool
        V4_UNI_MIGRATOR, // Migrate the liquidity to a Uniswap V4 pool (Base, XLayer)
        PCS_INFINITY_CL_MIGRATOR // Migrate the liquidity to a Pancake Infinity CL Pool (BNB)
    }

    /// @notice the V3 LP fee profile
    /// @dev determines the LP fee tier to use when migrating tokens to Uniswap V3 or Pancake V3
    enum V3LPFeeProfile {
        LP_FEE_PROFILE_STANDARD, // Standard fee tier:  0.25% on PancakeSwap, 0.3% on Uniswap
        LP_FEE_PROFILE_LOW, // Low fee tier: typically, 0.01% on PancakeSwap, 0.05% on Uniswap
        LP_FEE_PROFILE_HIGH // High fee tier (1% for exotic pairs)
    }

    /// @notice the DEX ID
    /// @dev determines the DEX we want to migrate to
    /// On BSC:
    ///   - only DEX0 will be enabled, which is PancakeSwap
    /// On xLayer:
    ///   - only DEX0 will be enabled, which is PotatoSwap
    /// On Monad:
    ///   - DEX0 is Uniswap
    ///   - DEX1 is PancakeSwap
    ///   - DEX2 is Monday
    /// On Robinhood Chain:
    ///   - DEX0 is Uniswap
    ///   - DEX1 is PancakeSwap
    /// Note that, currently, we only support at most 3 DEXes
    /// We may add more DEXes in the future if needed
    enum DEXId {
        DEX0,
        DEX1,
        DEX2
    }

    /// @notice the state of a token (with dex related fields)
    struct TokenStateV2 {
        TokenStatus status; // the status of the token
        uint256 reserve; // the reserve of the token
        uint256 circulatingSupply; // the circulatingSupply of the token
        uint256 price; // the price of the token
        TokenVersion tokenVersion; // the version of the token implementation this token is using
        uint256 r; // the r of the curve of the token
        uint256 dexSupplyThresh; // the cirtulating supply threshold for adding the token to the DEX
    }

    /// @notice the state of a token (with all V2 fields plus quoteTokenAddress and nativeToQuoteSwapEnabled)
    struct TokenStateV3 {
        /// The status of the token (see TokenStatus enum)
        TokenStatus status;
        /// The reserve amount of the quote token held by the bonding curve
        uint256 reserve;
        /// The circulating supply of the token
        uint256 circulatingSupply;
        /// The current price of the token (in quote token units, 18 decimals)
        uint256 price;
        /// The version of the token implementation (see TokenVersion enum)
        TokenVersion tokenVersion;
        /// The curve parameter 'r' used for the bonding curve
        uint256 r;
        /// The circulating supply threshold for adding the token to the DEX
        uint256 dexSupplyThresh;
        /// The address of the quote token (address(0) if native gas token)
        address quoteTokenAddress;
        /// Whether native-to-quote swap is enabled for this token
        bool nativeToQuoteSwapEnabled;
    }

    /// @notice the state of a token (with all V3 fields plus extensionID and 'r' curve parameter only)
    struct TokenStateV4 {
        /// The status of the token (see TokenStatus enum)
        TokenStatus status;
        /// The reserve amount of the quote token held by the bonding curve
        uint256 reserve;
        /// The circulating supply of the token
        uint256 circulatingSupply;
        /// The current price of the token (in quote token units, 18 decimals)
        uint256 price;
        /// The version of the token implementation (see TokenVersion enum)
        TokenVersion tokenVersion;
        /// The curve parameter 'r' used for the bonding curve
        uint256 r;
        /// The circulating supply threshold for adding the token to the DEX
        uint256 dexSupplyThresh;
        /// The address of the quote token (address(0) if native gas token)
        address quoteTokenAddress;
        /// Whether native-to-quote swap is enabled for this token
        bool nativeToQuoteSwapEnabled;
        /// The extension ID used by the token (bytes32(0) if no extension)
        bytes32 extensionID;
    }

    /// @notice the state of a token (with all V4 fields plus all curve parameters)
    struct TokenStateV5 {
        /// The status of the token (see TokenStatus enum)
        TokenStatus status;
        /// The reserve amount of the quote token held by the bonding curve
        uint256 reserve;
        /// The circulating supply of the token
        uint256 circulatingSupply;
        /// The current price of the token (in quote token units, 18 decimals)
        uint256 price;
        /// The version of the token implementation (see TokenVersion enum)
        TokenVersion tokenVersion;
        /// The curve parameter 'r' used for the bonding curve
        uint256 r;
        /// The curve parameter 'h' - virtual token reserve
        uint256 h;
        /// The curve parameter 'k' - square of virtual liquidity
        uint256 k;
        /// The circulating supply threshold for adding the token to the DEX
        uint256 dexSupplyThresh;
        /// The address of the quote token (address(0) if native gas token)
        address quoteTokenAddress;
        /// Whether native-to-quote swap is enabled for this token
        bool nativeToQuoteSwapEnabled;
        /// The extension ID used by the token (bytes32(0) if no extension)
        bytes32 extensionID;
    }

    /// @notice the state of a token (with all V5 fields plus taxRate, pool, and progress)
    struct TokenStateV6 {
        /// The status of the token (see TokenStatus enum)
        TokenStatus status;
        /// The reserve amount of the quote token held by the bonding curve
        uint256 reserve;
        /// The circulating supply of the token
        uint256 circulatingSupply;
        /// The current price of the token (in quote token units, 18 decimals)
        uint256 price;
        /// The version of the token implementation (see TokenVersion enum)
        TokenVersion tokenVersion;
        /// The curve parameter 'r' used for the bonding curve
        uint256 r;
        /// The curve parameter 'h' - virtual token reserve
        uint256 h;
        /// The curve parameter 'k' - square of virtual liquidity
        uint256 k;
        /// The circulating supply threshold for adding the token to the DEX
        uint256 dexSupplyThresh;
        /// The address of the quote token (address(0) if native gas token)
        address quoteTokenAddress;
        /// Whether native-to-quote swap is enabled for this token
        bool nativeToQuoteSwapEnabled;
        /// The extension ID used by the token (bytes32(0) if no extension)
        bytes32 extensionID;
        /// The tax rate in basis points (0 if not a tax token)
        uint256 taxRate;
        /// The DEX pool address (address(0) if not listed on DEX)
        address pool;
        /// The progress towards DEX listing (0 to 1e18, where 1e18 = 100%)
        uint256 progress;
    }

    /// @notice the state of a token (with all V6 fields plus lpFeeProfile)
    struct TokenStateV7 {
        /// The status of the token (see TokenStatus enum)
        TokenStatus status;
        /// The reserve amount of the quote token held by the bonding curve
        uint256 reserve;
        /// The circulating supply of the token
        uint256 circulatingSupply;
        /// The current price of the token (in quote token units, 18 decimals)
        uint256 price;
        /// The version of the token implementation (see TokenVersion enum)
        TokenVersion tokenVersion;
        /// The curve parameter 'r' used for the bonding curve
        uint256 r;
        /// The curve parameter 'h' - virtual token reserve
        uint256 h;
        /// The curve parameter 'k' - square of virtual liquidity
        uint256 k;
        /// The circulating supply threshold for adding the token to the DEX
        uint256 dexSupplyThresh;
        /// The address of the quote token (address(0) if native gas token)
        address quoteTokenAddress;
        /// Whether native-to-quote swap is enabled for this token
        bool nativeToQuoteSwapEnabled;
        /// The extension ID used by the token (bytes32(0) if no extension)
        bytes32 extensionID;
        /// The tax rate in basis points (0 if not a tax token)
        uint256 taxRate;
        /// The DEX pool address (address(0) if not listed on DEX)
        address pool;
        /// The progress towards DEX listing (0 to 1e18, where 1e18 = 100%)
        uint256 progress;
        /// The V3 LP fee profile for the token
        V3LPFeeProfile lpFeeProfile;
        /// The Dex Id
        DEXId dexId;
    }

    struct TokenStateV8 {
        /// The status of the token (see TokenStatus enum)
        TokenStatus status;
        /// The reserve amount of the quote token held by the bonding curve
        uint256 reserve;
        /// The circulating supply of the token
        uint256 circulatingSupply;
        /// The current price of the token (in quote token units, 18 decimals)
        uint256 price;
        /// The version of the token implementation (see TokenVersion enum)
        TokenVersion tokenVersion;
        /// The curve parameter 'r' used for the bonding curve
        uint256 r;
        /// The curve parameter 'h' - virtual token reserve
        uint256 h;
        /// The curve parameter 'k' - square of virtual liquidity
        uint256 k;
        /// The circulating supply threshold for adding the token to the DEX
        uint256 dexSupplyThresh;
        /// The address of the quote token (address(0) if native gas token)
        address quoteTokenAddress;
        /// Whether native-to-quote swap is enabled for this token
        bool nativeToQuoteSwapEnabled;
        /// The extension ID used by the token (bytes32(0) if no extension)
        bytes32 extensionID;
        /// The buy tax rate in basis points (0 if not a tax token)
        uint256 buyTaxRate;
        /// The sell tax rate in basis points (0 if not a tax token)
        uint256 sellTaxRate;
        /// The DEX pool address (address(0) if not listed on DEX)
        address pool;
        /// The progress towards DEX listing (0 to 1e18, where 1e18 = 100%)
        uint256 progress;
        /// The V3 LP fee profile for the token
        V3LPFeeProfile lpFeeProfile;
        /// The Dex Id
        DEXId dexId;
    }

    /// @notice A forward-compatible version of TokenStateV8, where enum-typed fields are returned
    ///         as uint8 instead of their Solidity enum types.
    /// @dev Safe because Solidity revert when decoding an enum value that exceeds the enum's
    ///      declared maximum (e.g. a new TOKEN_TAXED_V3 value read by a contract compiled against
    ///      an old TokenVersion enum). By using uint8 for TokenStatus, TokenVersion,
    ///      V3LPFeeProfile, and DEXId, callers are not affected when new variants are added to
    ///      any of those enums in the future. Use getTokenV8 if you are always up-to-date with
    ///      the latest interface; use this method if you need forward/backward compatibility.
    struct TokenStateV8Safe {
        /// The status of the token (see TokenStatus enum for interpretation)
        uint8 status;
        /// The reserve amount of the quote token held by the bonding curve
        uint256 reserve;
        /// The circulating supply of the token
        uint256 circulatingSupply;
        /// The current price of the token (in quote token units, 18 decimals)
        uint256 price;
        /// The version of the token implementation (see TokenVersion enum for interpretation)
        uint8 tokenVersion;
        /// The curve parameter 'r' used for the bonding curve
        uint256 r;
        /// The curve parameter 'h' - virtual token reserve
        uint256 h;
        /// The curve parameter 'k' - square of virtual liquidity
        uint256 k;
        /// The circulating supply threshold for adding the token to the DEX
        uint256 dexSupplyThresh;
        /// The address of the quote token (address(0) if native gas token)
        address quoteTokenAddress;
        /// Whether native-to-quote swap is enabled for this token
        bool nativeToQuoteSwapEnabled;
        /// The extension ID used by the token (bytes32(0) if no extension)
        bytes32 extensionID;
        /// The buy tax rate in basis points (0 if not a tax token)
        uint256 buyTaxRate;
        /// The sell tax rate in basis points (0 if not a tax token)
        uint256 sellTaxRate;
        /// The DEX pool address (address(0) if not listed on DEX)
        address pool;
        /// The progress towards DEX listing (0 to 1e18, where 1e18 = 100%)
        uint256 progress;
        /// The V3 LP fee profile for the token (see V3LPFeeProfile enum for interpretation)
        uint8 lpFeeProfile;
        /// The Dex Id (see DEXId enum for interpretation)
        uint8 dexId;
    }

    /// @notice Parameters for creating a new token (V2)
    struct NewTokenV2Params {
        /// The name of the token
        string name;
        /// The symbol of the token
        string symbol;
        /// The metadata URI of the token
        string meta;
        /// The DEX supply threshold type
        DexThreshType dexThresh;
        /// The salt for deterministic deployment
        bytes32 salt;
        /// The tax rate in basis points (if non-zero, this is a tax token)
        uint16 taxRate;
        /// The migrator type (see MigratorType enum)
        MigratorType migratorType;
        /// The quote token address (native gas token if zero address)
        address quoteToken;
        /// The initial quote token amount to spend for buying
        uint256 quoteAmt;
        /// The beneficiary address for the token
        /// For rev share tokens, this is the address that can claim the LP fees
        /// For tax tokens, this is the address that receives the tax fees
        address beneficiary;
        /// The optional permit data for the quote token
        bytes permitData;
    }

    /// @notice Parameters for creating a new token (V3) with extension support
    struct NewTokenV3Params {
        /// The name of the token
        string name;
        /// The symbol of the token
        string symbol;
        /// The metadata URI of the token
        string meta;
        /// The DEX supply threshold type
        DexThreshType dexThresh;
        /// The salt for deterministic deployment
        bytes32 salt;
        /// The tax rate in basis points (if non-zero, this is a tax token)
        uint16 taxRate;
        /// The migrator type (see MigratorType enum)
        MigratorType migratorType;
        /// The quote token address (native gas token if zero address)
        address quoteToken;
        /// The initial quote token amount to spend for buying
        uint256 quoteAmt;
        /// The beneficiary address for the token
        /// For rev share tokens, this is the address that can claim the LP fees
        /// For tax tokens, this is the address that receives the tax fees
        address beneficiary;
        /// The optional permit data for the quote token
        bytes permitData;
        /// @notice The ID of the extension to be used for the new token if not zero
        bytes32 extensionID;
        /// @notice Additional extension specific data to be passed to the extension's `onTokenCreation` method, check the extension's documentation for details on the expected format and content.
        bytes extensionData;
    }

    /// @notice Parameters for creating a new token (V4) with DEX ID and LP fee profile support
    struct NewTokenV4Params {
        /// The name of the token
        string name;
        /// The symbol of the token
        string symbol;
        /// The metadata URI of the token
        string meta;
        /// The DEX supply threshold type
        DexThreshType dexThresh;
        /// The salt for deterministic deployment
        bytes32 salt;
        /// The tax rate in basis points (if non-zero, this is a tax token)
        uint16 taxRate;
        /// The migrator type (see MigratorType enum)
        MigratorType migratorType;
        /// The quote token address (native gas token if zero address)
        address quoteToken;
        /// The initial quote token amount to spend for buying
        uint256 quoteAmt;
        /// The beneficiary address for the token
        /// For rev share tokens, this is the address that can claim the LP fees
        /// For tax tokens, this is the address that receives the tax fees
        address beneficiary;
        /// The optional permit data for the quote token
        bytes permitData;
        /// @notice The ID of the extension to be used for the new token if not zero
        bytes32 extensionID;
        /// @notice Additional extension specific data to be passed to the extension's `onTokenCreation` method, check the extension's documentation for details on the expected format and content.
        bytes extensionData;
        /// @notice The preferred DEX ID for the token
        DEXId dexId;
        /// @notice The preferred V3 LP fee profile for the token
        V3LPFeeProfile lpFeeProfile;
    }

    /// @notice Parameters for creating a new token (V5) with tax V2 support
    struct NewTokenV5Params {
        /// The name of the token
        string name;
        /// The symbol of the token
        string symbol;
        /// The metadata URI of the token
        string meta;
        /// The DEX supply threshold type
        DexThreshType dexThresh;
        /// The salt for deterministic deployment
        bytes32 salt;
        /// The tax rate in basis points (if non-zero, this is a tax token)
        uint16 taxRate;
        /// The migrator type (see MigratorType enum)
        MigratorType migratorType;
        /// The quote token address (native gas token if zero address)
        address quoteToken;
        /// The initial quote token amount to spend for buying
        uint256 quoteAmt;
        /// The beneficiary address for the token
        /// For rev share tokens, this is the address that can claim the LP fees
        /// For tax tokens, this is the address that receives the tax fees
        address beneficiary;
        /// The optional permit data for the quote token
        bytes permitData;
        /// @notice The ID of the extension to be used for the new token if not zero
        bytes32 extensionID;
        /// @notice Additional extension specific data to be passed to the extension's `onTokenCreation` method, check the extension's documentation for details on the expected format and content.
        bytes extensionData;
        /// @notice The preferred DEX ID for the token
        DEXId dexId;
        /// @notice The preferred V3 LP fee profile for the token
        V3LPFeeProfile lpFeeProfile;
        // New V5 tax-specific fields (only used when taxRate > 0)
        /// Tax duration in seconds (max: 100 years)
        uint64 taxDuration;
        /// Anti-farmer duration in seconds (max: 1 year)
        uint64 antiFarmerDuration;
        /// Market allocation basis points (to beneficiary)
        uint16 mktBps;
        /// Deflation basis points (burned)
        uint16 deflationBps;
        /// Dividend basis points (to dividend contract)
        uint16 dividendBps;
        /// Liquidity provision basis points (LP to dead address)
        uint16 lpBps;
        /// Minimum balance for dividend eligibility (min: 10K ether, required when dividendBps > 0)
        uint256 minimumShareBalance;
    }

    /// @dev Magic address value for `dividendToken` in NewTokenV6Params.
    ///      When set to MAGIC_DIVIDEND_SELF, the dividend token is resolved to the tax token's own address
    ///      after it is created. See MAGIC_DIVIDEND_SELF file-level constant.

    /// @notice Parameters for creating a new token (V6) with asymmetric tax and custom dividend token
    struct NewTokenV6Params {
        // --- Same base fields as V5 ---
        /// The name of the token
        string name;
        /// The symbol of the token
        string symbol;
        /// The metadata URI of the token
        string meta;
        /// The DEX supply threshold type
        DexThreshType dexThresh;
        /// The salt for deterministic deployment
        bytes32 salt;
        /// The migrator type (see MigratorType enum)
        MigratorType migratorType;
        /// The quote token address (native gas token if zero address)
        address quoteToken;
        /// The initial quote token amount to spend for buying
        uint256 quoteAmt;
        /// The beneficiary address for the token
        address beneficiary;
        /// The optional permit data for the quote token
        bytes permitData;
        /// The ID of the extension to be used for the new token if not zero
        bytes32 extensionID;
        /// Additional extension specific data
        bytes extensionData;
        /// The preferred DEX ID for the token
        DEXId dexId;
        /// The preferred V3 LP fee profile for the token
        V3LPFeeProfile lpFeeProfile;
        // --- Tax fields (all zero = non-tax token, behaves like newTokenV5) ---
        /// Buy tax rate in basis points (0 = no buy tax)
        uint16 buyTaxRate;
        /// Sell tax rate in basis points (0 = no sell tax)
        uint16 sellTaxRate;
        /// Tax duration in seconds
        uint64 taxDuration;
        /// Anti-farmer duration in seconds
        uint64 antiFarmerDuration;
        /// Market allocation basis points (to beneficiary)
        uint16 mktBps;
        /// Deflation basis points (burned)
        uint16 deflationBps;
        /// Dividend basis points (to dividend contract)
        uint16 dividendBps;
        /// Liquidity provision basis points (LP to dead address)
        uint16 lpBps;
        /// Minimum balance for dividend eligibility (required when dividendBps > 0)
        uint256 minimumShareBalance;
        // --- New V3-only fields ---
        /// @notice Dividend distribution token:
        ///   address(0)         = native gas token dividend (only valid when quoteToken is also address(0))
        ///   MAGIC_DIVIDEND_SELF = distribute the tax token itself as dividend
        ///   quoteToken address  = explicitly use the quote token as dividend (must be set explicitly)
        ///   any other ERC-20   = use that specific ERC-20 as dividend token
        address dividendToken;
        /// @notice Commission receiver address (zero = disabled).
        ///         MUST be address(0) for non-tax tokens (buyTaxRate == 0 && sellTaxRate == 0).
        ///         commissionBps is NOT provided here — it is calculated internally by the launcher
        ///         based on the effective tax rate via _commissionForTax().
        address commissionReceiver;
        /// @notice The token version to create. Determines which launcher path is used.
        ///   TOKEN_V2_PERMIT  — non-tax token (standard ERC-20 with permit)
        ///   TOKEN_TAXED      — V1 tax token (symmetric rates, mktBps=10000, no commission)
        ///   TOKEN_TAXED_V2   — V2 tax token (asymmetric rates, flexible distribution, no commission, mktBps != 10000)
        ///   TOKEN_TAXED_V3   — V3 tax token (asymmetric rates, flexible distribution, commission supported)
        TokenVersion tokenVersion;
    }

    // NOTE: `converter` is NOT in this struct — it is provided as an immutable
    // in PortalBase and automatically passed to TaxProcessor at initialization.

    // ─── V7 Fee Configuration ────────────────────────────────────────────────

    /// @notice Fee type for token V7
    /// @dev Not only tax fees but also LP fees and other fee types
    enum FeeType {
        NONE, // Not used / disabled
        MARKETING_OR_VAULT, // Fee goes to marketing address or vault
        DIVIDEND, // Fee distributed as dividends
        DEFLATION, // Fee is burned
        LP_BPS // Fee added to LP
    }

    /// @notice Fee configuration for a single fee slot
    /// @dev Not only tax fees but also possibly the LP fee.
    ///      Only the fields relevant to the feeType need to be populated:
    ///        MARKETING_OR_VAULT → marketingAddress + bps
    ///        DIVIDEND           → dividendToken + minimumShareBalance + bps
    ///        DEFLATION          → bps
    ///        LP_BPS             → bps
    ///        NONE               → all fields ignored
    struct FeeConfig {
        FeeType feeType; // The type of fee
        uint16 bps; // Basis points for this fee (0-10000)
        address marketingAddress; // Only used for MARKETING_OR_VAULT type
        address dividendToken; // Only used for DIVIDEND type (the token to distribute)
        uint256 minimumShareBalance; // Only used for DIVIDEND type (min balance for eligibility)
    }

    /// @notice Parameters for launching a V7 token (V4/PCS Infinity migration support)
    /// @dev Flat struct — all fields from V6 are duplicated here for clarity and forward-compatibility.
    ///      If a V8 is needed, copy this struct and append new fields (same pattern).
    ///
    ///      Fee distribution is fully described by feeConfigs[4]. Each slot is independent and
    ///      can hold any FeeType (NONE, MARKETING_OR_VAULT, DIVIDEND, DEFLATION, LP_BPS).
    ///      All non-NONE slots' bps MUST sum to exactly 10000.
    ///      The first MARKETING_OR_VAULT entry's marketingAddress is used as the primary beneficiary
    ///      for LP-fee claiming purposes.  Up to 4 MARKETING_OR_VAULT slots are supported.
    struct NewTokenV7Params {
        // ── Base fields (same as NewTokenV6Params) ──────────────────
        /// The name of the token
        string name;
        /// The symbol of the token
        string symbol;
        /// The metadata URI of the token
        string meta;
        /// The DEX supply threshold type
        DexThreshType dexThresh;
        /// The salt for deterministic deployment
        bytes32 salt;
        /// The migrator type (see MigratorType enum)
        MigratorType migratorType;
        /// The quote token address (native gas token if zero address)
        address quoteToken;
        /// The initial quote token amount to spend for buying
        uint256 quoteAmt;
        /// The optional permit data for the quote token
        bytes permitData;
        /// The ID of the extension to be used for the new token if not zero
        bytes32 extensionID;
        /// Additional extension specific data
        bytes extensionData;
        /// The preferred DEX ID for the token
        DEXId dexId;
        // ── Tax fields (all zero = non-tax token) ───────────────────
        /// Buy tax rate in basis points (0 = no buy tax)
        uint16 buyTaxRate;
        /// Sell tax rate in basis points (0 = no sell tax)
        uint16 sellTaxRate;
        /// Tax duration in seconds
        uint64 taxDuration;
        /// Anti-farmer duration in seconds
        uint64 antiFarmerDuration;
        /// @notice Commission receiver address (zero = disabled).
        /// @dev Only meaningful for TOKEN_TAXED_V3.
        ///      MUST be address(0) for TOKEN_V3_PERMIT (non-tax tokens do not support commission).
        ///      commissionBps is NOT provided here — it is auto-computed internally by the launcher
        ///      based on the effective tax rate via _commissionForTax().
        address commissionReceiver;
        /// @notice The token version to create (see TokenVersion enum)
        TokenVersion tokenVersion;
        /// @notice Fee distribution configuration (4 slots, must sum to 10000 bps).
        /// @dev Not only tax fees but also possibly the LP fee.
        FeeConfig[4] feeConfigs;
    }

    /// @notice Parameters for staging a new token (V5) - immutable parameters only
    struct StageNewTokenV5Params {
        /// The DEX supply threshold type
        DexThreshType dexThresh;
        /// The salt for deterministic deployment
        bytes32 salt;
        /// Whether this is a tax token
        bool isTaxToken;
        /// The migrator type (see MigratorType enum)
        MigratorType migratorType;
        /// The quote token address (native gas token if zero address)
        address quoteToken;
        /// @notice The preferred DEX ID for the token
        DEXId dexId;
    }

    /// @notice Parameters for committing a staged token (V5) - mutable/deployment-time parameters
    struct CommitNewTokenV5Params {
        /// The salt for deterministic deployment (must match staged salt)
        bytes32 salt;
        /// The tax rate in basis points (0 for non-tax tokens)
        uint16 taxRate;
        /// The name of the token
        string name;
        /// The symbol of the token
        string symbol;
        /// The metadata URI of the token
        string meta;
        /// The initial quote token amount to spend for buying
        uint256 quoteAmt;
        /// The beneficiary address for the token
        /// For rev share tokens, this is the address that can claim the LP fees
        /// For tax tokens, this is the address that receives the tax fees
        address beneficiary;
        /// The optional permit data for the quote token
        bytes permitData;
        // New V5 tax-specific fields (only used when taxRate > 0)
        /// Tax duration in seconds (max: 100 years)
        uint64 taxDuration;
        /// Anti-farmer duration in seconds (max: 1 year)
        uint64 antiFarmerDuration;
        /// Market allocation basis points (to beneficiary)
        uint16 mktBps;
        /// Deflation basis points (burned)
        uint16 deflationBps;
        /// Dividend basis points (to dividend contract)
        uint16 dividendBps;
        /// Liquidity provision basis points (LP to dead address)
        uint16 lpBps;
        /// Minimum balance for dividend eligibility (required when dividendBps > 0)
        uint256 minimumShareBalance;
    }

    /// @dev The configuration of the "native to quote" swap
    /// i.e How to swap ETH for the quote token when the quote token is not ETH
    enum NativeToQuoteSwapType {
        SWAP_DISABLED, // 0: disabled
        SWAP_VIA_V2_POOL, // 1: swap through v2 pool
        SWAP_VIA_V3_2500_POOL, // 2: swap through v3 2500 pool
        SWAP_VIA_V3_500_POOL, // 3: swap through v3 500 pool
        SWAP_VIA_V3_3000_POOL, // 4: swap through v3 3000 pool
        SWAP_VIA_V3_10000_POOL, // 5: swap through v3 10000 pool
        SWAP_VIA_MIXED_ROUTER // 6: multi-hop via PancakeSwap Infinity MixedQuoter + UniversalRouter (BSC only)
        //    used for tokens like uUSD that route BNB ↔ USDT(V3) ↔ uUSD(BinPool).
        //    The actual routing logic is bypassed in _shouldUseMixedRouter() before
        //    the enum is checked, so this value serves as a meaningful marker when
        //    calling setQuoteTokenConfiguration — any non-SWAP_DISABLED value would
        //    work, but this makes intent explicit.
    }

    /// @dev  the quote token configurations
    struct QuoteTokenConfiguration {
        uint8 enabled; // 8bit: 1 if allowed, 0 if not allowed
        CurveType defaultCurve; // 8bit: the default token curve type of the quote token
        CurveType alternativeCurve; // 8bit: the alternative token curve type of the quote token
        NativeToQuoteSwapType nativeToQuoteSwapType; // 8bit: the native to quote swap feature configuration of the quote token
        uint8 dexId; // 8bit: DEX ID for multiple DEXes support
    }

    /// @dev Enum for DEX pool types
    enum PoolType {
        V2, // Uniswap V2 style pools
        V3, // Uniswap V3 style pools
        V4, // Uniswap V4 style pools
        PCS_INFINITY_CL // PancakeSwap Infinity Concentrated Liquidity pools
    }

    /// @dev Packed DEX pool information
    struct PackedDexPool {
        address pool; // 160 bits: pool address
        uint24 fee; // 24 bits: fee tier (for V3), 0 for V2
        PoolType poolType; // 8 bits: enum for pool type
        uint24 tickSpacing; // 24 bits: tick spacing (for V4/PCS), 0 for V2/V3
        uint40 unused; // 40 bits: reserved for future use
    }

    /// @notice Records who locked a CREATE2 salt and for which token version.
    ///         locker + tokenVersion + isUsed pack into a single 32-byte storage slot.
    struct SaltLockEntry {
        address locker; // 20 bytes — address that paid to reserve this salt
        uint8 tokenVersion; // 1 byte   — TokenVersion enum value at lock time
        bool isUsed; // 1 byte   — true once the salt has been consumed by a token launch
        // locker + tokenVersion + isUsed pack into a single 32-byte slot
    }

    //
    // Events
    //

    /// @notice emitted when a token is staged (but not yet created)
    ///
    /// @param ts The timestamp of the event
    /// @param creator The address of the creator
    /// @param token The predetermined address of the token
    event FlapTokenStaged(uint256 ts, address creator, address token);

    /// @notice emitted when a new token is created
    ///
    /// @param ts The timestamp of the event
    /// @param creator The address of the creator
    /// @param nonce The nonce of the token
    /// @param token  The address of the token
    /// @param name  The name of the token
    /// @param symbol  The symbol of the token
    /// @param meta The meta URI of the token
    event TokenCreated(
        uint256 ts, address creator, uint256 nonce, address token, string name, string symbol, string meta
    );

    /// @notice emitted when a token is bought
    ///
    /// @param ts The timestamp of the event
    /// @param token  The address of the token
    /// @param buyer  The address of the buyer
    /// @param amount  The amount of tokens bought
    /// @param eth  The amount of ETH spent
    /// @param fee The amount of ETH spent on fee
    /// @param postPrice The price of the token after this trade
    event TokenBought(
        uint256 ts, address token, address buyer, uint256 amount, uint256 eth, uint256 fee, uint256 postPrice
    );

    /// @notice emitted when a token is sold
    ///
    /// @param ts The timestamp of the event
    /// @param token  The address of the token
    /// @param seller  The address of the seller
    /// @param amount  The amount of tokens sold
    /// @param eth  The amount of ETH received
    /// @param fee  The amount of ETH deducted as a fee
    /// @param postPrice The price of the token after this trade
    event TokenSold(
        uint256 ts, address token, address seller, uint256 amount, uint256 eth, uint256 fee, uint256 postPrice
    );

    /// emitted when a token's curve is set
    /// @param token The address of the token
    /// @param curve The address of the curve
    /// @param curveParameter The parameter of the curve
    event TokenCurveSet(address token, address curve, uint256 curveParameter);

    /// @notice emitted when a token's curve parameters are set (V2)
    /// @param token The address of the token
    /// @param r The virtual ETH reserve parameter
    /// @param h The virtual token reserve parameter
    /// @param k The square of the virtual Liquidity parameter
    event TokenCurveSetV2(address token, uint256 r, uint256 h, uint256 k);

    /// emitted when a token's dexSupplyThresh is set
    /// @param token The address of the token
    /// @param dexSupplyThresh The new dexSupplyThresh of the token
    event TokenDexSupplyThreshSet(address token, uint256 dexSupplyThresh);

    /// emitted when a token's implementation is set
    /// @param token The address of the token
    /// @param version The version of the token
    event TokenVersionSet(address token, TokenVersion version);

    /// @notice emitted when a new vanity token is created
    /// @param token The address of the created token
    /// @param creator The address of the creator
    /// @param beneficiary The address of the beneficiary
    event VanityTokenCreated(address token, address creator, address beneficiary);

    /// @notice emitted when a token's quote token is set
    /// @param token The address of the token
    /// @param quoteToken The address of the quote token
    event TokenQuoteSet(address token, address quoteToken);

    /// @notice emitted when a token's migrator is set
    /// @param token The address of the token
    /// @param migratorType The migrator type
    event TokenMigratorSet(address token, MigratorType migratorType);

    /// @notice emitted when a token's extension is enabled
    /// @param token The address of the token
    /// @param extensionID The extension ID
    /// @param extensionAddress The address of the extension contract
    /// @param version The version of the extension
    event TokenExtensionEnabled(address token, bytes32 extensionID, address extensionAddress, uint8 version);

    /// @notice emitted when a token's pool info is updated
    /// @param token The address of the token
    /// @param poolInfo The new pool information
    event TokenPoolInfoUpdated(address token, PackedDexPool poolInfo);

    /// @notice emitted when a trader's fee exemption status is updated
    /// @param trader The address of the trader
    /// @param isExempted Whether the trader is exempted from fees
    event FeeExemptionUpdated(address indexed trader, bool isExempted);

    //
    // events
    //

    /// @notice emitted when token is redeemed
    /// @param ts The timestamp of the event
    /// @param srcToken The address of the token to redeem
    /// @param dstToken The address of the token to receive
    /// @param srcAmount The amount of srcToken to redeem
    /// @param dstAmount The amount of dstToken to receive
    /// @param who The address of the redeemer
    event TokenRedeemed(
        uint256 ts, address srcToken, address dstToken, uint256 srcAmount, uint256 dstAmount, address who
    );

    /// @notice emitted when the bit flags are changed
    /// @param oldFlags The old flags
    /// @param newFlags The new flags
    event BitFlagsChanged(uint256 oldFlags, uint256 newFlags);

    /// @notice emitted when adding liquidity to DEX
    /// @param token The address of the token
    /// @param pool The address of the pool
    /// @param amount The amount of token added
    /// @param eth The amount of quote Token added
    event LaunchedToDEX(address token, address pool, uint256 amount, uint256 eth);

    /// @notice Emitted when V4/PCS LP fees are collected and forwarded
    event V4LPFeesCollected(address indexed token, uint256 quoteAmount, uint256 tokenAmount);

    /// @notice emitted when a new CL pool is created for a token
    /// @param token The address of the token
    /// @param poolId The ID of the created CL pool
    /// @param sqrtPriceX96 The initial sqrt price used to initialize the CL pool
    event FlapTokenCLPoolCreated(address token, bytes32 poolId, uint160 sqrtPriceX96);

    /// @notice emitted when the progress of a token changes
    /// @param token The address of the token
    /// @param newProgress The new progress value in Wad
    event FlapTokenProgressChanged(address token, uint256 newProgress);

    //
    // Token V2 supply change
    //

    /// @notice emitted when the circulating supply of a token changes
    /// @param token The address of the token
    /// @param newSupply The new circulating supply
    event FlapTokenCirculatingSupplyChanged(address token, uint256 newSupply);

    /// @notice emitted when a new tax is set for a token
    /// @dev For V3 tokens with asymmetric rates, this carries max(buyTax, sellTax) for backward compatibility.
    ///      Listen for FlapTokenAsymmetricTaxSet to get the full buy/sell split.
    /// @param token The address of the token
    /// @param tax The tax value set for the token (max of buy and sell rates for V3 tokens)
    event FlapTokenTaxSet(address token, uint256 tax);

    /// @notice Emitted when a token is launched with asymmetric buy/sell tax rates (V3 tokens only).
    /// @dev Emitted in addition to FlapTokenTaxSet (which carries max(buyTax, sellTax) for backward
    ///      compatibility). New systems that support asymmetric rates should listen to this event.
    ///      Old systems that only read FlapTokenTaxSet will continue to work correctly.
    /// @param token The address of the token
    /// @param buyTax The buy tax rate in basis points
    /// @param sellTax The sell tax rate in basis points
    event FlapTokenAsymmetricTaxSet(address token, uint256 buyTax, uint256 sellTax);

    // operation related
    // should remove later

    /// @notice emitted when a users successfully checked in
    /// @param user The address of the user
    event CheckedIn(address user);

    /// @notice emitted when a beneficiary claims fees
    /// @param token The address of the token
    /// @param beneficiary The address of the beneficiary
    /// @param tokenAmount The amount of the token claimed
    /// @param ethAmount The amount of ETH claimed
    event BeneficiaryClaimed(address token, address beneficiary, uint256 tokenAmount, uint256 ethAmount);

    /// @notice emitted when a token beneficiary is changed
    /// @param token The address of the token
    /// @param oldBeneficiary The previous beneficiary address
    /// @param newBeneficiary The new beneficiary address
    event BeneficiaryChanged(address token, address oldBeneficiary, address newBeneficiary);

    /// @notice emitted when an extension is registered
    /// @param extensionId The unique identifier for the extension
    /// @param extensionAddress The address of the extension contract
    /// @param version The version of the extension
    event ExtensionRegistered(bytes32 extensionId, address extensionAddress, uint8 version);

    /// @notice emitted when a spammer's blocked status is changed
    /// @param spammer The address of the spammer
    /// @param blocked True if blocked, false if unblocked
    event SpammerBlockedStatusChanged(address spammer, bool blocked);

    /// @notice emitted when a user is rate limited from creating tokens
    /// @param user The address of the rate-limited user
    /// @param lastCreationTime The timestamp of their last successful token creation
    event RateLimited(address user, uint256 lastCreationTime);

    /// @notice emitted when a quote token configuration is set
    /// @param quoteToken The address of the quote token
    /// @param config The configuration set for the quote token
    event QuoteTokenConfigurationSet(address quoteToken, QuoteTokenConfiguration config);

    /// @notice emitted when a V3 favored fee is set for a quote token
    /// @param quoteToken The address of the quote token
    /// @param favoredFee The favored fee set for the quote token
    event V3FavoredFeeSet(address quoteToken, uint24 favoredFee);

    /// @notice emitted when a token's DEX preference is set
    /// @param token The address of the token
    /// @param dexId The preferred DEX ID for the token
    /// @param lpFeeProfile The preferred V3 LP fee profile for the token
    event TokenDexPreferenceSet(address token, DEXId dexId, V3LPFeeProfile lpFeeProfile);

    /// @notice emitted when a message is sent
    /// @param sender The address of the sender
    /// @param token The address of the token
    /// @param message The message sent
    event MsgSent(address sender, address token, string message);

    //
    // Custom Errors
    //

    /// @notice error if the dex is both pancake and algebra1.9
    ///         which is impossible
    error DEXCannotBeBothPancakeAndAlgebra1_9();

    /// @notice error if the portal lens address is zero
    error PortalLensCannotBeZero();

    /// @notice error if the portal lens v2 address is zero
    error PortalLensV2CannotBeZero();

    /// @notice error if the multi dex router address is zero
    error MultiDexRouterCannotBeZero();

    /// @notice error if the token does not exist
    error TokenNotFound(address token);

    /// @notice error if the amount is too small
    error AmountTooSmall(uint256 amount);

    /// @notice error if slippage is too high
    /// i.e: actualAmount < minAmount
    error SlippageTooHigh(uint256 actualAmount, uint256 minAmount);

    /// @notice error if the input token & output token of a swap is the same
    error SameToken(address tokenA);

    /// @notice error if trying to trade a killed token
    error TokenKilled(address token);

    /// @notice error if token is not tradable
    error TokenNotTradable(address token);

    /// @notice error if trying to sell a token that is in a battle
    error TokenInDuel(address token);

    /// @notice error if trying to redeem a token that is not killed
    error TokenNotKilled(address token);

    /// @notice error if the token has already been added to the DEX
    error TokenAlreadyDEXed(address token);

    /// @notice error if the token has already been staged or created
    error TokenAlreadyStaged(address token);

    /// @notice error if the token is not in staged status
    error TokenNotStaged(address token);

    /// @notice error if the token is not listed on DEX yet
    error TokenNotDEXed(address token);

    /// @notice error if there is no conversion path from srcToken to dstToken
    error NoConversionPath(address srcToken, address dstToken);

    /// @notice error if the round is not found
    error RoundNotFound(uint256 id);

    /// @notice error if the round id is invalid
    error InvalidRoundID(uint256 id);

    /// @notice error if try to start a new round but the last round is not resolved
    error LastRoundNotResolved();

    /// @notice cannot use a token for the next round of the game
    error InvalidTokenForBattle(address token);

    /// @notice error if the signature is invalid
    error InvalidSigner(address signer);

    /// @notice error if the seq is not found in Game queue
    error SeqNotFound(uint256 seq);

    /// @notice error if not implemented yet
    error NotImplemented();

    /// @notice error a token is already in the game
    error TokenAlreadyInGame(address token);

    /// @notice error if a call reverted but without any data
    error CallReverted();

    /// @notice error if creating token is disabled
    error PermissionlessCreateDisabled();

    /// @notice error if trading is disabled
    error TradeDisabled();

    /// @notice error if the circuit breakers are off
    error ProtocolDisabled();

    /// @notice error if the game supply threshold is not valid
    error InvalidGameSupplyThreshold();

    /// @notice error if the dex supply threshold is not valid
    error InvalidDEXSupplyThreshold();

    /// @notice error if the proof does not match the msg.sender
    error MismatchedAddressInProof(address expected, address actual);

    /// @notice error if the whitlist creator cannot create more tokens
    error NoQuotaForCreator(uint256 created, uint256 max);

    /// @notice error if the piggyback lenght is not valid
    error InvalidPiggybackLength(uint256 expected, uint256 actual);

    /// @notice error if the tax processor is not set for UniV4 related operations
    error TaxProcessorUniV4Required();

    /// @notice error if the feature is disabled
    error FeatureDisabled();

    /// @notice error if caller is not authorized (e.g., only SaleForge can call)
    error OnlySaleForge();

    /// @notice error if the quote token is not allowed
    error QuoteTokenNotAllowed(address quoteToken);

    /// @notice error if a native to quote swap is required but not supported
    /// native to quote swap: i.e, swap the input token to the desired quote token
    error NativeToQuoteSwapNotSupported();

    /// @notice error if the native to quote swap v3 fee type is not supported
    /// @param NativeToQuoteSwapType The unsupported native to quote swap type
    error NativeToQuoteSwapFeeTierNotSupported(uint8 NativeToQuoteSwapType);

    /// @notice error if met any dirty bits
    error DirtyBits();

    //
    // Dex Related
    //

    /// @notice error if sqrPriceA is gte than sqrtPriceB
    error PriceAMustLTPriceB(uint160 sqrtPriceA, uint160 sqrtPriceB);

    /// @notice error if the actual amount is more than the expected amount
    error ActualAmountMustLTEAmount(uint256 actualAmount, uint256 amount1);

    /// @notice error if the msg.sender is not a Uniswap V3 pool
    error NotUniswapV3Pool(address sender);

    /// @notice error if the uniswap v2 pool's liquidity is not zero
    error UniswapV2PoolNotZero(address pool, uint256 liquidity);

    /// @notice error if the required token amount for adding Uniswap v2 liquidity is more than the remaining token
    error RequiredTokenMustLTE(uint256 requiredToken, uint256 reserveToken);

    /// @notice revert when calling slot0 of a Uniswap V3 pool failed
    error UniswapV3Slot0Failed();

    /// @notice error if a non-position NFT is received
    error NonPositionNFTReceived(address collection);

    /// @notice error if the provided dex threshold is invalid
    error InvalidDexThreshold(uint256 threshold);

    /// @notice error if the provided address is not a valid pool
    error InvalidPoolAddress(address pool, address expected);

    //
    // staking related
    //

    /// @notice error if the locks are invalid
    error InvalidLocks();

    /// @notice error if staking feature is not enabled
    error StakingDisabled();

    /// @notice error if the operator does not have the roller role
    error NotRoller();

    // operation related

    /// @notice error if the user cannot check in yet
    /// @param next The timestamp when the user can check in again
    error cannotCheckInUntil(uint256 next);

    // misc

    /// @notice error if another token has the same meta
    error MetaAlreadyUsedByOtherToken(string meta);

    /// @notice error if the creation fee is insufficient
    error InsufficientCreationFee(uint256 required, uint256 provided);

    /// @notice error if the provided ETH is insufficient to cover the required fee
    /// @param required The required fee amount
    /// @param provided The provided ETH amount
    error InsufficientFee(uint256 required, uint256 provided);

    /// @notice error if the vanity address requirement is not met
    /// @param token The generated token address
    error VanityAddressRequirementNotMet(address token);

    /// @notice error if the token is not in DEX status
    error TokenNotInDEXStatus(address token);

    /// @notice error if the caller is not the token's beneficiary
    error CallerNotBeneficiary(address caller, address expected);

    /// @notice error if no locks are available for the token
    error NoLocksAvailable(address token);

    /// @notice error if the provided ETH is insufficient to cover the required input amount
    /// @param provided The provided ETH amount
    /// @param required The required ETH amount
    error InsufficientEth(uint256 provided, uint256 required);

    /// @notice error if the provided tax bps is invalid
    /// @param tax The provided tax bps
    error InvalidTaxBps(uint256 tax);

    /// @notice error if the transferFrom call failed
    /// @param token The address of the token
    /// @param from The address from which the tokens were to be transferred
    /// @param amount The amount of tokens that were to be transferred
    error TransferFromFailed(address token, address from, uint256 amount);

    /// @notice error if the migrator type is invalid
    error InvalidMigratorType();

    /// @notice error if the quote token is not native but not using PortalTradeV2
    error QuoteTokenNotNativeButNotUsingTradeV2();

    /// @notice error if the msg.value is less than expected value when creating
    /// a tax token using an ERC20 (e.g: USDC) token as the quote token.
    /// @dev For the tax token's tax splitter to work properly, we need approximately 1gwei due to our
    /// implemenation of the tax splitter.
    error InsufficientValueForTaxTokenCreation(uint256 expected, uint256 provided);

    /// @notice error if the caller is not a guardian or admin
    /// @param caller The address of the caller
    error NotGuardian(address caller);

    /// @notice error if the extension version is not supported
    /// @param version The unsupported extension version
    error UnsupportedExtensionVersion(uint8 version);

    /// @notice error if the token uses an extension but is traded through the legacy PortalTrade contract
    /// @param token The address of the token with an extension
    error TokenWithExtensionNotSupported(address token);

    /// @notice error if the parameters for ToshiMart are invalid
    error InvalidParamsForToshiMart();

    /// @notice error if the parameters for X_LAYER are invalid
    error InvalidParamsForXLayer();

    /// @notice error if the quote token configuration is invalid
    error InvalidQuoteTokenConfiguration();

    /// @notice error when trying to use PortalTrade with tokens that have non-zero h parameter
    error ErrShouldUsePortalTradeV2();

    /// @notice error when no supported DEX is found for the token
    error NoSupportedDEX();

    /// @notice invalid fee tier for DEX
    error InvalidFeeTierForDEX();

    /// @notice error if the tax distribution percentages don't add up to 100%
    error InvalidTaxDistribution();

    /// @notice error if tax duration exceeds maximum allowed
    error TaxDurationTooLong();

    /// @notice error if tax duration is less than minimum allowed
    error TaxDurationTooShort();

    /// @notice error if anti-farmer duration exceeds maximum allowed
    error AntiFarmerDurationTooLong();

    /// @notice error when the swap from quoteToken to dividendToken is not supported by the SwapRegistry
    error DividendSwapNotSupported(address quoteToken, address dividendToken);

    /// @notice error if minimum share balance is too low for dividend eligibility
    error MinimumShareBalanceTooLow();

    /// @notice error when dividend parameters are missing but required
    error DividendParametersRequired();

    /// @notice error when `dividendBps > 0` is configured together with a dividend token that is
    ///         not the quote token. Emitted by `launchTaxTokenV3` while live quote→custom-token
    ///         dividend conversion is not yet enabled. Custom dividend tokens remain permitted
    ///         when `dividendBps == 0` (tracker-only mode).
    error DividendTokenMustEqualQuoteToken();

    /// @notice error when a non-NONE FeeConfig slot has bps == 0
    error InvalidFeeConfigBps();

    /// @notice error when a FeeConfig slot type appears more than once in the feeConfigs array
    /// @param feeType The uint8 value of the duplicated FeeType enum entry
    error DuplicateFeeSlot(uint8 feeType);

    /// @notice error when a MARKETING_OR_VAULT fee slot has a zero marketing/vault address
    error ZeroMarketingAddress();

    /// @notice error when a newTokenV7 request violates the currently supported launch rules.
    /// @dev Detailed code mapping is documented in `src/PortalTokenLauncher.sol`.
    error NewTokenV7RuleViolation(uint8 code);

    /// @notice error when TOKEN_V3_PERMIT via newTokenV7 is configured with a fee type
    ///         that is not enabled for the initial V7 release.
    /// @param feeType The unsupported FeeType enum value.
    error UnsupportedV7TokenV3PermitFeeType(uint8 feeType);

    /// @notice error when user is rate limited from creating tokens
    /// @param user The address that is rate limited
    /// @param lastCreationTime The timestamp of the user's last successful token creation
    error RateLimitExceeded(address user, uint256 lastCreationTime);

    /// @notice error when user is permanently blocked from creating tokens
    /// @param user The address that is blocked
    error SpammerBlocked(address user);

    // --- Salt Locking ---

    /// @notice Emitted when a salt is locked by a user.
    /// @param locker       The address that paid to lock the salt.
    /// @param salt         The CREATE2 salt that was locked.
    /// @param tokenAddress The predicted token address derived from salt + tokenVersion.
    /// @param tokenVersion The TokenVersion enum value the lock applies to.
    /// @param ts           Block timestamp of the lock.
    event FlapSaltLocked(address locker, bytes32 salt, address tokenAddress, uint8 tokenVersion, uint256 ts);

    /// @notice Emitted when a locked salt is consumed by a successful launch.
    /// @param locker       The address that originally locked the salt.
    /// @param salt         The CREATE2 salt that was used.
    /// @param tokenAddress The token address that was launched.
    /// @param tokenVersion The TokenVersion enum value that was locked.
    /// @param ts           Block timestamp of the launch.
    event FlapSaltUsed(address locker, bytes32 salt, address tokenAddress, uint8 tokenVersion, uint256 ts);

    /// @notice msg.value does not equal the required SALT_LOCK_FEE.
    error SaltLockFeeMismatch(uint256 required, uint256 provided);

    /// @notice The salt is already locked by a different address.
    error SaltAlreadyLockedByAnotherUser(bytes32 salt, address existingLocker);

    /// @notice The caller tried to lock a salt they already hold the lock for.
    error SaltAlreadyLockedBySelf(bytes32 salt);

    /// @notice Launch was rejected because the salt is locked by someone other than the caller.
    error FlapSaltLockedByAnotherUser(bytes32 salt, address locker);

    /// @notice Launch was rejected because the locked tokenVersion does not match the one being launched.
    error SaltLockTokenVersionMismatch(bytes32 salt, TokenVersion locked, TokenVersion provided);

    /// @notice The tokenVersion passed to lockSalt() is not currently supported.
    error UnsupportedTokenVersion(TokenVersion provided);
}

/// @notice Callback interface implemented by VaultPortal (or any trusted caller).
///         Portal calls getSaltOwner() when msg.sender == VAULT_PORTAL to resolve the
///         effective user for salt-lock enforcement without changing any parameter structs.
interface ISaltOwnerProvider {
    /// @notice Return the address that should be treated as the salt owner for the current launch.
    /// @dev VaultPortal must set _pendingSaltOwner before delegating into Portal.
    ///      The call is a staticcall so it cannot modify state.
    function getSaltOwner() external view returns (address);
}

/// @title IPortalLauncherTwoStep
/// @notice Interface for the two-step token launch process
interface IPortalLauncherTwoStep is IPortalTypes {
    /// @notice Stage a new token (V5) without creating it yet
    /// @param params The immutable parameters for the new token
    /// @return token The predetermined address of the token
    /// @dev This stages a token by validating parameters and reserving the token address.
    ///      The token contract is not deployed yet. Call commitNewTokenV5 to actually deploy it.
    ///      Extensions are not supported in two-step launch. LP fee profile is always STANDARD.
    /// FIXME: this is not enabled in this version. Will be available once the audit for this part is ready.
    function stageNewTokenV5(StageNewTokenV5Params calldata params) external returns (address token);

    /// @notice Commit a staged token (V5) and create it
    /// @param params The parameters for token deployment (includes salt to identify staged token)
    /// @dev This deploys the token contract and initializes it. The token must have been staged first via stageNewTokenV5.
    ///      The salt and isTaxToken in params must match what was used during staging.
    ///      If taxRate > 0, creates FlapTaxTokenV2, otherwise creates regular token.
    /// FIXME: this is not enabled in this version. Will be available once the audit for this part is ready.
    function commitNewTokenV5(CommitNewTokenV5Params calldata params) external payable;
}

/// @notice Handles token creation and related operations
interface IPortalLauncher is IPortalTypes {
    /// @notice Create a new token (V2) with flexible parameters
    /// @param params The parameters for the new token
    /// @return token The address of the created
    /// @dev due to the implementation limit, when creating a tax token and using an ERC20 token as the quote token,
    /// You need to pay an extra 1gwei native gas token (i.e msg.value = 1 gwei), or you will encounter an InsufficientValueForTaxTokenCreation error
    function newTokenV2(NewTokenV2Params calldata params) external payable returns (address token);

    /// @notice Create a new token (V3) with extension support
    /// @param params The parameters for the new token including extension configuration
    /// @return token The address of the created token
    /// @dev Similar to newTokenV2 but with extension support. Extension hooks will be called if extensionID is non-zero
    function newTokenV3(NewTokenV3Params calldata params) external payable returns (address token);

    /// @notice Create a new token (V4) with DEX ID and LP fee profile support
    /// @param params The parameters for the new token including DEX ID and LP fee profile
    /// @return token The address of the created token
    /// @dev Similar to newTokenV3 but with DEX ID and LP fee profile support. Allows specifying preferred DEX and fee tier
    function newTokenV4(NewTokenV4Params calldata params) external payable returns (address token);

    /// @notice Create a new token (V5) with tax V2 support
    /// @param params The parameters for the new token including advanced tax features
    /// @return token The address of the created token
    /// @dev Similar to newTokenV4 but with support for FlapTaxTokenV2 when taxRate > 0.
    ///      When taxRate is 0, behaves like newTokenV4 (uses regular token or FlapTaxToken).
    ///      When taxRate > 0, creates a FlapTaxTokenV2 with advanced tax distribution features.
    /// FIXME: this is not enabled in this version. Will be available once the audit for this part is ready.
    function newTokenV5(NewTokenV5Params calldata params) external payable returns (address token);

    /// @notice Create a new token (V6) — unified entry point for all token versions.
    /// @param params The parameters for the new token (see NewTokenV6Params).
    /// @return token The address of the created token.
    ///
    /// @dev Dispatch logic based on `params.tokenVersion`:
    ///
    ///   TOKEN_V2_PERMIT (non-tax token):
    ///     - buyTaxRate and sellTaxRate MUST both be 0.
    ///     - commissionReceiver MUST be address(0).
    ///     - Dispatched to PortalLauncherV5 → creates a standard ERC-20 (TOKEN_V2_PERMIT).
    ///
    ///   TOKEN_TAXED (V1 tax token):
    ///     - At least one tax rate must be > 0.
    ///     - buyTaxRate MUST equal sellTaxRate (symmetric rates only).
    ///     - mktBps MUST be 10000 (all tax goes to beneficiary after protocol fee).
    ///     - dividendBps MUST be 0; deflationBps and lpBps MUST be 0.
    ///     - commissionReceiver MUST be address(0) (commission not supported).
    ///     - Dispatched to PortalLauncherV5Tax → creates a FlapTaxToken (TOKEN_TAXED).
    ///
    ///   TOKEN_TAXED_V2 (V2 tax token):
    ///     - At least one tax rate must be > 0.
    ///     - buyTaxRate MUST equal sellTaxRate (symmetric rates only via V5 path).
    ///     - mktBps MUST NOT be 10000 (use TOKEN_TAXED for that case).
    ///     - mktBps + deflationBps + dividendBps + lpBps MUST equal 10000.
    ///     - commissionReceiver MUST be address(0) (commission not supported).
    ///     - Dispatched to PortalLauncherV5Tax → creates a FlapTaxTokenV2 (TOKEN_TAXED_V2).
    ///
    ///   TOKEN_TAXED_V3 (V3 tax token):
    ///     - At least one tax rate must be > 0.
    ///     - Asymmetric rates allowed (buyTaxRate != sellTaxRate is OK).
    ///     - mktBps + deflationBps + dividendBps + lpBps MUST equal 10000.
    ///     - mktBps == 10000 IS allowed (all tax → protocol fee + commission, no marketing).
    ///     - commissionReceiver CAN be non-zero (commission supported).
    ///     - Dispatched to PortalLauncherTaxV3.launchTaxTokenV3 → creates a FlapTaxTokenV3 (TOKEN_TAXED_V3).
    ///
    /// Emits FlapTokenTaxSet (with max rate) AND FlapTokenAsymmetricTaxSet for tax tokens.
    function newTokenV6(NewTokenV6Params calldata params) external payable returns (address token);

    /// @notice Reserve the token address derived from `salt` by paying SALT_LOCK_FEE.
    /// @dev    The caller must pass the `tokenVersion` they intend to lock for.
    ///         Accepted values: TOKEN_V2_PERMIT (2) for non-tax tokens (NON_TAX_TOKEN_SUFFIX
    ///         vanity requirement, typically 0x8888) and TOKEN_TAXED_V3 (6) for tax tokens
    ///         (TAX_TOKEN_SUFFIX vanity requirement, typically 0x7777).  Any other value
    ///         reverts with UnsupportedTokenVersion.
    ///         Fee is forwarded to FEE_RECEIVER.  Emits FlapSaltLocked.
    /// @param salt         CREATE2 salt to reserve.
    /// @param tokenVersion Token version to lock for. Must be TOKEN_V2_PERMIT or TOKEN_TAXED_V3.
    function lockSalt(bytes32 salt, TokenVersion tokenVersion) external payable;

    /// @notice Launch a new token with V4/PCS Infinity migration support
    /// @param params The V7 token parameters
    /// @return token The created token address
    function newTokenV7(NewTokenV7Params calldata params) external payable returns (address token);
}

/// @title Portal Lens Interface
/// @notice Handles read-only token state queries
interface IPortalLens is IPortalTypes {
    /// @notice Get token state
    /// @param token  The address of the token
    /// @return state  The state of the token
    function getTokenV2(address token) external view returns (TokenStateV2 memory state);
    /// @notice Get token state (V3)
    /// @param token  The address of the token
    /// @return state  The state of the token (V3)
    function getTokenV3(address token) external view returns (TokenStateV3 memory state);
    /// @notice Get token state (V4)
    /// @param token  The address of the token
    /// @return state  The state of the token (V4) with only 'r' curve parameter
    function getTokenV4(address token) external view returns (TokenStateV4 memory state);
    /// @notice Get token state (V5)
    /// @param token  The address of the token
    /// @return state  The state of the token (V5) with all curve parameters (r, h, k)
    function getTokenV5(address token) external view returns (TokenStateV5 memory state);
    /// @notice Get token state (V6)
    /// @param token  The address of the token
    /// @return state  The state of the token (V6) with all V5 fields plus taxRate, pool, and progress
    function getTokenV6(address token) external view returns (TokenStateV6 memory state);
    /// @notice Get token state (V7)
    /// @param token  The address of the token
    /// @return state  The state of the token (V7) with all V6 fields plus lpFeeProfile
    function getTokenV7(address token) external view returns (TokenStateV7 memory state);
    /// @notice Get token state (V8)
    /// @param token  The address of the token
    /// @return state  The state of the token (V8) with asymmetric buyTaxRate and sellTaxRate
    function getTokenV8(address token) external view returns (TokenStateV8 memory state);
    /// @notice Get token state (V8Safe)
    /// @dev Returns enum-typed fields (TokenStatus, TokenVersion, V3LPFeeProfile, DEXId) as uint8
    ///      instead of their Solidity enum types, preventing ABI-decoding reverts when new enum
    ///      variants are introduced. Use this method when you need forward/backward compatibility
    ///      with future contract upgrades that may add new enum values.
    /// @param token  The address of the token
    /// @return state  The state of the token with enum fields encoded as uint8
    function getTokenV8Safe(address token) external view returns (TokenStateV8Safe memory state);
    /// @notice Get the quote token configuration for a given quote token address
    /// @param quoteToken The address of the quote token
    /// @return config The configuration of the quote token
    function getQuoteTokenConfiguration(address quoteToken)
    external
    view
    returns (QuoteTokenConfiguration memory config);
}

interface IPortalLensV2 is IPortalTypes {

    /// @notice Get the salt lock entry for a given CREATE2 salt.
    /// @param salt The CREATE2 salt to query.
    /// @return entry The SaltLockEntry (locker == address(0) means unlocked).
    function getSaltLock(bytes32 salt) external view returns (SaltLockEntry memory entry);

    /// @notice Get the total number of salts locked by a user for a specific token version in the on-chain index.
    /// @dev Only locks made from v5.11.0 onwards are present; pre-upgrade locks are not indexed.
    /// @param user         The address of the user.
    /// @param tokenVersion The TokenVersion (as uint8) to filter on.
    /// @return count The number of salts in the on-chain index for this user and version.
    function getLockedSaltsCountByUserAndVersion(address user, uint8 tokenVersion) external view returns (uint256 count);

    /// @notice Paginated enumeration of salts locked by a user for a specific token version.
    /// @dev Only locks made from v5.11.0 onwards are present; pre-upgrade locks are not indexed.
    ///      `limit` is capped at 100.
    /// @param user         The address of the user.
    /// @param tokenVersion The TokenVersion (as uint8) to filter on.
    /// @param offset       Zero-based start index within the user+version set.
    /// @param limit        Maximum number of entries to return (capped at 100).
    /// @return salts   The salt values in [offset, offset+limit).
    /// @return entries The corresponding SaltLockEntry structs.
    /// @return total   The total number of salts in the on-chain index for this user and version.
    function getLockedSaltsByUserAndVersion(address user, uint8 tokenVersion, uint256 offset, uint256 limit)
    external
    view
    returns (bytes32[] memory salts, SaltLockEntry[] memory entries, uint256 total);
}

/// @title IPortalTrade Interface
/// @notice Handles token trading and redemption
interface IPortalTrade is IPortalTypes {
    /// @notice Buy token with ETH on creation
    /// @param token  The address of the token to buy
    /// @param recipient  The address to send the token to
    /// @param inputAmount The amount of ETH to spend
    ///
    /// @dev  This function is mainly for internal use (be delegated called from the portal contract)
    ///       The msg.value can be greater than inputAmount, the excess ETH will not be
    ///       refunded to the caller. They will be charged as a fee.
    ///
    ///       Note: the slippage is not checked in this function.
    ///
    function buyOnCreation(address token, address recipient, uint256 inputAmount)
    external
    payable
    returns (uint256 amount);

    /// @notice Buy token with ETH
    /// @param token  The address of the token to buy
    /// @param recipient  The address to send the token to
    /// @param minAmount  The minimum amount of tokens to buy
    function buy(address token, address recipient, uint256 minAmount) external payable returns (uint256 amount);

    /// @notice Sell token for ETH
    /// @param token  The address of the token to sell
    /// @param amount The amount of tokens to sell
    /// @param minEth The minimum amount of ETH to receive
    function sell(address token, uint256 amount, uint256 minEth) external returns (uint256 eth);

    /// @notice Redeem a killed token for another token
    /// @param srcToken The address of the token to redeem
    /// @param dstToken The address of the token to receive
    /// @param srcAmount The amount of srcToken to redeem
    /// @return dstAmount The amount of dstToken to receive
    function redeem(address srcToken, address dstToken, uint256 srcAmount) external returns (uint256 dstAmount);

    /// @notice Preview the amount of tokens to buy with ETH
    /// @param token  The address of the token to buy
    /// @param eth  The amount of ETH to spend
    /// @return amount  The amount of tokens to buy
    function previewBuy(address token, uint256 eth) external view returns (uint256 amount);

    /// @notice Preview the amount of ETH to receive for selling tokens
    /// @param token  The address of the token to sell
    /// @param amount  The amount of tokens to sell
    /// @return eth  The amount of ETH to receive
    function previewSell(address token, uint256 amount) external view returns (uint256 eth);

    /// @notice Preview redeem
    /// @param srcToken The address of the token to redeem
    /// @param dstToken The address of the token to receive
    /// @param srcAmount The amount of srcToken to redeem
    /// @return dstAmount The amount of dstToken to receive
    function previewRedeem(address srcToken, address dstToken, uint256 srcAmount)
    external
    view
    returns (uint256 dstAmount);
}

/// @title IPortalTradeV2 Interface
/// @notice Handles unified token swaps and quoting
interface IPortalTradeV2 is IPortalTypes {
    /// @notice Emitted when tax is paid on bonding curve for TAX_TOKEN
    /// @param token The address of the token
    /// @param amount The amount of tax paid
    event TaxOnBondingCurvePaid(address indexed token, uint256 amount);

    /// @notice Emitted when tax is paid on bonding curve for TAX_TOKEN_V2
    /// @param token The address of the token
    /// @param amount The amount of tax paid
    event TaxV2OnBondingCurvePaid(address indexed token, uint256 amount);

    /// @notice Parameters for swapping exact input amount for output token
    struct ExactInputParams {
        /// @notice The address of the input token (use address(0) for native asset)
        address inputToken;
        /// @notice The address of the output token (use address(0) for native asset)
        address outputToken;
        /// @notice The amount of input token to swap (in input token decimals)
        uint256 inputAmount;
        /// @notice The minimum amount of output token to receive
        uint256 minOutputAmount;
        /// @notice Optional permit data for the input token (can be empty)
        bytes permitData;
    }

    /// @notice Parameters for swapping exact input amount for output token (V3) with extension support
    struct ExactInputV3Params {
        /// @notice The address of the input token (use address(0) for native asset)
        address inputToken;
        /// @notice The address of the output token (use address(0) for native asset)
        address outputToken;
        /// @notice The amount of input token to swap (in input token decimals)
        uint256 inputAmount;
        /// @notice The minimum amount of output token to receive
        uint256 minOutputAmount;
        /// @notice Optional permit data for the input token (can be empty)
        bytes permitData;
        /// @notice Additional extension specific data to be passed to the extension's `onTrade` method, check the extension's documentation for details on the expected format and content
        bytes extensionData;
    }

    /// @notice Parameters for quoting the output amount for a given input
    struct QuoteExactInputParams {
        /// @notice The address of the input token (use address(0) for native asset)
        address inputToken;
        /// @notice The address of the output token (use address(0) for native asset)
        address outputToken;
        /// @notice The amount of input token to swap (in input token decimals)
        uint256 inputAmount;
    }
    /// @notice Swap exact input amount for output token
    /// @param params The swap parameters
    /// @return outputAmount The amount of output token received
    /// @dev Here are some possible scenarios:
    ///   If the token's reserve is BNB or ETH (i.e: the quote token is the native gas token):
    ///      - BUY: input token is address(0), output token is the token address
    ///      - SELL: input token is the token address, output token is address(0)
    ///   If the token's reserve is another ERC20 token (eg. USD*, i.e, the quote token is an ERC20 token):
    ///      - BUY with USD*: input token is the USD* address, output token is the token address
    ///      - SELL for USD*: input token is the token address, output token is the USD* address
    ///      - BUY with BNB or ETH: input token is address(0), output token is the token address.
    ///        (Note: this requires an internal swap to convert BNB/ETH to USD*, nativeToQuoteSwap must be anabled for this quote token)
    /// Note: Currently, this method supports trading tokens that is either still on the bonding curve or already listed on DEX.

    function swapExactInput(ExactInputParams calldata params) external payable returns (uint256 outputAmount);

    /// @notice Swap exact input amount for output token (V3) with extension support
    /// @param params The swap parameters including extension data
    /// @return outputAmount The amount of output token received
    /// @dev Similar to swapExactInput but with extension support. Extension hooks will be called if the token uses an extension
    function swapExactInputV3(ExactInputV3Params calldata params) external payable returns (uint256 outputAmount);

    /// @notice Quote the output amount for a given input
    /// @param params The quote parameters
    /// @return outputAmount The quoted output amount
    /// @dev refer to the swapExactInput method for the scenarios
    function quoteExactInput(QuoteExactInputParams calldata params) external returns (uint256 outputAmount);
}

interface IV4Locker {
    /// @notice Collect V4/PCS Infinity LP fees for a token and distribute via TaxProcessor
    /// @dev Can be called by anyone (permissionless keeper). The Locker's collectAddress is
    ///      set to Portal, so fees always flow through Portal → TaxProcessor → distribution.
    /// @param token The token whose LP fees to collect
    function collectV4Fees(address token) external;

    /// @notice Add liquidity to locked V4/PCS Infinity LP positions for a token.
    /// @dev Called by TaxProcessor to reinvest LP-share tax revenue.
    ///      Tokens must be transferred to Portal before calling this function.
    ///      Portal (as lock owner) calls increaseLiquidity on the GoPlus/UNCX locker
    ///      for both the quote-only (lower) and token-only (upper) positions.
    /// @param token        The protocol token address
    /// @param tokenAmount  Amount of protocol token available for LP
    /// @param quoteAmount  Amount of quote token available for LP
    /// @return actualTokenUsed Total protocol token consumed
    /// @return actualQuoteUsed Total quote token consumed
    function addV4LPLiquidity(address token, uint256 tokenAmount, uint256 quoteAmount)
    external
    returns (uint256 actualTokenUsed, uint256 actualQuoteUsed);
}

/// @title IPortalCore Interface
/// @notice Combines IPortalLauncher and IPortalTrade
interface IPortalCore is IPortalLauncher, IPortalTrade, IPortalTradeV2 {}

/// @title IPortalMigrator Interface
/// @notice Add liquidity from the bonding curve to DEX
/// @dev this is not a public interface of the portal.
///      All the functions of this interface are either called from the portal
///      or from the UniswapV3Pool contract.
interface IPortalMigrator {
    /// @notice Add liquidity to DEX
    /// @param token The address of the token
    /// @dev This is an internal function
    ///      Any dispatch to this function should be checked in portal contract
    ///      This function may be dellegated called from a payable function.
    function luanchToDEX(address token) external payable;
}

/// @title IRoller Interface
/// @notice This acts as the glue between the portal and the flap staking contract
interface IRoller {
    /// @notice The lock the token is using
    enum LockType {
        INVALID_LOCK, // Invalid lock
        UNCX_LOCK, // The UNCX lock
        GOPLUS_UNIV3_LOCK, // The Goplus UNIv3 lock
        TOSHI_LP_LOCK, // The Toshi LP lock
        IZI_LP_LOCK // The IziSwap LP locker
    }

    /// @notice get the locks by token address
    /// @param token The address of the token
    /// @return locks The lock ids of the token
    function getLocks(address token) external view returns (uint256[] memory locks);

    /// @dev deprecated
    function rollv2(bytes calldata packedParams) external;

    /// @notice Revenue Share: Claim LP fees for a vanity token
    /// @param token The address of the token
    /// @return tokenAmount The amount of the token claimed
    /// @return ethAmount The amount of ETH claimed
    /// @dev Only the beneficiary of the token can call this function.
    function claim(address token) external returns (uint256 tokenAmount, uint256 ethAmount);

    /// @notice Allows the default admin to change the beneficiary of a token
    /// @param token The address of the token
    /// @param newBeneficiary The new beneficiary address
    function setTokenBeneficiary(address token, address newBeneficiary) external;

    /// @notice Allows a roller or default admin to claim LP fees on behalf of the beneficiary
    /// @param token The address of the token
    /// @return tokenAmount The amount of the token claimed
    /// @return quoteAmount The amount of quote token (or ETH) claimed
    /// @dev Only the roller or default admin can call this function.
    /// The claimed fee will be sent to the beneficiary of the token.
    function delegateClaim(address token) external returns (uint256 tokenAmount, uint256 quoteAmount);
}

interface IPortalDexRouter {
    // @notice Update the DEX pool information for a token
    // @dev can only be called by DEX_ROUTER_MANAGER_ROLE roles
    function updateTokenPoolInfo(address token, IPortalTypes.PackedDexPool calldata poolInfo) external;
}

/// @title IPortalTweak Interface
/// @notice Handles admin-only configuration operations for the Portal
interface IPortalTweak is IPortalTypes {
    /// @notice Parameters for updating tax token addresses
    struct TaxTokenAddressUpdate {
        /// @notice The address of the tax token to update
        address token;
        /// @notice The new beneficiary address for the tax splitter
        address beneficiary;
        /// @notice The new fee receiver address for the tax splitter
        address feeReceiver;
    }

    /// @notice Emitted when tax token addresses are updated
    /// @param token The address of the tax token
    /// @param beneficiary The new beneficiary address
    /// @param feeReceiver The new fee receiver address
    /// @dev Only Default ADMIN can change this
    event TaxTokenAddressesUpdated(address indexed token, address beneficiary, address feeReceiver);

    /// @notice Set the configuration for a quote token
    /// @dev Only callable by the default admin
    /// @param quoteToken The address of the quote token
    /// @param config The configuration struct for the quote token
    function setQuoteTokenConfiguration(address quoteToken, QuoteTokenConfiguration calldata config) external;

    /// @notice Set the fee exemption status for a list of traders
    /// @dev Only callable by the default admin
    /// @param traders The addresses of the traders to set exemption for
    /// @param isExempted Whether the traders should be exempted from fees
    function setFeeExemption(address[] memory traders, bool isExempted) external;

    /// @notice Get the current buy and sell fee rates
    /// @return buyFeeRate The current buy fee rate in basis points (e.g. 200 = 2%)
    /// @return sellFeeRate The current sell fee rate in basis points (e.g. 200 = 2%)
    function getFeeRate() external view returns (uint256 buyFeeRate, uint256 sellFeeRate);

    /// @notice Set the fee profile for a token
    /// @dev Only callable by DEFAULT_ADMIN_ROLE or TOKEN_FLAP_FEE_SETTER_ROLE
    /// @param token The address of the token
    /// @param feeProfile The fee profile to set
    function setFlapFeeProfile(address token, FlapFeeProfile feeProfile) external;

    /// @notice Update beneficiary and feeReceiver addresses for one or more tax tokens
    /// @dev Only callable by the default admin, TAX_MANAGER_ROLE, or TAX_GUARDIAN_ROLE
    /// @param updates Array of TaxTokenAddressUpdate structs containing token addresses and new addresses
    function updateTaxTokenAddresses(TaxTokenAddressUpdate[] calldata updates) external;

    /// @notice Register an extension for use with the portal
    /// @param extensionId The unique identifier for this extension
    /// @param extensionAddress The address of the extension contract
    /// @param version The version of the extension interface it implements (starting from 1)
    /// @dev Only callable by the default admin
    function registerExtension(bytes32 extensionId, address extensionAddress, uint8 version) external;

    /// @notice Block or unblock multiple addresses from creating tokens
    /// @param spammers The addresses to block or unblock
    /// @param blocked True to block, false to unblock
    /// @dev Only callable by the default admin or MODERATOR_ROLE
    function setSpammerBlockedBatch(address[] calldata spammers, bool blocked) external;

    /// @notice Quickly pause only `newTokenV7` while leaving other global-switch entrypoints unaffected.
    /// @dev Callable by V7_GUARDIAN_ROLE or DEFAULT_ADMIN_ROLE.
    function haltNewTokenV7() external;

    /// @notice Emitted when stuck tax tokens are recovered from the tax splitter
    /// @param taxToken The address of the tax token
    /// @param amountSentToToken The amount of tokens sent back to the tax token contract
    /// @param amountReturnedToSplitter The amount of tokens returned to the tax splitter
    event StuckTaxTokenRecovered(address indexed taxToken, uint256 amountSentToToken, uint256 amountReturnedToSplitter);

    /// @notice Recover stuck tax tokens from the tax splitter and re-inject up to liquidationThreshold into the token
    /// @dev Only callable by AUDITOR_ROLE. Currently supports V1 (TOKEN_TAXED) tax tokens.
    ///      The name is intentionally generic to allow extension to V2/V3 tax tokens in the future.
    /// @param taxToken The address of the V1 tax token to recover stuck tokens for
    function recoverStuckTaxToken(address taxToken) external;

    /// @notice Emitted when stuck tax tokens are burned (sent to the dead address)
    /// @param taxToken The address of the tax token
    /// @param amountBurned The amount of tokens sent to the dead address
    event StuckTaxTokenBurned(address indexed taxToken, uint256 amountBurned);

    /// @notice Burn stuck tax tokens by sweeping them from the tax splitter and sending to the dead address
    /// @dev Only callable by DEFAULT_ADMIN_ROLE. Supports V1 (TOKEN_TAXED) tax tokens.
    /// @param taxToken The address of the V1 tax token to burn stuck tokens for
    function burnStuckTaxToken(address taxToken) external;

    /// @notice Emitted when marketAddress is updated for a V2/V3 tax token.
    event MarketWalletChanged(address indexed token, address indexed oldMarket, address indexed newMarket);

    /// @notice Changes the market wallet (TaxProcessor.marketAddress) for a TOKEN_TAXED_V2 or TOKEN_TAXED_V3 token.
    /// @dev Restricted to TAX_GUARDIAN_ROLE or DEFAULT_ADMIN_ROLE.
    /// @param token The tax token address.
    /// @param newMarketWallet The new market wallet address.
    function changeMarketWallet(address token, address newMarketWallet) external;
}

/// @title Portal Interface
/// @notice This interface combines the core and game interfaces
interface IPortal is
    IPortalCore,
    IAccessControlUpgradeable,
    IRoller,
    IV4Locker,
    IPortalDexRouter,
    IPortalTweak,
    IPortalLens,
    IPortalLensV2,
    IPortalLauncherTwoStep
{
    /// @notice Get the version of the portal
    /// @return The version string
    function version() external view returns (string memory);

    /// @notice Check if tax on bonding curve is enabled
    /// @return enabled True if tax on bonding curve is enabled
    function enableTaxOnBondingCurve() external view returns (bool enabled);

    /// @notice Change the protocol bit flags
    /// @dev Can only be called with DEFAULT_ADMIN_ROLE
    /// @param flags The new flags
    function setBitFlags(uint256 flags) external;

    /// @notice Can only be called by the guardian role or the default admin role
    /// @dev This function is used to pause the protocol
    function halt() external;

    /// @notice Check if an address is blocked from creating tokens
    /// @param spammer The address to check
    /// @return True if the address is blocked
    function isSpammerBlocked(address spammer) external view returns (bool);

    /// @notice Send a message for a token
    /// @param token The address of the token
    /// @param message The message to send
    function sendMsg(address token, string memory message) external;

    /// @notice Get the current nonce of the portal
    function nonce() external view returns (uint256);
}
```


# Launch token through VaultPortal

`VaultPortal` builds on top of `Portal` to add Vault functionality for vault-backed token launches. It creates the token and its associated Vault in a single transaction.

Use `VaultPortal` when you need a Vault-backed launch. For launches without a Vault, use `Portal` instead.

For the `VaultPortal` contract address, see [VaultPortal deployed addresses](/flap/developers/vault-developers/flap-tax-vault#deployed-addresses).

## Key interfaces

### Recommended: `newTokenV6WithVault`

The preferred entry point for new integrations is `newTokenV6WithVault`, which supports `TOKEN_TAXED_V3` (Tax Token V3) with a vault in a single transaction:

```solidity
/// @notice Create a new tax token via Portal's newTokenV6 with an associated vault in a single transaction
/// @dev Only TOKEN_TAXED_V3 is currently supported as tokenVersion; any other version reverts with FeatureDisabled.
/// @param params The parameters for creating the tax token and vault (mirrors NewTokenV6Params, minus beneficiary)
/// @return token The address of the newly created tax token
function newTokenV6WithVault(NewTokenV6WithVaultParams calldata params) external payable returns (address token);
```

`NewTokenV6WithVaultParams` mirrors `NewTokenV6Params` (without `beneficiary`) and adds vault-specific fields:

```solidity
struct NewTokenV6WithVaultParams {
    string name;
    string symbol;
    string meta;
    IPortalTypes.DexThreshType dexThresh;
    bytes32 salt;
    IPortalTypes.MigratorType migratorType;
    address quoteToken;
    uint256 quoteAmt;
    bytes permitData;
    bytes32 extensionID;
    bytes extensionData;
    IPortalTypes.DEXId dexId;
    IPortalTypes.V3LPFeeProfile lpFeeProfile;
    // V3 tax fields
    uint16 buyTaxRate;
    uint16 sellTaxRate;
    uint64 taxDuration;
    uint64 antiFarmerDuration;
    uint16 mktBps;
    uint16 deflationBps;
    uint16 dividendBps;
    uint16 lpBps;
    uint256 minimumShareBalance;
    address dividendToken;
    address commissionReceiver;
    IPortalTypes.TokenVersion tokenVersion; // must be TOKEN_TAXED_V3
    // vault fields
    address vaultFactory;
    bytes vaultData;
}
```

{% hint style="info" %}
`newTokenV6WithVault` only accepts `TOKEN_TAXED_V3`. Passing any other `tokenVersion` reverts with `FeatureDisabled`. For new integrations, always use Tax Token V3 with `newTokenV6WithVault`.
{% endhint %}

{% hint style="warning" %}
Legacy tax token versions (`TOKEN_TAXED`, `TOKEN_TAXED_V2`) are not supported by `newTokenV6WithVault` and are not recommended for new launches. The old `newTaxTokenWithVault` entry point is deprecated and should not be used for new or existing integrations.
{% endhint %}

### New: `newTokenV7WithVault`

`newTokenV7WithVault` is the VaultPortal wrapper around `Portal.newTokenV7`. It creates the vault first, then rewrites the active `MARKETING_OR_VAULT` fee receiver to that vault before forwarding the launch to `Portal`.

```solidity
/// @notice Create a new token via a V7-style fee-config API with an associated vault.
/// @dev Supports TOKEN_V3_PERMIT (non-tax) and TOKEN_TAXED_V3 (tax). The wrapper creates the vault first,
///      then rewrites MARKETING_OR_VAULT receivers to that vault before calling Portal.newTokenV7.
/// @param params The parameters for creating the token and vault (mirrors upstream newTokenV7 launch UX)
/// @return token The address of the newly created token
function newTokenV7WithVault(NewTokenV7WithVaultParams calldata params) external payable returns (address token);
```

`NewTokenV7WithVaultParams` mirrors `Portal`'s `NewTokenV7Params` and adds vault-specific fields:

```solidity
struct NewTokenV7WithVaultParams {
    string name;
    string symbol;
    string meta;
    IPortalTypes.DexThreshType dexThresh;
    bytes32 salt;
    IPortalTypes.MigratorType migratorType;
    address quoteToken;
    uint256 quoteAmt;
    bytes permitData;
    bytes32 extensionID;
    bytes extensionData;
    IPortalTypes.TokenVersion tokenVersion;
    IPortalTypes.DEXId dexId;
    uint64 antiFarmerDuration;
    uint16 buyTaxRate;
    uint16 sellTaxRate;
    uint64 taxDuration;
    address commissionReceiver;
    IPortalTypes.FeeConfig[4] feeConfigs;
    address vaultFactory;
    bytes vaultData;
}
```

### Current `newTokenV7WithVault` behavior

The interface comment describes both `TOKEN_V3_PERMIT` and `TOKEN_TAXED_V3`, but the current implementation in `FlapTaxVaults/src/VaultPortalLaunch.sol` is narrower:

* it currently reverts with `FeatureDisabled()` unless `tokenVersion == TOKEN_V3_PERMIT`
* `quoteToken` must be `address(0)` or the wrapper reverts with `UnsupportedQuoteToken(...)`
* exactly one active `MARKETING_OR_VAULT` slot is required
* active `feeConfigs` must sum to exactly `10000` bps
* after validation, the wrapper overwrites that single `MARKETING_OR_VAULT` slot's `marketingAddress` with the freshly created vault address
* the current source hard-codes an allowlist of supported vault factories for this path

{% hint style="warning" %}
The supplied interface and simulation script describe a broader V7-with-vault design, but the current source implementation is stricter. When integrating, follow the behavior enforced in `FlapTaxVaults/src/VaultPortalLaunch.sol` rather than assuming every upstream `newTokenV7` combination is already live through `VaultPortal`.
{% endhint %}

### Deprecated: `newTaxTokenWithVault`

`newTaxTokenWithVault` still exists in the ABI for compatibility, but the current `VaultPortal` implementation no longer supports it. The function always reverts and should be treated as deprecated.

```solidity
/// @notice Deprecated. Use newTokenV6WithVault instead.
/// @dev This function previously created a V1/V2 tax token via Portal's newTokenV5.
///      It is no longer supported. Calling it will always revert.
/// @param params Unused — kept for ABI compatibility only.
function newTaxTokenWithVault(NewTaxTokenWithVaultParams calldata params)
    external
    payable
    returns (address token);
```

{% hint style="danger" %}
Do not integrate `newTaxTokenWithVault`. Use `newTokenV6WithVault` for `TOKEN_TAXED_V3`, or `newTokenV7WithVault` for the V7 vault-backed flow.
{% endhint %}

## How to launch with `newTokenV6WithVault`

1. **Choose a registered vault factory.** Vault factories are registered in `VaultPortal`. Each factory defines its own `vaultData` schema. See [Registered vaults](/flap/developers/token-launcher-developers/registered-vaults).
2. **Prepare metadata and core token parameters.** Use the same metadata flow as `Portal` (see [launch-token-through-portal.md](/flap/developers/token-launcher-developers/launch-token-through-portal)). Set `tokenVersion = TOKEN_TAXED_V3` and both `buyTaxRate`/`sellTaxRate` > 0.
3. **Encode `vaultData` for the selected factory.** The `vaultData` bytes are factory-specific. Always follow the factory's schema (see [Registered vaults](/flap/developers/token-launcher-developers/registered-vaults)).
4. **Call `newTokenV6WithVault`.** The contract emits `FlapTaxVaultTokenCreated` with the token, vault, and vault factory addresses.

Example:

```typescript
import { encodeAbiParameters, parseEther, zeroAddress } from "viem";

const params = {
    name: "My Token",
    symbol: "MTK",
    meta: "<ipfs-cid>",
    dexThresh: DexThreshType.TWO_THIRDS,
    salt, // must produce 7777 vanity suffix
    migratorType: MigratorType.V2_MIGRATOR,
    quoteToken: zeroAddress,
    quoteAmt: parseEther("0.01"),
    permitData: "0x",
    extensionID: "0x0000000000000000000000000000000000000000000000000000000000000000",
    extensionData: "0x",
    dexId: DEXId.DEX0,
    lpFeeProfile: V3LPFeeProfile.LP_FEE_PROFILE_STANDARD,
    buyTaxRate: 300,
    sellTaxRate: 1000,
    taxDuration: 365n * 24n * 60n * 60n,
    antiFarmerDuration: 60n * 60n,
    mktBps: 10000,
    deflationBps: 0,
    dividendBps: 0,
    lpBps: 0,
    minimumShareBalance: 0n,
    dividendToken: zeroAddress,
    commissionReceiver: zeroAddress,
    tokenVersion: TokenVersion.TOKEN_TAXED_V3,
    vaultFactory: vaultFactoryAddress,
    vaultData: encodeAbiParameters(
        [
            // replace with the selected vault factory's expected schema
        ],
        []
    ),
};

const hash = await walletClient.writeContract({
    address: vaultPortalAddress,
    abi: vaultPortalAbi,
    functionName: "newTokenV6WithVault",
    args: [params],
    value: parseEther("0.01"),
});
```

{% hint style="info" %}
Each vault factory can define a different `vaultData` schema. Always validate the factory and its expected payload before encoding.
{% endhint %}

## How to launch with `newTokenV7WithVault`

1. **Choose a supported vault factory.** The current implementation validates the V7-with-vault path against a hard-coded factory allowlist inside `VaultPortalLaunch`. Verify your factory is supported before integrating this path.
2. **Prepare metadata and V7 token parameters.** Use the same metadata flow as `Portal` (see [launch-token-through-portal.md](/flap/developers/token-launcher-developers/launch-token-through-portal)).
3. **Configure `feeConfigs` with exactly one vault receiver slot.** Provide exactly one active `MARKETING_OR_VAULT` slot. Its address is only a placeholder at input time — `VaultPortal` will replace it with the newly created vault address before forwarding to `Portal.newTokenV7`.
4. **Encode `vaultData` for the selected factory.** The schema is factory-specific. See [Registered vaults](/flap/developers/token-launcher-developers/registered-vaults) and the factory's own docs.
5. **Call `newTokenV7WithVault`.** On success, `VaultPortal` stores the launched token → vault mapping and emits `FlapTaxVaultTokenCreated`.

Example based on `FlapTaxVaults/script/testnets/bsc_test/simulation/001-simulate-new-token-v7-with-flap-xvault.sol`:

```solidity
function launchV7WithVault(IVaultPortal vaultPortal, bytes32 salt, address vaultFactoryAddress)
    external
    payable
    returns (address token)
{
    IVaultPortalTypes.NewTokenV7WithVaultParams memory params;

    params.name = "My V7 Vault Token";
    params.symbol = "MV7";
    params.meta = "<ipfs-cid>";
    params.dexThresh = IPortalTypes.DexThreshType.FOUR_FIFTHS;
    params.salt = salt; // TOKEN_V3_PERMIT currently requires the 8888 suffix
    params.migratorType = IPortalTypes.MigratorType.PCS_INFINITY_CL_MIGRATOR;
    params.quoteToken = address(0);
    params.quoteAmt = 0.1 ether;
    params.permitData = "";
    params.extensionID = bytes32(0);
    params.extensionData = "";
    params.tokenVersion = IPortalTypes.TokenVersion.TOKEN_V3_PERMIT;
    params.dexId = IPortalTypes.DEXId.DEX0;
    params.antiFarmerDuration = 0;
    params.buyTaxRate = 0;
    params.sellTaxRate = 0;
    params.taxDuration = 0;
    params.commissionReceiver = address(0);
    params.vaultFactory = vaultFactoryAddress;
    params.vaultData = abi.encode(/* vault init data */);

    params.feeConfigs[0] = IPortalTypes.FeeConfig({
        feeType: IPortalTypes.FeeType.MARKETING_OR_VAULT,
        bps: 10000,
        marketingAddress: msg.sender, // placeholder; replaced with the created vault address
        dividendToken: address(0),
        minimumShareBalance: 0
    });
    params.feeConfigs[1] = IPortalTypes.FeeConfig({
        feeType: IPortalTypes.FeeType.NONE,
        bps: 0,
        marketingAddress: address(0),
        dividendToken: address(0),
        minimumShareBalance: 0
    });
    params.feeConfigs[2] = IPortalTypes.FeeConfig({
        feeType: IPortalTypes.FeeType.NONE,
        bps: 0,
        marketingAddress: address(0),
        dividendToken: address(0),
        minimumShareBalance: 0
    });
    params.feeConfigs[3] = IPortalTypes.FeeConfig({
        feeType: IPortalTypes.FeeType.NONE,
        bps: 0,
        marketingAddress: address(0),
        dividendToken: address(0),
        minimumShareBalance: 0
    });

    token = vaultPortal.newTokenV7WithVault{value: msg.value}(params);
}
```

Key notes:

* `newTokenV7WithVault` does not expose a standalone `beneficiary`; the vault-bound receiver comes from the single active `MARKETING_OR_VAULT` slot.
* If you configure zero or more than one active `MARKETING_OR_VAULT` slot, the wrapper reverts with `InvalidFeeConfig()`.
* The wrapper predicts the token address before vault creation, so your salt must already match the correct vanity suffix for the chosen token implementation.
* The current V7-with-vault implementation only accepts `quoteToken = address(0)`.

## Find the salt (vanity suffix)

`VaultPortal` uses CREATE2 through `Portal`, so the `salt` must produce a token address with the required suffix.

Follow the same salt-finding flow as [launch-token-through-portal.md](/flap/developers/token-launcher-developers/launch-token-through-portal), using the implementation that matches the launch path:

* `newTokenV6WithVault` with `TOKEN_TAXED_V3`: use **Tax Token V3 Impl** (`tokenImplTaxedV3`), suffix `7777`
* `newTokenV7WithVault` with `TOKEN_V3_PERMIT`: use **Standard Token V3 Impl** (`tokenImplV3` / Standard Token V3 Impl), suffix `8888`

## Salt locking

Portal supports locking a vanity salt so that only you can use it to launch a token. This works seamlessly with `VaultPortal` — lock the salt directly on Portal, then launch through VaultPortal as normal.

```solidity
function lockAndLaunch(
    IPortal portal,
    IVaultPortal vaultPortal,
    bytes32 mySalt,
    IPortalTypes.TokenVersion tokenVersion,
    IVaultPortalTypes.NewTokenV6WithVaultParams memory params,
    uint256 quoteAmt
) external payable returns (address token) {
    // 1. Lock the salt on Portal directly
    portal.lockSalt{value: msg.value}(mySalt, tokenVersion);

    // 2. Launch through VaultPortal — the lock is honoured automatically
    token = vaultPortal.newTokenV6WithVault{value: quoteAmt}(params);
}
```

No changes to `NewTokenV6WithVaultParams` are needed. VaultPortal communicates the real caller back to Portal automatically during the launch transaction.

The same salt-lock behavior also applies to `newTokenV7WithVault`.

## Reading vault info

You can query vault information after launch:

* `getVault(taxToken)` returns the full vault info and reverts if not found.
* `tryGetVault(taxToken)` returns `(found, info)` without reverting.

## IVaultPortal.sol

Full interface reference for developers:

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

pragma solidity ^0.8.13;

import {IPortalTypes} from "src/interfaces/IPortal.sol";

/// @title IVaultPortalTypes
/// @notice Type definitions for the VaultPortal contract
/// @dev Contains all structs and custom types used by VaultPortal
interface IVaultPortalTypes {
    /// @notice Risk level classification for vault audits (8 bits)
    /// @dev Used to represent the security assessment of a vault or factory
    enum RiskLevel {
        UNVERIFIED, // 0 - Not yet verified/audited
        LOW_RISK, // 1 - Low risk
        LOW_MEDIUM_RISK, // 2 - Low to medium risk
        MEDIUM_RISK, // 3 - Medium risk
        HIGH_RISK // 4 - High risk

    }

    /// @notice Category classification for vaults (8 bits)
    /// @dev Used to categorize vaults by their type or functionality
    enum VaultCategory {
        NONE, // 0 - Not in any category (default)
        TYPE_AI_ORACLE_POWERED // 1 - AI Oracle powered vaults

    }

    /// @notice Information about a registered vault factory
    /// @dev Packed into a single storage slot for gas efficiency
    /// @param enabled Whether this factory can be used to create new vaults
    /// @param official Whether vaults created by this factory are marked as official/endorsed
    /// @param riskLevel The risk level classification from audit
    /// @param category The category classification for vaults from this factory (1 byte)
    /// @param reserved Reserved space for future upgrades (28 bytes)
    struct VaultFactoryInfo {
        bool enabled;
        bool official;
        RiskLevel riskLevel;
        VaultCategory category;
        bytes28 reserved;
    }

    /// @notice Packed vault information stored in contract storage
    /// @dev Uses three storage slots (64 bytes total)
    /// @param vault The address of the vault contract (20 bytes)
    /// @param vaultFactory The address of the vault factory that created this vault (20 bytes)
    /// @param adapter The address of the adapter contract for legacy vaults (20 bytes)
    /// @param isOfficial Whether this vault is marked as official/endorsed (1 byte)
    /// @param riskLevel The risk level classification from audit (1 byte)
    /// @param category The category classification for this vault (1 byte)
    struct VaultedTaxTokenInfo {
        // slot 0
        address vault;
        bool isOfficial;
        RiskLevel riskLevel;
        VaultCategory category;
        bytes9 reserved0;
        // slot 1
        address vaultFactory;
        bytes12 reserved1;
        // slot 2
        address adapter;
        bytes12 reserved2;
    }

    /// @notice Audit report for a tax token
    /// @param auditor The address of the auditor who submitted this report
    /// @param riskLevel The risk level classification from this audit
    /// @param ipfsCid The IPFS CID of the audit report document
    struct AuditReport {
        address auditor;
        RiskLevel riskLevel;
        string ipfsCid;
    }

    /// @notice Complete vault information returned to external callers
    /// @dev This struct is used for view functions and includes human-readable description
    /// @param vault The address of the vault contract
    /// @param vaultFactory The address of the vault factory that created this vault (zero address if unknown)
    /// @param description Human-readable description of the vault's purpose and functionality
    /// @param isOfficial Whether this vault is marked as official/endorsed by the protocol
    /// @param riskLevel The risk level classification from audit
    struct VaultInfo {
        address vault;
        address vaultFactory;
        string description;
        bool isOfficial;
        RiskLevel riskLevel;
    }

    /// @notice Parameters required to create a new tax token with an associated vault
    /// @dev All parameters are passed as a single struct to avoid stack-too-deep errors
    /// @param name The name of the tax token (e.g., "MyToken")
    /// @param symbol The symbol of the tax token (e.g., "MTK")
    /// @param meta Metadata URI or string for additional token information
    /// @param dexThresh The DEX supply threshold type
    /// @param salt A unique salt for deterministic address generation (must produce vanity suffix)
    /// @param taxRate The tax rate in basis points (e.g., 100 = 1%, max 1000 = 10%)
    /// @param migratorType The migrator type (see MigratorType enum)
    /// @param quoteToken The token used for initial liquidity (address(0) for native token)
    /// @param quoteAmt The amount of quote token to provide as initial liquidity
    /// @param permitData The optional permit data for the quote token
    /// @param extensionID The ID of the extension to be used for the new token if not zero
    /// @param extensionData Additional extension specific data
    /// @param dexId The preferred DEX ID for the token
    /// @param lpFeeProfile The preferred V3 LP fee profile for the token
    /// @param taxDuration Tax duration in seconds (max: 100 years)
    /// @param antiFarmerDuration Anti-farmer duration in seconds (max: 1 year)
    /// @param mktBps Market allocation basis points (to beneficiary)
    /// @param deflationBps Deflation basis points (burned)
    /// @param dividendBps Dividend basis points (to dividend contract)
    /// @param lpBps Liquidity provision basis points (LP to dead address)
    /// @param minimumShareBalance Minimum balance for dividend eligibility
    /// @param vaultFactory The address of the vault factory to use for creating the vault (any factory is allowed; unregistered/disabled factories result in unverified vaults)
    /// @param vaultData Encoded data specific to the vault type being created
    struct NewTaxTokenWithVaultParams {
        string name;
        string symbol;
        string meta;
        IPortalTypes.DexThreshType dexThresh;
        bytes32 salt;
        uint16 taxRate;
        IPortalTypes.MigratorType migratorType;
        address quoteToken;
        uint256 quoteAmt;
        bytes permitData;
        bytes32 extensionID;
        bytes extensionData;
        IPortalTypes.DEXId dexId;
        IPortalTypes.V3LPFeeProfile lpFeeProfile;
        uint64 taxDuration;
        uint64 antiFarmerDuration;
        uint16 mktBps;
        uint16 deflationBps;
        uint16 dividendBps;
        uint16 lpBps;
        uint256 minimumShareBalance;
        address vaultFactory;
        bytes vaultData;
    }

    /// @notice Parameters required to create a new V3 tax token with an associated vault
    /// @dev tokenVersion must be TOKEN_TAXED_V3; if it is not, the implementation reverts with FeatureDisabled
    struct NewTokenV6WithVaultParams {
        string name;
        string symbol;
        string meta;
        IPortalTypes.DexThreshType dexThresh;
        bytes32 salt;
        IPortalTypes.MigratorType migratorType;
        address quoteToken;
        uint256 quoteAmt;
        bytes permitData;
        bytes32 extensionID;
        bytes extensionData;
        IPortalTypes.DEXId dexId;
        IPortalTypes.V3LPFeeProfile lpFeeProfile;
        // V3 tax fields
        uint16 buyTaxRate;
        uint16 sellTaxRate;
        uint64 taxDuration;
        uint64 antiFarmerDuration;
        uint16 mktBps;
        uint16 deflationBps;
        uint16 dividendBps;
        uint16 lpBps;
        uint256 minimumShareBalance;
        address dividendToken;
        address commissionReceiver;
        /// The token version to use (must be TOKEN_TAXED_V3, otherwise reverts with FeatureDisabled)
        IPortalTypes.TokenVersion tokenVersion;
        // vault fields
        address vaultFactory;
        bytes vaultData;
    }

    /// @notice Parameters required to create a V7-style token with an associated vault.
    /// @dev Mirrors upstream `newTokenV7`, then swaps every MARKETING_OR_VAULT receiver to the
    ///      freshly created vault before calling `Portal.newTokenV7`.
    struct NewTokenV7WithVaultParams {
        string name;
        string symbol;
        string meta;
        IPortalTypes.DexThreshType dexThresh;
        bytes32 salt;
        IPortalTypes.MigratorType migratorType;
        address quoteToken;
        uint256 quoteAmt;
        bytes permitData;
        bytes32 extensionID;
        bytes extensionData;
        IPortalTypes.TokenVersion tokenVersion;
        IPortalTypes.DEXId dexId;
        uint64 antiFarmerDuration;
        uint16 buyTaxRate;
        uint16 sellTaxRate;
        uint64 taxDuration;
        address commissionReceiver;
        IPortalTypes.FeeConfig[4] feeConfigs;
        address vaultFactory;
        bytes vaultData;
    }

    /* ========== EVENTS ========== */

    /// @notice Emitted when a new tax token with an associated vault is successfully created
    /// @param token The address of the newly deployed tax token
    /// @param vault The address of the newly created vault that will receive tax revenue
    /// @param vaultFactory The vault factory address that was used to create the vault
    event FlapTaxVaultTokenCreated(address indexed token, address indexed vault, address indexed vaultFactory);

    /// @notice Emitted when a vault factory is registered or its configuration is updated
    /// @param factory The address of the vault factory being registered/updated
    /// @param enabled Whether the factory is enabled for creating new vaults
    /// @param official Whether vaults from this factory should be marked as official
    /// @param riskLevel The risk level classification for vaults from this factory
    event FlapTaxVaultFactoryRegistered(address factory, bool enabled, bool official, RiskLevel riskLevel);

    /// @notice Emitted when a vault factory's category is set during registration or update
    /// @param factory The address of the vault factory
    /// @param category The category classification for vaults from this factory
    event FlapTaxVaultFactoryCategorySet(address factory, VaultCategory category);

    /// @notice Emitted when an adapter is registered for a legacy tax token
    /// @param taxToken The address of the tax token
    /// @param adapter The address of the adapter contract
    event AdapterRegistered(address indexed taxToken, address indexed adapter);

    /// @notice Emitted when a new audit report is submitted
    /// @param taxToken The address of the tax token being audited
    /// @param auditor The address of the auditor
    /// @param riskLevel The risk level classification from this audit
    /// @param ipfsCid The IPFS CID of the audit report
    event AuditReportSubmitted(address indexed taxToken, address indexed auditor, RiskLevel riskLevel, string ipfsCid);

    /// @notice Emitted when a new audit report is submitted for a vault factory
    /// @param factory The address of the vault factory being audited
    /// @param auditor The address of the auditor
    /// @param riskLevel The risk level classification from this audit
    /// @param ipfsCid The IPFS CID of the audit report
    event FactoryAuditReportSubmitted(
        address indexed factory, address indexed auditor, RiskLevel riskLevel, string ipfsCid
    );

    /// @notice Emitted when a vault's category is updated
    /// @param taxToken The address of the tax token
    /// @param category The new category
    event VaultCategoryUpdated(address indexed taxToken, VaultCategory category);

    /// @notice Emitted when a factory's category is updated
    /// @param factory The address of the vault factory
    /// @param category The new category
    event FactoryCategoryUpdated(address indexed factory, VaultCategory category);

    /* ========== ERRORS ========== */

    /// @notice Thrown when the provided tax rate is invalid (0 or > 1000 basis points)
    /// @param taxRate The invalid tax rate that was provided
    error InvalidTaxRate(uint256 taxRate);

    /// @notice Thrown when mktBps is invalid (must be > 0)
    error InvalidMktBps();

    /// @notice Thrown when V7-style fee configs are malformed.
    /// @dev Used for wrapper-level validation such as missing MARKETING_OR_VAULT buckets.
    error InvalidFeeConfig();

    /// @notice Thrown when an unsupported quote token is specified
    /// @param quoteToken The address of the unsupported quote token
    error UnsupportedQuoteToken(address quoteToken);

    /// @notice Thrown when attempting to use a vault factory that is not registered
    /// @param factory The address of the unregistered vault factory
    error VaultFactoryNotRegistered(address factory);

    /// @notice Thrown when the predicted token address does not match the required vanity suffix
    /// @param predictedAddress The predicted address that failed the vanity check
    error InvalidVanity(address predictedAddress);

    /// @notice Thrown when trying to get vault info for a token that has no associated vault
    /// @param taxToken The address of the tax token with no vault
    error VaultNotFound(address taxToken);

    /// @notice Thrown when token address does not match predicted address
    error TokenAddressMismatch();

    /// @notice Thrown when BNB transfer fails
    error BnbTransferFailed();

    /// @notice Thrown when a non-V3 tax token version is specified (only TOKEN_TAXED_V3 is allowed)
    error OnlyV3TaxTokenAllowed();

    /// @notice Thrown when a requested feature is not yet enabled
    error FeatureDisabled();

    /// @notice Thrown when portal address is zero
    error ZeroPortalAddress();

    /// @notice Thrown when token implementation address is zero
    error ZeroTokenImplAddress();

    /// @notice Thrown when trying to perform an operation on a non-existent token
    /// @param taxToken The address of the non-existent tax token
    error TokenNotFound(address taxToken);

    /// @notice error when user is rate limited from creating tokens
    /// @param user The address that is rate limited
    /// @param lastCreationTime The timestamp of the user's last successful token creation
    error RateLimitExceeded(address user, uint256 lastCreationTime);
}

/// @title IVaultPortal
/// @notice Interface for the VaultPortal contract that manages vault-backed token launches
/// @dev VaultPortal acts as a registry and factory orchestrator for different vault types
/// @dev It coordinates token launches with their associated revenue vaults
interface IVaultPortal is IVaultPortalTypes {
    /* ========== FUNCTIONS ========== */

    /// @notice Get information about a registered vault factory
    /// @param factory The address of the vault factory
    /// @return enabled Whether this factory can be used to create new vaults
    /// @return official Whether vaults created by this factory are marked as official/endorsed
    /// @return riskLevel The risk level classification for vaults from this factory
    /// @return reserved Reserved space for future upgrades (29 bytes)
    function vaultFactories(address factory)
        external
        view
        returns (bool enabled, bool official, RiskLevel riskLevel, bytes29 reserved);
    /* ========== FUNCTIONS ========== */

    /// @notice Deprecated. Use newTokenV6WithVault instead.
    /// @dev This function previously created a V1/V2 tax token via Portal's newTokenV5.
    ///      It is no longer supported. Calling it will always revert.
    /// @param params Unused — kept for ABI compatibility only.
    function newTaxTokenWithVault(NewTaxTokenWithVaultParams calldata params)
        external
        payable
        returns (address token);

    /// @notice Create a new tax token via Portal's newTokenV6 with an associated vault in a single transaction
    /// @dev Only TOKEN_TAXED_V3 is currently supported as tokenVersion; any other version reverts with FeatureDisabled.
    /// @param params The parameters for creating the tax token and vault (mirrors NewTokenV6Params, minus beneficiary)
    /// @return token The address of the newly created tax token
    function newTokenV6WithVault(NewTokenV6WithVaultParams calldata params) external payable returns (address token);

    /// @notice Create a new token via a V7-style fee-config API with an associated vault.
    /// @dev Supports TOKEN_V3_PERMIT (non-tax) and TOKEN_TAXED_V3 (tax). The wrapper creates the vault first,
    ///      then rewrites MARKETING_OR_VAULT receivers to that vault before calling Portal.newTokenV7.
    /// @param params The parameters for creating the token and vault (mirrors upstream newTokenV7 launch UX)
    /// @return token The address of the newly created token
    function newTokenV7WithVault(NewTokenV7WithVaultParams calldata params) external payable returns (address token);

    /// @notice Predict the address of a tax token V1 given a salt
    /// @dev Uses CREATE2 deterministic address prediction
    /// @dev Useful for generating valid salts that produce the required vanity suffix
    /// @param salt The salt for deterministic deployment
    /// @return predictedAddress The predicted address of the tax token
    function predictTaxTokenV1Address(bytes32 salt) external view returns (address predictedAddress);

    /// @notice Get the complete vault information for a tax token
    /// @dev Reverts if no vault is found for the given tax token
    /// @dev Attempts to fetch the vault's description by calling its description() function
    /// @param taxToken The address of the tax token
    /// @return info The VaultInfo struct containing complete vault details
    function getVault(address taxToken) external view returns (VaultInfo memory info);

    /// @notice Attempt to get vault information for a tax token without reverting
    /// @dev First checks the internal mapping, then falls back to searching the Portal
    /// @dev For fallback results, isOfficial and isVerified will always be false
    /// @param taxToken The address of the tax token
    /// @return found Whether a vault was found for this tax token
    /// @return info The VaultInfo struct (empty if not found)
    function tryGetVault(address taxToken) external view returns (bool found, VaultInfo memory info);

    /// @notice Register a new vault factory or update an existing one (backward-compatible overload)
    /// @dev Only accounts with VAULT_ADMIN_ROLE can call this function
    /// @dev Allows enabling/disabling factories and updating their official/riskLevel status
    /// @dev Category defaults to VaultCategory.NONE
    /// @param factory The address of the vault factory to register/update
    /// @param enabled Whether the factory should be enabled for creating new vaults
    /// @param official Whether vaults from this factory should be marked as official
    /// @param riskLevel The risk level classification for vaults from this factory
    function registerVaultFactory(address factory, bool enabled, bool official, RiskLevel riskLevel) external;

    /// @notice Register a new vault factory or update an existing one with a specific category
    /// @dev Only accounts with VAULT_ADMIN_ROLE can call this function
    /// @dev Allows enabling/disabling factories and updating their official/riskLevel/category status
    /// @param factory The address of the vault factory to register/update
    /// @param enabled Whether the factory should be enabled for creating new vaults
    /// @param official Whether vaults from this factory should be marked as official
    /// @param riskLevel The risk level classification for vaults from this factory
    /// @param category The category classification for vaults from this factory
    function registerVaultFactory(
        address factory,
        bool enabled,
        bool official,
        RiskLevel riskLevel,
        VaultCategory category
    ) external;

    /// @notice Register an adapter for a legacy tax token vault
    /// @dev Only accounts with AUDITOR_ROLE can call this function
    /// @dev The adapter must implement the VaultBase interface for compatibility
    /// @param taxToken The address of the tax token
    /// @param adapter The address of the adapter contract
    function registerAdapter(address taxToken, address adapter) external;

    /// @notice Submit a new audit report for a tax token
    /// @dev Only accounts with AUDITOR_ROLE can call this function
    /// @dev The tax token must exist (have a vault or adapter registered)
    /// @dev This may change the risk level of the token
    /// @param taxToken The address of the tax token being audited
    /// @param riskLevel The risk level classification from this audit
    /// @param ipfsCid The IPFS CID of the audit report document
    function submitAuditReport(address taxToken, RiskLevel riskLevel, string calldata ipfsCid) external;

    /// @notice Submit a new audit report for a vault factory
    /// @dev Only accounts with VAULT_ADMIN_ROLE can call this function
    /// @dev The factory should be registered or have existing audit reports
    /// @param factory The address of the vault factory being audited
    /// @param riskLevel The risk level classification from this audit
    /// @param ipfsCid The IPFS CID of the audit report document
    function submitFactoryAuditReport(address factory, RiskLevel riskLevel, string calldata ipfsCid) external;

    /// @notice Get recent audit reports for a tax token with pagination
    /// @dev Returns reports starting from the most recent (end of array)
    /// @dev If the token has no audit reports, falls back to its factory's audit reports
    /// @param taxToken The address of the tax token
    /// @param offset The number of reports to skip from the end (0 = most recent)
    /// @param limit The maximum number of reports to return
    /// @return reports The array of audit reports
    /// @return total The total number of audit reports for this token
    function getAuditReports(address taxToken, uint256 offset, uint256 limit)
        external
        view
        returns (AuditReport[] memory reports, uint256 total);

    /// @notice Get the category of a vault
    /// @param taxToken The tax token address
    /// @return category The category of the vault
    function getVaultCategory(address taxToken) external view returns (VaultCategory category);

    /// @notice Get the category of a vault factory
    /// @param factory The vault factory address
    /// @return category The category of the factory
    function getFactoryCategory(address factory) external view returns (VaultCategory category);

    /// @notice Set the category of a vault
    /// @dev Only accounts with VAULT_ADMIN_ROLE can call this function
    /// @param taxToken The tax token address
    /// @param category The new category
    function setVaultCategory(address taxToken, VaultCategory category) external;

    /// @notice Set the category of a vault factory
    /// @dev Only accounts with VAULT_ADMIN_ROLE can call this function
    /// @param factory The vault factory address
    /// @param category The new category
    function setFactoryCategory(address factory, VaultCategory category) external;
}

```


# Registered vaults

## Registered vaults

This page lists registered vault factories and the expected `vaultData` schema for each factory.

### BNB mainnet

| name                    | description                                                                                                  | is official (yes/no) | vaultFactory address                       | vaultData schema description                                                                                                                                                                                                                                                                              | interface                                                                                                                                                        |
| ----------------------- | ------------------------------------------------------------------------------------------------------------ | -------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Split Vault             | Splits received BNB across up to 10 recipients based on basis points and allows dispatch/claim.              | yes                  | 0xfab75Dc774cB9B38b91749B8833360B46a52345F | ABI-encode `SplitVault.Recipient[]` where each item is `{address recipient, uint16 bps}`. `recipient` is the payout address (must be non-zero and unique across the array). `bps` is the share in basis points (1% = 100, 100% = 10,000). The array length must be 1–10 and total bps must sum to 10,000. | [misc/interfaces/vaults/SplitVault.sol](https://github.com/flap-sh/gitbook/tree/main/developers/token-launcher-developers/misc/interfaces/vaults/SplitVault.sol) |
| Gift Vault (FlapXVault) | A gift vault that routes tax token revenue based on X (Twitter) proof and can fall back to snowball buyback. | yes                  | 0x025549F52B03cF36f9e1a337c02d3AA7Af66ab32 | ABI-encode `VaultData` with `{string xHandle}` (the fee manager’s X handle). The `xHandle` must be all lowercase.                                                                                                                                                                                         | [Gift Vault Guide](/flap/developers/vault-developers/gift-vault)                                                                                                 |

### BNB testnet

| name                    | description                                                                                                  | is official (yes/no) | vaultFactory address                       | vaultData schema description                                                                                                                                                                                                                                                                              | interface                                                                                                                                                        |
| ----------------------- | ------------------------------------------------------------------------------------------------------------ | -------------------- | ------------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Split Vault             | Splits received BNB across up to 10 recipients based on basis points and allows dispatch/claim.              | yes                  | 0x1ae091F75D593eb7dC6539600a185C8A6076A424 | ABI-encode `SplitVault.Recipient[]` where each item is `{address recipient, uint16 bps}`. `recipient` is the payout address (must be non-zero and unique across the array). `bps` is the share in basis points (1% = 100, 100% = 10,000). The array length must be 1–10 and total bps must sum to 10,000. | [misc/interfaces/vaults/SplitVault.sol](https://github.com/flap-sh/gitbook/tree/main/developers/token-launcher-developers/misc/interfaces/vaults/SplitVault.sol) |
| Gift Vault (FlapXVault) | A gift vault that routes tax token revenue based on X (Twitter) proof and can fall back to snowball buyback. | yes                  | 0xa02DA44D67DB6D692efa7f751b5952bd670d5326 | ABI-encode `VaultData` with `{string xHandle}` (the fee manager’s X handle). The `xHandle` must be all lowercase.                                                                                                                                                                                         | [Gift Vault Guide](/flap/developers/vault-developers/gift-vault)                                                                                                 |

## Interfaces

### Split Vault interface

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

/// @title ISplitVault
/// @notice Interface for the Split Vault implementation.
/// @dev Derived from the on-chain SplitVault contract.
interface ISplitVault {
	/// @notice A payout recipient with a basis-point share.
	/// @param recipient The payout address (non-zero and unique).
	/// @param bps Share in basis points (1% = 100, 100% = 10,000).
	struct Recipient {
		address recipient;
		uint16 bps;
	}

	/// @notice Balance tracking for a recipient.
	/// @param accumulated Total BNB credited to the user.
	/// @param claimed Total BNB claimed by the user.
	struct UserBalance {
		uint128 accumulated;
		uint128 claimed;
	}

	/// @notice View structure returned by `getRecipientsInfo()`.
	/// @param recipient The payout address.
	/// @param bps Share in basis points.
	/// @param accumulated Total BNB credited to the user.
	/// @param claimed Total BNB claimed by the user.
	struct RecipientInfo {
		address recipient;
		uint16 bps;
		uint128 accumulated;
		uint128 claimed;
	}

	/// @notice Emitted when BNB is received and split across recipients.
	/// @param amount The total amount distributed.
	event FlapSplitVaultDistributed(uint256 amount);

	/// @notice Emitted when a recipient is dispatched funds.
	/// @param recipient The recipient address.
	/// @param amount The amount dispatched.
	event FlapSplitVaultDispatched(address recipient, uint256 amount);

	/// @notice Emitted when a recipient claims funds manually.
	/// @param user The recipient address.
	/// @param amount The amount claimed.
	event FlapSplitVaultClaimed(address user, uint256 amount);

	/// @notice Error when number of recipients exceeds the limit.
	error TooManyRecipients();

	/// @notice Error when the total basis points is not 10,000.
	error InvalidBpsSum();

	/// @notice Error when a recipient address is zero.
	error ZeroRecipient();

	/// @notice Error when no recipients are provided.
	error NoRecipients();

	/// @notice Error when recipients are duplicated.
	error DuplicateRecipient();

	/// @notice Initialize the vault after cloning.
	/// @param _taxToken The associated tax token address.
	/// @param _recipients Array of recipients and their shares.
	function initialize(address _taxToken, Recipient[] calldata _recipients) external;

	/// @notice Dispatch claimable balances to all recipients.
	function dispatch() external;

	/// @notice Claim the caller’s accumulated balance.
	/// @param user The recipient address to claim for.
	function claim(address user) external;

	/// @notice Return detailed information for all recipients.
	/// @return info Array of recipient info entries.
	function getRecipientsInfo() external view returns (RecipientInfo[] memory info);

	/// @notice Retrieve a recipient by index.
	/// @param index The recipient index.
	/// @return recipient The recipient address.
	/// @return bps The recipient share in basis points.
	function recipients(uint256 index) external view returns (address recipient, uint16 bps);

	/// @notice Retrieve user balance totals for a recipient.
	/// @param user The recipient address.
	/// @return accumulated Total BNB credited to the user.
	/// @return claimed Total BNB claimed by the user.
	function userBalances(address user) external view returns (uint128 accumulated, uint128 claimed);

	/// @notice The associated tax token address.
	function taxToken() external view returns (address);

	/// @notice The factory address that created this vault.
	function factory() external view returns (address);

	/// @notice The total basis points constant (10,000).
	function TOTAL_BPS() external view returns (uint256);
}
```

### Gift Vault (FlapXVault) interface

For detailed API integration guide and the complete interface, see the [Gift Vault Guide](/flap/developers/vault-developers/gift-vault).


# Robinhood Chain Integration Guide

> **TL;DR for terminal/bot integrators** — everything you need is in the table below. Skip the rest of the doc unless you need more context.

## Quick Reference

| # | Question                                               | Answer                                                                                                                                                                  |
| - | ------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| 1 | **Which token versions are supported?**                | Only **2** on Robinhood Chain: **Non-Tax Token** (`TOKEN_V2_PERMIT`) and **Tax Token V3** (`TOKEN_TAXED_V3`). All other token versions revert with `FeatureDisabled()`. |
| 2 | **Which migrator is supported?**                       | **`V2_MIGRATOR`** only, for both token types. `V3_MIGRATOR`, `V4_UNI_MIGRATOR`, `PCS_INFINITY_CL_MIGRATOR` all revert.                                                  |
| 3 | **What DEX are tokens migrated to?**                   | **Uniswap V2** (native fork on Robinhood Chain).                                                                                                                        |
| 4 | **Token implementation address + vanity suffix?**      | See table below.                                                                                                                                                        |
| 5 | **Bonding curve params per token type + quote token?** | See table below.                                                                                                                                                        |

### Deployed contract addresses (Robinhood Chain)

| Name                                                   | Address                                      | Notes                                                         |
| ------------------------------------------------------ | -------------------------------------------- | ------------------------------------------------------------- |
| Portal (proxy)                                         | `0x26605f322f7fF986f381bB9A6e3f5DAb0bEaEb09` | Main entry point for launches, trading, and token state reads |
| Standard Token Impl (Non-Tax Token, `TOKEN_V2_PERMIT`) | `0x88882688a067FE97E11C2185b996286e53132222` | Vanity suffix: `8888` · launched via `newTokenV5`             |
| Tax Token V3 Impl (`TOKEN_TAXED_V3`)                   | `0x7777C8743C88B3aff3cf262135beF2c8b2e83333` | Vanity suffix: `7777` · launched via `newTokenV6`             |
| VaultPortal                                            | `0xe9F7AB7DE8FB8756acbB6a1cd13316a43308197B` | Vault-integrated launch path                                  |
| Tax Token Helper                                       | `0xb10bD2672aE63735d677164A54B573a016f0203C` |                                                               |

Full cross-chain address list: [Deployed Contract Addresses](/flap/developers/deployed-contract-addresses).

### Bonding curve parameters

| Token Version | Quote Token               | Curve                 | Graduation Depth |
| ------------- | ------------------------- | --------------------- | ---------------- |
| Non-Tax Token | Native ETH (`address(0)`) | `CURVE_RH_TOSHI_5ETH` | \~5 ETH          |
| Tax Token V3  | Native ETH (`address(0)`) | `CURVE_RH_TOSHI_5ETH` | \~5 ETH          |

Native ETH (`address(0)`) is currently the only enabled quote token on Robinhood Chain, for both token types. Always confirm live config before integrating (see §2 below).

### Required launch params by token type

| Field                                  | Non-Tax Token (`newTokenV5`)    | Tax Token V3 (`newTokenV6`) |
| -------------------------------------- | ------------------------------- | --------------------------- |
| `tokenVersion`                         | n/a (implicit via `newTokenV5`) | `TOKEN_TAXED_V3`            |
| `taxRate` / `buyTaxRate`+`sellTaxRate` | `taxRate = 0`                   | non-zero                    |
| `migratorType`                         | `V2_MIGRATOR`                   | `V2_MIGRATOR`               |
| `quoteToken`                           | `address(0)`                    | `address(0)`                |

***

## 1. Token Versions & Launch Details

### Non-Tax Token

The only entry point that reaches the non-tax implementation on Robinhood Chain is:

```solidity
function newTokenV5(NewTokenV5Params calldata params) external payable returns (address token);
```

```solidity
IPortalTypes.NewTokenV5Params memory p;
p.name = "My Token";
p.symbol = "MTK";
p.meta = "";
p.dexThresh = IPortalCommonTypes.DexThreshType.FOUR_FIFTHS;
p.salt = salt; // vanity salt targeting the 8888 suffix
p.taxRate = 0;
p.migratorType = IPortalTypes.MigratorType.V2_MIGRATOR;
p.quoteToken = address(0);
p.quoteAmt = 0;
p.beneficiary = msg.sender;
p.dexId = IPortalTypes.DEXId.DEX0;
p.lpFeeProfile = IPortalTypes.V3LPFeeProfile.LP_FEE_PROFILE_STANDARD;
p.antiFarmerDuration = 1 days;

address token = portal.newTokenV5(p);
```

Public interface:

```solidity
interface IFlapNonTaxToken {
    function initialize(InitParams memory params) external;
    function removeTransferConstraints() external;      // owner-only, called by the migrator
    function metaURI() external view returns (string memory);
    function maxSupply() external view returns (uint256); // 1,000,000,000 ether for all launches
    function pools() external view returns (address v2, address v3);
    function antiFarmerExpirationTime() external view returns (uint256);
}
```

### Tax Token V3

```solidity
function newTokenV6(NewTokenV6Params calldata params) external payable returns (address token);
```

Robinhood Chain only accepts `tokenVersion == TOKEN_TAXED_V3` with `migratorType == V2_MIGRATOR` on this entry point — any other combination reverts with `FeatureDisabled()`.

***

## 2. Curve

Verify the live configuration on-chain before integrating (applies to both token types, since both currently use the same native-ETH quote config):

```solidity
IPortalTypes.QuoteTokenConfiguration memory config = portal.getQuoteTokenConfiguration(address(0));
require(config.enabled == 1, "native ETH quote not enabled");
require(
    uint8(config.defaultCurve) == uint8(IPortalCommonTypes.CurveType.CURVE_RH_TOSHI_5ETH),
    "unexpected curve"
);
```

### Bonding curve lifecycle (pre-graduation)

While a token is in its pre-graduation phase, **all transfers to or from any registered pool are blocked**. Only wallet-to-wallet transfers are allowed — this prevents any DEX liquidity provisioning or direct pool interaction before the token has graduated.

Buys and sells go through the standard Portal trade path:

```solidity
uint256 tokensOut = portal.swapExactInput{value: ethIn}(
    IPortalTradeV2.ExactInputParams({
        inputToken: address(0),
        outputToken: token,
        inputAmount: ethIn,
        minOutputAmount: 0,
        permitData: ""
    })
);
```

Graduation occurs automatically once the configured `DexThreshType` supply fraction has been sold on the curve (default `FOUR_FIFTHS`, i.e. 80% of supply):

```solidity
IPortalTypes.TokenStateV8Safe memory state = portal.getTokenV8Safe(token);
bool graduated = state.status == IPortalTypes.TokenStatus.DEX;
```

Observed graduation depth for the non-tax token (verified against the live Robinhood RPC, 1 ETH buy steps): graduation consistently occurs at the 6th buy round, just above 5 ETH cumulative spend.

***

## 3. Migration

Both supported token types (Non-Tax, Tax V3) migrate exclusively to **Uniswap V2** via `V2_MIGRATOR` — no other migrator type is accepted on this chain.

**Why the non-tax token is V2-only:** its `mainPool` is set to the Uniswap V2 pair address at initialization. During the post-graduation anti-farmer window, only `mainPool` is exempt from the pool-transfer block (see below). A V3/V4 migration would need to LP into a different, non-exempt pool address, which the anti-farmer check would then block — causing the migration itself to revert.

### Migration flow (non-tax token)

1. **Graduation trigger** — once bonding-curve supply crosses the DEX threshold, the Portal migrates the token's liquidity to a Uniswap V2 pool.
2. **Transfer-constraint release** — the migrator calls `IFlapNonTaxToken(token).removeTransferConstraints()` (owner-only). This starts the anti-farmer expiry countdown (`antiFarmerExpirationTime = now + antiFarmerDuration`).
3. **LP provisioning** — the remaining token balance and collected ETH reserve are added to the Uniswap V2 pair (`mainPool`), which is exempt from the anti-farmer block.

### Anti-farmer enforcement (non-tax token, post-graduation)

| Phase                                               | Behavior                                                                 |
| --------------------------------------------------- | ------------------------------------------------------------------------ |
| Pre-graduation                                      | ALL pool transfers blocked (both directions)                             |
| Anti-farmer window (post-graduation, before expiry) | Transfers to/from `mainPool` allowed; all other registered pools blocked |
| Free (after expiry)                                 | No restrictions — any transfer succeeds                                  |

This is a **hard revert**, not a tax — the non-tax token has no fee mechanism at all.

```solidity
uint256 expiry = IFlapNonTaxToken(token).antiFarmerExpirationTime();
bool antiFarmerActive = expiry != 0 && block.timestamp <= expiry;

(address v2Pool, address v3Pool) = IFlapNonTaxToken(token).pools();
```

**Scope limitation:** the block only applies to pools registered at initialization time — an address outside that set is not tracked and is not blocked. Anti-farmer here raises friction on the known/expected venues; it does not prevent farming through arbitrary unlisted pools.

### Trading after graduation

Once the anti-farmer window has expired (or when routing through `mainPool`, which is always exempt), trading is a normal Uniswap V2 swap. Continuing to route through the Portal also works:

```solidity
uint256 tokensOut = portal.swapExactInput{value: ethIn}(
    IPortalTradeV2.ExactInputParams({
        inputToken: address(0),
        outputToken: token,
        inputAmount: ethIn,
        minOutputAmount: 0,
        permitData: ""
    })
);
```

***

## Reference

* Deployed addresses: [Deployed Contract Addresses](/flap/developers/deployed-contract-addresses)


# Vault Developers


# Quick start for Vault Developers

Welcome to Flap Vault. Please follow the steps below to build your Vault.

{% hint style="info" %}
**Permissionless vault creation** — You can create and deploy your own vaults and vault factories **without any permission or registration**. Vault factories no longer need to be registered in the VaultPortal before they can be used to launch tokens, and users can use any vault they want when launching a token.
{% endhint %}

## 1. Start with the example repo

Work through the [FlapVaultExample repository](https://github.com/flap-sh/FlapVaultExample) for a complete, working example of a vault and vault factory implementation. It is the fastest way to understand the patterns you need to follow.

Quick-start Vault example: [`src/FreeCoinBeacon.sol`](https://github.com/flap-sh/FlapVaultExample/blob/main/src/FreeCoinBeacon.sol)

## 2. Confirm your design if needed

You can implement any vault logic you want — there is no gatekeeping if you don't need the low-risk badge. If you are unsure whether your Vault design, decentralization model, or implementation meets Flap's standard for the low-risk badge specifically, feel free to reach out before building too much and we can help review and improve your plan. Even if you do want the badge, you don't need to lock in your design up front — you can launch on the [beacon proxy pattern](/flap/developers/vault-developers/case-study-beacon-upgradeable-vault) and upgrade later if something needs to change.

## 3. Check if Flap infrastructure already solves part of your problem

Before you build a custom backend for your vault, check whether one of Flap's existing pieces of infrastructure already covers it. These are shared, audited services any vault can call into — you don't need to run your own signing service, scheduler, or oracle backend.

| If you need to...                                                                                                          | Use...                                                                                                                                                                                                                                                                                               |
| -------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| Schedule a delayed or MEV-protected callback (e.g. a keeper that fires a buyback, unlock, or distribution at a later time) | [Flap Trigger Service](/flap/developers/preview/flap-trigger-service) — request a `trigger()` callback for or after a given timestamp, executed by a trusted backend.                                                                                                                                |
| Verify that a specific X (Twitter) account posted a specific piece of text before letting a vault action proceed           | [X General Verifier](/flap/developers/preview/x-general-verifier) — a generalized, stateless on-chain verifier + off-chain oracle. Not limited to the fixed "gift the fee" sentence used by [Gift Vault](/flap/developers/vault-developers/gift-vault); you choose the substring your vault expects. |
| Get a verifiable LLM/AI decision on-chain (e.g. an AI-moderated vault or agent-driven logic)                               | [Flap AI Oracle](/flap/developers/preview/flap-ai-oracle) — commit-and-reveal AI reasoning with an on-chain auditable IPFS proof of every decision.                                                                                                                                                  |

{% hint style="info" %}
These pages live under **Preview** in the docs — they're still evolving, but the deployed contracts are live and usable today. If your vault design doesn't fit any of the patterns documented on those pages, [reach out to us](https://flap.sh) — we're happy to help you integrate, or to hear what new infrastructure you might need.
{% endhint %}

## 4. Run a basic self-check

After building your Vault, use the [Vault spec checker](https://github.com/flap-sh/FlapVaultExample/blob/main/.agents/skills/flap-vault-spec-checker/SKILL.md) for a basic review before you move on to writing tests.

## 5. Write integration tests

We highly recommend writing integration tests before submitting for audit.

Reference test: [`test/FreeCoinBeacon.mainnet.t.sol`](https://github.com/flap-sh/FlapVaultExample/blob/main/test/FreeCoinBeacon.mainnet.t.sol)

## 6. Test on BNB / Robinhood Testnet

You can also test your Vault on BNB Testnet or Robinhood Testnet: <https://testnet.flap.sh>

## 7. Audit and low-risk badge

Audit is **optional** — you can launch without one. However, if you want a low-risk badge, your Vault must pass an audit from our partner audit company.

Current cost: 0.5 BNB / 0.16 ETH per factory.

{% hint style="success" %}
If you launch your token using the recommended [beacon proxy pattern](/flap/developers/vault-developers/case-study-beacon-upgradeable-vault), you don't have to complete the audit before launch — you can launch first and do the audit afterwards. If the audit finds an issue, the vault can be upgraded through the beacon to fix it. See [Building Upgradeable Vaults — Beacon Proxy Pattern](/flap/developers/vault-developers/case-study-beacon-upgradeable-vault) for details.
{% endhint %}

## 8. Bespoke UI

If you want a custom UI, use the [flap-vault-component-template](https://github.com/flap-sh/flap-vault-component-template). See [Building a Vault UI](/flap/developers/vault-developers/building-a-vault-ui) for a full walkthrough: the four-file package boundary, manifest binding, SDK usage, the mandatory risk-status display, and the check/package/handoff pipeline.

After your Vault audit passes, you can submit the UI for review.

## 9. Optional: align on your launch plan with us

If you would like our support, you can align with the Flap team on your launch setup, such as whether to lock the CA, make the factory exclusive to a specific developer address, when to bind the UI, and when to display the low-risk badge.

Feel free to reach out to the Flap team anytime during the process. @I0ene @JingYixiaozhang @cedricflap

## Further reading

* Review the [Vault & VaultFactory specification](/flap/developers/vault-developers/vault-and-vaultfactory-specification) for the full interface definitions and how to build your own vaults and vault factories.
* Read the [Flap Tax Vault](/flap/developers/vault-developers/flap-tax-vault) documentation for an overview of the vault concept and VaultPortal integration.
* Explore registered (i.e. verified by Flap) vaults in the [Registered Vaults](/flap/developers/token-launcher-developers/registered-vaults) page to see available vault factories and their configuration schemas.
* Learn about the [Gift Vault](/flap/developers/vault-developers/gift-vault) family and how to integrate X proof-based revenue routing.
* Building a custom UI? See [Building a Vault UI](/flap/developers/vault-developers/building-a-vault-ui) for the full walkthrough based on the [flap-vault-component-template](https://github.com/flap-sh/flap-vault-component-template).
* Need a keeper, a social-proof check, or an on-chain AI decision? See [Flap Trigger Service](/flap/developers/preview/flap-trigger-service), [X General Verifier](/flap/developers/preview/x-general-verifier), and [Flap AI Oracle](/flap/developers/preview/flap-ai-oracle) — shared infrastructure you can plug into your vault instead of building your own.

## Available vault types

Flap currently supports the following official vault types:

1. **Split Vault** - Distributes BNB revenue across multiple recipients based on configurable basis points (percentage shares)
2. **Gift Vault** - Routes revenue based on X (Twitter) proof verification, with a fallback that varies by version (buyback & burn for V1, holder dividends for V2)

## Common workflows

1. **Launch a token with a custom vault** — You can launch a token and set the fund recipient wallet to your smart contract. If it follows the spec defined in the [Vault & VaultFactory specification](/flap/developers/vault-developers/vault-and-vaultfactory-specification), its info will be automatically available on our website and partner websites. No registration is required.
2. **Build a vault factory for others** — If you have an idea to build a vault to be used by others, you can build a `VaultFactoryBaseV2` and take a portion of the tax as a commission in each vault you create. Implement `vaultDataSchema()` so the UI can automatically render a launch form for your vault type. See the [Vault & VaultFactory specification](/flap/developers/vault-developers/vault-and-vaultfactory-specification) for full details.


# Deployed Contract Addresses

See [Deployed Contract Addresses](/flap/developers/deployed-contract-addresses).


# Vault & VaultFactory Specification

## Table of Contents

* [Quick start](#quick-start)
* [Overview](#overview)
* [The Vault Specification](#the-vault-specification)
  * [VaultBase / VaultBaseV2 / VaultBaseV3 (current interface)](#vaultbase--vaultbasev2--vaultbasev3-current-interface)
  * [Implementation requirements](#implementation-requirements)
  * [The Flap Guardian](#the-flap-guardian)
  * [Why the Beacon Proxy pattern matters](#why-the-beacon-proxy-pattern-matters)
  * [Adapter for legacy vaults](#adapter-for-legacy-vaults)
  * [VaultBaseV3 — ERC20 quote token support](#vaultbasev3--erc20-quote-token-support)
    * [How the ping works](#how-the-ping-works)
    * [Normative accounting model](#normative-accounting-model)
    * [Implementing a V3 vault — checklist](#implementing-a-v3-vault--checklist)
* [VaultFactory Specification](#vaultfactory-specification)
  * [IVaultFactory / VaultFactoryBaseV2 (current interface)](#ivaultfactory--vaultfactorybasev2-current-interface)
  * [Generic validation hook — `onBeforeLaunch(bytes)`](#generic-validation-hook--onbeforelaunchbytes)
  * [Spec version discovery — `factorySpecVersion()`](#spec-version-discovery--factoryspecversion)
  * [Policy discovery — `tokenCreationPolicies()`](#policy-discovery--tokencreationpolicies)
  * [v2.3: Computed dividend tokens — `resolveDividendToken`](#v23-computed-dividend-tokens--resolvedividendtoken)
  * [Recommended commission fee structure](#recommended-commission-fee-structure)
* [UI Schema Reference (IVaultSchemasV1)](#ui-schema-reference-ivaultschemasv1)
* [Get your vault verified](#get-your-vault-verified)
* [Appendix: Older spec versions](#appendix-older-spec-versions)

## Quick start

1. **Follow the example** — Work through the [FlapVaultExample repository](https://github.com/flap-sh/FlapVaultExample) to see a complete, working vault and vault factory implementation. It is the fastest way to understand the patterns you need to follow.
2. **Verify your implementation** — Once you have written your vault, use the [Vault spec checker](https://github.com/flap-sh/FlapVaultExample/blob/main/.agents/skills/flap-vault-spec-checker/SKILL.md) to automatically check that your implementation correctly follows this specification before deploying.

See the [Quick start for Vault Developers](/flap/developers/vault-developers/quick-start-vault-developers) page for the full onboarding checklist.

## Overview

Flap's vault system allows anyone to build smart contracts for managing and distributing BNB revenue generated from tax tokens. Vaults can be customized to fit various use cases — splitting revenue among multiple recipients, routing funds based on social proof, funding a game treasury, or anything else you can imagine.

{% hint style="info" %}
**Permissionless vault creation** — You can create and deploy your own vaults and vault factories **without any permission or registration**. Vault factories no longer need to be registered in the VaultPortal before they can be used to launch tokens. Users can use any vault they want when launching tokens.
{% endhint %}

{% hint style="warning" %}
For a complete working example of a vault and vault factory implementation, check out the [FlapVaultExample repository](https://github.com/flap-sh/FlapVaultExample).
{% endhint %}

This page documents the **current** vault and vault factory interfaces — `VaultBaseV3` (which extends `VaultBaseV2`, which extends `VaultBase`) and `VaultFactoryBaseV2` targeting spec `v2.3`. **We recommend all new vaults and factories target these latest interfaces** — `VaultBaseV3` (not `VaultBaseV2`) and factory spec `v2.3` (not `v2.2`) — even if your vault only ever handles the native gas token today: declaring `vaultQuoteToken() == address(0)` costs nothing and future-proofs the vault for ERC20 quote support. Older revisions (`VaultBase`/V1, `VaultBaseV2`, and the `v2.1`/`v2.2` factory validation surfaces) are fully backwards-compatible and are summarized in the [Appendix](#appendix-older-spec-versions) at the end of this page — they remain supported, but new vaults and factories should target the interfaces documented below, not the legacy ones.

## The Vault Specification

### VaultBase / VaultBaseV2 / VaultBaseV3 (current interface)

To be compatible with the VaultPortal system, your vault smart contract must inherit `VaultBaseV3` (which extends `VaultBaseV2`, which extends `VaultBase`) — this is the **recommended, latest interface** for all new vaults, regardless of whether your vault handles native or ERC20 quote revenue. This is the complete, current interface chain, pulled directly from [`FlapVaultExample`](https://github.com/flap-sh/FlapVaultExample/blob/main/src/flap/VaultBase.sol) and [`VaultBaseV3.sol`](https://github.com/flap-sh/FlapVaultExample/blob/main/src/flap/VaultBaseV3.sol) — including the current per-chain Portal and Guardian addresses (BNB mainnet, BNB testnet, and **Robinhood Chain**):

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

pragma solidity ^0.8.13;

/// @title VaultBase
/// @notice Abstract base contract for all vault implementations
/// @author The Flap Team
abstract contract VaultBase {
    /// @notice Error thrown when the current chain is not supported
    error UnsupportedChain(uint256 chainId);

    /// @notice Get the Portal address for the current chain
    /// @dev Currently supports BNB Chain (56), BNB Testnet (97), and Robinhood Chain (4663)
    /// @return portal The Portal contract address
    function _getPortal() internal view returns (address portal) {
        uint256 chainId = block.chainid;
        if (chainId == 56) {
            // BNB Chain Portal
            return 0xe2cE6ab80874Fa9Fa2aAE65D277Dd6B8e65C9De0;
        } else if (chainId == 97) {
            // BNB Testnet Portal
            return 0x5bEacaF7ABCbB3aB280e80D007FD31fcE26510e9;
        } else if (chainId == 4663) {
            // Robinhood Chain Portal
            return 0x26605f322f7fF986f381bB9A6e3f5DAb0bEaEb09;
        }
        revert UnsupportedChain(chainId);
    }

    /// @notice Get the Guardian address for the current chain
    /// @dev Currently supports BNB Chain (56), BNB Testnet (97), and Robinhood Chain (4663)
    /// @return guardian The Guardian contract address
    function _getGuardian() internal view returns (address guardian) {
        uint256 chainId = block.chainid;
        if (chainId == 56) {
            // BNB Chain Guardian address
            return 0x9e27098dcD8844bcc6287a557E0b4D09C86B8a4b;
        } else if (chainId == 97) {
            // BNB Testnet Guardian address
            return 0x76Fa8C526f8Bc27ba6958B76DeEf92a0dbE46950;
        } else if (chainId == 4663) {
            // Robinhood Chain Guardian address
            return 0x0000b48720d3B4ED6BC5031768B07F2b59270000;
        }
        revert UnsupportedChain(chainId);
    }

    /// @notice Returns a description of the vault
    /// @dev Must be overridden to provide a dynamic description based on the vault's current state
    /// @return A string describing the vault's current state and configuration
    function description() public view virtual returns (string memory);
}

/// @title VaultBaseV2
/// @author The Flap Team
/// @notice Extended abstract base contract for vault implementations that adds
///         on-chain UI schema discovery via `vaultUISchema()`.
/// @dev    Inherits all `VaultBase` obligations (`description()`, `_getPortal()`,
///         `_getGuardian()`, the Guardian mandate). New vault implementations
///         should extend `VaultBaseV2` instead of `VaultBase` directly, to gain
///         automatic UI generation.
abstract contract VaultBaseV2 is VaultBase {
    /// @notice Returns the UI schema describing which methods the UI should
    ///         render for this vault.
    /// @dev Each vault implementation **must** override this to describe its
    ///      own user-facing methods. See the UI Schema Reference section below
    ///      for the full `VaultUISchema` struct definition and rendering algorithm.
    /// @return schema The complete UI schema for this vault.
    function vaultUISchema() public pure virtual returns (VaultUISchema memory schema);
}

/// @title VaultBaseV3
/// @author The Flap Team
/// @notice Extended abstract base contract for vault implementations that adds
///         support for ERC20 quote tokens (stablecoins, tokenized equities / RWA)
///         as the vault's revenue currency, via `vaultQuoteToken()` discovery and
///         a normative balance-delta accounting model. See "VaultBaseV3 — ERC20
///         quote token support" below for the full spec (the ping mechanism and
///         the accounting rules a V3 vault must implement).
abstract contract VaultBaseV3 is VaultBaseV2 {
    /// @notice The revenue currency this vault accounts for.
    /// @dev MUST equal the quote token of the tax token this vault serves:
    ///      `address(0)` for the native gas token, the ERC20 address
    ///      otherwise. MUST be stable for the life of the vault and MUST NOT
    ///      revert. Consumed by the VaultPortal launch cross-check and by
    ///      lens/UI discovery.
    /// @return quoteToken The revenue currency (`address(0)` = native).
    function vaultQuoteToken() public view virtual returns (address quoteToken);

    /// @notice The vault-side spec revision this implementation conforms to.
    /// @dev Distinct from the factory-side `factorySpecVersion()` ("v2.x"
    ///      series): this string versions the VAULT contract surface.
    ///      Lenses read it to distinguish V3 vaults from legacy ones (which
    ///      implement neither this method nor `vaultQuoteToken()`).
    /// @return The spec version string, "v3" for this revision.
    function vaultSpecVersion() public pure virtual returns (string memory) {
        return "v3";
    }
}
```

{% hint style="success" %}
**Recommended: always inherit `VaultBaseV3`, not `VaultBaseV2`.** Even for native-only vaults, declaring `vaultQuoteToken()` to return `address(0)` costs nothing and future-proofs the vault — see [VaultBaseV3 — ERC20 quote token support](#vaultbasev3--erc20-quote-token-support) below for the full spec (motivation, the ping mechanism, the balance-delta accounting model, and the implementation checklist). `VaultBaseV2` remains fully supported for existing vaults, but new vaults should target `VaultBaseV3`.
{% endhint %}

### Implementation requirements

1. **Implement `description()`** — Return a dynamic string that describes the vault's current state. The description should change based on the vault's state (e.g. balance, streaming status, etc.).
2. **Implement `receive()`** — Accept revenue (BNB, or an ERC20 quote token if using `VaultBaseV3`) and process it according to your vault's logic. For `VaultBaseV3` vaults, `receive()` must follow the [balance-delta accounting model](#normative-accounting-model) described below.
3. **Implement `vaultUISchema()`** — Return a `VaultUISchema` describing every user-facing method your vault exposes, so the UI can automatically render an interaction page without custom code. See the [UI Schema Reference](#ui-schema-reference-ivaultschemasv1) section for the full struct definitions and worked examples.
4. **Guardian mandate** — If you have any permissioned functions that should be triggered by an external address, and it is not suitable to make them public (e.g. buyback which may be sandwich attacked), you **must** also give the Guardian address the permissions alongside other allowed addresses as a backup. See the [Flap Guardian](#the-flap-guardian) section for details.
5. **If inheriting `VaultBaseV3`** (recommended for all new vaults) — **override `vaultQuoteToken()`** to declare your revenue currency (`address(0)` for native), and follow the [balance-delta accounting model](#normative-accounting-model) in `receive()`. See [VaultBaseV3 — ERC20 quote token support](#vaultbasev3--erc20-quote-token-support) below for the full checklist.

### The Flap Guardian

The Flap Guardian is a privileged address that can always call permissioned functions in vault contracts that implement the Vault Specification. The Guardian serves as a backup mechanism to ensure that critical functions can be executed even if the primary authorized addresses are unable to do so.

At this moment, the Guardian is an empty but upgradeable contract managed by Flap:

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

pragma solidity ^0.8.13;

/// @title FlapGuardian
/// @notice Guardian contract for vault emergency management and CTO cases
/// @dev Currently does nothing, but this contract is upgradeable.
contract FlapGuardian {
    function version() external pure returns (string memory) {
        return "0.0.1";
    }
}
```

Ideally, we don't need to implement any functionality in the Guardian contract. It just serves as a trusted address that can step in when necessary to protect users' funds.

{% hint style="warning" %}
If your vault has permissioned functions, you **must** grant the Guardian the same permissions. The Guardian's access **must not** be revocable by any other account — only the Guardian itself may renounce its own access.

When using OpenZeppelin's `AccessControl`, override `revokeRole()`:

```solidity
function revokeRole(bytes32 role, address account)
    public
    override
    onlyRole(getRoleAdmin(role))
{
    address guardian = _getGuardian();
    if (account == guardian) {
        revert CannotRevokeGuardianRole();
    }
    super.revokeRole(role, account);
}
```

{% endhint %}

### Why the Beacon Proxy pattern matters

For any factory that will deploy more than a handful of vault instances, we recommend building on the **Beacon Proxy pattern** (`UpgradeableBeacon` + `BeaconProxy`, upgrade authority gated to the Flap Guardian). See [Building Upgradeable Vaults — Beacon Proxy Pattern](/flap/developers/vault-developers/case-study-beacon-upgradeable-vault) for the full implementation guide.

{% hint style="warning" %}
**The beacon proxy pattern is mandatory if you want the low-risk verification badge** — see [Get your vault verified](#get-your-vault-verified) below. Flap must reserve the ability to upgrade a verified vault if the audit (or a later production issue) finds a bug; that's only possible if upgrade authority sits behind a beacon, held exclusively by the Flap Guardian. It remains fully optional if you don't intend to pursue verification.
{% endhint %}

The beacon proxy pattern serves **two** distinct purposes:

1. **Launch before audit, fix after.** An audit is not required to launch a token (see [Get your vault verified](#get-your-vault-verified) below) — but it, and the beacon, are both required if you want the low-risk badge. If you launch on a beacon proxy and later decide to pursue an audit — or your audit turns up a bug — the implementation behind the beacon can be upgraded to ship the fix, without redeploying or migrating any already-launched token. Without a beacon, a discovered bug means the vault is stuck as-is, and the vault cannot be verified in the first place.
2. **Static analysis has blind spots.** An audit is fundamentally a static analysis of the code as written — it does not catch every issue that only manifests at runtime under real market conditions (unexpected call patterns, edge-case reentrancy, gas griefing under specific mempool conditions, etc.). The beacon proxy gives Flap a way to "save the day" and patch a vault that passed audit but still hit an issue in production, again without asking every token creator to migrate.

{% hint style="info" %}
If your vault is a one-instance-per-token, immutable-for-life design and you don't intend to pursue verification, you can skip the proxy and inherit `VaultBaseV3` directly. The beacon pattern is required for **any vault pursuing the low-risk verification badge**, and is the default recommendation for **factories that will deploy many vault instances** or anyone who wants to launch before completing an audit.
{% endhint %}

### Adapter for legacy vaults

If you have built a vault to receive tax token revenue before the Vault Specification was introduced, you can still make your vault compatible with the VaultPortal system by creating an adapter contract that inherits from `VaultBase` and wraps around your existing vault. The adapter will implement the `description()` method and forward calls to your legacy vault as needed. This way, you can leverage VaultPortal features without modifying your original vault contract.

Please reach out to our team for assistance in creating an adapter for your legacy vault.

### VaultBaseV3 — ERC20 quote token support

Historically, vaults only supported the native gas token (BNB / ETH) as the revenue currency. That's a consequence of how `receive()` gets triggered: a native value transfer invokes the recipient's `receive()`, but an ERC20 `transfer` executes the **token contract's** code, not the recipient's — the vault would receive the funds but never get a chance to run its own logic.

**`VaultBaseV3`** (extends `VaultBaseV2`) lifts that restriction so tokens quoted in an ERC20 — stablecoins, tokenized equities / RWA, or any other ERC20 — can use vaults too. It adds exactly two members on top of `VaultBaseV2` — `vaultQuoteToken()` and `vaultSpecVersion()` — shown in the [current interface](#vaultbase--vaultbasev2--vaultbasev3-current-interface) above.

{% hint style="success" %}
**`VaultBaseV3` is the recommended interface for all new vaults** — including native-only ones. Existing vaults that extend `VaultBase` or `VaultBaseV2` are unaffected and keep working unchanged (fully backwards-compatible), but new or upgraded vault implementations should extend `VaultBaseV3` rather than `VaultBaseV2`. Native-only vaults simply declare `vaultQuoteToken()` as `address(0)` to gain the explicit currency declaration at no cost.
{% endhint %}

#### How the ping works

An ERC20 `transfer` never invokes the recipient's code, so a vault receiving ERC20 tax revenue would otherwise have no way to know new funds arrived. V3 closes that gap with a **wake call ("ping"), sent by the protocol**: after every ERC20 quote payout, the TaxProcessor follows up with `wallet.call{value: 0}("")` — an empty-calldata, zero-value call to the vault. This invokes the vault's `receive()` exactly like a native transfer would, giving the vault a chance to run its accounting logic even though no value accompanies the call itself.

The TaxProcessor's dispatch upholds four invariants — design your vault against them:

1. **Transfer first, ping after.** By the time your `receive()` runs, the ERC20 revenue is already sitting in your balance. The ping is purely a wake-up signal, not the transfer itself.
2. **Ping failures are ignored.** If your `receive()` reverts, dispatch proceeds without you — the funds still arrive, but your logic did not run, and you will only recognize the revenue on the next wake. Do not rely on reverting to refuse revenue.
3. **Ping gas is the dispatcher's, not yours.** The protocol does not cap the wake call — it forwards the dispatch caller's remaining gas (EIP-150), and dispatch is permissionless. The protocol's keeper runs the **entire** dispatch (swaps, dividends, burn, up to four wallet payouts) under one fixed gas budget; a heavy `receive()` makes the keeper's dispatch of your token fail until someone re-dispatches with a higher gas limit. Keep `receive()` lightweight — update accounting, emit an event, return. Put heavy work (swaps, staking, buybacks) behind explicit public functions driven by your own automation or the protocol's keeper/trigger services.
4. **You may be pinged spuriously.** The same wallet can occupy several payout slots and get pinged more than once per dispatch, and anyone can call your `receive()` at any time. A wake that finds no new revenue **must** be a silent no-op.

Native revenue is unchanged — it still arrives as a plain value transfer invoking `receive()`. A V3 vault handles both currencies through the same code path; the accounting model below is currency-agnostic.

{% hint style="warning" %}
Because the ping carries no value, `msg.value` is useless for recognizing ERC20 revenue. The vault must instead compare its **current quote balance** against a stored baseline — see the accounting model below.
{% endhint %}

#### Normative accounting model

Since a ping never carries an amount, a V3 vault must track revenue by **balance-delta accounting**: compare the vault's current quote-token balance against a stored baseline, and treat any increase as newly-recognized revenue.

```solidity
uint256 public accountedQuote; // recognized-and-not-yet-spent revenue

receive() external payable { _syncRevenue(); }

function _syncRevenue() internal {
    address quoteToken = vaultQuoteToken();
    uint256 bal = quoteToken == address(0)
        ? address(this).balance
        : IERC20(quoteToken).balanceOf(address(this));
    if (bal <= accountedQuote) return;            // rule 2: idempotent no-op
    uint256 newRevenue = bal - accountedQuote;
    accountedQuote = bal;                          // rule 1: advance baseline
    _onRevenue(newRevenue);                        // lightweight only (ping law 3)
}
```

Rules:

1. **Recognize by delta, never by raw balance.** Crediting `balanceOf(this)` as "new revenue" double-counts everything already recognized. Only `balance - accountedQuote` is new.
2. **Zero-delta wakes are silent no-ops.** Required by ping law 4 above.
3. **Every outflow MUST decrement `accountedQuote`** by the amount spent, in the same transaction. Forgetting this leaves the baseline above the real balance, and `bal <= accountedQuote` then suppresses revenue recognition forever — the vault deadlocks. This is the single most dangerous mistake a V3 vault can make.
4. **Direct transfers are credited late, and that is fine.** Funds sent to the vault outside dispatch (donations, airdrops, misdirected transfers) trigger no ping; they are recognized on the next wake and are indistinguishable from revenue. Treat them as donations.
5. **Recognize before you act.** Any function whose behavior depends on available revenue — spending it, paying it out, or gating on it — **must** run the sync routine first, so its outcome never depends on whether a ping happened to arrive beforehand. Exposing a permissionless `sync()` wrapper is **recommended**: it is the neutral, side-effect-free recognition entry point for keepers and UIs, and the recovery path when revenue arrives without a wake call (direct transfers, or pings administratively disabled on the processor).

{% hint style="danger" %}
Rule 3 is the most common way a V3 vault breaks in production. If an outflow function forgets to decrement `accountedQuote`, the baseline permanently exceeds the real balance and the vault stops recognizing new revenue forever — with no error, no revert, just silent deadlock.
{% endhint %}

#### Implementing a V3 vault — checklist

1. **Inherit from `VaultBaseV3`.** All obligations from `VaultBase` and `VaultBaseV2` still apply (implement `description()` and `vaultUISchema()`, use `_getPortal()` / `_getGuardian()`, honor the Guardian mandate).
2. **Override `vaultQuoteToken()`** to return the revenue currency this vault instance accounts for — `address(0)` for the native gas token, the ERC20 address otherwise. It **must** equal the quote token of the tax token this vault serves, **must** be immutable for the life of the vault (set it once at initialization from the factory's `newVault` parameters), and **must not** revert. VaultPortal cross-checks it at launch; lenses and UIs read it to label the vault and to warn when a vault is wired to a token with a different quote.
3. **Implement `receive()` with the balance-delta accounting model above.** This is required even if your quote is an ERC20 — the wake call lands there.
4. **Keep `vaultSpecVersion()` at the default** (`"v3"`) unless a later spec revision instructs otherwise.
5. **On the factory side** (see [`VaultFactoryBaseV2`](#ivaultfactory--vaultfactorybasev2-current-interface) below):
   * `isQuoteTokenSupported(quote)` **must** answer truthfully for your vault implementation. This is enforced on-chain at launch — the VaultPortal rejects any launch whose quote your factory does not support. Misdeclaring support strands your users' tax revenue in a vault that ignores it.
   * `factorySpecVersion()` **must** return `"v2.3"` (or higher) so the VaultPortal applies the V3 validation flow.
   * `newVault(taxToken, quoteToken, creator, vaultData)` receives the launch quote token — pass it into your vault's initializer.
   * Creation-time first buys in an ERC20 quote are funded by the launcher, not the vault: the VaultPortal pulls `quoteAmt` from the launching user (via an EIP-2612 permit signed with the VaultPortal as spender, or a prior approval) and refunds any partial fill. Frontends integrating your factory should request that approval or permit; the vault itself plays no part in launch funding.

{% hint style="success" %}
See [`VaultBaseV3`](https://github.com/flap-sh/FlapVaultExample/blob/main/src/flap/VaultBaseV3.sol) in the `FlapVaultExample` repository for the full NatSpec (this is the normative spec) and [`FreeCoinV3Beacon.sol`](https://github.com/flap-sh/FlapVaultExample/blob/main/src/FreeCoinV3Beacon.sol) for a quote-agnostic reference implementation that handles native and ERC20 revenue through the same code path.
{% endhint %}

## VaultFactory Specification

A vault factory deploys vault instances for new tax tokens. When a user launches a token through the `VaultPortal`, the portal calls your factory's `newVault()` method to create the vault.

{% hint style="info" %}
**No registration required** — Any contract that implements `VaultFactoryBaseV2` can be passed to the VaultPortal to launch tokens. You do not need to register your factory on-chain.
{% endhint %}

### IVaultFactory / VaultFactoryBaseV2 (current interface)

This is the complete, current factory interface, pulled directly from [`FlapVaultExample`](https://github.com/flap-sh/FlapVaultExample/blob/main/src/flap/VaultFactoryBaseV2.sol) — including the current per-chain VaultPortal and Guardian addresses (BNB mainnet, BNB testnet, and **Robinhood Chain**). **We recommend all new factories override `factorySpecVersion()` to return `"v2.3"`** (not the `"v2.2"` shown as the abstract base's default below) and implement `IVaultFactoryDividendV23` — see [Spec version discovery](#spec-version-discovery--factoryspecversion) below:

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

pragma solidity ^0.8.13;

/// @title IVaultFactory
/// @notice Interface that all vault factory contracts must implement
interface IVaultFactory {
    error OnlyVaultPortal();
    error ZeroAddress();

    /// @notice Creates a new vault instance for a tax token
    /// @dev IMPORTANT: The taxToken does not exist yet when this method is called.
    ///      The VaultPortal predicts the token address and passes it here.
    ///      The actual token will be created AFTER the vault is created.
    /// @param taxToken The predicted address of the tax token (not yet deployed)
    /// @param quoteToken The quote token address (e.g., address(0) for native BNB)
    /// @param creator The original msg.sender to VaultPortal who initiated token creation
    /// @param vaultData Custom encoded data specific to this vault type
    /// @return vault The address of the newly created vault
    function newVault(address taxToken, address quoteToken, address creator, bytes calldata vaultData)
        external
        returns (address vault);

    /// @notice Checks if a quote token is supported by this vault factory
    /// @param quoteToken The quote token address to check
    /// @return supported True if the quote token is supported, false otherwise
    function isQuoteTokenSupported(address quoteToken) external view returns (bool supported);
}

/// @title IVaultFactoryValidationV2
/// @notice Wrapper-agnostic pre-launch validation hook (spec v2.2+).
interface IVaultFactoryValidationV2 {
    /// @notice Normalized launch payload used by VaultPortal when talking to v2.2+ factories.
    struct LaunchValidationDataV1 {
        IPortalTypes.TokenVersion tokenVersion;
        address quoteToken;
        uint16 buyTaxRate;
        uint16 sellTaxRate;
        uint16 vaultBps;
        uint16 deflationBps;
        uint16 dividendBps;
        uint16 lpBps;
        address dividendToken;
        uint256 minimumShareBalance;
    }

    /// @notice Generic pre-launch validation hook.
    /// @param validationData ABI-encoded normalized launch payload.
    /// @return success True when the launch satisfies this factory's product constraints.
    /// @return reason Human-readable explanation when `success` is false.
    function onBeforeLaunch(bytes calldata validationData) external view returns (bool success, string memory reason);
}

/// @title IVaultFactoryDividendV23
/// @notice Optional factory-spec v2.3 extension for computed dividend-token resolution.
/// @dev    Only factories that explicitly opt into v2.3 need to implement this interface.
interface IVaultFactoryDividendV23 {
    /// @notice Resolve the dividend token for a predicted (not yet deployed) tax token.
    /// @dev    VaultPortal calls this only when the launcher supplied MAGIC_DIVIDEND_COMPUTED.
    /// @param predictedToken The CREATE2-predicted tax-token address.
    /// @param launchVersion `DIVIDEND_TOKEN_LAUNCH_VERSION_V6` or `DIVIDEND_TOKEN_LAUNCH_VERSION_V7`.
    /// @param launchParams ABI-encoded `NewTokenV6WithVaultParams` or `NewTokenV7WithVaultParams`.
    /// @return dividendToken The actual dividend token to forward into Portal.
    function resolveDividendToken(address predictedToken, uint8 launchVersion, bytes calldata launchParams)
        external
        view
        returns (address dividendToken);
}

/// @title VaultFactoryBaseV2
/// @author The Flap Team
/// @notice Extended abstract base contract for vault factory implementations that adds:
///           1. On-chain UI schema discovery via `vaultDataSchema()`
///           2. A `_getGuardian()` / `_getVaultPortal()` helper (mirrors VaultBase's pattern)
///           3. Policy discovery via `tokenCreationPolicies()`
///           4. `factorySpecVersion()` to identify the spec revision (default `"v2.2"`)
///           5. `onBeforeLaunch(bytes)` for wrapper-agnostic pre-launch validation
abstract contract VaultFactoryBaseV2 is IVaultFactory, IVaultFactoryValidationV2 {
    error UnsupportedChain(uint256 chainId);
    error LegacyV6ValidationHookNotImplemented();

    /// @notice Returns the schema describing the `vaultData` bytes expected by `newVault()`.
    /// @dev Each factory implementation **must** override this. See the UI Schema Reference
    ///      section for the full `VaultDataSchema` struct definition.
    function vaultDataSchema() public pure virtual returns (VaultDataSchema memory schema);

    /// @notice Deprecated legacy hook, kept only for factories that intentionally stay on the
    ///         legacy v2.1 validation surface. Reverts by default — see the Appendix for details.
    function onBeforeNewTokenV6WithVault(IVaultPortalTypes.NewTokenV6WithVaultParams calldata params)
        external
        virtual
        returns (bool, string memory)
    {
        params = params;
        revert LegacyV6ValidationHookNotImplemented();
    }

    /// @notice Wrapper-agnostic pre-launch validation hook (spec v2.2+). Default implementation
    ///         decodes the normalized payload and forwards it to `_validateBeforeLaunch(...)`.
    function onBeforeLaunch(bytes calldata validationData)
        external
        view
        virtual
        override
        returns (bool success, string memory reason)
    {
        IVaultFactoryValidationV2.LaunchValidationDataV1 memory data =
            abi.decode(validationData, (IVaultFactoryValidationV2.LaunchValidationDataV1));
        return _validateBeforeLaunch(data);
    }

    /// @notice Internal validation hook for spec-v2.2+ factories. Override this, not
    ///         `onBeforeLaunch` directly, to enforce your factory's product rules.
    function _validateBeforeLaunch(IVaultFactoryValidationV2.LaunchValidationDataV1 memory data)
        internal
        view
        virtual
        returns (bool success, string memory reason)
    {
        data = data;
        return (true, "");
    }

    /// @notice Returns the version of the VaultFactoryBaseV2 specification this contract
    ///         conforms to. Defaults to `"v2.2"` for backwards compatibility, but new
    ///         factories should override this to return `"v2.3"` — required to opt into
    ///         computed dividend tokens (also requires implementing `IVaultFactoryDividendV23`)
    ///         and to opt into ERC20 quote token support for `VaultBaseV3` vaults. Override to
    ///         `"v2.1"` only to intentionally stay on the legacy V6-only validation path.
    function factorySpecVersion() public pure virtual returns (string memory) {
        return "v2.2";
    }

    /// @notice Returns machine-readable UI hints describing the constraints this factory
    ///         enforces. Informational only — see the Policy Discovery section below.
    function tokenCreationPolicies() public pure virtual returns (FactoryPolicy[] memory policies) {
        return new FactoryPolicy[](0);
    }

    /// @notice Get the VaultPortal address for the current chain.
    /// @dev Currently supports BNB Chain (56), BNB Testnet (97), and Robinhood Chain (4663).
    function _getVaultPortal() internal view returns (address vaultPortal) {
        uint256 chainId = block.chainid;
        if (chainId == 56) {
            return 0x90497450f2a706f1951b5bdda52B4E5d16f34C06;
        } else if (chainId == 97) {
            return 0x027e3704fC5C16522e9393d04C60A3ac5c0d775f;
        } else if (chainId == 4663) {
            // Robinhood Chain — VaultPortal address
            return 0xe9F7AB7DE8FB8756acbB6a1cd13316a43308197B;
        }
        revert UnsupportedChain(chainId);
    }

    /// @notice Get the Guardian address for the current chain.
    /// @dev Currently supports BNB Chain (56), BNB Testnet (97), and Robinhood Chain (4663).
    function _getGuardian() internal view returns (address guardian) {
        uint256 chainId = block.chainid;
        if (chainId == 56) {
            return 0x9e27098dcD8844bcc6287a557E0b4D09C86B8a4b;
        } else if (chainId == 97) {
            return 0x76Fa8C526f8Bc27ba6958B76DeEf92a0dbE46950;
        } else if (chainId == 4663) {
            // Robinhood Chain Guardian address
            return 0x0000b48720d3B4ED6BC5031768B07F2b59270000;
        }
        revert UnsupportedChain(chainId);
    }
}
```

**Implementation requirements:**

* Implement `newVault()` and `isQuoteTokenSupported()` (from `IVaultFactory`).
* Override `vaultDataSchema()` to describe the fields your factory expects in `vaultData`. See the [UI Schema Reference](#ui-schema-reference-ivaultschemasv1) section.
* Override `_validateBeforeLaunch(...)` (not `onBeforeLaunch` directly) to enforce your factory's product rules.
* If your factory (or the vaults it creates) exposes any privileged / role-gated functions, the Guardian **must** also be granted the required role(s) — see the [Flap Guardian](#the-flap-guardian) mandate above; the same rule applies to factories.

### Generic validation hook — `onBeforeLaunch(bytes)`

`VaultPortal` normalizes launch parameters from whichever entrypoint the user called (`newTokenV6WithVault`, `newTokenV7WithVault`, ...) into the shared `LaunchValidationDataV1` payload and calls `onBeforeLaunch(bytes)` via `staticcall`. In most cases you should override `_validateBeforeLaunch(...)`, not `onBeforeLaunch` directly — the default `onBeforeLaunch` already handles decoding.

**Return value contract:**

| Outcome                             | Meaning                                                      | VaultPortal action                              |
| ----------------------------------- | ------------------------------------------------------------ | ----------------------------------------------- |
| Returns `(true, "")`                | Params are accepted                                          | Continue with token creation                    |
| Returns `(false, reason)`           | Params rejected                                              | Revert with `reason`                            |
| Reverts with error data             | Unexpected validation error                                  | Bubble up the revert                            |
| Selector missing / empty returndata | Factory claimed v2.2+ but does not expose the hook correctly | Revert with `"Factory validation hook missing"` |

{% hint style="info" %}
`onBeforeLaunch(bytes)` is **read-only**. It is called via `staticcall`, so it should only inspect inputs and return pass/fail + reason. Any stateful setup still belongs in `newVault()`.
{% endhint %}

**Concrete example — `GiftV4VaultFactoryV2`** (from the current `FlapTaxVaults` repo) enforces:

```solidity
function _validateBeforeLaunch(LaunchValidationDataV1 memory data)
    internal
    pure
    override
    returns (bool success, string memory reason)
{
    if (data.tokenVersion != IPortalTypes.TokenVersion.TOKEN_V3_PERMIT) {
        return (false, "Gift V4 requires TOKEN_V3_PERMIT.");
    }
    if (data.quoteToken != address(0)) {
        return (false, "Gift V4 currently supports native BNB only.");
    }
    if (data.dividendBps != 0) {
        return (false, "Use tracker-only dividend mode. The vault decides when to deposit rewards.");
    }
    if (data.dividendToken != address(0)) {
        return (false, "Dividend claims should be paid in native BNB (wrapped internally by the Dividend contract).");
    }
    if (data.buyTaxRate != 0 || data.sellTaxRate != 0) {
        return (false, "Gift V4 requires buyTaxRate = sellTaxRate = 0%.");
    }
    return (true, "");
}
```

### Spec version discovery — `factorySpecVersion()`

`factorySpecVersion()` tells `VaultPortal` which validation generation the factory belongs to:

* `v2.2`, `v2.3`, `v3.0`, etc. → use the generic `onBeforeLaunch(bytes)` path
* `v2.1` and below → treat as legacy / probe `onBeforeNewTokenV6WithVault(...)`
* missing / reverting selector → probe for the legacy V6 hook directly

The parser uses **major/minor semantics**, so anything `v2.2+` counts as part of the normalized pre-launch validation generation.

{% hint style="success" %}
**Recommendation: return `"v2.3"`, not `"v2.2"`, for all new factories.** Beyond the generic `onBeforeLaunch(bytes)` path shared by both, `"v2.3"` is required if you ever want to support computed dividend tokens or launch `VaultBaseV3` vaults with an ERC20 quote token — both are gated on `factorySpecVersion() >= "v2.3"`. There is no downside to declaring `"v2.3"` even if your factory doesn't use either feature today; implementing `IVaultFactoryDividendV23` itself remains optional.
{% endhint %}

{% hint style="warning" %}
Do not return a version string unless your factory actually supports the corresponding path. If `VaultPortal` detects `v2.2+`, it will call `onBeforeLaunch(bytes)` and expect a valid `(bool,string)` response. If it detects `v2.3+`, it may also call `resolveDividendToken(...)` — see below.
{% endhint %}

### Policy discovery — `tokenCreationPolicies()`

Policies are **machine-readable UI hints** that mirror whichever validation hook the factory actually uses — they let the UI show inline validation before a transaction is sent, but they enforce nothing on-chain by themselves.

```solidity
function tokenCreationPolicies() public pure virtual returns (FactoryPolicy[] memory policies) {
    return new FactoryPolicy[](0);
}
```

**`FactoryPolicy` struct:**

```solidity
struct FactoryPolicy {
    string target;      // Field name in normalized launch params (e.g. "dividendToken")
    string operator;    // Comparison operator (see table below)
    bytes  value;       // ABI-encoded expected value
    string description; // Human-readable hint shown in the UI
}
```

**Supported operators:**

| Operator  | Meaning                                        |
| --------- | ---------------------------------------------- |
| `"eq"`    | Field must equal `value`                       |
| `"neq"`   | Field must not equal `value`                   |
| `"gt"`    | Field must be strictly greater than `value`    |
| `"gte"`   | Field must be ≥ `value`                        |
| `"lt"`    | Field must be strictly less than `value`       |
| `"lte"`   | Field must be ≤ `value`                        |
| `"in"`    | Field must be one of a set (ABI-encoded array) |
| `"notIn"` | Field must not be any of a set                 |

Unknown operators must be **ignored** by the UI (forward-compatible).

**Worked example:**

```solidity
function tokenCreationPolicies() public pure returns (FactoryPolicy[] memory policies) {
    policies = new FactoryPolicy[](2);

    // dividendToken must equal a specific address (WBNB)
    policies[0] = FactoryPolicy({
        target:      "dividendToken",
        operator:    "eq",
        value:       abi.encode(address(0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c)),
        description: "Dividend token must equal the quote token (WBNB)."
    });

    // dividendBps must be at least 100 (1%)
    policies[1] = FactoryPolicy({
        target:      "dividendBps",
        operator:    "gte",
        value:       abi.encode(uint256(100)),
        description: "Dividend BPS must be at least 100 (1%) for this vault type."
    });
}
```

{% hint style="warning" %}
Policies are **informational only**. Always implement the actual enforcement in `_validateBeforeLaunch(...)`. Policies without a corresponding validation check have no effect on-chain.
{% endhint %}

### v2.3: Computed dividend tokens — `resolveDividendToken`

Factory spec **v2.3** adds support for a **computed dividend token** — a dividend token whose address cannot be known before the tax token is deployed, such as a Uniswap/PancakeSwap LP pair or a child token deployed deterministically from the tax token address.

At launch time, the caller passes the sentinel value `MAGIC_DIVIDEND_COMPUTED` as `dividendToken`. `VaultPortal` then checks that the factory declares `factorySpecVersion() >= "v2.3"`, calls `resolveDividendToken(predictedToken, launchVersion, launchParams)` on the factory, and substitutes the returned address as the real `dividendToken` before forwarding to Portal.

This is a fully optional, opt-in extension — factories that don't need computed dividend tokens can ignore `resolveDividendToken`/`IVaultFactoryDividendV23` entirely. However, **we still recommend declaring `factorySpecVersion() = "v2.3"`** (rather than staying on `"v2.2"`) even if you don't need computed dividend tokens, because `factorySpecVersion() >= "v2.3"` is also the gate `VaultPortal` checks before allowing a factory to launch `VaultBaseV3` vaults with an ERC20 quote token — see [VaultBaseV3 — ERC20 quote token support](#vaultbasev3--erc20-quote-token-support) above. See [Using an LP Token or Child Token as the Dividend Token](/flap/developers/vault-developers/tutorials/lp-token-as-dividend) for the full implementation guide, including the `IVaultFactoryDividendV23` interface, worked examples for LP-pair and child-token resolution, the on-chain flow diagram, and the version-gating table.

### Recommended commission fee structure

Vault factories can charge a commission fee from the tax revenue. The fee structure must be clearly described in the vault's `description()` method. The recommended fee calculation is based on the tax rate (`taxRateBps`) and the received tax revenue (`msg.value`):

* If `taxRate` ≤ 1% (100 bps), the fee is **6%** of `msg.value`.
* If `taxRate` > 1%, the fee is `(msg.value * 6) / taxRateBps`.

```solidity
receive() external payable {
    if (msg.value == 0) return;

    if (taxRateBps == 0) {
        try ITaxToken(taxToken).taxRate() returns (uint256 _taxRate) {
            if (_taxRate > 0) {
                taxRateBps = _taxRate;
            }
        } catch {}
    }

    uint256 fee = 0;
    if (taxRateBps <= 100) {
        // 6% of msg.value if taxRate <= 1%
        fee = msg.value * 600 / 10000;
    } else {
        // Examples:
        //   1% (100 bps)  → 6%
        //   2% (200 bps)  → 3%
        //   3% (300 bps)  → 2%
        //  10% (1000 bps) → 0.6%
        fee = (msg.value * 6) / taxRateBps;
    }

    // your main logic — accumulate fee or send fee
}
```

## UI Schema Reference (IVaultSchemasV1)

The shared struct definitions in `IVaultSchemasV1.sol` power the automatic UI generation for both vault factories (token launch forms) and vaults (vault interaction pages). This is the complete, current file:

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

pragma solidity ^0.8.13;

/// @notice Describes a single field (parameter, return value, or vault-data component).
/// @param name        Machine-readable name, e.g. "recipient", "bps", "amount".
/// @param fieldType   Solidity ABI type string, e.g. "string", "address", "uint256".
///                    The special value "time" is an alias for "uint256" whose encoded value
///                    is a Unix timestamp in seconds; the UI renders a date/time picker for
///                    inputs and a human-readable time string / countdown for outputs.
///                    The special value "msg.value" indicates the field is the native
///                    currency amount (wei) sent with the transaction — NOT ABI-encoded into
///                    calldata. Only valid for write-method inputs; at most one per method.
/// @param description Human-readable explanation shown as a label or tooltip.
/// @param decimals    Decimal precision hint for numeric fields. For factory encoding
///                    (inputs), the UI multiplies user input by 10^decimals before
///                    ABI-encoding. For vault display (outputs), the UI divides the raw
///                    on-chain value by 10^decimals for display. Always 0 for non-numeric
///                    fields.
struct FieldDescriptor {
    string name;
    string fieldType;
    string description;
    uint8 decimals;
}

/// @notice Describes the shape of the `vaultData` bytes expected by a factory's `newVault()`.
/// @dev    Supports two shapes: a single tuple (`isArray == false`) or an array of tuples
///         (`isArray == true`). If `fields` is empty and `isArray` is false, the factory
///         ignores `vaultData` entirely.
struct VaultDataSchema {
    string description;
    FieldDescriptor[] fields;
    bool isArray;
}

/// @notice An ERC-20 `approve` action the UI must execute before calling a write method.
/// @dev    The spender is always the vault contract itself. Known `tokenType` values:
///         "taxToken" (resolved via vault.taxToken()), "lpToken" (via vault.lpToken()).
///         The UI MUST ignore any `tokenType` it does not recognize (forward-compatible).
struct ApproveAction {
    string tokenType;
    string amountFieldName;
}

/// @notice Describes a single view or write method the UI should render for a vault.
/// @param name          Solidity method name, e.g. "claim", "stats", "dispatch".
/// @param description   Human-readable explanation, shown as subtitle/tooltip.
/// @param inputs        Ordered input parameters. Empty for no-arg methods.
/// @param outputs       Ordered return values. Empty for write methods.
/// @param approvals     ERC-20 approvals the UI must execute before sending a write tx.
/// @param isInputArray  True if the method's input is tuple[].
/// @param isOutputArray True if the method's return value is tuple[].
/// @param isWriteMethod True = state-changing (send tx); false = view (display result).
struct VaultMethodSchema {
    string name;
    string description;
    FieldDescriptor[] inputs;
    FieldDescriptor[] outputs;
    ApproveAction[] approvals;
    bool isInputArray;
    bool isOutputArray;
    bool isWriteMethod;
}

/// @notice Top-level schema describing the vault's entire UI surface. Returned by
///         `VaultBaseV2.vaultUISchema()`.
/// @param vaultType    Human-readable vault type identifier, e.g. "FlapXVault", "SplitVault".
/// @param description  Overall explanation of the vault for the UI.
/// @param methods      All methods the UI should render, in display order.
struct VaultUISchema {
    string vaultType;
    string description;
    VaultMethodSchema[] methods;
}

/// @notice A single constraint the factory enforces on the normalized launch validation
///         payload. Inspired by AWS IAM policy conditions — informational only, see
///         "Policy discovery" above for the operator table and enforcement rules.
/// @param target      Field name in the normalized launch params, e.g. "dividendToken".
/// @param operator    "eq" | "neq" | "gt" | "gte" | "lt" | "lte" | "in" | "notIn".
/// @param value       ABI-encoded expected value (type inferred from `target`).
/// @param description Human-readable hint shown in the UI.
struct FactoryPolicy {
    string target;
    string operator;
    bytes value;
    string description;
}
```

**How the UI uses `VaultDataSchema` (factory side):**

1. UI calls `factory.vaultDataSchema()` to get the schema.
2. For each field in `fields`, render the appropriate input widget based on `fieldType` (`"string"` → text, `"address"` → address input with checksum, `"uint16"`/`"uint256"`/`"uint128"` → number input, `"bool"` → checkbox, `"bytes"`/`"bytes32"` → hex input, `"time"` → date/time picker).
3. If `isArray == true`, render an "Add Item" button for dynamic array entries.
4. Encode the user input: `abi.encode(tuple[])` if `isArray`, else `abi.encode(tuple)`. Pass the result as `vaultData` in `NewTaxTokenWithVaultParams`.

**Example — Single tuple schema:**

```solidity
function vaultDataSchema() public pure override returns (VaultDataSchema memory schema) {
    schema.description = "Creates a staking vault. Specify a reward rate and lock duration.";
    schema.fields = new FieldDescriptor[](2);
    schema.fields[0] = FieldDescriptor("rewardRateBps", "uint16", "Annual reward rate in basis points", 0);
    schema.fields[1] = FieldDescriptor("lockDuration", "uint256", "Lock duration in seconds", 0);
    schema.isArray = false;
}
```

**Example — Array of tuples schema:**

```solidity
function vaultDataSchema() public pure override returns (VaultDataSchema memory schema) {
    schema.description = "Creates a vault that distributes received BNB "
        "among a dynamic set of payees by basis-point shares.";
    schema.fields = new FieldDescriptor[](2);
    schema.fields[0] = FieldDescriptor("payee", "address", "Payee wallet address", 0);
    schema.fields[1] = FieldDescriptor("bps", "uint16", "Basis points share (10000 = 100%)", 0);
    schema.isArray = true;  // vaultData = abi.encode((address,uint16)[])
}
```

**Example — Factory that ignores vaultData:**

```solidity
function vaultDataSchema() public pure override returns (VaultDataSchema memory schema) {
    schema.description = "Creates a vault with no configurable parameters. "
        "No user input is required — vaultData is ignored.";
    schema.fields = new FieldDescriptor[](0);
    schema.isArray = false;
}
```

**How the UI uses `VaultUISchema` (vault side):**

1. Display `vaultType` as a badge/header and `description` as subtitle.
2. Always call `vault.description()` and display the result as a dynamic status banner (polled periodically).
3. For each method: **view methods** (`isWriteMethod == false`) call immediately if no inputs, otherwise render input fields with a "Query" button. **Write methods** render a form with inputs; if `approvals` is non-empty, execute each `ApproveAction` (resolve token via `tokenType`, check allowance, call `approve` if needed) before sending the write transaction.
4. Methods are displayed in the order returned.

**Example — Hypothetical DonationVault:**

```solidity
function vaultUISchema() public pure override returns (VaultUISchema memory schema) {
    schema.vaultType = "DonationVault";
    schema.description = "Collects BNB donations and lets a designated charity withdraw.";
    schema.methods = new VaultMethodSchema[](3);

    // View: totalDonated() — no inputs, one uint256 output
    schema.methods[0].name        = "totalDonated";
    schema.methods[0].description = "Returns the total BNB donated so far.";
    schema.methods[0].outputs     = new FieldDescriptor[](1);
    schema.methods[0].outputs[0]  = FieldDescriptor("total", "uint256", "Total BNB donated", 18);
    schema.methods[0].approvals   = new ApproveAction[](0);

    // Write: donate() — msg.value input
    // fieldType "msg.value": the UI sets tx.value on the transaction instead of
    // ABI-encoding the amount as a calldata parameter. Only valid on write methods.
    schema.methods[1].name          = "donate";
    schema.methods[1].description   = "Send BNB as a donation.";
    schema.methods[1].inputs        = new FieldDescriptor[](1);
    schema.methods[1].inputs[0]     = FieldDescriptor("amount", "msg.value", "BNB to donate", 18);
    schema.methods[1].outputs       = new FieldDescriptor[](0);
    schema.methods[1].approvals     = new ApproveAction[](0);
    schema.methods[1].isWriteMethod = true;

    // Write: withdraw() — no inputs
    schema.methods[2].name          = "withdraw";
    schema.methods[2].description   = "Withdraws accumulated donations to the charity address.";
    schema.methods[2].inputs        = new FieldDescriptor[](0);
    schema.methods[2].outputs       = new FieldDescriptor[](0);
    schema.methods[2].approvals     = new ApproveAction[](0);
    schema.methods[2].isWriteMethod = true;
}
```

**Example — StakingVault with approve actions:**

```solidity
function vaultUISchema() public pure override returns (VaultUISchema memory schema) {
    schema.vaultType = "StakingVault";
    schema.description = "Stakes tax tokens or LP tokens to earn rewards.";
    schema.methods = new VaultMethodSchema[](2);

    // View: stakedBalance(address)
    schema.methods[0].name = "stakedBalance";
    schema.methods[0].description = "Returns the staked balance for a given user.";
    schema.methods[0].inputs = new FieldDescriptor[](1);
    schema.methods[0].inputs[0] = FieldDescriptor("user", "address", "The user address to query", 0);
    schema.methods[0].outputs = new FieldDescriptor[](1);
    schema.methods[0].outputs[0] = FieldDescriptor("balance", "uint256", "Staked token balance", 18);

    // Write: deposit(uint256) — requires ERC-20 approval of the tax token
    schema.methods[1].name = "deposit";
    schema.methods[1].description = "Stake tax tokens into the vault to earn rewards.";
    schema.methods[1].inputs = new FieldDescriptor[](1);
    schema.methods[1].inputs[0] = FieldDescriptor("amount", "uint256", "Amount of tax tokens to stake", 18);
    schema.methods[1].approvals = new ApproveAction[](1);
    schema.methods[1].approvals[0] = ApproveAction("taxToken", "amount");
    schema.methods[1].isWriteMethod = true;
}
```

**Example — NoteVault (string input, paginated array output):**

```solidity
function vaultUISchema() public pure override returns (VaultUISchema memory schema) {
    schema.vaultType   = "NoteVault";
    schema.description = "A public on-chain diary. Anyone can submit a note with an optional BNB tip. "
                         "All entries are stored on-chain and readable page-by-page.";

    schema.methods = new VaultMethodSchema[](3);

    // View: noteCount() — no inputs, plain uint256 output (raw count, decimals = 0)
    schema.methods[0].name        = "noteCount";
    schema.methods[0].description = "Total number of notes submitted.";
    schema.methods[0].inputs      = new FieldDescriptor[](0);
    schema.methods[0].outputs     = new FieldDescriptor[](1);
    schema.methods[0].outputs[0]  = FieldDescriptor("count", "uint256", "Total notes", 0);
    schema.methods[0].approvals   = new ApproveAction[](0);

    // View: getNotes(uint256 offset, uint256 limit) — paginated, returns Note[]
    // isOutputArray = true: outputs describe the fields of *each tuple* in the returned array,
    // not a single return value. The UI renders a table with one row per Note.
    schema.methods[1].name           = "getNotes";
    schema.methods[1].description    = "Fetch a page of notes. Use offset + limit to paginate.";
    schema.methods[1].inputs         = new FieldDescriptor[](2);
    schema.methods[1].inputs[0]      = FieldDescriptor("offset", "uint256", "Start index (0-based)", 0);
    schema.methods[1].inputs[1]      = FieldDescriptor("limit",  "uint256", "Max items to return",   0);
    schema.methods[1].outputs        = new FieldDescriptor[](3);
    schema.methods[1].outputs[0]     = FieldDescriptor("author",    "address", "Author address", 0);
    schema.methods[1].outputs[1]     = FieldDescriptor("createdAt", "time",    "Submitted at",   0);
    schema.methods[1].outputs[2]     = FieldDescriptor("content",   "string",  "Note content",   0);
    schema.methods[1].isOutputArray  = true;   // ← output is Note[], not a single tuple
    schema.methods[1].approvals      = new ApproveAction[](0);

    // Write: submitNote(string content) — string input + optional msg.value tip
    schema.methods[2].name          = "submitNote";
    schema.methods[2].description   = "Submit a note. Attach a BNB tip (optional, set 0 to skip).";
    schema.methods[2].inputs        = new FieldDescriptor[](2);
    schema.methods[2].inputs[0]     = FieldDescriptor("content", "string",    "Note content",      0);
    schema.methods[2].inputs[1]     = FieldDescriptor("tip",     "msg.value", "Optional BNB tip", 18);
    schema.methods[2].outputs       = new FieldDescriptor[](0);
    schema.methods[2].approvals     = new ApproveAction[](0);
    schema.methods[2].isWriteMethod = true;
}
```

## Get your vault verified

{% hint style="info" %}
Getting your vault verified is **optional**. You can launch a token with your vault today without any audit or verification — see the [Quick start for Vault Developers](/flap/developers/vault-developers/quick-start-vault-developers) guide. Verification only matters if you want the **low-risk badge** shown on Flap's website and partner sites.
{% endhint %}

To get your vault or vault factory verified, you must **pass an audit from our partner audit company** (current cost: 0.5 BNB / 0.16 ETH per factory). Please reach out to our team to schedule this.

{% hint style="warning" %}
**The** [**beacon proxy pattern**](/flap/developers/vault-developers/case-study-beacon-upgradeable-vault) **is mandatory to get verified — it is not optional for the low-risk badge.** Flap must be able to actually fix any issue the audit finds, and the only way to do that without asking every token creator to migrate is upgrade authority behind a beacon, held by the Flap Guardian. A non-upgradeable vault/factory cannot be verified, full stop — see [Why the Beacon Proxy pattern matters](#why-the-beacon-proxy-pattern-matters) above for the full rationale.

This also means you don't need to complete the audit before launch: you can launch first on the beacon and pursue the audit afterwards. If the audit surfaces an issue, the vault is upgraded through the beacon to fix it — that upgrade path is precisely what Flap reserves the Guardian's authority for.
{% endhint %}

Before reaching out, make sure:

* You have correctly implemented the Vault Specification (the `description()` method, the Guardian access to permissioned functions, and `vaultUISchema()` / `vaultDataSchema()`).
* Your vault and factory contracts are deployed behind the beacon proxy pattern, with upgrade authority gated **exclusively** to the Flap Guardian. This is a **hard requirement for verification** — Flap reserves the right to upgrade the vault if the audit (or a later production issue) surfaces a bug, so that a fix can ship without asking every token creator to migrate. A non-upgradeable factory/vault, or one where upgrade authority sits with any address other than the Guardian, cannot be verified. If you need to change a non-beacon factory in the future, you must deploy a new factory contract and get it verified again.
* The commission fee structure is clearly described in the vault's `description()` method.

## Appendix: Older spec versions

The current interface (`VaultBaseV3` and `VaultFactoryBaseV2` targeting spec `v2.3`, documented above) is **fully backwards-compatible** with everything below. Existing vaults built against these older revisions continue to work unmodified — this section exists purely for historical reference. New vaults and factories should target `VaultBaseV3` and factory spec `v2.3`, documented earlier on this page, not these older revisions.

| Version | Vault base contract                   | Factory base contract                                        | Key addition                                                                                                                                                                               |
| ------- | ------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| V1      | `VaultBase`                           | `IVaultFactory`                                              | Core spec — `description()`, Guardian mandate                                                                                                                                              |
| V2      | `VaultBaseV2` (extends `VaultBase`)   | `VaultFactoryBaseV2` (implements `IVaultFactory`)            | On-chain UI schema for automatic UI generation                                                                                                                                             |
| V2.1    | *(no change)*                         | `VaultFactoryBaseV2` (updated)                               | Legacy V6 validation hook (`onBeforeNewTokenV6WithVault`) + machine-readable policy discovery                                                                                              |
| V2.2    | *(no change)*                         | `VaultFactoryBaseV2` (updated)                               | Wrapper-agnostic validation via `onBeforeLaunch(bytes)` and normalized launch payloads — this became the default                                                                           |
| V2.3    | *(no change)*                         | `VaultFactoryBaseV2` + `IVaultFactoryDividendV23` (optional) | Computed dividend tokens via `resolveDividendToken` — see [dedicated tutorial](/flap/developers/vault-developers/tutorials/lp-token-as-dividend)                                           |
| V3      | `VaultBaseV3` (extends `VaultBaseV2`) | `VaultFactoryBaseV2` (`factorySpecVersion() >= "v2.3"`)      | ERC20 quote token support via `vaultQuoteToken()` + the ping/balance-delta accounting model — see [VaultBaseV3 — ERC20 quote token support](#vaultbasev3--erc20-quote-token-support) above |

**V1 — `VaultBase` / `IVaultFactory`:** The original spec. Vaults implement `description()` and the Guardian mandate; factories implement `newVault()` and `isQuoteTokenSupported()`. No UI schema discovery — every new vault type needed a hand-built UI.

**V2.1 — legacy validation hook:** Before the wrapper-agnostic `onBeforeLaunch(bytes)` hook existed, factories that wanted to validate launch parameters had to override `onBeforeNewTokenV6WithVault(NewTokenV6WithVaultParams calldata params)`, called by `VaultPortal` only for V6-style launches:

```solidity
function onBeforeNewTokenV6WithVault(IVaultPortalTypes.NewTokenV6WithVaultParams calldata params)
    external
    virtual
    returns (bool success, string memory reason)
{
    revert LegacyV6ValidationHookNotImplemented();
}
```

`VaultPortal` interprets the low-level call result as: `(true, "")` → accept and continue; `(false, reason)` → revert with `reason`; revert with error data → propagate; selector missing / empty returndata → treat as "no legacy hook present". This path is still reachable today by overriding `factorySpecVersion()` to return `"v2.1"`, for factories that intentionally stay on the legacy V6-only validation surface instead of adopting `onBeforeLaunch(bytes)`.

**Migrating from V1 to the current spec:** extend `VaultBaseV2` / `VaultFactoryBaseV2` instead of `VaultBase` / `IVaultFactory` directly, then implement `vaultUISchema()` / `vaultDataSchema()`. No other changes are required — all V1 obligations still apply unchanged.


# Flap Tax Vault

## Table of Contents

* [Overview](#overview)
* [VaultPortal](#vaultportal)
  * [Deployed Addresses](#deployed-addresses)
  * [Get Vault Info](#get-vault-info)
  * [Get Audit Reports for A token](#get-audit-reports-for-a-token)
  * [Launch A Token With A Vault](#launch-a-token-with-a-vault)

## Overview

When you use a smart contract as the "Funds Recipient Wallet", our system will try to parse it as a `vault`.

<figure><img src="https://2671086575-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F5KujUBwRoHVjn8OZEgtZ%2Fuploads%2FA6wu9nXyZMOsoNAt3IgO%2Fimage.png?alt=media&amp;token=956f3ffe-53a4-418f-94c7-43bb68a9da34" alt="" width="563"><figcaption></figcaption></figure>

If you implement your smart contract following our Vault Specification, your Vault's information can be shown on our website.

<figure><img src="https://2671086575-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F5KujUBwRoHVjn8OZEgtZ%2Fuploads%2FhIYk0hlhj6pK0ExMueWT%2Fimage.png?alt=media&amp;token=cf218161-b486-4c8d-b646-f9fb7815f422" alt="" width="563"><figcaption></figcaption></figure>

Furthermore, if your vault is verified by our team or our auditing partners, your vault's "unverified" risk level can be updated to the corresponding risk level based on the audit results. And all audit reports will be uploaded to ipfs and available on our website for users to check. Users will be able to do their own research on the vaults before investing in your token.

{% hint style="info" %}
You can now create and deploy your own tax vaults **without any permission or registration**. Users can also use any vault they want when launching tokens. Vault factories no longer need to be registered in the VaultPortal before they can be used. See the [Vault & VaultFactory specification](/flap/developers/vault-developers/vault-and-vaultfactory-specification) page for full details on how to build your own vaults and vault factories.
{% endhint %}

{% hint style="success" %}
If you are a developer, you can build a vault factory that deploys vaults following our Vault Specification. Once your vault factory is verified by our team or our auditing partners, all vaults deployed from your factory will have the same risk level as your factory. This way, you don't need to submit each vault for verification individually. Besides, you can charge a fee for each vault deployed from your factory to build your business upon Flap. Please refer to the [Vault & VaultFactory specification](/flap/developers/vault-developers/vault-and-vaultfactory-specification) page for more details.
{% endhint %}

## VaultPortal

The `VaultPortal` is a registry that manages vault factories and their deployed vaults. It provides a unified interface for interacting with vaults across different chains.

### Deployed Addresses

| Chain       | VaultPortal Address                        |
| ----------- | ------------------------------------------ |
| BNB mainnet | 0x90497450f2a706f1951b5bdda52B4E5d16f34C06 |
| BNB testnet | 0x027e3704fC5C16522e9393d04C60A3ac5c0d775f |

The ABI of the VaultPortal contract is as follows:

{% file src="/files/GankgjlEVKt1RdWhSGQw" %}

### Get Vault Info

We have two methods for fetching the vault info of a token:

* `tryGetVault(address taxToken)` : This method first tries to find the vault created through the VaultPortal. If not found, it will try to find the vault by searching all registered vault factories and manually verified vaults. **You should use this method for most cases to avoid unnecessary reverts.**
* `getVault(address taxToken)` : This method returns vault that created through the VaultPortal.

```solidity

/// @notice Complete vault information returned to external callers
/// @dev This struct is used for view functions and includes human-readable description
/// @param vault The address of the vault contract
/// @param vaultFactory The address of the vault factory that created this vault (zero address if unknown)
/// @param description Human-readable description of the vault's purpose and functionality
/// @param isOfficial Whether this vault is marked as official/endorsed by the protocol
/// @param riskLevel The risk level classification from audit
struct VaultInfo {
    address vault;
    address vaultFactory;
    string description;
    bool isOfficial;
    RiskLevel riskLevel;
}

/// @notice Get the complete vault information for a tax token
/// @dev Reverts if no vault is found for the given tax token
/// @dev Attempts to fetch the vault's description by calling its description() function
/// @param taxToken The address of the tax token
/// @return info The VaultInfo struct containing complete vault details
function getVault(address taxToken) external view returns (VaultInfo memory info);

/// @notice Attempt to get vault information for a tax token without reverting
/// @dev First checks the internal mapping, then falls back to searching the Portal
/// @dev For fallback results, isOfficial and isVerified will always be false
/// @param taxToken The address of the tax token
/// @return found Whether a vault was found for this tax token
/// @return info The VaultInfo struct (empty if not found)
function tryGetVault(address taxToken) external view returns (bool found, VaultInfo memory info);

```

### Get Audit Reports for A token

For each token, you can get its audit reports with pagination support using the getAuditReports method. If the token itself has no audit reports, it will fall back to its vault's factory's audit reports.

```solidity


/// @notice Audit report for a tax token
/// @param auditor The address of the auditor who submitted this report
/// @param riskLevel The risk level classification from this audit
/// @param ipfsCid The IPFS CID of the audit report document
struct AuditReport {
    address auditor;
    RiskLevel riskLevel;
    string ipfsCid;
}

/// @notice Get recent audit reports for a tax token with pagination
/// @dev Returns reports starting from the most recent (end of array)
/// @dev If the token has no audit reports, falls back to its factory's audit reports
/// @param taxToken The address of the tax token
/// @param offset The number of reports to skip from the end (0 = most recent)
/// @param limit The maximum number of reports to return
/// @return reports The array of audit reports
/// @return total The total number of audit reports for this token
function getAuditReports(address taxToken, uint256 offset, uint256 limit)
    external
    view
    returns (AuditReport[] memory reports, uint256 total);
```

### Launch A Token With A Vault

How to launch a token using a vault?

Vault is a system built outside of the main Flap Bonding Curve Protocol. To launch a token with a vault, you need to use the `VaultPortal` contract:

```solidity
pragma solidity ^0.8.13;

import {IPortalTypes} from "./IPortal.sol";

/// @title IVaultPortalTypes
/// @notice Type definitions for the VaultPortal contract
/// @dev Contains all structs and custom types used by VaultPortal
interface IVaultPortalTypes {
   /// @notice Parameters required to create a new tax token with an associated vault
    /// @dev All parameters are passed as a single struct to avoid stack-too-deep errors
    /// @param name The name of the tax token (e.g., "MyToken")
    /// @param symbol The symbol of the tax token (e.g., "MTK")
    /// @param meta Metadata URI or string for additional token information
    /// @param dexThresh The DEX supply threshold type
    /// @param salt A unique salt for deterministic address generation (must produce vanity suffix)
    /// @param taxRate The tax rate in basis points (e.g., 100 = 1%, max 1000 = 10%)
    /// @param migratorType The migrator type (see MigratorType enum)
    /// @param quoteToken The token used for initial liquidity (address(0) for native token)
    /// @param quoteAmt The amount of quote token to provide as initial liquidity
    /// @param permitData The optional permit data for the quote token
    /// @param extensionID The ID of the extension to be used for the new token if not zero
    /// @param extensionData Additional extension specific data
    /// @param dexId The preferred DEX ID for the token
    /// @param lpFeeProfile The preferred V3 LP fee profile for the token
    /// @param taxDuration Tax duration in seconds (max: 100 years)
    /// @param antiFarmerDuration Anti-farmer duration in seconds (max: 1 year)
    /// @param mktBps Market allocation basis points (to beneficiary)
    /// @param deflationBps Deflation basis points (burned)
    /// @param dividendBps Dividend basis points (to dividend contract)
    /// @param lpBps Liquidity provision basis points (LP to dead address)
    /// @param minimumShareBalance Minimum balance for dividend eligibility
    /// @param vaultFactory The address of the vault factory to use for creating the vault
    /// @param vaultData Encoded data specific to the vault type being created
    struct NewTaxTokenWithVaultParams {
        string name;
        string symbol;
        string meta;
        IPortalTypes.DexThreshType dexThresh;
        bytes32 salt;
        uint16 taxRate;
        IPortalTypes.MigratorType migratorType;
        address quoteToken;
        uint256 quoteAmt;
        bytes permitData;
        bytes32 extensionID;
        bytes extensionData;
        IPortalTypes.DEXId dexId;
        IPortalTypes.V3LPFeeProfile lpFeeProfile;
        uint64 taxDuration;
        uint64 antiFarmerDuration;
        uint16 mktBps;
        uint16 deflationBps;
        uint16 dividendBps;
        uint16 lpBps;
        uint256 minimumShareBalance;
        address vaultFactory;
        bytes vaultData;
    }
}
```

The parameters are similar to the `newTokenV5` of the `Portal` contract, with two additional parameters at the end: `vaultFactory` and `vaultData`.


# Gift Vault

## Overview

The **Gift Vault** lets you assign an X (Twitter) account as the **Gift Owner** for a token. The gift owner can prove control of that X account and direct the vault's fee flow to any EVM address of their choice — and change that address anytime by presenting a new proof.

You can use it as a way to give tax fee to an X user, or build more interesting use cases on top of it. It makes it easier for a web2 developer or an agent to manage tax fee routing through social proof. Some ideas:

* Create an X account for an agent and launch a tax token that assigns the agent as the gift owner. The agent routes the tax fee to any EVM address, accepting funding requests via tweets. This could work as charity funds or a VC that supports new projects.
* A social game where players tweet to a game X account and get the tax fee routed to their address based on social tasks or achievements.

This page documents the **current Gift Vault V2** product line, which is what new integrations should target. The legacy **Gift Vault V1 (FlapXVault)** is documented in the [Appendix: Gift Vault V1 (legacy)](#appendix-gift-vault-v1-legacy) at the end of this page — the API integration is identical between versions, so if you already support V1 you mainly need to read the "What's different in V2" section.

## Lifecycle

Gift Vault V2 has three states:

### 1. `ACCUMULATING`

* Initial state after deployment.
* Vault accepts native BNB fees from the token.
* Funds stay inside the vault while waiting for a valid X proof to assign the streaming recipient.

### 2. `STREAMING`

* Entered after a valid `manageByProof()` call.
* All accumulated BNB is flushed immediately to `streamingTarget`.
* Future BNB is forwarded to the same target on receipt.
* The target can be updated again by a newer valid proof.

### 3. `DIVIDEND`

* Entered after `createdAt + timeoutPeriod` if streaming was never activated. Default timeout is **7 days**, configurable at the factory level for future vaults.
* Sticky once entered.
* Vault wraps native BNB into the dividend token (normally WBNB) and deposits the wrapped balance into the token's Dividend contract for holders.

{% hint style="info" %}
This is a change from Gift Vault V1, whose grace-period fallback was buyback & burn instead of holder dividends. See the [Appendix](#appendix-gift-vault-v1-legacy) for the V1 lifecycle.
{% endhint %}

## Network Details

### BNB Smart Chain Mainnet

* `GiftV4VaultFactoryV2`: `0x6909aD1822Ece349CDDAb98E6F62EeeD9fAa2e10`
* `GiftTaxVaultFactoryV2`: `0xFb7ccc4Fd09Da5b7016A18d51e227Af4ABE53f44`

### BNB Smart Chain Testnet

* `GiftV4VaultFactoryV2`: `0xaa00b4CeFeE8b4BF7D2E23C21ae82cfC9AFC9D67`
* `GiftTaxVaultFactoryV2`: `0x2eDD880AB36b07bD030BDa28A13c3E9148C11622`

## Product matrix

Gift Vault V2 has two product options. The difference is mainly the launch constraints of the selected factory.

### `GiftV4VaultFactoryV2`

Use this factory for the zero-tax Gift V4 product.

Launch constraints:

* `tokenVersion == TOKEN_V3_PERMIT`
* `quoteToken == address(0)`
* `buyTaxRate == 0`
* `sellTaxRate == 0`
* `dividendBps == 0`
* `dividendToken == address(0)`

### `GiftTaxVaultFactoryV2`

Use this factory for the taxed Gift product.

Launch constraints:

* `tokenVersion == TOKEN_TAXED_V3`
* `quoteToken == address(0)`
* `buyTaxRate == sellTaxRate`
* `buyTaxRate` / `sellTaxRate` must both be one of: `100` (1%), `200` (2%), `300` (3%)
* `dividendBps == 0`
* `dividendToken == address(0)`

### Shared launch-time vault data

For both Gift Vault V2 product options, the only launch-time vault parameter is the gift owner's X handle.

## API integration guide

Gift Vault V2 uses the **exact same off-chain API** as the legacy Gift Vault V1 — the endpoints, request format, and response format are unchanged. If you already have a working V1 integration, you can reuse it as-is; the only thing that changes for V2 is the factory address and the on-chain launch constraints (product matrix above).

The mechanism, end to end:

1. **Assign Gift Owner** — the X account you specify becomes the gift owner who controls where fees go.
2. **7-Day Grace Period** — if the gift owner doesn't manage the fees at least once within the timeout, control is forfeited to the fallback (holder dividends for V2, buyback & burn for legacy V1).

### Step 1: Parse and validate tweet content

Integrators must fetch the tweet themselves. The tweet format is **strictly enforced**:

**Tweet Format:** `gift the fee from [tax_token_address] to [target_address] #FlapGift`

```typescript
interface ParsedTweet {
  targetAddress: string;
  taxTokenAddress: string;
}

interface Tweet {
  __typename: string;
  text?: string;
  user?: {
    screen_name?: string;
    legacy?: { screen_name?: string };
  };
}

function parseTweetText(text: string): ParsedTweet | null {
  const regex = /gift\s+the\s+fee\s+from\s+(0x[a-fA-F0-9]{40})\s+to\s+(0x[a-fA-F0-9]{40})\s+#FlapGift/i;
  const match = text.match(regex);
  if (!match) return null;
  return { taxTokenAddress: match[1], targetAddress: match[2] };
}

function validateTweetContent(
  tweet: Tweet,
  expectedXHandle: string,
  expectedTaxToken: string
): ParsedTweet {
  if (!tweet || tweet.__typename !== "Tweet") {
    throw new Error("Tweet is not valid (deleted, suspended, or not found)");
  }
  if (!tweet.text) {
    throw new Error("Tweet has no text content");
  }
  if (!tweet.user) {
    throw new Error("Tweet has no user data");
  }

  const tweetScreenName = tweet.user.screen_name || tweet.user.legacy?.screen_name;
  if (!tweetScreenName) {
    throw new Error("Tweet author screen_name not found");
  }
  if (tweetScreenName.toLowerCase() !== expectedXHandle.toLowerCase()) {
    throw new Error(`Tweet author mismatch. Expected: @${expectedXHandle}, Found: @${tweetScreenName}`);
  }

  const parsed = parseTweetText(tweet.text);
  if (!parsed) {
    throw new Error(
      "Tweet text does not match the required format. " +
      "Expected: 'gift the fee from [tax token address] to [EVM address] #FlapGift'"
    );
  }
  if (parsed.taxTokenAddress.toLowerCase() !== expectedTaxToken.toLowerCase()) {
    throw new Error(`Tax token mismatch. Expected: ${expectedTaxToken}, Found: ${parsed.taxTokenAddress}`);
  }

  return parsed;
}
```

### Step 2: ⚠️ MANDATORY PREFLIGHT CHECK

**Always call `canManageVault()` on-chain before submitting to the backend.** The backend is rate-limited to 1 request per minute per IP, and skipping this step risks an IP ban on rejected requests.

```typescript
import { Contract, JsonRpcProvider } from 'ethers';

const VAULT_FACTORY_ABI = [
  {
    "inputs": [
      {"internalType": "address", "name": "taxToken", "type": "address"},
      {"internalType": "string", "name": "xHandle", "type": "string"},
      {"internalType": "uint128", "name": "tweetId", "type": "uint128"}
    ],
    "name": "canManageVault",
    "outputs": [
      {"internalType": "bool", "name": "canManage", "type": "bool"},
      {"internalType": "string", "name": "errorMessage", "type": "string"}
    ],
    "stateMutability": "view",
    "type": "function"
  }
];

async function preflightCheck(
  provider: JsonRpcProvider,
  factoryAddress: string,
  taxToken: string,
  xHandle: string,
  tweetId: string
): Promise<void> {
  const contract = new Contract(factoryAddress, VAULT_FACTORY_ABI, provider);

  // xHandle MUST be lowercase for contract compatibility
  const result = await contract.canManageVault(taxToken, xHandle.toLowerCase(), BigInt(tweetId));
  const [canManage, errorMessage]: [boolean, string] = result;

  if (!canManage) {
    throw new Error(`PREFLIGHT CHECK FAILED: ${errorMessage} — DO NOT call backend API, request will be rejected!`);
  }
}
```

{% hint style="warning" %}

* `xHandle` must be **lowercase** when calling the contract.
* This is a **view function** — no gas required, instant response.
* Returns `[bool canManage, string errorMessage]`.
* **Always check this before calling the backend** to avoid rate-limit violations.
  {% endhint %}

### Step 3: Submit to backend API

**Only call if Step 2 passed.** Rate limit: 1 request per minute per IP; violating may result in an IP ban.

**Endpoint:** `POST {relayerEndpoint}/submit`

```typescript
interface SubmitRequest {
  tax_token: string;
  tweet_id: string;
}

interface SubmitResponse {
  message: string;
  x_proof: {
    target_address: string;
    tax_token: string;
    x_handle: string;
    x_id: string;
    tweet_id: string;
  };
  signature: string;
}

async function submitToBackend(
  relayerEndpoint: string,
  taxToken: string,
  tweetId: string
): Promise<SubmitResponse> {
  const response = await fetch(`${relayerEndpoint}submit`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ tax_token: taxToken, tweet_id: tweetId })
  });

  if (!response.ok) {
    const errorData = await response.json();
    throw new Error(`Backend error: ${errorData.error}`);
  }

  return response.json();
}
```

### Step 4: Poll for on-chain confirmation

The relayer processes transactions asynchronously and does not return a transaction hash — poll `canManageVault` to confirm success. Once processed, `canManageVault` returns `false` with error `"The tweet is outdated"`, which signals success.

```typescript
async function pollForConfirmation(
  provider: JsonRpcProvider,
  factoryAddress: string,
  taxToken: string,
  xHandle: string,
  tweetId: string
): Promise<boolean> {
  const TIMEOUT = 60000;      // 60 seconds
  const POLL_INTERVAL = 15000; // 15 seconds
  const startTime = Date.now();
  const contract = new Contract(factoryAddress, VAULT_FACTORY_ABI, provider);

  return new Promise((resolve, reject) => {
    const interval = setInterval(async () => {
      if (Date.now() - startTime >= TIMEOUT) {
        clearInterval(interval);
        reject(new Error("Polling timeout (60s) - transaction may still be processing"));
        return;
      }

      try {
        const [canManage, errorMessage] = await contract.canManageVault(taxToken, xHandle.toLowerCase(), BigInt(tweetId));
        if (!canManage && errorMessage === "The tweet is outdated") {
          clearInterval(interval);
          resolve(true);
        }
      } catch (error) {
        console.error("Polling error:", error);
      }
    }, POLL_INTERVAL);
  });
}
```

### Manual submission fallback

If the backend relayer times out after 60 seconds, you can submit the proof directly on-chain using the proof data returned from the backend:

```typescript
async function manualSubmit(
  factoryAddress: string,
  taxToken: string,
  proofData: SubmitResponse,
  signer: any
): Promise<void> {
  const MANUAL_SUBMIT_ABI = [
    {
      "inputs": [
        {"name": "taxToken", "type": "address"},
        {
          "name": "proof",
          "type": "tuple",
          "components": [
            {"name": "targetAddress", "type": "address"},
            {"name": "taxToken", "type": "address"},
            {"name": "xHandle", "type": "string"},
            {"name": "XId", "type": "uint256"},
            {"name": "tweetId", "type": "uint128"}
          ]
        },
        {"name": "signature", "type": "bytes"}
      ],
      "name": "manageByProof",
      "outputs": [],
      "stateMutability": "nonpayable",
      "type": "function"
    }
  ];

  const contract = new Contract(factoryAddress, MANUAL_SUBMIT_ABI, signer);
  const tx = await contract.manageByProof(
    taxToken,
    {
      targetAddress: proofData.x_proof.target_address,
      taxToken: proofData.x_proof.tax_token,
      xHandle: proofData.x_proof.x_handle,
      XId: BigInt(proofData.x_proof.x_id),
      tweetId: BigInt(proofData.x_proof.tweet_id)
    },
    proofData.signature
  );
  await tx.wait();
}
```

### `canManageVault()` reference

**Parameters:** `taxToken` (address), `xHandle` (string, must be lowercase), `tweetId` (uint128)

**Returns:** `canManage` (bool), `errorMessage` (string)

**Possible error messages:**

| Error Message                              | Meaning                         |
| ------------------------------------------ | ------------------------------- |
| "GiftVaultV2 not found for this tax token" | No vault exists                 |
| "xHandle does not match"                   | Wrong X handle                  |
| "Vault is already in dividend mode"        | Vault cannot be managed anymore |
| "The tweet is outdated"                    | Tweet already used (success!)   |
| "" (empty string)                          | Can manage (success)            |

### Error handling summary

| Error                               | Cause                  | Solution                             |
| ----------------------------------- | ---------------------- | ------------------------------------ |
| "Tweet is not valid"                | Deleted/suspended      | Use valid, active tweet              |
| "Tweet has no text"                 | Media-only tweet       | Ensure text content                  |
| "Author mismatch"                   | Wrong X handle         | Verify correct user                  |
| "Format mismatch"                   | Incorrect format       | Use exact format                     |
| "Token mismatch"                    | Wrong address          | Verify addresses match               |
| "Vault not found"                   | No vault exists        | Check token has a vault              |
| "xHandle does not match"            | Wrong vault owner      | Use correct X handle                 |
| "Vault is already in dividend mode" | Timed out / locked     | Cannot manage anymore                |
| "Tweet is outdated"                 | Already used (success) | Use new tweet ID for the next update |
| Rate limit error                    | Too many requests      | Wait 60 seconds                      |
| Network timeout                     | Backend slow           | Retry later                          |

{% hint style="danger" %}
**Key points:**

* NEVER skip the preflight check — it prevents IP bans.
* The backend is rate-limited (1/min) — no exceptions.
* `xHandle` must be lowercase for contract calls.
* Polling confirms success — the relayer is asynchronous.
* Consider showing the manual submission option to users after \~30 seconds of polling, while continuing to poll up to 60 seconds in the background.
  {% endhint %}

## Summary checklist

1. Fetch the tweet yourself.
2. Validate tweet content (author, format, tax token match).
3. ⚠️ MANDATORY: call `canManageVault()`. If `false`, **stop** — do not call the backend.
4. Submit to backend (only if preflight passed; rate limited to 1/min).
5. Poll `canManageVault()` every 15s for up to 60s. Success = error `"The tweet is outdated"`.
6. Implement timeouts and the manual submission fallback.

## Appendix: Gift Vault V1 (legacy)

This section documents the original Gift Vault (`FlapXVault`), kept for integrators who still support the legacy factory. **New integrations should use Gift Vault V2** (documented above) — the API integration mechanics above apply unchanged to V1; only the factory address, states, and fallback differ.

### V1 vs V2 summary

| Topic                 | Gift Vault V1                                    | Gift Vault V2                                                           |
| --------------------- | ------------------------------------------------ | ----------------------------------------------------------------------- |
| Product options       | one legacy Gift Vault flow                       | two product options: `GiftV4VaultFactoryV2` and `GiftTaxVaultFactoryV2` |
| API integration       | same API as V2                                   | same API, request format, and response format as V1                     |
| Fee routing lifecycle | `ACCUMULATING -> STREAMING -> FALLBACK_SNOWBALL` | `ACCUMULATING -> STREAMING -> DIVIDEND`                                 |
| Timeout fallback      | buyback & burn                                   | holder dividends                                                        |

### V1 lifecycle

1. **Assign Gift Owner** — the X account you specify becomes the gift owner who controls where fees go.
2. **Flexible Beneficiary** — the gift owner can assign fees to any EVM address and change it anytime.
3. **7-Day Grace Period** — if the gift owner doesn't manage the fees at least once in the first 7 days, control is forfeited.
4. **Fallback: Buyback & Burn** — if control is forfeited, fees are automatically used for buyback and burn.

### V1 Network Details

**BNB Smart Chain Mainnet**

* Gift Vault Factory: `0x025549F52B03cF36f9e1a337c02d3AA7Af66ab32`
* Relayer API Endpoint: `https://bnb-x-relayer.taxed.fun/submit`

**BNB Smart Chain Testnet**

* Gift Vault Factory: `0xa02DA44D67DB6D692efa7f751b5952bd670d5326`
* Relayer API Endpoint: `https://bnbtest-x-relayer.taxed.fun/submit`

### V1 Gift Vault interface (`IFlapXVault`)

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

/// @title IFlapXVault
/// @notice Interface for the legacy Gift Vault (FlapXVault) implementation.
interface IFlapXVault {
    enum State {
        ACCUMULATING,
        STREAMING,
        FALLBACK_SNOWBALL
    }

    enum BalanceUpdateType {
        ACCUMULATION,
        SNOWBALL
    }

    struct VaultStats {
        uint128 totalBNBSpent;
        uint128 totalTokenBurn;
    }

    struct ProofRecord {
        uint128 XId;
        uint128 tweetId;
        address targetAddress;
    }

    struct XProof {
        address targetAddress;
        address taxToken;
        string xHandle;
        uint128 XId;
        uint128 tweetId;
    }

    event FlapTaxVaultStateChanged(address token, uint8 newState);
    event FlapTaxVaultStreamingTargetUpdated(address token, address newTarget);
    event FlapSnowballBalanceUpdated(address token, address vault, uint256 newBalance, BalanceUpdateType updateType);
    event FlapStreamingForwardFailed(address token, address target, uint256 amount);

    error OutdatedProof(uint128 providedTweetId, uint128 lastTweetId);
    error InvalidState(State currentState);
    error EmptyXHandle();
    error InvalidProof();
    error MismatchedTaxToken();
    error MismatchedXHandle();
    error CannotRevokeGuardianRole();

    function SNOWBALL_ROLE() external view returns (bytes32);
    function DEAD_ADDRESS() external view returns (address);

    function initialize(address _taxToken, address _quoteToken, string calldata _xHandle, uint256 _timeoutPeriod)
        external;

    function state() external view returns (State);
    function transitState() external;
    function manageByProof(XProof calldata proof, bytes calldata signature) external;
    function snowball(uint256 quoteAmt) external;

    function getHistoricalProofs(uint256 offset, uint256 limit)
        external
        view
        returns (ProofRecord[] memory records, uint256 total);

    function description() external view returns (string memory);
    function revokeRole(bytes32 role, address account) external;
    function stats() external view returns (uint128 totalBNBSpent, uint128 totalTokenBurn);
    function taxToken() external view returns (address);
    function quoteToken() external view returns (address);
    function xHandle() external view returns (string memory);
    function timeoutPeriod() external view returns (uint256);
    function createdAt() external view returns (uint256);
    function factory() external view returns (address);
    function streamingTarget() external view returns (address);
    function streamedAmount(address beneficiary) external view returns (uint256);
    function lastTweetId() external view returns (uint128);
}
```


# Building a Vault UI

This page covers how to build a **custom UI** for your Vault using the [flap-vault-component-template](https://github.com/flap-sh/flap-vault-component-template). If you don't need a bespoke UI, Flap can render a generic interaction page automatically from your `vaultUISchema()` / `vaultDataSchema()` (see the [Vault & VaultFactory Specification](/flap/developers/vault-developers/vault-and-vaultfactory-specification)). Use this guide when you want full control over layout, copy, and business logic instead.

{% hint style="info" %}
This is Step 7 in the [Quick start for Vault Developers](/flap/developers/vault-developers/quick-start-vault-developers): build the bespoke UI **after** your Vault contract is written, tested, and (per your audit plan) submitted for review.
{% endhint %}

## Table of Contents

* [What the template is (and isn't)](#what-the-template-is-and-isnt)
* [The four-file package boundary](#the-four-file-package-boundary)
* [Quick start](#quick-start)
* [Scaffold your Vault package](#scaffold-your-vault-package)
* [manifest.json — declaring your binding](#manifestjson--declaring-your-binding)
* [The Flap SDK — what your component may call](#the-flap-sdk--what-your-component-may-call)
* [Mandatory: render the current risk status](#mandatory-render-the-current-risk-status)
* [Market-phase and wrong-network gating](#market-phase-and-wrong-network-gating)
* [A minimal Component.tsx walkthrough](#a-minimal-componenttsx-walkthrough)
* [Safety rules you must not violate](#safety-rules-you-must-not-violate)
* [Check, preview, and package](#check-preview-and-package)
* [Handoff to Flap Artifact Workbench](#handoff-to-flap-artifact-workbench)

## What the template is (and isn't)

`flap-vault-component-template` is **not** a free-form website container. A custom Vault UI is a controlled business component that runs inside Flap's runtime boundary:

* **Flap SDK** for chain reads/writes, wallet, oracle, i18n, notifications, formatting, and tx-error handling.
* **Flap-owned host context** (taxinfo/feeinfo) for token state, tax info, VaultPortal info, deployment binding, fee mode, and market phase — you consume this, you don't reimplement it.
* **Flap preview shell** with real wallet connect, chain switching, and language preference, so you can test against real chain data locally.
* **Packaged Vault artifacts** that contain only your Vault-specific business UI. The shell/header/frame around it is never part of your package.
* **A minimal manifest** declaring your deployment binding (chain + factory/vault/token), i18n locales, and any unavoidable non-oracle endpoints — nothing else.

The production flow, end to end:

```
private zip
  -> Flap Artifact Workbench validation
  -> Flap build
  -> component.mjs + manifest + i18n + metadata
  -> Flap static artifact storage
  -> Flap deployment binding
  -> flap.sh runtime loader
```

Your zip is never loaded by `flap.sh` directly — Flap builds the actual runtime artifact from it after validation.

## The four-file package boundary

Every Vault UI package lives at `src/vaults/{folder-name}/` and contains **exactly** these four files:

```
src/vaults/{folder-name}/
  Component.tsx     # the controlled React Vault UI component
  manifest.json      # artifactId, match.bindings, i18n
  VaultABI.ts        # minimal non-standard ABI fragments only
  i18n.json          # locale dictionaries for manifest.i18n
```

No `helpers/`, no nested components, no local asset files, no README, no extra docs. Any other file or subfolder is a blocking `vault:check` issue. The only exception is Mini App mode (`manifest.mode: "mini-app"`, for `8888`-suffix token-scoped artifacts only), which may also include reviewed top-level audio files (`.mp3`/`.wav`/`.ogg`/`.m4a`/`.aac`, ≤5 MiB each, ≤12 MiB total).

Folder naming is strict lowercase kebab-case: 3–64 characters, letters/numbers separated by single hyphens (e.g. `flap-nft-vault`). No spaces, underscores, uppercase, leading/trailing hyphens, or nested folders — the folder name is both your source directory and your local preview route (`/{folder-name}`).

## Quick start

```bash
git clone https://github.com/flap-sh/flap-vault-component-template
cd flap-vault-component-template
yarn
yarn dev
```

The template runs with no local `.env` file — it ships with a shared preview WalletConnect Project ID, BNB mainnet/testnet RPC fallbacks, and a host-presentation proxy target already wired up. Open the built-in examples:

```
http://localhost:3000/example                     # reward/oracle pattern: approve, simulate, write, claim, refetch
http://localhost:3000/dex-listed-example           # listed-only stage gate example
http://localhost:3000/action-gallery-example       # richer multi-state action button gallery
http://localhost:3000/community-buyback-example    # live example bound to a real BNB token/factory
http://localhost:3000/flapixel-example             # live NFT vault example
```

If you're unsure which example to start from, use `example` — it carries the default compact visual baseline that `vault:check` expects new Vault UIs to follow (a row-heavy dashboard with stacked metric cards is blocked by default).

## Scaffold your Vault package

Don't hand-create the four files — use `vault:scaffold`. Recommended for a factory-scoped mainnet launch:

```bash
yarn vault:scaffold my-vault --name "My Vault UI" \
  --chain 97 --factory 0xTestnetFactory --token 0xReal7777TestToken \
  --chain 56 --factory 0xMainnetFactory --locales en,zh
```

For a single Vault with no factory:

```bash
yarn vault:scaffold my-vault --name "My Vault UI" \
  --chain 56 --vault 0xVaultAddressRequired --token 0xReal7777TestToken --locales en,zh
```

This generates the strict four-file package, a stable `artifactId` (`vaultui_my-vault_<ULID>`), and registers `my-vault` in `src/vaults/index.ts` so `/my-vault` renders locally. Replace every placeholder address with a real deployed address before running the command — `vault:check` blocks zero addresses and reserved template placeholders.

If the four files already exist (e.g. an AI agent generated them from a manifest first), just register the local preview route:

```bash
yarn vault:register my-vault
```

## manifest.json — declaring your binding

`manifest.json` is intentionally small — it's a match/review declaration, not a place to configure runtime behavior. Minimal factory-scoped example:

```json
{
  "artifactId": "vaultui_my-vault_01HZY7J4S9D0W5XJ8H2Q3K4M5N",
  "name": "My Vault UI",
  "match": {
    "bindings": [
      {
        "chainId": 97,
        "factoryAddress": "0xTestnetFactoryRequired",
        "tokenAddresses": ["0xReal7777TestToken"]
      },
      { "chainId": 56, "factoryAddress": "0xMainnetFactoryRequired" }
    ]
  },
  "i18n": ["en", "zh"]
}
```

There are exactly three supported match modes, and you never bind by a "type" field:

| Mode                      | Shape                                                                        |
| ------------------------- | ---------------------------------------------------------------------------- |
| Factory-scoped            | `chainId` + non-zero `factoryAddress`                                        |
| Vault-scoped (no factory) | `chainId` + exactly one `vaultAddresses` entry (+ optional `tokenAddresses`) |
| Token-scoped (no factory) | `chainId` + one or more `tokenAddresses`                                     |

Every binding-scoped `tokenAddresses` entry — including proof tokens on factory bindings — must be a **real deployed** ERC20 token address ending in `7777` or `8888`. At least one binding in the manifest must include one of these, since it's what `vault:check`/E2E test against. In factory mode, `tokenAddresses` is package proof, **not** the production CA restriction — that's a Workbench/registry `caRestrictionMode` decision (`none` / `reserved` / `verified`), never a manifest field.

Do **not** declare in `manifest.json`: `id`, `owner`, `version`, `actions`, `oracles`, `media`, `fallback`, `contracts`, `chainIds`, or global `tokenAddresses`. Actions live in `Component.tsx` via SDK calls; oracle usage is auto-detected by `vault:check`, not declared.

Optional per-binding field: `externalContracts` — for a truly fixed contract address that isn't your runtime token/vault/factory:

```json
{
  "chainId": 56,
  "factoryAddress": "0xFactoryAddressRequired",
  "externalContracts": [
    { "address": "0xExternalContractIfNeeded", "label": "Reward distributor" }
  ]
}
```

See [`docs/manifest.md`](https://github.com/flap-sh/flap-vault-component-template/blob/main/docs/manifest.md) in the template repo for the full schema, including Mini App and fullscreen-layout variants.

## The Flap SDK — what your component may call

Import everything from the public aliases — never introduce another SDK-like wrapper:

```ts
import { useFlapSdk, useVaultContext, useFlapI18n, useFlapNotify, useFlapWallet } from "@/src/sdk";
import { Button, Card, CardHeader, CardTitle, CardContent, Metric, StatusBadge, Alert, TxButton } from "@/src/ui";
```

Common SDK methods:

```ts
sdk.readContract<T>(request)
sdk.simulateContract(request)
sdk.writeContract(request)
sdk.waitForTx(hash)
sdk.readOracle<T>(oracleId, params)
sdk.refetch(keys)
sdk.openExplorerTx(hash)
```

Contract calls should target `context.vaultAddress`, `context.tokenAddress`, `context.factoryAddress`, or a binding-scoped `tokenAddresses`/`vaultAddresses`/`externalContracts` entry — never a hardcoded address, and never an unrelated router/bridge/aggregator contract. `vault:check` blocks calls to undeclared fixed addresses.

For methods with multiple return values, type the read as a tuple array — not an object — even if the ABI names the outputs:

```ts
// function poolInfo() view returns (uint256 currentPool, uint256 totalReceived)
type PoolInfoTuple = readonly [currentPool: bigint, totalReceived: bigint];
```

Use the SDK's standard ERC20 ABI for normal token flows instead of copying it into `VaultABI.ts`:

```ts
import { erc20Abi, standardErc20Abi } from "@/src/sdk";
```

Only add fragments to `VaultABI.ts` for your Vault's own non-standard methods.

## Mandatory: render the current risk status

**Every default Vault UI must visibly render the current contract risk status.** This is a hard, checker-enforced requirement (the only exception is Mini App mode on `8888`-suffix tokens).

```ts
import { readTaxVaultHostContext, useFlapSdk } from "@/src/sdk";

const { context } = useFlapSdk();
const host = readTaxVaultHostContext(context.host);
const riskLevel = host.vaultInfo?.riskLevel ?? host.taxInfo?.vaultInfo?.riskLevel ?? null;
```

Rules:

* The risk status must appear within the **first three visible Vault-specific business rows/blocks**, and **before** any hero, banner, preview, chart, or large visual block. The safest placement is the first `CardHeader` badge row.
* If `riskLevel` is unavailable, render a prominent warning that risk-status integration is required — don't silently omit it. `vault:check` treats a missing risk-status integration as a blocking issue.
* Never hardcode or unconditionally render a "Low risk" badge or reassuring copy. A low-risk label may only appear when it's selected from the host-derived `riskLevel === 1` branch.

## Market-phase and wrong-network gating

Every action-bearing Vault must decide whether each button is available in `internal-market`, `dex-listed`, `both`, or `read-only` stage, and gate it at runtime:

```ts
import { isActionAvailableForPhase, readTaxVaultHostContext, useFlapSdk } from "@/src/sdk";

const { context, wallet } = useFlapSdk();
const host = readTaxVaultHostContext(context.host);
const actionsAvailable = isActionAvailableForPhase("both", host.marketPhase);
```

Show unavailable actions with a clear disabled state — don't hide them silently. Wrong-network handling is a separate, orthogonal concern:

```ts
if (sdk.wallet.isWrongNetwork) {
  await sdk.wallet.switchChain();
}
```

Keep write buttons visible but disabled when `sdk.wallet.isWrongNetwork` is true, and prompt a chain switch before sending any transaction.

## A minimal Component.tsx walkthrough

The built-in `example` package (`src/vaults/example/Component.tsx`) is the canonical reference: a reward vault with deposit/approve/claim actions, live oracle data, and full risk/phase gating. Its shape, condensed:

```tsx
"use client";

import { erc20Abi, formatTokenAmount, handleTxError, isActionAvailableForPhase, parseTokenAmount, readTaxVaultHostContext, useFlapSdk } from "@/src/sdk";
import { exampleVaultAbi } from "./VaultABI";
import { Alert, Button, Card, CardContent, CardHeader, CardTitle, Metric, StatusBadge, TxButton } from "@/src/ui";

export default function ExampleRewardVault() {
  const sdk = useFlapSdk();
  const { context, i18n } = sdk;
  const t = i18n.t;

  const host = readTaxVaultHostContext(context.host);
  const riskLevel = host.vaultInfo?.riskLevel ?? host.taxInfo?.vaultInfo?.riskLevel ?? null;
  const actionsAvailable = isActionAvailableForPhase("both", host.marketPhase);

  // ...read vaultInfo / myInfo / balance / allowance via sdk.readContract,
  // gate writes with actionsAvailable + sdk.wallet.isWrongNetwork,
  // route deposit() through approve() first when allowance is insufficient.

  return (
    <div className="w-full space-y-3">
      <Card>
        <CardHeader>
          <CardTitle>{t("title")}</CardTitle>
          {/* Risk badge must appear here — first CardHeader row, before any hero/preview */}
          <StatusBadge tone={riskLevel === 1 ? "success" : "warning"}>
            {riskLevel === 1 ? t("states.riskLow") : t("states.riskMissing")}
          </StatusBadge>
        </CardHeader>
        <CardContent>
          <Metric label={t("labels.totalDeposited")} value={formatTokenAmount(/* ... */)} />
          {/* deposit / approve / claim TxButton flow, gated by actionsAvailable + isWrongNetwork */}
        </CardContent>
      </Card>
    </div>
  );
}
```

Key habits this example demonstrates:

* Reads run on mount and on a polling interval; writes call `sdk.refetch()` (or reload data directly) after `sdk.waitForTx(...)` resolves.
* `needsApproval` is computed by comparing live `allowance` to the parsed input amount, and the deposit button transparently routes through `approve()` first when needed.
* Every user-facing string comes from `i18n.t(...)` — never hardcoded text — since `i18n.json` is validated against every locale declared in `manifest.i18n`.
* Errors from failed reads/writes are normalized through `handleTxError(...)` rather than ad hoc string matching.

For layout consistency, read [`docs/ui-pattern-snippets.md`](https://github.com/flap-sh/flap-vault-component-template/blob/main/docs/ui-pattern-snippets.md) in the template before building `Component.tsx` — it has sanitized card/action-panel/read-write-flow patterns. For icons, search [lucide-react](https://lucide.dev/icons/) before hand-writing SVG.

## Safety rules you must not violate

These are blocked by default and will fail `vault:check`:

* `window.ethereum.request`, `eval`/`new Function`, raw `<iframe>` or `srcDoc`
* dynamic imports, CommonJS `require(...)`, runtime remote imports
* undeclared external URLs/endpoints/frames, or any endpoint that isn't a static absolute HTTPS string covered by `manifest.endpoints`
* browser storage, navigation, workers, `postMessage`, permission APIs, clipboard access
* direct browser network/media APIs (`XMLHttpRequest`, `WebSocket`, `EventSource`, `new Image()`)
* hidden transaction targets or contract calls to addresses outside your declared bindings
* remote images (immutable Vault images must use `IpfsImage`/`IpfsBackground` from `@/src/ui` with a static CID)
* relative imports other than `./VaultABI`
* binding by a "type" field instead of `chainId` + factory/vault/token address

Prefer SDK/on-chain reads over external endpoints and frames whenever the same result is achievable that way. If a non-oracle endpoint really is unavoidable, predeclare it as a static HTTPS string in `manifest.endpoints` — declaring it doesn't guarantee approval, it just makes it reviewable. See [`docs/manifest.md`](https://github.com/flap-sh/flap-vault-component-template/blob/main/docs/manifest.md) for the full rule set, including the reviewed `ReviewedFrame` allowance for TradingView/DexScreener/GeckoTerminal charts.

If an immutable Vault image is already on Public IPFS but is not available through the Flap gateway, follow [Pin Existing IPFS Content](/flap/developers/pin-existing-ipfs-content) before packaging the Vault UI. Use the actual image or directory CID in `IpfsImage`/`IpfsBackground`, not a metadata CID or full gateway URL.

## Check, preview, and package

```bash
yarn vault:check my-vault      # JSON: ok, summary, agent.verdict, issues — fix every blocking issue
yarn vault:e2e my-vault        # deterministic Playwright gate: PC/iPad/H5, real/internal-market/DEX-listed/wrong-network
yarn vault:package my-vault    # writes dist/my-vault.zip, only after check + a current E2E report pass
yarn vault:verify-package dist/my-vault.zip
```

Preview your route with real runtime params before packaging:

```
http://localhost:3000/my-vault?chainId=56&factoryAddress=0x...&tokenAddress=0x...&vaultAddress=0x...
```

`vault:check` also enforces that your local checkout is exactly up to date with `origin/main` and that your `package.json` version matches the latest published `@flapsdk/vault-runtime` — a stale template checkout fails with `template-freshness/*` errors before it even reaches your Vault-specific issues. `vault:e2e` requires Playwright's Chromium (`yarn playwright install chromium` on first run).

## Handoff to Flap Artifact Workbench

The only deliverable Flap accepts is the zip produced by `yarn vault:package <folder-name>` — not a hand-made zip, not a prompt-only result, and not a package without a passing E2E proof. The zip embeds a package marker, npm runtime provenance, source/schema/E2E file hashes, and the E2E report; Workbench rejects anything missing that proof.

Once you have a verified zip, follow [Step 6 (Audit and low-risk badge)](/flap/developers/vault-developers/quick-start-vault-developers) and [Step 8 (align on your launch plan)](/flap/developers/vault-developers/quick-start-vault-developers) from the Quick Start guide, then hand the zip to the Flap team.


# Building Upgradeable Vaults — Beacon Proxy Pattern

This guide explains how to build an upgradeable vault for the Flap protocol. The recommended pattern is **OpenZeppelin `BeaconProxy` + `UpgradeableBeacon`**, with the upgrade authority gated to the **Flap Guardian** for risk-control reasons.

The reference implementation lives in the [FlapVaultExample](https://github.com/flap-sh/FlapVaultExample) repository:

* Source: [`src/FreeCoinBeacon.sol`](https://github.com/flap-sh/FlapVaultExample/blob/main/src/FreeCoinBeacon.sol)
* Beacon-specific tests: [`test/FreeCoinBeacon.mainnet.t.sol`](https://github.com/flap-sh/FlapVaultExample/blob/main/test/FreeCoinBeacon.mainnet.t.sol)
* Bonding-curve and post-DEX dispatch tests (non-beacon variant, identical protocol surface): [`test/FreeCoin.mainnet.t.sol`](https://github.com/flap-sh/FlapVaultExample/blob/main/test/FreeCoin.mainnet.t.sol)
* Deployment scripts: [`script/mainnet/bnb/DeployFreeCoinBeacon.s.sol`](https://github.com/flap-sh/FlapVaultExample/blob/main/script/mainnet/bnb/DeployFreeCoinBeacon.s.sol), [`script/testnet/bnb/DeployFreeCoinBeacon.s.sol`](https://github.com/flap-sh/FlapVaultExample/blob/main/script/testnet/bnb/DeployFreeCoinBeacon.s.sol)

## Overview

For a new vault factory that will deploy more than a handful of vault instances, the recommended setup is:

1. Implement the vault as an **upgradeable implementation contract** with `_disableInitializers()` in the constructor and an `initialize(...)` function gated by the `initializer` modifier.
2. In the factory's constructor, deploy the implementation and an `UpgradeableBeacon` pointing at it. The factory becomes the beacon's owner.
3. In `newVault(...)`, deploy a `BeaconProxy` and pass `abi.encodeCall(YourVault.initialize, (...))` as the proxy's initialization calldata.
4. Expose `upgradeVaultImplementation(address)` and `lockVaultUpgrades()` on the factory, **gated to the Flap Guardian only**.
5. Cover bonding-curve dispatch, post-DEX dispatch, the Guardian-only upgrade path, and the irreversible lock with integration tests.

The rest of this guide walks through the rationale and the reference code.

## Why beacon proxies

For production vaults, deploy the vault implementation behind an **`UpgradeableBeacon`** and create one **`BeaconProxy`** per token. Compared with deploying many immutable vault instances:

1. **Operational flexibility** — if a bug or protocol change shows up after launch, upgrading a single beacon implementation rolls the fix out to every existing and future vault of this type. No redeployment, no migration ask of token creators.
2. **Cleaner factory design** — the factory ABI-encodes initialization data and hands it to a fresh `BeaconProxy`, rather than duplicating constructor logic into every deployment path.
3. **Consistent multi-vault upgrades** — one beacon coordinates upgrades across every vault instance of the same type.
4. **Smaller audit surface** — auditors and integrators reason about a single implementation contract plus a single beacon authority model.

If the vault is a one-instance-per-token, immutable-for-life design, inherit `VaultBaseV2` directly and skip the proxy. The beacon pattern in this guide is the default recommendation for **factories that deploy many vault instances**.

## Why upgrade authority stays with the Guardian

Once a vault is upgradeable, the implementation address becomes the highest-risk asset in the setup. Anyone who can swap it can drain the vault, rewrite its accounting, or break dispatch. Gating that authority exclusively to the Flap Guardian is a risk-control requirement:

* **Centralized incident response.** If a vulnerability is discovered post-launch — through monitoring, audit follow-up, or an incident on a sibling vault — the Guardian path enables an immediate fix without coordinating with each token creator.
* **Fewer privileged keys for vault developers to maintain.** No HSM, multisig, or deployer EOA needs to remain online for the lifetime of the token on the developer's side. The upgrade key cannot be lost or compromised by the vault developer.
* **Single trust assumption for users.** Users already trust the Flap protocol. A separate non-Guardian upgrade owner widens the trust surface without a corresponding benefit.
* **Optional irreversible immutability.** When the project is ready, the Guardian can call `lockVaultUpgrades()` to renounce beacon ownership permanently — making the immutability commitment on-chain and verifiable.

In practice this means: the factory must not retain a separate non-Guardian owner, proxy admin, beacon owner, upgrader role, deployer EOA, or external multisig with equivalent power. The factory should expose exactly two privileged entrypoints — `upgradeVaultImplementation` and `lockVaultUpgrades` — both gated to `_getGuardian()`.

{% hint style="warning" %}
A factory that keeps any additional upgrade authority alongside the Guardian will not pass Flap's verification. The risk-control posture described above relies on the Guardian being the only path that can swap the implementation.
{% endhint %}

## How to build it

### The implementation contract

The implementation is the logic contract that all `BeaconProxy` instances delegate to. It looks like a normal vault except for three upgradeable-pattern obligations:

```solidity
contract FreeCoinVaultUpgradeable is Initializable, VaultBaseV2, ReentrancyGuardUpgradeable {
    address public taxToken;
    uint256 public maxReward;
    uint256 public cooldown;
    // ...

    /// @custom:oz-upgrades-unsafe-allow constructor
    constructor() {
        _disableInitializers();
    }

    function initialize(address _taxToken, uint256 _maxReward, uint256 _cooldown) external initializer {
        __ReentrancyGuard_init();
        taxToken = _taxToken;
        maxReward = _maxReward;
        cooldown = _cooldown;
    }
```

Three things to check off:

* **`_disableInitializers()` in the constructor** — locks the bare implementation so nobody can call `initialize` on it directly. Only proxies pointing at it can be initialized.
* **`initialize(...)` replaces the constructor** — every freshly deployed `BeaconProxy` calls this exactly once with the configuration the factory chose. The `initializer` modifier (from OpenZeppelin's `Initializable`) makes the second call revert.
* **Use OpenZeppelin's upgradeable variants** — `ReentrancyGuardUpgradeable` instead of `ReentrancyGuard`, plus the matching `__*_init()` calls inside `initialize`.

The rest of the contract (`claim()`, `description()`, `vaultUISchema()`, etc.) is shaped exactly like a non-upgradeable vault. Full source is in [`src/FreeCoinBeacon.sol`](https://github.com/flap-sh/FlapVaultExample/blob/main/src/FreeCoinBeacon.sol).

### The beacon factory

The factory extends `VaultFactoryBaseV2` and owns the `UpgradeableBeacon` for this vault type:

```solidity
contract FreeCoinVaultBeaconFactory is VaultFactoryBaseV2 {
    address public immutable beacon;

    constructor() {
        FreeCoinVaultUpgradeable impl = new FreeCoinVaultUpgradeable();
        beacon = address(new UpgradeableBeacon(address(impl)));
    }

    function newVault(address taxToken, address /*quoteToken*/, address /*creator*/, bytes calldata vaultData)
        external
        override
        returns (address vault)
    {
        require(msg.sender == _getVaultPortal(), unicode"Only VaultPortal / 仅限 VaultPortal 调用");

        (uint256 maxReward, uint256 cooldown) = abi.decode(vaultData, (uint256, uint256));

        vault = address(
            new BeaconProxy(
                beacon,
                abi.encodeCall(FreeCoinVaultUpgradeable.initialize, (taxToken, maxReward, cooldown))
            )
        );
    }
```

What's happening:

1. **Constructor** deploys the implementation and an `UpgradeableBeacon` pointing at it. The factory becomes the beacon's owner — the only account that can later call `upgradeTo` on the underlying `Ownable` beacon.
2. **`newVault(...)`** is invoked by VaultPortal during token launch. It deploys a fresh `BeaconProxy`, passing `abi.encodeCall(initialize, (...))` as the proxy's initialization calldata. The proxy delegates to the beacon's current implementation.
3. **Quote-token whitelist** is enforced by `isQuoteTokenSupported` and the V2.2 validation hook `_validateBeforeLaunch` — this factory accepts native BNB only.

### Wiring the upgrade authority to the Guardian

Although the factory technically owns the beacon, the only externally callable upgrade path goes through Guardian-gated functions:

```solidity
function upgradeVaultImplementation(address newImplementation) external {
    require(msg.sender == _getGuardian(), unicode"Only Guardian / 仅限 Guardian");
    UpgradeableBeacon(beacon).upgradeTo(newImplementation);
}

function lockVaultUpgrades() external {
    require(msg.sender == _getGuardian(), unicode"Only Guardian / 仅限 Guardian");
    UpgradeableBeacon(beacon).renounceOwnership();
}

function isVaultUpgradesLocked() external view returns (bool locked) {
    locked = UpgradeableBeacon(beacon).owner() == address(0);
}
```

Two details worth highlighting:

* After `lockVaultUpgrades()`, the beacon's `owner()` is `address(0)`. From that point forward, **`upgradeTo` reverts at the beacon's own `onlyOwner` check** — even when the Guardian re-enters `upgradeVaultImplementation`, the call bottoms out in `Ownable: caller is not the owner`. The lock is on-chain irreversible.
* `isVaultUpgradesLocked()` derives from the beacon's owner, so it stays accurate even if the factory is later replaced or local tooling forgets state.

`_getGuardian()` is provided by `VaultFactoryBaseV2` and resolves to the Flap Guardian address per chain — the address does not need to be hardcoded. Any additional permissioned function added to the factory or vault later should be gated the same way.

### Deployment

Both reference scripts construct the factory; the implementation and beacon are deployed inline:

```solidity
// script/mainnet/bnb/DeployFreeCoinBeacon.s.sol
function run() external {
    vm.startBroadcast();
    FreeCoinVaultBeaconFactory factory = new FreeCoinVaultBeaconFactory();
    console.log("FreeCoinVaultBeaconFactory deployed at:", address(factory));
    console.log("UpgradeableBeacon deployed at:      ", factory.beacon());
    console.log("Beacon implementation deployed at:  ", factory.beaconImplementation());
    vm.stopBroadcast();
}
```

Run them with:

```bash
# Mainnet
forge script script/mainnet/bnb/DeployFreeCoinBeacon.s.sol:DeployFreeCoinBeacon \
    --rpc-url https://bsc-dataseed.bnbchain.org --broadcast --verify --private-key <KEY>

# Testnet
forge script script/testnet/bnb/DeployFreeCoinBeacon.s.sol:DeployFreeCoinBeacon \
    --rpc-url https://bsc-testnet-dataseed.bnbchain.org --broadcast --verify --private-key <KEY>
```

VaultPortal is permissionless, so the factory does not need to be registered anywhere after deployment. To clear the warning flag on Flap.sh, contact the Flap team to schedule the third-party audit.

## Required tests

For an upgradeable vault, the test surface is bigger than for a plain vault: alongside the usual logic and dispatch tests, the upgrade-authority model must be asserted directly.

The reference test file [`test/FreeCoinBeacon.mainnet.t.sol`](https://github.com/flap-sh/FlapVaultExample/blob/main/test/FreeCoinBeacon.mainnet.t.sol) covers the proxy / upgrade / lock surface. The bonding-curve and post-DEX dispatch tests are in [`test/FreeCoin.mainnet.t.sol`](https://github.com/flap-sh/FlapVaultExample/blob/main/test/FreeCoin.mainnet.t.sol) and apply unchanged here, because the integration surface is identical (same VaultPortal, same TaxProcessor dispatch).

Minimum coverage expected before audit:

| # | Scenario                                                                           | What to assert                                                                                                                                                                                 | Reference test                                                                                         |
| - | ---------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ |
| 1 | **Registration** — factory deploys an initialized proxy when called by VaultPortal | `taxToken`, `maxReward`, `cooldown` set; non-VaultPortal callers rejected                                                                                                                      | `test_beaconFactoryDeploysInitializedProxyVault`, `test_factoryRejectsNonVaultPortalCaller`            |
| 2 | **Initializer is locked** — implementation cannot be re-initialized                | `initialize()` reverts on both proxy and bare implementation after first call                                                                                                                  | `test_beaconProxyClaimAndInitializerLock`                                                              |
| 3 | **Bonding-curve buy → dispatch → vault**                                           | Buy on BC, call `dispatch()`, assert vault BNB balance increased                                                                                                                               | `test_buyOnBCAndDispatch` (in `FreeCoin.mainnet.t.sol`)                                                |
| 4 | **DEX graduation → sell → dispatch → vault**                                       | Buy until graduation (`status == 4`), sell on DEX, dispatch, assert vault received BNB                                                                                                         | `test_graduateAndDispatchPostDEX` (in `FreeCoin.mainnet.t.sol`)                                        |
| 5 | **Guardian can upgrade the beacon**                                                | Non-Guardian callers revert; Guardian successfully changes `beaconImplementation()`                                                                                                            | `test_guardianCanUpgradeBeaconImplementation`, `test_nonGuardianCannotUpgradeBeaconImplementation`     |
| 6 | **Guardian can lock the beacon, and lock is irreversible**                         | After `lockVaultUpgrades()`, `isVaultUpgradesLocked() == true` and any further upgrade reverts at the beacon's `onlyOwner` check                                                               | `test_guardianCanLockVaultUpgrades`, `test_lockingIsIrreversibleAndIdempotentlyReverts`                |
| 7 | **Vault logic & gating**                                                           | Vault's own business rules (claim, cooldown, double-claim, failed transfer rollback, getters)                                                                                                  | `test_claimCooldownAndDoubleClaimReverts`, `test_claimRevertsWhenRecipientRejectsNativeTransfer`, etc. |
| 8 | **Schema sanity**                                                                  | `vaultUISchema()` and `vaultDataSchema()` return the shape the UI expects; `factorySpecVersion()` is `"v2.2"`; `onBeforeLaunch(bytes)` rejects non-native quote tokens with the correct reason | `test_vaultUISchemaMetadata`, `test_factoryMetadataAndValidationHelpers`                               |

### Why both bonding-curve and DEX tests are required

The vault's `receive()` is invoked indirectly through `TaxProcessor.dispatch()`. The dispatch pipeline behaves slightly differently in each phase:

* **Bonding curve.** Each buy or sell on Portal applies the buy or sell tax, accumulates BNB on the `TaxProcessor`, and `dispatch()` flushes it to the vault. Tests verify that dispatch reaches the vault and that `receive()` does not revert under that gas budget.
* **Post-DEX (graduated).** Once the bonding curve crosses the `FOUR_FIFTHS` threshold the token graduates to DEX. Sell-side tax is then collected through the token's own swap-and-distribute path, accumulated on the `TaxProcessor`, and dispatched. Tests verify that the same vault still receives BNB after graduation, since this is the path that runs for the entire post-launch lifetime of the token.

Both phases must be covered. A vault that handles bonding-curve dispatch correctly but reverts after DEX graduation is broken in production, and that bug is invisible without a graduation test.

A condensed sketch of the integration tests (full code in [`test/FreeCoin.mainnet.t.sol`](https://github.com/flap-sh/FlapVaultExample/blob/main/test/FreeCoin.mainnet.t.sol)):

```solidity
// 1. Bonding curve
function test_buyOnBCAndDispatch() public {
    uint256 vaultBefore = address(vault).balance;

    vm.startPrank(user1);
    _buyOnBC(token, 5 ether);
    vm.stopPrank();

    _dispatchTax(token);                // flush TaxProcessor → vault
    assertGt(address(vault).balance, vaultBefore);
}

// 2. Post-DEX
function test_graduateAndDispatchPostDEX() public {
    vm.startPrank(user1); _buyOnBC(token, 5 ether);  vm.stopPrank();
    _dispatchTax(token);
    vm.startPrank(user2); _buyOnBC(token, 15 ether); vm.stopPrank();
    // ...buy more if not yet graduated...
    assertEq(portal.getTokenV8Safe(token).status, 4, "should have graduated");

    uint256 vaultBefore = address(vault).balance;
    vm.startPrank(user1);
    require(IERC20(token).transfer(token, 400_000 * 1e18));   // seed liquidation threshold
    _sell(token, IERC20(token).balanceOf(user1) - 400_000 * 1e18);
    vm.stopPrank();

    _dispatchTax(token);
    assertGt(address(vault).balance, vaultBefore);
}
```

{% hint style="warning" %}
Always wrap multi-call helpers like `_sell()` and `_buyOnBC()` in `vm.startPrank(...)` / `vm.stopPrank()`. `vm.prank()` only covers the **next** external call, so the second internal call (e.g. `swapExactInput` after `approve`) silently runs as the wrong sender and produces confusing reverts. See the [FlapVaultExample README](https://github.com/flap-sh/FlapVaultExample#prank-convention-in-tests) for the full convention.
{% endhint %}

### Tests for the upgrade and lock authority

Excerpts from [`test/FreeCoinBeacon.mainnet.t.sol`](https://github.com/flap-sh/FlapVaultExample/blob/main/test/FreeCoinBeacon.mainnet.t.sol):

```solidity
function test_guardianCanUpgradeBeaconImplementation() public {
    FreeCoinVaultUpgradeable nextImplementation = new FreeCoinVaultUpgradeable();

    vm.prank(TESTNET_GUARDIAN);
    factory.upgradeVaultImplementation(address(nextImplementation));

    assertEq(factory.beaconImplementation(), address(nextImplementation));
}

function test_nonGuardianCannotUpgradeBeaconImplementation() public {
    FreeCoinVaultUpgradeable nextImplementation = new FreeCoinVaultUpgradeable();

    vm.expectRevert(bytes(unicode"Only Guardian / 仅限 Guardian"));
    vm.prank(USER);
    factory.upgradeVaultImplementation(address(nextImplementation));
}

function test_guardianCanLockVaultUpgrades() public {
    assertTrue(!factory.isVaultUpgradesLocked());

    vm.prank(TESTNET_GUARDIAN);
    factory.lockVaultUpgrades();
    assertTrue(factory.isVaultUpgradesLocked());

    // Further upgrades fail at the beacon's onlyOwner check, even when called by the Guardian
    FreeCoinVaultUpgradeable nextImplementation = new FreeCoinVaultUpgradeable();
    vm.expectRevert(bytes("Ownable: caller is not the owner"));
    vm.prank(TESTNET_GUARDIAN);
    factory.upgradeVaultImplementation(address(nextImplementation));
}
```

These three tests are the minimum set demonstrating that the authority model holds: the Guardian is the only path in, non-Guardian callers are rejected, and locking is genuinely irreversible.

## Pre-audit checklist

* [ ] Implementation calls `_disableInitializers()` in its constructor.
* [ ] `initialize(...)` is the only place that sets vault state, and it has the `initializer` modifier.
* [ ] Factory deploys the implementation and `UpgradeableBeacon` in its own constructor; the beacon owner is the factory itself.
* [ ] `upgradeVaultImplementation(address)` is gated to `_getGuardian()` — no other caller passes.
* [ ] `lockVaultUpgrades()` is gated to `_getGuardian()` and renounces beacon ownership.
* [ ] No other upgrade path exists on the factory (no admin EOA, no proxy admin, no extra owner, no parallel multisig).
* [ ] Tests assert: registration via VaultPortal, initializer lockdown, bonding-curve dispatch, post-DEX dispatch, Guardian upgrade success, non-Guardian upgrade rejection, lock irreversibility.
* [ ] All tests pass against a BSC fork (`forge test --fork-url https://bsc-dataseed.bnbchain.org`).
* [ ] The [`flap-vault-spec-checker`](https://github.com/flap-sh/FlapVaultExample/tree/main/.agents/skills/flap-vault-spec-checker) Copilot skill reports no ❌ findings.

Once those are green, contact the Flap team to schedule the third-party audit and clear the warning flag on Flap.sh. See the [Vault & VaultFactory specification](/flap/developers/vault-developers/vault-and-vaultfactory-specification#get-your-vault-verified) page for the verification process.


# Tutorials

Step-by-step guides for building specific vault features.

* [Using an LP Token or Child Token as the Dividend Token](/flap/developers/vault-developers/tutorials/lp-token-as-dividend)
* [Using a Basket Token to Distribute Multiple Dividend Assets](/flap/developers/vault-developers/tutorials/basket-token-multi-asset-dividends)


# Using an LP Token or Child Token as the Dividend Token

{% hint style="warning" %}
**Preview.** This document has not been reviewed yet. Content may be incomplete or inaccurate.
{% endhint %}

This tutorial walks you through implementing a vault factory that uses a **computed dividend token** — a token whose address cannot be known before the tax token is deployed, such as a Uniswap/PancakeSwap LP pair or a child token deployed deterministically from the tax token address.

This feature was introduced in **factory spec v2.3**.

***

## The Problem

When launching a token through VaultPortal, the caller must supply a `dividendToken` address — the ERC-20 token that vault holders receive as dividends. Most of the time this is straightforward (e.g. use WBNB, or USDT).

But some vault designs require a dividend token whose address **depends on the tax token address itself**:

* **LP pair tokens** — the PancakeSwap pair for `(taxToken, WBNB)` has an address derived from `taxToken`.
* **Child tokens / receipt tokens** — a token deployed via CREATE2 keyed on `taxToken`.

At launch time the tax token does not exist yet — VaultPortal predicts its address via CREATE2, but the actual deployment happens inside Portal. The `dividendToken` field is needed *before* that deployment, so there is no way to supply the real LP/child address upfront.

***

## The Solution: `MAGIC_DIVIDEND_COMPUTED`

VaultPortal supports a special sentinel value for `dividendToken`:

```solidity
// IPortal.sol
address constant MAGIC_DIVIDEND_COMPUTED =
    address(0xC0Dec0dec0DeC0Dec0dEc0DEC0DEC0DEC0DEC0dE);
```

When the launcher passes `MAGIC_DIVIDEND_COMPUTED` as `dividendToken`, VaultPortal will:

1. Compute the CREATE2-predicted tax token address.
2. Check that the selected vault factory implements **spec v2.3** (`factorySpecVersion()` returns `"v2.3"` or higher).
3. Call `resolveDividendToken(predictedToken, launchVersion, launchParams)` on the factory.
4. Use the returned address as the actual `dividendToken` forwarded to Portal.

If the factory does not support v2.3, the launch reverts with `FactoryDoesNotSupportComputedDividend()`.

***

## Implementation Guide

### Step 1 — Declare spec v2.3 in your factory

Your factory must extend `VaultFactoryBaseV2` and override `factorySpecVersion()` to return `"v2.3"` (or higher):

```solidity
import {VaultFactoryBaseV2} from "flap-sh/FlapTaxVaults/src/interfaces/VaultFactoryBaseV2.sol";
import {IVaultFactoryDividendV23} from "flap-sh/FlapTaxVaults/src/interfaces/IVaultFactory.sol";

contract MyLPVaultFactory is VaultFactoryBaseV2, IVaultFactoryDividendV23 {

    function factorySpecVersion() external pure override returns (string memory) {
        return "v2.3";
    }

    // ... rest of factory
}
```

{% hint style="warning" %}
You **must** also implement `IVaultFactoryDividendV23`. Declaring `"v2.3"` in `factorySpecVersion()` but not implementing `resolveDividendToken` will cause every `MAGIC_DIVIDEND_COMPUTED` launch to revert.
{% endhint %}

***

### Step 2 — Implement `resolveDividendToken`

`IVaultFactoryDividendV23` requires a single function:

```solidity
interface IVaultFactoryDividendV23 {
    function resolveDividendToken(
        address predictedToken,
        uint8   launchVersion,
        bytes   calldata launchParams
    ) external view returns (address dividendToken);
}
```

| Parameter        | Description                                                                                                   |
| ---------------- | ------------------------------------------------------------------------------------------------------------- |
| `predictedToken` | The CREATE2-predicted address of the tax token (not yet deployed)                                             |
| `launchVersion`  | `DIVIDEND_TOKEN_LAUNCH_VERSION_V6` or `DIVIDEND_TOKEN_LAUNCH_VERSION_V7` depending on the launch wrapper used |
| `launchParams`   | ABI-encoded `NewTokenV6WithVaultParams` or `NewTokenV7WithVaultParams`                                        |

VaultPortal calls this as a **`staticcall`**, so the function must be `view` (no state writes).

#### Example — LP pair dividend

This example resolves the PancakeSwap V2 LP pair of `(predictedToken, WBNB)` as the dividend token:

```solidity
import {IUniswapV2Factory} from "@uniswap/v2-core/contracts/interfaces/IUniswapV2Factory.sol";

address constant PANCAKE_FACTORY = 0xcA143Ce32Fe78f1f7019d7d551a6402fC5350c73; // BSC mainnet
address constant WBNB            = 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c;

function resolveDividendToken(
    address predictedToken,
    uint8   /* launchVersion */,
    bytes   calldata /* launchParams */
) external view override returns (address dividendToken) {
    // The LP pair is deterministically computable from the two token addresses.
    // PancakeSwap V2 uses the same CREATE2 formula as Uniswap V2.
    dividendToken = IUniswapV2Factory(PANCAKE_FACTORY).getPair(predictedToken, WBNB);

    // If the pair does not exist yet, compute it via CREATE2.
    if (dividendToken == address(0)) {
        dividendToken = _computePairAddress(predictedToken, WBNB);
    }
}

function _computePairAddress(address tokenA, address tokenB)
    internal pure returns (address)
{
    (address t0, address t1) = tokenA < tokenB
        ? (tokenA, tokenB)
        : (tokenB, tokenA);

    bytes32 salt = keccak256(abi.encodePacked(t0, t1));
    // PancakeSwap V2 init code hash on BSC
    bytes32 initCodeHash =
        0x00fb7f630766e6a796048ea87d01acd3068e8ff67d078148a3fa3f4a84f69bd5;

    return address(uint160(uint256(keccak256(abi.encodePacked(
        hex"ff",
        PANCAKE_FACTORY,
        salt,
        initCodeHash
    )))));
}
```

#### Example — Child token dividend

If your vault deploys a child token deterministically from the tax token address:

```solidity
function resolveDividendToken(
    address predictedToken,
    uint8   /* launchVersion */,
    bytes   calldata /* launchParams */
) external view override returns (address dividendToken) {
    // Child token is deployed via CREATE2 with `predictedToken` as the salt
    dividendToken = address(uint160(uint256(keccak256(abi.encodePacked(
        hex"ff",
        address(this),        // deployer is the factory itself
        bytes32(uint256(uint160(predictedToken))),
        CHILD_TOKEN_INIT_HASH
    )))));
}
```

***

### Step 3 — Declare a policy so the UI pre-fills the sentinel

`tokenCreationPolicies()` lets the UI know that `dividendToken` **must** equal `MAGIC_DIVIDEND_COMPUTED`. The UI reads this policy and pre-fills (or locks) the field before the user submits the launch form.

```solidity
import {MAGIC_DIVIDEND_COMPUTED} from "./interfaces/IPortal.sol";
import {FactoryPolicy} from "./interfaces/IVaultSchemasV1.sol";

function tokenCreationPolicies()
    public
    pure
    override
    returns (FactoryPolicy[] memory policies)
{
    policies = new FactoryPolicy[](1);
    policies[0] = FactoryPolicy({
        target:      "dividendToken",
        operator:    "eq",
        value:       abi.encode(MAGIC_DIVIDEND_COMPUTED),
        description: "Dividend token must be set to MAGIC_DIVIDEND_COMPUTED. "
                     "The factory will resolve the real LP token address on-chain."
    });
}
```

{% hint style="info" %}
Policies are **advisory hints for the UI only** — they do not enforce anything on-chain. Always pair a policy with the corresponding `_validateBeforeLaunch` check (Step 4) to enforce the rule on-chain.
{% endhint %}

***

### Step 4 — Enforce the sentinel in `_validateBeforeLaunch`

Override `_validateBeforeLaunch` to reject any launch that does not carry `MAGIC_DIVIDEND_COMPUTED`. This is the on-chain enforcement that backs the UI policy declared above.

```solidity
import {LaunchValidationDataV1} from "./interfaces/IVaultFactory.sol";

function _validateBeforeLaunch(LaunchValidationDataV1 memory data)
    internal
    pure
    override
    returns (bool success, string memory reason)
{
    if (data.dividendToken != MAGIC_DIVIDEND_COMPUTED) {
        return (
            false,
            "dividendToken must be MAGIC_DIVIDEND_COMPUTED for this vault type."
        );
    }
    return (true, "");
}
```

`VaultPortal` calls `onBeforeLaunch(bytes)` (which decodes into `_validateBeforeLaunch`) **before** it calls `resolveDividendToken`. This means the hook sees `dividendToken == MAGIC_DIVIDEND_COMPUTED` — the raw sentinel, not yet the resolved LP address. That is the correct value to check against.

{% hint style="warning" %}
Your factory must return `"v2.3"` from `factorySpecVersion()` for `onBeforeLaunch` to be reached. If you return `"v2.1"` or lower, `VaultPortal` will use the legacy `onBeforeNewTokenV6WithVault` path instead.
{% endhint %}

***

### Step 5 — Launch with `MAGIC_DIVIDEND_COMPUTED`

On the caller side, set `dividendToken` to the sentinel when building the launch params:

```solidity
import {MAGIC_DIVIDEND_COMPUTED} from "flap-sh/FlapTaxVaults/src/interfaces/IPortal.sol";

IVaultPortal.NewTokenV6WithVaultParams memory params = IVaultPortal.NewTokenV6WithVaultParams({
    // ... other fields
    dividendToken: MAGIC_DIVIDEND_COMPUTED,   // <-- the key field
    factory:       address(myLPVaultFactory),
    vaultData:     abi.encode(/* your vault config */),
    // ...
});

vaultPortal.newTokenV6WithVault{value: msg.value}(params);
```

VaultPortal will call `myLPVaultFactory.resolveDividendToken(...)` and substitute the result before forwarding the launch to Portal.

***

## What Happens On-Chain (Flow Summary)

```
Caller
  │  dividendToken = MAGIC_DIVIDEND_COMPUTED
  ▼
VaultPortal.newTokenV6WithVault / newTokenV7WithVault
  │  1. Predict tax token address (CREATE2)
  │  2. Check factory spec >= v2.3
  │  3. staticcall factory.resolveDividendToken(predictedToken, ...)
  │     └─ factory returns real LP / child token address
  │  4. Replace dividendToken with resolved address
  │  5. newVault(predictedToken, quoteToken, creator, vaultData)
  │  6. Forward to Portal.newTokenV6WithVault(params with real dividendToken)
  ▼
Portal deploys tax token, links vault
```

***

## Version Gating

| `factorySpecVersion()` | `MAGIC_DIVIDEND_COMPUTED` supported?                   |
| ---------------------- | ------------------------------------------------------ |
| `""` (legacy / V1)     | ❌ reverts with `FactoryDoesNotSupportComputedDividend` |
| `"v2.1"`, `"v2.2"`     | ❌ reverts                                              |
| `"v2.3"` or higher     | ✅ `resolveDividendToken` is called                     |

Factories that do not implement v2.3 are completely unaffected — the new sentinel only triggers for launches that explicitly pass `MAGIC_DIVIDEND_COMPUTED`.

***

## Checklist

* [ ] Factory extends `VaultFactoryBaseV2` and implements `IVaultFactoryDividendV23`
* [ ] `factorySpecVersion()` returns `"v2.3"` or higher
* [ ] `resolveDividendToken` is `view` and always returns a non-zero address
* [ ] `tokenCreationPolicies()` declares `dividendToken eq MAGIC_DIVIDEND_COMPUTED`
* [ ] `_validateBeforeLaunch` rejects any launch where `dividendToken != MAGIC_DIVIDEND_COMPUTED`
* [ ] Launcher passes `MAGIC_DIVIDEND_COMPUTED` as `dividendToken`
* [ ] Launcher selects the v2.3 factory in the launch params


# Using a Basket Token to Distribute Multiple Dividend Assets

{% hint style="warning" %}
**Preview.** This document has not been reviewed yet. Content may be incomplete or inaccurate.
{% endhint %}

This tutorial explains a pattern for distributing **multiple underlying assets** as dividends, even though Flap's dividend contract can only track **one** dividend token per tax token. It uses the design from the Stocks Vault (multi-asset index vault) as a worked example — the mechanism generalizes to any vault that needs to pay out a basket of assets through a single-token dividend pipe.

***

## 1. The Constraint: One Dividend Token Per Tax Token

Every Flap tax token has exactly one `dividendToken` configured at launch (see [Vault & VaultFactory Specification](/flap/developers/vault-developers/vault-and-vaultfactory-specification)). The dividend contract's accounting — balances, `withdrawableDividends`, `withdrawDividendsFor` — is all built around tracking a **single ERC-20 balance** per holder.

```solidity
interface IIndexVaultDividend {
    function deposit(uint256 amount) external returns (bool success);
    function withdrawDividendsFor(address user) external returns (bool success);
    function withdrawableDividends(address user) external view returns (uint256);
    function dividendToken() external view returns (address);
}
```

This is a deliberate simplification: the dividend contract does one thing well (pro-rata distribution of a single balance) and does not need to know anything about a basket of underlying assets. But it means a vault that wants to pay out **NVDA + AAPL + TSLA** (for example) to its holders cannot deposit three different tokens into the dividend contract — there's only one `dividendToken` slot.

***

## 2. The Solution: A Basket Token as the Dividend Medium

The fix is to introduce an intermediate ERC-20 — a **basket token** — that represents a claim on a pool of underlying assets, and use *that* as the `dividendToken`. Instead of depositing NVDA, AAPL, and TSLA directly, the vault:

1. Buys/receives the underlying assets.
2. Mints basket shares against them.
3. Deposits the basket shares into the dividend contract.

```solidity
interface IIndexBasketToken {
    function mint(address asset, uint256 amount, address to) external returns (uint256 shares);
    function isSupportedAsset(address asset) external view returns (bool);
    function getSubset() external view returns (address[] memory);
    function previewMint(address asset, uint256 amount) external view returns (uint256);
}
```

Now the dividend contract only ever sees **one token** (the basket) — it doesn't need to change at all.

### NAV: why minted shares must equal value spent

The basket token tracks a Net Asset Value (NAV) in USD across its pooled assets:

```solidity
function totalNavUsd() public view returns (uint256 nav) {
    uint256 len = _subset.length;
    for (uint256 i; i < len; ++i) {
        nav += assetValueUsd(_subset[i], pooledAmount[_subset[i]]);
    }
}
```

{% hint style="warning" %}
**Minting must use a live oracle-priced NAV, not a fixed rate.** Every component asset needs a live USD price feed so `totalNavUsd()` reflects the basket's *current* value at mint time. Minting shares 1:1 against a fixed/assumed price (e.g. "1 unit of asset = X USD, always") would let the share ⇄ NAV ratio drift the moment any component's real price moves, silently diluting or over-crediting whoever mints next. The oracle read in `assetValueUsd()` below is not optional.
{% endhint %}

When a vault mints new shares, it must mint an amount that keeps the **share ⇄ NAV ratio constant** — otherwise existing holders get diluted or unfairly enriched. That means every mint must re-read each component asset's **current** oracle price to compute both the deposit's USD value and the basket's total NAV — never a cached or assumed price. The formula used is the standard proportional-mint formula:

```solidity
uint256 supplyBefore = totalSupply();
uint256 navBefore = totalNavUsd();       // sum of oracle USD price × pooled amount, across all components
uint256 depositValue = assetValueUsd(asset, amount); // oracle USD price × amount

if (supplyBefore == 0) {
    // First mint seeds the ratio 1:1 with USD value (minus a small locked-liquidity buffer)
    shares = depositValue - MINIMUM_LIQUIDITY;
} else {
    // shares = depositValue * supplyBefore / navBefore
    shares = Math.mulDiv(depositValue, supplyBefore, navBefore);
}
```

`assetValueUsd()` and `totalNavUsd()` both depend on a live price feed per component asset — this is why the basket token needs its own oracle wiring (or a factory-level price registry it can read from). There is no shortcut that lets you skip pricing components individually: NAV is, by definition, the sum of each component's *current* value.

{% hint style="info" %}
**Why USD, not the quote asset (e.g. BNB)?** Component assets (stocks, RWAs, etc.) are natively priced in USD by most oracle providers. Denominating NAV in USD means you consume each component's price feed directly, with no extra conversion step. If you instead tried to denominate NAV in BNB, you'd still need a USD price for every component (that's how they're quoted) *plus* a BNB/USD feed to convert — an extra moving part and an extra point of oracle staleness/failure, for no benefit. USD is the natural, minimal-dependency unit here.
{% endhint %}

This is the same reason **the amount of basket token minted (and then deposited into the dividend contract) must equal the USD value of BNB that was actually spent** buying the underlying asset. If a vault only sold `X` BNB worth of NVDA, it can only mint (and pay out) shares worth that same USD amount — never more. This keeps the basket's NAV per share honest: every share always redeems for the same proportional value it was minted at, regardless of how many times mint/unwrap happens afterward.

{% hint style="info" %}
Single-asset baskets (a vault backed by just one underlying token) skip the NAV math entirely and mint 1 share per 1 unit of the underlying token — no oracle read needed, and no rounding drift.
{% endhint %}

### Exposing NAV off-chain: the `NavSnapshot` event

`totalNavUsd()` and `totalSupply()` give the basket's value on-chain, but a frontend or indexer that wants to show "1 share = X USD" should not have to replay every mint and unwrap to reconstruct pooled balances. The basket token therefore emits one snapshot event every time its composition or supply changes:

```solidity
event FlapIndexBasketTokenNavSnapshot(
    uint256 totalSupply,
    uint256 totalNavUsd,
    uint256[] pooledAmounts
);
```

It is emitted by an internal `_emitNavSnapshot()` helper, called at the end of:

* **Mint** — after `FlapIndexBasketTokenMinted`, when the vault deposits assets and receives shares.
* **Unwrap** — after `FlapIndexBasketTokenUnwrapped`, when a holder claims dividends and the basket auto-unwraps to underlying assets (see section 3).

| Field           | Meaning                                                                                                                                   |
| --------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
| `totalSupply`   | Basket token supply after the operation.                                                                                                  |
| `totalNavUsd`   | Total USD value of all pooled underlying assets after the operation (18-decimal USD, computed via the oracle exactly as `totalNavUsd()`). |
| `pooledAmounts` | Pooled amount of each underlying asset, as a parallel array to `getSubset()` (the basket's frozen asset list).                            |

`totalNavUsd / totalSupply` is the per-share price at the moment of the event. The numerator and denominator are emitted separately, rather than a single price, so consumers can also check the contract's arithmetic and re-value the basket between events.

{% hint style="warning" %}
**`totalNavUsd` is a snapshot, not a live price.** Between two events the pooled amounts do not change, but component prices do. To quote the current per-share price, take `pooledAmounts` and `totalSupply` from the latest snapshot and re-value them with current oracle prices — do not serve a stale `totalNavUsd` as "the price" once markets have moved.
{% endhint %}

The other basket events describe composition and lifecycle rather than price:

* `FlapIndexBasketTokenInitialized(vault, taxToken, subset)` — basket creation.
* `FlapIndexBasketTokenMinted(asset, amount, to, shares)` — a deposit.
* `FlapIndexBasketTokenUnwrapped(from, to, shares, amounts)` — a withdrawal or dividend claim.
* `FlapIndexBasketTokenRescaled(oldSupply, newSupply, navUsd)` — emitted when `rescaleInitialShares()` is triggered on a very-low-supply basket to repair the share ⇄ NAV ratio; it also carries a `navUsd` figure.

`FlapIndexBasketTokenNavSnapshot` is the one purpose-built for price discovery; index that event and treat the rest as its context.

***

## 3. The Trick: Users Never Need to Hold the Basket Token

A basket token is an implementation detail — end users should never need to understand it, hold it, or manually redeem it. This is solved with an **auto-unwrap-on-transfer** hook on the basket token itself:

```solidity
function _transfer(address from, address to, uint256 value) internal override {
    if (!_unwrapping && value != 0 && from != address(0) && to != address(0)) {
        address dividendContract = IFlapTaxTokenV3(taxToken).dividendContract();
        if (dividendContract != address(0) && from == dividendContract) {
            // Transfer FROM the dividend contract triggers a pro-rata unwrap
            // instead of a normal ERC-20 transfer.
            _unwrapFrom(from, to, value);
            return;
        }
    }
    super._transfer(from, to, value);
}
```

The key insight: the dividend contract's `withdrawDividendsFor(user)` internally does an ERC-20 `transfer` of the basket token from itself to the claiming user. The basket token's overridden `_transfer` detects that the sender is the dividend contract and, instead of moving basket-token balance, it **burns the shares and releases the underlying assets pro-rata directly to the user**:

```solidity
function _unwrapFrom(address from, address to, uint256 shares) internal returns (uint256[] memory amounts) {
    uint256 supplyBefore = totalSupply();
    // amounts[i] = pooledAmount[asset_i] * shares / supplyBefore
    // ... burns `shares`, transfers each underlying asset slice to `to`
}
```

So from the user's perspective: they call `withdrawDividendsFor(self)` (or someone calls it for them) and **NVDA, AAPL, TSLA (or whatever assets are in the basket) land directly in their wallet** — they never see or hold the basket token itself. The basket token only ever "exists" transiently between the vault's `mint()` call and the dividend contract's payout.

***

## 4. Bonus: Other Launches Can Reuse This Basket as Their Dividend Token Too

The auto-unwrap trick isn't locked to a single tax token. If you're launching your **own** token and want the same "holders receive real underlying assets automatically" experience, you don't need to build your own vault + basket + dividend plumbing from scratch:

> **We can whitelist your token to use an existing basket as its `dividendToken`.**

When your `dividendToken` points at a whitelisted basket, our automation handles the distribution side for you — deposits into the dividend contract and the resulting payouts to your holders are managed the same way as the vault's own flow, so your holders still get the auto-unwrap experience (real underlying assets landing in their wallet, never the basket token itself) without you having to run any of that infrastructure yourself.

If your use case is instead "distribute a single fixed known asset" (like WBNB or an LP pair) rather than a multi-asset basket, see [Using an LP Token or Child Token as the Dividend Token](/flap/developers/vault-developers/tutorials/lp-token-as-dividend) for that simpler pattern.

***

## Summary

| Step                                | What happens                                                                                                                                          | Why                                                                                                                                                                                                    |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| 1. Dividend contract limitation     | Tracks exactly one `dividendToken` balance per holder                                                                                                 | Keeps dividend accounting simple and general                                                                                                                                                           |
| 2. Basket token as medium           | Vault mints basket shares against the **current oracle-priced USD NAV** of assets bought, deposits shares (not raw assets) into the dividend contract | Lets a single-token dividend pipe represent a multi-asset basket; shares minted must equal the live USD value of BNB spent, preserving NAV per share — a fixed/assumed price would let the ratio drift |
| 3. Auto-unwrap on claim             | Basket token's `_transfer` override detects transfers from the dividend contract and unwraps to underlying assets instead                             | Users never need to acquire, hold, or manually redeem the basket token                                                                                                                                 |
| 4. Whitelistable for other launches | A new token can point its `dividendToken` at an existing whitelisted basket                                                                           | Other launches get the same auto-unwrap payout experience without building their own vault/basket/dividend stack                                                                                       |


# Flap Token Locker

## Overview

`FlapTokenLocker` locks tokens under a vesting schedule while the locked position keeps earning. For a Flap tax token, the locker holds the tokens as one shareholder of the token's `Dividend` contract and splits every payout across its locks by locked principal, so a vesting allocation does not lose its dividends. Any other ERC20 locks as principal only.

Each lock has one beneficiary who can claim released principal, collect dividends, transfer the lock, or renounce it. The contract is a non-upgradeable singleton; admission rules live in a separate upgradeable `FlapTokenRegistry`.

## Deployed Addresses

| Chain       | Contract          | Address                                      |
| ----------- | ----------------- | -------------------------------------------- |
| BNB Mainnet | FlapTokenLocker   | `0x0000A8B6741441BB4Ba0DFE313a2bc7b2Cf10000` |
| BNB Mainnet | FlapTokenRegistry | `0x0000e9f9F5e469f0EEC27F192B4DdDE04D4c0000` |
| BNB Testnet | FlapTokenLocker   | `0x0000A8B6741441BB4Ba0DFE313a2bc7b2Cf10000` |
| BNB Testnet | FlapTokenRegistry | `0x0000e9f9F5e469f0EEC27F192B4DdDE04D4c0000` |

Both contracts are deployed with CREATE2, so the addresses are the same on every chain.

The ABI of the locker:

{% file src="/files/6Jvksy3uRZpSszn9emHq" %}

## How It Works

### Vesting

A lock releases on a curve fixed at creation: a first slice at `tgeDate` (the TGE), then one more slice every `cycle` seconds until everything is released. Nothing is released before `tgeDate`.

```
unlocked = locked * min(10000, tgeBps + elapsedCycles * cycleBps) / 10000
```

| Schedule                                              | Result                                                                    |
| ----------------------------------------------------- | ------------------------------------------------------------------------- |
| `tgeBps = 10000`                                      | Full lock: 100% releases at `tgeDate`; `cycle` and `cycleBps` are ignored |
| `tgeBps = 2000`, `cycle = 30 days`, `cycleBps = 1000` | 20% at TGE, then 10% every 30 days; fully released 240 days after TGE     |
| `tgeBps = 0`, `cycle = 1 days`, `cycleBps = 100`      | Nothing at TGE, then 1% per day for 100 days                              |

A partial release (`tgeBps < 10000`) requires a positive `cycle` and `cycleBps`. Releases are computed on demand from the timestamp; there is no keeper, and the beneficiary may claim any released amount at any time, in one or many calls.

### Dividends

All locks of the same token share one pool. The pool pulls the token's dividends from its `Dividend` contract and credits them to locks in proportion to their still-locked principal (MasterChef-style accumulator). Claiming principal shrinks a lock's future share; renouncing burns the remaining principal to `0xdead`.

The `Dividend` contract sees the locker as a single holder, so the token's `minimumShareBalance` is checked against the locker's total balance of that token, not against each lock. A lock below the threshold still earns while the pool as a whole is at or above it; while the total is below it, no lock of that token earns.

### Fees

Fees are snapshotted into each lock at creation and never change afterwards.

| Fee          | Current | Cap |
| ------------ | ------- | --- |
| Creation fee | 0       |     |
| Principal    | 0%      | 10% |
| Dividend     | 5%      | 10% |

A negotiated fee can be issued off-chain as an EIP-712 `CustomFee` signed by `feeSigner`; see the [Integration Guide](/flap/developers/flap-token-locker/integration-guide#custom-fee).

## Supported Tokens

| Token                                                                                                                         | Behaviour                                                   |
| ----------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- |
| Flap token whose version is open in the registry: 5 (TaxTokenV2), 6 (TaxTokenV3), 7 (non-tax TokenV3)                         | Principal vests; dividends are collected through the locker |
| Flap token with no `Dividend` contract (`dividendContract()` is zero or missing, e.g. early launches with `dividendBps == 0`) | Principal only; `collect` and `pendingDividend` return 0    |
| Token the Portal does not know (any plain ERC20)                                                                              | Principal only; `collect` and `pendingDividend` return 0    |
| Flap token whose version is not open in the registry                                                                          | Rejected with `TokenNotAllowed`                             |

A pool's dividend token must be the token's own quote token (a native quote resolves to WBNB), the token itself, any token launched through the Flap Portal (every version, 0–7), or a token the registry owner has allowlisted. To use any other token as a custom dividend token, contact the Flap team to have it allowlisted before locking. Dividend tokens with transfer hooks that redirect or transform payouts (basket or vault wrapper tokens) are never admitted. Both allowlists can be read on-chain; see [Check Admission](/flap/developers/flap-token-locker/integration-guide#check-admission).

{% hint style="warning" %}
Rebasing tokens are not supported. Tokens in vault mode (`dividendBps == 0` with rewards paid by the vault directly to holders) lock fine, but the vault's rewards for the locked period accrue to the locker address and cannot be claimed; only dividends paid through the token's `Dividend` contract reach the beneficiary.
{% endhint %}

## Trust Assumptions

The locker owner cannot touch locked principal or change an existing lock's fee or schedule; it only sets fee parameters for new locks. Admission, which token versions and dividend tokens may open pools, is controlled by the registry owner, and the registry's ProxyAdmin can replace the registry implementation. Dividends depend on Flap's `Dividend` contract and its owner (the Portal), which can exclude an address, redirect a holder's payouts or swap the dividend token before its first deposit. None of these powers reach locked principal.

There is deliberately no sweep or rescue function. Tokens sent to the locker by mistake stay there.


# Integration Guide

## Create a Lock

```solidity
struct Schedule {
    uint64 tgeDate;   // first release timestamp, must be in the future
    uint16 tgeBps;    // released at tgeDate; 10000 = full lock
    uint64 cycle;     // seconds between later releases
    uint16 cycleBps;  // released per cycle
}

struct LockParams {
    address taxToken;
    address beneficiary;
    uint256 amount;      // pulled from the caller; the lock records what actually arrives
    Schedule schedule;
}

function lock(LockParams calldata params) external payable returns (uint256 lockId);
```

Approve the locker for `amount`, then call `lock` with `msg.value` equal to the current `defaultFee.creationFee` (0 today). The lock records the amount that actually arrived, so a token that taxes the transfer in records less than `amount`. `lockId` is the index into the lock array and is emitted in `FlapLockCreated`.

The first lock of a token opens its pool: the registry is asked whether the token is a Flap token, which version it is and where its `Dividend` contract lives. A token the Portal does not know opens a pool with no dividend wiring.

## Entry Points

| Function                          | Who may call | What it does                                                                                                                    |
| --------------------------------- | ------------ | ------------------------------------------------------------------------------------------------------------------------------- |
| `claim(lockId)`                   | beneficiary  | Settles pending dividends, then pays all currently releasable principal. Reverts `NothingToClaim` when nothing is releasable    |
| `collect(lockId)`                 | anyone       | Pays the lock's pending dividends to its beneficiary. Returns `(0, 0)` when there are none                                      |
| `transferBeneficiary(lockId, to)` | beneficiary  | Hands the whole lock to `to`, including unclaimed released principal and unsettled dividends. Not available on custom-fee locks |
| `renounceBeneficiary(lockId)`     | beneficiary  | Irreversible. Settles dividends, then burns **all** remaining principal, released or not, to `0xdead`                           |

{% hint style="info" %}
Order of operations: `claim` includes dividend settlement, so `collect` alone is only needed while nothing is releasable. Call `claim` (or `collect`) before `transferBeneficiary` to keep what has been earned, and `claim` before `renounceBeneficiary` so released principal is not burned.
{% endhint %}

## Read a Lock

```solidity
function getLock(uint256 lockId) external view returns (Lock memory);
function unlockedAmount(uint256 lockId) external view returns (uint256);  // released so far, claimed or not
function claimable(uint256 lockId) external view returns (uint256);       // released and not yet claimed
function pendingDividend(uint256 lockId) external view returns (uint256); // estimate for UIs; collect is exact
function locksLength() external view returns (uint256);
```

```solidity
struct Lock {
    address taxToken;
    address beneficiary;   // address(0) once renounced
    uint256 locked;        // principal held for this lock, after the principal fee
    uint256 claimed;
    uint256 shares;        // locked - claimed while live, 0 after renounce
    uint256 rewardDebt;
    uint16  dividendBps;   // fee snapshotted at creation
    bool    fromCustomFee;
    Schedule schedule;
}
```

Whether a token's locks earn dividends: `pools(token).dividendContract` is non-zero when the pool is wired to a `Dividend` contract, zero when the token locks as principal only.

## Check Admission

Admission lives on `FlapTokenRegistry`, not the locker. Before the first lock of a token, check it there:

```solidity
function allowedTokenVersions(uint8 version) external view returns (bool);   // Flap token versions that may open pools
function allowedDividendTokens(address token) external view returns (bool);  // dividend tokens admitted by the registry owner
function query(address token) external view
    returns (bool isAllow, address dividendContract, address dividendToken, address quoteToken);
function isDividendTokenAllowed(address token, address quoteToken, address dividendToken) external view returns (bool);
```

`query` is what `lock` asks before opening a pool: `isAllow` is false for a Flap token whose version is closed (`TokenNotAllowed`), and `dividendContract` is zero when the token locks as principal only. `isDividendTokenAllowed` is the full dividend-token rule (quote token, the token itself, Flap-launched tokens and the owner allowlist); pass the `quoteToken` returned by `query`. `allowedDividendTokens` shows the owner allowlist alone.

## Custom Fee

A negotiated fee is an EIP-712 message signed by `feeSigner` and submitted together with the lock:

```solidity
function lockWithCustomFee(LockParams calldata params, CustomFee calldata customFee, bytes calldata signature)
    external payable returns (uint256 lockId);
```

Domain: `name = "FlapTokenLocker"`, `version = "1"`, `chainId = 56` (mainnet) or `97` (testnet), `verifyingContract = <locker address>`.

```
CustomFee(address beneficiary,address taxToken,address lockBeneficiary,uint256 amount,
          uint64 tgeDate,uint16 tgeBps,uint64 cycle,uint16 cycleBps,
          uint96 creationFee,uint16 principalBps,uint16 dividendBps,uint256 nonce,uint256 deadline)
```

`beneficiary` is the only address allowed to submit the fee; every other field must match the submitted `LockParams`, so an offer signed for one deal cannot be spent on another. Each signature is consumed once, expires at `deadline`, and both bps fields stay under the 10% cap. Rotating `feeSigner` invalidates every unconsumed offer; re-request if it changed since issuance. Custom-fee locks cannot be transferred.

## Events

| Event                        | Emitted on                                                                                                         |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `FlapLockCreated`            | `lock` / `lockWithCustomFee`; carries the full `Schedule`, so an indexer can compute vesting from this event alone |
| `FlapClaimed`                | `claim`                                                                                                            |
| `FlapDividendsSettled`       | every dividend payout, whichever entry point triggered it                                                          |
| `FlapCollected`              | an explicit `collect` call                                                                                         |
| `FlapPoked`                  | every time a pool pulls dividends                                                                                  |
| `FlapBeneficiaryTransferred` | `transferBeneficiary` and `renounceBeneficiary` (new beneficiary `address(0)`)                                     |
| `FlapRenounced`              | `renounceBeneficiary`                                                                                              |

## Errors

| Error                                                                                                      | Meaning                                                               |
| ---------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------- |
| `TokenNotAllowed`                                                                                          | Flap token whose version is not open in the registry                  |
| `DividendTokenNotSupported`                                                                                | The token's dividend token is not admitted (or has no code)           |
| `PortalUnavailable`                                                                                        | The registry could not reach the Portal; retry                        |
| `InvalidSchedule`                                                                                          | TGE in the past, bps over 10000, or a partial release without a cycle |
| `ZeroAmount`                                                                                               | Nothing arrived at the locker                                         |
| `WrongCreationFee`                                                                                         | `msg.value` differs from the creation fee                             |
| `NotBeneficiary`, `NothingToClaim`                                                                         | Caller is not the beneficiary; nothing is releasable                  |
| `CustomFeeLockNotTransferable`                                                                             | Custom-fee locks cannot change beneficiary                            |
| `InvalidSignature`, `SignatureExpired`, `SignatureUsed`, `NotFeeBeneficiary`, `Fee*Mismatch`, `FeeOverCap` | Custom-fee validation                                                 |


# Preview

This section contains previews of upcoming products and features by Flap. Content here is subject to change before final release.


# Flap AI Oracle

## 1. Overview & Roadmap

### Overview

Three simple steps — build your prompt on-chain, let us handle the AI, and get back a verified decision.

**FlapAIProvider** is a standardized on-chain AI oracle plugin built for the Flap ecosystem. It gives Vault contracts (and any smart contract) access to verifiable LLM reasoning via a **commit-and-reveal** scheme inspired by Chainlink VRF.

FlapAIProvider separates concerns of AI-powered smart contracts cleanly:

* **On-chain (commit):** The consumer calls `reason()` with a prompt and pays a BNB fee. The contract records the request and emits an event.
* **Off-chain:** The oracle backend listens for the event, feeds the prompt to the selected LLM, and obtains a numeric choice.
* **On-chain (reveal):** The oracle calls `fulfillReasoning()` with the result and an IPFS CID linking to the full reasoning proof. The consumer's callback is invoked automatically.

Every request is permanently auditable: the IPFS CID stored on-chain pins the raw LLM inputs, outputs, temperature, model version, and salt that produced the decision.

### Architecture

![Flap AI Oracle](https://2671086575-files.gitbook.io/~/files/v0/b/gitbook-x-prod.appspot.com/o/spaces%2F5KujUBwRoHVjn8OZEgtZ%2Fuploads%2Fgit-blob-5d52fd6f6e0e7572426cf3cbe7989a243a424371%2Fimage-ai.png?alt=media)

```mermaid
flowchart TD
    subgraph on-chain["🔗 On-Chain"]
        Vault(["🏦 Your Vault"])
        Provider(["⚙️ FlapAIProvider"])
    end

    subgraph off-chain["☁️ Off-Chain"]
        Oracle(["🤖 Oracle Backend"])
        LLM(["🧠 LLM Model"])
        Tools(["🔧 Tools\n(ave_token_tool, etc.)"])
        IPFS(["📦 IPFS\n(proof storage)"])
    end

    Vault -->|"① pay fee + send prompt"| Provider
    Provider -->|"② emit request event"| Oracle
    Oracle -->|"③ forward prompt"| LLM
    LLM -->|"④ request tool data"| Tools
    Tools -->|"⑤ return data"| LLM
    LLM -->|"⑥ return choice"| Oracle
    Oracle -->|"⑦ upload reasoning proof"| IPFS
    Oracle -->|"⑧ fulfill / refund"| Provider
    Provider -->|"⑨ callback with result\n— or — BNB refund"| Vault
```

### Roadmap

| Release          | Features                                                                                                                                                                                         |
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **v1 (current)** | Commit-and-reveal reasoning, multi-model registry, IPFS proof anchoring, refund fallback, `FlapAIConsumerBase` integration base, **tool calling** support (currently `ave_token_tool` available) |
| **v2 (next)**    | Additional tools, multi-step reasoning workflows, and enhanced oracle features                                                                                                                   |

> **Note:** The LLM can only return a single numeric choice in the range `[0, numOfChoices)`.

***

## 2. Deployed Addresses & Supported Models

### Deployed Addresses

| Network     | Chain ID | Address                                                                                                                        | AI Explorer                                        | Max Gas Limit |
| ----------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------- | ------------- |
| BSC Testnet | 97       | [`0xFfddcE44e8cFf7703Fd85118524bfC8B2f70b744`](https://testnet.bscscan.com/address/0xFfddcE44e8cFf7703Fd85118524bfC8B2f70b744) | [AI Scan BNB](https://flap.sh/ai/bnb-testnet/scan) | 2,000,000     |
| BNB Mainnet | 56       | [`0xaEe3a7Ca6fe6b53f6c32a3e8407eC5A9dF8B7E39`](https://bscscan.com/address/0xaEe3a7Ca6fe6b53f6c32a3e8407eC5A9dF8B7E39)         | [AI Scan BNB](https://flap.sh/ai/bnb/scan)         | 2,000,000     |

**Max Gas Limit:** The maximum gas limit supported for a `fulfillReasoning` callback.

{% hint style="warning" %}
**Testnet note:** Since AI inference is expensive, our testnet LLM backend operates with a limited budget. Fulfillment may not respond if funds are exhausted. If you'd like to test on testnet, feel free to reach out to us. Note that some tools may not be supported on testnet (e.g. `ave_token_tool`).
{% endhint %}

### Supported Models

The following models are available on both BSC Testnet and BNB Mainnet:

| Model ID | Name                          | Price per Request      |
| -------- | ----------------------------- | ---------------------- |
| `0`      | `google/gemini-3-flash`       | ~~0.01 BNB~~ 0.005 BNB |
| `1`      | `anthropic/claude-sonnet-4.6` | 0.05 BNB               |
| `2`      | `deepseek/deepseek-r1`        | 0.03 BNB               |
| `3`      | `deepseek/deepseek-v4-flash`  | 0.01 BNB               |

Query any model on-chain at any time:

```solidity
IFlapAIProvider.Model memory m = IFlapAIProvider(flapAIProvider).getModel(modelId);
// m.name, m.price, m.enabled
```

***

## 3. Tool Calling Support

FlapAIProvider supports **tool calling**, allowing the LLM to fetch real-time external data before making a decision. The oracle backend automatically executes tool calls embedded in your prompt and injects the results into the LLM context.

### Available Tools

Currently, the following tool is supported:

#### `ave_token_tool`

Fetches comprehensive market data for a Flap token, including:

* Current price and market cap
* Price change percentages
* Trading volume (24h, 7d)
* Holder count
* Liquidity status

**Usage example in prompt:**

```solidity
string memory prompt = string(abi.encodePacked(
    "I am managing a vault for token 0x1234...5678. ",
    "My main goal is to use my fund for market making. ",
    "Check the market data (use ave_token_tool) of this token and decide: ",
    "(0) buy tokens to support the floor, ",
    "(1) sell holdings to gain more funds for future market making, ",
    "(2) generate volumes only (buy and sell immediately)."
));
```

The oracle will automatically detect the tool request, fetch the token data, and provide it to the LLM before it makes a choice.

{% hint style="info" %}
**Need additional tools?** If you need specific data sources or APIs to implement your vault strategy, feel free to reach out to us. We're actively expanding the tool library based on community needs.
{% endhint %}

***

## 4. How to Integrate

### Step 1 — Extend `FlapAIConsumerBase`

Your contract must inherit `FlapAIConsumerBase` and implement three members:

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {FlapAIConsumerBase, IFlapAIProvider} from "src/plugins/AIProvider/IFlapAIProvider.sol";

contract MyVault is FlapAIConsumerBase {
    // Track the outstanding request.
    uint256 private _lastRequestId;

    // Required: expose the pending request ID (0 = none).
    function lastRequestId() public view override returns (uint256) {
        return _lastRequestId;
    }

    // Required: handle the LLM's decision.
    // At most 1M gas is forwarded, so keep it efficient and avoid complex logic or external calls.
    function _fulfillReasoning(uint256 requestId, uint8 choice) internal override {
        require(requestId == _lastRequestId, "unknown request");
        _lastRequestId = 0;

        if (choice == 0) {
            _executeBuy();
        } else if (choice == 1) {
            _executeSell();
        } else {
            // noop
        }
    }

    // Required: handle a refund / failed request.
    function _onFlapAIRequestRefunded(uint256 requestId) internal override {
        require(requestId == _lastRequestId, "unknown request");
        _lastRequestId = 0;
        // Re-enable any paused operations or retry logic here.
    }
}
```

### Step 2 — Submit a Reasoning Request

The vault triggers `reason()` inside `receive()`. An internal `_shouldReason()` method encapsulates the go/no-go logic (balance threshold, cooldown, etc.) so the `receive()` body stays clean:

```solidity
receive() external payable {
    // Only one request at a time.
    if (_lastRequestId != 0) return;

    // Delegate the go/no-go decision to an internal helper.
    if (!_shouldReason()) return;

    uint256 MODEL_ID = 0; // google/gemini-3-flash
    uint8   NUM_CHOICES = 3;

    // Build a self-contained prompt that defines all choices.
    string memory prompt = string(abi.encodePacked(
        "You are a DeFi vault manager. The vault holds 10 BNB. ",
        "Current price is $300. 7-day trend is +15%. ",
        "Choose one action:\n",
        "0 = buy more BNB\n",
        "1 = sell all BNB\n",
        "2 = do nothing\n",
        "Respond with only the number of your choice."
    ));

    // Fetch the required fee from the registry.
    IFlapAIProvider provider = IFlapAIProvider(_getFlapAIProvider());
    uint256 fee = provider.getModel(MODEL_ID).price;

    _lastRequestId = provider.reason{value: fee}(MODEL_ID, prompt, NUM_CHOICES);
}

/// @dev Override to implement your go/no-go logic (e.g. balance threshold, cooldown).
function _shouldReason() internal view virtual returns (bool);
```

> **Cost note:** `reason()` does **not** refund excess `msg.value`. Overpayment becomes protocol revenue. Always pass exactly `model.price` (or query it on-chain first).

### Step 3 — Verify a Fulfilled Request

After the oracle calls back, the full reasoning proof is available on-chain forever:

```solidity
string memory cid = IFlapAIProvider(_getFlapAIProvider()).getReasoningCid(requestId);
// Fetch from IPFS: https://ipfs.io/ipfs/<cid>
```

### Step 4 — Override `_getFlapAIProvider()` for Testing

In tests or on unsupported chains, override the address resolver:

```solidity
function _getFlapAIProvider() internal view override returns (address) {
    return address(mockProvider); // your test double
}
```

### Full Example — Minimal Consumer

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {FlapAIConsumerBase, IFlapAIProvider} from "src/plugins/AIProvider/IFlapAIProvider.sol";
import {VaultBase} from "src/interfaces/VaultBase.sol";

/// @notice Minimal example consumer that lets an LLM decide between buy / sell / noop.
contract MinimalVault is VaultBase, FlapAIConsumerBase {
    uint256 public lastRequestId;
    uint8   public lastChoice;

    uint256 constant MODEL_ID    = 0;   // google/gemini-3-flash
    uint8   constant NUM_CHOICES = 3;
    uint256 constant THRESHOLD   = 1 ether; // only ask AI when vault holds > 1 BNB

    /// @notice Accept incoming tax revenue from the token.
    /// @dev Triggers AI reasoning when _shouldReason() approves.
    receive() external payable {
        // Only send one request at a time; skip if one is already pending.
        if (lastRequestId != 0) return;

        // Delegate the go/no-go decision to an internal helper.
        if (!_shouldReason()) return;

        IFlapAIProvider provider = IFlapAIProvider(_getFlapAIProvider());
        uint256 fee = provider.getModel(MODEL_ID).price;

        // Build a self-contained prompt that describes the vault state and
        // the meaning of every numbered choice.
        string memory prompt = string(abi.encodePacked(
            "You are a DeFi vault manager for a tax token on BNB Chain. ",
            "The vault currently holds ", _uint2str(address(this).balance / 1 ether), " BNB. ",
            "The threshold to act is 1 BNB. ",
            "Decide what to do with the accumulated BNB:\n",
            "0 = buyback tokens using all available BNB\n",
            "1 = hold and accumulate more BNB\n",
            "2 = distribute BNB to token holders\n",
            "Respond with only the number of your choice."
        ));

        lastRequestId = provider.reason{value: fee}(MODEL_ID, prompt, NUM_CHOICES);
    }

    /// @dev Override to implement your trigger logic (e.g. balance threshold, cooldown).
    function _shouldReason() internal view override returns (bool) {}

    function _fulfillReasoning(uint256 requestId, uint8 choice) internal override {
        require(requestId == lastRequestId);
        lastRequestId = 0;
        lastChoice = choice;

        if (choice == 0) {
            // TODO: execute buyback via _getPortal()
        } else if (choice == 1) {
            // hold — do nothing
        } else if (choice == 2) {
            // TODO: distribute BNB to token holders
        }
    }

    function _onFlapAIRequestRefunded(uint256 requestId) internal override {
        require(requestId == lastRequestId);
        lastRequestId = 0;
        // Next receive() call will retry automatically.
    }

    /// @inheritdoc VaultBase
    function description() public view override returns (string memory) {
        return string(abi.encodePacked(
            "Minimal AI vault. Balance: ", _uint2str(address(this).balance / 1 ether), " BNB. ",
            "Last choice: ", _uint2str(lastChoice), "."
        ));
    }

    // ----------------------------------------------------------------
    //  Internal helpers
    // ----------------------------------------------------------------

    function _uint2str(uint256 v) internal pure returns (string memory) {
        if (v == 0) return "0";
        uint256 tmp = v;
        uint256 digits;
        while (tmp != 0) { digits++; tmp /= 10; }
        bytes memory buf = new bytes(digits);
        while (v != 0) { buf[--digits] = bytes1(uint8(48 + v % 10)); v /= 10; }
        return string(buf);
    }
}
```

***

## 5. References

### `IFlapAIProvider.sol`

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

// ============================================================
//                    IFlapAIProvider Interface
// ============================================================

/// @title IFlapAIProvider
/// @notice Oracle interface for the FlapAIProvider commit-and-reveal AI reasoning service.
/// @dev The FlapAIProvider operates as a commit-and-reveal oracle inspired by Chainlink VRF.
///      Consumers (Vault contracts) submit a reasoning request on-chain by calling `reason()`,
///      which emits an event. The off-chain oracle backend picks up the event, feeds the prompt
///      to an LLM, and posts the result back via `fulfillReasoning()`. If the backend cannot
///      process the request, it calls `refundRequest()` to return the BNB fee and notify the
///      consumer. Consumers must extend `FlapAIConsumerBase` to receive callbacks securely.
///
///      TOOL CALLING: The oracle backend supports tool calling, allowing the LLM to fetch
///      real-time external data (e.g. token market data, prices) before producing its choice.
///      Consumers embed tool invocations in the prompt using a structured format; the backend
///      executes the tools and injects their results into the LLM context automatically.
///      For the full list of supported tools, see:
///        https://docs.flap.sh/flap/developers/preview/flap-ai-oracle
///
///      Example — `ave_token_info` tool:
///        "I am managing a vault for token 0x....7777, "
///        "my main goal is to use my fund to market making for this token. "
///        "Check the market data (use ave_token_info tool) of this token and then decide what to do: "
///        "(0) buy tokens to support the floor "
///        "(1) sell my holdings to gain more funds for future market making "
///        "(2) generate volumes only (i.e: buy and then sell immediately)."
interface IFlapAIProvider {
    // ----------------------------------------------------------------
    //  Structs
    // ----------------------------------------------------------------

    /// @notice Represents a registered LLM model.
    /// @param name   Human-readable name of the model (e.g. "gpt-4").
    /// @param price  Per-request fee in native currency (BNB), in wei.
    /// @param enabled Whether the model currently accepts new requests.
    struct Model {
        string name;
        uint256 price;
        bool enabled;
    }

    /// @notice Lifecycle state of an AI reasoning request.
    /// @dev NONE      = default, request was never created.
    ///      PENDING   = request submitted on-chain, awaiting oracle fulfillment.
    ///      FULFILLED = oracle fulfilled the request and consumer callback succeeded.
    ///      UNDELIVERED = oracle fulfilled but consumer callback reverted (result stored but not delivered).
    ///      REFUNDED  = oracle refunded the request fee to the consumer.
    enum RequestStatus {
        NONE,
        PENDING,
        FULFILLED,
        UNDELIVERED,
        REFUNDED
    }

    /// @notice Stores all data for a single AI reasoning request.
    /// @dev Tightly packed into two 32-byte storage slots:
    ///      Slot 0 (immutable after `reason()`): consumer (160) + modelId (16) + numOfChoices (8) + timestamp (64) = 248 bits
    ///      Slot 1 (written by oracle): feePaid (128) + status (8) + choice (8) + reserved (112) = 256 bits
    ///      Separating immutable fields from mutable oracle results means `fulfillReasoning` and
    ///      `refundRequest` only issue a single SSTORE to slot 1, leaving slot 0 untouched.
    /// @param consumer     Address of the consuming contract that submitted the request.
    /// @param modelId      ID of the LLM model used (max 65535 distinct models).
    /// @param numOfChoices Number of choices the LLM can pick from (valid range 0..numOfChoices-1).
    /// @param timestamp    `block.timestamp` when the request was submitted via `reason()`.
    /// @param feePaid      BNB amount paid with the request (full `msg.value`), capped at uint128.
    /// @param status       Current lifecycle status of the request.
    /// @param choice       The LLM's chosen action index; only valid when status is FULFILLED or UNDELIVERED.
    /// @param reserved     Reserved for future upgrades.
    struct Request {
        // slot 0 — immutable after reason()
        address consumer; // 160 bits
        uint16 modelId; //  16 bits
        uint8 numOfChoices; //   8 bits
        uint64 timestamp; //  64 bits
        // slot 1 — written by fulfillReasoning() / refundRequest()
        uint128 feePaid; // 128 bits
        RequestStatus status; //   8 bits
        uint8 choice; //   8 bits
        uint112 reserved; // 112 bits
    }

    // ----------------------------------------------------------------
    //  Custom Errors
    // ----------------------------------------------------------------

    /// @notice Read-only summary of a request, used exclusively by explorer view functions.
    /// @dev Avoids multiple RPC calls per request in list views by bundling all fields —
    ///      including the IPFS CID — into a single return value.
    /// @param requestId   The unique request ID.
    /// @param consumer    Address of the consuming contract that submitted the request.
    /// @param modelId     ID of the LLM model used.
    /// @param numOfChoices Number of choices the LLM could pick from.
    /// @param timestamp   `block.timestamp` when the request was submitted.
    /// @param feePaid     BNB amount paid with the request (in wei).
    /// @param status      Current lifecycle status of the request.
    /// @param choice      The LLM's chosen action index; only meaningful when status is FULFILLED or UNDELIVERED.
    /// @param reasoningCid IPFS CID of the reasoning proof; non-empty only when status is FULFILLED or UNDELIVERED.
    struct RequestView {
        uint256 requestId;
        address consumer;
        uint16 modelId;
        uint8 numOfChoices;
        uint64 timestamp;
        uint128 feePaid;
        RequestStatus status;
        uint8 choice;
        string reasoningCid;
    }

    /// @notice Reverts when the prompt byte length exceeds `maxPromptLength`.
    /// @param promptLength   Actual length of the provided prompt in bytes.
    /// @param maxPromptLength Current maximum allowed prompt length.
    error FlapAIProviderPromptExceedsMaxLength(uint256 promptLength, uint256 maxPromptLength);

    /// @notice Reverts when `numOfChoices` is zero; there must be at least one choice.
    /// @param numOfChoices The invalid zero value that was passed.
    error FlapAIProviderInvalidNumOfChoices(uint8 numOfChoices);

    /// @notice Reverts when `fulfillReasoning` or `refundRequest` is called on a request that is not PENDING.
    /// @param requestId The request ID that is not in the PENDING state.
    error FlapAIProviderRequestNotPending(uint256 requestId);

    /// @notice Reverts when the oracle's `choice` is >= `numOfChoices`.
    /// @param choice       The out-of-range choice returned by the oracle.
    /// @param numOfChoices The number of valid choices for the request.
    error FlapAIProviderChoiceOutOfRange(uint8 choice, uint8 numOfChoices);

    /// @notice Reverts when the BNB sent with `reason()` is less than the model's required price.
    /// @param sent     Amount of BNB sent (in wei).
    /// @param required Minimum required fee for the selected model (in wei).
    error FlapAIProviderInsufficientFee(uint256 sent, uint256 required);

    /// @notice Reverts when `reason()` or `getModel()` is called with a `modelId` that was never registered.
    /// @param modelId The unregistered model ID.
    error FlapAIProviderModelNotRegistered(uint256 modelId);

    /// @notice Reverts when `reason()` is called with a registered but currently disabled model.
    /// @param modelId The disabled model ID.
    error FlapAIProviderModelNotEnabled(uint256 modelId);

    /// @notice Reverts when `setCallbackGasLimit()` is called with a value below the minimum of 1_000_000.
    /// @param provided The gas limit value that was passed.
    /// @param minimum  The minimum allowed gas limit (1_000_000).
    error FlapAIProviderCallbackGasLimitTooLow(uint256 provided, uint256 minimum);

    // ----------------------------------------------------------------
    //  Events
    // ----------------------------------------------------------------

    /// @notice Emitted when a new AI reasoning request is submitted.
    /// @param requestId    Unique identifier for this request.
    /// @param consumer     Address of the consuming contract.
    /// @param modelId      ID of the LLM model selected.
    /// @param prompt       The full prompt string sent to the oracle.
    /// @param numOfChoices Number of choices the LLM can return.
    /// @param feePaid      BNB amount paid with the request (full msg.value).
    event FlapAIProviderRequestMade(
        uint256 requestId, address consumer, uint256 modelId, string prompt, uint8 numOfChoices, uint256 feePaid
    );

    /// @notice Emitted when the oracle successfully fulfills a request and the consumer callback succeeds.
    /// @param requestId              The fulfilled request ID.
    /// @param consumer               Address of the consuming contract.
    /// @param choice                 The choice returned by the LLM (0..numOfChoices-1).
    /// @param reasoningDetailsIpfsCid IPFS CID of the full reasoning proof stored on IPFS.
    event FlapAIProviderRequestFulfilled(
        uint256 requestId, address consumer, uint8 choice, string reasoningDetailsIpfsCid
    );

    /// @notice Emitted when the oracle fulfills a request but the consumer callback reverts.
    /// @param requestId              The request ID whose consumer callback failed.
    /// @param consumer               Address of the consuming contract.
    /// @param choice                 The choice returned by the LLM.
    /// @param reasoningDetailsIpfsCid IPFS CID of the reasoning proof (still stored on-chain).
    /// @param reason                 The revert data from the consumer callback.
    event FlapAIProviderRequestUndelivered(
        uint256 requestId, address consumer, uint8 choice, string reasoningDetailsIpfsCid, bytes reason
    );

    /// @notice Emitted when the oracle refunds a pending request.
    /// @param requestId    The refunded request ID.
    /// @param consumer     Address of the consuming contract that receives the refund.
    /// @param refundAmount BNB amount refunded (equals the original feePaid).
    event FlapAIProviderRequestRefunded(uint256 requestId, address consumer, uint256 refundAmount);

    /// @notice Emitted when the oracle marks a request as refunded but the consumer callback
    ///         reverts or runs out of gas, leaving the BNB stranded in the provider.
    /// @param requestId    The refunded request ID.
    /// @param consumer     Address of the consuming contract whose callback failed.
    /// @param refundAmount BNB amount that could not be delivered (equals the original feePaid).
    /// @param reason       The revert data from the consumer callback (empty on OOG).
    event FlapAIProviderRefundUndelivered(uint256 requestId, address consumer, uint256 refundAmount, bytes reason);

    /// @notice Emitted when the maximum prompt length is updated.
    /// @param oldMaxPromptLength Previous maximum prompt length in bytes.
    /// @param newMaxPromptLength New maximum prompt length in bytes.
    event FlapAIProviderMaxPromptLengthUpdated(uint256 oldMaxPromptLength, uint256 newMaxPromptLength);

    /// @notice Emitted when the consumer callback gas limit is updated.
    /// @param oldCallbackGasLimit Previous gas limit for consumer callbacks.
    /// @param newCallbackGasLimit New gas limit for consumer callbacks.
    event FlapAIProviderCallbackGasLimitUpdated(uint256 oldCallbackGasLimit, uint256 newCallbackGasLimit);

    /// @notice Emitted when a new LLM model is registered.
    /// @param modelId ID of the newly registered model.
    /// @param name    Human-readable name of the model.
    /// @param price   Per-request fee in native BNB (wei).
    event FlapAIProviderModelRegistered(uint256 modelId, string name, uint256 price);

    // ----------------------------------------------------------------
    //  Functions
    // ----------------------------------------------------------------

    /// @notice Submit an AI reasoning request to the oracle.
    /// @dev Validates that:
    ///        1. `modelId` is a registered model (reverts FlapAIProviderModelNotRegistered).
    ///        2. The model is enabled (reverts FlapAIProviderModelNotEnabled).
    ///        3. `numOfChoices > 0` (reverts FlapAIProviderInvalidNumOfChoices).
    ///        4. `bytes(prompt).length <= maxPromptLength` (reverts FlapAIProviderPromptExceedsMaxLength).
    ///        5. `msg.value >= model.price` (reverts FlapAIProviderInsufficientFee).
    ///      Any excess BNB beyond the model price is intentionally NOT refunded — it becomes protocol revenue.
    ///      Emits {FlapAIProviderRequestMade}.
    ///
    ///      TOOL CALLING: The oracle supports tool calling so the LLM can pull live data before
    ///      reasoning. Declare the tools you want available inside the prompt string. The backend
    ///      will execute any tool calls the LLM makes and feed the results back automatically.
    ///      For the complete list of supported tools visit:
    ///        https://docs.flap.sh/flap/developers/preview/flap-ai-oracle
    ///
    ///      Example using `ave_token_info` to fetch live market data for a token:
    ///        "I am managing a vault for token 0x....7777, "
    ///        "my main goal is to use my fund to market making for this token. "
    ///        "Check the market data (use ave_token_info tool) of this token and then decide what to do: "
    ///        "(0) buy tokens to support the floor "
    ///        "(1) sell my holdings to gain more funds for future market making "
    ///        "(2) generate volumes only (i.e: buy and then sell immediately)."
    /// @param modelId      ID of the LLM model to use for this request.
    /// @param prompt       Combined system + user prompt. The consumer must embed the choice meanings
    ///                     (e.g., "0 = buy, 1 = sell") inside the prompt string. Tool descriptions
    ///                     may also be included here to enable tool calling (see @dev above).
    /// @param numOfChoices Number of choices the LLM may return (valid responses are 0..numOfChoices-1).
    /// @return requestId   Unique uint256 identifier for this request. Store it to correlate with the callback.
    function reason(uint256 modelId, string calldata prompt, uint8 numOfChoices)
        external
        payable
        returns (uint256 requestId);

    /// @notice Return the full Model struct for a registered model.
    /// @dev Reverts with `FlapAIProviderModelNotRegistered` if the `modelId` was never registered.
    ///      Does NOT revert for disabled models — callers can inspect the `enabled` field.
    /// @param modelId ID of the model to query.
    /// @return model  The Model struct containing `name`, `price`, and `enabled`.
    function getModel(uint256 modelId) external view returns (Model memory model);

    /// @notice Fulfill a pending AI reasoning request.
    /// @dev Restricted to the oracle backend (FULFILLER_ROLE). Validates:
    ///        1. Request status is PENDING (reverts FlapAIProviderRequestNotPending).
    ///        2. `choice < request.numOfChoices` (reverts FlapAIProviderChoiceOutOfRange).
    ///      Stores `reasoningDetailsIpfsCid` on-chain, then calls `fulfillReasoning(requestId, choice)`
    ///      on the consumer inside a try/catch:
    ///        - On success: status → FULFILLED, emits {FlapAIProviderRequestFulfilled}.
    ///        - On consumer revert: status → UNDELIVERED, emits {FlapAIProviderRequestUndelivered}.
    ///      The IPFS CID is always stored regardless of callback outcome.
    /// @param requestId              The ID of the pending request to fulfill.
    /// @param choice                 The LLM's chosen action (must be < numOfChoices).
    /// @param reasoningDetailsIpfsCid IPFS CID of the full reasoning proof document.
    function fulfillReasoning(uint256 requestId, uint8 choice, string calldata reasoningDetailsIpfsCid) external;

    /// @notice Refund a pending request when the oracle cannot process it.
    /// @dev Restricted to the oracle backend (FULFILLER_ROLE). Validates that the request is PENDING.
    ///      Sets status to REFUNDED, transfers the original `feePaid` BNB back to the consumer,
    ///      then calls `onFlapAIRequestRefunded{gas: 1_000_000}(requestId)` on the consumer.
    ///      Emits {FlapAIProviderRequestRefunded}.
    /// @param requestId The ID of the pending request to refund.
    function refundRequest(uint256 requestId) external;

    /// @notice Returns the current maximum allowed prompt length in bytes.
    /// @return The current `_maxPromptLength` value (default: 6000).
    function maxPromptLength() external view returns (uint256);

    /// @notice Update the maximum allowed prompt length in bytes.
    /// @dev Restricted to DEFAULT_ADMIN_ROLE. Emits {FlapAIProviderMaxPromptLengthUpdated}.
    /// @param newMaxPromptLength The new maximum prompt length in bytes.
    function setMaxPromptLength(uint256 newMaxPromptLength) external;

    /// @notice Returns the gas limit forwarded to consumer callbacks (`fulfillReasoning` and `onFlapAIRequestRefunded`).
    /// @return The current `_maxCallbackGas` value (default: 1_000_000).
    function callbackGasLimit() external view returns (uint256);

    /// @notice Update the gas limit forwarded to consumer callbacks.
    /// @dev Restricted to DEFAULT_ADMIN_ROLE. Reverts with `FlapAIProviderCallbackGasLimitTooLow` if
    ///      `newCallbackGasLimit` is less than 1_000_000. Emits {FlapAIProviderCallbackGasLimitUpdated}.
    /// @param newCallbackGasLimit The new gas limit for consumer callbacks (must be >= 1_000_000).
    function setCallbackGasLimit(uint256 newCallbackGasLimit) external;

    // ----------------------------------------------------------------
    //  Explorer View Functions
    // ----------------------------------------------------------------

    /// @notice Returns the total number of reasoning requests ever submitted.
    /// @dev Equivalent to `_nextRequestId - 1`. Used by the explorer to compute pagination metadata.
    /// @return total The total request count.
    function getTotalRequests() external view returns (uint256 total);

    /// @notice Returns the total number of reasoning requests submitted by a specific consumer.
    /// @dev Used to compute per-consumer pagination metadata.
    /// @param consumer The consumer address to query.
    /// @return total The number of requests made by `consumer`.
    function getTotalRequestsByConsumer(address consumer) external view returns (uint256 total);

    /// @notice Returns a single `RequestView` for the given request ID.
    /// @dev Populates `reasoningCid` for all statuses (non-empty only when FULFILLED or UNDELIVERED).
    ///      Returns a zero-valued struct if `requestId` was never created.
    /// @param requestId The request ID to look up.
    /// @return view_    The populated `RequestView` for the given request.
    function getRequest(uint256 requestId) external view returns (RequestView memory view_);

    /// @notice Returns up to `limit` requests in newest-first order, starting at `offset` from the end.
    /// @dev `offset = 0, limit = 10` returns the 10 most recent requests.
    ///      Returns an empty array if `offset >= getTotalRequests()`.
    ///      Clamps the result to available entries — never reverts out-of-bounds.
    ///      `reasoningCid` is populated inline for each entry.
    /// @param offset Zero-based offset from the most recent request (0 = most recent).
    /// @param limit  Maximum number of entries to return.
    /// @return views Array of `RequestView` in newest-first order.
    function getRecentRequests(uint256 offset, uint256 limit) external view returns (RequestView[] memory views);

    /// @notice Returns up to `limit` requests by a specific consumer in newest-first order.
    /// @dev Same pagination semantics as `getRecentRequests` but scoped to a single consumer.
    ///      Returns an empty array if `offset >= getTotalRequestsByConsumer(consumer)`.
    /// @param consumer The consumer address to filter by.
    /// @param offset   Zero-based offset from the consumer's most recent request.
    /// @param limit    Maximum number of entries to return.
    /// @return views   Array of `RequestView` in newest-first order for the given consumer.
    function getRequestsByConsumer(address consumer, uint256 offset, uint256 limit)
        external
        view
        returns (RequestView[] memory views);
}

// ============================================================
//                FlapAIConsumerBase Abstract Contract
// ============================================================

/// @title FlapAIConsumerBase
/// @notice Base contract for contracts consuming FlapAIProvider responses.
/// @dev Inheritors must override:
///        - `_fulfillReasoning(uint256 requestId, uint8 choice)`: executes the action chosen by the LLM.
///        - `_onFlapAIRequestRefunded(uint256 requestId)`: handles refund cleanup or retry logic.
///        - `lastRequestId()`: returns the consumer's most recent pending request ID (0 if none).
///      The `onlyFlapAIProvider` modifier is applied automatically to the external entry-points
///      `fulfillReasoning` and `onFlapAIRequestRefunded`, ensuring that only the FlapAIProvider
///      contract can invoke them. The provider address is resolved at runtime via
///      `_getFlapAIProvider()` using `block.chainid`, so no constructor parameter is needed.
abstract contract FlapAIConsumerBase {
    // ----------------------------------------------------------------
    //  Custom Errors
    // ----------------------------------------------------------------

    /// @notice Reverts when a function guarded by `onlyFlapAIProvider` is called by a non-provider address.
    error FlapAIConsumerOnlyProvider();

    /// @notice Reverts when `_getFlapAIProvider()` is queried on an unsupported chain.
    /// @param chainId The unsupported chain ID.
    error FlapAIConsumerUnsupportedChain(uint256 chainId);

    // ----------------------------------------------------------------
    //  Modifier
    // ----------------------------------------------------------------

    /// @dev Guards external entry-points so only the FlapAIProvider contract can call them.
    ///      Reverts with `FlapAIConsumerOnlyProvider` if called by any other address.
    modifier onlyFlapAIProvider() {
        if (msg.sender != _getFlapAIProvider()) revert FlapAIConsumerOnlyProvider();
        _;
    }

    // ----------------------------------------------------------------
    //  Provider Address Resolution
    // ----------------------------------------------------------------

    /// @notice Returns the FlapAIProvider proxy address for the current chain.
    /// @dev Uses `block.chainid` to resolve the stable proxy address:
    ///        - Chain 56  (BSC Mainnet): returns `address(0)` — placeholder, update when deployed.
    ///        - Chain 97  (BSC Testnet): returns `address(0)` — placeholder, update when deployed.
    ///        - Any other chain: reverts with `FlapAIConsumerUnsupportedChain`.
    ///      Subclasses may override this function to support additional chains or test environments.
    /// @return The address of the FlapAIProvider proxy on the current chain.
    function _getFlapAIProvider() internal view virtual returns (address) {
        uint256 id = block.chainid;
        if (id == 56) {
            return 0xaEe3a7Ca6fe6b53f6c32a3e8407eC5A9dF8B7E39;
        } else if (id == 97) {
            return 0xFfddcE44e8cFf7703Fd85118524bfC8B2f70b744;
        } else {
            revert FlapAIConsumerUnsupportedChain(id);
        }
    }

    // ----------------------------------------------------------------
    //  Consumer State (abstract)
    // ----------------------------------------------------------------

    /// @notice Returns the request ID of the most recent AI reasoning request made by this consumer.
    /// @dev Must be overridden by the consuming contract. Returns `0` if no request has been made yet.
    ///      This allows the provider backend and frontends to look up the consumer's outstanding request
    ///      without iterating through the provider's full request history.
    /// @return The last submitted request ID, or 0 if none.
    function lastRequestId() public view virtual returns (uint256);

    // ----------------------------------------------------------------
    //  External Entry-Points (access-controlled)
    // ----------------------------------------------------------------

    /// @notice Called by the FlapAIProvider to deliver the LLM's chosen action.
    /// @dev Only callable by the FlapAIProvider contract (enforced by `onlyFlapAIProvider`).
    ///      Delegates to the internal virtual `_fulfillReasoning` for override-able logic.
    ///      This external/internal split prevents inheritors from inadvertently removing the
    ///      access control check while still overriding the business logic.
    /// @param requestId The ID of the fulfilled request.
    /// @param choice    The LLM's chosen action (index in 0..numOfChoices-1).
    function fulfillReasoning(uint256 requestId, uint8 choice) external onlyFlapAIProvider {
        _fulfillReasoning(requestId, choice);
    }

    /// @notice Called by the FlapAIProvider when a request is refunded.
    /// @dev Only callable by the FlapAIProvider contract (enforced by `onlyFlapAIProvider`).
    ///      Delegates to the internal virtual `_onFlapAIRequestRefunded`.
    ///      The provider calls this with `{value: feePaid}` so the BNB refund is delivered here
    ///      rather than via a bare ETH transfer (which would trigger `receive()`/`fallback()`).
    ///      The provider also applies an explicit gas cap to prevent unbounded gas consumption.
    /// @param requestId The ID of the refunded request.
    function onFlapAIRequestRefunded(uint256 requestId) external payable onlyFlapAIProvider {
        _onFlapAIRequestRefunded(requestId);
    }

    // ----------------------------------------------------------------
    //  Internal Virtual Hooks (to be overridden)
    // ----------------------------------------------------------------

    /// @notice Internal hook invoked when the FlapAIProvider delivers the LLM's choice.
    /// @dev Must be overridden by the consuming contract to execute the action corresponding to `choice`.
    ///      The consumer contract is responsible for mapping each `choice` index to its intended action,
    ///      consistent with the meanings encoded in the original prompt.
    /// @param requestId The ID of the fulfilled request.
    /// @param choice    The LLM's chosen action index.
    function _fulfillReasoning(uint256 requestId, uint8 choice) internal virtual;

    /// @notice Internal hook invoked when the FlapAIProvider refunds a request.
    /// @dev Must be overridden by the consuming contract to perform cleanup or retry logic
    ///      (e.g., reset a pending-action flag, re-enable a paused operation).
    ///      `msg.value` equals the original `feePaid` — the BNB refund is delivered with this call.
    /// @param requestId The ID of the refunded request.
    function _onFlapAIRequestRefunded(uint256 requestId) internal virtual;
}

```


# Flap Video Generator

{% hint style="warning" %}
**Preview.** This document has not been reviewed yet. Content may be incomplete or inaccurate.
{% endhint %}

## 1. Overview

**Flap Video Generator** lets any smart contract (a Flap vault, a game, a story contract) generate AI video clips **on-chain**. You call `generateVideo()` on `FlapAIProvider` with a text prompt and a flat BNB fee. Flap's backend generates the clip, pins it to IPFS, and writes the result back on-chain.

Each requester (`msg.sender`) owns an append-only **video session**. Each fulfilled clip is appended to it, and the clips are encoded so that playing them back-to-back gives **one continuous stream**. A contract can grow a story clip by clip, and anyone can watch the whole session as a single video.

Clips can be conditioned on **reference images** (IPFS CIDs). That gives you two separate controls:

* **Continuity:** the next clip starts exactly where the previous one ended.
* **Character consistency:** the same character keeps the same look across clips.

Section 4 explains how to combine the two, using a real deployed consumer as the example.

{% hint style="success" %}
**Live demo:** [video.munch.cash](https://video.munch.cash/) plays real on-chain sessions on BSC Testnet and BNB Mainnet (switch at the top of the page). Connect a wallet to extend the story with your own prompt.
{% endhint %}

```mermaid
flowchart TD
    subgraph on-chain["🔗 On-Chain"]
        Consumer(["🏦 Your Contract<br/>(vault / story consumer)"])
        Provider(["⚙️ FlapAIProvider"])
        Session(["🎞️ Video Session<br/>(per requester, append-only)"])
    end

    subgraph off-chain["☁️ Off-Chain"]
        Backend(["🤖 Flap Video Backend"])
        Model(["🎬 Video Model"])
        IPFS(["📦 IPFS<br/>(clips + last frames)"])
    end

    Consumer -->|"① generateVideo(prompt, referenceType, referenceCids) + fee"| Provider
    Provider -->|"② emit FlapAIProviderVideoRequested"| Backend
    Backend -->|"③ prompt + reference images"| Model
    Model -->|"④ video"| Backend
    Backend -->|"⑤ upload clip + last frame"| IPFS
    Backend -->|"⑥ submit clip CIDs"| Provider
    Provider -->|"⑦ append clip"| Session
    Provider -->|"⑧ onVideoRequestSettled(result)"| Consumer
```

### Key properties

| Property              | Value                                                                                                   |
| --------------------- | ------------------------------------------------------------------------------------------------------- |
| Fee                   | Flat fee per clip, read it from `videoGenerateFee()`; `msg.value` must match **exactly**                |
| Concurrency           | **One pending request per requester.** A second call reverts until the first one is fulfilled or failed |
| Output                | MPEG-TS clip CID + last-frame JPEG CID + duration, stored on-chain in the requester's session           |
| Clip format (current) | \~8 s, 720p, 16:9                                                                                       |
| Typical latency       | \~1–2 minutes from request to fulfillment                                                               |
| Callback              | Optional best-effort `IFlapVideoReceiver.onVideoRequestSettled`, which can never block fulfillment      |
| Failure               | Terminal `FAILED` with a machine-readable reason. **No refund**                                         |

***

## 2. Deployed Addresses

The video generator lives on the same `FlapAIProvider` contract as the [Flap AI Oracle](/flap/developers/preview/flap-ai-oracle).

| Network     | Chain ID | FlapAIProvider                                                                                                                 | Fee per clip | Callback gas limit |
| ----------- | -------- | ------------------------------------------------------------------------------------------------------------------------------ | ------------ | ------------------ |
| BSC Testnet | 97       | [`0xFfddcE44e8cFf7703Fd85118524bfC8B2f70b744`](https://testnet.bscscan.com/address/0xFfddcE44e8cFf7703Fd85118524bfC8B2f70b744) | 0.01 BNB     | 2,000,000          |
| BNB Mainnet | 56       | [`0xaEe3a7Ca6fe6b53f6c32a3e8407eC5A9dF8B7E39`](https://bscscan.com/address/0xaEe3a7Ca6fe6b53f6c32a3e8407eC5A9dF8B7E39)         | 0.01 BNB     | 2,000,000          |

{% hint style="warning" %}
**Always call `videoGenerateFee()` at request time. Never hardcode the fee.** It is configurable and may change. A wrong `msg.value` (too much or too little) reverts with `FlapAIProviderWrongVideoFee`.
{% endhint %}

{% hint style="info" %}
**Testnet note:** video generation is expensive, so the testnet backend runs on a limited budget and may stop fulfilling if it runs out. If you'd like to test on testnet, feel free to reach out to us.
{% endhint %}

### Demo consumers

Both are `ReferenceStoryVideoConsumer` (§4) and can be watched and extended in the [demo app](https://video.munch.cash/).

| Network     | Consumer                                                                                                                       | Character               | Story                           | Watch                                                                          |
| ----------- | ------------------------------------------------------------------------------------------------------------------------------ | ----------------------- | ------------------------------- | ------------------------------------------------------------------------------ |
| BSC Testnet | [`0xB7f4398BEECE5Ec0da06497e647107D697c55c66`](https://testnet.bscscan.com/address/0xB7f4398BEECE5Ec0da06497e647107D697c55c66) | Ice boy (2D cartoon)    | *The Ice Boy Who Melted Upward* | [video.munch.cash/?network=testnet](https://video.munch.cash/?network=testnet) |
| BNB Mainnet | [`0x8b527D3f104A1945BD68B391fBd6b4a00B7EA3a2`](https://bscscan.com/address/0x8b527D3f104A1945BD68B391fBd6b4a00B7EA3a2)         | Arthur (photorealistic) | *Arthur's Boat*, 8 clips, 64 s  | [video.munch.cash/?network=mainnet](https://video.munch.cash/?network=mainnet) |

***

## 3. Reference Types

`generateVideo` takes a `uint8 referenceType` that tells the backend how to use the reference image CIDs you pass:

| Code | Constant                         | `referenceCids`                               | How the image is used                                                                                        | Enabled             |
| ---- | -------------------------------- | --------------------------------------------- | ------------------------------------------------------------------------------------------------------------ | ------------------- |
| `0`  | `VIDEO_REF_NONE`                 | empty                                         | Text-to-video, no image                                                                                      | ✅                   |
| `1`  | `VIDEO_REF_PREV_LAST_FRAME`      | **empty** (the provider resolves it on-chain) | The previous clip's **last frame becomes the exact first frame** of the new clip                             | ✅                   |
| `2`  | `VIDEO_REF_FIRST_FRAME`          | exactly 1                                     | Your image becomes the exact first frame                                                                     | ✅                   |
| `3`  | `VIDEO_REF_FIRST_AND_LAST_FRAME` | exactly 2 `[first, last]`                     | Pinned first and last frames                                                                                 | ❌ (not enabled yet) |
| `4`  | `VIDEO_REF_CHARACTER`            | 1–3                                           | **Soft references** that guide the character's look, subject and style. They are **not** pinned to any frame | ✅                   |

Rules enforced on-chain:

* At most `MAX_VIDEO_REFERENCE_CIDS = 3` CIDs per request, each non-empty and ≤ `MAX_VIDEO_REFERENCE_CID_LENGTH = 128` bytes.
* A disabled type reverts with `FlapAIProviderUnsupportedReferenceType`. Check `isVideoReferenceTypeEnabled(type)` first.
* Type `1` reverts with `FlapAIProviderNoPreviousClip` if the requester has no clip yet. The first clip of a session must use another type.
* A wrong CID count reverts with `FlapAIProviderBadReferenceCidCount`.

{% hint style="info" %}
The legacy one-argument `generateVideo(prompt)` is still supported. It automatically uses type `0` for the first clip and type `1` for every later clip.
{% endhint %}

### Frame references vs. character references: you can't fully have both in one clip

This is the most important thing to understand when designing your consumer:

```mermaid
flowchart LR
    subgraph T1["Type 1: PREV_LAST_FRAME"]
        A1["Previous clip's<br/>last frame"] -->|"pinned as frame 0"| B1["New clip"]
    end
    subgraph T4["Type 4: CHARACTER"]
        A4["Character image"] -->|"identity guide"| B4["New clip"]
        C4["Previous last frame<br/>(optional)"] -->|"scene guide only"| B4
    end
```

|                                      | **Type 1 (previous last frame)**                                                                                                                              | **Type 4 (character + last frame as references)**                   |
| ------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- |
| Seamless join with the previous clip | ✅ **Pixel-exact.** The new clip opens on the old clip's final frame                                                                                           | ⚠️ Same scene, setting and style, but a **visible cut** at the join |
| Character consistency                | ⚠️ Carried only through the pixels of the starting frame. Details **drift** over many clips (e.g. the model adds clothing that wasn't in the original design) | ✅ **Strong.** The character image is re-applied on every clip       |

The current video model can't take a pinned starting frame **and** a soft character reference in the same clip. When a frame is pinned, soft references are ignored. That is why these are two separate types and not one combined mode.

**Recommended pattern: alternate the two types.**

* Use **type 4** `[character, previous last frame]` to (re)anchor the character, at the start of each "scene".
* Use **type 1** for the following beat(s) to keep motion seamless.

Every type-4 clip resets any drift from the type-1 clips before it, and the type-1 clips keep the story flowing without cuts.

**Prompt tips:**

* In type 4, **say which reference image is which** at the start of the prompt (e.g. *"Reference image 1 is the main character … Reference image 2 is the final frame of the previous clip …"*).
* Restate the character's key traits (colours, outfit, style) in the prompt. The model then follows them more closely, in both types.

***

## 4. Example: `ReferenceStoryVideoConsumer`

`ReferenceStoryVideoConsumer` is a small contract that builds a continuous story around **one fixed character**. Anyone can pay the fee to add the next clip and choose, per clip, between:

* **Type 1:** continue seamlessly from the previous clip.
* **Type 4:** re-anchor on the character, with the previous last frame as a scene hint.

The contract checks the reference CIDs and adds the "which image is which" prefix to the prompt itself, so the prompt always matches the images.

|                 |                                                                                                                                                  |
| --------------- | ------------------------------------------------------------------------------------------------------------------------------------------------ |
| Network         | BSC Testnet                                                                                                                                      |
| Consumer        | [`0xB7f4398BEECE5Ec0da06497e647107D697c55c66`](https://testnet.bscscan.com/address/0xB7f4398BEECE5Ec0da06497e647107D697c55c66) (source verified) |
| Provider        | `0xFfddcE44e8cFf7703Fd85118524bfC8B2f70b744`                                                                                                     |
| Character image | `bafkreigo6g3mkveu5w3l7ud56qr4oq3sa62hawcdmybdhbi5agurqwm5ye` (an "ice boy", flat 2D cartoon style)                                              |

### Request flow

```mermaid
sequenceDiagram
    participant U as Caller (anyone)
    participant C as ReferenceStoryVideoConsumer
    participant P as FlapAIProvider
    participant B as Flap Video Backend

    U->>C: generateVideo(prompt, type, cids) + fee
    C->>P: getLastVideoFrameCid(this)
    alt type 1 (PREV_LAST_FRAME)
        C->>C: require a previous clip, cids == []
    else type 4 (CHARACTER)
        C->>C: require cids == [CHARACTER_CID] (first clip)<br/>or [CHARACTER_CID, lastFrame]
        C->>C: prefix prompt with "image 1 = character, image 2 = previous scene"
    end
    C->>P: generateVideo{value: fee}(fullPrompt, type, cids)
    P-->>B: FlapAIProviderVideoRequested
    B->>P: submit clip (videoCid, lastFrameCid, duration)
    P->>C: onVideoRequestSettled(result)
    C->>C: store latest clip / failure
```

### The contract

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {IFlapAIProvider, IFlapVideoReceiver} from "src/plugins/AIProvider/IFlapAIProvider.sol";

/// @dev Reference-type surface of FlapAIProvider used by this consumer.
interface IFlapAIProviderReferenceTypes {
    function generateVideo(string calldata prompt, uint8 referenceType, string[] calldata referenceCids)
        external
        payable
        returns (uint256 requestId);
    function getLastVideoFrameCid(address user) external view returns (string memory);
}

contract ReferenceStoryVideoConsumer is IFlapVideoReceiver {
    uint8 public constant VIDEO_REF_PREV_LAST_FRAME = 1;
    uint8 public constant VIDEO_REF_CHARACTER = 4;

    string internal constant CHARACTER_HINT =
        "Reference image 1 is the main character: keep the exact same character design, colors, outfit and art style. ";
    string internal constant SCENE_HINT =
        "Reference image 2 is the final frame of the previous clip: continue from that scene, same setting, lighting and camera framing, as a natural continuation. ";

    IFlapAIProvider public immutable provider;
    string public CHARACTER_CID;

    uint256 public lastRequestId;
    string public latestVideoCid;
    string public latestFrameCid;
    uint32 public latestDurationMs;
    bool public lastRequestFailed;
    IFlapAIProvider.VideoFailureReason public lastFailureReason;
    string public lastFailureDetail;

    event VideoGenerationRequested(
        uint256 indexed requestId, address indexed caller, string prompt, uint8 referenceType, string[] referenceCids
    );
    event VideoGenerationFulfilled(uint256 indexed requestId, string videoCid, string lastFrameCid, uint32 durationMs);
    event VideoGenerationFailed(uint256 indexed requestId, IFlapAIProvider.VideoFailureReason reason, string failureDetail);

    error WrongFee(uint256 sent, uint256 required);
    error EmptyPrompt();
    error OnlyProvider();
    error RequestAlreadyPending(uint256 requestId);
    error ReferenceTypeNotAllowed(uint8 referenceType);
    error NoPreviousClip();
    error BadCharacterReferences();

    constructor(address provider_, string memory characterCid_) {
        require(provider_ != address(0), "zero provider");
        require(bytes(characterCid_).length != 0, "empty character cid");
        provider = IFlapAIProvider(provider_);
        CHARACTER_CID = characterCid_;
    }

    /// @param referenceType VIDEO_REF_PREV_LAST_FRAME (1) or VIDEO_REF_CHARACTER (4).
    /// @param referenceCids Type 1: empty. Type 4: [CHARACTER_CID] for the first clip,
    ///                      [CHARACTER_CID, current last frame] afterwards.
    function generateVideo(string calldata prompt, uint8 referenceType, string[] calldata referenceCids)
        external
        payable
        returns (uint256 requestId)
    {
        uint256 fee = provider.videoGenerateFee();
        if (msg.value != fee) revert WrongFee(msg.value, fee);
        if (bytes(prompt).length == 0) revert EmptyPrompt();
        if (lastRequestId != 0) {
            IFlapAIProvider.VideoRequestView memory prior = provider.getVideoRequest(lastRequestId);
            if (prior.status == IFlapAIProvider.VideoRequestStatus.PENDING) revert RequestAlreadyPending(lastRequestId);
        }

        IFlapAIProviderReferenceTypes p = IFlapAIProviderReferenceTypes(address(provider));
        // No race: the provider's one-pending rule means the last frame can't change
        // before this request is recorded.
        string memory lastFrame = p.getLastVideoFrameCid(address(this));
        bool hasLastFrame = bytes(lastFrame).length != 0;

        string memory fullPrompt;
        if (referenceType == VIDEO_REF_PREV_LAST_FRAME) {
            // Continuity: the provider resolves the previous last frame on-chain.
            if (!hasLastFrame) revert NoPreviousClip();
            if (referenceCids.length != 0) revert BadCharacterReferences();
            fullPrompt = prompt;
        } else if (referenceType == VIDEO_REF_CHARACTER) {
            // Character consistency: [character] or [character, previous last frame].
            uint256 expected = hasLastFrame ? 2 : 1;
            if (referenceCids.length != expected) revert BadCharacterReferences();
            if (keccak256(bytes(referenceCids[0])) != keccak256(bytes(CHARACTER_CID))) revert BadCharacterReferences();
            if (hasLastFrame && keccak256(bytes(referenceCids[1])) != keccak256(bytes(lastFrame))) {
                revert BadCharacterReferences();
            }
            fullPrompt = hasLastFrame
                ? string.concat(CHARACTER_HINT, SCENE_HINT, prompt)
                : string.concat(CHARACTER_HINT, prompt);
        } else {
            revert ReferenceTypeNotAllowed(referenceType);
        }

        requestId = p.generateVideo{value: msg.value}(fullPrompt, referenceType, referenceCids);
        lastRequestId = requestId;
        emit VideoGenerationRequested(requestId, msg.sender, prompt, referenceType, referenceCids);
    }

    function onVideoRequestSettled(VideoRequestResult calldata result) external override {
        if (msg.sender != address(provider)) revert OnlyProvider();
        if (result.status == IFlapAIProvider.VideoRequestStatus.FULFILLED) {
            latestVideoCid = result.videoCid;
            latestFrameCid = result.lastFrameCid;
            latestDurationMs = result.durationMs;
            lastRequestFailed = false;
            emit VideoGenerationFulfilled(result.requestId, result.videoCid, result.lastFrameCid, result.durationMs);
        } else if (result.status == IFlapAIProvider.VideoRequestStatus.FAILED) {
            lastRequestFailed = true;
            lastFailureReason = result.failureReason;
            lastFailureDetail = result.failureDetail;
            emit VideoGenerationFailed(result.requestId, result.failureReason, result.failureDetail);
        }
    }

    // ---- views ----

    function getLastFrame() external view returns (string memory) {
        return IFlapAIProviderReferenceTypes(address(provider)).getLastVideoFrameCid(address(this));
    }

    function getFullSession() external view returns (IFlapAIProvider.VideoClip[] memory) {
        uint256 len = provider.getVideoSessionLength(address(this));
        return provider.getVideoSessionSlice(address(this), 0, len);
    }
}
```

**Design notes:**

1. **The session belongs to the consumer contract**, because it is the `msg.sender` of the provider call. Many callers can extend one shared story.
2. **The character CID is fixed in the constructor.** Type-4 calls must pass it as reference image 1, so no caller can swap the character.
3. **The last frame is read from the provider** (`getLastVideoFrameCid(address(this))`), not from the consumer's own callback storage. The value is still correct if a callback was skipped or ran out of gas.
4. **The prompt prefix matches the CID order the contract enforces.** "Image 1 is the character, image 2 is the previous scene" is always true.
5. **The callback only records state.** It is best-effort and gas-capped, so heavy logic doesn't belong there.

### Example: an 8-clip story

This 8-clip story, *"The Ice Boy Who Melted Upward"*, was generated on the testnet consumer above. It alternates the two types as recommended in §3:

| # | Type | `referenceCids`          | Purpose                        | Prompt (abridged)                                                                 |
| - | ---- | ------------------------ | ------------------------------ | --------------------------------------------------------------------------------- |
| 1 | 4    | `[character]`            | Establish the character        | The ice boy wakes up alone inside a giant snow globe on a kitchen table…          |
| 2 | 1    | `[]`                     | Seamless continuation          | The glass softens like jelly; he pushes through and drops onto the table…         |
| 3 | 4    | `[character, lastFrame]` | New scene, re-anchor character | He follows floating ice cubes out of the window into a sky where it snows upward… |
| 4 | 1    | `[]`                     | Seamless continuation          | A whale made of folded newspaper swims past; he rides its fin higher…             |
| 5 | 4    | `[character, lastFrame]` | New scene, re-anchor character | The whale delivers him to a floating island where tiny suns sunbathe…             |
| 6 | 1    | `[]`                     | Seamless continuation          | He melts, but his water flows upward into a cloud shaped like his face…           |
| 7 | 4    | `[character, lastFrame]` | Re-anchor character            | The cloud rains back down and rebuilds him, covered in crystal flowers…           |
| 8 | 1    | `[]`                     | Seamless ending                | He shakes a new snow globe; the camera reveals the whole world is inside one…     |

Calling it with `cast`:

```bash
CONSUMER=0xB7f4398BEECE5Ec0da06497e647107D697c55c66
CHAR=bafkreigo6g3mkveu5w3l7ud56qr4oq3sa62hawcdmybdhbi5agurqwm5ye
RPC=https://bsc-testnet-rpc.publicnode.com
FEE=$(cast call 0xFfddcE44e8cFf7703Fd85118524bfC8B2f70b744 "videoGenerateFee()(uint256)" --rpc-url $RPC | awk '{print $1}')

# Type 4: re-anchor the character, previous last frame as scene reference
LAST=$(cast call $CONSUMER "getLastFrame()(string)" --rpc-url $RPC | tr -d '"')
cast send $CONSUMER "generateVideo(string,uint8,string[])" \
  "The ice boy follows a trail of floating ice cubes into a sky where it snows upward." \
  4 "[$CHAR,$LAST]" --value $FEE --rpc-url $RPC --private-key <KEY>

# Type 1: seamless continuation (wait for the previous request to settle first)
cast send $CONSUMER "generateVideo(string,uint8,string[])" \
  "A whale made of folded newspaper swims past; the ice boy grabs its fin and rides it higher." \
  1 "[]" --value $FEE --rpc-url $RPC --private-key <KEY>
```

***

## 5. Playing a Session

A session is an ordered list of `VideoClip`s. Every clip is a separate, immutable **MPEG-TS** file on IPFS (`videoCid`). Together they are designed to play as **one continuous video**: load the clip list from the chain, give it to an MPEG-TS player as consecutive segments, and the viewer sees one timeline across all clips.

### Why MPEG-TS

Every `VideoClip` carries `startPtsMs`, its start time on the session's timeline: `0` for the first clip, then the previous clip's `startPtsMs + durationMs` (the cumulative duration of all earlier clips). The backend encodes each clip with its timestamps **already shifted by that offset**, so clip *N*'s first frame is stamped exactly where clip *N-1* ended.

This only works because of how MPEG-TS is built:

* **No global header.** An MPEG-TS file is a flat sequence of self-describing packets with absolute timestamps. Two TS files placed back-to-back (as player segments, or even byte-concatenated) are still a valid stream.
* **Timestamps live in the stream.** Each clip can be encoded once with its final position on the session timeline. Neither the player nor any server has to rewrite anything.
* **Clean splices.** Clips are encoded with **no B-frames** (so presentation and decode timestamps are always equal) and **start with a keyframe**. Decode time never jumps backwards at a clip boundary, and every clip can be decoded on its own from its first packet.

What this means for you:

|                         | MPEG-TS (what the session uses)                  | Plain MP4 per clip                                                 |
| ----------------------- | ------------------------------------------------ | ------------------------------------------------------------------ |
| Append a new clip       | Upload one more file; earlier clips never change | Each file has its own header (`moov`) and a timeline starting at 0 |
| Play clips back-to-back | One continuous timeline, no reset between clips  | Player resets or seeks at every clip, or you re-mux everything     |
| Infrastructure          | Static files on IPFS + the clip list on-chain    | Needs re-encoding or a server-generated HLS/DASH manifest          |
| Content addressing      | Each clip CID is final the moment it's on-chain  | A merged file would get a new CID every time it grows              |

In short: the session is **append-only and content-addressed**, and MPEG-TS is the format where "append one more immutable file" and "play everything as one video" are compatible. The on-chain clip list **is** the playlist.

{% hint style="info" %}
`durationMs` and `startPtsMs` are in **milliseconds** on-chain. mpegts.js segment durations are in **seconds**, so always pass `durationMs / 1000`.
{% endhint %}

### Reading the session

From a contract:

```solidity
uint256 len = provider.getVideoSessionLength(consumer);
IFlapAIProvider.VideoClip[] memory clips = provider.getVideoSessionSlice(consumer, 0, len);
// clips[i].videoCid      → MPEG-TS segment
// clips[i].durationMs    → segment duration (ms)
// clips[i].startPtsMs    → segment start on the session timeline (ms)
// clips[i].lastFrameCid  → thumbnail / reference for the next clip
```

From a frontend, with [viem](https://viem.sh). `user` is the address that called `generateVideo` (usually your consumer contract):

```ts
import { createPublicClient, http, type Address } from "viem";
import { bsc } from "viem/chains"; // use `bscTestnet` for testnet

// FlapAIProvider: BNB Mainnet. Testnet: 0xFfddcE44e8cFf7703Fd85118524bfC8B2f70b744
const FLAP_AI_PROVIDER: Address = "0xaEe3a7Ca6fe6b53f6c32a3e8407eC5A9dF8B7E39";

const abi = [
  {
    type: "function",
    name: "getVideoSessionLength",
    stateMutability: "view",
    inputs: [{ name: "user", type: "address" }],
    outputs: [{ name: "", type: "uint256" }],
  },
  {
    type: "function",
    name: "getVideoSessionSlice",
    stateMutability: "view",
    inputs: [
      { name: "user", type: "address" },
      { name: "start", type: "uint256" },
      { name: "count", type: "uint256" },
    ],
    outputs: [
      {
        name: "clips",
        type: "tuple[]",
        components: [
          { name: "requestId", type: "uint256" },
          { name: "videoCid", type: "string" },
          { name: "lastFrameCid", type: "string" },
          { name: "durationMs", type: "uint32" },
          { name: "startPtsMs", type: "uint64" },
          { name: "createdAt", type: "uint64" },
          { name: "referenceType", type: "uint8" },
        ],
      },
    ],
  },
] as const;

const client = createPublicClient({ chain: bsc, transport: http() });

/** Reads every fulfilled clip of `user`'s session, in timeline order. */
export async function fetchSession(user: Address) {
  const length = await client.readContract({
    address: FLAP_AI_PROVIDER,
    abi,
    functionName: "getVideoSessionLength",
    args: [user],
  });
  if (length === 0n) return [];

  return client.readContract({
    address: FLAP_AI_PROVIDER,
    abi,
    functionName: "getVideoSessionSlice",
    args: [user, 0n, length],
  });
  // → readonly { requestId: bigint; videoCid: string; lastFrameCid: string;
  //     durationMs: number; startPtsMs: bigint; createdAt: bigint; referenceType: number }[]
}

export type VideoClip = Awaited<ReturnType<typeof fetchSession>>[number];
```

For long sessions, page through with `getVideoSessionSlice(user, start, count)`. It clamps `count` to the end of the session.

### Playing with mpegts.js

[mpegts.js](https://github.com/xqq/mpegts.js) is an open-source HTML5 MPEG-TS player. It transmuxes TS to fragmented MP4 in the browser and plays it through **Media Source Extensions (MSE)**. Give it **all clips as one multi-segment source**: it loads and stitches the segments itself, and the native `<video>` seek bar covers the whole session.

```bash
npm install mpegts.js
```

Segment URLs point at any IPFS gateway: `https://<your-gateway>/ipfs/<videoCid>`. The gateway must send CORS headers (`Access-Control-Allow-Origin`), because mpegts.js downloads the segments with `fetch`. A dedicated gateway is recommended for anything beyond testing.

#### Minimal example (plain HTML, no build step)

A single HTML file that reads a session from BSC Testnet and plays it:

```html
<!doctype html>
<html>
<body>
  <video id="video" controls playsinline style="width: 100%; max-width: 960px"></video>

  <!-- mpegts.js UMD build: exposes a global `mpegts` -->
  <script src="https://cdn.jsdelivr.net/npm/mpegts.js@1.8.2/dist/mpegts.js"></script>
  <script type="module">
    import { createPublicClient, http, parseAbi } from "https://esm.sh/viem@2";
    import { bscTestnet } from "https://esm.sh/viem@2/chains";

    const PROVIDER = "0xFfddcE44e8cFf7703Fd85118524bfC8B2f70b744"; // FlapAIProvider (BSC Testnet)
    const USER = "0x...";                     // the requester whose session you want to play
    const GATEWAY = "https://<your-gateway>"; // IPFS gateway with CORS enabled

    const abi = parseAbi([
      "function getVideoSessionLength(address user) view returns (uint256)",
      "function getVideoSessionSlice(address user, uint256 start, uint256 count) view returns ((uint256 requestId, string videoCid, string lastFrameCid, uint32 durationMs, uint64 startPtsMs, uint64 createdAt, uint8 referenceType)[])",
    ]);
    const client = createPublicClient({ chain: bscTestnet, transport: http() });

    const len = await client.readContract({
      address: PROVIDER, abi, functionName: "getVideoSessionLength", args: [USER],
    });
    const clips = await client.readContract({
      address: PROVIDER, abi, functionName: "getVideoSessionSlice", args: [USER, 0n, len],
    });

    const video = document.getElementById("video");
    if (clips.length > 0 && mpegts.isSupported()) {
      const player = mpegts.createPlayer({
        type: "mse",
        isLive: false,
        segments: clips.map((c) => ({
          url: `${GATEWAY}/ipfs/${c.videoCid}`,
          duration: c.durationMs / 1000, // seconds
        })),
      });
      player.attachMediaElement(video);
      player.load();
      player.play()?.catch(() => {}); // autoplay may need a user click first
    } else {
      // Fallback: no MSE (or empty session). Offer the individual clips instead.
      video.replaceWith(...clips.map((c, i) => {
        const a = document.createElement("a");
        a.href = `${GATEWAY}/ipfs/${c.videoCid}`;
        a.textContent = `Clip ${i + 1} (.ts) `;
        return a;
      }));
    }
  </script>
</body>
</html>
```

#### React / Next.js component

Sessions grow over time, so a real app re-reads the session (for example every few seconds) and appends new clips as they're fulfilled. mpegts.js **can't add a segment to a running player**, so the component below rebuilds the player whenever a new clip shows up. It then seeks back to the viewer's current position, so the rebuild doesn't look like a restart.

```tsx
"use client";

import { useEffect, useRef } from "react";
import type Mpegts from "mpegts.js";

type Player = ReturnType<typeof Mpegts.createPlayer>;

export interface Clip {
  requestId: bigint;
  videoCid: string;
  durationMs: number;
}

const GATEWAY = "https://<your-gateway>"; // IPFS gateway with CORS enabled

export function SessionPlayer({ clips }: { clips: readonly Clip[] }) {
  const videoRef = useRef<HTMLVideoElement>(null);
  const playerRef = useRef<Player | null>(null);
  const builtForRef = useRef<bigint | null>(null); // requestId of the newest clip loaded

  // (Re)build the player whenever a new clip is appended to the session.
  useEffect(() => {
    const video = videoRef.current;
    if (!video || clips.length === 0) return;

    const newest = clips[clips.length - 1].requestId;
    if (builtForRef.current === newest) return; // nothing new (e.g. a poll re-render)

    const isFirstBuild = builtForRef.current === null;
    const resumeAt = isFirstBuild ? 0 : video.currentTime;
    const shouldPlay = isFirstBuild || video.ended || !video.paused;
    let cancelled = false;

    (async () => {
      // Dynamic import: mpegts.js touches `window`, so never load it during SSR.
      const mpegts = (await import("mpegts.js")).default;
      if (cancelled || !mpegts.isSupported()) return; // no MSE: render a fallback instead

      // mpegts.js cannot append a segment to a running player, so replace it.
      playerRef.current?.destroy();
      const player = mpegts.createPlayer({
        type: "mse",
        isLive: false,
        segments: clips.map((c) => ({
          url: `${GATEWAY}/ipfs/${c.videoCid}`,
          duration: c.durationMs / 1000, // SECONDS, not milliseconds
        })),
      });
      playerRef.current = player;
      player.attachMediaElement(video);
      player.load();
      builtForRef.current = newest;

      // Put the viewer back where they were, so the rebuild is invisible.
      if (resumeAt > 0) video.currentTime = resumeAt;
      if (shouldPlay) {
        const p = player.play();
        if (p) p.catch(() => {}); // autoplay may be blocked until the user interacts
      }
    })();

    return () => {
      cancelled = true;
    };
  }, [clips]);

  // Tear down on unmount.
  useEffect(
    () => () => {
      playerRef.current?.destroy();
      playerRef.current = null;
    },
    []
  );

  return <video ref={videoRef} controls playsInline style={{ width: "100%" }} />;
}
```

Usage: keep the clips from `fetchSession(user)` in client state, re-fetch on an interval, and render `<SessionPlayer clips={clips} />`. Only replace the array when the session actually changed (compare length / last `requestId`), so that background polls don't trigger rebuilds.

**Jump to a clip.** Since `startPtsMs` is each clip's position on the timeline, seeking to clip `i` is `video.currentTime = Number(clips[i].startPtsMs) / 1000`. To highlight the clip that's playing, find the last clip whose `startPtsMs <= video.currentTime * 1000`.

Seeking inside a player built from every clip makes mpegts.js download all earlier clips first, which is slow for later clips. A faster option (used by the [demo app](https://video.munch.cash/)) is to rebuild the player from `clips.slice(i)`, so only clip `i` and later are downloaded. mpegts.js then starts its timeline at clip `i`, so add `clips[i].startPtsMs` back when mapping `video.currentTime` to the session timeline.

### Caching clips

Clips are content-addressed, so a `/ipfs/<cid>` URL never changes and can be cached forever. The browser HTTP cache alone is not reliable for them, especially on mobile: clips are 5–6 MB each and mobile disk caches evict them quickly. The demo app adds a small service worker ([`clip-cache-sw.js`](https://github.com/flap-sh/video-generator-demo-dapp/blob/main/public/clip-cache-sw.js)) that keeps every played clip in Cache Storage, keyed by URL, and answers later requests (including `Range` requests) from there. Browsers may still clear site storage under pressure, and iOS Safari clears it after about 7 days without a visit.

### Browser support and fallbacks

* mpegts.js needs **Media Source Extensions**. Always check `mpegts.isSupported()` before creating a player.
* Desktop Chrome, Edge, Firefox and Safari are supported. On **iPhone**, MSE isn't available before iOS 17.1. Newer iOS versions go through Apple's Managed Media Source, which mpegts.js supports, but test on your target devices.
* If MSE is unavailable, a simple fallback is to link or download the individual clips (`https://<your-gateway>/ipfs/<videoCid>`) or show the `lastFrameCid` images as a storyboard.
* **Downloading a whole session:** because the clips are MPEG-TS with continuous timestamps, concatenating their bytes in order gives one valid `.ts` file (for example `cat clip0.ts clip1.ts … > session.ts`). It plays in desktop players such as VLC and mpv, or can be converted to MP4 without re-encoding, e.g. `ffmpeg -i session.ts -c copy session.mp4`.

***

## 6. Interface Reference

### Requesting

```solidity
/// Text-to-video for the first clip, then automatically PREV_LAST_FRAME (legacy).
function generateVideo(string calldata prompt) external payable returns (uint256 requestId);

/// Explicit reference type + caller-supplied reference CIDs (see §3).
function generateVideo(string calldata prompt, uint8 referenceType, string[] calldata referenceCids)
    external payable returns (uint256 requestId);

function videoGenerateFee() external view returns (uint256);
function isVideoReferenceTypeEnabled(uint8 referenceType) external view returns (bool);
```

### Reading

```solidity
function getVideoRequest(uint256 requestId) external view returns (VideoRequestView memory);
function getVideoReferenceCids(uint256 requestId) external view returns (uint8 referenceType, string[] memory referenceCids);
function getVideoSessionLength(address user) external view returns (uint256);
function getVideoClip(address user, uint256 index) external view returns (VideoClip memory);
function getVideoSessionSlice(address user, uint256 start, uint256 count) external view returns (VideoClip[] memory);
function getLastVideoFrameCid(address user) external view returns (string memory);
function getLastVideoRequestId(address user) external view returns (uint256);
function videoCallbackGasLimit() external view returns (uint256);
```

### Data types

```solidity
enum VideoRequestStatus { NONE, PENDING, FULFILLED, FAILED }

enum VideoFailureReason { NONE, CONTENT_POLICY, PROVIDER_ERROR, PIPELINE_ERROR, TIMEOUT, OTHER }

struct VideoClip {
    uint256 requestId;
    string videoCid;      // MPEG-TS clip
    string lastFrameCid;  // JPEG of the clip's last frame
    uint32 durationMs;
    uint64 startPtsMs;    // start on the session's continuous timeline
    uint64 createdAt;
    uint8 referenceType;  // VIDEO_REF_* used for this clip
}

struct VideoRequestView {
    uint256 requestId;
    address requester;
    VideoRequestStatus status;
    uint8 referenceType;
    uint64 timestamp;
    uint128 feePaid;
    VideoFailureReason failureReason;
}
```

### Callback (optional)

```solidity
interface IFlapVideoReceiver {
    struct VideoRequestResult {
        uint256 requestId;
        IFlapAIProvider.VideoRequestStatus status; // FULFILLED or FAILED
        string videoCid;                            // FULFILLED only
        string lastFrameCid;                        // FULFILLED only
        uint32 durationMs;                          // FULFILLED only
        IFlapAIProvider.VideoFailureReason failureReason; // FAILED only
        string failureDetail;                       // FAILED only
    }

    function onVideoRequestSettled(VideoRequestResult calldata result) external;
}
```

The callback runs **after** the clip has been appended to the session. It gets at most `videoCallbackGasLimit()` gas. A revert, out-of-gas, or missing implementation is tolerated and recorded in `FlapAIProviderVideoCallbackAttempted(requestId, requester, success)`. EOA requesters are never called back.

`CONTENT_POLICY` means the same prompt would be refused again, so change it before retrying. `PROVIDER_ERROR` / `TIMEOUT` mean a fresh request may succeed. A `FAILED` request never adds a clip, so the next type-1 request still continues from the last **fulfilled** clip.

### Events

```solidity
event FlapAIProviderVideoRequested(
    uint256 indexed requestId, address indexed requester, string prompt,
    uint8 referenceType, string[] referenceCids, uint256 feePaid
);
event FlapAIProviderVideoFulfilled(
    uint256 indexed requestId, address indexed requester, string videoCid,
    string lastFrameCid, uint32 durationMs, uint64 startPtsMs, uint256 sessionIndex
);
event FlapAIProviderVideoFailed(
    uint256 indexed requestId, address indexed requester, VideoFailureReason reason, string failureDetail
);
event FlapAIProviderVideoCallbackAttempted(uint256 indexed requestId, address indexed requester, bool success);
```

In `FlapAIProviderVideoRequested`, `referenceCids` is already resolved: for type `1` it holds the previous clip's last-frame CID.

### Errors

| Error                                                                   | When                                        |
| ----------------------------------------------------------------------- | ------------------------------------------- |
| `FlapAIProviderWrongVideoFee(sent, required)`                           | `msg.value != videoGenerateFee()`           |
| `FlapAIProviderEmptyVideoPrompt()`                                      | Empty prompt                                |
| `FlapAIProviderVideoRequestAlreadyPending(requester, pendingRequestId)` | The requester already has a pending request |
| `FlapAIProviderUnsupportedReferenceType(referenceType)`                 | Type not enabled                            |
| `FlapAIProviderBadReferenceCidCount(referenceType, count)`              | Wrong number of CIDs for the type           |
| `FlapAIProviderInvalidReferenceCid(index)`                              | Empty CID or CID > 128 bytes                |
| `FlapAIProviderNoPreviousClip(requester)`                               | Type `1` with an empty session              |

***

## 7. Checklist

* [ ] Read `videoGenerateFee()` at call time and send exactly that amount
* [ ] Handle the one-pending-request rule (check `getVideoRequest(lastRequestId).status` before submitting)
* [ ] Start every session with type `0`, `2` or `4`. Type `1` needs a previous clip
* [ ] Pin your character image to IPFS and keep its CID fixed in your contract
* [ ] Use type `4` `[character, lastFrame]` to keep the character consistent, and type `1` for seamless joins. Alternate them
* [ ] In type-4 prompts, say which reference image is which, and restate the character's key traits
* [ ] Read the last frame from `getLastVideoFrameCid(address(this))`, not from callback-only storage
* [ ] Keep `onVideoRequestSettled` light and check `msg.sender == provider`


# Flap Candy Box

## Overview

Flap Candy Box is an on-chain data oracle that delivers paginated, addressable lists to consumer contracts via a request-response callback pattern. It serves as an infrastructure layer for programmable, transparent, and automated reward distribution — vaults and other smart contracts can access dynamic CandyBox lists on-chain and act on them automatically, without maintaining the lists themselves.

Each subscription ID represents a list — it could be a static snapshot, a dynamically refreshed dataset, or any curated set of addresses. The first subscription is provided by Flap and refreshed every 24 hours, featuring the most active users from the previous day's trading activity on Flap. More subscriptions will be added over time.

> Want to build your own custom list? [Reach out to us](https://flap.sh) — we'd love to explore it with you.

**Deployed addresses:**

| Network     | Address                                      | Max Callback Gas |
| ----------- | -------------------------------------------- | ---------------- |
| BSC Mainnet | `0x6255fbd731272a517022e99f6CaCf6A5De9414Ee` | 2,000,000        |
| BSC Testnet | `0x8e6C16Bf07022a7Da9398B543f38846E9355Bd70` | 2,000,000        |

**Max Callback Gas:** The maximum gas limit supported for an `onDataReceived` callback.

### How it works

```mermaid
sequenceDiagram
    participant C as Consumer Contract
    participant O as FlapCandyBox
    participant B as Backend (off-chain)

    C->>O: requestData{value: fee}(subscriptionId, timestamp, offset, limit, "")
    O-->>B: emit FlapDataRequested(subscriptionId, requestId, ...)
    Note over B: resolves the list for this subscriptionId
    B->>O: fulfillData(requestId, encodedData)
    O->>C: onDataReceived(subscriptionId, requestId, data) callback
    O-->>B: emit FlapDataFulfilled(subscriptionId, requestId, success, ...)
```

**Step by step:**

1. The consumer contract calls `requestData()`, paying the subscription fee and specifying the page (`offset`, `limit`) to fetch.
2. The oracle records the request and emits `FlapDataRequested`.
3. The backend queries the previous day's trade data and encodes it as a `ResponseData` struct.
4. The backend calls `fulfillData(requestId, encodedData)` on the oracle.
5. The oracle forwards the data to the consumer via `onDataReceived()` with a bounded gas limit.
6. The oracle emits `FlapDataFulfilled` with the callback outcome.

### Request status lifecycle

```
PENDING → FULFILLED   (callback succeeded)
        → FAILED      (callback reverted — eligible for retry by operator)
```

If the consumer's `onDataReceived()` reverts, the request status is set to `FAILED`. The oracle operator can retry delivery via `retryFulfillData()` once the consumer issue is resolved.

***

## Subscriptions

Each subscription defines a dataset, its pricing, and the maximum page size.

```solidity
IFlapCandyBox.Subscription[] memory subs = IFlapCandyBox(oracleAddress).getSubscriptions();
```

| Field            | Type      | Description                                           |
| ---------------- | --------- | ----------------------------------------------------- |
| `subscriptionId` | `bytes32` | Pass to `requestData()`                               |
| `description`    | `string`  | Human-readable rule                                   |
| `responseStruct` | `string`  | Struct name for decoding `data` in `onDataReceived()` |
| `fee`            | `uint256` | Required `msg.value` per request (wei)                |
| `maxLimit`       | `uint32`  | Maximum records per page                              |

> **Current limit cap:** `maxLimit` is **145** per request. This accounts for consumer contracts that store the returned records on-chain — larger payloads may exceed the callback gas budget. Use pagination for more records.

### Available subscriptions

| Constant             | `subscriptionId`                  | Description                                                                                                                           | `responseStruct` |
| -------------------- | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------- | ---------------- |
| `FLAP_TRADE_BNB_24H` | `keccak256("FLAP_TRADE_BNB_24H")` | Addresses based on their trading activity in the previous 24 hours — across both Bonding Curve and DEX — on tokens launched from Flap | `ResponseData`   |

Fees may change. Always call `getFee(subscriptionId)` on-chain immediately before sending `requestData()`.

***

## How to integrate

### Step 1 — Query the fee

```solidity
uint256 fee = IFlapCandyBox(oracleAddress).getFee(subscriptionId);
```

### Step 2 — Call `requestData()`

```solidity
bytes32 requestId = IFlapCandyBox(oracleAddress).requestData{value: fee}(
    subscriptionId,
    uint64(block.timestamp), // backend derives previous day's window from this
    offset,                  // zero-based page offset
    limit,                   // records per page, must be <= maxLimit
    ""                       // extraParams: leave empty
);
```

> Pass `block.timestamp` directly — do not manually compute `startTime`/`endTime`. The backend derives the previous UTC day window from the request timestamp.

**Pagination example** — fetching 3000 addresses in pages of 145:

```solidity
// Page 0: offset=0,   limit=145
// Page 1: offset=145, limit=145
// Page 2: offset=290, limit=145
// ...
// Use ResponseData.totalSize from the first callback to calculate how many pages are needed.
```

### Step 3 — Implement the consumer

The recommended approach is to inherit `FlapCandyBoxConsumerBase`, which provides the `onlyFlapCandyBox` modifier and hardcoded address resolution automatically.

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import {IFlapCandyBox, FlapCandyBoxConsumerBase} from "src/interfaces/IFlapCandyBox.sol";

contract MyVault is FlapCandyBoxConsumerBase {

    bytes32 public constant SUBSCRIPTION_BNB = keccak256("FLAP_TRADE_BNB_24H");

    mapping(bytes32 => bool) private _fulfilled;

    function requestStats(uint32 offset, uint32 limit) external payable returns (bytes32 requestId) {
        IFlapCandyBox oracle = IFlapCandyBox(_getFlapCandyBox());
        uint256 fee = oracle.getFee(SUBSCRIPTION_BNB);
        requestId = oracle.requestData{value: fee}(
            SUBSCRIPTION_BNB,
            uint64(block.timestamp),
            offset,
            limit,
            ""
        );
    }

    // Called by FlapCandyBox — protected by onlyFlapCandyBox modifier from base contract
    function _onDataReceived(
        bytes32 subscriptionId,
        bytes32 requestId,
        bytes calldata data
    ) internal override {
        require(!_fulfilled[requestId], "already fulfilled");
        _fulfilled[requestId] = true; // mark before any external calls

        if (subscriptionId == SUBSCRIPTION_BNB) {
            IFlapCandyBox.ResponseData memory resp = abi.decode(data, (IFlapCandyBox.ResponseData));
            // resp.totalSize   — total qualifying addresses across all pages
            // resp.totalAmount — sum of tradeAmount across ALL matching records (not just this page)
            // resp.records     — address-level records for this page
            // resp.startTime / resp.endTime — actual data window
            _handleDistribution(resp);
        }
    }

    function _handleDistribution(IFlapCandyBox.ResponseData memory resp) internal {
        for (uint256 i = 0; i < resp.records.length; i++) {
            // distribute proportionally using resp.records[i].wallet and resp.records[i].tradeAmount
        }
    }
}
```

Alternatively, implement `IFlapCandyBoxConsumer` directly and validate `msg.sender` yourself:

```solidity
function onDataReceived(bytes32 subscriptionId, bytes32 requestId, bytes calldata data) external override {
    require(msg.sender == address(oracle), "Only FlapCandyBox");
    // ...
}
```

***

## ResponseData struct

The `data` payload in `onDataReceived()` is ABI-encoded as `ResponseData`. Use `getSubscription(subscriptionId).responseStruct` to confirm the struct name for each subscription.

```solidity
struct AddressRecord {
    address wallet;
    uint256 tradeAmount; // 1e18-scaled; BNB subscription → wei, USD subscription → 1e18-scaled USD
}

struct ResponseData {
    uint256 totalSize;     // total records matching the query across all pages
    uint256 offset;        // zero-based index of the first record in this page
    uint256 returnedCount; // actual number of records in this page (may be < limit on last page)
    bool isDesc;           // true — records are sorted by tradeAmount descending
    uint64 startTime;      // query window start (Unix timestamp, UTC 00:00:00)
    uint64 endTime;        // query window end   (Unix timestamp, UTC 23:59:59)
    uint256 totalAmount;   // sum of tradeAmount across ALL matching records (not just this page)
    AddressRecord[] records;
}
```

> Use `totalSize` (not `returnedCount`) for proportional distribution calculations across pages.

***

## Security checklist

| Item                  | Detail                                                                                        |
| --------------------- | --------------------------------------------------------------------------------------------- |
| Validate `msg.sender` | Use `FlapCandyBoxConsumerBase` (recommended) or `require(msg.sender == address(oracle))`      |
| Guard double-delivery | Track a `fulfilled` flag per `requestId` and revert on repeat                                 |
| Reentrancy            | Mark fulfilled **before** any external calls or token transfers                               |
| Callback gas          | Keep `onDataReceived()` within `getMaxCallbackGas()`; check with `oracle.getMaxCallbackGas()` |
| Fee freshness         | Call `getFee()` immediately before sending `requestData()` — fees may change                  |
| Page size             | Use `getMaxLimit(subscriptionId)` to check the current cap before submitting                  |

***

## Reference

### Full interface

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

interface IFlapCandyBox {

    // ── Enums ────────────────────────────────────────────────────────────────

    enum RequestStatus {
        PENDING,   // 0 — created, waiting for fulfiller
        FULFILLED, // 1 — data delivered and consumer callback succeeded
        FAILED     // 2 — data delivered but consumer callback reverted; eligible for retry
    }

    // ── Structs ──────────────────────────────────────────────────────────────

    struct Subscription {
        bytes32 subscriptionId;
        string description;
        string responseStruct; // name of the ResponseData struct, e.g. "ResponseData"
        uint256 fee;           // required msg.value per requestData() call (wei)
        uint32 maxLimit;       // maximum allowed limit per request
        bool active;
    }

    struct OracleRequest {
        address consumer;
        bytes32 subscriptionId;
        uint64 timestamp;  // caller's block.timestamp; backend derives previous day's window
        uint32 offset;
        uint32 limit;
        bytes extraParams;
        uint128 feePaid;
        RequestStatus status;
    }

    struct AddressRecord {
        address wallet;
        uint256 tradeAmount;
    }

    struct ResponseData {
        uint256 totalSize;     // total records matching the query across all pages
        uint256 offset;        // zero-based index of the first record in this page
        uint256 returnedCount; // actual records in this page
        bool isDesc;           // true if sorted by tradeAmount descending
        uint64 startTime;      // query window start (Unix timestamp)
        uint64 endTime;        // query window end   (Unix timestamp)
        uint256 totalAmount;   // sum of tradeAmount across ALL matching records (not just this page)
        AddressRecord[] records;
    }

    // ── Events ───────────────────────────────────────────────────────────────

    event FlapSubscriptionRegistered(bytes32 indexed subscriptionId, string description, uint256 fee);
    event FlapSubscriptionFeeUpdated(bytes32 indexed subscriptionId, uint256 oldFee, uint256 newFee);
    event FlapSubscriptionLimitUpdated(bytes32 indexed subscriptionId, uint32 oldLimit, uint32 newLimit);
    event FlapDataRequested(
        bytes32 indexed subscriptionId,
        bytes32 indexed requestId,
        address indexed consumer,
        uint64 timestamp,
        uint32 offset,
        uint32 limit,
        uint256 feePaid,
        bytes extraParams
    );
    event FlapDataFulfilled(bytes32 indexed subscriptionId, bytes32 indexed requestId, bool success, bytes returnData);
    event FlapFulfillerUpdated(address indexed fulfiller, bool authorized);

    // ── Errors ───────────────────────────────────────────────────────────────

    error InvalidSubscription(bytes32 subscriptionId);
    error SubscriptionAlreadyExists(bytes32 subscriptionId);
    error InsufficientFee(uint256 required, uint256 provided);
    error LimitExceeded(uint32 requested, uint32 max);
    error AlreadyFulfilled(bytes32 requestId);
    error InvalidRequestId(bytes32 requestId);
    error OnlyFulfiller();
    error RequestNotFailed(bytes32 requestId, RequestStatus status);
    error RetryFailed(bytes32 requestId, bytes returnData);

    // ── Write ────────────────────────────────────────────────────────────────

    /// @notice Submit a data request. Emits FlapDataRequested for the backend to pick up.
    /// @param subscriptionId  Identifies which dataset and pricing rule to query.
    /// @param timestamp       Pass block.timestamp. Backend derives previous day's window from this.
    /// @param offset          Zero-based page offset (first page = 0).
    /// @param limit           Records per page. Must be <= getMaxLimit(subscriptionId).
    /// @param extraParams     Reserved. Pass "".
    /// @return requestId      Store this to correlate the callback.
    function requestData(
        bytes32 subscriptionId,
        uint64 timestamp,
        uint32 offset,
        uint32 limit,
        bytes calldata extraParams
    ) external payable returns (bytes32 requestId);

    // ── View ─────────────────────────────────────────────────────────────────

    function getSubscription(bytes32 subscriptionId) external view returns (Subscription memory);
    function getSubscriptions() external view returns (Subscription[] memory);
    function getFee(bytes32 subscriptionId) external view returns (uint256);
    function getMaxLimit(bytes32 subscriptionId) external view returns (uint32);
    function getRequest(bytes32 requestId) external view returns (OracleRequest memory);
    function getMaxCallbackGas() external view returns (uint256);
}

/// @notice Base contract for FlapCandyBox consumers.
/// @dev Inherit this to get address resolution and the onlyFlapCandyBox modifier for free.
abstract contract FlapCandyBoxConsumerBase {
    error FlapCandyBoxConsumerOnlyBox();
    error FlapCandyBoxConsumerUnsupportedChain(uint256 chainId);

    modifier onlyFlapCandyBox() {
        if (msg.sender != _getFlapCandyBox()) revert FlapCandyBoxConsumerOnlyBox();
        _;
    }

    /// @notice Returns the FlapCandyBox proxy address for the current chain.
    function _getFlapCandyBox() internal view virtual returns (address) {
        uint256 id = block.chainid;
        if (id == 56)  return 0x6255fbd731272a517022e99f6CaCf6A5De9414Ee; // BSC Mainnet
        if (id == 97)  return 0x8e6C16Bf07022a7Da9398B543f38846E9355Bd70; // BSC Testnet
        revert FlapCandyBoxConsumerUnsupportedChain(id);
    }

    function onDataReceived(bytes32 subscriptionId, bytes32 requestId, bytes calldata data)
        external virtual onlyFlapCandyBox
    {
        _onDataReceived(subscriptionId, requestId, data);
    }

    function _onDataReceived(bytes32 subscriptionId, bytes32 requestId, bytes calldata data) internal virtual;
}

/// @notice Minimal interface for consumers that prefer not to use the base contract.
interface IFlapCandyBoxConsumer {
    function onDataReceived(bytes32 subscriptionId, bytes32 requestId, bytes calldata data) external;
}
```


# Flap Trigger Service

## Overview

FlapTriggerService is a decentralized on-chain scheduler that allows smart contracts to request delayed or immediate function callbacks executed by a trusted backend. It bridges on-chain logic with off-chain coordination, enabling complex time-sensitive operations without requiring the caller to manage execution timing directly.

**Deployed addresses:**

| Network                 | Address                                      | Max Gas Limit |
| ----------------------- | -------------------------------------------- | ------------- |
| BNB Chain Mainnet       | `0xcf4EE25035CF883895110f367F5BA8172416a7F9` | 2,000,000     |
| BNB Chain Testnet       | `0x560E9830926C9e0EB98a59c6b9902383Fc0D9Eb2` | 2,000,000     |
| Robinhood Chain Mainnet | `0xD3421B1b616a72bB88993A0cf75709BB8D532cc1` | 2,000,000     |
| Robinhood Chain Testnet | `0x34e7624e2c8F944Db1adD9a604fdB7C439CaEa1c` | 2,000,000     |

**Max Gas Limit:** The maximum gas limit supported for a `trigger` callback.

**Primary use cases:**

* Time-delayed operations (vesting unlocks, periodic distributions, deferred settlements)
* Backend-coordinated operations requiring MEV protection
* Operations that need external computation before on-chain execution

### How it works

```mermaid
sequenceDiagram
    participant R as Requester Contract
    participant S as FlapTriggerService
    participant B as Backend (off-chain)

    R->>S: requestTrigger{value: fee}(executeAfter)
    S-->>B: emit FlapTriggerRequested(requestId, requester, executeAfter, fee)
    Note over B: indexes event, waits until executeAfter
    B->>S: trigger(requestId) via MEV-protected RPC
    S->>R: trigger(requestId) [callback, bounded gas]
    S-->>B: emit FlapTriggerExecuted(requestId, success, data)
```

**Step by step:**

1. The requester contract calls `requestTrigger()`, paying the required gas fee and specifying an `executeAfter` timestamp (or `0` for immediate execution).
2. The service records the request and emits a `FlapTriggerRequested` event.
3. The off-chain backend monitors for these events and indexes all pending requests.
4. When the scheduled time arrives, the backend submits a `trigger(requestId)` transaction via an MEV-protected RPC (e.g., Flashbots, BloXroute).
5. FlapTriggerService calls back `requester.trigger(requestId)` with a bounded gas limit.
6. The requester's callback uses the `requestId` to identify and execute the intended operation.

### Timing guarantees

{% hint style="warning" %}
`executeAfter` is a lower bound, not a hard deadline. The service only guarantees that execution happens **after** `executeAfter`.
{% endhint %}

Integrators **must** assume there can be an unpredictable delay due to:

* Network congestion
* Backend processing latency
* Block inclusion delays
* MEV protection overhead

Requester contracts **must** be designed to handle late execution gracefully and **must not** rely on execution at a precise time.

***

## Trigger pricing

A fixed native-currency fee is charged per trigger request, set independently per network:

| Network                 | Current Fee |
| ----------------------- | ----------- |
| BNB Chain Mainnet       | 0.0002 BNB  |
| BNB Chain Testnet       | 0.0002 BNB  |
| Robinhood Chain Mainnet | 0.0004 ETH  |
| Robinhood Chain Testnet | 0.0004 ETH  |

This fee covers:

* Gas costs for the backend's `trigger()` transaction
* Gas costs for the callback to the requester contract
* A small service fee

{% hint style="warning" %}
**Robinhood Chain fee is temporarily elevated.** Due to gas-price fluctuation on Robinhood Chain, the current fee is set higher than strictly necessary while we evaluate a more precise pricing approach. This fee is expected to change.

**Always call `getFee()` at call time — never hardcode the fee value.** This applies to every network, including BNB Chain. BNB Chain's fee is fixed today, but Robinhood Chain's fee is expected to become **dynamic** in the future: when Ethereum L1 (which Robinhood Chain settles to) is congested and gas is expensive, the fee may be increased; when the network is quiet, it may be decreased. A contract that hardcodes the fee instead of calling `getFee()` on every `requestTrigger()` call will eventually either underpay (causing `InsufficientGasFee` reverts) or overpay unnecessarily once dynamic pricing is in effect.
{% endhint %}

**Getting the current fee:**

```solidity
uint256 fee = IFlapTriggerService(triggerService).getFee();
```

The fee is paid upfront when calling `requestTrigger()` as `msg.value`. Excess payment above the required fee is accumulated as protocol fees — there is no refund for overpayment.

**Callback gas limit:**

Each trigger callback is forwarded at most `getMaxCallbackGas()` gas. Requester contracts must ensure their `trigger()` callback completes within this limit. Exceeding the limit will cause the callback to fail (status becomes `FAILED`).

```solidity
uint256 maxGas = IFlapTriggerService(triggerService).getMaxCallbackGas();
```

**Failed callbacks and retry:**

If a callback fails (e.g., out of gas or revert), the request status is set to `FAILED`. Anyone can retry a failed request by calling `retryTrigger(requestId)` — this forwards all available gas to the callback, allowing the caller to supply sufficient gas. The fee stored in the request is transferred to the fee receiver on successful retry.

***

## How to integrate

### Step 1 — Import the interfaces

```solidity
import { IFlapTriggerService } from "src/misc/IFlapTriggerService.sol";
import { ITriggerReceiver } from "src/misc/IFlapTriggerService.sol";
```

### Step 2 — Implement `ITriggerReceiver`

Your contract must implement the `trigger(uint256 requestId)` callback. This function is called by FlapTriggerService when the scheduled time has passed.

**Security requirements for the callback:**

* **Must** validate `msg.sender == address(triggerService)`
* **Should** implement a reentrancy guard if performing external calls or state changes
* **Must** complete within `getMaxCallbackGas()` gas
* **Must not** assume execution happens exactly at `executeAfter` — always assume potential delay

### Step 3 — Schedule a trigger

Call `requestTrigger()` with the desired `executeAfter` timestamp. Store the returned `requestId` to map it back to the intended operation in your callback.

### Pseudo-code example

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import { IFlapTriggerService, ITriggerReceiver } from "src/misc/IFlapTriggerService.sol";
import { ReentrancyGuard } from "@openzeppelin/utils/ReentrancyGuard.sol";

contract MyScheduledVault is ITriggerReceiver, ReentrancyGuard {

    IFlapTriggerService public immutable triggerService;

    struct PendingAction {
        address recipient;
        uint256 amount;
    }

    mapping(uint256 => PendingAction) private pendingActions;

    constructor(address _triggerService) {
        triggerService = IFlapTriggerService(_triggerService);
    }

    /// @notice Schedule a delayed distribution 1 day from now.
    function scheduleDistribution(address recipient, uint256 amount) external payable {
        // 1. Get the required fee
        uint256 fee = triggerService.getFee();

        // 2. Request the trigger (executeAfter = 1 day from now)
        uint256 requestId = triggerService.requestTrigger{value: fee}(
            uint64(block.timestamp + 1 days)
        );

        // 3. Store the action data keyed by requestId
        pendingActions[requestId] = PendingAction({
            recipient: recipient,
            amount: amount
        });
    }

    /// @notice Called back by FlapTriggerService after executeAfter has passed.
    function trigger(uint256 requestId) external nonReentrant override {
        // SECURITY: only the trigger service can call this
        require(msg.sender == address(triggerService), "Only trigger service");

        PendingAction memory action = pendingActions[requestId];
        require(action.recipient != address(0), "Unknown requestId");

        // Clean up before external call (checks-effects-interactions)
        delete pendingActions[requestId];

        // Execute the intended operation
        // NOTE: Execution may happen well after the scheduled time — design accordingly
        _distribute(action.recipient, action.amount);
    }

    function _distribute(address recipient, uint256 amount) internal {
        // ... distribution logic ...
    }
}
```

### Scheduling for immediate execution

Pass `0` as `executeAfter` to request execution as soon as the backend processes the event:

```solidity
uint256 requestId = triggerService.requestTrigger{value: fee}(0);
```

### Querying request status

```solidity
// Get details about a specific request
IFlapTriggerService.TriggerRequest memory req = triggerService.getRequest(requestId);
// req.status: PENDING (0), EXECUTED (1), FAILED (2)
// req.executeAfter: scheduled time
// req.feePaid: fee paid (wei)

// Check if a request is ready to execute right now
bool ready = triggerService.isRequestReady(requestId);

// Get all requests by your contract (paginated, newest first)
(IFlapTriggerService.TriggerRequest[] memory page, uint256 total) =
    triggerService.getRequestsByRequesterPaginated(address(this), 0, 20);
```

### Retrying a failed trigger

If a callback fails (e.g., the callback used more gas than the limit), anyone can retry:

```solidity
triggerService.retryTrigger(requestId);
```

***

## Reference

### Full interface

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

interface IFlapTriggerService {

    // ── Enums ────────────────────────────────────────────────────────────────

    enum TriggerStatus {
        PENDING,   // 0 — Request created, waiting to be executed
        EXECUTED,  // 1 — Successfully executed by the backend
        FAILED     // 2 — Execution attempted but the callback reverted
    }

    // ── Structs ──────────────────────────────────────────────────────────────

    /// @notice Packed into two 32-byte storage slots.
    /// Slot 0: status (8 bits) | executeAfter (64 bits) | requester (160 bits) | 24 bits reserved
    /// Slot 1: feePaid (128 bits)
    struct TriggerRequest {
        address requester;    // 160 bits — address that requested the trigger
        uint64 executeAfter;  // 64 bits  — Unix timestamp lower bound for execution
        TriggerStatus status; // 8 bits   — current lifecycle status
        uint128 feePaid;      // 128 bits — native fee paid in wei (slot 1)
    }

    // ── Events ───────────────────────────────────────────────────────────────

    /// @notice Emitted when a new trigger request is created.
    event FlapTriggerRequested(
        uint256 requestId,
        address indexed requester,
        uint64 executeAfter,
        uint256 gasFeesPaid
    );

    /// @notice Emitted when a trigger execution is attempted (success or failure).
    event FlapTriggerExecuted(uint256 requestId, bool success, bytes data);

    /// @notice Emitted when a trigger is skipped (invalid ID, wrong status, or not yet due).
    event FlapTriggerSkipped(uint256 requestId, string reason);

    /// @notice Emitted when the required gas fee is updated by admin.
    event FlapTriggerGasFeeUpdated(uint256 oldFee, uint256 newFee);

    /// @notice Emitted when the maximum callback gas limit is updated by admin.
    event FlapTriggerMaxCallbackGasUpdated(uint256 oldLimit, uint256 newLimit);

    // ── Errors ───────────────────────────────────────────────────────────────

    /// @notice msg.value is below the required fee.
    error InsufficientGasFee(uint256 required, uint256 provided);

    /// @notice Request is not in PENDING status.
    error InvalidRequestStatus(uint256 requestId, TriggerStatus currentStatus);

    /// @notice block.timestamp is before executeAfter.
    error TooEarly(uint256 requestId, uint64 executeAfter, uint256 currentTime);

    /// @notice The provided requestId does not exist.
    error InvalidRequestId(uint256 requestId);

    /// @notice Caller does not have TRIGGER_ROLE.
    error OnlyTriggerRole();

    /// @notice Admin tried to set an invalid gas fee (e.g., zero).
    error InvalidGasFee();

    /// @notice Admin tried to set an invalid gas limit (e.g., zero or too high).
    error InvalidGasLimit();

    /// @notice Fee receiver address is invalid (e.g., zero address).
    error InvalidFeeReceiver();

    /// @notice A retried trigger callback reverted.
    error RetryFailed(uint256 requestId, bytes data);

    /// @notice msg.value overflows uint128 and cannot be stored as feePaid.
    error FeePaidOverflow(uint256 provided);

    // ── Write methods ────────────────────────────────────────────────────────

    /// @notice Request a trigger callback at or after a specified time.
    /// @param executeAfter Unix timestamp after which execution may happen. Pass 0 for immediate.
    /// @return requestId   Unique ID for this trigger request.
    /// Requirements: msg.value >= getFee(); if executeAfter > 0, must be >= block.timestamp.
    function requestTrigger(uint64 executeAfter) external payable returns (uint256 requestId);

    /// @notice Execute a single pending trigger (backend only, requires TRIGGER_ROLE).
    /// @param requestId  The ID of the request to execute.
    function trigger(uint256 requestId) external;

    /// @notice Execute multiple pending triggers in one transaction (backend only, requires TRIGGER_ROLE).
    /// @param requestIds  Array of request IDs to execute. Invalid or already-done IDs are skipped.
    function triggerMultiple(uint256[] calldata requestIds) external;

    /// @notice Retry a previously failed trigger request. Callable by anyone.
    ///         Forwards all available gas; reverts with RetryFailed on failure.
    /// @param requestId  The ID of the FAILED request to retry.
    function retryTrigger(uint256 requestId) external;

    // ── View methods ─────────────────────────────────────────────────────────

    /// @notice The native-currency fee (wei) required as msg.value for requestTrigger().
    function getFee() external view returns (uint256 gasFee);

    /// @notice Maximum gas forwarded to a requester's callback. Callbacks must fit within this.
    function getMaxCallbackGas() external view returns (uint256 maxGas);

    /// @notice Get details about a single trigger request. Reverts for unknown IDs.
    function getRequest(uint256 requestId) external view returns (TriggerRequest memory request);

    /// @notice Total number of trigger requests ever created (= next request ID).
    function getRequestCount() external view returns (uint256 count);

    /// @notice True if the request exists, is PENDING, and block.timestamp >= executeAfter.
    function isRequestReady(uint256 requestId) external view returns (bool ready);

    /// @notice Fetch multiple requests by ID in one call. Unknown IDs return zero-initialised structs.
    function getRequests(uint256[] calldata requestIds) external view returns (TriggerRequest[] memory requests);

    /// @notice Paginated list of all requests, newest first (descending by ID).
    /// @param offset  Requests to skip from the newest.
    /// @param limit   Maximum number to return.
    function getRequestsPaginated(uint256 offset, uint256 limit)
        external
        view
        returns (TriggerRequest[] memory requests, uint256 total);

    /// @notice Paginated list of requests for a specific requester address, newest first.
    /// @param requester  Address whose requests to query.
    /// @param offset     Requests to skip from the newest.
    /// @param limit      Maximum number to return.
    function getRequestsByRequesterPaginated(address requester, uint256 offset, uint256 limit)
        external
        view
        returns (TriggerRequest[] memory requests, uint256 total);
}

/// @notice Interface that requester contracts must implement to receive trigger callbacks.
interface ITriggerReceiver {
    /// @notice Called by FlapTriggerService when a scheduled trigger executes.
    /// @dev MUST validate msg.sender == triggerService address.
    ///      SHOULD implement reentrancy guard.
    ///      MUST complete within getMaxCallbackGas().
    ///      MUST NOT assume execution at exactly executeAfter — delays are possible.
    /// @param requestId  ID of the trigger request being executed.
    function trigger(uint256 requestId) external;
}
```


# World Cup Resolver

{% hint style="warning" %}
This is an older solution and can still be used. For new integrations, prefer [worldcup-viewer.md](/flap/developers/preview/worldcup-viewer).
{% endhint %}

## Overview

`WorldCupResolver` is a read-only smart contract deployed on BSC mainnet that reads **2026 FIFA World Cup** match outcomes from the on-chain oracle system and exposes the results to downstream consumers.

**Deployed address (BSC mainnet):** `0x134C6b9562E226096947e018ddEe4804c9146921`

The contract covers 43 entries: the 32 tournament teams, a selection of additional national teams with odds, and an "Other" catch-all. Each team maps to a unique oracle question: *"Will \[Team] win the 2026 FIFA World Cup?"* The underlying oracle resolves each question as YES or NO on-chain.

{% hint style="info" %}
No result state is stored inside `WorldCupResolver`. Every call to `resolveWinner()` reads the oracle live, so the returned snapshot always reflects the latest state.
{% endhint %}

***

## How It Works

### Oracle questions and team resolution

For every team, there is exactly one oracle question: *"Will this team win the World Cup?"*

| Oracle field       | Meaning                                                       |
| ------------------ | ------------------------------------------------------------- |
| `reportedAt > 0`   | The oracle has published a result for this team               |
| `results == true`  | The result is **YES** — this team won                         |
| `results == false` | The result is **NO** — this team did **not** win (eliminated) |
| `flaggedAt > 0`    | The result is currently under dispute; treat as unresolved    |

A team is confirmed as the **final winner** only when all three conditions hold simultaneously:

1. `reportedAt > 0` — oracle has reported.
2. `results == true` — result is YES.
3. `flaggedAt == 0` — result is **not** flagged/disputed.

### Resolving the final winner

Call `resolveWinner()`. It scans all 43 entries and returns a `WinnerResult` struct:

* `isResolved = true` → a confirmed winning team was found; read `winnerIndex` and `winnerName`.
* `isResolved = false` → no confirmed winner yet; inspect `statuses[]` for per-team progress.

### Resolving a single match (team elimination)

{% hint style="warning" %}
**Limitation — teams grouped under "Other" (index 42):**

`WorldCupResolver` does not run its own oracle. It reads data from an external UMA-based oracle, and the resolution data provided by that oracle only covers 42 individually named teams. All remaining qualified national teams that were not given their own oracle question are grouped into a single `Other` entry (index 42).

As a result, if one of those unlisted teams is eliminated in a match, their elimination **cannot** be detected individually — the `Other` question stays unresolved until one of those teams wins the entire World Cup (at which point `Other` resolves YES). Per-team match resolution is only supported for the 42 named teams.
{% endhint %}

Because every oracle question is about the **overall World Cup winner**, an answer of NO for a team means that team has been **definitively eliminated** — they can never win the tournament.

This makes it possible to infer match outcomes incrementally, well before the final is played:

**Example — Group stage match: Team A vs Team B**

1. Call `resolveWinner()` and read `statuses[]`.
2. Check Team A's status: `isReported = true`, `result = false`, `isFlagged = false` → **Team A is eliminated** (they lost and are out of the tournament).
3. Check Team B's status: `isReported = false` → **Team B is still in contention** (their question has not yet been resolved).

In this scenario Team A's result is fully settled — it can be used to pay out prediction positions — while Team B remains pending until a later match resolves their fate.

{% hint style="warning" %}
Only treat `isReported = true` **and** `isFlagged = false` as a settled result. A flagged result is under oracle dispute and must not be acted upon.
{% endhint %}

***

## Interface

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

/// @notice Per-team oracle status, returned by read-only query methods.
struct TeamStatus {
    uint8 index;       // 0-based team index (same order as getSupportedTeams())
    string name;       // human-readable team name
    bool isReported;   // true when reportedAt > 0 on the oracle
    bool result;       // true = YES (winner), false = NO or not yet reported
    bool isFlagged;    // true when flaggedAt > 0 — result is disputed
}

/// @notice Return value of resolveWinner().
struct WinnerResult {
    bool isResolved;       // true if a definitive YES answer was found
    uint8 winnerIndex;     // set only when isResolved = true
    string winnerName;     // set only when isResolved = true
    TeamStatus[] statuses; // full per-team snapshot at time of call
}

interface IWorldCupResolver {
    /// @notice Returns the full list of 43 supported team names in index order.
    ///         names[0] = "Spain", names[42] = "Other".
    ///         Safe to call at any time, including while the contract is paused.
    function getSupportedTeams() external view returns (string[] memory names);

    /// @notice Reads the oracle live and computes the World Cup winner.
    ///
    ///         - isResolved = true  → winnerIndex / winnerName identify the champion.
    ///         - isResolved = false → no winner yet; inspect statuses[] for per-team progress.
    ///
    ///         A team counts as winner only when:
    ///           1. reportedAt > 0  (oracle has reported)
    ///           2. results == true (oracle result is YES)
    ///           3. flaggedAt == 0  (result is not disputed)
    function resolveWinner() external view returns (WinnerResult memory result);
}
```

***

## Supported Teams

`getSupportedTeams()` returns 43 entries. The index is stable and matches the `TeamStatus.index` field returned by `resolveWinner()`.

| Index | Team        | Index | Team         |
| ----- | ----------- | ----- | ------------ |
| 0     | Spain       | 22    | Canada       |
| 1     | France      | 23    | Scotland     |
| 2     | England     | 24    | South Korea  |
| 3     | Argentina   | 25    | Paraguay     |
| 4     | Brazil      | 26    | Ivory Coast  |
| 5     | Portugal    | 27    | Egypt        |
| 6     | Germany     | 28    | Iran         |
| 7     | Netherlands | 29    | Ghana        |
| 8     | Norway      | 30    | Algeria      |
| 9     | Italy       | 31    | Tunisia      |
| 10    | Belgium     | 32    | Austria      |
| 11    | USA         | 33    | New Zealand  |
| 12    | Morocco     | 34    | Haiti        |
| 13    | Colombia    | 35    | Jordan       |
| 14    | Japan       | 36    | Curacao      |
| 15    | Uruguay     | 37    | Uzbekistan   |
| 16    | Croatia     | 38    | South Africa |
| 17    | Mexico      | 39    | Cape Verde   |
| 18    | Switzerland | 40    | Qatar        |
| 19    | Ecuador     | 41    | Saudi Arabia |
| 20    | Senegal     | 42    | Other        |
| 21    | Australia   |       |              |

***

## Integration Examples

### Check for the final winner (Solidity)

```solidity
IWorldCupResolver resolver = IWorldCupResolver(0x134C6b9562E226096947e018ddEe4804c9146921);

WinnerResult memory r = resolver.resolveWinner();
if (r.isResolved) {
    // r.winnerName  → e.g. "Brazil"
    // r.winnerIndex → e.g. 4
}
```

### Check whether a specific team has been eliminated (Solidity)

```solidity
WinnerResult memory r = resolver.resolveWinner();

// Example: check if Spain (index 0) is eliminated
TeamStatus memory spain = r.statuses[0];
if (spain.isReported && !spain.result && !spain.isFlagged) {
    // Spain has been definitively eliminated — safe to settle Spain positions
}
```

### Read all statuses off-chain (ethers.js)

```js
const resolver = new ethers.Contract(
    "0x134C6b9562E226096947e018ddEe4804c9146921",
    IWorldCupResolverABI,
    provider
);

const result = await resolver.resolveWinner();

console.log("Is resolved:", result.isResolved);
if (result.isResolved) {
    console.log("Winner:", result.winnerName, "(index", result.winnerIndex, ")");
}

for (const status of result.statuses) {
    if (status.isReported && !status.isFlagged) {
        const outcome = status.result ? "WINNER" : "ELIMINATED";
        console.log(`${status.name}: ${outcome}`);
    } else {
        console.log(`${status.name}: pending`);
    }
}
```

***

## Deployment Details

| Field            | Value                                        |
| ---------------- | -------------------------------------------- |
| Network          | BSC Mainnet                                  |
| Contract address | `0x134C6b9562E226096947e018ddEe4804c9146921` |


# World Cup Viewer

## Overview

`WorldCupViewer` is a read-only smart contract deployed on BSC mainnet that provides a clean, high-level view of **2026 FIFA World Cup** match results.

Users and integrators can query `WorldCupViewer` to find out the winner of the entire tournament or the winner of any specific group, without needing to understand the underlying oracle structure.

{% hint style="info" %}
`WorldCupViewer` is a read-only contract. All state — including match results and team names — is either stored in the contract or read live from the oracle. No transactions are required to query match results.
{% endhint %}

***

## Deployed address

| Network     | Address                                      |
| ----------- | -------------------------------------------- |
| BSC mainnet | `0x00036192958C2aaAF9F445d3Cdc2979995EA333e` |

***

## Methods

All query methods return a `MatchViewResult` struct:

```solidity
struct MatchViewResult {
    uint256 matchId;   // the match ID queried
    string matchName;  // human-readable name of the match
    bool isResolved;   // true if the oracle has settled this match
    uint256 teamId;    // ID of the winning team (0 if not yet resolved; 50 = draw/tie; 49 = others)
    string teamName;   // name of the winning team ("" if not yet resolved; "draw" for ties; "others" for unlisted teams)
}
```

{% hint style="info" %}
`teamId` uses two reserved values: `49` (`others`) for any team not individually tracked by the oracle, and `50` (`draw`) when a match ends in a tie.
{% endhint %}

### `getWorldCupWinner()`

```solidity
function getWorldCupWinner() external view returns (MatchViewResult memory);
```

Returns the winner of the 2026 FIFA World Cup (match ID `1`).

* If `isResolved = false`, the tournament winner has not yet been determined.
* If `isResolved = true`, `teamId` and `teamName` identify the champion.

{% hint style="info" %}
If the champion is a team not individually tracked by the oracle (i.e., grouped under "Others"), the contract resolves the winner using the separately configured `othersWinner` field, which is set by the contract owner.
{% endhint %}

***

### `getGroupMatchWinners(uint256 matchId)`

```solidity
function getGroupMatchWinners(uint256 matchId) external view returns (MatchViewResult memory);
```

Returns the winner of a group stage match. Pass the match ID for the group you want to query (e.g., `2` for Group A, `3` for Group B, and so on up to `13` for Group L).

* If `isResolved = false`, the group winner has not yet been determined.
* If `isResolved = true`, `teamId` and `teamName` identify the group winner.

***

### `getMatchResult(uint256 matchId)`

```solidity
function getMatchResult(uint256 matchId) external view returns (MatchViewResult memory);
```

Returns the resolved result for any match ID directly from the underlying oracle mapping.

* If `isResolved = false`, the match is not settled yet and the result remains pending.
* If `isResolved = true`, `teamId` identifies the winning outcome mapped from oracle request IDs.
* For a draw outcome (`teamId = 50`), `teamName` is returned as a tie message in the format `TeamA(teamIdA) and TeamB(teamIdB) are tied`.
* For non-draw outcomes, `teamName` is the mapped team name for that `teamId`.

***

### `getTeamName(uint256 teamId)`

```solidity
function getTeamName(uint256 teamId) external view returns (string memory);
```

Looks up the name for a given `teamId`. The standard query range is `1`-`48` (listed teams). If `teamId = 49`, it represents `"others"`; if `teamId = 50`, it represents `"draw"`. Unknown IDs return an empty string.

***

## Reference data

### Team ID mapping

| teamId | Team                       |
| ------ | -------------------------- |
| 1      | Mexico                     |
| 2      | South Africa               |
| 3      | South Korea                |
| 4      | Czechia                    |
| 5      | Canada                     |
| 6      | Bosnia and Herzegovina     |
| 7      | Qatar                      |
| 8      | Switzerland                |
| 9      | Brazil                     |
| 10     | Morocco                    |
| 11     | Haiti                      |
| 12     | Scotland                   |
| 13     | USA                        |
| 14     | Paraguay                   |
| 15     | Australia                  |
| 16     | Türkiye                    |
| 17     | Germany                    |
| 18     | Curaçao                    |
| 19     | Ivory Coast                |
| 20     | Ecuador                    |
| 21     | Netherlands                |
| 22     | Japan                      |
| 23     | Sweden                     |
| 24     | Tunisia                    |
| 25     | Belgium                    |
| 26     | Egypt                      |
| 27     | Iran                       |
| 28     | New Zealand                |
| 29     | Spain                      |
| 30     | Cape Verde                 |
| 31     | Saudi Arabia               |
| 32     | Uruguay                    |
| 33     | France                     |
| 34     | Senegal                    |
| 35     | Iraq                       |
| 36     | Norway                     |
| 37     | Argentina                  |
| 38     | Algeria                    |
| 39     | Austria                    |
| 40     | Jordan                     |
| 41     | Portugal                   |
| 42     | DR Congo                   |
| 43     | Uzbekistan                 |
| 44     | Colombia                   |
| 45     | England                    |
| 46     | Croatia                    |
| 47     | Ghana                      |
| 48     | Panama                     |
| 49     | Others (any unlisted team) |
| 50     | Draw                       |

***

### Match ID reference

{% hint style="info" %}
More matches will be added to this document as the tournament progresses.
{% endhint %}

#### Chapter 1: winner (Match 1)

#### Match 1 — 2026 FIFA World Cup Winner

{% hint style="warning" %}
Do **not** use `getMatchResult(1)` to determine the 2026 FIFA World Cup champion.\
Instead, always use `getWorldCupWinner()`, which fully resolves the tournament winner even if the oracle data merges several teams under "Others".\
Using `getMatchResult(1)` returns the raw oracle grouping and may not identify the actual team; `getWorldCupWinner()` always provides the true winner team ID and name.
{% endhint %}

Resolves to the team that wins the entire 2026 FIFA World Cup. This match tracks all individually listed teams plus an "Others" catch-all for any team not given their own oracle question.

| teamId | Team         | Oracle question title                                      |
| ------ | ------------ | ---------------------------------------------------------- |
| 29     | Spain        | Will Spain win the 2026 FIFA World Cup?                    |
| 33     | France       | Will France win the 2026 FIFA World Cup?                   |
| 45     | England      | Will England win the 2026 FIFA World Cup?                  |
| 37     | Argentina    | Will Argentina win the 2026 FIFA World Cup?                |
| 9      | Brazil       | Will Brazil win the 2026 FIFA World Cup?                   |
| 41     | Portugal     | Will Portugal win the 2026 FIFA World Cup?                 |
| 17     | Germany      | Will Germany win the 2026 FIFA World Cup?                  |
| 21     | Netherlands  | Will Netherlands win the 2026 FIFA World Cup?              |
| 36     | Norway       | Will Norway win the 2026 FIFA World Cup?                   |
| 25     | Belgium      | Will Belgium win the 2026 FIFA World Cup?                  |
| 13     | USA          | Will USA win the 2026 FIFA World Cup?                      |
| 10     | Morocco      | Will Morocco win the 2026 FIFA World Cup?                  |
| 44     | Colombia     | Will Colombia win the 2026 FIFA World Cup?                 |
| 22     | Japan        | Will Japan win the 2026 FIFA World Cup?                    |
| 32     | Uruguay      | Will Uruguay win the 2026 FIFA World Cup?                  |
| 46     | Croatia      | Will Croatia win the 2026 FIFA World Cup?                  |
| 1      | Mexico       | Will Mexico win the 2026 FIFA World Cup?                   |
| 8      | Switzerland  | Will Switzerland win the 2026 FIFA World Cup?              |
| 20     | Ecuador      | Will Ecuador win the 2026 FIFA World Cup?                  |
| 34     | Senegal      | Will Senegal win the 2026 FIFA World Cup?                  |
| 15     | Australia    | Will Australia win the 2026 FIFA World Cup?                |
| 5      | Canada       | Will Canada win the 2026 FIFA World Cup?                   |
| 12     | Scotland     | Will Scotland win the 2026 FIFA World Cup?                 |
| 3      | South Korea  | Will South Korea win the 2026 FIFA World Cup?              |
| 14     | Paraguay     | Will Paraguay win the 2026 FIFA World Cup?                 |
| 19     | Ivory Coast  | Will Ivory Coast win the 2026 FIFA World Cup?              |
| 26     | Egypt        | Will Egypt win the 2026 FIFA World Cup?                    |
| 27     | Iran         | Will Iran win the 2026 FIFA World Cup?                     |
| 47     | Ghana        | Will Ghana win the 2026 FIFA World Cup?                    |
| 38     | Algeria      | Will Algeria win the 2026 FIFA World Cup?                  |
| 24     | Tunisia      | Will Tunisia win the 2026 FIFA World Cup?                  |
| 39     | Austria      | Will Austria win the 2026 FIFA World Cup?                  |
| 28     | New Zealand  | Will New Zealand win the 2026 FIFA World Cup?              |
| 11     | Haiti        | Will Haiti win the 2026 FIFA World Cup?                    |
| 40     | Jordan       | Will Jordan win the 2026 FIFA World Cup?                   |
| 18     | Curaçao      | Will Curaçao win the 2026 FIFA World Cup?                  |
| 43     | Uzbekistan   | Will Uzbekistan win the 2026 FIFA World Cup?               |
| 2      | South Africa | Will South Africa win the 2026 FIFA World Cup?             |
| 30     | Cape Verde   | Will Cape Verde win the 2026 FIFA World Cup?               |
| 7      | Qatar        | Will Qatar win the 2026 FIFA World Cup?                    |
| 31     | Saudi Arabia | Will Saudi Arabia win the 2026 FIFA World Cup?             |
| —      | Others       | Will none of the listed teams win the 2026 FIFA World Cup? |

***

{% hint style="info" %}
For all group winner matches (Match 2–Match 13), you can use either `getMatchResult(matchId)` or `getGroupMatchWinners(matchId)`. Both will return the same result.
{% endhint %}

#### Chapter 2: group match winners (Match 2-13)

#### Match 2 — Group A Winner

Resolves to the team that wins Group A in the 2026 FIFA World Cup group stage.

| teamId | Team         | Oracle question title                                                                                                      |
| ------ | ------------ | -------------------------------------------------------------------------------------------------------------------------- |
| 1      | Mexico       | Will Mexico win Group A in the 2026 FIFA World Cup?                                                                        |
| 2      | South Africa | Will South Africa win Group A in the 2026 FIFA World Cup?                                                                  |
| 3      | South Korea  | Will South Korea win Group A in the 2026 FIFA World Cup?                                                                   |
| 4      | Czechia      | Will the winner of the Czechia/Denmark/North Macedonia/Republic of Ireland playoff win Group A in the 2026 FIFA World Cup? |

***

#### Match 3 — Group B Winner

Resolves to the team that wins Group B in the 2026 FIFA World Cup group stage.

| teamId | Team                   | Oracle question title                                                                                                 |
| ------ | ---------------------- | --------------------------------------------------------------------------------------------------------------------- |
| 5      | Canada                 | Will Canada win Group B in the 2026 World Cup?                                                                        |
| 6      | Bosnia and Herzegovina | Will the winner of the Bosnia and Herzegovina/Italy/Northern Ireland/Wales playoff win Group B in the 2026 World Cup? |
| 7      | Qatar                  | Will Qatar win Group B in the 2026 World Cup?                                                                         |
| 8      | Switzerland            | Will Switzerland win Group B in the 2026 World Cup?                                                                   |

***

#### Match 4 — Group C Winner

Resolves to the team that wins Group C in the 2026 FIFA World Cup group stage.

| teamId | Team     | Oracle question title                            |
| ------ | -------- | ------------------------------------------------ |
| 9      | Brazil   | Will Brazil win Group C in the 2026 World Cup?   |
| 10     | Morocco  | Will Morocco win Group C in the 2026 World Cup?  |
| 11     | Haiti    | Will Haiti win Group C in the 2026 World Cup?    |
| 12     | Scotland | Will Scotland win Group C in the 2026 World Cup? |

***

#### Match 5 — Group D Winner

Resolves to the team that wins Group D in the 2026 FIFA World Cup group stage.

| teamId | Team      | Oracle question title                                                                             |
| ------ | --------- | ------------------------------------------------------------------------------------------------- |
| 13     | USA       | Will USA win Group D in the 2026 World Cup?                                                       |
| 14     | Paraguay  | Will Paraguay win Group D in the 2026 World Cup?                                                  |
| 15     | Australia | Will Australia win Group D in the 2026 World Cup?                                                 |
| 16     | Türkiye   | Will the winner of the Kosovo/Romania/Slovakia/Türkiye playoff win Group D in the 2026 World Cup? |

***

#### Match 6 — Group E Winner

Resolves to the team that wins Group E in the 2026 FIFA World Cup group stage.

| teamId | Team        | Oracle question title                               |
| ------ | ----------- | --------------------------------------------------- |
| 17     | Germany     | Will Germany win Group E in the 2026 World Cup?     |
| 18     | Curaçao     | Will Curaçao win Group E in the 2026 World Cup?     |
| 19     | Ivory Coast | Will Ivory Coast win Group E in the 2026 World Cup? |
| 20     | Ecuador     | Will Ecuador win Group E in the 2026 World Cup?     |

***

#### Match 7 — Group F Winner

Resolves to the team that wins Group F in the 2026 FIFA World Cup group stage.

| teamId | Team        | Oracle question title                                                                           |
| ------ | ----------- | ----------------------------------------------------------------------------------------------- |
| 21     | Netherlands | Will Netherlands win Group F in the 2026 World Cup?                                             |
| 22     | Japan       | Will Japan win Group F in the 2026 World Cup?                                                   |
| 23     | Sweden      | Will the winner of the Albania/Poland/Sweden/Ukraine playoff win Group F in the 2026 World Cup? |
| 24     | Tunisia     | Will Tunisia win Group F in the 2026 World Cup?                                                 |

***

#### Match 8 — Group G Winner

Resolves to the team that wins Group G in the 2026 FIFA World Cup group stage.

| teamId | Team        | Oracle question title                               |
| ------ | ----------- | --------------------------------------------------- |
| 25     | Belgium     | Will Belgium win Group G in the 2026 World Cup?     |
| 26     | Egypt       | Will Egypt win Group G in the 2026 World Cup?       |
| 27     | Iran        | Will Iran win Group G in the 2026 World Cup?        |
| 28     | New Zealand | Will New Zealand win Group G in the 2026 World Cup? |

***

#### Match 9 — Group H Winner

Resolves to the team that wins Group H in the 2026 FIFA World Cup group stage.

| teamId | Team         | Oracle question title                                |
| ------ | ------------ | ---------------------------------------------------- |
| 29     | Spain        | Will Spain win Group H in the 2026 World Cup?        |
| 30     | Cape Verde   | Will Cape Verde win Group H in the 2026 World Cup?   |
| 31     | Saudi Arabia | Will Saudi Arabia win Group H in the 2026 World Cup? |
| 32     | Uruguay      | Will Uruguay win Group H in the 2026 World Cup?      |

***

#### Match 10 — Group I Winner

Resolves to the team that wins Group I in the 2026 FIFA World Cup group stage.

| teamId | Team    | Oracle question title                                                                   |
| ------ | ------- | --------------------------------------------------------------------------------------- |
| 33     | France  | Will France win Group I in the 2026 World Cup?                                          |
| 34     | Senegal | Will Senegal win Group I in the 2026 World Cup?                                         |
| 35     | Iraq    | Will the winner of the Bolivia/Iraq/Suriname playoff win Group I in the 2026 World Cup? |
| 36     | Norway  | Will Norway win Group I in the 2026 World Cup?                                          |

***

#### Match 11 — Group J Winner

Resolves to the team that wins Group J in the 2026 FIFA World Cup group stage.

| teamId | Team      | Oracle question title                             |
| ------ | --------- | ------------------------------------------------- |
| 37     | Argentina | Will Argentina win Group J in the 2026 World Cup? |
| 38     | Algeria   | Will Algeria win Group J in the 2026 World Cup?   |
| 39     | Austria   | Will Austria win Group J in the 2026 World Cup?   |
| 40     | Jordan    | Will Jordan win Group J in the 2026 World Cup?    |

***

#### Match 12 — Group K Winner

Resolves to the team that wins Group K in the 2026 FIFA World Cup group stage.

| teamId | Team       | Oracle question title                                                                                                |
| ------ | ---------- | -------------------------------------------------------------------------------------------------------------------- |
| 41     | Portugal   | Will Portugal win Group K in the 2026 World Cup?                                                                     |
| 42     | DR Congo   | Will the winner of the Democratic Republic of Congo/Jamaica/New Caledonia playoff win Group K in the 2026 World Cup? |
| 43     | Uzbekistan | Will Uzbekistan win Group K in the 2026 World Cup?                                                                   |
| 44     | Colombia   | Will Colombia win Group K in the 2026 World Cup?                                                                     |

***

#### Match 13 — Group L Winner

Resolves to the team that wins Group L in the 2026 FIFA World Cup group stage.

| teamId | Team    | Oracle question title                           |
| ------ | ------- | ----------------------------------------------- |
| 45     | England | Will England win Group L in the 2026 World Cup? |
| 46     | Croatia | Will Croatia win Group L in the 2026 World Cup? |
| 47     | Ghana   | Will Ghana win Group L in the 2026 World Cup?   |
| 48     | Panama  | Will Panama win Group L in the 2026 World Cup?  |

***

#### Chapter 3: group matches (Match 14-85)

#### Match 14 — Mexico vs. South Africa

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team         | Oracle question title                                                                         |
| ------ | ------------ | --------------------------------------------------------------------------------------------- |
| 1      | Mexico       | Will Mexico win on 2026-06-11 in Group A of the 2026 FIFA World Cup group stage?              |
| 50     | draw         | Will Mexico vs. South Africa end in a draw in Group A of the 2026 FIFA World Cup group stage? |
| 2      | South Africa | Will South Africa win on 2026-06-11 in Group A of the 2026 FIFA World Cup group stage?        |

***

#### Match 15 — South Korea vs. Czechia

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team        | Oracle question title                                                                            |
| ------ | ----------- | ------------------------------------------------------------------------------------------------ |
| 3      | South Korea | Will Korea Republic win on 2026-06-11 in Group A of the 2026 FIFA World Cup group stage?         |
| 50     | draw        | Will Korea Republic vs. Czechia end in a draw in Group A of the 2026 FIFA World Cup group stage? |
| 4      | Czechia     | Will Czechia win on 2026-06-11 in Group A of the 2026 FIFA World Cup group stage?                |

***

#### Match 16 — Canada vs. Bosnia and Herzegovina

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team                   | Oracle question title                                                                                   |
| ------ | ---------------------- | ------------------------------------------------------------------------------------------------------- |
| 5      | Canada                 | Will Canada win on 2026-06-12 in Group B of the 2026 FIFA World Cup group stage?                        |
| 50     | draw                   | Will Canada vs. Bosnia and Herzegovina end in a draw in Group B of the 2026 FIFA World Cup group stage? |
| 6      | Bosnia and Herzegovina | Will Bosnia and Herzegovina win on 2026-06-12 in Group B of the 2026 FIFA World Cup group stage?        |

***

#### Match 17 — USA vs. Paraguay

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team     | Oracle question title                                                                            |
| ------ | -------- | ------------------------------------------------------------------------------------------------ |
| 13     | USA      | Will United States win on 2026-06-12 in Group D of the 2026 FIFA World Cup group stage?          |
| 50     | draw     | Will United States vs. Paraguay end in a draw in Group D of the 2026 FIFA World Cup group stage? |
| 14     | Paraguay | Will Paraguay win on 2026-06-12 in Group D of the 2026 FIFA World Cup group stage?               |

***

#### Match 18 — Qatar vs. Switzerland

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team        | Oracle question title                                                                       |
| ------ | ----------- | ------------------------------------------------------------------------------------------- |
| 7      | Qatar       | Will Qatar win on 2026-06-13 in Group B of the 2026 FIFA World Cup group stage?             |
| 50     | draw        | Will Qatar vs. Switzerland end in a draw in Group B of the 2026 FIFA World Cup group stage? |
| 8      | Switzerland | Will Switzerland win on 2026-06-13 in Group B of the 2026 FIFA World Cup group stage?       |

***

#### Match 19 — Brazil vs. Morocco

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team    | Oracle question title                                                                    |
| ------ | ------- | ---------------------------------------------------------------------------------------- |
| 9      | Brazil  | Will Brazil win on 2026-06-13 in Group C of the 2026 FIFA World Cup group stage?         |
| 50     | draw    | Will Brazil vs. Morocco end in a draw in Group C of the 2026 FIFA World Cup group stage? |
| 10     | Morocco | Will Morocco win on 2026-06-13 in Group C of the 2026 FIFA World Cup group stage?        |

***

#### Match 20 — Haiti vs. Scotland

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team     | Oracle question title                                                                    |
| ------ | -------- | ---------------------------------------------------------------------------------------- |
| 11     | Haiti    | Will Haiti win on 2026-06-13 in Group C of the 2026 FIFA World Cup group stage?          |
| 50     | draw     | Will Haiti vs. Scotland end in a draw in Group C of the 2026 FIFA World Cup group stage? |
| 12     | Scotland | Will Scotland win on 2026-06-13 in Group C of the 2026 FIFA World Cup group stage?       |

***

#### Match 21 — Australia vs. Türkiye

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team      | Oracle question title                                                                       |
| ------ | --------- | ------------------------------------------------------------------------------------------- |
| 15     | Australia | Will Australia win on 2026-06-14 in Group D of the 2026 FIFA World Cup group stage?         |
| 50     | draw      | Will Australia vs. Türkiye end in a draw in Group D of the 2026 FIFA World Cup group stage? |
| 16     | Türkiye   | Will Türkiye win on 2026-06-14 in Group D of the 2026 FIFA World Cup group stage?           |

***

#### Match 22 — Germany vs. Curaçao

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team    | Oracle question title                                                                     |
| ------ | ------- | ----------------------------------------------------------------------------------------- |
| 17     | Germany | Will Germany win on 2026-06-14 in Group E of the 2026 FIFA World Cup group stage?         |
| 50     | draw    | Will Germany vs. Curaçao end in a draw in Group E of the 2026 FIFA World Cup group stage? |
| 18     | Curaçao | Will Curaçao win on 2026-06-14 in Group E of the 2026 FIFA World Cup group stage?         |

***

#### Match 23 — Netherlands vs. Japan

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team        | Oracle question title                                                                       |
| ------ | ----------- | ------------------------------------------------------------------------------------------- |
| 21     | Netherlands | Will Netherlands win on 2026-06-14 in Group F of the 2026 FIFA World Cup group stage?       |
| 50     | draw        | Will Netherlands vs. Japan end in a draw in Group F of the 2026 FIFA World Cup group stage? |
| 22     | Japan       | Will Japan win on 2026-06-14 in Group F of the 2026 FIFA World Cup group stage?             |

***

#### Match 24 — Ivory Coast vs. Ecuador

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team        | Oracle question title                                                                           |
| ------ | ----------- | ----------------------------------------------------------------------------------------------- |
| 19     | Ivory Coast | Will Côte d'Ivoire win on 2026-06-14 in Group E of the 2026 FIFA World Cup group stage?         |
| 50     | draw        | Will Côte d'Ivoire vs. Ecuador end in a draw in Group E of the 2026 FIFA World Cup group stage? |
| 20     | Ecuador     | Will Ecuador win on 2026-06-14 in Group E of the 2026 FIFA World Cup group stage?               |

***

#### Match 25 — Sweden vs. Tunisia

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team    | Oracle question title                                                                    |
| ------ | ------- | ---------------------------------------------------------------------------------------- |
| 23     | Sweden  | Will Sweden win on 2026-06-14 in Group F of the 2026 FIFA World Cup group stage?         |
| 50     | draw    | Will Sweden vs. Tunisia end in a draw in Group F of the 2026 FIFA World Cup group stage? |
| 24     | Tunisia | Will Tunisia win on 2026-06-14 in Group F of the 2026 FIFA World Cup group stage?        |

***

#### Match 26 — Spain vs. Cape Verde

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team       | Oracle question title                                                                      |
| ------ | ---------- | ------------------------------------------------------------------------------------------ |
| 29     | Spain      | Will Spain win on 2026-06-15 in Group H of the 2026 FIFA World Cup group stage?            |
| 50     | draw       | Will Spain vs. Cabo Verde end in a draw in Group H of the 2026 FIFA World Cup group stage? |
| 30     | Cape Verde | Will Cabo Verde win on 2026-06-15 in Group H of the 2026 FIFA World Cup group stage?       |

***

#### Match 27 — Belgium vs. Egypt

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team    | Oracle question title                                                                   |
| ------ | ------- | --------------------------------------------------------------------------------------- |
| 25     | Belgium | Will Belgium win on 2026-06-15 in Group G of the 2026 FIFA World Cup group stage?       |
| 50     | draw    | Will Belgium vs. Egypt end in a draw in Group G of the 2026 FIFA World Cup group stage? |
| 26     | Egypt   | Will Egypt win on 2026-06-15 in Group G of the 2026 FIFA World Cup group stage?         |

***

#### Match 28 — Saudi Arabia vs. Uruguay

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team         | Oracle question title                                                                          |
| ------ | ------------ | ---------------------------------------------------------------------------------------------- |
| 31     | Saudi Arabia | Will Saudi Arabia win on 2026-06-15 in Group H of the 2026 FIFA World Cup group stage?         |
| 50     | draw         | Will Saudi Arabia vs. Uruguay end in a draw in Group H of the 2026 FIFA World Cup group stage? |
| 32     | Uruguay      | Will Uruguay win on 2026-06-15 in Group H of the 2026 FIFA World Cup group stage?              |

***

#### Match 29 — Iran vs. New Zealand

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team        | Oracle question title                                                                         |
| ------ | ----------- | --------------------------------------------------------------------------------------------- |
| 27     | Iran        | Will IR Iran win on 2026-06-15 in Group G of the 2026 FIFA World Cup group stage?             |
| 50     | draw        | Will IR Iran vs. New Zealand end in a draw in Group G of the 2026 FIFA World Cup group stage? |
| 28     | New Zealand | Will New Zealand win on 2026-06-15 in Group G of the 2026 FIFA World Cup group stage?         |

***

#### Match 30 — France vs. Senegal

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team    | Oracle question title                                                                    |
| ------ | ------- | ---------------------------------------------------------------------------------------- |
| 33     | France  | Will France win on 2026-06-16 in Group I of the 2026 FIFA World Cup group stage?         |
| 50     | draw    | Will France vs. Senegal end in a draw in Group I of the 2026 FIFA World Cup group stage? |
| 34     | Senegal | Will Senegal win on 2026-06-16 in Group I of the 2026 FIFA World Cup group stage?        |

***

#### Match 31 — Iraq vs. Norway

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team   | Oracle question title                                                                 |
| ------ | ------ | ------------------------------------------------------------------------------------- |
| 35     | Iraq   | Will Iraq win on 2026-06-16 in Group I of the 2026 FIFA World Cup group stage?        |
| 50     | draw   | Will Iraq vs. Norway end in a draw in Group I of the 2026 FIFA World Cup group stage? |
| 36     | Norway | Will Norway win on 2026-06-16 in Group I of the 2026 FIFA World Cup group stage?      |

***

#### Match 32 — Argentina vs. Algeria

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team      | Oracle question title                                                                       |
| ------ | --------- | ------------------------------------------------------------------------------------------- |
| 37     | Argentina | Will Argentina win on 2026-06-16 in Group J of the 2026 FIFA World Cup group stage?         |
| 50     | draw      | Will Argentina vs. Algeria end in a draw in Group J of the 2026 FIFA World Cup group stage? |
| 38     | Algeria   | Will Algeria win on 2026-06-16 in Group J of the 2026 FIFA World Cup group stage?           |

***

#### Match 33 — Austria vs. Jordan

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team    | Oracle question title                                                                    |
| ------ | ------- | ---------------------------------------------------------------------------------------- |
| 39     | Austria | Will Austria win on 2026-06-17 in Group J of the 2026 FIFA World Cup group stage?        |
| 50     | draw    | Will Austria vs. Jordan end in a draw in Group J of the 2026 FIFA World Cup group stage? |
| 40     | Jordan  | Will Jordan win on 2026-06-17 in Group J of the 2026 FIFA World Cup group stage?         |

***

#### Match 34 — Portugal vs. DR Congo

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team     | Oracle question title                                                                       |
| ------ | -------- | ------------------------------------------------------------------------------------------- |
| 41     | Portugal | Will Portugal win on 2026-06-17 in Group K of the 2026 FIFA World Cup group stage?          |
| 50     | draw     | Will Portugal vs. DR Congo end in a draw in Group K of the 2026 FIFA World Cup group stage? |
| 42     | DR Congo | Will DR Congo win on 2026-06-17 in Group K of the 2026 FIFA World Cup group stage?          |

***

#### Match 35 — England vs. Croatia

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team    | Oracle question title                                                                     |
| ------ | ------- | ----------------------------------------------------------------------------------------- |
| 45     | England | Will England win on 2026-06-17 in Group L of the 2026 FIFA World Cup group stage?         |
| 50     | draw    | Will England vs. Croatia end in a draw in Group L of the 2026 FIFA World Cup group stage? |
| 46     | Croatia | Will Croatia win on 2026-06-17 in Group L of the 2026 FIFA World Cup group stage?         |

***

#### Match 36 — Ghana vs. Panama

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team   | Oracle question title                                                                  |
| ------ | ------ | -------------------------------------------------------------------------------------- |
| 47     | Ghana  | Will Ghana win on 2026-06-17 in Group L of the 2026 FIFA World Cup group stage?        |
| 50     | draw   | Will Ghana vs. Panama end in a draw in Group L of the 2026 FIFA World Cup group stage? |
| 48     | Panama | Will Panama win on 2026-06-17 in Group L of the 2026 FIFA World Cup group stage?       |

***

#### Match 37 — Uzbekistan vs. Colombia

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team       | Oracle question title                                                                         |
| ------ | ---------- | --------------------------------------------------------------------------------------------- |
| 43     | Uzbekistan | Will Uzbekistan win on 2026-06-17 in Group K of the 2026 FIFA World Cup group stage?          |
| 50     | draw       | Will Uzbekistan vs. Colombia end in a draw in Group K of the 2026 FIFA World Cup group stage? |
| 44     | Colombia   | Will Colombia win on 2026-06-17 in Group K of the 2026 FIFA World Cup group stage?            |

***

#### Match 38 — Czechia vs. South Africa

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team         | Oracle question title                                                                          |
| ------ | ------------ | ---------------------------------------------------------------------------------------------- |
| 4      | Czechia      | Will Czechia win on 2026-06-18 in Group A of the 2026 FIFA World Cup group stage?              |
| 50     | draw         | Will Czechia vs. South Africa end in a draw in Group A of the 2026 FIFA World Cup group stage? |
| 2      | South Africa | Will South Africa win on 2026-06-18 in Group A of the 2026 FIFA World Cup group stage?         |

***

#### Match 39 — Switzerland vs. Bosnia and Herzegovina

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team                   | Oracle question title                                                                                        |
| ------ | ---------------------- | ------------------------------------------------------------------------------------------------------------ |
| 8      | Switzerland            | Will Switzerland win on 2026-06-18 in Group B of the 2026 FIFA World Cup group stage?                        |
| 50     | draw                   | Will Switzerland vs. Bosnia and Herzegovina end in a draw in Group B of the 2026 FIFA World Cup group stage? |
| 6      | Bosnia and Herzegovina | Will Bosnia and Herzegovina win on 2026-06-18 in Group B of the 2026 FIFA World Cup group stage?             |

***

#### Match 40 — Canada vs. Qatar

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team   | Oracle question title                                                                  |
| ------ | ------ | -------------------------------------------------------------------------------------- |
| 5      | Canada | Will Canada win on 2026-06-18 in Group B of the 2026 FIFA World Cup group stage?       |
| 50     | draw   | Will Canada vs. Qatar end in a draw in Group B of the 2026 FIFA World Cup group stage? |
| 7      | Qatar  | Will Qatar win on 2026-06-18 in Group B of the 2026 FIFA World Cup group stage?        |

***

#### Match 41 — Mexico vs. South Korea

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team        | Oracle question title                                                                           |
| ------ | ----------- | ----------------------------------------------------------------------------------------------- |
| 1      | Mexico      | Will Mexico win on 2026-06-18 in Group A of the 2026 FIFA World Cup group stage?                |
| 50     | draw        | Will Mexico vs. Korea Republic end in a draw in Group A of the 2026 FIFA World Cup group stage? |
| 3      | South Korea | Will Korea Republic win on 2026-06-18 in Group A of the 2026 FIFA World Cup group stage?        |

***

#### Match 42 — USA vs. Australia

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team      | Oracle question title                                                                             |
| ------ | --------- | ------------------------------------------------------------------------------------------------- |
| 13     | USA       | Will United States win on 2026-06-19 in Group D of the 2026 FIFA World Cup group stage?           |
| 50     | draw      | Will United States vs. Australia end in a draw in Group D of the 2026 FIFA World Cup group stage? |
| 15     | Australia | Will Australia win on 2026-06-19 in Group D of the 2026 FIFA World Cup group stage?               |

***

#### Match 43 — Scotland vs. Morocco

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team     | Oracle question title                                                                      |
| ------ | -------- | ------------------------------------------------------------------------------------------ |
| 12     | Scotland | Will Scotland win on 2026-06-19 in Group C of the 2026 FIFA World Cup group stage?         |
| 50     | draw     | Will Scotland vs. Morocco end in a draw in Group C of the 2026 FIFA World Cup group stage? |
| 10     | Morocco  | Will Morocco win on 2026-06-19 in Group C of the 2026 FIFA World Cup group stage?          |

***

#### Match 44 — Brazil vs. Haiti

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team   | Oracle question title                                                                  |
| ------ | ------ | -------------------------------------------------------------------------------------- |
| 9      | Brazil | Will Brazil win on 2026-06-19 in Group C of the 2026 FIFA World Cup group stage?       |
| 50     | draw   | Will Brazil vs. Haiti end in a draw in Group C of the 2026 FIFA World Cup group stage? |
| 11     | Haiti  | Will Haiti win on 2026-06-19 in Group C of the 2026 FIFA World Cup group stage?        |

***

#### Match 45 — Türkiye vs. Paraguay

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team     | Oracle question title                                                                      |
| ------ | -------- | ------------------------------------------------------------------------------------------ |
| 16     | Türkiye  | Will Türkiye win on 2026-06-19 in Group D of the 2026 FIFA World Cup group stage?          |
| 50     | draw     | Will Türkiye vs. Paraguay end in a draw in Group D of the 2026 FIFA World Cup group stage? |
| 14     | Paraguay | Will Paraguay win on 2026-06-19 in Group D of the 2026 FIFA World Cup group stage?         |

***

#### Match 46 — Netherlands vs. Sweden

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team        | Oracle question title                                                                        |
| ------ | ----------- | -------------------------------------------------------------------------------------------- |
| 21     | Netherlands | Will Netherlands win on 2026-06-20 in Group F of the 2026 FIFA World Cup group stage?        |
| 50     | draw        | Will Netherlands vs. Sweden end in a draw in Group F of the 2026 FIFA World Cup group stage? |
| 23     | Sweden      | Will Sweden win on 2026-06-20 in Group F of the 2026 FIFA World Cup group stage?             |

***

#### Match 47 — Germany vs. Ivory Coast

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team        | Oracle question title                                                                           |
| ------ | ----------- | ----------------------------------------------------------------------------------------------- |
| 17     | Germany     | Will Germany win on 2026-06-20 in Group E of the 2026 FIFA World Cup group stage?               |
| 50     | draw        | Will Germany vs. Côte d'Ivoire end in a draw in Group E of the 2026 FIFA World Cup group stage? |
| 19     | Ivory Coast | Will Côte d'Ivoire win on 2026-06-20 in Group E of the 2026 FIFA World Cup group stage?         |

***

#### Match 48 — Ecuador vs. Curaçao

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team    | Oracle question title                                                                     |
| ------ | ------- | ----------------------------------------------------------------------------------------- |
| 20     | Ecuador | Will Ecuador win on 2026-06-20 in Group E of the 2026 FIFA World Cup group stage?         |
| 50     | draw    | Will Ecuador vs. Curaçao end in a draw in Group E of the 2026 FIFA World Cup group stage? |
| 18     | Curaçao | Will Curaçao win on 2026-06-20 in Group E of the 2026 FIFA World Cup group stage?         |

***

#### Match 49 — Tunisia vs. Japan

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team    | Oracle question title                                                                   |
| ------ | ------- | --------------------------------------------------------------------------------------- |
| 24     | Tunisia | Will Tunisia win on 2026-06-21 in Group F of the 2026 FIFA World Cup group stage?       |
| 50     | draw    | Will Tunisia vs. Japan end in a draw in Group F of the 2026 FIFA World Cup group stage? |
| 22     | Japan   | Will Japan win on 2026-06-21 in Group F of the 2026 FIFA World Cup group stage?         |

***

#### Match 50 — Spain vs. Saudi Arabia

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team         | Oracle question title                                                                        |
| ------ | ------------ | -------------------------------------------------------------------------------------------- |
| 29     | Spain        | Will Spain win on 2026-06-21 in Group H of the 2026 FIFA World Cup group stage?              |
| 50     | draw         | Will Spain vs. Saudi Arabia end in a draw in Group H of the 2026 FIFA World Cup group stage? |
| 31     | Saudi Arabia | Will Saudi Arabia win on 2026-06-21 in Group H of the 2026 FIFA World Cup group stage?       |

***

#### Match 51 — Belgium vs. Iran

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team    | Oracle question title                                                                     |
| ------ | ------- | ----------------------------------------------------------------------------------------- |
| 25     | Belgium | Will Belgium win on 2026-06-21 in Group G of the 2026 FIFA World Cup group stage?         |
| 50     | draw    | Will Belgium vs. IR Iran end in a draw in Group G of the 2026 FIFA World Cup group stage? |
| 27     | Iran    | Will IR Iran win on 2026-06-21 in Group G of the 2026 FIFA World Cup group stage?         |

***

#### Match 52 — Uruguay vs. Cape Verde

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team       | Oracle question title                                                                        |
| ------ | ---------- | -------------------------------------------------------------------------------------------- |
| 32     | Uruguay    | Will Uruguay win on 2026-06-21 in Group H of the 2026 FIFA World Cup group stage?            |
| 50     | draw       | Will Uruguay vs. Cabo Verde end in a draw in Group H of the 2026 FIFA World Cup group stage? |
| 30     | Cape Verde | Will Cabo Verde win on 2026-06-21 in Group H of the 2026 FIFA World Cup group stage?         |

***

#### Match 53 — New Zealand vs. Egypt

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team        | Oracle question title                                                                       |
| ------ | ----------- | ------------------------------------------------------------------------------------------- |
| 28     | New Zealand | Will New Zealand win on 2026-06-21 in Group G of the 2026 FIFA World Cup group stage?       |
| 50     | draw        | Will New Zealand vs. Egypt end in a draw in Group G of the 2026 FIFA World Cup group stage? |
| 26     | Egypt       | Will Egypt win on 2026-06-21 in Group G of the 2026 FIFA World Cup group stage?             |

***

#### Match 54 — Argentina vs. Austria

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team      | Oracle question title                                                                       |
| ------ | --------- | ------------------------------------------------------------------------------------------- |
| 37     | Argentina | Will Argentina win on 2026-06-22 in Group J of the 2026 FIFA World Cup group stage?         |
| 50     | draw      | Will Argentina vs. Austria end in a draw in Group J of the 2026 FIFA World Cup group stage? |
| 39     | Austria   | Will Austria win on 2026-06-22 in Group J of the 2026 FIFA World Cup group stage?           |

***

#### Match 55 — France vs. Iraq

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team   | Oracle question title                                                                 |
| ------ | ------ | ------------------------------------------------------------------------------------- |
| 33     | France | Will France win on 2026-06-22 in Group I of the 2026 FIFA World Cup group stage?      |
| 50     | draw   | Will France vs. Iraq end in a draw in Group I of the 2026 FIFA World Cup group stage? |
| 35     | Iraq   | Will Iraq win on 2026-06-22 in Group I of the 2026 FIFA World Cup group stage?        |

***

#### Match 56 — Norway vs. Senegal

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team    | Oracle question title                                                                    |
| ------ | ------- | ---------------------------------------------------------------------------------------- |
| 36     | Norway  | Will Norway win on 2026-06-22 in Group I of the 2026 FIFA World Cup group stage?         |
| 50     | draw    | Will Norway vs. Senegal end in a draw in Group I of the 2026 FIFA World Cup group stage? |
| 34     | Senegal | Will Senegal win on 2026-06-22 in Group I of the 2026 FIFA World Cup group stage?        |

***

#### Match 57 — Jordan vs. Algeria

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team    | Oracle question title                                                                    |
| ------ | ------- | ---------------------------------------------------------------------------------------- |
| 40     | Jordan  | Will Jordan win on 2026-06-22 in Group J of the 2026 FIFA World Cup group stage?         |
| 50     | draw    | Will Jordan vs. Algeria end in a draw in Group J of the 2026 FIFA World Cup group stage? |
| 38     | Algeria | Will Algeria win on 2026-06-22 in Group J of the 2026 FIFA World Cup group stage?        |

***

#### Match 58 — Portugal vs. Uzbekistan

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team       | Oracle question title                                                                         |
| ------ | ---------- | --------------------------------------------------------------------------------------------- |
| 41     | Portugal   | Will Portugal win on 2026-06-23 in Group K of the 2026 FIFA World Cup group stage?            |
| 50     | draw       | Will Portugal vs. Uzbekistan end in a draw in Group K of the 2026 FIFA World Cup group stage? |
| 43     | Uzbekistan | Will Uzbekistan win on 2026-06-23 in Group K of the 2026 FIFA World Cup group stage?          |

***

#### Match 59 — England vs. Ghana

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team    | Oracle question title                                                                   |
| ------ | ------- | --------------------------------------------------------------------------------------- |
| 45     | England | Will England win on 2026-06-23 in Group L of the 2026 FIFA World Cup group stage?       |
| 50     | draw    | Will England vs. Ghana end in a draw in Group L of the 2026 FIFA World Cup group stage? |
| 47     | Ghana   | Will Ghana win on 2026-06-23 in Group L of the 2026 FIFA World Cup group stage?         |

***

#### Match 60 — Panama vs. Croatia

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team    | Oracle question title                                                                    |
| ------ | ------- | ---------------------------------------------------------------------------------------- |
| 48     | Panama  | Will Panama win on 2026-06-23 in Group L of the 2026 FIFA World Cup group stage?         |
| 50     | draw    | Will Panama vs. Croatia end in a draw in Group L of the 2026 FIFA World Cup group stage? |
| 46     | Croatia | Will Croatia win on 2026-06-23 in Group L of the 2026 FIFA World Cup group stage?        |

***

#### Match 61 — Colombia vs. DR Congo

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team     | Oracle question title                                                                       |
| ------ | -------- | ------------------------------------------------------------------------------------------- |
| 44     | Colombia | Will Colombia win on 2026-06-23 in Group K of the 2026 FIFA World Cup group stage?          |
| 50     | draw     | Will Colombia vs. DR Congo end in a draw in Group K of the 2026 FIFA World Cup group stage? |
| 42     | DR Congo | Will DR Congo win on 2026-06-23 in Group K of the 2026 FIFA World Cup group stage?          |

***

#### Match 62 — Bosnia and Herzegovina vs. Qatar

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team                   | Oracle question title                                                                                  |
| ------ | ---------------------- | ------------------------------------------------------------------------------------------------------ |
| 6      | Bosnia and Herzegovina | Will Bosnia and Herzegovina win on 2026-06-24 in Group B of the 2026 FIFA World Cup group stage?       |
| 50     | draw                   | Will Bosnia and Herzegovina vs. Qatar end in a draw in Group B of the 2026 FIFA World Cup group stage? |
| 7      | Qatar                  | Will Qatar win on 2026-06-24 in Group B of the 2026 FIFA World Cup group stage?                        |

***

#### Match 63 — Switzerland vs. Canada

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team        | Oracle question title                                                                        |
| ------ | ----------- | -------------------------------------------------------------------------------------------- |
| 8      | Switzerland | Will Switzerland win on 2026-06-24 in Group B of the 2026 FIFA World Cup group stage?        |
| 50     | draw        | Will Switzerland vs. Canada end in a draw in Group B of the 2026 FIFA World Cup group stage? |
| 5      | Canada      | Will Canada win on 2026-06-24 in Group B of the 2026 FIFA World Cup group stage?             |

***

#### Match 64 — Morocco vs. Haiti

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team    | Oracle question title                                                                   |
| ------ | ------- | --------------------------------------------------------------------------------------- |
| 10     | Morocco | Will Morocco win on 2026-06-24 in Group C of the 2026 FIFA World Cup group stage?       |
| 50     | draw    | Will Morocco vs. Haiti end in a draw in Group C of the 2026 FIFA World Cup group stage? |
| 11     | Haiti   | Will Haiti win on 2026-06-24 in Group C of the 2026 FIFA World Cup group stage?         |

***

#### Match 65 — Scotland vs. Brazil

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team     | Oracle question title                                                                     |
| ------ | -------- | ----------------------------------------------------------------------------------------- |
| 12     | Scotland | Will Scotland win on 2026-06-24 in Group C of the 2026 FIFA World Cup group stage?        |
| 50     | draw     | Will Scotland vs. Brazil end in a draw in Group C of the 2026 FIFA World Cup group stage? |
| 9      | Brazil   | Will Brazil win on 2026-06-24 in Group C of the 2026 FIFA World Cup group stage?          |

***

#### Match 66 — Czechia vs. Mexico

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team    | Oracle question title                                                                    |
| ------ | ------- | ---------------------------------------------------------------------------------------- |
| 4      | Czechia | Will Czechia win on 2026-06-24 in Group A of the 2026 FIFA World Cup group stage?        |
| 50     | draw    | Will Czechia vs. Mexico end in a draw in Group A of the 2026 FIFA World Cup group stage? |
| 1      | Mexico  | Will Mexico win on 2026-06-24 in Group A of the 2026 FIFA World Cup group stage?         |

***

#### Match 67 — South Africa vs. South Korea

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team         | Oracle question title                                                                                 |
| ------ | ------------ | ----------------------------------------------------------------------------------------------------- |
| 2      | South Africa | Will South Africa win on 2026-06-24 in Group A of the 2026 FIFA World Cup group stage?                |
| 50     | draw         | Will South Africa vs. Korea Republic end in a draw in Group A of the 2026 FIFA World Cup group stage? |
| 3      | South Korea  | Will Korea Republic win on 2026-06-24 in Group A of the 2026 FIFA World Cup group stage?              |

***

#### Match 68 — Curaçao vs. Ivory Coast

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team        | Oracle question title                                                                           |
| ------ | ----------- | ----------------------------------------------------------------------------------------------- |
| 18     | Curaçao     | Will Curaçao win on 2026-06-25 in Group E of the 2026 FIFA World Cup group stage?               |
| 50     | draw        | Will Curaçao vs. Côte d'Ivoire end in a draw in Group E of the 2026 FIFA World Cup group stage? |
| 19     | Ivory Coast | Will Côte d'Ivoire win on 2026-06-25 in Group E of the 2026 FIFA World Cup group stage?         |

***

#### Match 69 — Ecuador vs. Germany

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team    | Oracle question title                                                                     |
| ------ | ------- | ----------------------------------------------------------------------------------------- |
| 20     | Ecuador | Will Ecuador win on 2026-06-25 in Group E of the 2026 FIFA World Cup group stage?         |
| 50     | draw    | Will Ecuador vs. Germany end in a draw in Group E of the 2026 FIFA World Cup group stage? |
| 17     | Germany | Will Germany win on 2026-06-25 in Group E of the 2026 FIFA World Cup group stage?         |

***

#### Match 70 — Japan vs. Sweden

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team   | Oracle question title                                                                  |
| ------ | ------ | -------------------------------------------------------------------------------------- |
| 22     | Japan  | Will Japan win on 2026-06-25 in Group F of the 2026 FIFA World Cup group stage?        |
| 50     | draw   | Will Japan vs. Sweden end in a draw in Group F of the 2026 FIFA World Cup group stage? |
| 23     | Sweden | Will Sweden win on 2026-06-25 in Group F of the 2026 FIFA World Cup group stage?       |

***

#### Match 71 — Tunisia vs. Netherlands

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team        | Oracle question title                                                                         |
| ------ | ----------- | --------------------------------------------------------------------------------------------- |
| 24     | Tunisia     | Will Tunisia win on 2026-06-25 in Group F of the 2026 FIFA World Cup group stage?             |
| 50     | draw        | Will Tunisia vs. Netherlands end in a draw in Group F of the 2026 FIFA World Cup group stage? |
| 21     | Netherlands | Will Netherlands win on 2026-06-25 in Group F of the 2026 FIFA World Cup group stage?         |

***

#### Match 72 — Türkiye vs. USA

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team    | Oracle question title                                                                           |
| ------ | ------- | ----------------------------------------------------------------------------------------------- |
| 16     | Türkiye | Will Türkiye win on 2026-06-25 in Group D of the 2026 FIFA World Cup group stage?               |
| 50     | draw    | Will Türkiye vs. United States end in a draw in Group D of the 2026 FIFA World Cup group stage? |
| 13     | USA     | Will United States win on 2026-06-25 in Group D of the 2026 FIFA World Cup group stage?         |

***

#### Match 73 — Paraguay vs. Australia

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team      | Oracle question title                                                                        |
| ------ | --------- | -------------------------------------------------------------------------------------------- |
| 14     | Paraguay  | Will Paraguay win on 2026-06-25 in Group D of the 2026 FIFA World Cup group stage?           |
| 50     | draw      | Will Paraguay vs. Australia end in a draw in Group D of the 2026 FIFA World Cup group stage? |
| 15     | Australia | Will Australia win on 2026-06-25 in Group D of the 2026 FIFA World Cup group stage?          |

***

#### Match 74 — Senegal vs. Iraq

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team    | Oracle question title                                                                  |
| ------ | ------- | -------------------------------------------------------------------------------------- |
| 34     | Senegal | Will Senegal win on 2026-06-26 in Group I of the 2026 FIFA World Cup group stage?      |
| 50     | draw    | Will Senegal vs. Iraq end in a draw in Group I of the 2026 FIFA World Cup group stage? |
| 35     | Iraq    | Will Iraq win on 2026-06-26 in Group I of the 2026 FIFA World Cup group stage?         |

***

#### Match 75 — Norway vs. France

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team   | Oracle question title                                                                   |
| ------ | ------ | --------------------------------------------------------------------------------------- |
| 36     | Norway | Will Norway win on 2026-06-26 in Group I of the 2026 FIFA World Cup group stage?        |
| 50     | draw   | Will Norway vs. France end in a draw in Group I of the 2026 FIFA World Cup group stage? |
| 33     | France | Will France win on 2026-06-26 in Group I of the 2026 FIFA World Cup group stage?        |

***

#### Match 76 — Cape Verde vs. Saudi Arabia

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team         | Oracle question title                                                                             |
| ------ | ------------ | ------------------------------------------------------------------------------------------------- |
| 30     | Cape Verde   | Will Cabo Verde win on 2026-06-26 in Group H of the 2026 FIFA World Cup group stage?              |
| 50     | draw         | Will Cabo Verde vs. Saudi Arabia end in a draw in Group H of the 2026 FIFA World Cup group stage? |
| 31     | Saudi Arabia | Will Saudi Arabia win on 2026-06-26 in Group H of the 2026 FIFA World Cup group stage?            |

***

#### Match 77 — Uruguay vs. Spain

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team    | Oracle question title                                                                   |
| ------ | ------- | --------------------------------------------------------------------------------------- |
| 32     | Uruguay | Will Uruguay win on 2026-06-26 in Group H of the 2026 FIFA World Cup group stage?       |
| 50     | draw    | Will Uruguay vs. Spain end in a draw in Group H of the 2026 FIFA World Cup group stage? |
| 29     | Spain   | Will Spain win on 2026-06-26 in Group H of the 2026 FIFA World Cup group stage?         |

***

#### Match 78 — Egypt vs. Iran

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team  | Oracle question title                                                                   |
| ------ | ----- | --------------------------------------------------------------------------------------- |
| 26     | Egypt | Will Egypt win on 2026-06-26 in Group G of the 2026 FIFA World Cup group stage?         |
| 50     | draw  | Will Egypt vs. IR Iran end in a draw in Group G of the 2026 FIFA World Cup group stage? |
| 27     | Iran  | Will IR Iran win on 2026-06-26 in Group G of the 2026 FIFA World Cup group stage?       |

***

#### Match 79 — New Zealand vs. Belgium

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team        | Oracle question title                                                                         |
| ------ | ----------- | --------------------------------------------------------------------------------------------- |
| 28     | New Zealand | Will New Zealand win on 2026-06-26 in Group G of the 2026 FIFA World Cup group stage?         |
| 50     | draw        | Will New Zealand vs. Belgium end in a draw in Group G of the 2026 FIFA World Cup group stage? |
| 25     | Belgium     | Will Belgium win on 2026-06-26 in Group G of the 2026 FIFA World Cup group stage?             |

***

#### Match 80 — Croatia vs. Ghana

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team    | Oracle question title                                                                   |
| ------ | ------- | --------------------------------------------------------------------------------------- |
| 46     | Croatia | Will Croatia win on 2026-06-27 in Group L of the 2026 FIFA World Cup group stage?       |
| 50     | draw    | Will Croatia vs. Ghana end in a draw in Group L of the 2026 FIFA World Cup group stage? |
| 47     | Ghana   | Will Ghana win on 2026-06-27 in Group L of the 2026 FIFA World Cup group stage?         |

***

#### Match 81 — Panama vs. England

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team    | Oracle question title                                                                    |
| ------ | ------- | ---------------------------------------------------------------------------------------- |
| 48     | Panama  | Will Panama win on 2026-06-27 in Group L of the 2026 FIFA World Cup group stage?         |
| 50     | draw    | Will Panama vs. England end in a draw in Group L of the 2026 FIFA World Cup group stage? |
| 45     | England | Will England win on 2026-06-27 in Group L of the 2026 FIFA World Cup group stage?        |

***

#### Match 82 — Colombia vs. Portugal

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team     | Oracle question title                                                                       |
| ------ | -------- | ------------------------------------------------------------------------------------------- |
| 44     | Colombia | Will Colombia win on 2026-06-27 in Group K of the 2026 FIFA World Cup group stage?          |
| 50     | draw     | Will Colombia vs. Portugal end in a draw in Group K of the 2026 FIFA World Cup group stage? |
| 41     | Portugal | Will Portugal win on 2026-06-27 in Group K of the 2026 FIFA World Cup group stage?          |

***

#### Match 83 — DR Congo vs. Uzbekistan

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team       | Oracle question title                                                                         |
| ------ | ---------- | --------------------------------------------------------------------------------------------- |
| 42     | DR Congo   | Will DR Congo win on 2026-06-27 in Group K of the 2026 FIFA World Cup group stage?            |
| 50     | draw       | Will DR Congo vs. Uzbekistan end in a draw in Group K of the 2026 FIFA World Cup group stage? |
| 43     | Uzbekistan | Will Uzbekistan win on 2026-06-27 in Group K of the 2026 FIFA World Cup group stage?          |

***

#### Match 84 — Jordan vs. Argentina

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team      | Oracle question title                                                                      |
| ------ | --------- | ------------------------------------------------------------------------------------------ |
| 40     | Jordan    | Will Jordan win on 2026-06-27 in Group J of the 2026 FIFA World Cup group stage?           |
| 50     | draw      | Will Jordan vs. Argentina end in a draw in Group J of the 2026 FIFA World Cup group stage? |
| 37     | Argentina | Will Argentina win on 2026-06-27 in Group J of the 2026 FIFA World Cup group stage?        |

***

#### Match 85 — Algeria vs. Austria

Resolves to the winner of this match in the 2026 FIFA World Cup.

| teamId | Team    | Oracle question title                                                                     |
| ------ | ------- | ----------------------------------------------------------------------------------------- |
| 38     | Algeria | Will Algeria win on 2026-06-27 in Group J of the 2026 FIFA World Cup group stage?         |
| 50     | draw    | Will Algeria vs. Austria end in a draw in Group J of the 2026 FIFA World Cup group stage? |
| 39     | Austria | Will Austria win on 2026-06-27 in Group J of the 2026 FIFA World Cup group stage?         |

***


# X General Verifier

{% hint style="success" %}
**This page is for vault developers.** If you haven't yet, read [Vault & VaultFactory Specification](/flap/developers/vault-developers/vault-and-vaultfactory-specification) first to understand what a vault is and how it receives tax revenue. X General Verifier is infrastructure you plug **into** a vault's control logic — it is not a standalone product on its own.
{% endhint %}

## Why vault developers should care

A vault is the smart contract that receives a token's tax/fee revenue on Flap. What a vault *does* with that revenue — who can claim it, when, and under what condition — is entirely up to you, the vault developer. [Gift Vault](/flap/developers/vault-developers/gift-vault) is one existing example: it lets an X account prove ownership of a fixed "gift the fee from X to Y" tweet format and redirect the vault's fee flow.

X General Verifier generalizes that same idea into reusable infrastructure any vault can call into:

* The **substring** your vault checks for is entirely up to you — it isn't locked to a fixed sentence format like Gift Vault V2. Your vault can construct any expected tweet content it wants, at launch time or dynamically.
* The **on-chain verifier** and **off-chain oracle** are shared, audited infrastructure. You don't need to build or run your own signing service — just call `verify()` from your vault.
* It's **multi-chain** — the same oracle signer works across every deployment, so a vault design ported to a new chain doesn't need new backend integration.

In short: whenever your vault design needs to answer *"has account @X actually said Y?"* on-chain, this is the infrastructure to reach for instead of building your own oracle from scratch.

**Deployed addresses:**

| Network                 | XGeneralVerifier                             |
| ----------------------- | -------------------------------------------- |
| BSC Mainnet             | `0xcA8DBE6CAC4BFDc41226b0BaF2359fd99989b3E4` |
| BSC Testnet             | `0x236C4AF8e0674fB22a139DDE4C2A518CBA819932` |
| Robinhood Chain Mainnet | `0xccDaB0d5Bc6E0aCb8B157cffFA062688Aa849c17` |

The same oracle signing key is used across every chain — only the verifying contract address (and therefore the EIP-712 domain) differs per chain.

**Live oracle server:** [`https://verifyx.taxed.fun`](https://verifyx.taxed.fun) — call `POST /prove` directly from your own frontend or backend. No API key or registration required. See the [Oracle server API](#oracle-server-api) section below for the full request/response format.

{% hint style="info" %}
Want to build on this infrastructure for a new use case? [Reach out to us](https://flap.sh) — we're happy to help you integrate.
{% endhint %}

***

## The two parts

* **A stateless on-chain verifier** (`XGeneralVerifier`) — a pure EIP-712 signature verification contract. It holds no state beyond the oracle's signing key and does not track proof usage.
* **An off-chain oracle server** — fetches a tweet, checks that it contains the substring your vault expects, and signs an EIP-712 attestation over the result.

***

## How it works, end to end

A typical flow looks like this:

1. Your vault deploys with (or later computes) an **expected substring** — some exact text tied to the claim it's gating. This might embed a beneficiary address, a vault-specific nonce, a role name, anything your logic needs.
2. A user, or the specific X account your vault expects, posts a tweet containing that exact substring.
3. A **bespoke UI you build** — or Flap's own website, if your vault is registered there — asks the oracle server to prove the tweet exists and matches. This is a normal off-chain HTTP call, not a transaction.
4. Your UI receives back a signed `XGeneralProof` and forwards it, along with the signature, into a transaction calling your vault contract.
5. Your vault calls `XGeneralVerifier.verify(proof, signature)` on-chain. If it returns `true`, your vault executes whatever logic you've written — release funds, hand over control, unlock a new state, etc.

```mermaid
sequenceDiagram
    participant U as X User
    participant D as Bespoke UI / flap.sh
    participant O as Oracle Server
    participant V as Your Vault Contract

    U->>U: Posts a tweet containing the substring your vault expects
    D->>O: POST /prove {tweet_id, substring}
    Note over O: Fetches the tweet, checks the substring is present
    O->>O: Signs an EIP-712 XGeneralProof
    O-->>D: {proof fields, signature, ipfs_cid}
    D->>V: yourClaimFunction(proof, signature)
    V->>V: verify(proof, signature) via XGeneralVerifier
    V-->>D: claim succeeds / reverts
```

Nothing about steps 3–5 requires Flap's website. You are free to build a completely custom frontend that talks to the oracle and your vault directly — Flap's site is just one possible entry point into vaults that choose to register there.

***

## The `XGeneralProof` struct

```solidity
struct XGeneralProof {
    uint128 tweetId;    // The tweet's unique Snowflake ID
    string  xHandle;    // The X account's handle — always lowercase
    uint128 xId;        // The X account's numeric user ID
    string  substring;  // The substring matched inside the tweet text
}
```

{% hint style="warning" %}
`xHandle` is **always lowercase**. The oracle normalizes it before signing, and `XGeneralVerifier.verify()` rejects any proof containing an uppercase character in `xHandle` — it returns `false` rather than reverting. Always store expected handles in lowercase in your vault contract to avoid mismatches.
{% endhint %}

The `substring` match performed by the oracle is **case-sensitive**. Design your vault's expected substring deterministically (for example, embedding a lowercase hex address) so that off-chain callers and your on-chain check always agree exactly.

***

## `XGeneralVerifier` contract interface

```solidity
interface IXGeneralVerifier {
    /// @notice Verifies an oracle-signed XGeneralProof.
    /// @dev Stateless — does not track or prevent proof replay.
    function verify(
        XGeneralProof calldata proof,
        bytes calldata signature
    ) external view returns (bool);

    /// @notice The oracle signing address this verifier trusts.
    function oracleKey() external view returns (address);
}
```

`XGeneralVerifier` is intentionally minimal:

* **Pure verification only.** It checks the EIP-712 signature against the immutable `oracleKey` set at deployment. No storage is mutated after initialization.
* **No replay protection.** The contract itself does not remember which proofs have already been used. Your vault is responsible for its own replay guard.

{% hint style="info" %}
**Recommended replay pattern:** track the last-used `tweetId` per beneficiary/context in your vault, and require `proof.tweetId > lastTweetId`. X assigns Snowflake IDs that are strictly increasing over time, so this is sufficient to prevent replay without any additional nonce infrastructure.
{% endhint %}

***

## Oracle server API

**Base URL:** `https://verifyx.taxed.fun` — this is the live, public endpoint. No API key, allowlisting, or registration is required; any vault, UI, or backend can call it directly.

### `POST /prove`

```
POST https://verifyx.taxed.fun/prove?chain_id=<optional>
```

**Request body:**

```json
{
  "tweet_id": "1234567890123456789",
  "substring": "the exact text that must appear in the tweet"
}
```

| Field       | Type             | Notes                                                                                       |
| ----------- | ---------------- | ------------------------------------------------------------------------------------------- |
| `tweet_id`  | string (numeric) | The tweet's Snowflake ID                                                                    |
| `substring` | string           | Must be non-empty and at most 200 bytes. Case-sensitive exact match against the tweet text. |

**Optional query parameter:**

| Parameter  | Behavior                                                                                                                                                                                         |
| ---------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `chain_id` | Selects which chain's `XGeneralVerifier` domain to sign for. If provided but not a supported chain, the request is rejected. If omitted, defaults to **BSC mainnet** for backward compatibility. |

**Response — 200 OK:**

```json
{
  "tweet_id": "1234567890123456789",
  "x_handle": "example",
  "x_id": "987654321",
  "substring": "the exact text that must appear in the tweet",
  "signature": "0x...",
  "ipfs_cid": "Qm...",
  "chain_id": 56,
  "verifier_address": "0xca8dbe6cac4bfdc41226b0baf2359fd99989b3e4"
}
```

`chain_id` and `verifier_address` in the response confirm which deployment the signature was produced for — check these match what your vault expects before submitting the proof on-chain.

**Error responses** return `400` for validation failures (empty/oversized substring, substring not found in the tweet, invalid or deleted tweet, unsupported `chain_id`) and `500` for unexpected internal errors.

### Try it with `curl`

```bash
curl -X POST "https://verifyx.taxed.fun/prove?chain_id=56" \
  -H "Content-Type: application/json" \
  -d '{
    "tweet_id": "1234567890123456789",
    "substring": "the exact text that must appear in the tweet"
  }'
```

### `GET /health`

```
GET https://verifyx.taxed.fun/health
```

Returns a bare `200 OK` with an empty body. Use this to confirm the oracle is reachable before troubleshooting a `/prove` call.

***

## Supported chains

| Chain                   | `chain_id` |
| ----------------------- | ---------- |
| BSC Mainnet             | `56`       |
| BSC Testnet             | `97`       |
| Robinhood Chain Mainnet | `4663`     |

More chains are added over time as new `XGeneralVerifier` deployments go live.

***

## Vault design patterns you can build with this

X General Verifier only answers one question — *did this X account post this exact text?* — but that primitive composes into a surprising range of vault behaviors. A few starting points:

### 1. Ownership handoff (generalized Gift Vault)

The simplest pattern, and the one [Gift Vault](/flap/developers/vault-developers/gift-vault) already implements with a fixed sentence. With X General Verifier you control the substring yourself, so you can:

* Assign an X account as the sole controller of the vault at launch.
* Let that account prove control at any time by posting your vault's expected substring (e.g. `"<handle> controls the vault for <token>"`).
* Once proven, hand the account full authority over the vault — redirect fees, trigger a buyback, or unlock any other admin function you've written.

### 2. Tweet-gated claims / airdrops

Instead of (or in addition to) a merkle-drop, gate a claim behind a tweet:

* Your vault computes a per-claimant expected substring, e.g. `"<address> is claiming from <token>"`.
* Anyone can post that tweet from any account, fetch a proof, and call `claim()`.
* Combine this with a per-account allowlist, a time window, or a maximum-claims-per-day cap for extra control.

### 3. Recurring / scheduled unlocks

Because `tweetId` is a monotonically increasing Snowflake ID, your vault can enforce ordering over time without needing an off-chain scheduler:

* Require the *newest* qualifying tweet since the last claim — e.g. "the vault distributes to whichever address the owner's tweet points to today", refreshed once every 24 hours by requiring `block.timestamp - lastClaimTime >= 1 days` alongside the proof.
* Build a "daily invite" mechanic: each day, the vault owner tweets who to invite/unlock next; the invited address submits the proof to claim a time-boxed reward slot.

### 4. Social consensus / voting proxies

Use tweets as a lightweight signaling channel for on-chain actions:

* A DAO-style vault where the "vote" is a tweet containing a specific option string; the vault tallies claims/actions per option and the majority substring (by claim count within a window) triggers the payout path.
* Gate an emergency pause/resume function behind a specific X account posting a specific phrase, without needing a multisig UI.

### 5. Streak / engagement rewards

Combine multiple proofs over time:

* Require N distinct valid proofs (distinct `tweetId`s) from the same `xId` within a rolling window before a bonus payout unlocks — a simple on-chain "engagement streak."
* Reward the *first* account to post a given day's substring, using `tweetId` ordering to determine who was first.

None of these require touching the oracle or verifier contract — they're all vault-side logic built on top of one primitive: `XGeneralVerifier.verify()` returning `true`. If you have a design that doesn't fit the patterns above, [reach out to us](https://flap.sh) — we're always interested in what vault developers are building.

***

## Building a custom vault integration

A typical pattern for a vault contract:

```solidity
function claimByProof(
    IXGeneralVerifier.XGeneralProof calldata proof,
    bytes calldata signature
) external {
    // 1. Confirm the substring matches what this vault expects.
    require(
        keccak256(bytes(proof.substring)) == keccak256(bytes(expectedSubstring(msg.sender))),
        "substring mismatch"
    );

    // 2. Verify the oracle's signature.
    require(verifier.verify(proof, signature), "invalid proof");

    // 3. Replay protection — tweetId must be newer than the last used one.
    require(proof.tweetId > lastTweetId[msg.sender], "outdated proof");
    lastTweetId[msg.sender] = proof.tweetId;

    // 4. Vault-specific logic (e.g. release tokens, hand over control, etc.)
    _onProofAccepted(msg.sender, proof);
}
```

Because `XGeneralVerifier` and the oracle server are chain-agnostic and vault-agnostic, this same pattern works for any vault design — from a simple claim gate to the more elaborate patterns above.


# FlapVaultEx

## Overview

FlapVaultEx is an **asynchronous, intent-based settlement layer** that lets a vault swap any token for any other token without knowing anything about DEX routing. The vault submits an *intent* — "sell this much of `inputToken`, I want `outputToken`" — and Flap's off-chain executor sources the best route across DEX aggregators and settles it back on-chain.

It bridges on-chain custody with off-chain routing:

* The contract **custodies the input** and records a `PENDING` request.
* The off-chain executor finds the best route and calls `fulfillSwap` with the route calldata.
* The contract **measures the real output by balance delta**, enforces an on-chain minimum, takes the fee, and delivers the result straight back to the vault via a callback — atomically.

There are **no signatures** and **no routing logic in the vault**. Routes are bound by on-chain whitelists, output is verified by balance delta, and the executor can only settle — never redirect — the funds.

**Deployed addresses:**

| Network           | FlapVaultEx (proxy)                          | Max Callback Gas |
| ----------------- | -------------------------------------------- | ---------------- |
| BNB Chain Mainnet | `0x000048Df69f3d2af22Df6DC2d3B01FB592b90000` | 200,000          |

**Primary use cases:**

* Converting tax/fee revenue held in an arbitrary token into a stablecoin or a target dividend asset
* Consolidating multiple fee tokens into one
* Rebalancing a vault's holdings without building or auditing DEX routing

{% hint style="warning" %}
**Fee-on-transfer (FOT) and rebasing tokens are not supported.** The contract verifies exact balance deltas on both sides, so a fee-on-transfer token makes `requestSwap` / settlement revert, and a rebasing balance (which can change between request and settlement) is rejected too. The protocol can also **block (denylist)** unsupported tokens, in which case any request using them reverts.
{% endhint %}

### How it works

```mermaid
sequenceDiagram
    participant V as Vault
    participant A as FlapVaultEx
    participant E as Executor (off-chain)
    participant R as DEX Aggregator

    V->>A: requestSwap(inputToken, outputToken, inputAmount)
    Note over A: custody input, status = PENDING
    A-->>E: emit FlapVaultExRequested(requestId, ...)
    E->>A: getRequest(requestId)
    E->>R: quote best route
    R-->>E: target, allowanceTarget, data, minBuyAmount
    E->>A: fulfillSwap(requestId, target, allowanceTarget, minOutputAmount, data)
    A->>R: target.call(data) (exact approval, reset after)
    R-->>A: outputToken
    A->>A: require output >= minOutputAmount, take fee
    A->>V: onFlapVaultExSettled(requestId, FULFILLED, outputToken, netOutput)
    Note over A: status = FULFILLED
```

**Step by step:**

1. The vault calls `requestSwap()` with `{inputToken, outputToken, inputAmount}`, transferring the input ERC-20 into FlapVaultEx via approval.
2. FlapVaultEx records the request as `PENDING` and emits `FlapVaultExRequested`.
3. The off-chain executor indexes the event, quotes across DEX aggregators, and picks the best route.
4. The executor calls `fulfillSwap()` with the winning route (`target`, `allowanceTarget`, `data`) and a `minOutputAmount` floor.
5. FlapVaultEx approves exactly the input amount, performs the router call, resets the approval, and measures the received output by balance delta.
6. If the output is at least `minOutputAmount`, it takes the fee and delivers the net output to the vault, then calls `onFlapVaultExSettled(...)`. If anything fails, the whole call reverts.

***

## Request lifecycle

| Status      | Value | Meaning                                                                                        |
| ----------- | ----- | ---------------------------------------------------------------------------------------------- |
| `NONE`      | 0     | No such request.                                                                               |
| `PENDING`   | 1     | Input custodied; awaiting settlement or a refund.                                              |
| `FULFILLED` | 2     | Swapped; output delivered to the vault.                                                        |
| `REFUNDED`  | 3     | Input returned to the vault (fee taken from the input side).                                   |
| `FAILED`    | 4     | A failed batch item whose automatic refund could not be delivered; retryable via `refundSwap`. |

{% hint style="info" %}
Requests **never expire** — there is no deadline. A backlogged order can always be settled later, so a vault is never punished because gas was high or a route was temporarily unavailable.
{% endhint %}

***

## Fees

FlapVaultEx charges a small protocol fee, currently **0.5% (50 bps)**. The fee exists primarily to **discourage abuse** — creating requests only to immediately refund them for free, or spamming requests the executor has to process.

**How the fee is taken:**

* It is charged on the **quote-token side**: if the input is a configured quote token (e.g. USDT/USDC/WBNB) and the output is not, it's taken from the input; if the output is the quote, from the output; if both or neither, it falls back to the admin-configured side.
* The rate is **snapshotted at `requestSwap`**. Settlement always uses the snapshotted rate, so a fee change never affects an in-flight request.
* A **refund** also takes the snapshotted `feeBps`, always from the input side (a refund has no output) — so a request → refund round trip is not free.

{% hint style="success" %}
You never compute the fee. `getRequest().swapInputAmount` (and the `FlapVaultExRequested` event) already expose the **net input actually swapped**, after any input-side fee — quote on that and you're done.
{% endhint %}

***

## Callback gas limit

Callbacks run with a bounded gas budget — **200,000** on the current deployment — so settlement can never be griefed by an expensive callback. Keep `onFlapVaultExSettled` cheap; it should record the result, not do heavy work. Read the live value via `maxCallbackGas()`.

{% hint style="warning" %}
The callback is **atomic with settlement**: if it reverts, the whole `fulfillSwap` reverts and the request stays `PENDING`.
{% endhint %}

***

## How to integrate

### Step 1 — Import the interfaces

```solidity
import { IFlapVaultEx, IFlapVaultExConsumer } from "src/flap/IFlapVaultEx.sol";
```

### Step 2 — Implement `IFlapVaultExConsumer`

Your vault must implement `onFlapVaultExSettled`. It is called for **both** outcomes, so branch on `status`:

* `status == 2` (`FULFILLED`): the **output** token was delivered, net of the fee.
* `status == 3` (`REFUNDED`): the **input** token was returned, net of the input-side fee.

In both cases the funds are **already in your vault** when the callback runs. Validate the caller, then do whatever your accounting needs.

{% hint style="warning" %}
`requestSwap` requires `msg.sender` to be a **real contract** (plain EOAs and EIP-7702 delegated accounts are rejected), precisely so this callback can be delivered.
{% endhint %}

### Step 3 — Trigger a swap

Do **not** let the caller pick the tokens or amount. Fix the parameters at **initialization** (like the other vault examples in this repo) and expose a **no-argument** `swap()` that just swaps what the vault actually holds. Revenue (tax payouts, direct transfers) accumulates in the vault, so read the live balance — `balanceOf(address(this))` — and only create a request once it is worth it.

Keep the returned `requestId` (or index it from `FlapVaultExRequested`) so you can correlate the callback.

### Example vault

```solidity
// SPDX-License-Identifier: MIT
pragma solidity ^0.8.13;

import { IFlapVaultEx, IFlapVaultExConsumer } from "src/flap/IFlapVaultEx.sol";
import { IERC20 } from "@openzeppelin/token/ERC20/IERC20.sol";

/// @notice Example vault that swaps the revenue token it accumulates into a fixed output token
///         through FlapVaultEx. The tokens and the minimum swap size are fixed at construction and
///         `swap()` takes no arguments — it can only ever sell `inputToken` for `outputToken`,
///         and only up to the balance it actually holds.
contract MySwappingVault is IFlapVaultExConsumer {
    IFlapVaultEx public immutable flapVaultEx;

    /// @notice Revenue token accumulated by the vault (the token we sell).
    address public immutable inputToken;
    /// @notice Token we buy on every swap.
    address public immutable outputToken;
    /// @notice Don't bother swapping below this balance (dust / gas).
    uint256 public immutable minSwapAmount;

    uint8 private constant FULFILLED = 2;
    uint8 private constant REFUNDED  = 3;

    mapping(uint256 => bool) public isOurRequest;

    event SwapRequested(uint256 indexed requestId, uint256 amountIn);
    event SwapSettled(uint256 indexed requestId, uint8 status, address token, uint256 amount);

    error BadConfig();
    error NotFlapVaultEx();
    error UnknownRequest();
    error NothingToSwap();

    // Params are fixed here — never accepted from a caller on the swap path. (In a beacon-proxy
    // vault this would be an `initialize(...)` called once behind the proxy.)
    constructor(address flapVaultEx_, address inputToken_, address outputToken_, uint256 minSwapAmount_) {
        if (
            flapVaultEx_ == address(0) || inputToken_ == address(0) || outputToken_ == address(0)
                || inputToken_ == outputToken_
        ) revert BadConfig();

        flapVaultEx = IFlapVaultEx(flapVaultEx_);
        inputToken = inputToken_;
        outputToken = outputToken_;
        minSwapAmount = minSwapAmount_;
    }

    /// @notice Revenue (tax payouts, direct transfers) lands here.
    receive() external payable {}

    /// @notice Swap the revenue currently held by the vault. No arguments: sells `inputToken` for
    ///         `outputToken`, and only for the balance it actually has.
    function swap() external returns (uint256 requestId) {
        address tokenIn = inputToken;
        uint256 balance = IERC20(tokenIn).balanceOf(address(this));
        if (balance < minSwapAmount) revert NothingToSwap();

        IERC20(tokenIn).approve(address(flapVaultEx), balance);
        requestId = flapVaultEx.requestSwap(
            IFlapVaultEx.ExactInputParams({inputToken: tokenIn, outputToken: outputToken, inputAmount: balance})
        );
        IERC20(tokenIn).approve(address(flapVaultEx), 0);

        isOurRequest[requestId] = true;
        emit SwapRequested(requestId, balance);
    }

    /// @notice Called by FlapVaultEx after settlement — success OR refund.
    function onFlapVaultExSettled(uint256 requestId, uint8 status, address token, uint256 amount) external override {
        // SECURITY: only FlapVaultEx, and only for a request this vault actually created.
        if (msg.sender != address(flapVaultEx)) revert NotFlapVaultEx();
        if (!isOurRequest[requestId]) revert UnknownRequest();
        delete isOurRequest[requestId];

        if (status == FULFILLED) {
            // `token` is `outputToken`; `amount` is the net output delivered.
            _onFulfilled(requestId, token, amount);
        } else if (status == REFUNDED) {
            // `token` is `inputToken`; `amount` is the refund net of the input-side fee.
            _onRefunded(requestId, token, amount);
        } else {
            revert("unexpected status");
        }

        emit SwapSettled(requestId, status, token, amount);
    }

    function _onFulfilled(uint256 requestId, address outputToken_, uint256 netOutput) internal {
        // ... record proceeds, update NAV, distribute, etc. ...
    }

    function _onRefunded(uint256 requestId, address inputToken_, uint256 refunded) internal {
        // ... handle the failure path (e.g. keep the refunded input for a later retry) ...
    }
}
```

**Notes:**

* Tokens and the swap threshold are **fixed at initialization**; `swap()` takes no arguments and uses the live `balanceOf(address(this))`, so a caller can never steer the vault into selling the wrong asset or amount.
* No-arg `swap()` is safe to leave callable by anyone (it only touches the vault's own revenue) — restrict it with an owner/operator check if your design needs to.
* Approve `FlapVaultEx` for exactly the amount you swap (it pulls exactly that; fee-on-transfer inputs are rejected).
* Branch on `status` — the same callback fires whether the swap settled or the input was refunded — and gate it on `msg.sender == flapVaultEx` plus your own request-id tracking.

***

## Native (BNB/ETH) input and output

FlapVaultEx only swaps **ERC-20** tokens — native is disabled. So if your vault wants to accept or pay out native BNB, **wrap it into WBNB yourself** (and unwrap on the way out):

* **Native input (sell native):** wrap the vault's native balance into WBNB with `deposit{value: amount}()`, then request a swap from **WBNB** to your target token.
* **Native output (buy native):** request a swap from your token to **WBNB**, then unwrap the WBNB you receive in the callback back into BNB with `withdraw(amount)`.

### Native input — sell native for an ERC-20

Vault receives native, wraps it, then requests `WBNB → outputToken`:

```solidity
interface IWETH {
    function deposit() external payable;
    function withdraw(uint256 amount) external;
    function balanceOf(address account) external view returns (uint256);
    function approve(address spender, uint256 amount) external returns (bool);
}

address constant WBNB = 0xbb4CdB9CBd36B01bD1cBaEBF2De08d9173bc095c;

// ...inside MySwappingVault, with `outputToken` set to something other than WBNB...
receive() external payable {} // native revenue lands here

function swap() external returns (uint256 requestId) {
    uint256 balance = address(this).balance;
    if (balance < minSwapAmount) revert NothingToSwap();

    IWETH(WBNB).deposit{value: balance}();                  // native -> WBNB
    uint256 wrapped = IWETH(WBNB).balanceOf(address(this));

    IWETH(WBNB).approve(address(flapVaultEx), wrapped);
    requestId = flapVaultEx.requestSwap(
        IFlapVaultEx.ExactInputParams({
            inputToken: WBNB,          // wrapped input
            outputToken: outputToken,
            inputAmount: wrapped
        })
    );
    IWETH(WBNB).approve(address(flapVaultEx), 0);

    isOurRequest[requestId] = true;
    emit SwapRequested(requestId, wrapped);
}
```

On the refund path the input comes back as `WBNB`; call `IWETH(WBNB).withdraw(amount)` if the vault wants native again.

### Native output — buy native with an ERC-20

Vault requests `inputToken → WBNB`, then unwraps the delivered amount in the callback:

```solidity
function swap() external returns (uint256 requestId) {
    uint256 balance = IERC20(inputToken).balanceOf(address(this));
    if (balance < minSwapAmount) revert NothingToSwap();

    IERC20(inputToken).approve(address(flapVaultEx), balance);
    requestId = flapVaultEx.requestSwap(
        IFlapVaultEx.ExactInputParams({
            inputToken: inputToken,
            outputToken: WBNB,        // wrapped output -> we unwrap it
            inputAmount: balance
        })
    );
    IERC20(inputToken).approve(address(flapVaultEx), 0);

    isOurRequest[requestId] = true;
    emit SwapRequested(requestId, balance);
}

function onFlapVaultExSettled(uint256 requestId, uint8 status, address token, uint256 amount) external override {
    if (msg.sender != address(flapVaultEx)) revert NotFlapVaultEx();
    if (!isOurRequest[requestId]) revert UnknownRequest();
    delete isOurRequest[requestId];

    if (status == FULFILLED) {
        require(token == WBNB, "unexpected token");
        IWETH(WBNB).withdraw(amount);   // WBNB -> native, stays in the vault
    } else if (status == REFUNDED) {
        // `token` is `inputToken` (an ERC-20) — nothing to unwrap.
    } else {
        revert("unexpected status");
    }

    emit SwapSettled(requestId, status, token, amount);
}
```

{% hint style="warning" %}
Always unwrap the **net `amount`** reported by the callback — the fee may be taken on the wrapped-native output — and keep `withdraw()` within the **200,000** callback gas budget. On the refund path `token` is the ERC-20 input, so do **not** unwrap it.
{% endhint %}

***

## Reference

### Full interface

```solidity
interface IFlapVaultEx {
    enum RequestStatus { NONE, PENDING, FULFILLED, REFUNDED, FAILED }

    struct ExactInputParams {
        address inputToken;   // ERC-20 being sold
        address outputToken;  // ERC-20 to buy
        uint256 inputAmount;
    }

    struct FulfillItem {
        uint256 requestId;
        address target;
        address allowanceTarget;
        uint256 minOutputAmount;
        bytes   data;
    }

    struct SwapRequestView {
        uint256 requestId;
        address consumer;
        address inputToken;
        address outputToken;
        uint256 inputAmount;
        uint256 swapInputAmount;  // net input actually swapped (quote on this)
        bool    feeInInput;
        uint64  feeBps;           // snapshotted at request time
        uint64  createdAt;        // informational; no expiry
        RequestStatus status;
    }

    // ── Events ──────────────────────────────────────────────────────────────
    event FlapVaultExRequested(uint256 indexed requestId, address indexed consumer,
        address inputToken, uint256 inputAmount, address outputToken, uint256 swapInputAmount,
        bool feeInInput, uint64 feeBps, uint64 createdAt);
    event FlapVaultExFulfilled(uint256 indexed requestId, address indexed consumer,
        address outputToken, uint256 netOutputAmount, uint256 feeAmount);
    event FlapVaultExRefunded(uint256 indexed requestId, address indexed consumer,
        address inputToken, uint256 refundAmount, uint256 feeAmount);
    event FlapVaultExUnspentInput(uint256 indexed requestId, address indexed consumer,
        address inputToken, uint256 swapInput, uint256 consumed, uint256 leftover);
    event FlapVaultExBatchItemFailed(uint256 indexed requestId, bytes reason);
    event FlapVaultExBatchItemRefunded(uint256 indexed requestId);
    event FlapVaultExBatchRefundFailed(uint256 indexed requestId, bytes reason);
    event FlapVaultExRequestFailed(uint256 indexed requestId, bytes reason);

    // ── Consumer ────────────────────────────────────────────────────────────
    function requestSwap(ExactInputParams calldata params) external payable returns (uint256 requestId);

    // ── Executor (FULFILLER_ROLE) ───────────────────────────────────────────
    function fulfillSwap(uint256 requestId, address target, address allowanceTarget,
        uint256 minOutputAmount, bytes calldata data) external returns (uint256 netOutputAmount);
    function batchFulfillSwap(FulfillItem[] calldata items) external returns (uint256 successCount);
    function refundSwap(uint256 requestId) external;

    // ── Views ───────────────────────────────────────────────────────────────
    function getRequest(uint256 requestId) external view returns (SwapRequestView memory);
    function getRequestsPaginated(uint256 offset, uint256 limit)
        external view returns (SwapRequestView[] memory requests, uint256 total);
    function getRequestsByRequesterPaginated(address requester, uint256 offset, uint256 limit)
        external view returns (SwapRequestView[] memory requests, uint256 total);
    function nextRequestId() external view returns (uint256);
    function feeConfig() external view returns (uint256 feeBps, address feeReceiver);
    function maxCallbackGas() external view returns (uint256);
    function isQuoteToken(address token) external view returns (bool);
    function isTokenBlocked(address token) external view returns (bool);
    function routerWhitelistEnabled() external view returns (bool);
}

interface IFlapVaultExConsumer {
    /// @param status  RequestStatus.FULFILLED (2) or RequestStatus.REFUNDED (3)
    /// @param token   ERC-20 delivered or returned
    /// @param amount  Amount delivered or returned (after fees on fulfillment)
    function onFlapVaultExSettled(uint256 requestId, uint8 status, address token, uint256 amount) external;
}
```


# Recent Release

This subsection documents features that have recently shipped and are available on-chain today. Unlike the rest of the Preview section (which covers upcoming, not-yet-released products), pages here describe functionality you can integrate against right now.


# Buy Quota (v5.14.16)

{% hint style="warning" %}
**Preview.** This document has not been reviewed yet. Content may be incomplete or inaccurate.
{% endhint %}

{% hint style="success" %}
**This page is for terminal / bot / wallet developers** who quote or execute trades against the bonding curve. Read [Trade Tokens](https://github.com/flap-sh/gitbook/tree/main/developers/preview/wallet-and-terminal-and-bot-developers/trade-tokens.md) first if you haven't yet — this page assumes you're already familiar with `quoteExactInput` / `swapExactInput`.
{% endhint %}

**Current Protocol Version:** `v5.14.16`

***

## What Is a Buy Quota?

A **buy quota** is an optional, per-token limit on how much of a token a single address can buy cumulatively from the **bonding curve** (not the DEX, after graduation).

Unlike a hard cap that reverts, exceeding your quota does **not** fail your transaction. Instead, the protocol automatically refunds the portion of your input that would push you over the limit — you simply receive less output than you asked for (down to zero if your quota is already exhausted), and the excess quote-asset input comes back to you in the same transaction.

{% hint style="info" %}
Not every token has a quota. If a token has no cap configured, this entire mechanism has zero effect on it — you can buy as much as the bonding curve otherwise allows.
{% endhint %}

## Why It Exists

The bonding curve is where a token's price is most volatile relative to supply — a single address accumulating a large share early can distort later pricing for everyone else. A buy quota lets a token limit how much cumulative exposure any single address can build up while the token is still in its bonding-curve phase, without needing to block or blacklist anyone outright.

## Key Design Points

* **Quota is keyed by `tx.origin`, not `msg.sender`.** This means the limit tracks the *ultimate transaction originator* — the externally-owned account (EOA) that signed and sent the transaction — regardless of how many intermediate contracts (routers, aggregators, smart-contract wallets acting on behalf of a single owner) the call passes through before reaching the bonding curve.
* **Quota only applies to bonding-curve buys.** Once a token has migrated to a DEX, trading there is unaffected — a buy quota is a bonding-curve-phase mechanism only.
* **Cumulative, not per-transaction.** Your quota usage accumulates across every buy you make on that token while a quota is active; it does not reset per transaction.
* **Selling never reduces your quota usage.** Once you've bought against your quota, selling the tokens back does not free up allowance — the cumulative-bought figure only ever increases while a quota is active for that token.
* **Never causes a revert on its own.** If you're already at your quota (or your input would exceed it), your buy simply executes for less than requested — with a full or partial refund — rather than failing outright. You should still set a normal slippage/minimum-output check on your own call if you require a guaranteed minimum output; the quota mechanism does not bypass that.

***

## Checking a Token's Quota Configuration

To find out whether a token has a quota configured at all, and what it is:

```solidity
/// @notice Get the per-tx.origin cumulative buy cap for a token on the internal market.
/// @param token The address of the token
/// @return bps The cap in bps of maxSupply; 0 means no limit
/// @return maxBuyAmount The absolute cap in token units; 0 means no limit
function maxBuyPerOrigin(address token) external view returns (uint16 bps, uint256 maxBuyAmount);
```

* `bps` is expressed in basis points of the token's max supply (e.g. `200` = 2%).
* `maxBuyAmount` is the same limit expressed directly in token units, for convenience.
* If both values come back `0`, the token has **no quota** — this entire feature is a no-op for that token.

## Checking Your Own Quota Usage

To find out how much of your own quota you've already used, and how much remains, for a specific token:

```solidity
/// @notice Get an origin's per-token buy quota usage on the internal market.
/// @param token The address of the token
/// @param origin The tx.origin address to query
/// @return bought The cumulative bought amount in token units
/// @return remaining The remaining buy allowance in token units
///         (0 if exhausted; the maximum uint256 value if the token has no quota at all)
function buyQuotaOf(address token, address origin) external view returns (uint256 bought, uint256 remaining);
```

{% hint style="warning" %}
**Important for quoting — you must set the `from` field.**

`buyQuotaOf` (and, indirectly, `quoteExactInput` — see below) depends on the address you're querying *as* — the `origin` parameter. When you're building a raw `eth_call` (or the equivalent simulation call in your library of choice, e.g. viem/ethers), you must explicitly set the caller address in the call's `from` field to the address you actually intend to trade from.

If you omit `from` (or leave it as a zero/arbitrary address), the call will still succeed, but it will report the quota state for the **wrong address** — you'll get back the quota status of whatever default caller your RPC client happens to use, not your own.

This matters even more when you're getting a **quote** for a prospective buy (via `quoteExactInput`), not just checking your quota directly: the quoted output amount for a bonding-curve buy is quota-aware — if your quota is partially or fully consumed, the quoted `outputAmount` will already reflect a smaller (or zero) fill. If you quote with the wrong `from`, you'll get a quote for the wrong address's remaining allowance, which will not match what you actually receive when you execute the real trade from your real address.

**Rule of thumb:** whenever you simulate a call that a specific address will eventually execute for real (quoting a buy, or checking `buyQuotaOf`), always set `from` to that exact address in your `eth_call` (or your library's equivalent parameter — e.g. `account` in viem, `from` in ethers/web3.js).
{% endhint %}

## What Happens When You Exceed Your Quota

If your requested buy amount would push your cumulative total past your remaining allowance:

* The protocol fills your buy **up to your remaining allowance only**.
* The unused portion of your input (the amount that would have gone toward the excess) is **automatically refunded** to you in the same transaction — no separate claim step needed.
* If your quota is already fully exhausted (`remaining == 0`), your buy is filled for **zero output**, and effectively your entire input is refunded.

This means a buy quota never blocks a transaction from succeeding — it only changes how much you actually receive. If your integration requires a guaranteed exact fill, check `buyQuotaOf` before submitting the trade, or set an appropriate minimum-output parameter on your swap call and treat a shortfall as an expected, valid outcome rather than an error.

## Watching Quota Changes (Events)

Two events let you track quota state without polling:

```solidity
/// @notice emitted when a token's per-tx.origin cumulative buy cap is set on the internal market.
/// @dev Absence of this event for a token means "no limit".
///      The cap only applies to internal (bonding-curve) buys, not DEX trades.
/// @param token The address of the token
/// @param bps The cap in bps of maxSupply (e.g. 200 = 2%). 0 means no limit.
/// @param maxBuyAmount The absolute per-origin cumulative buy cap in token units. 0 means no limit.
event FlapTokenMaxBuyPerOriginSet(address indexed token, uint16 bps, uint256 maxBuyAmount);
```

```solidity
/// @notice emitted after a bonding-curve buy updates an origin's per-tx.origin buy quota usage.
/// @dev Only emitted when a cap is active for the token; a token with no cap set never emits this event.
/// @param token The address of the token
/// @param origin The tx.origin address whose quota usage changed
/// @param bought The cumulative bought amount for this origin after the buy, in token units
/// @param remaining The remaining buy allowance for this origin, in token units (0 if exhausted)
event FlapBuyQuotaUpdated(address indexed token, address indexed origin, uint256 bought, uint256 remaining);
```

* Index `FlapTokenMaxBuyPerOriginSet` to build a live map of which tokens currently have a quota configured, and what it is.
* Index `FlapBuyQuotaUpdated` (filterable by `token` and `origin`, both indexed) to track a specific address's quota usage on a specific token over time, without repeatedly calling `buyQuotaOf`.

{% hint style="info" %}
Neither event fires for a token with no quota configured — absence of `FlapTokenMaxBuyPerOriginSet` for a given token is itself the signal that the token has no limit.
{% endhint %}

## Summary Checklist

* [ ] Before quoting or trading a token you don't fully trust, call `maxBuyPerOrigin(token)` to check whether a quota exists.
* [ ] If a quota exists, call `buyQuotaOf(token, yourAddress)` to see your remaining allowance before submitting a large buy.
* [ ] Always set the `from` field on any `eth_call` / simulated quote to the exact address that will execute the real transaction — quota state is resolved by `tx.origin`, so an incorrect `from` gives you someone else's quota state, not yours.
* [ ] Design your integration to treat a partial fill (with automatic refund) as a normal, expected outcome when a quota is active — not as a failure.
* [ ] Index `FlapTokenMaxBuyPerOriginSet` and `FlapBuyQuotaUpdated` if you want to track quota state off-chain instead of polling the view functions.


# Audit Reports

* The Flap Launchpad Protocol V2, V4 and Flap Tax Token V1 were audited by Certik. Check the audit report on[ Certik Skynet](https://skynet.certik.com/projects/flap).
* Flap Tax Token V1 (Part of the V4 Protocol) passed a comprehensive security audit by BlockSec.
* Flap Launchpad Protocol V5 (including Flap Tax Token V2 and Flap PreLaunch V1) passed a comprehensive security audit by BlockSec.

{% file src="/files/CWIFdhUbnZ19pU4znsUe" %}

{% file src="/files/P8NIjZbfzFTXxOTvSgGo" %}


# Media Kit

{% file src="/files/OjipfsRyYhJSzS2Dzqpt" %}


# Flap Skills for AI Agents

Flap publishes AI-agent skills that any compatible coding/agent assistant can use — Claude Code, Codex, OpenClaw, Hermes, or another agent that supports this skill format. You can invoke them directly from a chat with your agent.

***

## Launch a BNB Token on Flap

**Registry page:** [clawhub.ai/flapguy/launch-bnb-token-on-flap](https://clawhub.ai/flapguy/launch-bnb-token-on-flap) (v1.3.0)

This skill walks an AI assistant through every step required to launch a **Flap Tax Token V3** on Flap (BNB Chain): uploading metadata to IPFS, mining a vanity salt, encoding calldata, and broadcasting the transaction.

{% hint style="info" %}
As of v1.3.0, this skill only launches tax tokens (Flap Tax Token V3). Standard (non-tax) token launches are no longer supported. It also supports launching against any quote token Flap offers on BNB Chain — not just native BNB — resolved live from Flap's quote-tokens API.
{% endhint %}

### How to use

Give your agent a prompt like one of the examples below. The skill handles the rest.

***

### Example 1 — Tax token gifted to an X account (Gift Vault)

Launch a tax token whose trading fees are gifted to the X account `@elonmusk` using the **Gift Vault**. No vault factory address is needed — just refer to it by name.

```
Launch a tax token on Flap BNB called "Elon's Rocket" with symbol ROKYT.
Use the Gift Vault and set the gift owner to the X account @elonmusk.
Set buy tax and sell tax both to 5%.
Spend 0.00001 BNB on the initial buy.
```

***

### Example 2 — Tax token with a custom vault factory

Launch a tax token and route its fees through a specific vault factory you have already deployed.

```
Launch a tax token on Flap BNB called "Alpha Fund" with symbol ALFX.
Use the vault factory at address 0xAbCd1234AbCd1234AbCd1234AbCd1234AbCd1234 to set up the vault.
Set buy tax to 3% and sell tax to 3%.
Skip the initial buy.
```

***

### Example 3 — Tax token without a vault

Launch a straightforward tax token with no vault. Fees go directly to a beneficiary address.

```
Launch a tax token on Flap BNB called "Simple Tax" with symbol SMTX.
No vault. Set buy tax to 0% and sell tax to 3%.
Set the beneficiary to 0xYourBeneficiaryAddressHere.
Spend 0.05 BNB on the initial buy.
```

***

### Example 4 — Tax token quoted against a non-BNB token

Launch a tax token that quotes and launches against a different Flap-supported quote token (e.g. `USDT`) instead of native BNB. The skill fetches the current BNB-chain quote token list from Flap and resolves the token's address/decimals automatically.

```
Launch a tax token on Flap BNB called "Dollar Tax" with symbol DTAX.
Quote it against USDT instead of BNB.
No vault. Set buy tax to 2% and sell tax to 5%.
Spend 50 USDT on the initial buy.
```

***

### Using this skill with the Binance Agentic Wallet

Want your agent to sign in to a wallet and broadcast the launch transaction itself, end to end, from chat? See [Using Flap Skills with the Binance Agentic Wallet](/flap/skills/using-with-binance-agentic-wallet) for a full walkthrough — installing both skills, signing in, the required wallet security settings, and a worked example launching a token quoted against a BStock.


# Using Flap Skills with the Binance Agentic Wallet

This tutorial walks through launching a token on Flap **entirely from a chat with your AI agent** on BNB chAIn — no manual transaction signing, no copy-pasting calldata into a wallet UI. It combines two agent skills:

* [**binance-agentic-wallet**](https://github.com/binance/binance-skills-hub/blob/main/skills/binance-web3/binance-agentic-wallet/SKILL.md) — lets your agent sign in to a Binance Agentic Wallet and sign/broadcast transactions on your behalf, with your explicit confirmation at each step.
* [**launch-bnb-token-on-flap**](https://github.com/flap-sh/flap-skills/blob/main/launch-bnb-token-on-flap/SKILL.md) — the skill described earlier in this chapter, which builds the Flap token-launch transaction.

These are not OpenClaw-exclusive skills — they follow a common skill format usable by any compatible agent, including Claude Code, Codex, OpenClaw, and Hermes. The examples below use generic instructions you can paste into any of them.

{% hint style="info" %}
The Binance Agentic Wallet's simulator has an internal gas ceiling on `contract-call preview`. As of this writing it has been raised to **5,000,000 gas**, comfortably above what a tax-token launch needs (\~2.0–2.5M gas depending on quote token and vault configuration). If a preview ever fails with a generic `"Transaction simulation failed: execution reverted"` error, independently verify the calldata against a live RPC node (e.g. with `cast call`) before assuming the transaction itself is invalid — the error message doesn't distinguish a real revert from a simulator-side limit.
{% endhint %}

***

## Step 1 — Install the Binance Agentic Wallet skill

Tell your agent:

```
Install the binance-agentic-wallet skill from https://github.com/binance/binance-skills-hub/blob/main/skills/binance-web3/binance-agentic-wallet/SKILL.md
```

Your agent will fetch and register the skill (and its CLI dependency, `baw`) so it knows how to drive wallet sign-in, balance checks, and transaction signing on your behalf.

***

## Step 2 — Sign in to your Agentic Wallet

Ask your agent to sign you in:

```
Sign me in to my Binance Agentic Wallet.
```

The agent starts the sign-in flow and gives you a **pairing code** and a **link to open** (or a QR code to scan) in the Binance App. It will look something like this — your actual code and link will differ, and both are single-use / time-limited (\~5 minutes):

```
Sign-in initiated. Please confirm in the Binance App.

Pairing code: 6█████1
Open this link on your phone (or scan the QR code shown):
https://web3.binance.com/en/agent-login?expireAt=████████████&url=████████████████████████████████

Waiting for confirmation in the Binance App...
```

1. Open the link (or scan the QR code) with the device that has your **Binance App** installed.
2. Confirm that the pairing code shown in the app matches the one your agent gave you.
3. Approve the sign-in inside the app.

Your agent's sign-in call blocks until you approve — once it returns successfully, your agent can confirm the wallet is connected and show you its address.

{% hint style="warning" %}
Never share your pairing code or sign-in link with anyone else, and don't paste them anywhere other than the official Binance App. Treat them like a login credential — anyone who completes the pairing gets agent-level control of that wallet.
{% endhint %}

***

## Step 3 — Configure your wallet's security settings (in the Binance App)

Before your agent can build and broadcast Flap launch transactions, three settings need to be adjusted **in the Binance App** — the CLI can only read these, not change them. Open the **Agentic Wallet management page → settings icon (top right)** and set:

| Setting                           | Value to set                                     | Why                                                                                                                                                                                                                                   |
| --------------------------------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| **Developer Mode**                | Enabled                                          | Required for any `contract-call preview`/`execute` — this is what lets your agent construct and sign arbitrary contract calls like `Portal.newTokenV6`, instead of only pre-built actions (send, swap, etc).                          |
| **Abnormal Transaction Handling** | `Need Confirmation` (instead of `Auto Reject`)   | A brand-new, unverified token contract can look "abnormal" to Binance's risk engine. `Need Confirmation` routes flagged transactions to you for a manual approve/reject in the app instead of silently blocking them.                 |
| **Tradable Tokens**               | `All tokens` (instead of the default allow-list) | The token you're launching doesn't exist yet, so it can't be on any allow-list. Widening this setting lets the wallet interact with newly created tokens (relevant for any post-launch buy/sell testing, not the launch call itself). |

Ask your agent to confirm these took effect:

```
Check my wallet settings and confirm Developer Mode is enabled.
```

***

## Step 4 — Install the Flap token-launch skill

```
Install the launch-bnb-token-on-flap skill from https://github.com/flap-sh/flap-skills/blob/main/launch-bnb-token-on-flap/SKILL.md
```

This is the skill described earlier in this chapter — see [Launch a BNB Token on Flap](/flap/skills) for what it covers and the four example prompts. It only launches **Flap Tax Token V3** tokens (standard/non-tax tokens are out of scope as of v1.3.0), and it supports quoting against any token in Flap's live quote-token list, not just native BNB.

***

## Step 5 — Launch a token quoted against a BStock

Besides native BNB and stablecoins, Flap lets you quote a launch against any of the **BStocks** currently supported on BNB Chain. The current list, category `rwa`, fetched live from Flap's `/api/launch/quote-tokens` endpoint (`chainId == 56`):

| Symbol | Underlying        |
| ------ | ----------------- |
| SPCXB  | SpaceX            |
| SKHYB  | SK Hynix          |
| SPYB   | SPY               |
| QQQB   | Invesco QQQ Trust |
| NVDAB  | NVIDIA            |
| AAPLB  | Apple             |
| TSLAB  | Tesla             |
| MSFTB  | Microsoft         |
| GOOGLB | Alphabet          |
| HOODB  | Robinhood         |
| BABAB  | Alibaba           |
| GMEB   | GameStop          |

{% hint style="info" %}
This list changes over time as Binance issues new BStocks — always have your agent re-fetch `/api/launch/quote-tokens` rather than relying on a hardcoded list (including the table above).
{% endhint %}

This example launches a tax token quoted against one of these BStocks instead of native BNB, using your now-connected Agentic Wallet as the signer. The specific BStock ticker and the resulting quote-token address are redacted below (shown as `████`) since the choice is arbitrary — swap in any symbol from the table above.

```
Use my agentic wallet to launch a token named "chAIn" on Flap BNB, symbol chAIn.
Quote it against ████ instead of BNB.
No vault. Set buy tax and sell tax both to 1%.
Route 100% of tax revenue to dividends.
Skip the initial buy.
```

Your agent will:

1. Resolve the chosen BStock's address and decimals from Flap's `/api/launch/quote-tokens` endpoint.
2. Upload your token's metadata (name, symbol, image, description) to Flap's IPFS gateway.
3. Mine a vanity CREATE2 salt so the token address ends in `7777`.
4. Encode the full `Portal.newTokenV6` calldata — including setting `dividendToken` to the BStock's address, since Flap requires the dividend token to match the quote token whenever quoting in a non-native ERC-20 with dividends enabled.
5. Independently sanity-check the transaction (e.g. via a `cast call` simulation against a live RPC) before ever touching your wallet.

### The two-step confirmation flow

Every contract call through the Agentic Wallet is deliberately split into a **preview** and an **execute** step, so nothing is signed without you seeing exactly what it does first.

**Request (preview):** the agent runs `contract-call preview` and shows you the parsed result (the address, IDs, and quote token below are illustrative placeholders, not a real launch):

```
Preview succeeded.

Type:              Contract Interaction
Contract:          Portal (0xe2cE6ab80874Fa9Fa2aAE65D277Dd6B8e65C9De0)
Quote token:        ████ (0x████████████████████████████████████████)
Meta CID:           bafkreihdwdcefgh4dqkjv67uzcmw7ojee6xedzdetojuzjevtenxquvyku (empty)
Predicted address:  0x██████████████████████████████████7777  (vanity suffix 7777)
Tax:                1% buy / 1% sell
Dividend:           100% of tax revenue, paid in ████
Initial buy:        skipped (quoteAmt = 0)
Risks flagged:      none
requireConfirmation: false  → would broadcast directly if executed

Shall I go ahead and execute this?
```

At this point **nothing has been signed or broadcast** — this is purely a simulation, run against Binance's backend, that tells you what the transaction would do.

**Confirm (execute):** only after you explicitly say "yes" does the agent run `contract-call execute` with the `requestId` from the preview:

```
Broadcasted.

Order ID:  ████████████████████
Tx hash:   0x240b9e67ddc824588c3c4d1e1af7437bee28f812a5eafb41b64f1ce312c5b49e
Status:    BROADCASTED
```

The agent then polls the chain to confirm the transaction landed, and reports back the final token address and a link to view it:

```
Confirmed on-chain — status: success.

Token launched: chAIn (chAIn)
Address: 0x██████████████████████████████████7777
https://flap.sh/bnb/0x██████████████████████████████████7777
```

{% hint style="info" %}
If `contract-call preview` ever fails with a generic `SERVICE_ERROR` / `"execution reverted"` message and you believe the transaction should be valid, ask your agent to cross-check the same calldata against a live RPC node independently (e.g. `cast call`). This distinguishes a genuine on-chain revert (bad parameters) from a wallet-side simulation limitation — the error message alone can't tell you which one happened.
{% endhint %}


